You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1794 lines
89 KiB

#include "chat_sync.h"
#include "chat_core.h"
#include "chat_core_priv.h"
#include "chat_member.h"
#include "chat_setting.h"
#include "chat_event.h"
#include "chat_admin.h"
#include "chat_join.h"
#include "../routing_layer/topo_node_sqlite.h"
#include "../routing_layer/topo_group.h"
#include "../routing_layer/topo_group_invite.h"
#include "../routing_layer/topo_group_connect.h"
#include "../../lib/json_flat.h"
#include "member_sync.h"
#include "merkle_sync.h"
#include "../utun_instance.h"
#include "../transport_layer/etcp_api.h"
#include "../transport_layer/etcp.h"
#include "../transport_layer/secure_channel.h"
#include "../ntp_time.h"
#include "../../lib/u_async.h"
#include "../../lib/ll_queue.h"
#include "../../lib/debug_config.h"
#include "../../lib/mem.h"
#include "../../lib/platform_compat.h"
#ifdef UTUN_HAVE_STANDBY
#include "standby.h"
#endif
#include <string.h>
#include <sqlite3.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#ifdef __ANDROID__
#include <android/log.h>
#endif
static inline struct chat_sync* cs_of(struct UTUN_INSTANCE* inst) {
return inst ? inst->chat_sync : NULL;
}
struct channel_cache {
char channel_id[64];
uint64_t* peer_ids;
int peer_count;
uint32_t msg_count;
uint8_t last_chain_hash[32];
uint8_t synced;
};
struct join_pending {
uint64_t node_id; /* joiner node_id */
char ch_id[64];
struct ETCP_CONN* conn; /* прямое соединение к джойнеру */
struct join_pending* next;
};
struct chat_sync {
struct UTUN_INSTANCE* inst;
struct channel_cache* channels;
int channel_count;
void* refresh_timer;
void* refresh_wait; /* standby_wait handle (Android) */
void* info_req_timer;
void* join_timer;
void* sync_timer;
void* invite_send_timer; /* timer waiting for joiner to respond to CHANNEL_INVITE */
uint8_t initialized;
uint8_t sync_scheduled;
uint64_t pending_invite_ch_id;
uint64_t pending_invite_node_id;
uint8_t pending_invite_is_inviter; /* 1 = inviter sends CHANNEL_INVITE on conn_up, 0 = joiner sends JOIN_INFO_REQ */
char pending_password[128]; /* пароль для JOIN_INFO_REQ */
uint64_t pending_join_key; /* join-ключ из invite-ссылки (джойнер) */
struct join_pending* join_pending; /* connection-узел: ожидающие ready джойнеры */
/* Pending incoming invites (для ask/deny) */
uint64_t pending_inv_in_ch_id[4];
uint64_t pending_inv_in_node_id[4];
uint64_t pending_inv_in_key[4];
int pending_inv_in_count;
};
#define CS_ID "chat_sync"
#define CS_INFO_REQ_TIMEOUT_MS 5000
#define CS_JOIN_TIMEOUT_MS 8000
#define CS_INVITE_SEND_TIMEOUT_MS 3000 /* inviter waits for joiner to respond */
#define CS_SYNC_INTERVAL_MS 1000
static const char* cs_msg_name(uint8_t type) {
switch (type) {
case CS_MSG_INIT_SYNC: return "INIT_SYNC";
case CS_MSG_INIT_RESP: return "INIT_RESP";
case CS_MSG_SEND_DATA: return "SEND_DATA";
case CS_MSG_PUSH: return "PUSH";
case CS_MSG_ACK_PUSH: return "ACK_PUSH";
case CS_MSG_SYNC_DONE: return "SYNC_DONE";
case CS_MSG_ERROR: return "ERROR";
case CS_MSG_CHANNEL_INVITE: return "CHANNEL_INVITE";
case CS_MSG_JOIN_INFO_REQ: return "JOIN_INFO_REQ";
case CS_MSG_JOIN_INFO_RESP: return "JOIN_INFO_RESP";
case CS_MSG_JOIN_REQUEST: return "JOIN_REQUEST";
case CS_MSG_JOIN_READY: return "JOIN_READY";
case CS_MSG_SOUND_SETTING: return "SOUND_SETTING";
default: return "???";
}
}
static void cs_cancel_proto_timers(struct chat_sync* cs) {
if (cs->info_req_timer) { uasync_cancel_timeout(cs->inst->ua, cs->info_req_timer); cs->info_req_timer = NULL; }
if (cs->join_timer) { uasync_cancel_timeout(cs->inst->ua, cs->join_timer); cs->join_timer = NULL; }
if (cs->invite_send_timer) { uasync_cancel_timeout(cs->inst->ua, cs->invite_send_timer); cs->invite_send_timer = NULL; }
}
/* ── Forward decl ── */
static struct ETCP_CONN* cs_find_conn_for_node(struct UTUN_INSTANCE* inst, uint64_t node_id);
/* ── Send ── */
/* group_id в wire: [group_id:8] вместо [ch_len:1][ch_id] */
static void cs_write_group_id(uint8_t* p, const char* ch_id) {
uint64_t gb = htobe64(strtoull(ch_id, NULL, 10));
memcpy(p, &gb, 8);
}
/* group_id → ch_id через группу канала (требует существующую CHAT-группу). 0=ок, -1=нет группы */
static int cs_group_id_to_ch_id(struct UTUN_INSTANCE* inst, uint64_t group_id, char* ch_id, size_t sz) {
if (!inst || !inst->topo_groups) return -1;
struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, group_id);
if (!g || !g->channel_id[0]) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: recv — no CHAT group for group=%016llx, drop", CS_ID, (unsigned long long)group_id);
return -1;
}
snprintf(ch_id, sz, "%s", g->channel_id);
return 0;
}
static int cs_send(struct chat_sync* cs, const char* ch_id, uint64_t dst,
const uint8_t* payload, size_t len) {
if (len < 1) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cs_send len<1", CS_ID); return -1; }
size_t total = 1 + 8 + len;
uint8_t* buf = u_malloc(total);
if (!buf) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cs_send alloc(%zu) failed", CS_ID, total); return -1; }
uint8_t* p = buf;
*p++ = ETCP_RT_ID_CHAT_SYNC;
cs_write_group_id(p, ch_id); p += 8;
memcpy(p, payload, len);
struct ll_entry* entry = queue_entry_new(0);
if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cs_send queue_entry_new failed", CS_ID); u_free(buf); return -1; }
entry->dgram = buf; entry->len = total;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: SEND %s to=%016llx ch=%s len=%zu",
CS_ID, cs_msg_name(payload[0]), (unsigned long long)dst, ch_id, len);
struct ETCP_CONN* conn = cs_find_conn_for_node(cs->inst, dst);
if (!conn) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: no conn for node %016llx", CS_ID, (unsigned long long)dst); u_free(buf); queue_entry_free(entry); return -1; }
int rc = etcp_send(conn, entry);
if (rc != 0) { queue_dgram_free(entry); queue_entry_free(entry); }
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: etcp_send rc=%d id=0x%02x to=%016llx conn=%s", CS_ID, rc, payload[0], (unsigned long long)dst, conn->log_name);
return rc;
}
/* ── Channel cache ── */
static struct channel_cache* cs_find(struct chat_sync* cs, const char* ch_id) {
for (int i = 0; i < cs->channel_count; i++)
if (strcmp(cs->channels[i].channel_id, ch_id) == 0) return &cs->channels[i];
return NULL;
}
/* ── Forward declarations for new message handlers ── */
static void cs_propagate(struct chat_sync* cs, const char* ch_id, uint64_t exclude_id,
const uint8_t* payload, size_t len);
static void cs_handle_join_info_req(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len);
static void cs_handle_join_info_resp(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len);
static void cs_handle_join_request(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len);
static void cs_handle_join_ready(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len);
static void cs_handle_channel_invite(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len);
static int cs_send_channel_invite(struct chat_sync* cs, struct UTUN_INSTANCE* inst,
const char* ch_id, uint64_t target_node_id,
const char* ch_name, uint64_t owner,
const uint8_t* ch_x25519, const uint8_t* ch_ed,
const uint8_t* ch_sig);
static void cs_on_member_applied(const char* ch_id, uint64_t node_id,
const uint8_t* x25519, uint64_t signed_by,
const uint8_t* signature, void* arg);
static void cs_pending_free_all(struct chat_sync* cs);
/* ── Recv dispatcher ── */
static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (!entry || entry->len < 10) {
if (entry) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: recv short entry len=%zu conn=%s", CS_ID, entry->len, conn ? conn->log_name : "?");
if (entry->dgram) u_free(entry->dgram); queue_entry_free(entry);
}
return;
}
struct chat_sync* cs = cs_of(conn->instance);
if (!cs || !cs->initialized) { u_free(entry->dgram); queue_entry_free(entry); return; }
uint64_t peer = conn ? conn->peer_node_id : 0;
const uint8_t* d = entry->dgram;
size_t dlen = entry->len;
uint64_t group_id; memcpy(&group_id, d + 1, sizeof(group_id));
group_id = be64toh(group_id);
uint8_t type = d[9];
char ch_id[64];
if (cs_group_id_to_ch_id(cs->inst, group_id, ch_id, sizeof(ch_id)) != 0) {
/* CHANNEL_INVITE/JOIN_INFO_REQ приходят для ещё не созданного у нас канала:
group_id == channel_id (numeric), поэтому выводим ch_id напрямую. */
if (type == CS_MSG_CHANNEL_INVITE || type == CS_MSG_JOIN_INFO_REQ || type == CS_MSG_JOIN_INFO_RESP ||
type == CS_MSG_JOIN_REQUEST || type == CS_MSG_JOIN_READY || type == CS_MSG_ERROR) {
snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)group_id);
} else {
u_free(entry->dgram); queue_entry_free(entry); return;
}
}
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: recv %s(%02x) from=%016llx ch=%s len=%zu",
CS_ID, cs_msg_name(type), type, (unsigned long long)peer, ch_id, dlen);
const uint8_t* pl = d + 10;
size_t plen = dlen - 10;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: RECV %s from=%016llx ch=%s len=%zu",
CS_ID, cs_msg_name(type), (unsigned long long)peer, ch_id, plen);
switch (type) {
case CS_MSG_JOIN_INFO_REQ: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_INFO_REQ", CS_ID); cs_handle_join_info_req(cs, peer, ch_id, pl, plen); break;
case CS_MSG_JOIN_INFO_RESP: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_INFO_RESP", CS_ID); cs_handle_join_info_resp(cs, peer, ch_id, pl, plen); break;
case CS_MSG_JOIN_REQUEST: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_REQUEST", CS_ID); cs_handle_join_request(cs, peer, ch_id, pl, plen); break;
case CS_MSG_JOIN_READY: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_READY", CS_ID); cs_handle_join_ready(cs, peer, ch_id, pl, plen); break;
case CS_MSG_ERROR: {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: RECV ERROR from=%016llx ch=%s", CS_ID, (unsigned long long)peer, ch_id);
if (cs->info_req_timer) { uasync_cancel_timeout(cs->inst->ua, cs->info_req_timer); cs->info_req_timer = NULL; }
uint8_t err[20]; memcpy(err, &peer, 8); int r = -1; memcpy(err + 8, &r, 4);
memcpy(err + 12, &cs->pending_invite_ch_id, 8); chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, err, 20);
cs->pending_invite_node_id = 0; cs->pending_invite_ch_id = 0;
break;
}
case CS_MSG_CHANNEL_INVITE: cs_handle_channel_invite(cs, peer, ch_id, pl, plen); break;
case CS_MSG_SOUND_SETTING:
if (plen >= 1)
db_sync_set_peer_sound(cs->inst, group_id, peer, pl[0] ? 1 : 0);
break;
default: DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: UNKNOWN msg type=%02x from=%016llx", CS_ID, type, (unsigned long long)peer); break;
}
u_free(entry->dgram); queue_entry_free(entry);
}
/* ── Connection callbacks ── */
static void cs_info_req_timeout_cb(void* arg) {
struct chat_sync* cs = (struct chat_sync*)arg;
if (!cs || !cs->initialized) return;
cs->info_req_timer = NULL;
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INFO_REQ timeout peer=%016llx ch=%llu",
CS_ID, (unsigned long long)cs->pending_invite_node_id,
(unsigned long long)cs->pending_invite_ch_id);
uint8_t err[20]; memcpy(err, &cs->pending_invite_node_id, 8);
int r = 2; memcpy(err + 8, &r, 4); /* timeout */
memcpy(err + 12, &cs->pending_invite_ch_id, 8);
chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, err, 20);
cs->pending_invite_node_id = 0;
cs->pending_invite_ch_id = 0;
}
static void cs_join_timeout_cb(void* arg) {
struct chat_sync* cs = (struct chat_sync*)arg;
if (!cs || !cs->initialized) return;
cs->join_timer = NULL;
uint64_t peer = cs->pending_invite_node_id;
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_JOIN timeout peer=%016llx ch=%llu",
CS_ID, (unsigned long long)peer, (unsigned long long)cs->pending_invite_ch_id);
uint8_t err[20]; memcpy(err, &peer, 8);
int r = 2; memcpy(err + 8, &r, 4); /* timeout */
memcpy(err + 12, &cs->pending_invite_ch_id, 8);
chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, err, 20);
cs->pending_invite_node_id = 0;
cs->pending_invite_ch_id = 0;
cs->pending_invite_is_inviter = 0;
}
static void cs_invite_send_timeout_cb(void* arg) {
struct chat_sync* cs = (struct chat_sync*)arg;
if (!cs || !cs->initialized) return;
cs->invite_send_timer = NULL;
uint64_t peer = cs->pending_invite_node_id;
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE timeout — joiner did not respond peer=%016llx ch=%llu",
CS_ID, (unsigned long long)peer, (unsigned long long)cs->pending_invite_ch_id);
uint8_t err[20]; memcpy(err, &peer, 8);
int r = 2; memcpy(err + 8, &r, 4); /* timeout */
memcpy(err + 12, &cs->pending_invite_ch_id, 8);
chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, err, 20);
cs->pending_invite_node_id = 0;
cs->pending_invite_ch_id = 0;
cs->pending_invite_is_inviter = 0;
}
/* ── helper: find active ETCP_CONN for node ── */
static struct ETCP_CONN* cs_find_conn_for_node(struct UTUN_INSTANCE* inst, uint64_t node_id) {
struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
if (conn && conn->initialized && conn->links_up) return conn;
if (!inst->connections) { DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: find_conn connections=NULL", CS_ID); return NULL; }
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: find_conn node=%016llx NOT FOUND (head=%p count=%d)",
CS_ID, (unsigned long long)node_id, (void*)inst->connections->head, queue_entry_count(inst->connections));
return NULL;
}
/* ── helper: find active ETCP_CONN by pubkey (for invite: same physical node, different node_id) ── */
static struct ETCP_CONN* cs_find_conn_by_pubkey(struct UTUN_INSTANCE* inst,
const uint8_t* pubkey_bin,
uint64_t* out_node_id) {
struct ll_entry* entry = inst->connections ? inst->connections->head : NULL;
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
&& memcmp(conn->crypto_ctx.peer_public_key, pubkey_bin, 32) == 0) {
*out_node_id = conn->peer_node_id;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: find_conn_by_pubkey MATCH conn=%s peer=0x%016llx",
CS_ID, conn->log_name, (unsigned long long)conn->peer_node_id);
return conn;
}
entry = entry->next;
}
return NULL;
}
/* ── helper: check if peer has active ETCP link ── */
static int cs_is_peer_online(struct UTUN_INSTANCE* inst, uint64_t peer_id) {
if (!inst->connections) { DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: is_online peer=0x%016llx CONNS=NULL", CS_ID, (unsigned long long)peer_id); return 0; }
struct ETCP_CONN* conn = instance_find_conn(inst, peer_id);
if (conn) return conn->initialized && conn->links_up;
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: is_online peer=0x%016llx NOT FOUND", CS_ID, (unsigned long long)peer_id);
return 0;
}
/* ── post per-channel online peers count to GUI ── */
static void cs_post_channel_online(struct chat_sync* cs, const char* ch_id) {
int online = 0;
struct channel_cache* ch = NULL;
for (int i = 0; i < cs->channel_count; i++)
if (strcmp(cs->channels[i].channel_id, ch_id) == 0) { ch = &cs->channels[i]; break; }
if (ch) {
for (int j = 0; j < ch->peer_count; j++) {
int p_on = cs_is_peer_online(cs->inst, ch->peer_ids[j]);
if (p_on) online++;
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: PEERS_ONLINE peer[%d]=0x%016llx online=%d",
CS_ID, j, (unsigned long long)ch->peer_ids[j], p_on);
}
}
size_t cl = strlen(ch_id);
if (cl > 63) cl = 63;
uint8_t evt[66]; evt[0] = (uint8_t)cl;
memcpy(evt + 1, ch_id, cl);
uint16_t oc = (uint16_t)online; memcpy(evt + 1 + cl, &oc, 2);
chat_event_post(cs->inst, CHAT_EVT_CHANNEL_PEERS_ONLINE, evt, 1 + (int)cl + 2);
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: PEERS_ONLINE ch=%.*s total_online=%d", CS_ID, (int)cl, ch_id, online);
}
/* ── Throttled sync (не чаще CS_SYNC_INTERVAL_MS) ── */
static void _on_member_sync_done(uint64_t peer, const char* ns, int result, void* arg);
static void cs_flush_sync(struct chat_sync* cs) {
uint64_t myid = cs->inst->node_id;
for (int i = 0; i < cs->channel_count; i++) {
struct channel_cache* ch = &cs->channels[i];
for (int j = 0; j < ch->peer_count; j++) {
uint64_t pid = ch->peer_ids[j];
if (pid == myid || !cs_is_peer_online(cs->inst, pid)) continue;
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: flush_sync start for peer=%016llx ch=%s",
CS_ID, (unsigned long long)pid, ch->channel_id);
member_sync_start(cs->inst, pid, ch->channel_id, _on_member_sync_done, cs);
}
}
}
static void cs_sync_timer_cb(void* arg) {
struct chat_sync* cs = (struct chat_sync*)arg;
if (!cs || !cs->initialized) return;
cs->sync_timer = NULL;
cs->sync_scheduled = 0;
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: sync timer fire", CS_ID);
cs_flush_sync(cs);
}
static void cs_schedule_sync(struct chat_sync* cs) {
if (cs->sync_scheduled) return;
cs->sync_scheduled = 1;
cs->sync_timer = uasync_set_timeout(cs->inst->ua,
CS_SYNC_INTERVAL_MS * 10, cs, cs_sync_timer_cb, "cs_sync");
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: sync scheduled in %ums", CS_ID, CS_SYNC_INTERVAL_MS);
}
/* ── Peer sleep (keepalive-протокол): спячка → отменить сессии; пробуждение → пересинхронизация ── */
static void cs_on_peer_sleep(struct UTUN_INSTANCE* inst, uint64_t peer_node_id, int sleeping, void* arg) {
struct chat_sync* cs = (struct chat_sync*)arg;
if (!cs || !cs->initialized) return;
if (sleeping) {
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: peer sleep peer=0x%016llx — cancel member_sync sessions", CS_ID, (unsigned long long)peer_node_id);
merkle_sync_cancel_peer(inst, peer_node_id);
} else {
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: peer awake peer=0x%016llx — schedule sync", CS_ID, (unsigned long long)peer_node_id);
cs_schedule_sync(cs);
}
}
/* ── Per-chat «play sound on message» (троттлинг db_sync на сервере) ── */
static void cs_send_sound_setting(struct chat_sync* cs, const char* ch_id, uint64_t dst, int on) {
uint8_t p[2] = { CS_MSG_SOUND_SETTING, (uint8_t)(on ? 1 : 0) };
cs_send(cs, ch_id, dst, p, sizeof(p));
}
void chat_sync_broadcast_sound_setting(struct UTUN_INSTANCE* inst, const char* ch_id, int on) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !cs->initialized || !ch_id || !ch_id[0]) return;
for (int i = 0; i < cs->channel_count; i++) {
if (strcmp(cs->channels[i].channel_id, ch_id) != 0) continue;
for (int j = 0; j < cs->channels[i].peer_count; j++) {
uint64_t pid = cs->channels[i].peer_ids[j];
if (pid == inst->node_id || !cs_is_peer_online(inst, pid)) continue;
cs_send_sound_setting(cs, ch_id, pid, on);
}
break;
}
}
/* На поднятии соединения: отправить пиру свои per-chat настройки звука (начальная синхронизация). */
static void cs_send_all_sound_settings(struct chat_sync* cs, uint64_t peer) {
for (int i = 0; i < cs->channel_count; i++) {
int on = chat_core_get_sound_on_message(cs->inst, cs->channels[i].channel_id);
cs_send_sound_setting(cs, cs->channels[i].channel_id, peer, on);
}
}
/* ── Peer status change (GUI + online-бар + schedule_sync; онлайн в БД не хранится) ── */
static void cs_on_peer_status_changed(struct chat_sync* cs, uint64_t peer, int online) {
/* 1. GUI + online-бар */
int found_in_channel = 0;
for (int i = 0; i < cs->channel_count; i++) {
for (int j = 0; j < cs->channels[i].peer_count; j++) {
if (cs->channels[i].peer_ids[j] == peer) {
found_in_channel = 1;
const char* ch_id = cs->channels[i].channel_id;
size_t cl = strlen(ch_id);
uint8_t mevt[1 + 64 + 1 + CHAT_MEMBER_DISPLAY_SIZE];
mevt[0] = (uint8_t)cl;
memcpy(mevt + 1, ch_id, cl);
mevt[1 + cl] = 1;
if (chat_core_get_single_member(cs->inst, ch_id, peer, mevt + 1 + cl + 1) == 0)
chat_event_post(cs->inst, CHAT_EVT_MEMBER_UPDATED, mevt, 1 + (int)cl + 1 + CHAT_MEMBER_DISPLAY_SIZE);
cs_post_channel_online(cs, ch_id);
break;
}
}
}
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: peer_status_changed peer=0x%016llx online=%d channels=%d found=%d",
CS_ID, (unsigned long long)peer, online, cs->channel_count, found_in_channel);
/* 2. Запланировать member_sync_start (throttled) */
cs_schedule_sync(cs);
}
static void cs_resume_db_sync(struct chat_sync* cs, const char* ch_id) {
if (!cs || !ch_id || !ch_id[0]) return;
struct DB_SYNC_INSTANCE* si = si_find(cs->inst, ch_id);
if (si) db_sync_instance_set_gated(si, 0);
}
static void _on_member_sync_done(uint64_t peer, const char* ns, int result, void* arg) {
struct chat_sync* cs = (struct chat_sync*)arg;
if (result == MT_OK) {
struct channel_cache* ch = cs ? cs_find(cs, ns) : NULL;
if (ch) ch->synced = CS_SYNC_DONE;
cs_resume_db_sync(cs, ns);
}
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member_sync %s ns=%s peer=%016llx",
CS_ID, result == MT_OK ? "OK" : "FAIL", ns, (unsigned long long)peer);
}
struct invite_sync_arg {
struct chat_sync* cs;
uint64_t node_id;
char ch_id[64];
};
static void _on_invite_sync_done(uint64_t peer, const char* ns, int result, void* arg) {
struct invite_sync_arg* sa = (struct invite_sync_arg*)arg;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_sync %s ns=%s peer=%016llx sa=%p node=0x%016llx ch=%s",
CS_ID, result == MT_OK ? "OK" : "FAIL", ns,
(unsigned long long)peer, (void*)sa, sa ? (unsigned long long)sa->node_id : 0, sa ? sa->ch_id : "(null)");
if (result == MT_OK && sa && sa->cs) {
if (sa->node_id == 0 || sa->ch_id[0] == '\0') {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_sync CORRUPTED sa=%p node_id=0x%016llx ch_id='%s' — skip, will free",
CS_ID, (void*)sa, (unsigned long long)sa->node_id, sa->ch_id);
} else {
cs_post_channel_online(sa->cs, sa->ch_id);
cs_resume_db_sync(sa->cs, sa->ch_id);
}
}
u_free(sa);
}
/* ── send JOIN_INFO_REQ to start join handshake ── */
static void cs_start_channel_join(struct chat_sync* cs, uint64_t peer) {
if (!cs || cs->info_req_timer) return;
char ch_id_str[64];
snprintf(ch_id_str, sizeof(ch_id_str), "%llu", (unsigned long long)cs->pending_invite_ch_id);
/* payload: [join_key:8][pass_len:1][password] */
size_t plen = strlen(cs->pending_password);
uint8_t req[140];
req[0] = CS_MSG_JOIN_INFO_REQ;
memcpy(req + 1, &cs->pending_join_key, 8);
req[9] = (uint8_t)(plen > 127 ? 127 : plen);
if (plen > 0) memcpy(req + 10, cs->pending_password, plen > 127 ? 127 : plen);
cs_send(cs, ch_id_str, peer, req, 10 + (plen > 127 ? 127 : (int)plen));
cs->info_req_timer = uasync_set_timeout(cs->inst->ua,
CS_INFO_REQ_TIMEOUT_MS * 10, cs, cs_info_req_timeout_cb, "cs_info_req");
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: sent JOIN_INFO_REQ ch=%llu peer=0x%016llx key=%016llx pass_len=%zu",
CS_ID, (unsigned long long)cs->pending_invite_ch_id, (unsigned long long)peer,
(unsigned long long)cs->pending_join_key, plen);
}
/* ── Connection callbacks ── */
static void cs_on_conn_up(struct ETCP_CONN* conn, int event, void* arg) { (void)event;
struct chat_sync* cs = (struct chat_sync*)arg;
if (!conn || !cs) return;
uint64_t peer = conn->peer_node_id;
if (peer == 0 || peer == cs->inst->node_id) return;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn_up peer=%016llx init=%d links=%d", CS_ID, (unsigned long long)peer, conn->initialized, conn->links_up);
if (!conn->initialized) { DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn_up SKIP — not initialized peer=%016llx", CS_ID, (unsigned long long)peer); return; }
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn_up peer=%016llx pending_invite=%llu links_up=%d", CS_ID, (unsigned long long)peer, (unsigned long long)cs->pending_invite_ch_id, conn->links_up);
if (cs->pending_invite_ch_id != 0) {
if (cs->pending_invite_node_id != 0 && cs->pending_invite_node_id != peer)
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite node_id MISMATCH: invite=0x%016llx ETCP_peer=0x%016llx — invite is STALE!",
CS_ID, (unsigned long long)cs->pending_invite_node_id, (unsigned long long)peer);
cs->pending_invite_node_id = peer;
if (cs->pending_invite_is_inviter) {
/* we are the inviter — send CHANNEL_INVITE to the joiner */
char ch_id[64]; snprintf(ch_id, sizeof(ch_id), "%llu",
(unsigned long long)cs->pending_invite_ch_id);
char ch_name[128]; uint8_t ch_x2[32], ch_ed2[32], ch_sig[64]; uint64_t ch_owner;
if (topo_node_sqlite_channel_get(cs->inst->topo_sqlite_db,
ch_id, ch_name, (int)sizeof(ch_name), &ch_owner, ch_x2, ch_ed2, ch_sig) == 0) {
cs_send_channel_invite(cs, cs->inst, ch_id, peer, ch_name, ch_owner, ch_x2, ch_ed2, ch_sig);
if (cs->invite_send_timer) { uasync_cancel_timeout(cs->inst->ua, cs->invite_send_timer); }
cs->invite_send_timer = uasync_set_timeout(cs->inst->ua,
CS_INVITE_SEND_TIMEOUT_MS * 10, cs, cs_invite_send_timeout_cb, "cs_invite_send");
}
} else {
cs_start_channel_join(cs, peer);
}
cs_on_peer_status_changed(cs, peer, 1);
return;
}
cs_on_peer_status_changed(cs, peer, 1);
cs_send_all_sound_settings(cs, peer);
}
static void cs_on_conn_down(struct ETCP_CONN* conn, int event, void* arg) { (void)event;
struct chat_sync* cs = (struct chat_sync*)arg;
if (!conn || !cs) return;
uint64_t peer = conn->peer_node_id;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn_down peer=%016llx", CS_ID, (unsigned long long)peer);
cs_on_peer_status_changed(cs, peer, 0);
cs_cancel_proto_timers(cs);
if (cs->pending_invite_node_id == peer) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn down while waiting invite resp peer=%016llx, timers cancelled, state kept for retry on reconnect",
CS_ID, (unsigned long long)peer);
/* invite state survives connection flaps: only timeout or explicit protocol end clears it */
}
for (int i = 0; i < cs->channel_count; i++) {
int found = 0;
for (int j = 0; j < cs->channels[i].peer_count; j++)
if (cs->channels[i].peer_ids[j] == peer) { found = 1; break; }
if (found)
cs->channels[i].synced = CS_SYNC_NONE;
}
}
static void cs_on_conn_status(struct ETCP_CONN* conn, int status, void* arg) {
switch (status) {
case ETCP_CONN_STATUS_UP: cs_on_conn_up(conn, ETCP_CBK_EVENT_UP, arg); break;
case ETCP_CONN_STATUS_DOWN:
case ETCP_CONN_STATUS_DELETE: cs_on_conn_down(conn, ETCP_CBK_EVENT_DOWN, arg); break;
default: break;
}
}
/* ── Periodic refresh from DB ── */
static void cs_refresh_channels(struct chat_sync* cs) {
uint8_t buf[4096]; size_t buf_len;
if (chat_core_list_channels(cs->inst, buf, sizeof(buf), &buf_len) != 0) return;
if (buf_len < 2) return;
uint16_t cnt; memcpy(&cnt, buf, 2);
const uint8_t* p = buf + 2; size_t rem = buf_len - 2;
for (int i = 0; i < cs->channel_count; i++)
if (cs->channels[i].peer_ids) u_free(cs->channels[i].peer_ids);
if (cs->channels) u_free(cs->channels);
cs->channels = u_calloc(cnt, sizeof(struct channel_cache));
cs->channel_count = cnt;
if (!cs->channels) { cs->channel_count = 0; return; }
for (int i = 0; i < (int)cnt && rem >= 1; i++) {
uint8_t id_len = *p++; rem--;
if (rem < id_len) break;
memcpy(cs->channels[i].channel_id, p, id_len);
cs->channels[i].channel_id[id_len] = '\0';
p += id_len; rem -= id_len;
cs->channels[i].msg_count = chat_core_count(cs->inst, cs->channels[i].channel_id);
if (cs->channels[i].msg_count > 0)
chat_core_chain_hash_at(cs->inst, cs->channels[i].channel_id,
cs->channels[i].msg_count - 1, cs->channels[i].last_chain_hash);
else
memset(cs->channels[i].last_chain_hash, 0, 32);
uint8_t peer_buf[2048]; size_t peer_len;
if (chat_core_list_peers(cs->inst, cs->channels[i].channel_id, peer_buf,
sizeof(peer_buf), &peer_len) == 0 && peer_len >= 2) {
uint16_t pc; memcpy(&pc, peer_buf, 2);
cs->channels[i].peer_ids = u_calloc(pc, sizeof(uint64_t));
cs->channels[i].peer_count = pc;
for (uint16_t j = 0; j < pc; j++)
memcpy(&cs->channels[i].peer_ids[j], peer_buf + 2 + j * 8, 8);
}
}
for (int i = 0; i < cs->channel_count; i++)
cs_post_channel_online(cs, cs->channels[i].channel_id);
}
static void refresh_timer_cb(void* arg) {
struct chat_sync* cs = (struct chat_sync*)arg;
if (!cs || !cs->initialized) return;
#ifdef UTUN_HAVE_STANDBY
if (standby_get_sleep_tb() > 0) {
cs->refresh_wait = standby_wait(cs, refresh_timer_cb);
return;
}
cs->refresh_wait = NULL;
#endif
cs_refresh_channels(cs);
cs->refresh_timer = uasync_set_timeout(cs->inst->ua,
30u * 10000u, cs, refresh_timer_cb, "cs_refresh");
}
/* ── TTL cleanup ── */
/* ── Public API ── */
int chat_sync_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: init", CS_ID);
struct chat_sync* cs = u_calloc(1, sizeof(*cs));
if (!cs) return -1;
cs->inst = inst;
cs->initialized = 1;
inst->chat_sync = cs;
etcp_bind(inst, ETCP_RT_ID_CHAT_SYNC, chat_sync_recv_cb);
etcp_add_conn_status_cbk(inst, cs_on_conn_status, cs);
cs_refresh_channels(cs);
cs->refresh_timer = uasync_set_timeout(inst->ua, 50000u, cs,
refresh_timer_cb, "cs_refresh");
cs->info_req_timer = NULL;
cs->join_timer = NULL;
member_sync_init(inst);
member_sync_add_apply_cbk(inst, cs_on_member_applied, cs);
utun_add_peer_sleep_cbk(inst, cs_on_peer_sleep, cs);
chat_admin_init(inst);
chat_join_init(inst);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: initialized", CS_ID);
return 0;
}
void chat_sync_destroy(struct UTUN_INSTANCE* inst) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !inst) return;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: destroy", CS_ID);
if (cs->sync_timer) { uasync_cancel_timeout(inst->ua, cs->sync_timer); cs->sync_timer = NULL; }
if (cs->invite_send_timer) { uasync_cancel_timeout(inst->ua, cs->invite_send_timer); cs->invite_send_timer = NULL; }
if (cs->sync_scheduled) { cs->sync_scheduled = 0; cs_flush_sync(cs); }
member_sync_remove_apply_cbk(inst, cs_on_member_applied, cs);
utun_remove_peer_sleep_cbk(inst, cs_on_peer_sleep, cs);
cs_pending_free_all(cs);
member_sync_destroy(inst);
chat_admin_destroy(inst);
chat_join_destroy(inst);
cs->initialized = 0; inst->chat_sync = NULL;
etcp_unbind(inst, ETCP_RT_ID_CHAT_SYNC);
etcp_remove_conn_status_cbk(inst, cs_on_conn_status, cs);
if (cs->refresh_timer) { uasync_cancel_timeout(inst->ua, cs->refresh_timer); cs->refresh_timer = NULL; }
#ifdef UTUN_HAVE_STANDBY
if (cs->refresh_wait) { standby_wait_cancel(cs->refresh_wait); cs->refresh_wait = NULL; }
#endif
cs_cancel_proto_timers(cs);
for (int i = 0; i < cs->channel_count; i++)
if (cs->channels[i].peer_ids) u_free(cs->channels[i].peer_ids);
if (cs->channels) u_free(cs->channels);
u_free(cs);
}
void chat_sync_retry_channels_on_socket_change(struct UTUN_INSTANCE* inst) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !cs->initialized || !inst->topo_groups) return;
for (int i = 0; i < cs->channel_count; i++) {
uint64_t gid = strtoull(cs->channels[i].channel_id, NULL, 10);
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, gid);
if (!group || !group->connect) continue;
if (topo_group_connect_active_count(group) == 0) {
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: socket change — restarting connect for ch=%s (no active connections)",
CS_ID, cs->channels[i].channel_id);
topo_group_connect_restart(group);
}
}
}
void chat_sync_connect_node(struct UTUN_INSTANCE* inst, uint64_t node_id) {
(void)inst; (void)node_id;
}
void chat_sync_on_channel_deleted(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !ch_id || !ch_id[0]) return;
for (int i = 0; i < cs->channel_count; i++) {
if (strcmp(cs->channels[i].channel_id, ch_id) != 0) continue;
if (cs->channels[i].peer_ids) u_free(cs->channels[i].peer_ids);
memmove(&cs->channels[i], &cs->channels[i + 1],
(size_t)(cs->channel_count - i - 1) * sizeof(struct channel_cache));
cs->channel_count--;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: channel removed from cache ch=%s", CS_ID, ch_id);
return;
}
}
struct chat_invite {
uint64_t channel_id;
uint64_t node_id;
uint8_t pubkey[32];
uint8_t* addrs_data;
int addrs_data_len;
int addr_count;
uint64_t join_key;
char password[128];
};
struct cm_invite_wrap { struct UTUN_INSTANCE* inst; struct chat_invite inv; };
/* Парсит reality-префикс addrs_data: has(1) + [short_id(8) pubkey(32) ver(3) snlen(1) sn(N)].
Сдвигает *ap за префикс. Возвращает 1, если reality присутствует (и params заполнены). */
static int invite_reality_parse(const uint8_t** ap, const uint8_t* end,
uint8_t short_id[8], uint8_t pubkey[32], uint8_t ver[3],
char* sname, size_t sname_sz) {
const uint8_t* p = *ap;
if (p + 1 > end) return 0;
if (!*p++) { *ap = p; return 0; }
if (p + 8 + 32 + 3 + 1 > end) { *ap = end; return 0; }
memcpy(short_id, p, 8); p += 8;
memcpy(pubkey, p, 32); p += 32;
memcpy(ver, p, 3); p += 3;
uint8_t snlen = *p++;
if (snlen > (uint8_t)(sname_sz - 1)) snlen = (uint8_t)(sname_sz - 1);
if (p + snlen > end) { *ap = end; return 0; }
memcpy(sname, p, snlen); sname[snlen] = '\0'; p += snlen;
*ap = p;
return 1;
}
/* Добавляет reality-сокет узла с node-global параметрами. */
static void invite_reality_sock_append(struct TOPO_NODE* ni, uint8_t socket_id,
const uint8_t short_id[8], const uint8_t pubkey[32],
const uint8_t ver[3], const char* sname) {
struct TOPO_REALITY_SOCK* r = u_calloc(1, sizeof(*r));
if (!r) return;
r->socket_id = socket_id;
memcpy(r->short_id, short_id, 8);
memcpy(r->server_pubkey, pubkey, 32);
memcpy(r->version, ver, 3);
snprintf(r->server_name, sizeof(r->server_name), "%s", sname);
r->next = ni->reality_socks; ni->reality_socks = r;
}
static void cm_invite_trampoline(void* arg) {
struct cm_invite_wrap* w = (struct cm_invite_wrap*)arg;
struct chat_invite* inv = &w->inv;
struct UTUN_INSTANCE* inst = w->inst;
struct chat_sync* cs = cs_of(inst);
uint64_t node_id = inv->node_id;
uint64_t channel_id = inv->channel_id;
if (!cs) { u_free(inv->addrs_data); u_free(w); return; }
/* Параметры invite храним в cs->pending_* здесь, в uasync-потоке (не на вызывающем):
* их читает cs_start_channel_join/cs_on_conn_up из uasync-потока. */
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = node_id;
cs->pending_join_key = inv->join_key;
snprintf(cs->pending_password, sizeof(cs->pending_password), "%s", inv->password);
/* build TOPO_NODE from invite data */
struct TOPO_NODE* ni = u_calloc(1, sizeof(*ni));
if (!ni) { u_free(inv->addrs_data); u_free(w); return; }
ni->node_id = node_id; memcpy(ni->public_key, inv->pubkey, 32);
const uint8_t* ap = inv->addrs_data;
const uint8_t* ap_end = inv->addrs_data + inv->addrs_data_len;
uint8_t r_short_id[8], r_pubkey[32], r_ver[3]; char r_sname[TOPO_REALITY_SNAME_MAX];
int has_reality = invite_reality_parse(&ap, ap_end, r_short_id, r_pubkey, r_ver, r_sname, sizeof(r_sname));
for (int i = 0; i < inv->addr_count && ap + 3 <= ap_end; i++) {
uint8_t family = *ap++; uint8_t sock_id = *ap++; uint8_t proto = *ap++;
if (family == 4 && ap + 6 <= ap_end) {
struct TOPO_ADDR4* a4 = u_calloc(1, sizeof(*a4));
if (!a4) break;
memcpy(a4->addr, ap, 4); ap += 4;
a4->port = ((uint16_t)ap[0] << 8) | ap[1]; ap += 2;
a4->type = TOPO_ADDR_NAT; a4->protocol = proto; a4->socket_id = sock_id;
a4->next = ni->v4_addrs; ni->v4_addrs = a4;
if (has_reality && (proto & 0x04)) invite_reality_sock_append(ni, sock_id, r_short_id, r_pubkey, r_ver, r_sname);
} else if (family == 6 && ap + 18 <= ap_end) {
struct TOPO_ADDR6* a6 = u_calloc(1, sizeof(*a6));
if (!a6) break;
memcpy(a6->addr, ap, 16); ap += 16;
a6->port = ((uint16_t)ap[0] << 8) | ap[1]; ap += 2;
a6->type = TOPO_ADDR_NAT; a6->protocol = proto; a6->socket_id = sock_id;
a6->next = ni->v6_addrs; ni->v6_addrs = a6;
if (has_reality && (proto & 0x04)) invite_reality_sock_append(ni, sock_id, r_short_id, r_pubkey, r_ver, r_sname);
} else { break; }
}
{ char buf[512]; int off = 0; int n4 = 0, n6 = 0;
for (const struct TOPO_ADDR4* a4 = ni->v4_addrs; a4; a4 = a4->next) {
n4++;
off += snprintf(buf + off, sizeof(buf) - (size_t)off, "%s%d.%d.%d.%d:%d(p=%d)",
off ? "," : "", a4->addr[0], a4->addr[1], a4->addr[2], a4->addr[3], a4->port, a4->protocol);
}
for (const struct TOPO_ADDR6* a6 = ni->v6_addrs; a6; a6 = a6->next) {
n6++;
off += snprintf(buf + off, sizeof(buf) - (size_t)off, "%s%s:%d(p=%d)",
off ? "," : "", ip_to_str(a6->addr, AF_INET6).str, a6->port, a6->protocol);
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "channel invite: parsed node=0x%016llx addrs v4=%d v6=%d [%s]",
(unsigned long long)node_id, n4, n6, (n4 || n6) ? buf : "(none)");
}
if (!ni->v4_addrs && !ni->v6_addrs) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: no valid addrs for node=0x%016llx", (unsigned long long)node_id);
while (ni->v4_addrs) { struct TOPO_ADDR4* n = ni->v4_addrs->next; u_free(ni->v4_addrs); ni->v4_addrs = n; }
while (ni->v6_addrs) { struct TOPO_ADDR6* n = ni->v6_addrs->next; u_free(ni->v6_addrs); ni->v6_addrs = n; }
u_free(ni); u_free(inv->addrs_data); u_free(w); return;
}
/* если линк к узлу уже есть и он up — не открываем новое подключение,
сразу запускаем join-хендшейк (JOIN_INFO_REQ → … → JOIN_READY) */
struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
if (conn && conn->links_up && conn->initialized) {
cs->pending_invite_node_id = node_id;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,
"channel invite: link to 0x%016llx already up — skipping connect, join handshake directly",
(unsigned long long)node_id);
cs_start_channel_join(cs, node_id);
} else {
/* topo_group_invite_to_channel: прямое ncd-подключение к connection-узлу БЕЗ INVITE_INFO.
Реальный join-протокол (JOIN_INFO_REQ → … → JOIN_READY) запускается по cs_on_conn_up. */
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "channel invite: connecting to 0x%016llx via %d addresses",
(unsigned long long)node_id, inv->addr_count);
int r = topo_group_invite_to_channel(inst, channel_id, ni, node_id);
if (r < 0)
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "channel invite: FAILED — topo_group_invite_to_channel error=%d", r);
}
/* free caller-owned ni */
while (ni->v4_addrs) { struct TOPO_ADDR4* n = ni->v4_addrs->next; u_free(ni->v4_addrs); ni->v4_addrs = n; }
while (ni->v6_addrs) { struct TOPO_ADDR6* n = ni->v6_addrs->next; u_free(ni->v6_addrs); ni->v6_addrs = n; }
u_free(ni);
u_free(inv->addrs_data);
u_free(w);
}
void chat_sync_connect_from_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t node_id,
const uint8_t* pubkey_bin,
const uint8_t* addrs_data, int addr_count,
int addrs_data_len,
const char* password, uint64_t join_key) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !cs->inst || !cs->inst->ua) {
int r = -7;
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_ERROR, "utun-chat", "invite ch=%llu node=%016llx REJECTED cs=%p inst=%p ua=%p",
(long long)channel_id, (long long)node_id, (void*)cs,
cs ? (void*)cs->inst : NULL,
(cs && cs->inst) ? (void*)cs->inst->ua : NULL);
#endif
uint8_t err[12]; memcpy(err, &node_id, 8); memcpy(err + 8, &r, 4);
chat_event_post(inst, CHAT_EVT_CONNECT_RESULT, err, 12);
return;
}
struct chat_invite* inv = u_calloc(1, sizeof(struct chat_invite));
if (!inv) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: connect_from_invite calloc failed", CS_ID); return; }
inv->channel_id = channel_id;
inv->node_id = node_id;
memcpy(inv->pubkey, pubkey_bin, 32);
size_t addrs_sz = addrs_data_len > 0 ? (size_t)addrs_data_len : (size_t)addr_count * 9;
inv->addrs_data = u_malloc(addrs_sz);
if (!inv->addrs_data) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: connect_from_invite malloc(%zu) failed", CS_ID, addrs_sz); u_free(inv); return; }
memcpy(inv->addrs_data, addrs_data, addrs_sz);
inv->addrs_data_len = (int)addrs_sz;
inv->addr_count = addr_count;
inv->join_key = join_key;
if (password && password[0]) snprintf(inv->password, sizeof(inv->password), "%s", password);
else inv->password[0] = '\0';
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_INFO, "utun-chat", "invite ch=%llu node=%016llx addrs=%d pass=%s",
(long long)channel_id, (long long)node_id, addr_count, password ? "yes" : "no");
#endif
#ifndef __ANDROID__
uint64_t pubkey_prefix; memcpy(&pubkey_prefix, pubkey_bin, sizeof(pubkey_prefix));
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite start ch=%llu node=0x%016llx pubkey=%016llx... addrs=%d pass=%s",
CS_ID, (unsigned long long)channel_id, (unsigned long long)node_id, (unsigned long long)pubkey_prefix, addr_count, password && password[0] ? "yes" : "no");
#endif
struct cm_invite_wrap* w = u_malloc(sizeof(struct cm_invite_wrap));
w->inst = inst;
w->inv = *inv; u_free(inv);
uasync_post(cs->inst->ua,
(void(*)(void*))cm_invite_trampoline, w);
}
/* ─── chat_sync_join_channel: join channel via already-connected peer ─── */
void chat_sync_join_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id) {
struct chat_sync* cs = cs_of(inst);
if (!cs) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel: cs=NULL, chat_sync not initialized", CS_ID); return; }
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel: inst=NULL", CS_ID); return; }
if (!ch_id || !ch_id[0]) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel: ch_id NULL/empty", CS_ID); return; }
uint64_t channel_id = strtoull(ch_id, NULL, 10);
if (channel_id == 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel invalid ch_id='%s'", CS_ID, ch_id); return; }
struct ETCP_CONN* conn = instance_find_conn(inst, target_node_id);
if (!conn || !conn->links_up || !conn->initialized) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel no active conn to 0x%016llx for ch=%s",
CS_ID, (unsigned long long)target_node_id, ch_id);
return;
}
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel ch=%s peer=0x%016llx",
CS_ID, ch_id, (unsigned long long)target_node_id);
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
cs_start_channel_join(cs, target_node_id);
cs_on_peer_status_changed(cs, target_node_id, 1);
}
/* ─── Ed25519 sign / verify helpers ─── */
static int cs_ed25519_sign(const uint8_t* privkey, const uint8_t* msg, size_t msg_len,
uint8_t* sig_out) {
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, privkey, SC_PRIVKEY_SIZE);
if (!pkey) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: EVP_PKEY_new failed", CS_ID); return -1; }
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
if (!mdctx) { EVP_PKEY_free(pkey); return -1; }
int ok = (EVP_DigestSignInit(mdctx, NULL, NULL, NULL, pkey) == 1)
&& (EVP_DigestSign(mdctx, sig_out, &(size_t){64}, msg, msg_len) == 1);
EVP_MD_CTX_free(mdctx);
EVP_PKEY_free(pkey);
return ok ? 0 : -1;
}
static int cs_ed25519_verify(const uint8_t* pubkey, const uint8_t* msg, size_t msg_len,
const uint8_t* sig) {
EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, pubkey, SC_PUBKEY_SIZE);
if (!pkey) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: EVP_PKEY_new pub failed", CS_ID); return -1; }
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
if (!mdctx) { EVP_PKEY_free(pkey); return -1; }
int rc = EVP_DigestVerifyInit(mdctx, NULL, NULL, NULL, pkey)
&& EVP_DigestVerify(mdctx, sig, 64, msg, msg_len) == 1;
EVP_MD_CTX_free(mdctx);
EVP_PKEY_free(pkey);
return rc ? 0 : -1;
}
/* ─── Propagation helper ─── */
static void cs_propagate(struct chat_sync* cs, const char* ch_id, uint64_t exclude_id,
const uint8_t* payload, size_t len) {
struct channel_cache* ch = cs_find(cs, ch_id);
if (!ch) return;
uint64_t myid = cs->inst->node_id;
for (int i = 0; i < ch->peer_count; i++) {
uint64_t pid = ch->peer_ids[i];
if (pid == exclude_id || pid == myid) continue;
if (!cs_find_conn_for_node(cs->inst, pid)) continue;
cs_send(cs, ch_id, pid, payload, len);
}
}
static int _get_node_name(sqlite3* db, uint64_t node_id, char* out, size_t sz) {
if (!db || !out || !sz) return -1;
sqlite3_stmt* st = NULL;
sqlite3_prepare_v2(db, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL);
if (!st) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: _get_node_name prepare failed node=0x%016llx", CS_ID, (unsigned long long)node_id); return -1; }
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
int rc = -1;
if (sqlite3_step(st) == SQLITE_ROW) {
const unsigned char* nm = sqlite3_column_text(st, 0);
if (nm) { snprintf(out, sz, "%s", nm); rc = 0; }
else { out[0] = '\0'; rc = 0; }
}
sqlite3_finalize(st);
return rc;
}
/* ─── JOIN_INFO_REQ (0x10): joiner → connection-узел ─── */
static void cs_handle_join_info_req(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len) {
/* payload: [join_key:8][pass_len:1][password] */
if (len < 9) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_REQ too short len=%zu peer=%016llx", CS_ID, len, (unsigned long long)peer); return; }
uint64_t join_key; memcpy(&join_key, pl, 8);
uint64_t channel_id = strtoull(ch_id, NULL, 10);
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_REQ ch=%s from=%016llx key=%016llx len=%zu",
CS_ID, ch_id, (unsigned long long)peer, (unsigned long long)join_key, len);
/* 1. валидация join-ключа (зарегистрирован инвайтером на connection-узле, TTL 10 мин) */
uint64_t inviter = chat_join_lookup_inviter(cs->inst, channel_id, join_key);
if (inviter == 0) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_REQ invalid/expired join key ch=%s from=%016llx key=%016llx",
CS_ID, ch_id, (unsigned long long)peer, (unsigned long long)join_key);
uint8_t err[1] = { CS_MSG_ERROR }; cs_send(cs, ch_id, peer, err, 1);
return;
}
/* 2. инфо канала */
char name[128]; uint64_t owner; uint8_t ch_x25519[32], ch_ed[32], ch_sig[64];
if (topo_node_sqlite_channel_get(cs->inst->topo_sqlite_db,
ch_id, name, (int)sizeof(name), &owner, ch_x25519, ch_ed, ch_sig) != 0) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_REQ unknown ch=%s from=%016llx", CS_ID, ch_id, (unsigned long long)peer);
uint8_t err[1] = { CS_MSG_ERROR }; cs_send(cs, ch_id, peer, err, 1);
return;
}
/* 4. мой мембер-рекорд (join_sig/update_sig + дерево приглашений) */
uint64_t myid = cs->inst->node_id;
uint8_t my_join_sig[64] = {0}; uint64_t my_join_ts = 0;
{
sqlite3* vdb = cs->inst->topo_sqlite_db;
if (topo_node_sqlite_member_get_join(vdb, ch_id, myid, my_join_sig, &my_join_ts) != 0) {
uint8_t join_msg[128];
my_join_ts = (uint64_t)ntp_time_get_seconds(cs->inst);
int jmlen = member_sync_build_join_msg(ch_x25519, ch_ed, myid, cs->inst->my_keys.public_key, my_join_ts,
join_msg, (int)sizeof(join_msg));
if (jmlen >= 0) cs_ed25519_sign(cs->inst->my_ed25519_privkey, join_msg, (size_t)jmlen, my_join_sig);
}
}
uint8_t my_update_sig[64] = {0}; uint64_t my_update_ts;
{
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}", cs->inst->name[0] ? cs->inst->name : "");
my_update_ts = (uint64_t)ntp_time_get_seconds(cs->inst);
uint8_t umsg[256];
int ulen = member_sync_build_update_msg(my_join_sig, my_update_ts, juser, umsg, (int)sizeof(umsg));
if (ulen >= 0) cs_ed25519_sign(cs->inst->my_ed25519_privkey, umsg, (size_t)ulen, my_update_sig);
}
uint64_t my_signed_by = 0; uint8_t my_signature[64] = {0};
topo_node_sqlite_member_get_sign(cs->inst->topo_sqlite_db, ch_id, myid, &my_signed_by, my_signature);
/* 5. JOIN_INFO_RESP: инфо канала + мой мембер-рекорд */
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}", cs->inst->name[0] ? cs->inst->name : "");
uint8_t il = (uint8_t)strlen(juser);
uint8_t buf[1536]; size_t boff = 0;
buf[boff++] = CS_MSG_JOIN_INFO_RESP;
uint8_t cnl = (uint8_t)strlen(name); buf[boff++] = cnl; memcpy(buf + boff, name, cnl); boff += cnl;
memcpy(buf + boff, &owner, 8); boff += 8;
memcpy(buf + boff, ch_x25519, 32); boff += 32;
memcpy(buf + boff, ch_ed, 32); boff += 32;
memcpy(buf + boff, ch_sig, 64); boff += 64;
memcpy(buf + boff, &myid, 8); boff += 8;
memcpy(buf + boff, cs->inst->my_keys.public_key, 32); boff += 32;
memcpy(buf + boff, cs->inst->my_ed25519_pubkey, 32); boff += 32;
memcpy(buf + boff, my_join_sig, 64); boff += 64;
memcpy(buf + boff, &my_join_ts, 8); boff += 8;
memcpy(buf + boff, my_update_sig, 64); boff += 64;
memcpy(buf + boff, &my_update_ts, 8); boff += 8;
buf[boff++] = il; memcpy(buf + boff, juser, il); boff += il;
memcpy(buf + boff, &my_signed_by, 8); boff += 8;
memcpy(buf + boff, my_signature, 64); boff += 64;
cs_send(cs, ch_id, peer, buf, boff);
}
/* ─── JOIN_INFO_RESP (0x11): connection-узел → joiner ─── */
static void cs_handle_join_info_resp(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len) {
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP ch=%s from=%016llx len=%zu", CS_ID, ch_id, (unsigned long long)peer, len);
if (cs->info_req_timer) { uasync_cancel_timeout(cs->inst->ua, cs->info_req_timer); cs->info_req_timer = NULL; }
if (len < 1) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP too short len=%zu", CS_ID, len); return; }
const uint8_t* p = pl;
uint8_t cnl = *p++;
if (cnl + 8 + 32 + 32 + 64 + 8 + 32 + 32 + 64 + 8 + 64 + 8 + 1 + 8 + 64 > len) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP truncated len=%zu", CS_ID, len); return;
}
char name[128]; size_t cn = cnl > 127 ? 127 : cnl; memcpy(name, p, cn); name[cn] = '\0'; p += cnl;
uint64_t owner; memcpy(&owner, p, 8); p += 8;
const uint8_t* ch_x25519 = p; p += 32;
const uint8_t* ch_ed = p; p += 32;
const uint8_t* ch_sig = p; p += 64;
uint64_t c_node_id; memcpy(&c_node_id, p, 8); p += 8;
const uint8_t* c_x25519 = p; p += 32;
const uint8_t* c_ed = p; p += 32;
const uint8_t* c_join_sig = p; p += 64;
uint64_t c_join_ts; memcpy(&c_join_ts, p, 8); p += 8;
const uint8_t* c_update_sig = p; p += 64;
uint64_t c_update_ts; memcpy(&c_update_ts, p, 8); p += 8;
uint8_t c_uil = *p++;
char c_userinfo[256] = "";
if (c_uil && p + c_uil <= pl + len) { size_t u = c_uil > 255 ? 255 : c_uil; memcpy(c_userinfo, p, u); c_userinfo[u] = '\0'; p += c_uil; }
uint64_t c_signed_by; memcpy(&c_signed_by, p, 8); p += 8;
const uint8_t* c_signature = p; p += 64;
uint64_t ch_x_prefix, ch_ed_prefix;
memcpy(&ch_x_prefix, ch_x25519, sizeof(ch_x_prefix)); memcpy(&ch_ed_prefix, ch_ed, sizeof(ch_ed_prefix));
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP from=%016llx owner=%016llx ch_x=%016llx ch_ed=%016llx name=%s conn_node=%016llx",
CS_ID, (unsigned long long)peer, (unsigned long long)owner,
(unsigned long long)ch_x_prefix, (unsigned long long)ch_ed_prefix, name, (unsigned long long)c_node_id);
/* verify channel signature */
uint8_t vmsg[1024]; size_t vlen = 0;
vlen += snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", ch_id) + 1;
vlen += snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", name) + 1;
memcpy(vmsg + vlen, &owner, 8); vlen += 8;
memcpy(vmsg + vlen, ch_x25519, 32); vlen += 32;
memcpy(vmsg + vlen, ch_ed, 32); vlen += 32;
if (cs_ed25519_verify(ch_ed, vmsg, vlen, ch_sig) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP invalid ch_sig ch=%s", CS_ID, ch_id);
return;
}
/* save channel */
topo_node_sqlite_channel_put(cs->inst->topo_sqlite_db, ch_id, name, owner, ch_x25519, NULL, ch_ed, NULL, ch_sig);
chat_core_ensure_channel_ready(cs->inst, ch_id);
/* добавить connection-узел в мемберы (дерево приглашений из INFO_RESP) */
sqlite3* vdb = cs->inst->topo_sqlite_db;
topo_node_sqlite_member_block_put(vdb, ch_id, c_node_id, c_join_sig, c_join_ts,
c_update_sig, c_update_ts, c_x25519, c_ed,
c_userinfo, NULL, c_signed_by, c_signature);
{ char cn_name[128]; json_flat_get(c_userinfo, "name", cn_name, sizeof(cn_name));
topo_node_sqlite_node_update_verified(vdb, c_node_id, cn_name, c_x25519, c_ed, c_join_ts, ntp_time_get_seconds(cs->inst)); }
/* построить собственный join_sig */
uint64_t myid = cs->inst->node_id;
const uint8_t* my_x25519 = cs->inst->my_keys.public_key;
uint8_t join_sig[64]; uint64_t join_ts;
{
uint8_t msg[128];
join_ts = (uint64_t)ntp_time_get_seconds(cs->inst);
int mlen = member_sync_build_join_msg(ch_x25519, ch_ed, myid, my_x25519, join_ts, msg, (int)sizeof(msg));
if (mlen < 0) { memset(join_sig, 0, 64); DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP build_join_msg failed", CS_ID); return; }
cs_ed25519_sign(cs->inst->my_ed25519_privkey, msg, (size_t)mlen, join_sig);
}
uint8_t my_update_sig[64] = {0}; uint64_t my_update_ts;
{
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}", cs->inst->name[0] ? cs->inst->name : "");
my_update_ts = (uint64_t)ntp_time_get_seconds(cs->inst);
uint8_t umsg[256];
int ulen = member_sync_build_update_msg(join_sig, my_update_ts, juser, umsg, (int)sizeof(umsg));
if (ulen >= 0) cs_ed25519_sign(cs->inst->my_ed25519_privkey, umsg, (size_t)ulen, my_update_sig);
}
/* send JOIN_REQUEST: [join_key:8][member...] */
struct join_member_data jm;
memset(&jm, 0, sizeof(jm));
jm.node_id = myid; jm.x25519 = my_x25519; jm.ed25519 = cs->inst->my_ed25519_pubkey;
jm.join_sig = join_sig; jm.join_ts = join_ts;
jm.update_sig = my_update_sig; jm.update_ts = my_update_ts;
snprintf(jm.userinfo, sizeof(jm.userinfo), "{\"name\":\"%s\"}", cs->inst->name[0] ? cs->inst->name : "");
uint8_t jm_buf[512]; size_t jm_len = 0;
if (chat_join_serialize_member(&jm, jm_buf, sizeof(jm_buf), &jm_len) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_REQUEST serialize failed", CS_ID);
return;
}
uint8_t jbuf[520]; size_t joff = 0;
jbuf[joff++] = CS_MSG_JOIN_REQUEST;
memcpy(jbuf + joff, &cs->pending_join_key, 8); joff += 8;
memcpy(jbuf + joff, jm_buf, jm_len); joff += jm_len;
int jrc = cs_send(cs, ch_id, peer, jbuf, joff);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_REQUEST sent myid=%016llx key=%016llx ch=%s to=0x%016llx rc=%d",
CS_ID, (unsigned long long)myid, (unsigned long long)cs->pending_join_key, ch_id, (unsigned long long)peer, jrc);
cs->join_timer = uasync_set_timeout(cs->inst->ua, CS_JOIN_TIMEOUT_MS * 10, cs, cs_join_timeout_cb, "cs_join");
}
/* ─── pending-джойнеры (connection-узел): node_id → прямое соединение ─── */
static struct join_pending* cs_pending_find(struct chat_sync* cs, uint64_t node_id, const char* ch_id) {
for (struct join_pending* jp = cs->join_pending; jp; jp = jp->next)
if (jp->node_id == node_id && strcmp(jp->ch_id, ch_id) == 0) return jp;
return NULL;
}
static void cs_pending_add(struct chat_sync* cs, uint64_t node_id, const char* ch_id, struct ETCP_CONN* conn) {
if (cs_pending_find(cs, node_id, ch_id)) return;
struct join_pending* jp = u_malloc(sizeof(*jp));
if (!jp) return;
jp->node_id = node_id; snprintf(jp->ch_id, sizeof(jp->ch_id), "%s", ch_id); jp->conn = conn;
jp->next = cs->join_pending; cs->join_pending = jp;
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: pending join added node=0x%016llx ch=%s", CS_ID, (unsigned long long)node_id, ch_id);
}
static void cs_pending_remove(struct chat_sync* cs, uint64_t node_id, const char* ch_id) {
struct join_pending** p = &cs->join_pending;
while (*p) {
if ((*p)->node_id == node_id && strcmp((*p)->ch_id, ch_id) == 0) {
struct join_pending* r = *p; *p = r->next; u_free(r);
return;
}
p = &(*p)->next;
}
}
static void cs_pending_free_all(struct chat_sync* cs) {
struct join_pending* jp = cs->join_pending;
while (jp) { struct join_pending* n = jp->next; u_free(jp); jp = n; }
cs->join_pending = NULL;
}
/* ─── отправка JOIN_READY джойнеру + снятие pending (connection-узел) ─── */
static void cs_send_join_ready(struct chat_sync* cs, const char* ch_id, uint64_t node_id) {
uint64_t group_id = strtoull(ch_id, NULL, 10);
uint8_t rbuf[9];
rbuf[0] = CS_MSG_JOIN_READY;
memcpy(rbuf + 1, &group_id, 8);
cs_send(cs, ch_id, node_id, rbuf, 9);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_READY sent to 0x%016llx ch=%s",
CS_ID, (unsigned long long)node_id, ch_id);
cs_pending_remove(cs, node_id, ch_id);
}
/* ─── коллбэк member_sync: пришёл рекорд мембера → отправить JOIN_READY джойнеру ─── */
static void cs_on_member_applied(const char* ch_id, uint64_t node_id,
const uint8_t* x25519, uint64_t signed_by,
const uint8_t* signature, void* arg) {
(void)x25519; (void)signed_by; (void)signature;
struct chat_sync* cs = (struct chat_sync*)arg;
if (!cs || !cs->initialized) return;
struct join_pending* jp = cs_pending_find(cs, node_id, ch_id);
if (!jp || !jp->conn) return;
cs_send_join_ready(cs, ch_id, node_id);
}
/* ─── JOIN_REQUEST (0x12): joiner → connection-узел ─── */
static void cs_handle_join_request(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len) {
/* payload: [join_key:8][member...] */
if (len < 8 + 1) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_REQUEST too short len=%zu peer=%016llx", CS_ID, len, (unsigned long long)peer); return; }
uint64_t join_key; memcpy(&join_key, pl, 8);
uint64_t channel_id = strtoull(ch_id, NULL, 10);
const uint8_t* member_data = pl + 8; size_t member_len = len - 8;
struct join_member_data m;
if (chat_join_parse_member(member_data, member_len, &m) != 0) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_REQUEST parse failed ch=%s from=%016llx", CS_ID, ch_id, (unsigned long long)peer);
return;
}
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_REQUEST ch=%s joiner=%016llx from=%016llx key=%016llx",
CS_ID, ch_id, (unsigned long long)m.node_id, (unsigned long long)peer, (unsigned long long)join_key);
/* привязка идентичности: pubkey из ETCP-соединения == заявленный x25519 */
struct ETCP_CONN* jconn = cs_find_conn_for_node(cs->inst, peer);
if (!jconn || memcmp(jconn->crypto_ctx.peer_public_key, m.x25519, 32) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_REQUEST identity mismatch joiner=0x%016llx peer=0x%016llx — drop",
CS_ID, (unsigned long long)m.node_id, (unsigned long long)peer);
return;
}
/* валидация ключа */
uint64_t inviter = chat_join_lookup_inviter(cs->inst, channel_id, join_key);
if (inviter == 0) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_REQUEST invalid/expired join key ch=%s joiner=0x%016llx",
CS_ID, ch_id, (unsigned long long)m.node_id);
uint8_t err[1] = { CS_MSG_ERROR }; cs_send(cs, ch_id, peer, err, 1);
return;
}
/* валидный ключ → сразу добавляем узел в группу для BGP sync */
{
uint64_t gid = strtoull(ch_id, NULL, 10);
struct TOPO_GROUP* g = topo_groups_find(cs->inst->topo_groups, gid);
if (g && g->group_type == TOPO_GROUP_TYPE_CHAT)
topo_group_new_conn(g, jconn);
}
/* добавить в pending (ждут JOIN_READY по приходу рекорда через merkle) */
cs_pending_add(cs, m.node_id, ch_id, jconn);
/* форвард инвайтеру (или локальная обработка, если инвайтер == мы) */
if (inviter == cs->inst->node_id) {
chat_join_process_request(cs->inst, channel_id, peer, join_key, member_data, member_len);
/* мы и есть connection-узел + инвайтер: рекорд добавлен локально,
merkle-apply-коллбэк (cs_on_member_applied) здесь не сработает —
отправляем JOIN_READY напрямую, если мембер реально добавлен. */
if (topo_node_sqlite_member_in_channel(cs->inst->topo_sqlite_db, ch_id, m.node_id))
cs_send_join_ready(cs, ch_id, m.node_id);
/* join завершён — снять pending-состояние инвайтера и таймер ожидания */
if (cs->pending_invite_ch_id == channel_id && cs->pending_invite_node_id == m.node_id) {
if (cs->invite_send_timer) { uasync_cancel_timeout(cs->inst->ua, cs->invite_send_timer); cs->invite_send_timer = NULL; }
cs->pending_invite_ch_id = 0;
cs->pending_invite_node_id = 0;
cs->pending_invite_is_inviter = 0;
}
} else {
chat_join_forward_request(cs->inst, channel_id, inviter, join_key, &m);
}
cs_refresh_channels(cs);
cs_post_channel_online(cs, ch_id);
}
/* ─── JOIN_READY (0x13): connection-узел → joiner (можно запускать BGP) ─── */
static void cs_handle_join_ready(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len) {
if (cs->join_timer) { uasync_cancel_timeout(cs->inst->ua, cs->join_timer); cs->join_timer = NULL; }
if (len < 8) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_READY too short len=%zu peer=%016llx", CS_ID, len, (unsigned long long)peer); return; }
uint64_t group_id; memcpy(&group_id, pl, 8);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_READY ch=%s group=%016llx from=%016llx — joining complete, starting BGP",
CS_ID, ch_id, (unsigned long long)group_id, (unsigned long long)peer);
/* запуск BGP + полноценное участие в канале */
{
struct TOPO_GROUP* g = topo_groups_find(cs->inst->topo_groups, group_id);
if (g && g->group_type == TOPO_GROUP_TYPE_CHAT) {
struct ETCP_CONN* c = cs_find_conn_for_node(cs->inst, peer);
if (c) topo_group_new_conn(g, c);
}
}
chat_core_ensure_channel_ready(cs->inst, ch_id);
uint8_t evt[65]; uint8_t ch_id_len = (uint8_t)strlen(ch_id);
evt[0] = ch_id_len; memcpy(evt + 1, ch_id, ch_id_len);
chat_event_post(cs->inst, CHAT_EVT_CHANNEL_UPDATED, evt, 1 + ch_id_len);
cs_post_channel_online(cs, ch_id);
cs_resume_db_sync(cs, ch_id);
if (cs->pending_invite_ch_id != 0) {
uint64_t ch_id_num = strtoull(ch_id, NULL, 10);
uint8_t cevt[20]; memcpy(cevt, &peer, 8); int r = 0; memcpy(cevt + 8, &r, 4); memcpy(cevt + 12, &ch_id_num, 8);
chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, cevt, 20);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_READY invite success ch=%s peer=%016llx", CS_ID, ch_id, (unsigned long long)peer);
cs->pending_invite_node_id = 0; cs->pending_invite_ch_id = 0; cs->pending_join_key = 0;
}
}
/* ─── CHANNEL_INVITE (0x0F): inviter → joiner (push-приглашение в канал) ─── */
static void cs_handle_channel_invite(struct chat_sync* cs, uint64_t peer,
const char* ch_id, const uint8_t* pl, size_t len) {
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE ch=%s from=%016llx len=%zu",
CS_ID, ch_id, (unsigned long long)peer, len);
if (len < 1 + 8 + 32 + 32 + 64 + 1 + 64 + 8 + 1) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE too short len=%zu peer=%016llx", CS_ID, len, (unsigned long long)peer);
return;
}
/* Same payload format as CHANNEL_INFO_RESP */
const uint8_t* p = pl;
uint8_t nl = *p++;
if (1 + nl + 8 + 32 + 32 + 64 + 1 + 64 + 8 + 1 > (int)len) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE truncated", CS_ID); return; }
char name[128]; memcpy(name, p, nl); name[nl] = '\0'; p += nl;
uint64_t owner; memcpy(&owner, p, 8); p += 8;
const uint8_t* x25519 = p; p += 32;
const uint8_t* ed_pub = p; p += 32;
const uint8_t* ch_sig = p; p += 64;
uint8_t inv_flags = *p++;
const uint8_t* inviter_join_sig = NULL; uint64_t inviter_join_ts = 0;
if (inv_flags & PEERS_FLAG_HAS_JOIN) {
if (p + 72 > pl + len) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE truncated at join_sig/ts from=%016llx", CS_ID, (unsigned long long)peer); return; }
inviter_join_sig = p; p += 64; memcpy(&inviter_join_ts, p, 8); p += 8; }
/* skip inviter_update_sig/ts/userinfo for now — verify channel sig only */
p += 64; uint64_t inviter_update_ts; memcpy(&inviter_update_ts, p, 8); p += 8;
uint8_t userinfo_len = *p++; p += userinfo_len;
/* join-ключ (зарегистрирован инвайтером) — хвостовые 8 байт */
uint64_t join_key = 0;
if (p + 8 > pl + len) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE truncated (no join_key) from=%016llx", CS_ID, (unsigned long long)peer); return; }
memcpy(&join_key, p, 8); p += 8;
/* Verify channel signature */
uint8_t vmsg[1024]; size_t vlen = 0;
vlen += snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", ch_id) + 1;
vlen += snprintf((char*)vmsg + vlen, sizeof(vmsg) - vlen, "%s", name) + 1;
memcpy(vmsg + vlen, &owner, 8); vlen += 8;
memcpy(vmsg + vlen, x25519, 32); vlen += 32;
memcpy(vmsg + vlen, ed_pub, 32); vlen += 32;
if (cs_ed25519_verify(ed_pub, vmsg, vlen, ch_sig) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE invalid ch_sig ch=%s from=%016llx", CS_ID, ch_id, (unsigned long long)peer);
return;
}
/* Check join_policy: server always autojoin, GUI uses chat_setting */
int policy;
{
struct global_config *gcfg = &cs->inst->config->global;
policy = gcfg->chatserver_enabled ? 0 : chat_setting_get_int(cs->inst, "join_policy", 1);
}
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE policy=%d (chatserver=%d) ch=%s from=%016llx",
CS_ID, policy, cs->inst->config->global.chatserver_enabled, ch_id, (unsigned long long)peer);
if (policy == 2) { /* deny */
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE denied (policy=deny) ch=%s from=%016llx",
CS_ID, ch_id, (unsigned long long)peer);
return;
}
if (policy == 0) { /* autojoin */
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE autojoin ch=%s from=%016llx owner=%016llx name=%s",
CS_ID, ch_id, (unsigned long long)peer, (unsigned long long)owner, name);
/* Save channel and start join handshake */
int cprc = topo_node_sqlite_channel_put(cs->inst->topo_sqlite_db,
ch_id, name, owner, x25519, NULL, ed_pub, NULL, ch_sig);
uint64_t ch_x_prefix; memcpy(&ch_x_prefix, x25519, sizeof(ch_x_prefix));
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE channel_put rc=%d ch=%s ch_x25519=%016llx",
CS_ID, cprc, ch_id, (unsigned long long)ch_x_prefix);
chat_core_ensure_channel_ready(cs->inst, ch_id);
/* Initiate join: connect back and send JOIN_INFO_REQ с зарегистрированным ключом */
cs->pending_join_key = join_key;
chat_sync_join_channel(cs->inst, ch_id, peer);
return;
}
/* policy == ask — post event to GUI */
{
/* сохранить join-ключ, чтобы принять инвайт позже через chat_sync_accept_invite */
uint64_t ch_num = strtoull(ch_id, NULL, 10);
int slot = -1;
for (int i = 0; i < cs->pending_inv_in_count; i++)
if (cs->pending_inv_in_ch_id[i] == ch_num && cs->pending_inv_in_node_id[i] == peer) { slot = i; break; }
if (slot < 0 && cs->pending_inv_in_count < 4) slot = cs->pending_inv_in_count++;
if (slot >= 0) {
cs->pending_inv_in_ch_id[slot] = ch_num;
cs->pending_inv_in_node_id[slot] = peer;
cs->pending_inv_in_key[slot] = join_key;
}
size_t inv_name_len = 0; char inv_name[128] = "";
{ sqlite3* vdb = cs->inst->topo_sqlite_db;
sqlite3_stmt* st = NULL;
sqlite3_prepare_v2(vdb, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL);
if (st) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)peer);
if (sqlite3_step(st) == SQLITE_ROW) {
const unsigned char* nm = sqlite3_column_text(st, 0);
if (nm) { snprintf(inv_name, sizeof(inv_name), "%s", nm); inv_name_len = strlen(inv_name); }
}
sqlite3_finalize(st);
}
}
uint8_t evt[512]; int epos = 0;
uint8_t cl = (uint8_t)strlen(ch_id); evt[epos++] = cl; memcpy(evt + epos, ch_id, cl); epos += cl;
evt[epos++] = nl; memcpy(evt + epos, name, nl); epos += nl;
memcpy(evt + epos, &peer, 8); epos += 8;
evt[epos++] = (uint8_t)inv_name_len;
if (inv_name_len) { memcpy(evt + epos, inv_name, inv_name_len); epos += inv_name_len; }
chat_event_post(cs->inst, CHAT_EVT_INVITE_RECEIVED, evt, epos);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE ask posted to GUI ch=%s name=%s from=%016llx (%s)",
CS_ID, ch_id, name, (unsigned long long)peer, inv_name);
}
}
/* ─── helper: build and send CS_MSG_CHANNEL_INVITE to peer ─── */
static int cs_send_channel_invite(struct chat_sync* cs, struct UTUN_INSTANCE* inst,
const char* ch_id, uint64_t target_node_id,
const char* ch_name, uint64_t owner,
const uint8_t* ch_x25519, const uint8_t* ch_ed,
const uint8_t* ch_sig) {
uint8_t join_sig[64] = {0}; uint64_t join_ts = 0;
{ sqlite3* vdb = inst->topo_sqlite_db;
topo_node_sqlite_member_get_join(vdb, ch_id, inst->node_id, join_sig, &join_ts); }
uint8_t upd_sig[64] = {0}; uint64_t upd_ts = (uint64_t)ntp_time_get_seconds(inst);
{
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}", inst->name[0] ? inst->name : "");
uint8_t umsg[256];
int ulen = member_sync_build_update_msg(join_sig, upd_ts, juser, umsg, (int)sizeof(umsg));
if (ulen >= 0) cs_ed25519_sign(inst->my_ed25519_privkey, umsg, (size_t)ulen, upd_sig);
}
/* join-ключ: генерируем и регистрируем локально (инвайтер == connection-узел),
чтобы джойнер смог пройти JOIN_INFO_REQ с валидным ключом. */
uint64_t join_key = 0;
if (RAND_bytes((uint8_t*)&join_key, sizeof(join_key)) != 1 || join_key == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_key generation failed ch=%s", CS_ID, ch_id);
return -1;
}
if (chat_join_store_local_key(inst, strtoull(ch_id, NULL, 10), join_key) != 0) return -1;
uint8_t buf[1024]; size_t boff = 0;
buf[boff++] = CS_MSG_CHANNEL_INVITE;
uint8_t cnl = (uint8_t)strlen(ch_name);
buf[boff++] = cnl; memcpy(buf + boff, ch_name, cnl); boff += cnl;
memcpy(buf + boff, &owner, 8); boff += 8;
memcpy(buf + boff, ch_x25519, 32); boff += 32;
memcpy(buf + boff, ch_ed, 32); boff += 32;
memcpy(buf + boff, ch_sig, 64); boff += 64;
uint8_t inv_flags = (join_sig[0] || join_ts) ? PEERS_FLAG_HAS_JOIN : 0;
buf[boff++] = inv_flags;
if (inv_flags & PEERS_FLAG_HAS_JOIN) {
memcpy(buf + boff, join_sig, 64); boff += 64;
memcpy(buf + boff, &join_ts, 8); boff += 8;
}
memcpy(buf + boff, upd_sig, 64); boff += 64;
memcpy(buf + boff, &upd_ts, 8); boff += 8;
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}", inst->name[0] ? inst->name : "");
uint8_t il = (uint8_t)strlen(juser);
buf[boff++] = il; memcpy(buf + boff, juser, il); boff += il;
memcpy(buf + boff, &join_key, 8); boff += 8;
int rc = cs_send(cs, ch_id, target_node_id, buf, boff);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: sent CHANNEL_INVITE to 0x%016llx ch=%s key=%016llx rc=%d",
CS_ID, (unsigned long long)target_node_id, ch_id, (unsigned long long)join_key, rc);
return rc;
}
/* ─── chat_sync_invite_to_channel: пригласить узел в наш канал ─── */
void chat_sync_invite_to_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id) {
struct chat_sync* cs = cs_of(inst);
if (!cs) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel: cs=NULL, chat_sync not initialized", CS_ID); return; }
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel: inst=NULL", CS_ID); return; }
if (!ch_id || !ch_id[0]) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel: ch_id NULL/empty", CS_ID); return; }
if (target_node_id == 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel: target_node_id=0", CS_ID); return; }
if (target_node_id == inst->node_id) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel: self-invite rejected self=0x%016llx", CS_ID, (unsigned long long)inst->node_id); return; }
uint64_t channel_id = strtoull(ch_id, NULL, 10);
if (channel_id == 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel invalid ch_id='%s'", CS_ID, ch_id); return; }
/* Get channel data from DB */
char ch_name[128]; uint64_t owner; uint8_t ch_x25519[32], ch_ed[32], ch_sig[64];
if (topo_node_sqlite_channel_get(inst->topo_sqlite_db,
ch_id, ch_name, (int)sizeof(ch_name), &owner, ch_x25519, ch_ed, ch_sig) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel channel not found: %s", CS_ID, ch_id);
return;
}
/* Check if already connected */
struct ETCP_CONN* conn = cs_find_conn_for_node(inst, target_node_id);
if (!conn || !conn->links_up || !conn->initialized) {
/* Try to connect via direct (ncd) */
if (inst->topo_groups) {
struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, channel_id);
if (g) {
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(g, target_node_id);
if (nq && nq->handle) {
/* Already have a handle — connection in progress or established */
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel — existing handle for node 0x%016llx, waiting for conn_up",
CS_ID, (unsigned long long)target_node_id);
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
return;
}
}
/* Адреса грузятся из node_addresses в БД внутри node_conn_direct_open */
int r = topo_group_invite_to_channel(inst, channel_id, NULL, target_node_id);
if (r < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel topo_group_invite_to_channel failed rc=%d", CS_ID, r);
}
} else {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel — no topo_groups, cannot connect", CS_ID);
return;
}
/* Mark pending: after conn_up, we send CHANNEL_INVITE. */
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
cs->pending_invite_is_inviter = 1;
return;
}
/* Already connected — send invite now, wait for joiner to respond */
cs_send_channel_invite(cs, inst, ch_id, target_node_id, ch_name, owner, ch_x25519, ch_ed, ch_sig);
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
cs->pending_invite_is_inviter = 1;
if (cs->invite_send_timer) { uasync_cancel_timeout(inst->ua, cs->invite_send_timer); }
cs->invite_send_timer = uasync_set_timeout(inst->ua,
CS_INVITE_SEND_TIMEOUT_MS * 10, cs, cs_invite_send_timeout_cb, "cs_invite_send");
}
/* ─── chat_sync_invite_to_channel_with_addrs: как invite_to_channel, но с адресами ─── */
struct cs_invite_wrap {
struct UTUN_INSTANCE* inst;
uint64_t channel_id;
uint64_t target_node_id;
uint8_t pubkey[32];
int addr_count;
int addrs_len;
uint8_t addrs_data[]; /* flexible array */
};
static void cs_invite_trampoline(void* arg) {
struct cs_invite_wrap* w = (struct cs_invite_wrap*)arg;
struct chat_sync* cs = cs_of(w->inst);
if (!cs || !cs->inst) { u_free(w); return; }
struct UTUN_INSTANCE* inst = cs->inst;
uint64_t channel_id = w->channel_id;
uint64_t target_node_id = w->target_node_id;
if (channel_id == 0 || target_node_id == 0 || target_node_id == inst->node_id) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_with_addrs invalid params ch=%llu target=0x%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)target_node_id);
u_free(w); return;
}
char ch_id[64]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)channel_id);
/* Check if already connected */
struct ETCP_CONN* conn = cs_find_conn_for_node(inst, target_node_id);
if (conn && conn->links_up && conn->initialized) {
char ch_name[128]; uint8_t ch_x2[32], ch_ed2[32], ch_sig[64]; uint64_t ch_owner;
if (topo_node_sqlite_channel_get(inst->topo_sqlite_db,
ch_id, ch_name, (int)sizeof(ch_name), &ch_owner, ch_x2, ch_ed2, ch_sig) == 0) {
cs_send_channel_invite(cs, inst, ch_id, target_node_id, ch_name, ch_owner, ch_x2, ch_ed2, ch_sig);
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
cs->pending_invite_is_inviter = 1;
if (cs->invite_send_timer) { uasync_cancel_timeout(inst->ua, cs->invite_send_timer); }
cs->invite_send_timer = uasync_set_timeout(inst->ua,
CS_INVITE_SEND_TIMEOUT_MS * 10, cs, cs_invite_send_timeout_cb, "cs_invite_send");
}
u_free(w); return;
}
/* Not connected — build TOPO_NODE with addresses and connect */
if (!inst->topo_groups) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_with_addrs — no topo_groups, cannot connect", CS_ID);
u_free(w); return;
}
struct TOPO_NODE* ni = u_calloc(1, sizeof(*ni));
if (!ni) { u_free(w); return; }
ni->node_id = target_node_id; memcpy(ni->public_key, w->pubkey, 32);
const uint8_t* ap = w->addrs_data;
const uint8_t* ap_end = ap + w->addrs_len;
uint8_t r_short_id[8], r_pubkey[32], r_ver[3]; char r_sname[TOPO_REALITY_SNAME_MAX];
int has_reality = invite_reality_parse(&ap, ap_end, r_short_id, r_pubkey, r_ver, r_sname, sizeof(r_sname));
for (int i = 0; i < w->addr_count && ap + 1 <= ap_end; i++) {
uint8_t family = *ap++; uint8_t sock_id = *ap++; uint8_t proto = *ap++;
if (family == 4 && ap + 6 <= ap_end) {
struct TOPO_ADDR4* a4 = u_calloc(1, sizeof(*a4));
if (!a4) break;
memcpy(a4->addr, ap, 4); ap += 4;
a4->port = ((uint16_t)ap[0] << 8) | ap[1]; ap += 2;
a4->type = TOPO_ADDR_NAT; a4->protocol = proto; a4->socket_id = sock_id;
a4->next = ni->v4_addrs; ni->v4_addrs = a4;
if (has_reality && (proto & 0x04)) invite_reality_sock_append(ni, sock_id, r_short_id, r_pubkey, r_ver, r_sname);
} else if (family == 6 && ap + 18 <= ap_end) {
struct TOPO_ADDR6* a6 = u_calloc(1, sizeof(*a6));
if (!a6) break;
memcpy(a6->addr, ap, 16); ap += 16;
a6->port = ((uint16_t)ap[0] << 8) | ap[1]; ap += 2;
a6->type = TOPO_ADDR_NAT; a6->protocol = proto; a6->socket_id = sock_id;
a6->next = ni->v6_addrs; ni->v6_addrs = a6;
if (has_reality && (proto & 0x04)) invite_reality_sock_append(ni, sock_id, r_short_id, r_pubkey, r_ver, r_sname);
} else { break; }
}
if (!ni->v4_addrs && !ni->v6_addrs) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_with_addrs no valid addrs for node=0x%016llx",
CS_ID, (unsigned long long)target_node_id);
u_free(ni); u_free(w); return;
}
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
cs->pending_invite_is_inviter = 1;
int r = topo_group_invite_to_channel(inst, channel_id, ni, target_node_id);
if (r < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_with_addrs topo_group_invite_to_channel failed rc=%d", CS_ID, r);
cs->pending_invite_node_id = 0; cs->pending_invite_ch_id = 0;
cs->pending_invite_is_inviter = 0;
}
while (ni->v4_addrs) { struct TOPO_ADDR4* n = ni->v4_addrs->next; u_free(ni->v4_addrs); ni->v4_addrs = n; }
while (ni->v6_addrs) { struct TOPO_ADDR6* n = ni->v6_addrs->next; u_free(ni->v6_addrs); ni->v6_addrs = n; }
u_free(ni);
u_free(w);
}
void chat_sync_invite_to_channel_with_addrs(
struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id,
const uint8_t* pubkey_bin, const uint8_t* addrs_data, int addr_count,
int addrs_data_len)
{
struct chat_sync* cs = cs_of(inst);
if (!cs || !inst || !ch_id || !ch_id[0] || !pubkey_bin || !addrs_data || addr_count <= 0 || addrs_data_len <= 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_with_addrs invalid args", CS_ID);
return;
}
uint64_t channel_id = strtoull(ch_id, NULL, 10);
if (channel_id == 0 || target_node_id == 0 || target_node_id == inst->node_id) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_with_addrs invalid params ch=%s target=0x%016llx",
CS_ID, ch_id, (unsigned long long)target_node_id);
return;
}
struct cs_invite_wrap* w = u_calloc(1, sizeof(*w) + (size_t)addrs_data_len);
if (!w) return;
w->inst = inst;
w->channel_id = channel_id;
w->target_node_id = target_node_id;
memcpy(w->pubkey, pubkey_bin, 32);
w->addr_count = addr_count;
w->addrs_len = addrs_data_len;
memcpy(w->addrs_data, addrs_data, (size_t)addrs_data_len);
uasync_post(inst->ua, cs_invite_trampoline, w);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_with_addrs posted to uasync ch=%s target=0x%016llx addrs=%d",
CS_ID, ch_id, (unsigned long long)target_node_id, addr_count);
}
/* ─── accept/deny incoming invite ─── */
void chat_sync_accept_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t inviter_node_id) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !inst) return;
char ch_id[64]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)channel_id);
if (channel_id == 0 || inviter_node_id == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: accept_invite invalid params ch=%llu node=%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id);
return;
}
/* достать join-ключ, сохранённый при приёме CHANNEL_INVITE (ask) */
uint64_t join_key = 0;
for (int i = 0; i < cs->pending_inv_in_count; i++) {
if (cs->pending_inv_in_ch_id[i] == channel_id && cs->pending_inv_in_node_id[i] == inviter_node_id) {
join_key = cs->pending_inv_in_key[i];
break;
}
}
if (join_key == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: accept_invite no join_key for ch=%llu inviter=0x%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id);
return;
}
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: accept_invite ch=%llu inviter=0x%016llx key=%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id, (unsigned long long)join_key);
chat_core_ensure_channel_ready(inst, ch_id);
cs->pending_join_key = join_key;
chat_sync_join_channel(inst, ch_id, inviter_node_id);
}
void chat_sync_deny_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t inviter_node_id) {
(void)inst;
if (!cs_of(inst)) return;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: deny_invite ch=%llu inviter=0x%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id);
/* silently ignore — no response needed */
}