diff --git a/lib/tcp_io.c b/lib/tcp_io.c index 884cabb6..017c0c76 100644 --- a/lib/tcp_io.c +++ b/lib/tcp_io.c @@ -414,9 +414,23 @@ static void error_cb(socket_t sock, void* arg) { (void)sock; struct tcp_conn* tc = (struct tcp_conn*)arg; if (!tc || tc->sock == SOCKET_INVALID) return; - DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: async error fd=%d connected=%d err=%d fin_r=%d fin_l=%d closed=%d wmon=%d", - (int)tc->sock, tc->connected, tc->error, tc->fin_remote, tc->fin_local, tc->closed, tc->write_monitor); - tcp_conn_handle_error(tc, -1); + int so_err = 0; socklen_t so_len = sizeof(so_err); + if (getsockopt(tc->sock, SOL_SOCKET, SO_ERROR, &so_err, &so_len) == 0 && so_err == 0) { + // Грациозное закрытие peer (FIN, EPOLLHUP) при приостановленном чтении: FIN пришёл не как + // EPOLLIN (чтение отключено backpressure-ом), а как HUP. Возобновляем чтение, чтобы + // read_cb дослил остаток и прочитал FIN (recv()==0 → fin_remote → отложенный on_fin). + DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: async HUP (graceful) fd=%d fin_r=%d fin_l=%d read_paused=%d rq=%d — resume read", + (int)tc->sock, tc->fin_remote, tc->fin_local, tc->read_paused, + tc->read_queue ? tc->read_queue->count : 0); + queue_waiter_cancel(tc->read_queue, &tc->read_waiter); + uasync_set_socket_read(tc->ua, tc->socket_id, 1); + tc->read_paused = 0; + read_cb(tc->sock, tc); + return; + } + DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: async error fd=%d connected=%d err=%d so_err=%d fin_r=%d fin_l=%d closed=%d wmon=%d", + (int)tc->sock, tc->connected, tc->error, so_err, tc->fin_remote, tc->fin_local, tc->closed, tc->write_monitor); + tcp_conn_handle_error(tc, so_err ? so_err : -1); } // ==================================================================== diff --git a/src/Makefile.am b/src/Makefile.am index 70562992..8d2b5d93 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -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) diff --git a/src/chat/chat_event.h b/src/chat/chat_event.h index d99d70a7..9b4732d3 100644 --- a/src/chat/chat_event.h +++ b/src/chat/chat_event.h @@ -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); diff --git a/src/chat/chat_headless_control.c b/src/chat/chat_headless_control.c index 85e0e7d4..bb0aa6e9 100644 --- a/src/chat/chat_headless_control.c +++ b/src/chat/chat_headless_control.c @@ -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); - - 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); + uint64_t channel_id = strtoull(ch, NULL, 10); + if (channel_id == 0) { send_response(cli, id, NULL, "invalid channel_id"); 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]; + 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; } - 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; - } + 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; } - if (inv.addrCount == 0) { send_response(cli, id, NULL, "no addresses available to build invite 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 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; } + 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); +} - char resp[1280]; snprintf(resp, sizeof(resp), "{\"link\":\"%s\",\"channel_id\":%llu,\"addrs\":%d}", link, - (unsigned long long)inv.channelId, (int)inv.addrCount); +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; diff --git a/src/chat/chat_headless_control.h b/src/chat/chat_headless_control.h index 4f6ea064..254ede94 100644 --- a/src/chat/chat_headless_control.h +++ b/src/chat/chat_headless_control.h @@ -6,8 +6,8 @@ * Response: {"id":N,"ok":true,"data":{...}} | {"id":N,"ok":false,"error":"..."} * Event: {"event":"",...} * - * 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 diff --git a/src/chat/chat_msg.c b/src/chat/chat_msg.c index f9bf2242..677c5ee3 100644 --- a/src/chat/chat_msg.c +++ b/src/chat/chat_msg.c @@ -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) { diff --git a/src/chat/invite_build.c b/src/chat/invite_build.c new file mode 100644 index 00000000..9e1432d9 --- /dev/null +++ b/src/chat/invite_build.c @@ -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 +#include +#include + +/* ── живость узла в канале (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); +} diff --git a/src/chat/invite_build.h b/src/chat/invite_build.h new file mode 100644 index 00000000..b69ba350 --- /dev/null +++ b/src/chat/invite_build.h @@ -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 +#include + +#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 */ diff --git a/tests/tcp_proxy_full/run_test.sh b/tests/tcp_proxy_full/run_test.sh index 4d38cf48..546b05c8 100755 --- a/tests/tcp_proxy_full/run_test.sh +++ b/tests/tcp_proxy_full/run_test.sh @@ -18,7 +18,7 @@ ECHO_PY="$SCRIPT_DIR/echo_server.py" STRESS_PY="$SCRIPT_DIR/stress_client.py" ECHO_PORT=19090 -HC_PORT=19091 +HC_PORT=19190 REFUSED_PORT=19099 N_IP=20 @@ -36,7 +36,11 @@ setup_net() { gw=$(ip route show default | awk '/via/ {print $3; exit}') [ -n "$gw" ] && ip route replace default via "$gw" table 100 2>/dev/null || true - # DNAT: unmarked packets to fake IPs → real localhost + # DNAT: unmarked packets to fake IPs → real localhost. + # half-close echo (dedicated port, вне stress-диапазона 19090-19109) — ДО общего правила, + # иначе порт перезапишется на ECHO_PORT. + iptables -t nat -C OUTPUT -d 10.200.100.1 -p tcp --dport "$HC_PORT" -m mark ! --mark 1 -j DNAT --to-destination 127.0.0.1:"$HC_PORT" 2>/dev/null \ + || iptables -t nat -A OUTPUT -d 10.200.100.1 -p tcp --dport "$HC_PORT" -m mark ! --mark 1 -j DNAT --to-destination 127.0.0.1:"$HC_PORT" for i in $(seq 1 $N_IP); do iptables -t nat -C OUTPUT -d "10.200.100.${i}" -p tcp -m mark ! --mark 1 -j DNAT --to-destination 127.0.0.1:"$ECHO_PORT" 2>/dev/null \ || iptables -t nat -A OUTPUT -d "10.200.100.${i}" -p tcp -m mark ! --mark 1 -j DNAT --to-destination 127.0.0.1:"$ECHO_PORT" @@ -63,6 +67,7 @@ setup_tun_route() { cleanup_net() { echo "=== Cleaning up networking ===" + iptables -t nat -D OUTPUT -d 10.200.100.1 -p tcp --dport "$HC_PORT" -m mark ! --mark 1 -j DNAT --to-destination 127.0.0.1:"$HC_PORT" 2>/dev/null || true for i in $(seq 1 $N_IP); do iptables -t nat -D OUTPUT -d "10.200.100.${i}" -p tcp -m mark ! --mark 1 -j DNAT --to-destination 127.0.0.1:"$ECHO_PORT" 2>/dev/null || true done diff --git a/tools/chatcli b/tools/chatcli index c24b4f1e..d67704c0 100755 --- a/tools/chatcli +++ b/tools/chatcli @@ -14,7 +14,8 @@ # members list members with full state # messages [count] read last messages # send send text message -# invite create invite link +# invite [node_id] create invite link (auto best node by default) +# invite_nodes list reachable nodes for invite # connect join channel via invite link # create create new channel # invite_to [proto] @@ -97,8 +98,14 @@ def cmd_messages(ch_id, count=None): def cmd_send(ch_id, text): _req("send", ch=ch_id, content_type="text", data=text) -def cmd_invite(ch_id): - _req("invite", ch=ch_id) +def cmd_invite(ch_id, node_id=None): + params = {"ch": ch_id} + if node_id: + params["node_id"] = node_id + _req("invite", **params) + +def cmd_invite_nodes(ch_id): + _req("invite_nodes", ch=ch_id) def cmd_connect(link): _req("connect", link=link) @@ -166,7 +173,8 @@ def main(): elif cmd == "members": cmd_members(*args[1:2] if len(args) > 1 else (_die("usage: members "),)) elif cmd in ("messages","msgs"): cmd_messages(*args[1:3] if len(args) > 1 else (_die("usage: messages [count]"),)) elif cmd == "send": cmd_send(*args[1:3] if len(args) > 2 else (_die("usage: send "),)) - elif cmd == "invite": cmd_invite(*args[1:2] if len(args) > 1 else (_die("usage: invite "),)) + elif cmd == "invite": cmd_invite(*args[1:3] if len(args) > 1 else (_die("usage: invite [node_id]"),)) + elif cmd == "invite_nodes": cmd_invite_nodes(*args[1:2] if len(args) > 1 else (_die("usage: invite_nodes "),)) elif cmd in ("connect","join"): cmd_connect(*args[1:2] if len(args) > 1 else (_die("usage: connect "),)) elif cmd == "create": cmd_create(*args[1:2] if len(args) > 1 else (_die("usage: create "),)) elif cmd == "invite_to": cmd_invite_to(*args[1:6] if len(args) > 4 else (_die("usage: invite_to [proto]"),)) diff --git a/tools/chatcli_commands.txt b/tools/chatcli_commands.txt index dbd86544..139a5220 100644 --- a/tools/chatcli_commands.txt +++ b/tools/chatcli_commands.txt @@ -49,10 +49,19 @@ send Отправить текстовое сообщение в канал. Пример: chatcli send 12345 "hello world" -invite +invite [node_id] Создать invite-ссылку для канала (формат utun://...). - Ссылка включает pubkey, адреса сервера и ID канала. + Без node_id — автоматический выбор лучшего достижимого узла. + С node_id — ссылка ведёт на указанный узел (hex, например 0x1234). + Ссылка включает pubkey, адреса выбранного узла и ID канала. Пример: chatcli invite 12345 + chatcli invite 12345 0x1a2b3c + +invite_nodes + Список достижимых узлов канала, пригодных для invite (включая свой узел). + Ответ содержит auto_node_id — узел, выбранный авто-логикой, + и список nodes (node_id, имя). + Пример: chatcli invite_nodes 12345 connect Подключиться к каналу по invite-ссылке. diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index 085ab3da..d591d309 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.c @@ -19,6 +19,7 @@ #include "../../../lib/mem.h" #include "../../../lib/sqlite3.h" #include "../../../src/chat/chat_core.h" +#include "../../../src/chat/invite_build.h" #include "../../../src/chat/chat_setting.h" #include "../../../src/utun_instance.h" #include "../../../src/transport_layer/etcp.h" @@ -229,202 +230,11 @@ void utun_bridge_join_channel(uint64_t channel_id, uint64_t node_id, chat_sync_connect_from_invite(channel_id, real_node_id, pubkey_bin, addrs_data, addr_count, addrs_data_len, NULL); } -/* Collect listening addresses from local sockets (for self-node fallback) */ -static int bridge_collect_invite_addrs(struct InviteAddrC* out, int max_cnt) { - int cnt = 0; - struct ETCP_SOCKET* s = g_cc.inst->etcp_sockets; - while (s) { - if (s->is_tcp) { s = s->next; continue; } - struct sockaddr_storage* sa = s->interface_addr.ss_family ? &s->interface_addr : &s->local_addr; - if (sa && sa->ss_family == AF_INET && cnt < max_cnt) { - 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 && sa->ss_family == AF_INET6 && cnt < max_cnt) { - 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++; - } - s = s->next; - } - { struct ETCP_SOCKET* s2 = g_cc.inst->etcp_sockets; - while (s2) { - if (!s2->is_tcp) { s2 = s2->next; continue; } - struct sockaddr_storage* sa = s2->interface_addr.ss_family ? &s2->interface_addr : &s2->local_addr; - if (sa && sa->ss_family == AF_INET && cnt < max_cnt) { - struct sockaddr_in* sin = (struct sockaddr_in*)sa; - out[cnt].family = 4; out[cnt].socketId = s2->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 && sa->ss_family == AF_INET6 && cnt < max_cnt) { - struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa; - out[cnt].family = 6; out[cnt].socketId = s2->sock_id; out[cnt].proto = INVITE_PROTO_TCP; - memcpy(out[cnt].address, &sin6->sin6_addr, 16); - out[cnt].port = ntohs(sin6->sin6_port); cnt++; - } - s2 = s2->next; } - } - return cnt; -} - -/* Collect addresses from node_addresses table for a remote node. - If dir_only=1 → only ADDR_TYPE_DIRECT, otherwise all addr_types */ -static int bridge_collect_invite_addrs_from_db(sqlite3* db, uint64_t node_id, - struct InviteAddrC* out, int max_cnt, int dir_only) { - int cnt = 0; - 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(db, sql, -1, &st, NULL) != SQLITE_OK) return 0; - sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); - 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; - if (addr && addr_len >= ip_len) memcpy(out[cnt].address, addr, (size_t)ip_len); - else { memset(out[cnt].address, 0, sizeof(out[cnt].address)); } - out[cnt].port = (uint16_t)sqlite3_column_int(st, 4); - cnt++; - } - sqlite3_finalize(st); - return cnt; -} - char* utun_bridge_encode_invite_link(uint64_t channel_id, const char* password) { - if (!chat_core_is_initialized() || !g_cc.inst || !g_cc.db) { - bridge_log(BLEV_ERROR, "encode_invite_link: not initialized inst=%p db=%p", - (void*)g_cc.inst, g_cc.inst ? (void*)g_cc.db : NULL); - return NULL; - } - uint64_t my_nid = g_cc.my_node_id; - sqlite3* db = g_cc.db; - struct TOPO_GROUPS* groups = g_cc.inst->topo_groups; - - /* ── Фаза 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)); - - uint64_t best_nid = 0; - int best_priority = 4; /* 1=regular, 2=self, 3=super, 4=invalid */ - int best_has_both_public = 0; - - sqlite3_stmt* pst = NULL; - char sql[512]; - snprintf(sql, sizeof(sql), - "SELECT node_id, node_type FROM \"%s\"", peers_tbl); - if (sqlite3_prepare_v2(db, sql, -1, &pst, NULL) == SQLITE_OK) { - while (sqlite3_step(pst) == SQLITE_ROW) { - uint64_t nid = (uint64_t)sqlite3_column_int64(pst, 0); - int ntype = sqlite3_column_int(pst, 1); - - /* alive = BGP visible */ - struct TOPO_GROUP* grp = groups ? topo_groups_find(groups, channel_id) : NULL; - if (!grp) continue; - struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(grp, nid); - if (!nq || !(nq->conn_presence & (NCONN_DIRECT | NCONN_INDIRECT | NCONN_BGP))) - continue; - - /* public v4+v6? */ - int has_v4 = 0, has_v6 = 0; - sqlite3_stmt* ast = NULL; - snprintf(sql, sizeof(sql), - "SELECT family FROM node_addresses WHERE node_id=? AND addr_type=%d", - ADDR_TYPE_DIRECT); - if (sqlite3_prepare_v2(db, sql, -1, &ast, NULL) == SQLITE_OK) { - sqlite3_bind_int64(ast, 1, (sqlite3_int64)nid); - while (sqlite3_step(ast) == SQLITE_ROW) { - if (sqlite3_column_int(ast, 0) == 4) has_v4 = 1; - if (sqlite3_column_int(ast, 0) == 6) has_v6 = 1; - } - sqlite3_finalize(ast); - } - int has_both = (has_v4 && has_v6) ? 1 : 0; - - /* priority */ - int priority; - if (nid == my_nid) priority = 2; /* self */ - else if (ntype == 4) priority = 3; /* supernode */ - else priority = 1; /* regular */ - - /* pick best (lower priority, or same → later) */ - if (priority <= best_priority) { - best_nid = nid; best_priority = priority; - best_has_both_public = has_both; - } - } - sqlite3_finalize(pst); - } - - /* ── Фаза 2: fallback ── */ - uint64_t target_nid = best_nid ? best_nid : my_nid; - int use_self = (target_nid == my_nid) ? 1 : 0; - - bridge_log(BLEV_INFO, "encode_invite_link: ch=%llu best=0x%016llx prio=%d self=%d both=%d tot_peer_rows=%d", - (unsigned long long)channel_id, (unsigned long long)target_nid, - best_priority, use_self, best_has_both_public, - best_nid ? 1 : 0); - - /* ── Фаза 3: pubkey ── */ - struct InviteDataC data; memset(&data, 0, sizeof(data)); - data.channelId = channel_id; data.nodeId = target_nid; - - if (use_self) { - memcpy(data.pubkey, g_cc.inst->my_keys.public_key, INVITE_PUBKEY_SIZE); - } else { - sqlite3_stmt* pkst = NULL; - snprintf(sql, sizeof(sql), "SELECT x25519_pubkey FROM nodes WHERE node_id=?"); - if (sqlite3_prepare_v2(db, sql, -1, &pkst, NULL) == SQLITE_OK) { - sqlite3_bind_int64(pkst, 1, (sqlite3_int64)target_nid); - if (sqlite3_step(pkst) == SQLITE_ROW) { - const void* pk = sqlite3_column_blob(pkst, 0); - int pklen = sqlite3_column_bytes(pkst, 0); - if (pk && pklen >= 32) memcpy(data.pubkey, pk, INVITE_PUBKEY_SIZE); - } - sqlite3_finalize(pkst); - } - if (data.pubkey[0] == 0) { - bridge_log(BLEV_ERROR, "encode_invite_link: no pubkey for selected node=0x%016llx", - (unsigned long long)target_nid); - return NULL; - } - } - - /* ── Фаза 4: адреса ── */ - if (use_self) { - data.addrCount = (uint8_t)bridge_collect_invite_addrs(data.addrs, INVITE_ADDR_MAX); - } else { - int dir_only = best_has_both_public ? 1 : 0; - data.addrCount = (uint8_t)bridge_collect_invite_addrs_from_db( - db, target_nid, data.addrs, INVITE_ADDR_MAX, dir_only); - } - if (data.addrCount == 0) { - bridge_log(BLEV_ERROR, "encode_invite_link: no addrs for node=0x%016llx", - (unsigned long long)target_nid); - return NULL; - } - - bridge_log(BLEV_INFO, "encode_invite_link: node=0x%016llx addrs=%d dir_only=%d", - (unsigned long long)target_nid, data.addrCount, - (!use_self && best_has_both_public) ? 1 : 0); - - /* ── Фаза 5: encode ── */ - char out[512]; - int r = invite_link_encode(&data, password, out, sizeof(out)); - if (r < 0) { - bridge_log(BLEV_ERROR, "encode_invite_link: encode failed r=%d", r); + char out[1024]; + if (chat_invite_build_link(channel_id, 0, password, out, sizeof(out)) < 0) { + bridge_log(BLEV_ERROR, "encode_invite_link: chat_invite_build_link failed ch=%llu", + (unsigned long long)channel_id); return NULL; } return u_strdup(out); diff --git a/tools/chatgui/src/invitedialog.cpp b/tools/chatgui/src/invitedialog.cpp index c7bcc685..59c2e381 100644 --- a/tools/chatgui/src/invitedialog.cpp +++ b/tools/chatgui/src/invitedialog.cpp @@ -1,15 +1,29 @@ #include "invitedialog.h" #include "qrcode_utils.h" +#include "../db/db_manager.h" +#include "../transport/gui_bridge.h" +#include "debug_ui.h" + #include #include #include #include #include +#include #include #include +#include +#include + +extern "C" { +#include "../../../src/chat/invite_build.h" +#include "../../../lib/mem.h" +} -ShareInviteDialog::ShareInviteDialog(const QString& link, QWidget* parent) - : QDialog(parent), m_link(link) +ShareInviteDialog::ShareInviteDialog(const QString& channelId, DbManager* db, QWidget* parent) + : QDialog(parent) + , m_channelId(channelId) + , m_db(db) { setWindowTitle(QString::fromUtf8("Пригласительная ссылка")); setMinimumWidth(420); @@ -18,33 +32,152 @@ ShareInviteDialog::ShareInviteDialog(const QString& link, QWidget* parent) auto *layout = new QVBoxLayout(this); layout->setSpacing(12); - auto *linkEdit = new QLineEdit(m_link, this); - linkEdit->setReadOnly(true); - linkEdit->setStyleSheet("padding: 6px; font-family: monospace;"); - layout->addWidget(linkEdit); + auto *nodeLabel = new QLabel(QString::fromUtf8("Узел входа:"), this); + nodeLabel->setStyleSheet("font-size: 12px; color: palette(window-text);"); + layout->addWidget(nodeLabel); + + m_nodeCombo = new QComboBox(this); + m_nodeCombo->setStyleSheet( + "QComboBox { border: 1px solid palette(mid); border-radius: 4px;" + " padding: 6px 10px; font-size: 13px; background: palette(base); }"); + layout->addWidget(m_nodeCombo); + + m_linkEdit = new QLineEdit(this); + m_linkEdit->setReadOnly(true); + m_linkEdit->setPlaceholderText(QString::fromUtf8("Генерация ссылки...")); + m_linkEdit->setStyleSheet("padding: 6px; font-family: monospace;"); + layout->addWidget(m_linkEdit); auto *btnLayout = new QHBoxLayout(); auto *copyBtn = new QPushButton(QString::fromUtf8("Копировать"), this); - connect(copyBtn, &QPushButton::clicked, this, [this, linkEdit]() { + connect(copyBtn, &QPushButton::clicked, this, [this]() { QClipboard* cb = QApplication::clipboard(); - cb->setText(m_link, QClipboard::Clipboard); + cb->setText(m_linkEdit->text(), QClipboard::Clipboard); if (cb->supportsSelection()) - cb->setText(m_link, QClipboard::Selection); - linkEdit->setFocus(); + cb->setText(m_linkEdit->text(), QClipboard::Selection); + m_linkEdit->setFocus(); }); btnLayout->addWidget(copyBtn); btnLayout->addStretch(); layout->addLayout(btnLayout); - auto *qrLabel = new QLabel(this); - QImage qrImage = generateQRCode(m_link); - if (!qrImage.isNull()) - qrLabel->setPixmap(QPixmap::fromImage(qrImage).scaled( - 180, 180, Qt::KeepAspectRatio, Qt::SmoothTransformation)); - qrLabel->setAlignment(Qt::AlignCenter); - layout->addWidget(qrLabel, 0, Qt::AlignCenter); + m_qrLabel = new QLabel(this); + m_qrLabel->setAlignment(Qt::AlignCenter); + layout->addWidget(m_qrLabel, 0, Qt::AlignCenter); auto *closeBtn = new QPushButton("OK", this); connect(closeBtn, &QPushButton::clicked, this, &QDialog::accept); layout->addWidget(closeBtn, 0, Qt::AlignRight); + + connect(m_nodeCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &ShareInviteDialog::onNodeSelected); + + /* колбэки результатов (по образцу JoinDialog) */ + gui_bridge_set_invite_link_ready_cb([](const char* chId, int chIdLen, const char* link) { + ShareInviteDialog* dlg = qobject_cast(QApplication::activeModalWidget()); + if (dlg) dlg->onLinkReady(chId, chIdLen, link); + }); + gui_bridge_set_invite_candidates_cb([](const char* chId, int chIdLen, const uint8_t* data, int len) { + ShareInviteDialog* dlg = qobject_cast(QApplication::activeModalWidget()); + if (dlg) dlg->onCandidates(chId, chIdLen, data, len); + }); + + /* запросить кандидатов; линк строится после их прихода */ + QByteArray chBytes = m_channelId.toUtf8(); + gui_bridge_post_uasync_fn(chat_invite_candidates_trampoline, u_strdup(chBytes.constData())); +} + +ShareInviteDialog::~ShareInviteDialog() { + gui_bridge_set_invite_link_ready_cb(nullptr); + gui_bridge_set_invite_candidates_cb(nullptr); +} + +void ShareInviteDialog::onNodeSelected(int index) { + if (index < 0) return; + qulonglong nodeId = m_nodeCombo->itemData(index).toULongLong(); + requestLink((uint64_t)nodeId); +} + +void ShareInviteDialog::requestLink(uint64_t targetNodeId) { + struct chat_invite_build_req* req = (struct chat_invite_build_req*)u_malloc(sizeof(*req)); + if (!req) { + GUI_ERROR("invite: requestLink — alloc failed"); + return; + } + memset(req, 0, sizeof(*req)); + QByteArray chBytes = m_channelId.toUtf8(); + strncpy(req->ch_id, chBytes.constData(), sizeof(req->ch_id) - 1); + req->target_node_id = targetNodeId; + m_linkEdit->setText(QString()); + m_linkEdit->setPlaceholderText(QString::fromUtf8("Генерация ссылки...")); + m_qrLabel->clear(); + gui_bridge_post_uasync_fn(chat_invite_build_link_trampoline, req); +} + +void ShareInviteDialog::setLink(const QString& link) { + m_linkEdit->setText(link); + QImage qrImage = generateQRCode(link); + if (!qrImage.isNull()) + m_qrLabel->setPixmap(QPixmap::fromImage(qrImage).scaled( + 180, 180, Qt::KeepAspectRatio, Qt::SmoothTransformation)); +} + +void ShareInviteDialog::onCandidates(const char* chId, int chIdLen, const uint8_t* data, int len) { + QString cid = QString::fromUtf8(chId, chIdLen); + if (cid != m_channelId) return; + if (!data || len < 10) return; + + uint64_t autoId = 0; + memcpy(&autoId, data, 8); + uint16_t count = 0; + memcpy(&count, data + 8, 2); + + if (autoId == 0) { + m_linkEdit->setText(QString()); + m_linkEdit->setPlaceholderText(QString::fromUtf8("Не удалось определить узел (см. лог)")); + return; + } + + auto nodeLabel = [this](uint64_t nid) { + QString name = m_db ? m_db->getDisplayName(nid) : QString(); + if (name.isEmpty()) + name = QString("0x%1").arg(nid, 16, 16, QChar('0')); + return name; + }; + + /* блокируем сигналы, чтобы не перезапрашивать линк при добавлении пунктов */ + QSignalBlocker blocker(m_nodeCombo); + m_nodeCombo->clear(); + + /* пункт 0 — узел, найденный авто-выбором */ + m_nodeCombo->addItem(nodeLabel(autoId) + QString::fromUtf8(" (auto)"), + QVariant::fromValue((qulonglong)autoId)); + + /* остальные узлы (пропуская авто), свой помечаем (self) */ + int off = 10; + for (int i = 0; i < (int)count && off + 8 <= len; i++) { + uint64_t nid = 0; + memcpy(&nid, data + off, 8); + off += 8; + if (nid == autoId) continue; + QString label = nodeLabel(nid); + if (m_db && nid == m_db->myNodeId()) + label += QString::fromUtf8(" (self)"); + m_nodeCombo->addItem(label, QVariant::fromValue((qulonglong)nid)); + } + + /* строим линк для авто-узла (сигнал заблокирован) */ + requestLink(autoId); +} + +void ShareInviteDialog::onLinkReady(const char* chId, int chIdLen, const char* link) { + QString cid = QString::fromUtf8(chId, chIdLen); + if (cid != m_channelId) return; + if (!link || link[0] == '\0') { + m_linkEdit->setText(QString()); + m_linkEdit->setPlaceholderText(QString::fromUtf8("Не удалось собрать ссылку (см. лог)")); + m_qrLabel->clear(); + return; + } + setLink(QString::fromUtf8(link)); } diff --git a/tools/chatgui/src/invitedialog.h b/tools/chatgui/src/invitedialog.h index 49fafe57..4dc74af2 100644 --- a/tools/chatgui/src/invitedialog.h +++ b/tools/chatgui/src/invitedialog.h @@ -2,12 +2,31 @@ #include #include +#include + +class DbManager; +class QComboBox; +class QLineEdit; +class QLabel; class ShareInviteDialog : public QDialog { Q_OBJECT public: - explicit ShareInviteDialog(const QString& link, QWidget* parent = nullptr); + explicit ShareInviteDialog(const QString& channelId, DbManager* db, QWidget* parent = nullptr); + ~ShareInviteDialog(); + +private slots: + void onNodeSelected(int index); private: - QString m_link; + void requestLink(uint64_t targetNodeId); + void setLink(const QString& link); + void onCandidates(const char* chId, int chIdLen, const uint8_t* data, int len); + void onLinkReady(const char* chId, int chIdLen, const char* link); + + QString m_channelId; + DbManager* m_db; + QComboBox* m_nodeCombo; + QLineEdit* m_linkEdit; + QLabel* m_qrLabel; }; diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index 35a42e86..2ea5e94f 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -430,24 +430,7 @@ void MainWindow::onTrayActivated(QSystemTrayIcon::ActivationReason reason) { void MainWindow::showShareInviteDialog(const QString& channelId) { if (!m_db || !m_db->isOpen()) return; - QByteArray pubkey; - if (m_node && m_node->isRunning()) - pubkey = QByteArray::fromHex(m_node->pubKeyHex().toLatin1()); - - if (pubkey.size() != 32) { - QMessageBox::warning(this, "Error", "Failed to get node public key."); - return; - } - - QList addrs = m_node->getInviteAddresses(m_db); - - QString link = encodeInviteLink(channelId.toULongLong(), pubkey, addrs); - if (link.isEmpty()) { - QMessageBox::warning(this, "Error", "Failed to create invite link."); - return; - } - - ShareInviteDialog dlg(link, this); + ShareInviteDialog dlg(channelId, m_db, this); dlg.exec(); } diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index f1bc979b..dd7b8236 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -488,40 +488,6 @@ void MessageList::refresh() { m_readDebounce->start(); } -void MessageList::addMessage(const QString& channelId, quint64 authorNodeId, - const QByteArray& content, qint64 timestamp) { - if (channelId != m_currentChannelId) return; - - AccountRow acc = m_db ? m_db->getAccount(authorNodeId) : AccountRow(); - QString author = m_db ? authorName(authorNodeId) : QString("node_%1").arg(authorNodeId); - QString letter = acc.avatarLetter.isEmpty() ? author.mid(0, 1) : acc.avatarLetter; - QColor color(acc.avatarColor); - auto avatar = makeAvatar(color, letter); - auto time = QDateTime::fromMSecsSinceEpoch(timestamp).toString("HH:mm"); - - /* dedup */ - for (int r = 0; r < m_model->rowCount(); r++) { - auto* it = m_model->item(r); - if (it->data(MsgTimeRole).toString() == time - && it->data(MsgAuthorRole).toString() == author) return; - } - - auto *item = new QStandardItem(); - setMsg(item, author, QString::fromUtf8(content), time, avatar, false, - false, QString(), QString(), QVariantList{}); - bool isSelf = m_db && (authorNodeId == m_db->myNodeId()); - item->setData(QVariant::fromValue(authorNodeId), MsgAuthorNodeIdRole); - item->setData(isSelf ? 1 : 0, MsgIsOutgoingRole); - setVoiceMessageRoles(item, content, QString(), - m_db ? m_db->dbPath() + "/media" : QString(), m_currentChannelId, QByteArray()); - m_model->appendRow(item); - - if (m_model->rowCount() > 50) - m_model->removeRow(0); - - m_view->scrollToBottom(); -} - void MessageList::saveChannelState() { if (m_currentChannelId.isEmpty() || m_model->rowCount() == 0) return; @@ -679,44 +645,36 @@ void MessageList::updateMessageProgress(const QString& chId, int64_t msgId, } void MessageList::onAttachmentDownloaded(const QString& chId, int64_t msgId) { - QString ct; - QString filePath; + if (chId != m_currentChannelId || !m_db || !m_db->isOpen()) return; + + MessageRow m = m_db->getMessageById(chId, msgId); + if (m.id == 0) return; + int row = -1; + bool wasPending = false; for (int r = 0; r < m_model->rowCount(); r++) { - QStandardItem* item = m_model->item(r); - if (!item) continue; - if (item->data(MsgIdRole).toLongLong() == msgId) { + QStandardItem* it = m_model->item(r); + if (it && it->data(MsgIdRole).toLongLong() == msgId) { row = r; - item->setData(2, MsgFileDownloadStateRole); - item->setData(0, MsgMediaBlocksDoneRole); - ct = item->data(MsgContentTypeRole).toString(); - filePath = item->data(MsgVoiceFileRole).toString(); + wasPending = it->data(MsgVoiceDownloadPendingRole).toBool(); break; } } - if (row >= 0 && chId == m_currentChannelId) { - bool wasPending = false; - for (int r = 0; r < m_model->rowCount(); r++) { - QStandardItem* it = m_model->item(r); - if (it && it->data(MsgIdRole).toLongLong() == msgId) { - wasPending = it->data(MsgVoiceDownloadPendingRole).toBool(); - if (wasPending) it->setData(false, MsgVoiceDownloadPendingRole); - break; - } - } - refresh(); /* reload to get updated local_attrs with file path */ - /* auto-play only if user clicked play to initiate download */ - if (ct == "audio/opus" && wasPending) { - QTimer::singleShot(500, this, [this, msgId]() { - for (int r = 0; r < m_model->rowCount(); r++) { - QStandardItem* it = m_model->item(r); - if (it && it->data(MsgIdRole).toLongLong() == msgId) { - QString fp = it->data(MsgVoiceFileRole).toString(); - if (!fp.isEmpty() && QFile::exists(fp)) - VoicePlayer::playOpusFile(fp); - break; - } - } + if (row < 0) return; + + QStandardItem* item = m_model->item(row); + setVoiceMessageRoles(item, m.data, m.contentType, + m_db->dbPath() + "/media", chId, m.localAttrs); + item->setData(0, MsgMediaBlocksDoneRole); + item->setData(false, MsgVoiceDownloadPendingRole); + + if (m.contentType == "audio/opus" && wasPending) { + QString fp = item->data(MsgVoiceFileRole).toString(); + if (!fp.isEmpty() && QFile::exists(fp)) { + QTimer::singleShot(500, this, [this, fp]() { + MessageDelegate::stopAllAutoPlay(); + float actualDur = VoicePlayer::playOpusFile(fp); + MessageDelegate::startAutoPlay(fp, actualDur); }); } } diff --git a/tools/chatgui/src/messagelist.h b/tools/chatgui/src/messagelist.h index 5e571c59..d709f763 100644 --- a/tools/chatgui/src/messagelist.h +++ b/tools/chatgui/src/messagelist.h @@ -24,8 +24,6 @@ public: void reloadLocalNicks(); void saveChannelState(); static void cleanupAnimations(); - void addMessage(const QString& channelId, quint64 authorNodeId, - const QByteArray& content, qint64 timestamp); void updateMessageProgress(const QString& chId, int64_t msgId, int blocksDone, int numBlocks); void onAttachmentDownloaded(const QString& chId, int64_t msgId); diff --git a/tools/chatgui/src/sound_manager.cpp b/tools/chatgui/src/sound_manager.cpp index b345735b..0668aa43 100644 --- a/tools/chatgui/src/sound_manager.cpp +++ b/tools/chatgui/src/sound_manager.cpp @@ -32,10 +32,19 @@ bool SoundManager::init() { return true; } +void SoundManager::clearCurrentPcm() { + if (m_currentPcmSound) { ma_sound_uninit(m_currentPcmSound); delete m_currentPcmSound; m_currentPcmSound = nullptr; } + if (m_currentPcmBuffer) { + ma_audio_buffer* buf = (ma_audio_buffer*)m_currentPcmBuffer; + ma_audio_buffer_uninit(buf); delete buf; m_currentPcmBuffer = nullptr; + } + m_currentPcmSampleRate = 0; +} + void SoundManager::shutdown() { if (!m_initialized) return; - stopCurrentPcm(); + clearCurrentPcm(); for (auto it = m_sounds.begin(); it != m_sounds.end(); ++it) { if (it->sound) { ma_sound_uninit(it->sound); delete it->sound; } @@ -45,11 +54,6 @@ void SoundManager::shutdown() { DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: before ma_engine_uninit engine=%p", (void*)m_engine); if (m_engine) { ma_engine_uninit(m_engine); delete m_engine; m_engine = nullptr; } - if (m_currentPcmSound) { delete m_currentPcmSound; m_currentPcmSound = nullptr; } - if (m_currentPcmBuffer) { - ma_audio_buffer* buf = (ma_audio_buffer*)m_currentPcmBuffer; - ma_audio_buffer_uninit(buf); delete buf; m_currentPcmBuffer = nullptr; - } DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: after ma_engine_uninit"); m_initialized = false; } @@ -152,7 +156,7 @@ void SoundManager::playRawPcm(const int16_t* pcm, int sampleCount, int sampleRat DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "SoundManager::playRawPcm: not initialized"); return; } - if (m_currentPcmSound) ma_sound_stop(m_currentPcmSound); + clearCurrentPcm(); m_currentPcmSampleRate = sampleRate; DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "SoundManager::playRawPcm: %d samples %dHz %dch", sampleCount, sampleRate, channels); @@ -189,8 +193,7 @@ void SoundManager::resumeCurrentPcm() { } void SoundManager::stopCurrentPcm() { - if (m_currentPcmSound) ma_sound_stop(m_currentPcmSound); - m_currentPcmSampleRate = 0; + clearCurrentPcm(); } unsigned long long SoundManager::currentPcmCursor() const { diff --git a/tools/chatgui/src/sound_manager.h b/tools/chatgui/src/sound_manager.h index dc4c48eb..c7928ee7 100644 --- a/tools/chatgui/src/sound_manager.h +++ b/tools/chatgui/src/sound_manager.h @@ -58,6 +58,8 @@ private: SoundManager() = default; ~SoundManager(); + void clearCurrentPcm(); + struct LoadedSound { QByteArray mp3Data; ma_decoder* decoder = nullptr; diff --git a/tools/chatgui/transport/gui_bridge.h b/tools/chatgui/transport/gui_bridge.h index 5d224b1b..9cef0ae7 100644 --- a/tools/chatgui/transport/gui_bridge.h +++ b/tools/chatgui/transport/gui_bridge.h @@ -36,6 +36,8 @@ struct TOPO_GROUP_NODE; #define GUI_EVT_MEMBER_REMOVED 25 /* data: [ch_id_len:1][ch_id:var][node_id:8] — мембер удалён из канала */ #define GUI_EVT_BGP_INFO 26 /* data: text — BGP hop summary for member detail */ #define GUI_EVT_ADMIN_KEY_RECEIVED 28 /* data: [ch_id_len:1][ch_id:var][from_node_id:8] — получен канальный ключ */ +#define GUI_EVT_INVITE_LINK_READY 29 /* data: [ch_id_len:1][ch_id:var][link_len:2][link:var] — пустой link = ошибка */ +#define GUI_EVT_INVITE_CANDIDATES 30 /* data: [ch_id_len:1][ch_id:var][auto_node_id:8][count:2][node_id:8]* — достижимые узлы для invite */ /* Размеры элементов пакета GUI_EVT_MEMBER_DETAIL (должны совпадать с chat_status.c) */ #define MEMBER_DETAIL_LINK_SIZE 63 @@ -147,6 +149,14 @@ void gui_bridge_set_bgp_info_cb(gui_bgp_info_fn cb); typedef void (*gui_admin_key_received_fn)(const uint8_t* data, int len); void gui_bridge_set_admin_key_received_cb(gui_admin_key_received_fn cb); +/* Callback: готова invite-ссылка (link = пустая строка при ошибке, детали в логе) */ +typedef void (*gui_invite_link_ready_fn)(const char* ch_id, int ch_id_len, const char* link); +void gui_bridge_set_invite_link_ready_cb(gui_invite_link_ready_fn cb); + +/* Callback: список достижимых узлов-кандидатов для invite — data: [auto_node_id:8][count:2][node_id:8]* */ +typedef void (*gui_invite_candidates_fn)(const char* ch_id, int ch_id_len, const uint8_t* data, int len); +void gui_bridge_set_invite_candidates_cb(gui_invite_candidates_fn cb); + /* uTun nodeinfo callback — registered via utun_add_nodeinfo_cbk, called from uasync thread */ void gui_nodeinfo_cb_impl(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq, void* arg); diff --git a/tools/chatgui/transport/gui_bridge_impl.cpp b/tools/chatgui/transport/gui_bridge_impl.cpp index 37e68892..f88654c0 100644 --- a/tools/chatgui/transport/gui_bridge_impl.cpp +++ b/tools/chatgui/transport/gui_bridge_impl.cpp @@ -46,6 +46,8 @@ static gui_member_updated_fn g_member_updated_cb = nullptr; static gui_member_removed_fn g_member_removed_cb = nullptr; static gui_bgp_info_fn g_bgp_info_cb = nullptr; static gui_admin_key_received_fn g_admin_key_received_cb = nullptr; +static gui_invite_link_ready_fn g_invite_link_ready_cb = nullptr; +static gui_invite_candidates_fn g_invite_candidates_cb = nullptr; static struct UASYNC* g_ua = nullptr; /* ── GuiBridgeReceiver implementation ── */ @@ -210,6 +212,29 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { } } break; + case GUI_EVT_INVITE_LINK_READY: + if (dlen >= 3) { + uint8_t chLen = d[0]; + uint16_t linkLen = 0; memcpy(&linkLen, d + 1 + chLen, 2); + if (dlen >= 1 + chLen + 2 + linkLen) { + if (linkLen == 0) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: INVITE_LINK_READY ch=%.*s — empty link (build failed)", + chLen, (const char*)d + 1); + if (g_invite_link_ready_cb) g_invite_link_ready_cb((const char*)d + 1, chLen, ""); + } else { + if (g_invite_link_ready_cb) g_invite_link_ready_cb((const char*)d + 1, chLen, (const char*)(d + 1 + chLen + 2)); + } + } + } + break; + case GUI_EVT_INVITE_CANDIDATES: + if (dlen >= 2) { + uint8_t chLen = d[0]; + if (dlen >= 1 + chLen + 10) { + if (g_invite_candidates_cb) g_invite_candidates_cb((const char*)d + 1, chLen, d + 1 + chLen, dlen - 1 - chLen); + } + } + break; default: DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: unknown event type %d", eventType); break; @@ -335,6 +360,14 @@ void gui_bridge_set_admin_key_received_cb(gui_admin_key_received_fn cb) { g_admin_key_received_cb = cb; } +void gui_bridge_set_invite_link_ready_cb(gui_invite_link_ready_fn cb) { + g_invite_link_ready_cb = cb; +} + +void gui_bridge_set_invite_candidates_cb(gui_invite_candidates_fn cb) { + g_invite_candidates_cb = cb; +} + } /* extern "C" */ /* ── uTun nodeinfo callback (called from uasync thread) ── */ diff --git a/tools/chatgui/transport/utun_node.cpp b/tools/chatgui/transport/utun_node.cpp index 75f91d2e..a77594f8 100644 --- a/tools/chatgui/transport/utun_node.cpp +++ b/tools/chatgui/transport/utun_node.cpp @@ -2,14 +2,9 @@ #include "utun_node.h" #include #include -#include -#include #include #include "../../lib/socket_compat.h" -#include "../db/db_manager.h" -#include "../src/invite_link.h" - // === C headers === extern "C" { #include "utun_instance.h" @@ -139,116 +134,6 @@ bool UtunNode::ntpEnabled() const { return m_instance ? m_instance->ntp.enabled : false; } -QList UtunNode::getInviteAddresses(DbManager* db) { - (void)db; - QList addrs; - QMap, uint8_t> addrProto; // key=(addr,port) → proto bits - - auto add_addr = [&](const QByteArray& addr, quint16 port, int family, - int sockId, uint8_t proto) { - QPair key(addr, port); - uint8_t existing = addrProto.value(key, 0); - addrProto[key] = existing | proto; - // Store sockId/family for serialization (overwrite is fine — same port) - }; - - auto append_from_addr_proto = [&](const QByteArray& addr, quint16 port, - int family, int sockId, uint8_t proto) { - NodeAddr a; - a.family = family; - a.address = addr; - a.port = port; - a.socketId = sockId; - a.protocol = proto; - addrs.append(a); - }; - if (m_instance) { - /* 1) UDP from active ETCP links */ - struct ll_entry* entry = m_instance->connections->head; - while (entry) { - struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data; - struct ETCP_CONN* conn = ce->conn; - if (conn && conn->initialized && conn->links_up > 0) { - struct ETCP_LINK* link = conn->links; - while (link) { - if (!link->is_tcp && link->link_state == 3 && link->nat_type == NAT_TYPE_DIRECT) { - struct sockaddr_storage* sa = &link->remote_addr; - if (sa->ss_family == AF_INET) { - struct sockaddr_in* sin = (struct sockaddr_in*)sa; - QByteArray addr((const char*)&sin->sin_addr, 4); - add_addr(addr, ntohs(sin->sin_port), 4, - link->conn ? (int)link->conn->sock_id : 0, INVITE_PROTO_UDP); - } else { - struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa; - QByteArray addr((const char*)&sin6->sin6_addr, 16); - add_addr(addr, ntohs(sin6->sin6_port), 6, - link->conn ? (int)link->conn->sock_id : 0, INVITE_PROTO_UDP); - } - } - link = link->next; - } - } - entry = entry->next; - } - - /* 2) UDP from local ETCP sockets (fallback) */ - { - struct ETCP_SOCKET* sock = m_instance->etcp_sockets; - while (sock) { - if (sock->is_tcp) { sock = sock->next; continue; } - struct sockaddr_storage* sa = NULL; - if (sock->nat_addr.ss_family != 0) - sa = &sock->nat_addr; - else if (sock->interface_addr.ss_family != 0) - sa = &sock->interface_addr; - if (sa) { - if (sa->ss_family == AF_INET) { - struct sockaddr_in* sin = (struct sockaddr_in*)sa; - QByteArray addr((const char*)&sin->sin_addr, 4); - add_addr(addr, ntohs(sin->sin_port), 4, (int)sock->sock_id, INVITE_PROTO_UDP); - } else { - struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa; - QByteArray addr((const char*)&sin6->sin6_addr, 16); - add_addr(addr, ntohs(sin6->sin6_port), 6, (int)sock->sock_id, INVITE_PROTO_UDP); - } - } - sock = sock->next; - } - } - - /* 3) TCP from local TCP sockets */ - { - struct ETCP_SOCKET* s = m_instance->etcp_sockets; - while (s) { - if (!s->is_tcp) { s = s->next; continue; } - struct sockaddr_storage* sa = &s->interface_addr; - if (sa->ss_family == AF_INET) { - struct sockaddr_in* sin = (struct sockaddr_in*)sa; - QByteArray addr((const char*)&sin->sin_addr, 4); - add_addr(addr, ntohs(sin->sin_port), 4, (int)s->sock_id, INVITE_PROTO_TCP); - } else if (sa->ss_family == AF_INET6) { - struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa; - QByteArray addr((const char*)&sin6->sin6_addr, 16); - add_addr(addr, ntohs(sin6->sin6_port), 6, (int)s->sock_id, INVITE_PROTO_TCP); - } - s = s->next; - } - } - - /* 4) Dedup by (addr,port); emit one entry per proto (UDP и TCP раздельно, без BOTH) */ - for (auto it = addrProto.begin(); it != addrProto.end(); ++it) { - QByteArray addr = it.key().first; - quint16 port = it.key().second; - uint8_t proto = it.value(); - int family = (addr.size() == 16) ? 6 : 4; - if (proto & INVITE_PROTO_UDP) append_from_addr_proto(addr, port, family, 0, INVITE_PROTO_UDP); - if (proto & INVITE_PROTO_TCP) append_from_addr_proto(addr, port, family, 0, INVITE_PROTO_TCP); - } - } - - return addrs; -} - void UtunNode::recvCallback(struct ETCP_CONN* conn, struct ll_entry* entry) { if (!g_currentNode || !entry || !entry->dgram || entry->len < ROUTER_SVC_HDR_SIZE) return; uint64_t src; diff --git a/tools/chatgui/transport/utun_node.h b/tools/chatgui/transport/utun_node.h index 8da6ef70..a9b0b44b 100644 --- a/tools/chatgui/transport/utun_node.h +++ b/tools/chatgui/transport/utun_node.h @@ -5,14 +5,11 @@ #include #include #include -#include #include #include #include struct UTUN_INSTANCE; -class DbManager; -struct NodeAddr; class UtunNode : public QObject { Q_OBJECT @@ -37,7 +34,6 @@ public: QString nodeIdHex() const; QString pubKeyHex() const; - QList getInviteAddresses(DbManager* db); int64_t ntpOffsetUs() const; bool ntpSynced() const; bool ntpEnabled() const;