dm: прямой обмен сообщениями (DM) + soundtouch + архитектура звонков

- src/dm/: dm_core/dm_crypto/dm_mailbox — прямой обмен сообщениями (DM)
- lib/soundtouch-master/: vendored SoundTouch (изменение темпа/высоты аудио)
- doc/dm_arch.md, doc/chat_call_arch.md: архитектура DM и звонков
- tests: test_dm.c, test_dm_e2e.c
- chat: рефакторинг chat_* модулей, member_sync, invite_build
- chatgui/chatgui-android: синхронизация с новым API чата
- lib/u_async, debug_config, utun_instance: сопутствующие изменения
This commit is contained in:
evgeny committed 2026-09-15 21:09:17 +03:00
1 parent 761f64cf76
commit 73fe99b33f
123 files changed
+14095 -1270

No files matched your search

+6
View File
@@ -92,6 +92,9 @@ utun_CORE_SOURCES = \
chat/invite_link.c \
chat/invite_build.c \
chat/chat_headless_control.c \
dm/dm_core.c \
dm/dm_crypto.c \
dm/dm_mailbox.c \
broadcast.c
# libutun: all core sources except main()
@@ -183,6 +186,9 @@ libutun_a_SOURCES = \
chat/invite_link.c \
chat/invite_build.c \
chat/chat_headless_control.c \
dm/dm_core.c \
dm/dm_crypto.c \
dm/dm_mailbox.c \
broadcast.c
libutun_a_CFLAGS = $(utun_CFLAGS)
+11 -9
View File
@@ -49,7 +49,8 @@ static void admin_key_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
}
struct UTUN_INSTANCE* inst = conn->instance;
if (!inst) { queue_dgram_free(entry); queue_entry_free(entry); return; }
if (!g_cc.initialized || !g_cc.db) { queue_dgram_free(entry); queue_entry_free(entry); return; }
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db) { queue_dgram_free(entry); queue_entry_free(entry); return; }
const uint8_t* d = entry->dgram;
uint64_t from_node;
@@ -64,7 +65,7 @@ static void admin_key_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
/* проверить, что ключи соответствуют публичным ключам канала (защита от подлога) */
uint8_t ch_x_pub[32], ch_ed_pub[32];
if (topo_node_sqlite_channel_get(g_cc.db, ch_id, NULL, 0, NULL, ch_x_pub, ch_ed_pub, NULL) != 0) {
if (topo_node_sqlite_channel_get(cc->db, ch_id, NULL, 0, NULL, ch_x_pub, ch_ed_pub, NULL) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: recv — no channel ch=%s from 0x%016llx, drop",
CA_ID, ch_id, (unsigned long long)from_node);
queue_dgram_free(entry); queue_entry_free(entry); return;
@@ -80,7 +81,7 @@ static void admin_key_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
queue_dgram_free(entry); queue_entry_free(entry); return;
}
if (topo_node_sqlite_channel_set_privs(g_cc.db, ch_id, x_priv, ed_priv) != 0) {
if (topo_node_sqlite_channel_set_privs(cc->db, ch_id, x_priv, ed_priv) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: recv — set_privs FAILED ch=%s from 0x%016llx",
CA_ID, ch_id, (unsigned long long)from_node);
queue_dgram_free(entry); queue_entry_free(entry); return;
@@ -93,7 +94,7 @@ static void admin_key_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
uint8_t cl = (uint8_t)strlen(ch_id);
evt[0] = cl; memcpy(evt + 1, ch_id, cl);
memcpy(evt + 1 + cl, &from_node, 8);
chat_event_post(CHAT_EVT_ADMIN_KEY_RECEIVED, evt, 1 + cl + 8);
chat_event_post(inst, CHAT_EVT_ADMIN_KEY_RECEIVED, evt, 1 + cl + 8);
queue_dgram_free(entry); queue_entry_free(entry);
}
@@ -118,8 +119,9 @@ void chat_admin_destroy(struct UTUN_INSTANCE* inst) {
/* ─── передача прав (вызывается из uasync-потока) ─── */
void chat_core_transfer_admin(const char* ch_id, uint64_t target_node_id) {
if (!g_cc.initialized || !g_cc.inst || !g_cc.db) {
void chat_core_transfer_admin(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !inst || !cc->db) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: transfer — not initialized", CA_ID);
return;
}
@@ -130,7 +132,7 @@ void chat_core_transfer_admin(const char* ch_id, uint64_t target_node_id) {
}
uint8_t x_priv[32], ed_priv[32];
if (topo_node_sqlite_channel_get_privs(g_cc.db, ch_id, x_priv, ed_priv) != 0) {
if (topo_node_sqlite_channel_get_privs(cc->db, ch_id, x_priv, ed_priv) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: transfer — no channel privkey ch=%s (not owner?)", CA_ID, ch_id);
return;
}
@@ -154,7 +156,7 @@ void chat_core_transfer_admin(const char* ch_id, uint64_t target_node_id) {
memcpy(entry->dgram + 1, payload, sizeof(payload));
entry->len = 1 + (uint16_t)sizeof(payload);
int rc = etcp_route_send(g_cc.inst, group_id, target_node_id, entry, 1,
int rc = etcp_route_send(inst, group_id, target_node_id, entry, 1,
ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT);
if (rc != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: transfer — etcp_route_send FAILED rc=%d ch=%s → 0x%016llx",
@@ -169,6 +171,6 @@ void chat_core_transfer_admin(const char* ch_id, uint64_t target_node_id) {
void chat_core_transfer_admin_trampoline(void* arg) {
struct chat_transfer_admin_req* req = (struct chat_transfer_admin_req*)arg;
if (!req) return;
chat_core_transfer_admin(req->ch_id, req->target_node_id);
chat_core_transfer_admin(req->inst, req->ch_id, req->target_node_id);
u_free(req);
}
+78 -67
View File
@@ -56,22 +56,25 @@ static int channel_sign_join(const uint8_t* ch_x25519, const uint8_t* ch_ed25519
/* ─── подготовка инфраструктуры канала (db_sync instance) ─── */
void chat_core_ensure_channel_ready(const char* ch_id) {
if (!g_cc.initialized || !ch_id || !ch_id[0]) return;
if (si_find(ch_id)) return;
void chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !ch_id[0]) return;
if (si_find(inst, ch_id)) return;
char tbl_msg[80]; msg_table_name(ch_id, tbl_msg, sizeof(tbl_msg));
uint64_t gid = strtoull(ch_id, NULL, 10);
struct DB_SYNC_INSTANCE* si = db_sync_instance_add(g_cc.inst, tbl_msg, gid, 1);
struct DB_SYNC_INSTANCE* si = db_sync_instance_add(inst, tbl_msg, gid, 1);
if (gid != 0 && g_cc.inst->topo_groups && !topo_groups_find(g_cc.inst->topo_groups, gid)) {
struct TOPO_GROUP* g = topo_groups_create_group(g_cc.inst->topo_groups, gid, TOPO_GROUP_TYPE_CHAT, ch_id);
if (g) member_sync_subscribe_group(g_cc.inst, g);
if (gid != 0 && inst->topo_groups && !topo_groups_find(inst->topo_groups, gid)) {
struct TOPO_GROUP* g = topo_groups_create_group(inst->topo_groups, gid, TOPO_GROUP_TYPE_CHAT, ch_id);
if (g) member_sync_subscribe_group(inst, g);
}
if (si) {
si_register(si, ch_id);
db_sync_set_insert_cb(si, on_msg_inserted, u_strdup(ch_id));
si_register(inst, si, ch_id);
struct msg_insert_arg* ia = u_malloc(sizeof(*ia));
if (ia) { ia->inst = inst; snprintf(ia->ch_id, sizeof(ia->ch_id), "%s", ch_id); }
db_sync_set_insert_cb(si, on_msg_inserted, ia);
db_sync_instance_set_gated(si, 1);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: channel ready ch=%s tbl=%s group=0x%016llx",
CC_ID, ch_id, tbl_msg, (unsigned long long)gid);
@@ -83,8 +86,9 @@ void chat_core_ensure_channel_ready(const char* ch_id) {
/* ─── создание канала ─── */
void chat_core_create_channel(struct chat_channel_create* req) {
if (!g_cc.initialized) {
void chat_core_create_channel(struct UTUN_INSTANCE* inst, struct chat_channel_create* req) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: create_channel NOT INITIALIZED ch=%s",
CC_ID, req ? req->channel_id : "(null)");
return;
@@ -94,7 +98,7 @@ void chat_core_create_channel(struct chat_channel_create* req) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: create_channel BEGIN ch=%s name=%s",
CC_ID, req->channel_id, req->name);
int rc = topo_node_sqlite_channel_put(g_cc.db,
int rc = topo_node_sqlite_channel_put(cc->db,
req->channel_id, req->name, req->owner_node_id,
req->x25519_pubkey, req->x25519_privkey,
req->ed25519_pubkey, req->ed25519_privkey,
@@ -107,7 +111,7 @@ void chat_core_create_channel(struct chat_channel_create* req) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: channel_put OK ch=%s name=%s owner=0x%016llx",
CC_ID, req->channel_id, req->name, (unsigned long long)req->owner_node_id);
chat_core_ensure_channel_ready(req->channel_id);
chat_core_ensure_channel_ready(inst, req->channel_id);
uint8_t ch_id_len = (uint8_t)strlen(req->channel_id);
uint8_t data[65];
@@ -115,49 +119,53 @@ void chat_core_create_channel(struct chat_channel_create* req) {
memcpy(data + 1, req->channel_id, ch_id_len);
{
uint64_t myid = g_cc.inst->node_id;
uint64_t join_ts = (uint64_t)ntp_time_get_seconds(g_cc.inst);
uint64_t myid = inst->node_id;
uint64_t join_ts = (uint64_t)ntp_time_get_seconds(inst);
uint8_t join_sig[64];
channel_sign_join(req->x25519_pubkey, req->ed25519_pubkey,
myid, g_cc.inst->my_keys.public_key,
join_ts, g_cc.inst->my_ed25519_privkey, join_sig);
myid, inst->my_keys.public_key,
join_ts, inst->my_ed25519_privkey, join_sig);
char juser3[256]; snprintf(juser3, sizeof(juser3), "{\"name\":\"%s\"}",
g_cc.inst->name[0] ? g_cc.inst->name : "");
inst->name[0] ? inst->name : "");
/* корень дерева приглашений: подписан ключом группы (канальный ed25519) */
uint8_t root_sig[64]; memset(root_sig, 0, 64);
member_sync_sign_pubkey(req->ed25519_privkey, g_cc.inst->my_keys.public_key, root_sig);
int mrc = member_sync_put(g_cc.inst, req->channel_id, myid,
g_cc.inst->my_keys.public_key, g_cc.inst->my_ed25519_pubkey,
member_sync_sign_pubkey(req->ed25519_privkey, inst->my_keys.public_key, root_sig);
int mrc = member_sync_put(inst, req->channel_id, myid,
inst->my_keys.public_key, inst->my_ed25519_pubkey,
join_sig, join_ts, NULL, 0, juser3, NULL, NULL, 0, 0, root_sig);
if (mrc < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member_sync_put(self) FAILED ch=%s rc=%d",
CC_ID, req->channel_id, mrc);
} else if (mrc & MS_APPLY_CHANGED) {
member_sync_broadcast_one(g_cc.inst, req->channel_id, myid);
member_sync_broadcast_one(inst, req->channel_id, myid);
}
/* адреса + update_ts + подпись + синк — единой функцией */
chat_core_update_my_member();
chat_core_update_my_member(inst);
}
chat_event_post(CHAT_EVT_CHANNEL_UPDATED, data, 1 + ch_id_len);
chat_event_post(inst, CHAT_EVT_CHANNEL_UPDATED, data, 1 + ch_id_len);
}
}
void chat_core_create_channel_trampoline(void* arg) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "chat_core: TRAMPOLINE invoked arg=%p", arg);
struct chat_channel_create* req = (struct chat_channel_create*)arg;
chat_core_create_channel(req);
if (!req) return;
chat_core_create_channel(req->inst, req);
u_free(req);
}
void chat_core_create_channel_auto_trampoline(void* arg) {
chat_core_create_channel_auto((const char*)arg);
struct chat_create_auto_arg* a = (struct chat_create_auto_arg*)arg;
if (!a) return;
chat_core_create_channel_auto(a->inst, a->name);
u_free(arg);
}
void chat_core_create_channel_auto(const char* name) {
if (!g_cc.initialized || !name) {
void chat_core_create_channel_auto(struct UTUN_INSTANCE* inst, const char* name) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !name) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: create_channel_auto — not initialized or NULL name", CC_ID);
return;
}
@@ -189,7 +197,7 @@ void chat_core_create_channel_auto(const char* name) {
uint8_t* p = msg;
memcpy(p, channelId, chIdLen); p += chIdLen; *p++ = '\0';
memcpy(p, name, nameLen); p += nameLen; *p++ = '\0';
memcpy(p, &g_cc.my_node_id, 8); p += 8;
memcpy(p, &cc->my_node_id, 8); p += 8;
memcpy(p, x25519_pub, 32); p += 32;
memcpy(p, ed25519_pub, 32);
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
@@ -201,31 +209,33 @@ void chat_core_create_channel_auto(const char* name) {
struct chat_channel_create* req = u_calloc(1, sizeof(*req));
if (!req) return;
req->inst = inst;
snprintf(req->channel_id, sizeof(req->channel_id), "%s", channelId);
snprintf(req->name, sizeof(req->name), "%s", name);
req->owner_node_id = g_cc.my_node_id;
req->owner_node_id = cc->my_node_id;
memcpy(req->x25519_pubkey, x25519_pub, 32); memcpy(req->x25519_privkey, x25519_priv, 32);
memcpy(req->ed25519_pubkey, ed25519_pub, 32); memcpy(req->ed25519_privkey, ed25519_priv, 32);
memcpy(req->signature, sig, 64);
chat_core_create_channel(req);
chat_core_create_channel(inst, req);
u_free(req);
}
/* ─── управление подключением к каналу (выбор в GUI) ─── */
void chat_core_connect_channel(const char* ch_id) {
DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel called ch_id=%s initialized=%d", CC_ID, ch_id ? ch_id : "(null)", g_cc.initialized);
if (!g_cc.initialized || !ch_id || !ch_id[0]) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel — skip (not ready)", CC_ID); return; }
if (!g_cc.inst->topo_groups) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel — no topo_groups", CC_ID); return; }
void chat_core_connect_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = CC(inst);
DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel called ch_id=%s initialized=%d", CC_ID, ch_id ? ch_id : "(null)", cc ? cc->initialized : 0);
if (!cc || !cc->initialized || !ch_id || !ch_id[0]) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel — skip (not ready)", CC_ID); return; }
if (!inst->topo_groups) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel — no topo_groups", CC_ID); return; }
uint64_t gid = strtoull(ch_id, NULL, 10);
if (gid == 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel — invalid ch_id=%s", CC_ID, ch_id); return; }
chat_core_ensure_channel_ready(ch_id);
chat_core_ensure_channel_ready(inst, ch_id);
struct TOPO_GROUP* group = topo_groups_find(g_cc.inst->topo_groups, gid);
if (!group) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel — group not found ch=%s gid=%016llx total_groups=%d", CC_ID, ch_id, (unsigned long long)gid, queue_entry_count(g_cc.inst->topo_groups->group_list)); return; }
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, gid);
if (!group) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel — group not found ch=%s gid=%016llx total_groups=%d", CC_ID, ch_id, (unsigned long long)gid, queue_entry_count(inst->topo_groups->group_list)); return; }
if (group->connect) {
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel — already in progress ch=%s active=%d",
@@ -233,7 +243,7 @@ void chat_core_connect_channel(const char* ch_id) {
return;
}
if (!chat_setting_get_int("group_autoconnect", 1)) {
if (!chat_setting_get_int(inst, "group_autoconnect", 1)) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel ch=%s — group_autoconnect disabled, skip auto-connect loop",
CC_ID, ch_id);
return;
@@ -244,29 +254,30 @@ void chat_core_connect_channel(const char* ch_id) {
}
void chat_core_connect_channel_trampoline(void* arg) {
char* ch_id = (char*)arg;
DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel_trampoline ch_id=%s", CC_ID, ch_id ? ch_id : "(null)");
if (!ch_id) return;
chat_core_connect_channel(ch_id);
u_free(ch_id);
struct chat_create_auto_arg* a = (struct chat_create_auto_arg*)arg;
DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel_trampoline ch_id=%s", CC_ID, a ? a->name : "(null)");
if (!a) return;
chat_core_connect_channel(a->inst, a->name);
u_free(a);
}
/* ─── однократное подключение к конкретному узлу канала (GUI → uasync) ─── */
void chat_core_connect_node(const char* ch_id, uint64_t node_id) {
if (!g_cc.initialized || !g_cc.inst || !ch_id || !ch_id[0] || !node_id) {
void chat_core_connect_node(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !inst || !ch_id || !ch_id[0] || !node_id) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_node — skip (not ready or invalid args)", CC_ID);
return;
}
if (node_id == g_cc.inst->node_id) {
if (node_id == inst->node_id) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_node — skip self node=0x%016llx", CC_ID, (unsigned long long)node_id);
return;
}
chat_core_ensure_channel_ready(ch_id);
chat_core_ensure_channel_ready(inst, ch_id);
uint64_t gid = strtoull(ch_id, NULL, 10);
struct TOPO_GROUP* group = topo_groups_find(g_cc.inst->topo_groups, gid);
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, gid);
if (!group) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_node — group not found ch=%s", CC_ID, ch_id);
return;
@@ -278,15 +289,14 @@ void chat_core_connect_node(const char* ch_id, uint64_t node_id) {
void chat_core_connect_node_trampoline(void* arg) {
struct chat_connect_node_req* r = (struct chat_connect_node_req*)arg;
if (!r) return;
chat_core_connect_node(r->ch_id, r->node_id);
chat_core_connect_node(r->inst, r->ch_id, r->node_id);
u_free(r);
}
/* ─── применение настройки group_autoconnect ко всем CHAT-группам ─── */
void chat_core_apply_group_autoconnect(void) {
int enabled = chat_setting_get_int("group_autoconnect", 1);
struct UTUN_INSTANCE* inst = g_cc.inst;
void chat_core_apply_group_autoconnect(struct UTUN_INSTANCE* inst) {
int enabled = chat_setting_get_int(inst, "group_autoconnect", 1);
if (!inst || !inst->topo_groups || !inst->topo_groups->group_list) return;
struct ll_entry* e = inst->topo_groups->group_list->head;
@@ -306,11 +316,11 @@ void chat_core_apply_group_autoconnect(void) {
/* ─── локальное удаление канала ─── */
static const char* cc_ui_state_get(const char* key) {
static const char* cc_ui_state_get(struct chat_core_ctx* cc, const char* key) {
static char val[512];
val[0] = '\0';
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT value FROM ui_state WHERE key=?", -1, &st, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, "SELECT value FROM ui_state WHERE key=?", -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC);
if (sqlite3_step(st) == SQLITE_ROW) {
const char* v = (const char*)sqlite3_column_text(st, 0);
@@ -321,8 +331,9 @@ static const char* cc_ui_state_get(const char* key) {
return val;
}
void chat_core_delete_channel(const char* ch_id) {
if (!g_cc.initialized || !g_cc.db || !g_cc.inst) {
void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db || !inst) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel — not initialized", CC_ID);
return;
}
@@ -332,35 +343,35 @@ void chat_core_delete_channel(const char* ch_id) {
uint64_t gid = strtoull(ch_id, NULL, 10);
/* 1. отмена медиа-загрузок/релэев/суперузлов канала (держат conn_mgr handle группы) */
media_delivery_cancel_channel(g_cc.inst, gid);
media_delivery_cancel_channel(inst, gid);
/* 2. снять merkle-сессии этого namespace */
member_sync_cancel_channel(g_cc.inst, ch_id);
member_sync_cancel_channel(inst, ch_id);
/* 3. удалить группу (conn_mgr → закрытие соединений, connect-цикл, broadcast, recovery) */
if (g_cc.inst->topo_groups)
topo_groups_remove_group(g_cc.inst->topo_groups, gid);
if (inst->topo_groups)
topo_groups_remove_group(inst->topo_groups, gid);
/* 4. удалить db_sync инстанс (освобождает память, отменяет таймеры) */
{
struct DB_SYNC_INSTANCE* si = si_find(ch_id);
struct DB_SYNC_INSTANCE* si = si_find(inst, ch_id);
if (si) db_sync_instance_remove(si);
}
/* 5. немедленно убрать канал из кеша chat_sync */
chat_sync_on_channel_deleted(g_cc.inst, ch_id);
chat_sync_on_channel_deleted(inst, ch_id);
/* 6. удалить медиафайлы канала на диске + строки media_files */
media_index_delete_channel(g_cc.db, ch_id, cc_ui_state_get("media_base"));
media_index_delete_channel(cc->db, ch_id, cc_ui_state_get(cc, "media_base"));
/* 7. удалить таблицы и метаданные канала */
topo_node_sqlite_channel_delete(g_cc.db, ch_id);
topo_node_sqlite_channel_delete(cc->db, ch_id);
/* 8. уведомить GUI */
{
uint8_t evt[65]; uint8_t cl = (uint8_t)strlen(ch_id);
evt[0] = cl; memcpy(evt + 1, ch_id, cl);
chat_event_post(CHAT_EVT_CHANNEL_DELETED, evt, 1 + cl);
chat_event_post(inst, CHAT_EVT_CHANNEL_DELETED, evt, 1 + cl);
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel DONE ch=%s gid=0x%016llx", CC_ID, ch_id, (unsigned long long)gid);
@@ -369,6 +380,6 @@ void chat_core_delete_channel(const char* ch_id) {
void chat_core_delete_channel_trampoline(void* arg) {
struct chat_delete_channel_req* req = (struct chat_delete_channel_req*)arg;
if (!req) return;
chat_core_delete_channel(req->ch_id);
chat_core_delete_channel(req->inst, req->ch_id);
u_free(req);
}
+92 -91
View File
@@ -1,5 +1,5 @@
/*
* chat_core.c — жизненный цикл чата: init, destroy, глобальное состояние g_cc
* chat_core.c — жизненный цикл чата: init, destroy, per-instance состояние (inst->chat_core)
*
* Под-модули (каждый в своём .c):
* chat_msg.c — отправка сообщений, DB-операции для chat_sync
@@ -28,10 +28,6 @@
#include <stdio.h>
#include "../../lib/platform_compat.h"
/* ─── глобальное состояние (extern в chat_core_priv.h) ─── */
struct chat_core_ctx g_cc;
/* ─── хелперы ─── */
void sanitize_ch_id(const char* ch_id, char* out, size_t out_sz) {
@@ -56,9 +52,9 @@ void peers_table_name(const char* ch_id, char* buf, size_t sz) {
snprintf(buf, sz, "peers_%s", san);
}
int db_exec(const char* sql) {
static int db_exec(struct chat_core_ctx* cc, const char* sql) {
char* err = NULL;
int rc = sqlite3_exec(g_cc.db, sql, NULL, NULL, &err);
int rc = sqlite3_exec(cc->db, sql, NULL, NULL, &err);
if (rc != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: sql error: %s", CC_ID, err);
sqlite3_free(err);
@@ -68,12 +64,12 @@ int db_exec(const char* sql) {
/* ─── db_sync registry ─── */
struct DB_SYNC_INSTANCE* si_find(const char* ch_id) {
if (!g_cc.inst || !g_cc.inst->db_sync || !ch_id || !ch_id[0]) return NULL;
return db_sync_instance_find(g_cc.inst, strtoull(ch_id, NULL, 10));
struct DB_SYNC_INSTANCE* si_find(struct UTUN_INSTANCE* inst, const char* ch_id) {
if (!inst || !inst->db_sync || !ch_id || !ch_id[0]) return NULL;
return db_sync_instance_find(inst, strtoull(ch_id, NULL, 10));
}
void si_register(struct DB_SYNC_INSTANCE* si, const char* ch_id) {
void si_register(struct UTUN_INSTANCE* inst, struct DB_SYNC_INSTANCE* si, const char* ch_id) {
uint32_t msg_cnt = db_sync_count(si);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: si_register ch=%s mc=%u", CC_ID, ch_id, msg_cnt);
}
@@ -82,40 +78,47 @@ void si_register(struct DB_SYNC_INSTANCE* si, const char* ch_id) {
int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
if (!inst || !db_path) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid args inst=%p db_path=%p", CC_ID, (void*)inst, (void*)db_path); return -1; }
memset(&g_cc, 0, sizeof(g_cc));
snprintf(g_cc.db_path, sizeof(g_cc.db_path), "%s", db_path);
struct chat_core_ctx* cc = u_calloc(1, sizeof(*cc));
if (!cc) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: OOM for chat_core_ctx", CC_ID); return -1; }
inst->chat_core = cc;
snprintf(cc->db_path, sizeof(cc->db_path), "%s", db_path);
if (inst->topo_sqlite_db) {
g_cc.db = inst->topo_sqlite_db; g_cc.shared_db = 1;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: using shared SQLite db=%p", CC_ID, (void*)g_cc.db);
cc->db = inst->topo_sqlite_db; cc->shared_db = 1;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: using shared SQLite db=%p", CC_ID, (void*)cc->db);
} else {
int rc = sqlite3_open_v2(db_path, &g_cc.db,
int rc = sqlite3_open_v2(db_path, &cc->db,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL);
if (rc != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: cannot open DB %s: %s",
CC_ID, db_path, sqlite3_errmsg(g_cc.db));
sqlite3_close(g_cc.db); g_cc.db = NULL; return -1;
CC_ID, db_path, sqlite3_errmsg(cc->db));
sqlite3_close(cc->db); inst->chat_core = NULL; u_free(cc); return -1;
}
sqlite3_exec(g_cc.db, "PRAGMA journal_mode=WAL", NULL, NULL, NULL);
sqlite3_exec(g_cc.db, "PRAGMA foreign_keys=ON", NULL, NULL, NULL);
sqlite3_exec(cc->db, "PRAGMA journal_mode=WAL", NULL, NULL, NULL);
sqlite3_exec(cc->db, "PRAGMA foreign_keys=ON", NULL, NULL, NULL);
}
g_cc.inst = inst;
g_cc.my_node_id = inst->node_id;
cc->inst = inst;
cc->my_node_id = inst->node_id;
/* перенести chat-настройки из конфига (заполнены парсером) в per-instance состояние */
if (inst->config) memcpy(&inst->chat_settings, &inst->config->global.chat_settings, sizeof(inst->chat_settings));
sqlite3_int64 now_sec = (sqlite3_int64)ntp_time_get_seconds(inst);
/* записать себя в nodes + local_identity */
{
const char* my_name = inst->name[0] ? inst->name : "Me";
topo_node_sqlite_node_update_verified(g_cc.db, g_cc.my_node_id,
topo_node_sqlite_node_update_verified(cc->db, cc->my_node_id,
my_name, inst->my_keys.public_key, inst->my_ed25519_pubkey,
(uint64_t)now_sec, (time_t)now_sec);
sqlite3_stmt* st = NULL;
sqlite3_prepare_v2(g_cc.db,
sqlite3_prepare_v2(cc->db,
"INSERT OR REPLACE INTO local_identity(id,node_id,name,x25519_pubkey,ed25519_pubkey,created_at,updated_at)"
" VALUES(1,?,?,?,?,?,?)", -1, &st, NULL);
if (st) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)g_cc.my_node_id);
sqlite3_bind_int64(st, 1, (sqlite3_int64)cc->my_node_id);
sqlite3_bind_text(st, 2, my_name, -1, SQLITE_STATIC);
sqlite3_bind_blob(st, 3, inst->my_keys.public_key, 32, SQLITE_STATIC);
sqlite3_bind_blob(st, 4, inst->my_ed25519_pubkey, 32, SQLITE_STATIC);
@@ -125,15 +128,15 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
}
}
g_cc.initialized = 1;
cc->initialized = 1;
/* register adm_tags change callback → CHAT_EVT_MEMBER_UPDATED */
chat_member_init();
chat_member_init(inst);
/* create media_files table */
media_index_init(g_cc.db);
media_index_init(cc->db);
db_exec(
db_exec(cc,
"CREATE TABLE IF NOT EXISTS local_identity ("
" id INTEGER PRIMARY KEY CHECK (id = 1),"
" node_id INTEGER NOT NULL UNIQUE,"
@@ -167,14 +170,14 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
else snprintf(mbase, sizeof(mbase), "%s", db_path);
char sql[1024]; snprintf(sql, sizeof(sql),
"INSERT OR REPLACE INTO ui_state(key,value) VALUES('media_base','%s')", mbase);
sqlite3_exec(g_cc.db, sql, NULL, NULL, NULL);
sqlite3_exec(cc->db, sql, NULL, NULL, NULL);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: media_base=%s", CC_ID, mbase);
}
/* seed stub accounts if empty */
{
sqlite3_stmt* cnt_st = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT COUNT(*) FROM accounts",
if (sqlite3_prepare_v2(cc->db, "SELECT COUNT(*) FROM accounts",
-1, &cnt_st, NULL) == SQLITE_OK) {
if (sqlite3_step(cnt_st) == SQLITE_ROW && sqlite3_column_int(cnt_st, 0) == 0) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: seeding 5 stub accounts", CC_ID);
@@ -185,7 +188,7 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
};
for (int i = 0; i < 5; i++) {
sqlite3_stmt* ins = NULL;
sqlite3_prepare_v2(g_cc.db,
sqlite3_prepare_v2(cc->db,
"INSERT OR IGNORE INTO accounts(node_id,display_name,avatar_color,avatar_letter,created_at)"
" VALUES(?,?,?,?,?)", -1, &ins, NULL);
if (ins) {
@@ -205,12 +208,12 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
/* ensure infrastructure for all existing channels (survives restart) */
{
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT channel_id FROM channels ORDER BY created_at ASC",
if (sqlite3_prepare_v2(cc->db, "SELECT channel_id FROM channels ORDER BY created_at ASC",
-1, &stmt, NULL) == SQLITE_OK) {
int loaded = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* ch = (const char*)sqlite3_column_text(stmt, 0);
if (ch && ch[0]) { chat_core_ensure_channel_ready(ch); loaded++; }
if (ch && ch[0]) { chat_core_ensure_channel_ready(inst, ch); loaded++; }
}
sqlite3_finalize(stmt);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: loaded %d channels from DB", CC_ID, loaded);
@@ -218,53 +221,46 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: initialized, db=%s node_id=0x%016llx",
CC_ID, db_path, (unsigned long long)g_cc.my_node_id);
CC_ID, db_path, (unsigned long long)cc->my_node_id);
{ uint8_t nid[8]; memcpy(nid, &g_cc.my_node_id, 8); chat_event_post(CHAT_EVT_MY_NODE_ID, nid, 8); }
{ uint8_t nid[8]; memcpy(nid, &cc->my_node_id, 8); chat_event_post(inst, CHAT_EVT_MY_NODE_ID, nid, 8); }
chat_event_post(CHAT_EVT_DB_READY, NULL, 0);
chat_event_post(inst, CHAT_EVT_DB_READY, NULL, 0);
return 0;
}
void chat_core_destroy(struct UTUN_INSTANCE* inst) {
(void)inst;
if (!g_cc.initialized) return;
g_cc.initialized = 0;
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized) return;
cc->initialized = 0;
if (g_cc.db && !g_cc.shared_db) { sqlite3_close(g_cc.db); }
g_cc.db = NULL; g_cc.inst = NULL;
if (cc->db && !cc->shared_db) { sqlite3_close(cc->db); }
inst->chat_core = NULL;
u_free(cc);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: destroyed", CC_ID);
}
sqlite3* chat_core_get_db(void) { return g_cc.db; }
struct UTUN_INSTANCE* chat_core_get_inst(void) { return g_cc.inst; }
int chat_core_is_initialized(void) { return g_cc.initialized; }
sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst) { return CC(inst) ? CC(inst)->db : NULL; }
int chat_core_is_initialized(struct UTUN_INSTANCE* inst) { return CC(inst) ? CC(inst)->initialized : 0; }
/* ─── chat settings (GUI → uasync) ─── */
void chat_core_set_setting(const char* name, const char* value) {
chat_setting_set(name, value);
if (name && strcmp(name, "group_autoconnect") == 0) chat_core_apply_group_autoconnect();
void chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value) {
chat_setting_set(inst, name, value);
if (name && strcmp(name, "group_autoconnect") == 0) chat_core_apply_group_autoconnect(inst);
}
struct chat_setting_arg { char data[256]; };
void chat_core_set_setting_trampoline(void* arg) {
struct chat_setting_arg* a = (struct chat_setting_arg*)arg;
const char* name = a->data;
const char* value = name + strlen(name) + 1;
chat_core_set_setting(name, value);
chat_core_set_setting(a->inst, name, value);
u_free(arg);
}
/* ─── headless API ─── */
void chat_core_set_ctx_for_headless(sqlite3* db, struct UTUN_INSTANCE* inst, uint64_t my_node_id) {
if (!g_cc.initialized) g_cc.initialized = 1;
g_cc.db = db; g_cc.inst = inst; g_cc.my_node_id = my_node_id;
g_cc.shared_db = 1;
}
static void json_escape(const char* src, char* dst, size_t dst_sz) {
size_t i = 0; const char* s = src;
while (*s && i < dst_sz - 2) {
@@ -274,12 +270,13 @@ static void json_escape(const char* src, char* dst, size_t dst_sz) {
dst[i] = '\0';
}
int chat_core_get_channels_json(char* buf, size_t buf_size, size_t* out_len) {
if (!g_cc.initialized || !buf || !out_len) return -1;
int chat_core_get_channels_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !buf || !out_len) return -1;
char* w = buf; char* end = buf + buf_size;
*w++ = '['; const char* sep = "";
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db,
if (sqlite3_prepare_v2(cc->db,
"SELECT c.channel_id, c.name, c.owner_node_id, "
"(SELECT COUNT(*) FROM nodes WHERE node_id=c.owner_node_id AND name IS NOT NULL) AS has_owner_name, "
"(SELECT name FROM nodes WHERE node_id=c.owner_node_id) AS owner_name "
@@ -292,16 +289,16 @@ int chat_core_get_channels_json(char* buf, size_t buf_size, size_t* out_len) {
const char* owner_name = (const char*)sqlite3_column_text(st, 4);
if (!ch_id) continue;
uint32_t msg_count = chat_core_count(ch_id);
uint32_t msg_count = chat_core_count(inst, ch_id);
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
int peer_count = 0, online_count = 0;
sqlite3_stmt* ps = NULL;
char psql[200]; snprintf(psql, sizeof(psql), "SELECT node_id FROM \"%s\" WHERE deleted=0", peers_tbl);
if (sqlite3_prepare_v2(g_cc.db, psql, -1, &ps, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, psql, -1, &ps, NULL) == SQLITE_OK) {
while (sqlite3_step(ps) == SQLITE_ROW) {
uint64_t pid = (uint64_t)sqlite3_column_int64(ps, 0);
peer_count++;
if (chat_core_member_online(ch_id, pid)) online_count++;
if (chat_core_member_online(inst, ch_id, pid)) online_count++;
}
sqlite3_finalize(ps);
}
@@ -323,9 +320,10 @@ int chat_core_get_channels_json(char* buf, size_t buf_size, size_t* out_len) {
return 0;
}
int chat_core_get_messages_json(const char* ch_id, int count, int offset,
int chat_core_get_messages_json(struct UTUN_INSTANCE* inst, const char* ch_id, int count, int offset,
char* buf, size_t buf_size, size_t* out_len) {
if (!g_cc.initialized || !ch_id || !buf || !out_len) return -1;
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !buf || !out_len) return -1;
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
char* w = buf; char* end = buf + buf_size;
*w++ = '['; const char* sep = "";
@@ -335,7 +333,7 @@ int chat_core_get_messages_json(const char* ch_id, int count, int offset,
"FROM \"%s\" m LEFT JOIN nodes n ON m.node_id=n.node_id "
"ORDER BY m.timestamp DESC LIMIT ? OFFSET ?", tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; }
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; }
sqlite3_bind_int(st, 1, count > 0 ? count : 100);
sqlite3_bind_int(st, 2, offset);
while (sqlite3_step(st) == SQLITE_ROW) {
@@ -367,8 +365,9 @@ int chat_core_get_messages_json(const char* ch_id, int count, int offset,
return 0;
}
int chat_core_get_members_json(const char* ch_id, char* buf, size_t buf_size, size_t* out_len) {
if (!g_cc.initialized || !ch_id || !buf || !out_len) return -1;
int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !buf || !out_len) return -1;
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
char* w = buf; char* end = buf + buf_size;
*w++ = '['; const char* sep = "";
@@ -377,11 +376,11 @@ int chat_core_get_members_json(const char* ch_id, char* buf, size_t buf_size, si
"SELECT p.node_id, n.name, n.x25519_pubkey, n.ed25519_pubkey, p.adm_tags "
"FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id", peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; }
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; }
while (sqlite3_step(st) == SQLITE_ROW) {
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
const char* name = (const char*)sqlite3_column_text(st, 1);
int online = chat_core_member_online(ch_id, nid);
int online = chat_core_member_online(inst, ch_id, nid);
const uint8_t* x25519 = (const uint8_t*)sqlite3_column_blob(st, 2);
const uint8_t* ed25519 = sqlite3_column_blob(st, 3);
int x25519_len = sqlite3_column_bytes(st, 2);
@@ -395,8 +394,8 @@ int chat_core_get_members_json(const char* ch_id, char* buf, size_t buf_size, si
/* check connected */
int connected = 0;
if (g_cc.inst && g_cc.inst->connections) {
struct ll_entry* e = queue_find_data_by_index(g_cc.inst->connections, (const uint8_t*)&nid);
if (inst && inst->connections) {
struct ll_entry* e = queue_find_data_by_index(inst->connections, (const uint8_t*)&nid);
if (e) connected = 1;
}
@@ -412,7 +411,7 @@ int chat_core_get_members_json(const char* ch_id, char* buf, size_t buf_size, si
sqlite3_stmt* as = NULL;
char asql[200]; snprintf(asql, sizeof(asql),
"SELECT family, protocol, address, port, rtt FROM node_addresses WHERE node_id=? ORDER BY family, protocol");
if (sqlite3_prepare_v2(g_cc.db, asql, -1, &as, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, asql, -1, &as, NULL) == SQLITE_OK) {
sqlite3_bind_int64(as, 1, (sqlite3_int64)nid);
const char* asep2 = "";
while (sqlite3_step(as) == SQLITE_ROW) {
@@ -446,11 +445,12 @@ int chat_core_get_members_json(const char* ch_id, char* buf, size_t buf_size, si
return 0;
}
int chat_core_get_node_name(uint64_t node_id, char* out, size_t sz) {
if (!g_cc.initialized || !out) return -1;
int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !out) return -1;
out[0] = '\0';
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL) != SQLITE_OK) return -1;
if (sqlite3_prepare_v2(cc->db, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
const char* n = (const char*)sqlite3_column_text(st, 0);
@@ -497,8 +497,8 @@ static int tags_is_append_key(const char* key) {
static void tags_set_effective_val(struct chat_member_tags_key* k, const char* raw) {
uint64_t ts = 0;
const char* eff = tags_append_effective(raw, &ts);
if (ts == 0) ts = (uint64_t)ntp_time_get_seconds(g_cc.inst);
snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s:%llu", eff, (unsigned long long)ts);
snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", eff);
(void)ts;
}
static int tags_json_cb(const char* key, const char* json_val, void* arg) {
@@ -508,13 +508,13 @@ static int tags_json_cb(const char* key, const char* json_val, void* arg) {
snprintf(k->key, CHAT_MEMBER_TAGS_KEY_SZ, "%s", key);
snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", json_val);
k->raw = tags_is_append_key(key) ? u_strdup(json_val) : NULL;
if (k->raw) tags_set_effective_val(k, json_val);
t->key_count++;
return 0;
}
struct chat_member_tags* chat_member_tags_load(const char* ch_id, uint64_t node_id) {
if (!g_cc.initialized || !ch_id || !g_cc.db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_load — not initialized", CC_ID); return NULL; }
struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !cc->db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_load — not initialized", CC_ID); return NULL; }
struct chat_member_tags* t = u_calloc(1, sizeof(*t));
if (!t) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_load — OOM", CC_ID); return NULL; }
snprintf(t->ch_id, sizeof(t->ch_id), "%s", ch_id);
@@ -523,7 +523,7 @@ struct chat_member_tags* chat_member_tags_load(const char* ch_id, uint64_t node_
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
sqlite3_stmt* st = NULL;
char sql[256]; snprintf(sql, sizeof(sql), "SELECT adm_tags FROM \"%s\" WHERE node_id=?", peers_tbl);
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
const char* tags = (const char*)sqlite3_column_text(st, 0);
@@ -576,8 +576,9 @@ int chat_member_tags_replace(struct chat_member_tags* t, const char* json) {
return 0;
}
int chat_member_tags_commit(struct chat_member_tags* t) {
if (!t || !g_cc.initialized || !g_cc.inst || !g_cc.db) {
int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* t) {
struct chat_core_ctx* cc = CC(inst);
if (!t || !cc || !cc->initialized || !cc->inst || !cc->db) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — invalid state", CC_ID); return -1;
}
char json[4096]; int off = 0, ver = 1;
@@ -597,7 +598,7 @@ int chat_member_tags_commit(struct chat_member_tags* t) {
int old_yes = (*old_eff == 'y');
int new_yes = (k->val[0] == 'y'); /* val = "yes" or "yes:<ts>" */
if (new_yes != old_yes) {
uint64_t ts = (uint64_t)ntp_time_get_seconds(g_cc.inst);
uint64_t ts = (uint64_t)ntp_time_get_seconds(cc->inst);
size_t oldl = strlen(k->raw);
char* nraw = u_malloc(oldl + 24);
if (!nraw) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — OOM for raw", CC_ID); return -1; }
@@ -616,7 +617,7 @@ int chat_member_tags_commit(struct chat_member_tags* t) {
#undef TAGS_ADVANCE
uint8_t ch_ed_priv[32];
if (topo_node_sqlite_channel_get_priv(g_cc.db, t->ch_id, ch_ed_priv) != 0) {
if (topo_node_sqlite_channel_get_priv(cc->db, t->ch_id, ch_ed_priv) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — no channel privkey for ch=%s (not owner?)", CC_ID, t->ch_id); return -1;
}
uint8_t sign_msg[8192]; size_t mlen = (size_t)off;
@@ -633,7 +634,7 @@ int chat_member_tags_commit(struct chat_member_tags* t) {
char sql[384]; snprintf(sql, sizeof(sql),
"SELECT x25519_pubkey, ed25519_pubkey, join_sig, join_ts, update_sig, update_ts, userinfo, storage"
" FROM \"%s\" WHERE node_id=?", peers_tbl);
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member query failed", CC_ID); return -1;
}
sqlite3_bind_int64(st, 1, (sqlite3_int64)t->node_id);
@@ -654,11 +655,11 @@ int chat_member_tags_commit(struct chat_member_tags* t) {
const char* stg_val = chat_member_tags_get(t, "storage");
if (stg_val && strcmp(stg_val, "yes") == 0) stg = 1;
int rc = member_sync_put(g_cc.inst, t->ch_id, t->node_id, x25, ed, jsig, jts, usig, uts,
int rc = member_sync_put(inst, t->ch_id, t->node_id, x25, ed, jsig, jts, usig, uts,
uinfo ? uinfo : "", json, sig, stg, 0, NULL);
sqlite3_finalize(st);
if (rc < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member_sync_put failed rc=%d", CC_ID, rc); return -1; }
if (rc & MS_APPLY_CHANGED) member_sync_broadcast_one(g_cc.inst, t->ch_id, t->node_id);
if (rc & MS_APPLY_CHANGED) member_sync_broadcast_one(inst, t->ch_id, t->node_id);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit ch=%s nid=0x%016llx ver=%d keys=%d json=%s",
CC_ID, t->ch_id, (unsigned long long)t->node_id, ver, t->key_count, json);
@@ -675,11 +676,11 @@ void chat_member_tags_free(struct chat_member_tags* t) {
void chat_member_tags_commit_trampoline(void* arg) {
struct chat_member_tags_req* req = (struct chat_member_tags_req*)arg;
if (!req || !g_cc.initialized) { u_free(arg); return; }
struct chat_member_tags* t = chat_member_tags_load(req->ch_id, req->node_id);
if (!req || !CC(req->inst) || !CC(req->inst)->initialized) { u_free(arg); return; }
struct chat_member_tags* t = chat_member_tags_load(req->inst, req->ch_id, req->node_id);
if (!t) { u_free(arg); return; }
if (req->json[0]) chat_member_tags_replace(t, req->json);
chat_member_tags_commit(t);
chat_member_tags_commit(req->inst, t);
chat_member_tags_free(t);
u_free(arg);
}
+69 -53
View File
@@ -2,7 +2,9 @@
* chat_core.h — центральный API чата в потоке uasync
*
* Все DB-операции и сетевые функции выполняются в uasync-потоке.
* GUI вызывает chat_core_submit_message() через gui_bridge_post_uasync().
* Все функции per-instance: первым аргументом struct UTUN_INSTANCE*.
* GUI вызывает через gui_bridge_post_uasync() trampoline-функции,
* чей arg-структура несёт inst.
*/
#ifndef CHAT_CORE_H
@@ -22,24 +24,24 @@ void chat_core_destroy(struct UTUN_INSTANCE* inst);
/* Backfill: регистрирует уже скачанные медиафайлы (лежат на диске, но отсутствуют
* в media_files) — вызывается после загрузки каналов/сообщений при старте. */
void chat_media_backfill(void);
void chat_media_backfill(struct UTUN_INSTANCE* inst);
/* Докачка недостающего медиа за последние storage_backfill_days дней. Проходит по
* медиасообщениям каналов и запускает загрузку для тех, чей файл отсутствует на диске. */
void chat_media_autodownload_backfill(void);
void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst);
/* Регистрирует триггер: после первого SYNC_DONE (+ дебаунс 2с) анонсирует локальные
* блоки суперузлам и запускает chat_media_autodownload_backfill. Один раз за процесс. */
void chat_media_startup_backfill_init(struct UTUN_INSTANCE* inst);
void chat_media_startup_backfill_destroy(struct UTUN_INSTANCE* inst);
struct sqlite3* chat_core_get_db(void);
struct UTUN_INSTANCE* chat_core_get_inst(void);
int chat_core_is_initialized(void);
struct sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst);
int chat_core_is_initialized(struct UTUN_INSTANCE* inst);
/* ── Отправка сообщения (GUI → uasync) ── */
struct chat_msg_submit {
struct UTUN_INSTANCE* inst;
char channel_id[64];
char content_type[32];
char media_src[1024];
@@ -56,22 +58,23 @@ struct chat_msg_submit {
uint64_t reply_to_node_id; /* node_id автора исходного сообщения */
};
void chat_core_submit_message(struct chat_msg_submit* req);
void chat_core_submit_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req);
/* Отправка медиа-сообщения (вложение): src->dest (copy) или видео (transcode). */
void chat_core_submit_media_message(struct chat_msg_submit* req);
void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req);
/* ── DB-операции (оставлены для интроспекции) ── */
uint32_t chat_core_count(const char* ch_id);
int chat_core_chain_hash_at(const char* ch_id, uint32_t pos, uint8_t* hash_out);
int chat_core_list_channels(uint8_t* buf, size_t buf_size, size_t* out_len);
int chat_core_list_peers(const char* ch_id, uint8_t* buf, size_t buf_size, size_t* out_len);
int chat_core_load_nodeinfo(uint64_t node_id, uint8_t* buf, size_t buf_size, size_t* out_len);
uint32_t chat_core_count(struct UTUN_INSTANCE* inst, const char* ch_id);
int chat_core_chain_hash_at(struct UTUN_INSTANCE* inst, const char* ch_id, uint32_t pos, uint8_t* hash_out);
int chat_core_list_channels(struct UTUN_INSTANCE* inst, uint8_t* buf, size_t buf_size, size_t* out_len);
int chat_core_list_peers(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t* buf, size_t buf_size, size_t* out_len);
int chat_core_load_nodeinfo(struct UTUN_INSTANCE* inst, uint64_t node_id, uint8_t* buf, size_t buf_size, size_t* out_len);
/* ── Создание канала (GUI → uasync) ── */
struct chat_channel_create {
struct UTUN_INSTANCE* inst;
char channel_id[64];
char name[128];
uint64_t owner_node_id;
@@ -82,50 +85,60 @@ struct chat_channel_create {
uint8_t signature[64];
};
void chat_core_ensure_channel_ready(const char* ch_id);
void chat_core_create_channel(struct chat_channel_create* req);
struct chat_create_auto_arg { struct UTUN_INSTANCE* inst; char name[128]; };
void chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id);
void chat_core_create_channel(struct UTUN_INSTANCE* inst, struct chat_channel_create* req);
void chat_core_create_channel_trampoline(void* arg);
void chat_core_create_channel_auto(const char* name);
void chat_core_create_channel_auto(struct UTUN_INSTANCE* inst, const char* name);
void chat_core_create_channel_auto_trampoline(void* arg);
/* ── Локальное удаление канала (GUI → uasync) ── */
struct chat_delete_channel_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
};
/* Удалить канал с этого устройства: отмена медиа-загрузок, удаление группы
* (соединения/синк), db_sync инстанса, таблиц msg_ и peers_, медиафайлов и
* merkle-хешей. Работает в uasync-потоке. Другие узлы канал не теряют. */
void chat_core_delete_channel(const char* ch_id);
void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id);
void chat_core_delete_channel_trampoline(void* arg);
/* ── Утилиты ── */
void chat_core_set_my_node_id(uint64_t node_id);
void chat_core_update_my_name(const char* name);
void chat_core_set_my_node_id(struct UTUN_INSTANCE* inst, uint64_t node_id);
void chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name);
struct update_my_name_arg { struct UTUN_INSTANCE* inst; char name[128]; };
void chat_core_update_my_name_trampoline(void* arg);
void chat_core_sync_my_addresses(void);
void chat_core_sync_my_addresses(struct UTUN_INSTANCE* inst);
/* Единое обновление своего мембера: адреса + bump update_ts + подпись (с адресами) + синк */
void chat_core_update_my_member(void);
void chat_core_update_my_member(struct UTUN_INSTANCE* inst);
/* Сохранение key-value в ui_state (вызывается из uasync-потока) */
void chat_core_save_ui_state(const char* key, const char* value);
void chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value);
struct save_ui_state_arg { struct UTUN_INSTANCE* inst; char data[256]; };
void chat_core_save_ui_state_trampoline(void* arg);
/* Установка chat-настройки (name=value) через chat_setting API (GUI → uasync) */
void chat_core_set_setting(const char* name, const char* value);
void chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value);
struct chat_setting_arg { struct UTUN_INSTANCE* inst; char data[256]; };
void chat_core_set_setting_trampoline(void* arg);
/* Запуск подключения к узлам выбранного канала (GUI → uasync) */
void chat_core_connect_channel(const char* ch_id);
void chat_core_connect_channel(struct UTUN_INSTANCE* inst, const char* ch_id);
void chat_core_connect_channel_trampoline(void* arg);
/* Однократная попытка подключения к конкретному узлу канала (GUI → uasync) */
void chat_core_connect_node(const char* ch_id, uint64_t node_id);
void chat_core_connect_node(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id);
void chat_core_connect_node_trampoline(void* arg);
struct chat_connect_node_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
uint64_t node_id;
};
@@ -135,58 +148,59 @@ void chat_core_submit_trampoline(void* arg);
/* Трамплин для ручного запуска загрузки медиа (JNI → uasync) */
struct attachment_dl_req {
struct UTUN_INSTANCE* inst;
char channel_id[64];
int64_t msg_id;
};
void chat_core_attachment_download_trampoline(void* arg);
/* Обновление local_attrs для записи (вызывается из uasync-потока) */
int chat_core_update_local_attrs(const char* ch_id, uint64_t ts,
int chat_core_update_local_attrs(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t ts,
const uint8_t* author_sig,
const char* local_attrs_json);
/* Пометить голосовое сообщение как проигранное (добавляет "pl":1 в local_attrs) */
void chat_core_mark_voice_played(const char* channel_id, uint64_t ts,
void chat_core_mark_voice_played(struct UTUN_INSTANCE* inst, const char* channel_id, uint64_t ts,
uint64_t author_node_id);
void chat_core_mark_voice_played_trampoline(void* arg);
struct voice_played_req {
struct UTUN_INSTANCE* inst;
char channel_id[64];
uint64_t ts;
uint64_t author_node_id;
};
/* Сбор статуса (NTP + connections) и отправка в GUI */
/* Сбор статуса (NTP + connections) и отправка в GUI. arg = struct UTUN_INSTANCE* (inst) */
void chat_core_collect_status_trampoline(void* arg);
/* Сбор списка ETCP-соединений и отправка в GUI (binary packet) */
/* Сбор списка ETCP-соединений и отправка в GUI (binary packet). arg = inst */
void chat_core_collect_conn_list_trampoline(void* arg);
/* Сбор полных метрик одного соединения (arg = u_malloc uint64_t peer_node_id) */
struct chat_node_arg { struct UTUN_INSTANCE* inst; uint64_t node_id; };
/* Сбор полных метрик одного соединения (arg = u_malloc struct chat_node_arg) */
void chat_core_collect_conn_metrics_trampoline(void* arg);
/* Сбор live-снапшота для панели деталей участника (arg = u_malloc uint64_t node_id) */
/* Сбор live-снапшота для панели деталей участника (arg = u_malloc struct chat_node_arg) */
void chat_core_collect_member_detail_trampoline(void* arg);
/* ── Headless CLI API ── */
/** Вариант для сборки без UTUN_INSTANCE (использует sqlite3* и node_id) */
void chat_core_set_ctx_for_headless(sqlite3* db, struct UTUN_INSTANCE* inst, uint64_t my_node_id);
/* Список каналов с метаданными в JSON: [{id,name,owner_id,owner_name,peers,msgs,online}] */
int chat_core_get_channels_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len);
/** Список каналов с метаданными в JSON: [{id,name,owner_id,owner_name,peers,msgs,online}] */
int chat_core_get_channels_json(char* buf, size_t buf_size, size_t* out_len);
/** Сообщения канала в JSON: [{id,ts,author_id,author_name,content_type,data}]
/* Сообщения канала в JSON: [{id,ts,author_id,author_name,content_type,data}]
* count=0 → все, offset=0 → сначала */
int chat_core_get_messages_json(const char* ch_id, int count, int offset,
int chat_core_get_messages_json(struct UTUN_INSTANCE* inst, const char* ch_id, int count, int offset,
char* buf, size_t buf_size, size_t* out_len);
/** Мемберы канала с полным состоянием в JSON:
/* Мемберы канала с полным состоянием в JSON:
* [{node_id,name,online,connected,x25519,ed25519,addrs:[{ip,port,proto,rtt}]}] */
int chat_core_get_members_json(const char* ch_id, char* buf, size_t buf_size, size_t* out_len);
int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len);
/** Имя узла из таблицы nodes */
int chat_core_get_node_name(uint64_t node_id, char* out, size_t sz);
/* Имя узла из таблицы nodes */
int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz);
/* ── Редактирование adm_tags (владелец канала) ── */
@@ -196,27 +210,28 @@ int chat_core_get_node_name(uint64_t node_id, char* out, size_t sz);
struct chat_member_tags;
/** Загрузить текущие adm_tags мембера в память. Возвращает NULL если мембера нет. */
struct chat_member_tags* chat_member_tags_load(const char* ch_id, uint64_t node_id);
/* Загрузить текущие adm_tags мембера в память. Возвращает NULL если мембера нет. */
struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id);
/** Установить ключ. val=NULL → удалить ключ. ver — особый, автоинкремент при commit. */
/* Установить ключ. val=NULL → удалить ключ. ver — особый, автоинкремент при commit. */
int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val);
/** Получить значение ключа. Возвращает NULL если ключ не найден. */
/* Получить значение ключа. Возвращает NULL если ключ не найден. */
const char* chat_member_tags_get(struct chat_member_tags* t, const char* key);
/** Заменить все теги данными из JSON-строки (плоский, строковые значения). */
/* Заменить все теги данными из JSON-строки (плоский, строковые значения). */
int chat_member_tags_replace(struct chat_member_tags* t, const char* json);
/** Сохранить: сериализовать в JSON, инкрементировать ver, подписать канальным Ed25519,
/* Сохранить: сериализовать в JSON, инкрементировать ver, подписать канальным Ed25519,
* записать через member_sync_put() → синхронизируется всем. */
int chat_member_tags_commit(struct chat_member_tags* t);
int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* t);
/** Освободить без сохранения. */
/* Освободить без сохранения. */
void chat_member_tags_free(struct chat_member_tags* t);
/** Трамплин для gui_bridge: arg = struct chat_member_tags_req* (доверенная память). */
/* Трамплин для gui_bridge: arg = struct chat_member_tags_req* (доверенная память). */
struct chat_member_tags_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
uint64_t node_id;
char json[512];
@@ -225,11 +240,12 @@ void chat_member_tags_commit_trampoline(void* arg);
/* ── Передача прав админа (владелец → другому узлу) ── */
/** Передать оба канальных privkey выбранному узлу (encrypted+signed через etcp_router). */
void chat_core_transfer_admin(const char* ch_id, uint64_t target_node_id);
/* Передать оба канальных privkey выбранному узлу (encrypted+signed через etcp_router). */
void chat_core_transfer_admin(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id);
void chat_core_transfer_admin_trampoline(void* arg);
struct chat_transfer_admin_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
uint64_t target_node_id;
};
+16 -5
View File
@@ -75,6 +75,9 @@ static inline size_t b64_decode(const char* src, size_t src_len, uint8_t* dst, s
/* ── Глобальное состояние (определено в chat_core.c) ── */
struct ms_props_cbk; /* member_sync.c */
struct ms_apply_cbk; /* member_sync.c */
struct chat_core_ctx {
struct UTUN_INSTANCE* inst;
sqlite3* db;
@@ -82,28 +85,36 @@ struct chat_core_ctx {
uint8_t shared_db;
uint64_t my_node_id;
uint8_t initialized;
/* per-instance sub-module state (was file-scope statics) */
struct ms_props_cbk* props_cbks; /* was member_sync.c g_props_cbks */
struct ms_apply_cbk* apply_cbks; /* was member_sync.c g_apply_cbks */
uint64_t media_cache_bytes; /* was chat_msg.c g_media_cache_bytes */
void* media_backfill_timer;/* was chat_msg.c g_media_startup_backfill_timer */
int media_backfill_ran; /* was chat_msg.c g_media_startup_backfill_ran */
struct UTUN_INSTANCE* media_backfill_inst; /* was chat_msg.c g_media_startup_backfill_inst */
};
extern struct chat_core_ctx g_cc;
/* Per-instance chat context. inst->chat_core обязателен после chat_core_init. */
#define CC(inst) ((inst)->chat_core)
/* ── Хелперы ── */
void sanitize_ch_id(const char* ch_id, char* out, size_t out_sz);
void msg_table_name(const char* ch_id, char* buf, size_t sz);
void peers_table_name(const char* ch_id, char* buf, size_t sz);
int db_exec(const char* sql);
/* ── db_sync registry (chat_core.c) ── */
struct DB_SYNC_INSTANCE* si_find(const char* ch_id);
void si_register(struct DB_SYNC_INSTANCE* si, const char* ch_id);
struct DB_SYNC_INSTANCE* si_find(struct UTUN_INSTANCE* inst, const char* ch_id);
void si_register(struct UTUN_INSTANCE* inst, struct DB_SYNC_INSTANCE* si, const char* ch_id);
/* ── db_sync callback (chat_msg.c) ── */
struct msg_insert_arg { struct UTUN_INSTANCE* inst; char ch_id[64]; };
void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, void* arg);
/* ── применить настройку group_autoconnect ко всем CHAT-группам (chat_channel.c) ── */
void chat_core_apply_group_autoconnect(void);
void chat_core_apply_group_autoconnect(struct UTUN_INSTANCE* inst);
#endif /* CHAT_CORE_PRIV_H */
+8 -7
View File
@@ -1,29 +1,30 @@
/*
* chat_event.c — реализация системы нотификаций
*
* Обработчик — per-instance (хранится в UTUN_INSTANCE.chat_event_handler).
* Пока обработчик не зарегистрирован — события логируются.
* При регистрации обработчика — форвардятся ему синхронно.
*/
#include "chat_event.h"
#include "../utun_instance.h"
#include "../lib/debug_config.h"
#ifdef __ANDROID__
#include <android/log.h>
#endif
static chat_event_handler_fn g_handler = NULL;
void chat_event_set_handler(chat_event_handler_fn handler) {
void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn handler) {
if (!inst) return;
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_INFO, "utun-evt", "chat_event_set_handler: handler=%p old=%p",
(void*)handler, (void*)g_handler);
(void*)handler, (void*)inst->chat_event_handler);
#endif
g_handler = handler;
inst->chat_event_handler = handler;
}
void chat_event_post(int type, const uint8_t* data, int len) {
if (g_handler) { g_handler(type, data, len); return; }
void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
if (inst && inst->chat_event_handler) { inst->chat_event_handler(inst, type, data, len); return; }
static const char* names[] = {
[1]="MSG_RECEIVED", [2]="CONNECT_RESULT", [3]="NEW_PEER",
+7 -3
View File
@@ -14,6 +14,8 @@
#include <stdint.h>
#include <stddef.h>
struct UTUN_INSTANCE;
#ifdef __cplusplus
extern "C" {
#endif
@@ -50,11 +52,13 @@ extern "C" {
#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 */
#define CHAT_EVT_CHANNEL_DELETED 31 /* [ch_id_len:1][ch_id:var] — канал удалён локально */
#define CHAT_EVT_DM_MSG_RECEIVED 32 /* [conv_id_len:1][conv_id:var][author_node_id:8] */
#define CHAT_EVT_DM_CONV_UPDATED 33 /* [conv_id_len:1][conv_id:var] — беседа создана/изменена */
typedef void (*chat_event_handler_fn)(int type, const uint8_t* data, int len);
typedef void (*chat_event_handler_fn)(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);
void chat_event_set_handler(chat_event_handler_fn handler);
void chat_event_post(int type, const uint8_t* data, int len);
void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn handler);
void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);
#ifdef __cplusplus
}
+213 -99
View File
@@ -1,40 +1,54 @@
/*
* chat_headless_control.c — TCP control socket for headless chat CLI
*
* JSON line protocol over TCP. Integrated into uasync event loop.
* Default: localhost-only, port from config.
* JSON line protocol:
* Request: {"id":N,"cmd":"<command>",...params}
* Response: {"id":N,"ok":true,"data":{...}} | {"id":N,"ok":false,"error":"..."}
* Event: {"event":"<type>",...}
*
* Commands: ping, status, channels, members, messages, send, invite,
* invite_nodes, connect, create_channel, subscribe, quit
*/
#include "chat_headless_control.h"
#include "chat_core.h"
#include "chat_core_priv.h"
#include "chat_event.h"
#include "chat_sync.h"
#include "chat_join.h"
#include "invite_link.h"
#include "invite_build.h"
#include "chat_core.h"
#include "chat_sync.h"
#include "chat_event.h"
#include "../utun_instance.h"
#include "../ntp_time.h"
#include "../dm/dm_core.h"
#include "../dm/dm_crypto.h"
#include "../transport_layer/secure_channel.h"
#include "../transport_layer/etcp.h"
#include "../transport_layer/etcp_connections.h"
#include "../transport_layer/secure_channel.h"
#include "../../lib/u_async.h"
#include "../../lib/debug_config.h"
#include "../../lib/mem.h"
#include "../utun_instance.h"
#include "../ntp_time.h"
#include "../../lib/ll_queue.h"
#include "../routing_layer/topo_node_sqlite.h"
#include "../../lib/u_async.h"
#include "../../lib/mem.h"
#include "../../lib/socket_compat.h"
#include "../../lib/debug_config.h"
#include "../../lib/platform_compat.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <stdarg.h>
#include <errno.h>
#include <sys/stat.h>
#include "../../lib/platform_compat.h"
#include "../../lib/socket_compat.h"
#include <time.h>
#include <sqlite3.h>
#define HC_ID "headless"
#define MAX_CLIENTS 16
#define RECV_BUF_SIZE 16384
#define SEND_BUF_SIZE 16384
#define SEND_BUF_SIZE 32768
#define MAX_CLIENTS 64
#ifndef DEBUG_CATEGORY_HEADLESS
#define DEBUG_CATEGORY_HEADLESS 27
#define DEBUG_CATEGORY_HEADLESS DEBUG_CATEGORY_GENERAL
#endif
struct headless_client {
@@ -47,10 +61,11 @@ struct headless_client {
int send_offset;
int subscribed;
int closing;
struct headless_control* hc;
struct headless_client* next;
};
static struct {
struct headless_control {
struct UASYNC* ua;
struct UTUN_INSTANCE* inst;
struct headless_client* clients;
@@ -58,7 +73,11 @@ static struct {
socket_t listen_fd;
void* listen_sock_id;
int running;
} g_hc;
};
static struct headless_control* hc_of(struct UTUN_INSTANCE* inst) {
return inst ? (struct headless_control*)inst->headless : NULL;
}
/* ── Forward declarations ── */
@@ -104,6 +123,7 @@ static const char* json_get_cmd_end(const char* cmd_start) {
/* ── Send helpers ── */
static void cli_send(struct headless_client* cli, const char* data, size_t len) {
struct headless_control* hc = cli->hc;
if (!cli || cli->closing || cli->fd == SOCKET_INVALID || !data || len == 0) return;
if (cli->send_len > 0) {
size_t space = sizeof(cli->send_buf) - (size_t)cli->send_len;
@@ -115,13 +135,13 @@ static void cli_send(struct headless_client* cli, const char* data, size_t len)
ssize_t r = send(cli->fd, data, (int)len, 0);
if (r < 0 && (socket_get_error() == ERR_AGAIN || socket_get_error() == ERR_WOULDBLOCK)) {
if (len <= sizeof(cli->send_buf)) { memcpy(cli->send_buf, data, len); cli->send_len = (int)len; cli->send_offset = 0; }
if (g_hc.ua) uasync_set_socket_write(g_hc.ua, cli->socket_id, 1);
if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 1);
return;
}
if (r >= 0 && (size_t)r < len) {
size_t remain = len - (size_t)r;
if (remain <= sizeof(cli->send_buf)) { memcpy(cli->send_buf, data + r, remain); cli->send_len = (int)remain; cli->send_offset = 0; }
if (g_hc.ua) uasync_set_socket_write(g_hc.ua, cli->socket_id, 1);
if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 1);
}
}
@@ -139,17 +159,19 @@ static void send_response(struct headless_client* cli, int id, const char* ok_da
/* ── Broadcast event ── */
static void hc_broadcast_event(const char* json) {
static void hc_broadcast_event(struct headless_control* hc, const char* json) {
size_t jlen = strlen(json);
char buf[4096]; int blen = snprintf(buf, sizeof(buf), "%s\n", json);
if (blen <= 0) return;
for (struct headless_client* c = g_hc.clients; c; c = c->next)
for (struct headless_client* c = hc->clients; c; c = c->next)
if (c->subscribed) cli_send(c, buf, (size_t)blen);
}
/* ── chat_event callback ── */
static void hc_on_chat_event(int type, const uint8_t* data, int len) {
static void hc_on_chat_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
struct headless_control* hc = hc_of(inst);
if (!hc) return;
if (type == 10) return; /* skip status text */
char json[1024]; json[0] = '\0';
if (type == 1 && data && len >= 10) { /* MSG_RECEIVED */
@@ -172,29 +194,38 @@ static void hc_on_chat_event(int type, const uint8_t* data, int len) {
(int)cl, data + 1, (int)nl, data + 2 + cl, (unsigned long long)inviter);
}
}
} else if (type == 32 && data && len >= 10) { /* DM_MSG_RECEIVED */
uint8_t cl = data[0];
uint64_t author; memcpy(&author, data + 1 + cl, 8);
snprintf(json, sizeof(json), "{\"event\":\"dm_msg\",\"conv\":\"%.*s\",\"author_id\":\"0x%016llx\"}", (int)cl, data + 1, (unsigned long long)author);
} else if (type == 33 && data && len >= 2) { /* DM_CONV_UPDATED */
uint8_t cl = data[0];
snprintf(json, sizeof(json), "{\"event\":\"dm_conv\",\"conv\":\"%.*s\"}", (int)cl, data + 1);
}
if (json[0]) hc_broadcast_event(json);
if (json[0]) hc_broadcast_event(hc, json);
}
/* ── Write callback ── */
static void client_write_callback(socket_t fd, void* arg) {
struct headless_client* cli = (struct headless_client*)arg;
struct headless_control* hc = cli ? cli->hc : NULL;
if (!cli || cli->closing || fd != cli->fd) return;
if (cli->send_len <= 0) { if (g_hc.ua) uasync_set_socket_write(g_hc.ua, cli->socket_id, 0); return; }
if (cli->send_len <= 0) { if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 0); return; }
ssize_t r = send(cli->fd, cli->send_buf + cli->send_offset, (int)(cli->send_len - cli->send_offset), 0);
if (r < 0 && (socket_get_error() == ERR_AGAIN || socket_get_error() == ERR_WOULDBLOCK)) return;
if (r <= 0) { cli->send_len = 0; cli->send_offset = 0; if (g_hc.ua) uasync_set_socket_write(g_hc.ua, cli->socket_id, 0); return; }
if (r <= 0) { cli->send_len = 0; cli->send_offset = 0; if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 0); return; }
cli->send_offset += (int)r;
if (cli->send_offset >= cli->send_len) { cli->send_len = 0; cli->send_offset = 0; if (g_hc.ua) uasync_set_socket_write(g_hc.ua, cli->socket_id, 0); }
if (cli->send_offset >= cli->send_len) { cli->send_len = 0; cli->send_offset = 0; if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 0); }
}
/* ── Close client ── */
static void hc_close_client(struct headless_client* cli) {
struct headless_control* hc = cli ? cli->hc : NULL;
if (!cli || cli->closing) return;
cli->closing = 1;
if (g_hc.ua && cli->socket_id) uasync_remove_socket_t(g_hc.ua, cli->fd);
if (hc && hc->ua && cli->socket_id) uasync_remove_socket_t(hc->ua, cli->fd);
if (cli->fd != SOCKET_INVALID) { socket_close_wrapper(cli->fd); cli->fd = SOCKET_INVALID; }
}
@@ -219,30 +250,29 @@ static void client_read_callback(socket_t fd, void* arg) {
static void hc_handle_ping(struct headless_client* cli, int id, const char* json) {
(void)json;
uint64_t uptime = 0;
(void)uptime;
char data[128]; snprintf(data, sizeof(data), "{\"pong\":true,\"uptime_sec\":%llu}", (unsigned long long)uptime);
char data[128]; snprintf(data, sizeof(data), "{\"pong\":true,\"uptime_sec\":0}");
send_response(cli, id, data, NULL);
}
static void hc_handle_status(struct headless_client* cli, int id, const char* json) {
(void)json;
struct headless_control* hc = cli->hc;
char buf[8192]; int off = 0;
off += snprintf(buf + off, sizeof(buf) - off, "\"");
if (!g_hc.inst) {
if (!hc->inst) {
off += snprintf(buf + off, sizeof(buf) - off, "ERROR: no instance");
} else {
int conn_count = g_hc.inst->connections ? queue_entry_count(g_hc.inst->connections) : 0;
struct NTP_TIME* ntp = &g_hc.inst->ntp;
int conn_count = hc->inst->connections ? queue_entry_count(hc->inst->connections) : 0;
struct NTP_TIME* ntp = &hc->inst->ntp;
off += snprintf(buf + off, sizeof(buf) - off, "NTP: enabled=%s synced=%s offset=%.1fs\\n",
ntp->enabled ? "yes" : "no", ntp->synced ? "yes" : "no", ntp->offset_us / 1000000.0);
off += snprintf(buf + off, sizeof(buf) - off, "Connections: %d active\\n", conn_count);
uint64_t my_id = g_hc.inst->node_id;
struct ll_entry* entry = g_hc.inst->connections ? g_hc.inst->connections->head : NULL;
uint64_t my_id = hc->inst->node_id;
struct ll_entry* entry = hc->inst->connections ? hc->inst->connections->head : NULL;
while (entry) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
if (ce) {
char name[64]; chat_core_get_node_name(ce->peer_node_id, name, sizeof(name));
char name[64]; chat_core_get_node_name(hc->inst, ce->peer_node_id, name, sizeof(name));
off += snprintf(buf + off, sizeof(buf) - off, " [%04llX]->[%04llX] %s ETCP:%s\\n",
(unsigned long long)(my_id & 0xFFFF), (unsigned long long)(ce->peer_node_id & 0xFFFF),
name[0] ? name : "?", ce->conn && ce->conn->links_up ? "UP" : "DOWN");
@@ -258,7 +288,7 @@ static void hc_handle_status(struct headless_client* cli, int id, const char* js
static void hc_handle_channels(struct headless_client* cli, int id, const char* json) {
(void)json;
char buf[16384]; size_t len = 0;
if (chat_core_get_channels_json(buf, sizeof(buf), &len) < 0)
if (chat_core_get_channels_json(cli->hc->inst, buf, sizeof(buf), &len) < 0)
send_response(cli, id, NULL, "failed to query channels");
else
send_response(cli, id, buf, NULL);
@@ -268,7 +298,7 @@ static void hc_handle_members(struct headless_client* cli, int id, const char* j
char ch[64];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
char buf[16384]; size_t len = 0;
if (chat_core_get_members_json(ch, buf, sizeof(buf), &len) < 0)
if (chat_core_get_members_json(cli->hc->inst, ch, buf, sizeof(buf), &len) < 0)
send_response(cli, id, NULL, "failed to query members");
else
send_response(cli, id, buf, NULL);
@@ -280,7 +310,7 @@ static void hc_handle_messages(struct headless_client* cli, int id, const char*
count = json_get_int(json, "count", 20);
offset = json_get_int(json, "offset", 0);
char buf[16384]; size_t len = 0;
if (chat_core_get_messages_json(ch, count, offset, buf, sizeof(buf), &len) < 0)
if (chat_core_get_messages_json(cli->hc->inst, ch, count, offset, buf, sizeof(buf), &len) < 0)
send_response(cli, id, NULL, "failed to query messages");
else
send_response(cli, id, buf, NULL);
@@ -293,11 +323,12 @@ static void hc_handle_send(struct headless_client* cli, int id, const char* json
if (json_get_str(json, "data", data, sizeof(data)) < 0) { send_response(cli, id, NULL, "missing 'data' param"); return; }
struct chat_msg_submit* req = u_calloc(1, sizeof(*req));
if (!req) { send_response(cli, id, NULL, "out of memory"); return; }
req->inst = cli->hc->inst;
snprintf(req->channel_id, sizeof(req->channel_id), "%s", ch);
snprintf(req->content_type, sizeof(req->content_type), "%s", ct);
req->data = (uint8_t*)u_strdup(data);
req->data_len = (uint32_t)strlen(data);
chat_core_submit_message(req);
chat_core_submit_message(cli->hc->inst, req);
if (req->data) u_free(req->data);
u_free(req);
send_response(cli, id, "{\"sent\":true}", NULL);
@@ -317,6 +348,7 @@ static int hc_mkdirs(const char* path) {
}
static void hc_handle_attach(struct headless_client* cli, int id, const char* json) {
struct headless_control* hc = cli->hc;
char ch[64], path[1024], ct[32];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
if (json_get_str(json, "path", path, sizeof(path)) < 0) { send_response(cli, id, NULL, "missing 'path' param"); return; }
@@ -327,10 +359,10 @@ static void hc_handle_attach(struct headless_client* cli, int id, const char* js
struct stat st;
if (stat(path, &st) != 0 || st.st_size <= 0) { send_response(cli, id, NULL, "file not found or empty"); return; }
if (!g_hc.inst || !g_hc.inst->media_async) { send_response(cli, id, NULL, "media engine not initialized"); return; }
if (!hc->inst || !hc->inst->media_async) { send_response(cli, id, NULL, "media engine not initialized"); return; }
/* media_base из ui_state (как в блок-сервере media_delivery) */
sqlite3* db = chat_core_get_db();
sqlite3* db = chat_core_get_db(hc->inst);
char media_base[512] = "/tmp/utun_media";
if (db) {
sqlite3_stmt* ms = NULL;
@@ -362,6 +394,7 @@ static void hc_handle_attach(struct headless_client* cli, int id, const char* js
struct chat_msg_submit* req = u_calloc(1, sizeof(*req));
if (!req) { send_response(cli, id, NULL, "out of memory"); return; }
req->inst = hc->inst;
snprintf(req->channel_id, sizeof(req->channel_id), "%s", ch);
snprintf(req->content_type, sizeof(req->content_type), "%s", as_video ? "video/mp4" : ct);
snprintf(req->media_src, sizeof(req->media_src), "%s", path);
@@ -374,7 +407,7 @@ static void hc_handle_attach(struct headless_client* cli, int id, const char* js
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: attach ch=%s src=%s dst=%s video=%d",
ch, path, dest, as_video);
chat_core_submit_media_message(req);
chat_core_submit_media_message(hc->inst, req);
u_free(req);
char resp[1400]; snprintf(resp, sizeof(resp), "{\"attached\":true,\"dest\":\"%s\",\"video\":%s}",
@@ -383,9 +416,10 @@ static void hc_handle_attach(struct headless_client* cli, int id, const char* js
}
static void hc_handle_invite(struct headless_client* cli, int id, const char* json) {
struct headless_control* hc = cli->hc;
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; }
if (!hc->inst) { send_response(cli, id, NULL, "no instance"); return; }
uint64_t channel_id = strtoull(ch, NULL, 10);
if (channel_id == 0) { send_response(cli, id, NULL, "invalid channel_id"); return; }
@@ -396,7 +430,7 @@ static void hc_handle_invite(struct headless_client* cli, int id, const char* js
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) {
if (chat_invite_build_link(hc->inst, channel_id, target_node_id, NULL, link, sizeof(link)) < 0) {
send_response(cli, id, NULL, "failed to build invite link (see log)");
return;
}
@@ -418,24 +452,25 @@ static void hc_handle_invite(struct headless_client* cli, int id, const char* js
}
static void hc_handle_invite_nodes(struct headless_client* cli, int id, const char* json) {
struct headless_control* hc = cli->hc;
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);
int cnt = chat_invite_candidate_nodes(hc->inst, 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);
chat_invite_best_node(hc->inst, 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));
chat_core_get_node_name(hc->inst, 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);
}
@@ -444,11 +479,12 @@ static void hc_handle_invite_nodes(struct headless_client* cli, int id, const ch
}
static void hc_handle_connect(struct headless_client* cli, int id, const char* json) {
struct headless_control* hc = cli->hc;
char link[1024];
if (json_get_str(json, "link", link, sizeof(link)) < 0) { send_response(cli, id, NULL, "missing 'link' param"); return; }
struct InviteData d; char err[256];
if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) { send_response(cli, id, NULL, err); return; }
if (!g_hc.inst) { send_response(cli, id, NULL, "no instance"); return; }
if (!hc->inst) { send_response(cli, id, NULL, "no instance"); return; }
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: connect ch=%llu node=0x%016llx addrs=%d",
(unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
@@ -457,13 +493,12 @@ static void hc_handle_connect(struct headless_client* cli, int id, const char* j
int addrs_len = invite_serialize_addrs(&d, addrs_buf, sizeof(addrs_buf));
if (addrs_len <= 0) { send_response(cli, id, NULL, "serialize addrs failed"); return; }
chat_core_ensure_channel_ready((const char*)(&(char[64]){0}));
{ char chstr[64]; snprintf(chstr, sizeof(chstr), "%llu", (unsigned long long)d.channelId);
chat_core_ensure_channel_ready(chstr); }
chat_core_ensure_channel_ready(hc->inst, chstr); }
chat_core_set_my_node_id(g_hc.inst->node_id);
chat_core_set_my_node_id(hc->inst, hc->inst->node_id);
chat_sync_connect_from_invite(d.channelId, d.nodeId, d.pubkey,
chat_sync_connect_from_invite(hc->inst, d.channelId, d.nodeId, d.pubkey,
addrs_buf, d.addrCount, addrs_len, d.password_len ? d.password : NULL, d.join_key);
char resp[512]; snprintf(resp, sizeof(resp),
@@ -475,21 +510,22 @@ static void hc_handle_connect(struct headless_client* cli, int id, const char* j
static void hc_handle_create_channel(struct headless_client* cli, int id, const char* json) {
char name[256];
if (json_get_str(json, "name", name, sizeof(name)) < 0) { send_response(cli, id, NULL, "missing 'name' param"); return; }
chat_core_create_channel_auto(name);
chat_core_create_channel_auto(cli->hc->inst, name);
send_response(cli, id, "{\"created\":true}", NULL);
}
static void hc_handle_invite_to(struct headless_client* cli, int id, const char* json) {
struct headless_control* hc = cli->hc;
char ch[64], node_id_str[32], pubkey_hex[128], addr[128];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
if (json_get_str(json, "node_id", node_id_str, sizeof(node_id_str)) < 0) { send_response(cli, id, NULL, "missing 'node_id' param"); return; }
if (json_get_str(json, "pubkey", pubkey_hex, sizeof(pubkey_hex)) < 0) { send_response(cli, id, NULL, "missing 'pubkey' param"); return; }
if (json_get_str(json, "addr", addr, sizeof(addr)) < 0) { send_response(cli, id, NULL, "missing 'addr' param"); return; }
int proto = json_get_int(json, "proto", 1);
if (!g_hc.inst) { send_response(cli, id, NULL, "no instance"); return; }
if (!hc->inst) { send_response(cli, id, NULL, "no instance"); return; }
uint64_t target_node_id = strtoull(node_id_str, NULL, 16);
if (target_node_id == 0 || target_node_id == g_hc.inst->node_id) { send_response(cli, id, NULL, "invalid node_id"); return; }
if (target_node_id == 0 || target_node_id == hc->inst->node_id) { send_response(cli, id, NULL, "invalid node_id"); return; }
uint8_t pubkey_bin[SC_PUBKEY_SIZE];
if (sc_hex_to_binary(pubkey_hex, pubkey_bin, SC_PUBKEY_SIZE) != 0) { send_response(cli, id, NULL, "invalid pubkey hex"); return; }
@@ -520,7 +556,7 @@ static void hc_handle_invite_to(struct headless_client* cli, int id, const char*
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: invite_to ch=%s node=0x%016llx addr=%s:%d proto=%d",
ch, (unsigned long long)target_node_id, ip, port, proto);
chat_sync_invite_to_channel_with_addrs(g_hc.inst, ch, target_node_id,
chat_sync_invite_to_channel_with_addrs(hc->inst, ch, target_node_id,
pubkey_bin, addrs_data, 1, addrs_len);
char resp[256]; snprintf(resp, sizeof(resp),
@@ -543,14 +579,85 @@ static void hc_handle_quit(struct headless_client* cli, int id, const char* json
hc_close_client(cli);
}
/* ── DM (прямой чат) ── */
static void hc_handle_dm_start(struct headless_client* cli, int id, const char* json) {
struct headless_control* hc = cli->hc;
char node_id_str[64], ch[64];
if (json_get_str(json, "node_id", node_id_str, sizeof(node_id_str)) < 0) { send_response(cli, id, NULL, "missing 'node_id' param"); return; }
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) ch[0] = '\0';
uint64_t target = strtoull(node_id_str, NULL, 16);
if (!target || !hc->inst) { send_response(cli, id, NULL, "invalid node_id"); return; }
sqlite3* db = chat_core_get_db(hc->inst);
uint8_t x25519[32], ed25519[32];
int ok = 0;
sqlite3_stmt* st = NULL;
if (db && sqlite3_prepare_v2(db,
"SELECT x25519_pubkey, ed25519_pubkey, COALESCE(name,'') FROM nodes WHERE node_id=?",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)target);
if (sqlite3_step(st) == SQLITE_ROW) {
const uint8_t* x = (const uint8_t*)sqlite3_column_blob(st, 0);
const uint8_t* e = (const uint8_t*)sqlite3_column_blob(st, 1);
if (x && e && sqlite3_column_bytes(st, 0) >= 32 && sqlite3_column_bytes(st, 1) >= 32) {
memcpy(x25519, x, 32); memcpy(ed25519, e, 32);
const char* name = (const char*)sqlite3_column_text(st, 2);
if (dm_start(hc->inst, target, x25519, ed25519, name, ch[0] ? ch : NULL) == 0) ok = 1;
}
}
}
if (st) sqlite3_finalize(st);
if (!ok) { send_response(cli, id, NULL, "failed to start DM (node not found?)"); return; }
char conv[64]; uint64_t conv_num = dm_derive_conv_id(hc->inst->node_id, target);
snprintf(conv, sizeof(conv), "%llu", (unsigned long long)conv_num);
char resp[256]; snprintf(resp, sizeof(resp), "{\"conv_id\":\"%s\",\"started\":true}", conv);
send_response(cli, id, resp, NULL);
}
/* Список DM-бесед. */
static void hc_handle_dm_list(struct headless_client* cli, int id, const char* json) {
(void)json;
char buf[16384]; size_t len = 0;
if (dm_list_conversations_json(cli->hc->inst, buf, sizeof(buf), &len) < 0)
send_response(cli, id, NULL, "failed to query conversations");
else
send_response(cli, id, buf, NULL);
}
/* Сообщения DM-беседы (расшифрованные). */
static void hc_handle_dm_messages(struct headless_client* cli, int id, const char* json) {
char conv[64]; int count = 20, offset = 0;
if (json_get_str(json, "conv_id", conv, sizeof(conv)) < 0) { send_response(cli, id, NULL, "missing 'conv_id' param"); return; }
count = json_get_int(json, "count", 20);
offset = json_get_int(json, "offset", 0);
char buf[16384]; size_t len = 0;
if (dm_list_messages_json(cli->hc->inst, conv, count, offset, buf, sizeof(buf), &len) < 0)
send_response(cli, id, NULL, "failed to query DM messages");
else
send_response(cli, id, buf, NULL);
}
/* Отправить сообщение в DM-беседу. */
static void hc_handle_dm_send(struct headless_client* cli, int id, const char* json) {
char conv[64], data[4096];
if (json_get_str(json, "conv_id", conv, sizeof(conv)) < 0) { send_response(cli, id, NULL, "missing 'conv_id' param"); return; }
if (json_get_str(json, "data", data, sizeof(data)) < 0) { send_response(cli, id, NULL, "missing 'data' param"); return; }
if (dm_send(cli->hc->inst, conv, "text", (const uint8_t*)data, (uint32_t)strlen(data)) != 0)
send_response(cli, id, NULL, "failed to send DM");
else
send_response(cli, id, "{\"sent\":true}", NULL);
}
/* ── Command dispatch ── */
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", "attach", "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_attach, hc_handle_invite, hc_handle_invite_nodes, 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", "attach", "invite", "invite_nodes", "connect", "create_channel", "invite_to", "subscribe", "dm_start", "dm_list", "dm_messages", "dm_send", "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_attach, hc_handle_invite, hc_handle_invite_nodes, hc_handle_connect, hc_handle_create_channel, hc_handle_invite_to, hc_handle_subscribe, hc_handle_dm_start, hc_handle_dm_list, hc_handle_dm_messages, hc_handle_dm_send, hc_handle_quit };
void hc_handle_command(struct headless_client* cli, const char* json) {
static void hc_handle_command(struct headless_client* cli, const char* json) {
if (!cli || !json) return;
int id = json_get_int(json, "id", 0);
const char* cs = json_get_cmd(json);
@@ -568,87 +675,94 @@ void hc_handle_command(struct headless_client* cli, const char* json) {
/* ── Accept callback ── */
static void accept_callback(socket_t fd, void* arg) {
(void)arg;
struct headless_control* hc = (struct headless_control*)arg;
struct sockaddr_storage addr; socklen_t alen = sizeof(addr);
socket_t cfd = accept(fd, (struct sockaddr*)&addr, &alen);
if (cfd == SOCKET_INVALID) { int e = socket_get_error(); if (e != ERR_AGAIN && e != ERR_WOULDBLOCK) DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: accept failed: %s", socket_strerror(e)); return; }
socket_set_nonblocking(cfd);
if (g_hc.client_count >= MAX_CLIENTS) { socket_close_wrapper(cfd); DEBUG_WARN((int)DEBUG_CATEGORY_HEADLESS, "headless: max clients reached"); return; }
if (hc->client_count >= MAX_CLIENTS) { socket_close_wrapper(cfd); DEBUG_WARN((int)DEBUG_CATEGORY_HEADLESS, "headless: max clients reached"); return; }
struct headless_client* cli = u_calloc(1, sizeof(*cli));
if (!cli) { socket_close_wrapper(cfd); DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: failed to allocate client"); return; }
cli->fd = cfd;
cli->hc = hc;
cli->socket_id = uasync_add_socket_t(g_hc.ua, cfd, client_read_callback, client_write_callback, NULL, cli);
cli->socket_id = uasync_add_socket_t(hc->ua, cfd, client_read_callback, client_write_callback, NULL, cli);
if (!cli->socket_id) { u_free(cli); socket_close_wrapper(cfd); DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: failed to register client fd"); return; }
cli->next = g_hc.clients; g_hc.clients = cli; g_hc.client_count++;
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: client connected fd=%d total=%d", (int)cfd, g_hc.client_count);
cli->next = hc->clients; hc->clients = cli; hc->client_count++;
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: client connected fd=%d total=%d", (int)cfd, hc->client_count);
}
/* ── Periodic cleanup ── */
static void hc_cleanup(void* arg) {
(void)arg;
struct headless_client** prev = &g_hc.clients;
struct headless_control* hc = (struct headless_control*)arg;
struct headless_client** prev = &hc->clients;
while (*prev) {
if ((*prev)->closing) {
struct headless_client* dead = *prev; *prev = dead->next;
if (dead->socket_id && g_hc.ua) uasync_remove_socket_t(g_hc.ua, dead->fd);
if (dead->socket_id && hc->ua) uasync_remove_socket_t(hc->ua, dead->fd);
if (dead->fd != SOCKET_INVALID) socket_close_wrapper(dead->fd);
u_free(dead); g_hc.client_count--;
u_free(dead); hc->client_count--;
} else prev = &(*prev)->next;
}
if (g_hc.ua) uasync_set_timeout(g_hc.ua, 5000, NULL, hc_cleanup, "hc_cleanup");
if (hc->ua) uasync_set_timeout(hc->ua, 5000, hc, hc_cleanup, "hc_cleanup");
}
/* ── Init / Destroy ── */
int chat_headless_control_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
const char* bind_ip, int port) {
if (!ua || !bind_ip || port <= 0 || port > 65535) return -1;
if (g_hc.running) return 0;
if (!ua || !inst || !bind_ip || port <= 0 || port > 65535) return -1;
struct headless_control* hc = hc_of(inst);
if (hc && hc->running) return 0;
memset(&g_hc, 0, sizeof(g_hc));
g_hc.ua = ua; g_hc.inst = inst;
if (!hc) {
hc = u_calloc(1, sizeof(*hc));
if (!hc) return -1;
inst->headless = hc;
}
hc->ua = ua; hc->inst = inst;
g_hc.listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (g_hc.listen_fd == SOCKET_INVALID) { DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: socket() failed: %s", socket_strerror(socket_get_error())); return -1; }
hc->listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (hc->listen_fd == SOCKET_INVALID) { DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: socket() failed: %s", socket_strerror(socket_get_error())); return -1; }
socket_set_reuseaddr(g_hc.listen_fd, 1);
socket_set_nonblocking(g_hc.listen_fd);
socket_set_reuseaddr(hc->listen_fd, 1);
socket_set_nonblocking(hc->listen_fd);
struct sockaddr_in sin; memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET; sin.sin_port = htons((uint16_t)port);
if (inet_pton(AF_INET, bind_ip, &sin.sin_addr) != 1) { socket_close_wrapper(g_hc.listen_fd); g_hc.listen_fd = SOCKET_INVALID; return -1; }
if (inet_pton(AF_INET, bind_ip, &sin.sin_addr) != 1) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; }
if (bind(g_hc.listen_fd, (struct sockaddr*)&sin, sizeof(sin)) < 0) {
if (bind(hc->listen_fd, (struct sockaddr*)&sin, sizeof(sin)) < 0) {
DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: bind(%s:%d) failed: %s", bind_ip, port, socket_strerror(socket_get_error()));
socket_close_wrapper(g_hc.listen_fd); g_hc.listen_fd = SOCKET_INVALID; return -1;
socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1;
}
if (listen(g_hc.listen_fd, 5) < 0) { socket_close_wrapper(g_hc.listen_fd); g_hc.listen_fd = SOCKET_INVALID; return -1; }
if (listen(hc->listen_fd, 5) < 0) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; }
g_hc.listen_sock_id = uasync_add_socket_t(ua, g_hc.listen_fd, accept_callback, NULL, NULL, NULL);
if (!g_hc.listen_sock_id) { socket_close_wrapper(g_hc.listen_fd); g_hc.listen_fd = SOCKET_INVALID; return -1; }
hc->listen_sock_id = uasync_add_socket_t(ua, hc->listen_fd, accept_callback, NULL, NULL, hc);
if (!hc->listen_sock_id) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; }
chat_event_set_handler(hc_on_chat_event);
uasync_set_timeout(ua, 5000, NULL, hc_cleanup, "hc_cleanup");
chat_event_set_handler(inst, hc_on_chat_event);
uasync_set_timeout(ua, 5000, hc, hc_cleanup, "hc_cleanup");
g_hc.running = 1;
hc->running = 1;
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: listening on %s:%d", bind_ip, port);
return 0;
}
void chat_headless_control_destroy(void) {
if (!g_hc.running) return;
g_hc.running = 0;
chat_event_set_handler(NULL);
if (g_hc.listen_sock_id && g_hc.ua) uasync_remove_socket_t(g_hc.ua, g_hc.listen_fd);
if (g_hc.listen_fd != SOCKET_INVALID) { socket_close_wrapper(g_hc.listen_fd); g_hc.listen_fd = SOCKET_INVALID; }
struct headless_client* c = g_hc.clients;
void chat_headless_control_destroy(struct UTUN_INSTANCE* inst) {
struct headless_control* hc = hc_of(inst);
if (!hc || !hc->running) return;
hc->running = 0;
chat_event_set_handler(inst, NULL);
if (hc->listen_sock_id && hc->ua) uasync_remove_socket_t(hc->ua, hc->listen_fd);
if (hc->listen_fd != SOCKET_INVALID) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; }
struct headless_client* c = hc->clients;
while (c) { struct headless_client* n = c->next; if (c->fd != SOCKET_INVALID) socket_close_wrapper(c->fd); u_free(c); c = n; }
g_hc.clients = NULL; g_hc.client_count = 0;
hc->clients = NULL; hc->client_count = 0;
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: destroyed");
}
+1 -1
View File
@@ -20,6 +20,6 @@ struct UTUN_INSTANCE;
int chat_headless_control_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
const char* bind_ip, int port);
void chat_headless_control_destroy(void);
void chat_headless_control_destroy(struct UTUN_INSTANCE* inst);
#endif /* CHAT_HEADLESS_CONTROL_H */
+17 -20
View File
@@ -38,13 +38,11 @@ struct join_key_entry {
uint64_t expiry_tb; /* timebase (0.1ms) истечения */
};
static struct join_key_entry* g_keys = NULL;
static uint64_t _now_tb(void) { return get_time_tb(); }
static void _keys_purge_expired(void) {
static void _keys_purge_expired(struct UTUN_INSTANCE* inst) {
uint64_t now = _now_tb();
struct join_key_entry** p = &g_keys;
struct join_key_entry** p = &inst->join_keys;
while (*p) {
if ((*p)->expiry_tb <= now) {
struct join_key_entry* r = *p;
@@ -59,30 +57,30 @@ static void _keys_purge_expired(void) {
}
}
static void _keys_put(uint64_t channel_id, uint64_t join_key, uint64_t inviter_id) {
_keys_purge_expired();
static void _keys_put(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key, uint64_t inviter_id) {
_keys_purge_expired(inst);
struct join_key_entry* e = u_malloc(sizeof(*e));
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key alloc failed", CJ_ID); return; }
e->join_key = join_key; e->channel_id = channel_id; e->inviter_id = inviter_id;
e->expiry_tb = _now_tb() + (uint64_t)JOIN_KEY_TTL_SECONDS * 10000ULL;
e->next = g_keys;
g_keys = e;
e->next = inst->join_keys;
inst->join_keys = e;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key stored key=%016llx ch=%llu inviter=%016llx ttl=%ds",
CJ_ID, (unsigned long long)join_key, (unsigned long long)channel_id,
(unsigned long long)inviter_id, JOIN_KEY_TTL_SECONDS);
}
static struct join_key_entry* _keys_find(uint64_t channel_id, uint64_t join_key) {
_keys_purge_expired();
for (struct join_key_entry* e = g_keys; e; e = e->next)
static struct join_key_entry* _keys_find(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) {
_keys_purge_expired(inst);
for (struct join_key_entry* e = inst->join_keys; e; e = e->next)
if (e->channel_id == channel_id && e->join_key == join_key) return e;
return NULL;
}
static void _keys_free_all(void) {
struct join_key_entry* e = g_keys;
static void _keys_free_all(struct UTUN_INSTANCE* inst) {
struct join_key_entry* e = inst->join_keys;
while (e) { struct join_key_entry* n = e->next; u_free(e); e = n; }
g_keys = NULL;
inst->join_keys = NULL;
}
/* ── Сериализация member-блока ── */
@@ -142,7 +140,7 @@ void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id,
sqlite3* db = inst->topo_sqlite_db;
/* 0. верификация join-ключа: принимаем только выданные нами (инвайтером) ключи */
struct join_key_entry* ke = _keys_find(group_id, join_key);
struct join_key_entry* ke = _keys_find(inst, group_id, join_key);
if (!ke || ke->inviter_id != inst->node_id) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: process_request invalid join key ch=%s from=%016llx key=%016llx — drop",
CJ_ID, ch_id, (unsigned long long)from_node, (unsigned long long)join_key);
@@ -228,7 +226,7 @@ static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
/* payload: [subcmd:1][join_key:8] */
if (plen < 1 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; }
uint64_t join_key; memcpy(&join_key, payload + 1, 8);
_keys_put(group_id, join_key, from_node);
_keys_put(inst, group_id, join_key, from_node);
} else if (subcmd == JOIN_SUBCMD_REQUEST_FWD) {
/* payload: [subcmd:1][join_key:8][member...] */
if (plen < 1 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; }
@@ -256,7 +254,7 @@ int chat_join_init(struct UTUN_INSTANCE* inst) {
void chat_join_destroy(struct UTUN_INSTANCE* inst) {
if (!inst) return;
etcp_router_unbind(inst, ETCP_RT_ID_JOIN);
_keys_free_all();
_keys_free_all(inst);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: destroyed", CJ_ID);
}
@@ -267,7 +265,7 @@ void chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id,
if (!inst || channel_id == 0 || join_key == 0) return;
/* инвайтер всегда хранит выданный ключ локально — для верификации REQUEST_FWD */
_keys_put(channel_id, join_key, inst->node_id);
_keys_put(inst, channel_id, join_key, inst->node_id);
if (target_node_id == inst->node_id || target_node_id == 0) {
return;
@@ -297,8 +295,7 @@ void chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id,
}
uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) {
(void)inst;
struct join_key_entry* e = _keys_find(channel_id, join_key);
struct join_key_entry* e = _keys_find(inst, channel_id, join_key);
return e ? e->inviter_id : 0;
}
+44 -33
View File
@@ -21,12 +21,13 @@
/* ─── live-онлайн: узел присутствует в BGP/topo_group канала ─── */
int chat_core_member_online(const char* ch_id, uint64_t node_id) {
if (!g_cc.inst || !ch_id || !ch_id[0]) return 0;
if (node_id == g_cc.my_node_id) return 1; /* self всегда онлайн (local_node вне group->nodes) */
if (!g_cc.inst->topo_groups) return 0;
int chat_core_member_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->inst || !ch_id || !ch_id[0]) return 0;
if (node_id == cc->my_node_id) return 1; /* self всегда онлайн (local_node вне group->nodes) */
if (!cc->inst->topo_groups) return 0;
uint64_t group_id = strtoull(ch_id, NULL, 10);
struct TOPO_GROUP* g = topo_groups_find(g_cc.inst->topo_groups, group_id);
struct TOPO_GROUP* g = topo_groups_find(cc->inst->topo_groups, group_id);
if (!g || g->group_type != TOPO_GROUP_TYPE_CHAT) return 0;
return topo_node_find_by_id(g, node_id) != NULL;
}
@@ -42,6 +43,10 @@ static uint8_t parse_adm_tags_flags(const char* adm_tags) {
if (json_flat_get(adm_tags, "supernode", buf, sizeof(buf)) == 0
&& strcmp(buf, "yes") == 0) flags |= CHAT_MEMBER_FLAG_SUPERNODE;
/* dm_storage: узел хранит offline-сообщения прямого чата (DM) */
if (json_flat_get(adm_tags, "dm_storage", buf, sizeof(buf)) == 0
&& strcmp(buf, "yes") == 0) flags |= CHAT_MEMBER_FLAG_DM_STORAGE;
/* admin: значение — накопительная строка ed...ed..., последний символ e/d */
if (json_flat_get(adm_tags, "admin", buf, sizeof(buf)) == 0) {
const char* last = buf;
@@ -91,16 +96,17 @@ int chat_core_deserialize_member(const uint8_t* src, struct chat_member_display*
/* ─── запрос списка мемберов ─── */
int chat_core_get_member_list(const char* ch_id, uint8_t** out, int* count) {
int chat_core_get_member_list(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t** out, int* count) {
struct chat_core_ctx* cc = CC(inst);
if (!out || !count) return -1;
*out = NULL; *count = 0;
if (!g_cc.initialized || !ch_id || !g_cc.db || !g_cc.inst) {
if (!cc || !cc->initialized || !ch_id || !cc->db || !cc->inst) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: get_list — not initialized");
return -1;
}
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
uint64_t my_id = g_cc.my_node_id;
uint64_t my_id = cc->my_node_id;
/* 1. запрашиваем всех мемберов */
sqlite3_stmt* st = NULL;
@@ -112,9 +118,9 @@ int chat_core_get_member_list(const char* ch_id, uint8_t** out, int* count) {
" FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id"
" ORDER BY p.node_id ASC", peers_tbl);
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: get_list — query failed ch=%s err=%s",
ch_id, sqlite3_errmsg(g_cc.db));
ch_id, sqlite3_errmsg(cc->db));
return -1;
}
@@ -130,7 +136,7 @@ int chat_core_get_member_list(const char* ch_id, uint8_t** out, int* count) {
}
struct chat_member_display* m = &members[cnt];
m->node_id = (uint64_t)sqlite3_column_int64(st, 0);
m->online = (uint8_t)chat_core_member_online(ch_id, m->node_id);
m->online = (uint8_t)chat_core_member_online(inst, ch_id, m->node_id);
const char* node_name = (const char*)sqlite3_column_text(st, 1);
const char* local_nick = (const char*)sqlite3_column_text(st, 2);
const char* adm_tags = (const char*)sqlite3_column_text(st, 3);
@@ -179,7 +185,7 @@ int chat_core_get_member_list(const char* ch_id, uint8_t** out, int* count) {
" FROM node_addresses WHERE node_id IN (%s) AND rtt > 0 GROUP BY node_id", id_list);
u_free(id_list);
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
while (sqlite3_step(st) == SQLITE_ROW) {
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
int rtt_val = sqlite3_column_int(st, 1);
@@ -222,12 +228,13 @@ int chat_core_get_member_list(const char* ch_id, uint8_t** out, int* count) {
/* ─── один мембер ─── */
int chat_core_get_single_member(const char* ch_id, uint64_t node_id, uint8_t* out) {
int chat_core_get_single_member(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id, uint8_t* out) {
struct chat_core_ctx* cc = CC(inst);
if (!out) return -1;
if (!g_cc.initialized || !ch_id || !g_cc.db || !g_cc.inst) return -1;
if (!cc || !cc->initialized || !ch_id || !cc->db || !cc->inst) return -1;
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
uint64_t my_id = g_cc.my_node_id;
uint64_t my_id = cc->my_node_id;
sqlite3_stmt* st = NULL;
char sql[384];
@@ -238,13 +245,13 @@ int chat_core_get_single_member(const char* ch_id, uint64_t node_id, uint8_t* ou
" FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id"
" WHERE p.node_id=?", peers_tbl);
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); return -1; }
struct chat_member_display m;
m.node_id = node_id;
m.online = (uint8_t)chat_core_member_online(ch_id, node_id);
m.online = (uint8_t)chat_core_member_online(inst, ch_id, node_id);
const char* node_name = (const char*)sqlite3_column_text(st, 0);
const char* local_nick = (const char*)sqlite3_column_text(st, 1);
const char* adm_tags = (const char*)sqlite3_column_text(st, 2);
@@ -274,7 +281,7 @@ int chat_core_get_single_member(const char* ch_id, uint64_t node_id, uint8_t* ou
" WHEN MAX(addr_type=3) THEN 3"
" ELSE 0 END"
" FROM node_addresses WHERE node_id=? AND rtt > 0");
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
int rtt_val = sqlite3_column_int(st, 0);
@@ -291,13 +298,14 @@ int chat_core_get_single_member(const char* ch_id, uint64_t node_id, uint8_t* ou
/* ─── трамплины для gui_bridge ─── */
void chat_core_request_member_list_trampoline(void* arg) {
if (!arg || !g_cc.initialized) { u_free(arg); return; }
char ch_id[64]; strncpy(ch_id, (const char*)arg, sizeof(ch_id) - 1); ch_id[sizeof(ch_id) - 1] = '\0';
struct chat_member_list_req* req = (struct chat_member_list_req*)arg;
if (!req || !CC(req->inst) || !CC(req->inst)->initialized) { u_free(arg); return; }
char ch_id[64]; strncpy(ch_id, req->ch_id, sizeof(ch_id) - 1); ch_id[sizeof(ch_id) - 1] = '\0';
u_free(arg);
uint8_t* members = NULL;
int count = 0;
if (chat_core_get_member_list(ch_id, &members, &count) != 0 || !members) return;
if (chat_core_get_member_list(req->inst, ch_id, &members, &count) != 0 || !members) return;
size_t ch_len = strlen(ch_id);
size_t evt_sz = 1 + ch_len + 2 + (size_t)count * CHAT_MEMBER_DISPLAY_SIZE;
@@ -309,20 +317,21 @@ void chat_core_request_member_list_trampoline(void* arg) {
memcpy(evt + 1, ch_id, ch_len);
memcpy(evt + 1 + ch_len, &u16cnt, 2);
memcpy(evt + 1 + ch_len + 2, members, (size_t)count * CHAT_MEMBER_DISPLAY_SIZE);
chat_event_post(CHAT_EVT_MEMBER_LIST, evt, (int)evt_sz);
chat_event_post(req->inst, CHAT_EVT_MEMBER_LIST, evt, (int)evt_sz);
u_free(evt);
u_free(members);
}
void chat_core_request_member_rtt_trampoline(void* arg) {
if (!arg || !g_cc.initialized) { u_free(arg); return; }
char ch_id[64]; strncpy(ch_id, (const char*)arg, sizeof(ch_id) - 1); ch_id[sizeof(ch_id) - 1] = '\0';
struct chat_member_list_req* req = (struct chat_member_list_req*)arg;
if (!req || !CC(req->inst) || !CC(req->inst)->initialized) { u_free(arg); return; }
char ch_id[64]; strncpy(ch_id, req->ch_id, sizeof(ch_id) - 1); ch_id[sizeof(ch_id) - 1] = '\0';
u_free(arg);
uint8_t* members = NULL;
int count = 0;
if (chat_core_get_member_list(ch_id, &members, &count) != 0 || !members) return;
if (chat_core_get_member_list(req->inst, ch_id, &members, &count) != 0 || !members) return;
size_t ch_len = strlen(ch_id);
size_t evt_sz = 1 + ch_len + 1 + CHAT_MEMBER_DISPLAY_SIZE;
@@ -339,7 +348,7 @@ void chat_core_request_member_rtt_trampoline(void* arg) {
memcpy(evt + 1, ch_id, ch_len);
evt[1 + ch_len] = 1;
memcpy(evt + 1 + ch_len + 1, members + i * CHAT_MEMBER_DISPLAY_SIZE, CHAT_MEMBER_DISPLAY_SIZE);
chat_event_post(CHAT_EVT_MEMBER_UPDATED, evt, (int)evt_sz);
chat_event_post(req->inst, CHAT_EVT_MEMBER_UPDATED, evt, (int)evt_sz);
}
u_free(evt);
@@ -349,14 +358,16 @@ void chat_core_request_member_rtt_trampoline(void* arg) {
/* ─── node_props_changed callback (BGP-события узла → MEMBER_UPDATED в GUI) ─── */
static void on_member_props_changed(uint64_t node_id, const char* adm_tags, const char* channel_id, void* arg) {
(void)adm_tags; (void)arg;
if (!g_cc.initialized || !g_cc.inst || !g_cc.db || !channel_id || !channel_id[0]) return;
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
struct chat_core_ctx* cc = CC(inst);
(void)adm_tags;
if (!cc || !cc->initialized || !cc->inst || !cc->db || !channel_id || !channel_id[0]) return;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[256]; snprintf(sql, sizeof(sql), "SELECT 1 FROM \"%s\" WHERE node_id=?", peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
sqlite3_finalize(st);
@@ -365,15 +376,15 @@ static void on_member_props_changed(uint64_t node_id, const char* adm_tags, cons
evt[0] = (uint8_t)cl;
memcpy(evt + 1, channel_id, cl);
evt[1 + cl] = 1;
if (chat_core_get_single_member(channel_id, node_id, evt + 1 + cl + 1) == 0)
chat_event_post(CHAT_EVT_MEMBER_UPDATED, evt, 1 + (int)cl + 1 + CHAT_MEMBER_DISPLAY_SIZE);
if (chat_core_get_single_member(inst, channel_id, node_id, evt + 1 + cl + 1) == 0)
chat_event_post(inst, CHAT_EVT_MEMBER_UPDATED, evt, 1 + (int)cl + 1 + CHAT_MEMBER_DISPLAY_SIZE);
return;
}
sqlite3_finalize(st);
}
}
void chat_member_init(void) {
member_sync_add_props_cbk(on_member_props_changed, NULL);
void chat_member_init(struct UTUN_INSTANCE* inst) {
member_sync_add_props_cbk(inst, on_member_props_changed, inst);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: initialized (BGP node events → MEMBER_UPDATED)");
}
+8 -6
View File
@@ -30,18 +30,19 @@ extern "C" {
#define CHAT_MEMBER_FLAG_ADMIN 0x02
#define CHAT_MEMBER_FLAG_MODER 0x04
#define CHAT_MEMBER_FLAG_DELETED 0x08
#define CHAT_MEMBER_FLAG_DM_STORAGE 0x10
#define CHAT_MEMBER_DISPLAY_SIZE 79
/* Получить всех мемберов канала. out = u_malloc-массив, count = количество. */
int chat_core_get_member_list(const char* ch_id, uint8_t** out, int* count);
int chat_core_get_member_list(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t** out, int* count);
/* Получить одного мембера по node_id. out = буфер размером CHAT_MEMBER_DISPLAY_SIZE. */
int chat_core_get_single_member(const char* ch_id, uint64_t node_id, uint8_t* out);
int chat_core_get_single_member(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id, uint8_t* out);
/* Live-онлайн: узел присутствует в BGP/topo_group канала (self всегда онлайн).
Не читает nodes.online — статус не хранится в БД. */
int chat_core_member_online(const char* ch_id, uint64_t node_id);
int chat_core_member_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id);
/* Сериализовать из внутренней структуры в wire-формат */
struct chat_member_display {
@@ -59,15 +60,16 @@ void chat_core_serialize_member(const struct chat_member_display* src, uint8_t*
int chat_core_deserialize_member(const uint8_t* src, struct chat_member_display* dst);
/* Трамплин для gui_bridge: GUI запрашивает полный список мемберов канала.
* arg = вызов u_strdup(ch_id). Результат: CHAT_EVT_MEMBER_LIST. */
* arg = struct chat_member_list_req*. Результат: CHAT_EVT_MEMBER_LIST. */
struct chat_member_list_req { struct UTUN_INSTANCE* inst; char ch_id[64]; };
void chat_core_request_member_list_trampoline(void* arg);
/* Трамплин для gui_bridge: GUI запрашивает RTT мемберов канала.
* arg = вызов u_strdup(ch_id). Результат: CHAT_EVT_MEMBER_UPDATED с RTT-обновлениями. */
* arg = struct chat_member_list_req*. Результат: CHAT_EVT_MEMBER_UPDATED с RTT-обновлениями. */
void chat_core_request_member_rtt_trampoline(void* arg);
/* Инициализация: регистрирует node_props_changed callback для рассылки MEMBER_UPDATED. */
void chat_member_init(void);
void chat_member_init(struct UTUN_INSTANCE* inst);
#ifdef __cplusplus
}
+222 -212
View File
@@ -27,24 +27,22 @@
chat_whisper_trigger_fn g_chat_whisper_trigger = NULL;
/* forward decl */
void chat_core_submit_media_message(struct chat_msg_submit* req);
void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req);
static void wh_transcribe_done_cb(void* arg, const char* ch_id, const char* text, int err,
uint64_t reply_ts, uint64_t reply_node);
static void chat_media_cache_cleanup_orphans(void);
static void chat_media_cache_enforce(void);
static void chat_media_cache_reconcile(void);
static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst);
static void chat_media_cache_enforce(struct UTUN_INSTANCE* inst);
static void chat_media_cache_reconcile(struct UTUN_INSTANCE* inst);
/* ─── счётчик объёма медиакеша (сумма файлов, соответствующих сообщениям) ─── */
static uint64_t g_media_cache_bytes = 0;
static void cache_bytes_add(int64_t sz) {
if (sz > 0) g_media_cache_bytes += (uint64_t)sz;
static void cache_bytes_add(struct chat_core_ctx* cc, int64_t sz) {
if (sz > 0) cc->media_cache_bytes += (uint64_t)sz;
}
static void cache_bytes_sub(int64_t sz) {
static void cache_bytes_sub(struct chat_core_ctx* cc, int64_t sz) {
uint64_t v = sz > 0 ? (uint64_t)sz : 0;
g_media_cache_bytes = g_media_cache_bytes >= v ? g_media_cache_bytes - v : 0;
cc->media_cache_bytes = cc->media_cache_bytes >= v ? cc->media_cache_bytes - v : 0;
}
/* Извлекает имя файла из local_attrs: {"st":"fl","fp":"<filename>"} */
@@ -61,11 +59,11 @@ static void extract_attrs_fp(const char* local_attrs, char* out, size_t out_sz)
/* ─── отправка сообщения (GUI → uasync) ─── */
static int msg_get_prev_chain_hash(const char* tbl, uint8_t out[32]) {
static int msg_get_prev_chain_hash(struct chat_core_ctx* cc, const char* tbl, uint8_t out[32]) {
char sql[128]; snprintf(sql, sizeof(sql),
"SELECT chain_hash FROM \"%s\" ORDER BY timestamp DESC LIMIT 1", tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) { memset(out,0,32); return -1; }
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) { memset(out,0,32); return -1; }
if (sqlite3_step(st) == SQLITE_ROW) {
const void* blob = sqlite3_column_blob(st, 0); int blen = sqlite3_column_bytes(st, 0);
if (blob && blen == 32) memcpy(out, blob, 32); else memset(out, 0, 32);
@@ -73,20 +71,21 @@ static int msg_get_prev_chain_hash(const char* tbl, uint8_t out[32]) {
sqlite3_finalize(st); return 0;
}
void chat_core_submit_message(struct chat_msg_submit* req) {
if (!g_cc.initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: submit before init", CC_ID); return; }
void chat_core_submit_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: submit before init", CC_ID); return; }
if (!req) return;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: SUBMIT ch=%s ct=%s len=%u",
CC_ID, req->channel_id, req->content_type, req->data_len);
struct DB_SYNC_INSTANCE* si = si_find(req->channel_id);
struct DB_SYNC_INSTANCE* si = si_find(inst, req->channel_id);
if (!si) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: no db_sync instance for ch=%s", CC_ID, req->channel_id); return; }
char json[4096];
int off = snprintf(json, sizeof(json),
"{\"n\":%llu,\"ch\":\"%s\",\"ct\":\"%s\"",
(unsigned long long)g_cc.my_node_id, req->channel_id, req->content_type);
(unsigned long long)cc->my_node_id, req->channel_id, req->content_type);
if (req->reply_to_ts) {
off += snprintf(json + off, sizeof(json) - off,
",\"rt\":%llu,\"rn\":%llu",
@@ -101,7 +100,7 @@ void chat_core_submit_message(struct chat_msg_submit* req) {
memcpy(sig_msg + soff, &ts, 8); soff += 8;
size_t jl = strlen(json); memcpy(sig_msg + soff, json, jl); soff += jl;
uint8_t sig[64];
if (sc_ed25519_sign(g_cc.inst->my_ed25519_privkey, sig_msg, soff, sig) != SC_OK) {
if (sc_ed25519_sign(inst->my_ed25519_privkey, sig_msg, soff, sig) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: Ed25519 sign failed", CC_ID);
return;
}
@@ -115,16 +114,18 @@ void chat_core_submit_message(struct chat_msg_submit* req) {
void chat_core_submit_trampoline(void* arg) {
struct chat_msg_submit* req = (struct chat_msg_submit*)arg;
if (!req) return;
if (req->media_src[0] != '\0')
chat_core_submit_media_message(req);
chat_core_submit_media_message(req->inst, req);
else
chat_core_submit_message(req);
chat_core_submit_message(req->inst, req);
u_free(req);
}
/* ─── Медиа-сообщения: async регистрация в media_files + отправка ─── */
struct media_submit_ctx {
struct UTUN_INSTANCE* inst;
char channel_id[64];
char content_type[32];
char media_dest[1024];
@@ -138,6 +139,7 @@ struct media_submit_ctx {
static void on_media_registered(void* arg, int err, const struct media_index_result* result) {
struct media_submit_ctx* mctx = (struct media_submit_ctx*)arg;
struct chat_core_ctx* cc = CC(mctx->inst);
if (err || !result) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media registration failed ch=%s err=%d",
@@ -146,7 +148,7 @@ static void on_media_registered(void* arg, int err, const struct media_index_res
return;
}
struct DB_SYNC_INSTANCE* si = si_find(mctx->channel_id);
struct DB_SYNC_INSTANCE* si = si_find(mctx->inst, mctx->channel_id);
if (!si) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: no db_sync instance for ch=%s", CC_ID, mctx->channel_id);
u_free(mctx);
@@ -199,7 +201,7 @@ static void on_media_registered(void* arg, int err, const struct media_index_res
if (!json) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: json alloc fail", CC_ID); u_free(full_data); u_free(mctx); return; }
snprintf(json, json_cap,
"{\"n\":%llu,\"ch\":\"%s\",\"ct\":\"%s\",\"d\":\"%.*s\"}",
(unsigned long long)g_cc.my_node_id, mctx->channel_id,
(unsigned long long)cc->my_node_id, mctx->channel_id,
mctx->content_type, (int)full_data_len, full_data);
uint64_t ts = db_sync_next_timestamp(si);
@@ -209,7 +211,7 @@ static void on_media_registered(void* arg, int err, const struct media_index_res
memcpy(sig_msg, &ts, 8);
memcpy(sig_msg + 8, json, sig_msg_len - 8);
uint8_t json_sig[64];
if (sc_ed25519_sign(g_cc.inst->my_ed25519_privkey, sig_msg, sig_msg_len, json_sig) != SC_OK) {
if (sc_ed25519_sign(mctx->inst->my_ed25519_privkey, sig_msg, sig_msg_len, json_sig) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: Ed25519 sign JSON failed", CC_ID);
u_free(sig_msg); u_free(json); u_free(full_data); u_free(mctx); return;
}
@@ -223,23 +225,23 @@ static void on_media_registered(void* arg, int err, const struct media_index_res
const char* base = strrchr(mctx->media_dest, '/');
const char* filename = base ? base + 1 : mctx->media_dest;
char attrs[1100]; snprintf(attrs, sizeof(attrs), "{\"st\":\"fl\",\"fp\":\"%s\"}", filename);
chat_core_update_local_attrs(mctx->channel_id, ts, json_sig, attrs);
chat_core_update_local_attrs(mctx->inst, mctx->channel_id, ts, json_sig, attrs);
/* анонсируем блоки суперузлам — иначе первый скачивающий не найдёт держателя */
media_delivery_announce_media(g_cc.inst, strtoull(mctx->channel_id, NULL, 10),
media_delivery_announce_media(mctx->inst, strtoull(mctx->channel_id, NULL, 10),
result->media_id, result->block_ids, result->num_blocks);
/* учесть локально отправленный файл в счётчике и применить лимит медиакеша */
cache_bytes_add(result->file_size);
chat_media_cache_enforce();
cache_bytes_add(cc, result->file_size);
chat_media_cache_enforce(mctx->inst);
/* whisper транскрипция для своих голосовых сообщений */
if (mctx->content_type[0] && strncmp(mctx->content_type, "voice", 5) == 0
&& g_cc.inst && g_cc.inst->media_async && g_chat_whisper_trigger) {
g_chat_whisper_trigger(g_cc.inst->media_async, g_cc.inst->ua,
&& mctx->inst && mctx->inst->media_async && g_chat_whisper_trigger) {
g_chat_whisper_trigger(mctx->inst, mctx->inst->media_async, mctx->inst->ua,
mctx->media_dest, mctx->channel_id,
ts, g_cc.my_node_id,
wh_transcribe_done_cb, NULL);
ts, cc->my_node_id,
wh_transcribe_done_cb, mctx->inst);
}
}
@@ -248,6 +250,7 @@ static void on_media_registered(void* arg, int err, const struct media_index_res
static void on_video_prepared(void* arg, int err) {
struct media_submit_ctx* mctx = (struct media_submit_ctx*)arg;
struct chat_core_ctx* cc = CC(mctx->inst);
if (err) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: video prepare failed ch=%s err=%d",
@@ -268,28 +271,29 @@ static void on_video_prepared(void* arg, int err) {
char media_base[1024];
{
const char* last_slash = strrchr(g_cc.db_path, '/');
const char* last_slash = strrchr(cc->db_path, '/');
if (last_slash)
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - g_cc.db_path), g_cc.db_path);
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
else
snprintf(media_base, sizeof(media_base), "%s", g_cc.db_path);
snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
}
media_index_register_async(
g_cc.inst->media_async, g_cc.inst->ua, g_cc.db,
g_cc.my_node_id, g_cc.inst->my_ed25519_privkey,
mctx->inst->media_async, mctx->inst->ua, cc->db,
cc->my_node_id, mctx->inst->my_ed25519_privkey,
mctx->channel_id,
mctx->media_dest, mctx->media_dest, 0,
media_base,
on_media_registered, mctx);
}
void chat_core_submit_media_message(struct chat_msg_submit* req) {
if (!g_cc.initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media submit before init", CC_ID); return; }
void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media submit before init", CC_ID); return; }
if (!req || req->media_src[0] == '\0') return;
/* лимит одного медиафайла применяется и к локальным отправкам */
uint64_t unit = g_cc.inst->config->global.chatserver_storage_unit_size;
uint64_t unit = inst->config->global.chatserver_storage_unit_size;
if (unit > 0) {
int64_t sz = (int64_t)ma_file_size(req->media_src);
if (sz > 0 && (uint64_t)sz > unit) {
@@ -303,7 +307,7 @@ void chat_core_submit_media_message(struct chat_msg_submit* req) {
CC_ID, req->channel_id, req->content_type,
req->media_src, req->media_dest, req->media_copy, req->media_video);
if (!g_cc.inst->media_async) {
if (!inst->media_async) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media_async not initialized", CC_ID);
return;
}
@@ -311,16 +315,17 @@ void chat_core_submit_media_message(struct chat_msg_submit* req) {
/* build media_base from db_path: dirname(db_path) e.g. /path/to/data */
char media_base[1024];
{
const char* last_slash = strrchr(g_cc.db_path, '/');
const char* last_slash = strrchr(cc->db_path, '/');
if (last_slash)
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - g_cc.db_path), g_cc.db_path);
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
else
snprintf(media_base, sizeof(media_base), "%s", g_cc.db_path);
snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
}
/* copy fields for async callback (req is freed by trampoline immediately) */
struct media_submit_ctx* mctx = u_calloc(1, sizeof(*mctx));
if (!mctx) return;
mctx->inst = inst;
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);
@@ -334,14 +339,14 @@ void chat_core_submit_media_message(struct chat_msg_submit* req) {
}
if (req->media_video) {
video_transcode_start(g_cc.inst->ua, req->media_src, req->media_dest,
video_transcode_start(inst->ua, req->media_src, req->media_dest,
on_video_prepared, mctx);
return;
}
media_index_register_async(
g_cc.inst->media_async, g_cc.inst->ua, g_cc.db,
g_cc.my_node_id, g_cc.inst->my_ed25519_privkey,
inst->media_async, inst->ua, cc->db,
cc->my_node_id, inst->my_ed25519_privkey,
req->channel_id,
req->media_src, req->media_dest, req->media_copy ? 1 : 0,
media_base,
@@ -350,11 +355,12 @@ void chat_core_submit_media_message(struct chat_msg_submit* req) {
/* ─── Обновление local_attrs (для будущей приёмной стороны) ─── */
int chat_core_update_local_attrs(const char* ch_id, uint64_t ts,
int chat_core_update_local_attrs(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t ts,
const uint8_t* author_sig,
const char* local_attrs_json)
{
if (!g_cc.initialized || !ch_id || !author_sig || !local_attrs_json) return -1;
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !author_sig || !local_attrs_json) return -1;
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
char sql[256];
@@ -362,8 +368,8 @@ int chat_core_update_local_attrs(const char* ch_id, uint64_t ts,
"UPDATE \"%s\" SET local_attrs=? WHERE timestamp=? AND author_signature=?", tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: update local_attrs prep fail: %s", CC_ID, sqlite3_errmsg(g_cc.db));
if (sqlite3_prepare_v2(cc->db, sql, -1, &stmt, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: update local_attrs prep fail: %s", CC_ID, sqlite3_errmsg(cc->db));
return -1;
}
sqlite3_bind_text(stmt, 1, local_attrs_json, -1, SQLITE_STATIC);
@@ -381,16 +387,17 @@ int chat_core_update_local_attrs(const char* ch_id, uint64_t ts,
/* ─── Отметка голосового как проигранного ─── */
void chat_core_mark_voice_played(const char* ch_id, uint64_t ts, uint64_t author_node_id) {
if (!g_cc.initialized || !ch_id || ts == 0) return;
void chat_core_mark_voice_played(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t ts, uint64_t author_node_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || ts == 0) return;
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
char sql[256];
snprintf(sql, sizeof(sql),
"SELECT local_attrs, author_signature FROM \"%s\" WHERE timestamp=? AND node_id=? LIMIT 1", tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: mark_voice_played prep fail: %s", CC_ID, sqlite3_errmsg(g_cc.db));
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: mark_voice_played prep fail: %s", CC_ID, sqlite3_errmsg(cc->db));
return;
}
sqlite3_bind_int64(st, 1, (sqlite3_int64)ts);
@@ -419,25 +426,27 @@ void chat_core_mark_voice_played(const char* ch_id, uint64_t ts, uint64_t author
}
sqlite3_finalize(st);
chat_core_update_local_attrs(ch_id, ts, author_sig, new_attrs);
chat_core_update_local_attrs(inst, ch_id, ts, author_sig, new_attrs);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: voice marked played ch=%s ts=%llu", CC_ID, ch_id, (unsigned long long)ts);
}
void chat_core_mark_voice_played_trampoline(void* arg) {
struct voice_played_req* req = (struct voice_played_req*)arg;
chat_core_mark_voice_played(req->channel_id, req->ts, req->author_node_id);
if (!req) return;
chat_core_mark_voice_played(req->inst, req->channel_id, req->ts, req->author_node_id);
u_free(req);
}
/* ─── DB-операции для chat_sync ─── */
uint32_t chat_core_count(const char* ch_id) {
if (!g_cc.initialized) return 0;
uint32_t chat_core_count(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized) return 0;
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
char sql[128]; snprintf(sql, sizeof(sql),
"SELECT COUNT(*) FROM \"%s\"", tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0;
if (sqlite3_prepare_v2(cc->db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0;
uint32_t cnt = 0;
if (sqlite3_step(stmt) == SQLITE_ROW)
cnt = (uint32_t)sqlite3_column_int64(stmt, 0);
@@ -445,14 +454,15 @@ uint32_t chat_core_count(const char* ch_id) {
return cnt;
}
int chat_core_chain_hash_at(const char* ch_id, uint32_t pos, uint8_t* hash_out) {
if (!g_cc.initialized || !hash_out) return -1;
int chat_core_chain_hash_at(struct UTUN_INSTANCE* inst, const char* ch_id, uint32_t pos, uint8_t* hash_out) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !hash_out) return -1;
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
char sql[200]; snprintf(sql, sizeof(sql),
"SELECT chain_hash FROM \"%s\" ORDER BY timestamp, node_id ASC LIMIT 1 OFFSET ?",
tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &stmt, NULL) != SQLITE_OK) {
memset(hash_out, 0, 32); return -1;
}
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)pos);
@@ -468,10 +478,11 @@ int chat_core_chain_hash_at(const char* ch_id, uint32_t pos, uint8_t* hash_out)
return 0;
}
int chat_core_list_channels(uint8_t* buf, size_t buf_size, size_t* out_len) {
if (!g_cc.initialized || !buf || !out_len) return -1;
int chat_core_list_channels(struct UTUN_INSTANCE* inst, uint8_t* buf, size_t buf_size, size_t* out_len) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !buf || !out_len) return -1;
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db,
if (sqlite3_prepare_v2(cc->db,
"SELECT channel_id FROM channels ORDER BY created_at ASC",
-1, &stmt, NULL) != SQLITE_OK) return -1;
@@ -497,14 +508,15 @@ int chat_core_list_channels(uint8_t* buf, size_t buf_size, size_t* out_len) {
return 0;
}
int chat_core_list_peers(const char* ch_id, uint8_t* buf, size_t buf_size,
int chat_core_list_peers(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t* buf, size_t buf_size,
size_t* out_len) {
if (!g_cc.initialized || !buf || !out_len) return -1;
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !buf || !out_len) return -1;
char tbl[80]; peers_table_name(ch_id, tbl, sizeof(tbl));
char sql[128]; snprintf(sql, sizeof(sql),
"SELECT node_id FROM \"%s\" WHERE deleted=0", tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &stmt, NULL) != SQLITE_OK) {
uint16_t z = 0; memcpy(buf, &z, 2); *out_len = 2; return -1;
}
uint16_t cnt = 0;
@@ -520,12 +532,13 @@ int chat_core_list_peers(const char* ch_id, uint8_t* buf, size_t buf_size,
return 0;
}
int chat_core_load_nodeinfo(uint64_t node_id, uint8_t* buf, size_t buf_size,
int chat_core_load_nodeinfo(struct UTUN_INSTANCE* inst, uint64_t node_id, uint8_t* buf, size_t buf_size,
size_t* out_len) {
if (!g_cc.initialized || !buf || !out_len) return -1;
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !buf || !out_len) return -1;
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db,
if (sqlite3_prepare_v2(cc->db,
"SELECT x25519_pubkey, ed25519_pubkey FROM nodes WHERE node_id=?",
-1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
@@ -544,7 +557,7 @@ int chat_core_load_nodeinfo(uint64_t node_id, uint8_t* buf, size_t buf_size,
out += 32;
sqlite3_finalize(stmt);
if (sqlite3_prepare_v2(g_cc.db,
if (sqlite3_prepare_v2(cc->db,
"SELECT family, protocol, address, port, rtt FROM node_addresses WHERE node_id=?",
-1, &stmt, NULL) != SQLITE_OK) {
*out++ = 0; *out_len = (size_t)(out - buf); return 0;
@@ -580,6 +593,7 @@ int chat_core_load_nodeinfo(uint64_t node_id, uint8_t* buf, size_t buf_size,
/* ─── download completion ─── */
struct md_done_ctx {
struct UTUN_INSTANCE* inst;
char channel_id[64];
uint64_t ts;
int64_t msg_id;
@@ -598,46 +612,47 @@ static void md_download_progress_cb(void* arg, int blocks_done, int num_blocks)
memcpy(evt + 1 + cl, &ctx->msg_id, 8);
uint32_t bd = (uint32_t)blocks_done, nb = (uint32_t)num_blocks;
memcpy(evt + 1 + cl + 8, &bd, 4); memcpy(evt + 1 + cl + 12, &nb, 4);
chat_event_post(CHAT_EVT_DOWNLOAD_PROGRESS, evt, 1 + cl + 16);
chat_event_post(ctx->inst, CHAT_EVT_DOWNLOAD_PROGRESS, evt, 1 + cl + 16);
}
static void md_download_done_cb(void* arg, int err) {
struct md_done_ctx* ctx = (struct md_done_ctx*)arg;
struct chat_core_ctx* cc = CC(ctx->inst);
if (!err) {
char attrs[1024];
snprintf(attrs, sizeof(attrs), "{\"st\":\"fl\",\"fp\":\"%s\"}", ctx->dest_relpath);
chat_core_update_local_attrs(ctx->channel_id, ctx->ts, ctx->author_sig, attrs);
chat_core_update_local_attrs(ctx->inst, ctx->channel_id, ctx->ts, ctx->author_sig, attrs);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: media downloaded ch=%s", CC_ID, ctx->channel_id);
/* whisper транскрипция для голосовых сообщений */
if (ctx->content_type[0] && strncmp(ctx->content_type, "voice", 5) == 0
&& g_cc.inst && g_cc.inst->media_async && g_chat_whisper_trigger) {
g_chat_whisper_trigger(g_cc.inst->media_async, g_cc.inst->ua,
&& ctx->inst && ctx->inst->media_async && g_chat_whisper_trigger) {
g_chat_whisper_trigger(ctx->inst, ctx->inst->media_async, ctx->inst->ua,
ctx->dest_abs_path, ctx->channel_id,
ctx->ts, ctx->author_node_id,
wh_transcribe_done_cb, NULL);
wh_transcribe_done_cb, ctx->inst);
}
/* после докачки — учесть файл в счётчике и применить лимит медиакеша */
cache_bytes_add(ma_file_size(ctx->dest_abs_path));
chat_media_cache_enforce();
cache_bytes_add(cc, ma_file_size(ctx->dest_abs_path));
chat_media_cache_enforce(ctx->inst);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "%s: media download failed ch=%s err=%d", CC_ID, ctx->channel_id, err);
/* пометить сообщение ошибкой, чтобы UI показал «повторить» */
char attrs[64];
snprintf(attrs, sizeof(attrs), "{\"st\":\"er\"}");
chat_core_update_local_attrs(ctx->channel_id, ctx->ts, ctx->author_sig, attrs);
chat_core_update_local_attrs(ctx->inst, ctx->channel_id, ctx->ts, ctx->author_sig, attrs);
}
uint8_t evt[80]; uint8_t cl = (uint8_t)strlen(ctx->channel_id);
evt[0] = cl; memcpy(evt + 1, ctx->channel_id, cl);
memcpy(evt + 1 + cl, &ctx->msg_id, 8);
chat_event_post(CHAT_EVT_ATTACHMENT_DOWNLOADED, evt, 1 + cl + 8);
chat_event_post(ctx->inst, CHAT_EVT_ATTACHMENT_DOWNLOADED, evt, 1 + cl + 8);
u_free(ctx);
}
static void wh_transcribe_done_cb(void* arg, const char* ch_id, const char* text, int err,
uint64_t reply_ts, uint64_t reply_node) {
(void)arg;
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
if (err || !text || !text[0]) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "%s: transcription failed ch=%s err=%d", CC_ID, ch_id, err);
return;
@@ -645,17 +660,18 @@ static void wh_transcribe_done_cb(void* arg, const char* ch_id, const char* text
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: transcription result ch=%s: [%s]", CC_ID, ch_id, text);
/* mark original voice message as played */
chat_core_mark_voice_played(ch_id, reply_ts, reply_node);
chat_core_mark_voice_played(inst, ch_id, reply_ts, reply_node);
struct chat_msg_submit* req = u_calloc(1, sizeof(*req));
if (!req) return;
req->inst = inst;
snprintf(req->channel_id, sizeof(req->channel_id), "%s", ch_id);
snprintf(req->content_type, sizeof(req->content_type), "voice_transcription");
req->data = (uint8_t*)text;
req->data_len = (uint32_t)strlen(text);
req->reply_to_ts = reply_ts;
req->reply_to_node_id = reply_node;
chat_core_submit_message(req);
chat_core_submit_message(inst, req);
u_free(req);
}
@@ -758,6 +774,7 @@ static int md_start_download(struct UTUN_INSTANCE* inst,
uint64_t author_node_id, int64_t msg_id,
const char* content_type) {
(void)data_len;
struct chat_core_ctx* cc = CC(inst);
struct media_index_result result;
if (chat_msg_parse_media_body(data_str, &result) != 0) return -1;
int nb = result.num_blocks;
@@ -769,17 +786,18 @@ static int md_start_download(struct UTUN_INSTANCE* inst,
char dest[1024], media_base[512];
{
const char* last_slash = strrchr(g_cc.db_path, '/');
const char* last_slash = strrchr(cc->db_path, '/');
if (last_slash) {
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - g_cc.db_path), g_cc.db_path);
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
} else {
snprintf(media_base, sizeof(media_base), "%s", g_cc.db_path);
snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
}
}
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; }
ctx->inst = inst;
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", final_name);
@@ -803,8 +821,8 @@ static int md_auto_download(struct UTUN_INSTANCE* inst,
uint64_t ts, const uint8_t* author_sig,
uint64_t author, int64_t msg_id,
const char* content_type) {
if (!chat_setting_get_int("storage_autoload", 1)) return 0;
uint64_t max_size = g_cc.inst->config->global.chatserver_storage_unit_size;
if (!chat_setting_get_int(inst, "storage_autoload", 1)) return 0;
uint64_t max_size = inst->config->global.chatserver_storage_unit_size;
const char* p = data_str;
while (*p && *p != '|') p++;
@@ -822,13 +840,17 @@ static int md_auto_download(struct UTUN_INSTANCE* inst,
void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, void* arg) {
(void)si;
const char* ch_id = (const char*)arg;
struct msg_insert_arg* ia = (struct msg_insert_arg*)arg;
struct UTUN_INSTANCE* inst = ia ? ia->inst : NULL;
struct chat_core_ctx* cc = CC(inst);
if (!ia || !cc) return;
const char* ch_id = ia->ch_id;
uint8_t evt[73]; uint8_t cl = (uint8_t)strlen(ch_id); evt[0] = cl; memcpy(evt + 1, ch_id, cl);
memcpy(evt + 1 + cl, &author, 8);
chat_event_post(CHAT_EVT_MSG_RECEIVED, evt, 1 + cl + 8);
chat_event_post(inst, CHAT_EVT_MSG_RECEIVED, evt, 1 + cl + 8);
/* auto-download media if message contains media metadata */
if (data && len > 0 && author != g_cc.my_node_id) {
if (data && len > 0 && author != cc->my_node_id) {
/* data from db_sync may not be null-terminated; copy to local buffer */
char buf[4096]; size_t blen = len < sizeof(buf) - 1 ? len : sizeof(buf) - 1;
memcpy(buf, data, blen); buf[blen] = '\0';
@@ -851,12 +873,7 @@ void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char
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};
if (sig_field) {
/* read author_signature from DB — find by ts in on_msg_inserted we don't have sig
so leave as zeros; use record_ts to locate the record later */
}
/* We don't have author_sig in the callback; derive from body content.
Instead, read it from the DB using the just-inserted record */
@@ -864,7 +881,7 @@ void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char
char sql[256];
snprintf(sql, sizeof(sql), "SELECT author_signature, id FROM \"%s\" WHERE timestamp=? AND node_id=? LIMIT 1", tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)record_ts);
sqlite3_bind_int64(st, 2, (sqlite3_int64)author);
int64_t msg_id = 0;
@@ -875,13 +892,13 @@ void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char
msg_id = sqlite3_column_int64(st, 1);
}
sqlite3_finalize(st);
if (msg_id > 0) md_auto_download(g_cc.inst, body, bi, ch_id, base_filename, record_ts, author_sig, author, msg_id, content_type);
if (msg_id > 0) md_auto_download(inst, body, bi, ch_id, base_filename, record_ts, author_sig, author, msg_id, content_type);
}
}
}
sqlite3_stmt* st = NULL;
sqlite3_prepare_v2(g_cc.db, "UPDATE channels SET last_msg_at = ? WHERE channel_id = ?", -1, &st, NULL);
sqlite3_prepare_v2(cc->db, "UPDATE channels SET last_msg_at = ? WHERE channel_id = ?", -1, &st, NULL);
if (st) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)record_ts);
sqlite3_bind_text(st, 2, ch_id, -1, SQLITE_STATIC);
@@ -891,13 +908,14 @@ void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char
/* ─── manual attachment download (called from JNI bridge via uasync post) ─── */
void chat_core_attachment_download(const char* channel_id, int64_t msg_id) {
if (!channel_id || msg_id <= 0) return;
void chat_core_attachment_download(struct UTUN_INSTANCE* inst, const char* channel_id, int64_t msg_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !channel_id || msg_id <= 0) return;
char tbl[80]; msg_table_name(channel_id, tbl, sizeof(tbl));
char sql[256];
snprintf(sql, sizeof(sql), "SELECT data, timestamp, author_signature, node_id FROM \"%s\" WHERE id=?", tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "%s: download sql prep failed ch=%s", CC_ID, channel_id);
return;
}
@@ -945,12 +963,13 @@ void chat_core_attachment_download(const char* channel_id, int64_t msg_id) {
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);
md_start_download(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) {
struct attachment_dl_req* req = (struct attachment_dl_req*)arg;
chat_core_attachment_download(req->channel_id, req->msg_id);
if (!req) return;
chat_core_attachment_download(req->inst, req->channel_id, req->msg_id);
u_free(req);
}
@@ -958,22 +977,19 @@ void chat_core_attachment_download_trampoline(void* arg) {
chat_core_attachment_download_trampoline_impl(arg);
}
/* ─── Backfill: регистрация уже скачанных медиафайлов в media_files ───
* Файлы, скачанные старым билдом (до md_dl_register_servable), лежат на диске,
* но отсутствуют в media_files. При старте проходим по сообщениям каналов,
* парсим тело "d" (метаданные media_id/block_ids/content_hash уже там) и для
* существующих на диске файлов дорегистрируем блоки — узел снова может их отдавать. */
void chat_media_backfill(void) {
if (!g_cc.initialized || !g_cc.db) return;
/* ─── Backfill: регистрация уже скачанных медиафайлов в media_files ─── */
void chat_media_backfill(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db) return;
char media_base[512];
const char* last_slash = strrchr(g_cc.db_path, '/');
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - g_cc.db_path), g_cc.db_path);
else snprintf(media_base, sizeof(media_base), "%s", g_cc.db_path);
const char* last_slash = strrchr(cc->db_path, '/');
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
else snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
int total = 0;
sqlite3_stmt* cs = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT channel_id FROM channels", -1, &cs, NULL) != SQLITE_OK) return;
if (sqlite3_prepare_v2(cc->db, "SELECT channel_id FROM channels", -1, &cs, NULL) != SQLITE_OK) return;
while (sqlite3_step(cs) == SQLITE_ROW) {
const char* ch_id = (const char*)sqlite3_column_text(cs, 0);
@@ -983,7 +999,7 @@ void chat_media_backfill(void) {
char sql[256];
snprintf(sql, sizeof(sql), "SELECT data FROM \"%s\"", tbl);
sqlite3_stmt* ms = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &ms, NULL) != SQLITE_OK) continue;
if (sqlite3_prepare_v2(cc->db, sql, -1, &ms, NULL) != SQLITE_OK) continue;
while (sqlite3_step(ms) == SQLITE_ROW) {
const char* jdata = (const char*)sqlite3_column_text(ms, 0);
@@ -1032,7 +1048,7 @@ void chat_media_backfill(void) {
for (int n = 0; n < result.num_blocks; n++) {
int exists = 0;
sqlite3_stmt* ex = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT 1 FROM media_files WHERE block_id=? LIMIT 1", -1, &ex, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, "SELECT 1 FROM media_files WHERE block_id=? LIMIT 1", -1, &ex, NULL) == SQLITE_OK) {
sqlite3_bind_blob(ex, 1, result.block_ids + n * 16, 16, SQLITE_STATIC);
exists = (sqlite3_step(ex) == SQLITE_ROW);
sqlite3_finalize(ex);
@@ -1047,8 +1063,8 @@ void chat_media_backfill(void) {
int64_t chunk_size = (n == result.num_blocks - 1) ? result.file_size - (int64_t)n * result.block_size : result.block_size;
int64_t offset = (int64_t)n * result.block_size;
if (media_index_register_downloaded(g_cc.db, result.media_id, result.block_ids + n * 16,
result.content_hash, ch_id, rel, g_cc.my_node_id,
if (media_index_register_downloaded(cc->db, result.media_id, result.block_ids + n * 16,
result.content_hash, ch_id, rel, cc->my_node_id,
result.file_size, chunk_size, n, offset) == 0) {
registered++;
}
@@ -1071,14 +1087,7 @@ void chat_media_backfill(void) {
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: media backfill done, %d blocks registered", CC_ID, total);
}
/* ─── Докачка недостающего медиа + вычистка старых файлов (единый проход) ───
* Инвариант кеша: на диске лежат самые свежие storage_total_size байт медиа
* (по timestamp сообщения). Один проход DESC по дате: свежие недостающие —
* докачиваем (в пределах окна storage_backfill_days и бюджета), старые сверх
* бюджета — вычищаем. Фронт докачки идёт с нового конца, фронт вычистки — со
* старого; сходятся на границе бюджета (used упирается в limit). Счётчик
* g_media_cache_bytes корректируется на каждой вычистке; докачки добавляют
* через md_download_done_cb. Вызывается один раз после стартовой синхронизации. */
/* ─── Докачка недостающего медиа + вычистка старых файлов (единый проход) ─── */
struct bf_entry {
uint64_t ts_ms;
@@ -1106,9 +1115,9 @@ static int bf_cmp_ts_desc(const void* a, const void* b) {
}
/* Собирает все медиасообщения всех каналов (presence + размеры + контекст докачки). */
static void bf_collect(const char* media_base, struct bf_list* l) {
static void bf_collect(struct chat_core_ctx* cc, const char* media_base, struct bf_list* l) {
sqlite3_stmt* cs = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT channel_id FROM channels", -1, &cs, NULL) != SQLITE_OK) return;
if (sqlite3_prepare_v2(cc->db, "SELECT channel_id FROM channels", -1, &cs, NULL) != SQLITE_OK) return;
while (sqlite3_step(cs) == SQLITE_ROW) {
const char* ch_id = (const char*)sqlite3_column_text(cs, 0);
@@ -1119,7 +1128,7 @@ static void bf_collect(const char* media_base, struct bf_list* l) {
snprintf(sql, sizeof(sql),
"SELECT id, timestamp, node_id, data, author_signature, local_attrs FROM \"%s\"", tbl);
sqlite3_stmt* ms = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &ms, NULL) != SQLITE_OK) continue;
if (sqlite3_prepare_v2(cc->db, sql, -1, &ms, NULL) != SQLITE_OK) continue;
while (sqlite3_step(ms) == SQLITE_ROW) {
int64_t msg_id = sqlite3_column_int64(ms, 0);
@@ -1196,28 +1205,29 @@ static void bf_collect(const char* media_base, struct bf_list* l) {
sqlite3_finalize(cs);
}
void chat_media_autodownload_backfill(void) {
if (!g_cc.initialized || !g_cc.db || !g_cc.inst) return;
void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db || !inst) return;
uint64_t limit = g_cc.inst->config->global.chatserver_storage_total_size;
int autoload = chat_setting_get_int("storage_autoload", 1);
int days = chat_setting_get_int("storage_backfill_days", 7);
uint64_t now_ms = (uint64_t)ntp_time_get_seconds(g_cc.inst) * 1000ULL;
uint64_t limit = inst->config->global.chatserver_storage_total_size;
int autoload = chat_setting_get_int(inst, "storage_autoload", 1);
int days = chat_setting_get_int(inst, "storage_backfill_days", 7);
uint64_t now_ms = (uint64_t)ntp_time_get_seconds(inst) * 1000ULL;
uint64_t cutoff_ms = now_ms - (uint64_t)days * 86400ULL * 1000ULL;
char media_base[512];
const char* last_slash = strrchr(g_cc.db_path, '/');
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - g_cc.db_path), g_cc.db_path);
else snprintf(media_base, sizeof(media_base), "%s", g_cc.db_path);
const char* last_slash = strrchr(cc->db_path, '/');
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
else snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
struct bf_list list = {0};
bf_collect(media_base, &list);
bf_collect(cc, media_base, &list);
if (list.count > 1) qsort(list.items, (size_t)list.count, sizeof(struct bf_entry), bf_cmp_ts_desc);
/* инициализация счётчика: сумма всех present-файлов */
g_media_cache_bytes = 0;
cc->media_cache_bytes = 0;
for (int i = 0; i < list.count; i++)
if (list.items[i].actual_size > 0) g_media_cache_bytes += (uint64_t)list.items[i].actual_size;
if (list.items[i].actual_size > 0) cc->media_cache_bytes += (uint64_t)list.items[i].actual_size;
uint64_t used = 0;
int present = 0, downloaded = 0, evicted = 0, skipped = 0;
@@ -1231,7 +1241,7 @@ void chat_media_autodownload_backfill(void) {
present++;
} else {
if (remove(e->full_path) == 0) {
cache_bytes_sub(e->actual_size);
cache_bytes_sub(cc, e->actual_size);
evicted++; evicted_bytes += (uint64_t)e->actual_size;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: cache evict: %s ts=%llu size=%lld",
CC_ID, e->full_path, (unsigned long long)e->ts_ms, (long long)e->actual_size);
@@ -1240,10 +1250,10 @@ void chat_media_autodownload_backfill(void) {
}
}
} else {
if (!autoload || e->author == g_cc.my_node_id) { skipped++; continue; }
if (!autoload || e->author == cc->my_node_id) { skipped++; continue; }
if (e->ts_ms < cutoff_ms) { skipped++; continue; }
if (limit > 0 && used + (uint64_t)e->claimed_size > limit) { skipped++; continue; }
if (md_auto_download(g_cc.inst, e->body, e->body_len, e->ch_id, e->base_filename,
if (md_auto_download(inst, e->body, e->body_len, e->ch_id, e->base_filename,
e->ts_ms, e->author_sig, e->author, e->msg_id, e->content_type)) {
downloaded++;
used += (uint64_t)e->claimed_size;
@@ -1255,18 +1265,15 @@ void chat_media_autodownload_backfill(void) {
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: cache backfill done present=%d downloaded=%d skipped=%d evicted=%d evicted_bytes=%llu bytes=%llu limit=%llu",
CC_ID, present, downloaded, skipped, evicted, (unsigned long long)evicted_bytes,
(unsigned long long)g_media_cache_bytes, (unsigned long long)limit);
(unsigned long long)cc->media_cache_bytes, (unsigned long long)limit);
for (int i = 0; i < list.count; i++) if (list.items[i].body) u_free(list.items[i].body);
u_free(list.items);
chat_media_cache_reconcile();
chat_media_cache_reconcile(inst);
}
/* ─── Кеш медиафайлов: очистка сирот + лимит storage_total_size ───
* Возраст файла = timestamp исходного сообщения (msg_<ch>.timestamp).
* Сироты (файлы без сообщения, включая .chunk_* остатки) удаляются на старте.
* Имя файла на диске берётся из local_attrs "fp" (fallback — по имени из "d"). */
/* ─── Кеш медиафайлов: очистка сирот + лимит storage_total_size ─── */
struct media_msg_file {
char rel_path[1536]; // media/<ch_id>/<final_name>
@@ -1276,17 +1283,17 @@ struct media_msg_file {
typedef void (*media_msg_file_cb)(const struct media_msg_file* f, void* arg);
static void chat_media_base(char* out, size_t out_sz) {
const char* ls = strrchr(g_cc.db_path, '/');
if (ls) snprintf(out, out_sz, "%.*s", (int)(ls - g_cc.db_path), g_cc.db_path);
else snprintf(out, out_sz, "%s", g_cc.db_path);
static void chat_media_base(struct chat_core_ctx* cc, char* out, size_t out_sz) {
const char* ls = strrchr(cc->db_path, '/');
if (ls) snprintf(out, out_sz, "%.*s", (int)(ls - cc->db_path), cc->db_path);
else snprintf(out, out_sz, "%s", cc->db_path);
}
/* Итератор по всем медиасообщениям всех каналов (без фильтра по node_id/времени). */
static void chat_media_foreach_media_message(const char* media_base, media_msg_file_cb cb, void* arg) {
if (!g_cc.db || !cb) return;
static void chat_media_foreach_media_message(struct chat_core_ctx* cc, const char* media_base, media_msg_file_cb cb, void* arg) {
if (!cc->db || !cb) return;
sqlite3_stmt* cs = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT channel_id FROM channels", -1, &cs, NULL) != SQLITE_OK) return;
if (sqlite3_prepare_v2(cc->db, "SELECT channel_id FROM channels", -1, &cs, NULL) != SQLITE_OK) return;
while (sqlite3_step(cs) == SQLITE_ROW) {
const char* ch_id = (const char*)sqlite3_column_text(cs, 0);
@@ -1296,7 +1303,7 @@ static void chat_media_foreach_media_message(const char* media_base, media_msg_f
char sql[256];
snprintf(sql, sizeof(sql), "SELECT timestamp, data, local_attrs FROM \"%s\"", tbl);
sqlite3_stmt* ms = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &ms, NULL) != SQLITE_OK) continue;
if (sqlite3_prepare_v2(cc->db, sql, -1, &ms, NULL) != SQLITE_OK) continue;
while (sqlite3_step(ms) == SQLITE_ROW) {
uint64_t ts = (uint64_t)sqlite3_column_int64(ms, 0);
@@ -1406,10 +1413,10 @@ static void cache_file_collect_cb(const struct media_msg_file* f, void* arg) {
}
/* Фактическая сумма размеров файлов, соответствующих сообщениям. */
static uint64_t chat_media_cache_scan_sum(void) {
char media_base[512]; chat_media_base(media_base, sizeof(media_base));
static uint64_t chat_media_cache_scan_sum(struct chat_core_ctx* cc) {
char media_base[512]; chat_media_base(cc, media_base, sizeof(media_base));
struct cache_file_list files = {0};
chat_media_foreach_media_message(media_base, cache_file_collect_cb, &files);
chat_media_foreach_media_message(cc, media_base, cache_file_collect_cb, &files);
uint64_t sum = 0;
for (int i = 0; i < files.count; i++)
if (files.files[i].size > 0) sum += (uint64_t)files.files[i].size;
@@ -1418,17 +1425,18 @@ static uint64_t chat_media_cache_scan_sum(void) {
}
/* Сверка счётчика с фактической суммой: при расхождении — WARN + самокоррекция. */
static void chat_media_cache_reconcile(void) {
if (!g_cc.initialized || !g_cc.db || !g_cc.inst) return;
uint64_t actual = chat_media_cache_scan_sum();
if (actual != g_media_cache_bytes) {
static void chat_media_cache_reconcile(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db || !inst) return;
uint64_t actual = chat_media_cache_scan_sum(cc);
if (actual != cc->media_cache_bytes) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "%s: cache counter drift: counter=%llu actual=%llu diff=%lld",
CC_ID, (unsigned long long)g_media_cache_bytes, (unsigned long long)actual,
(long long)((int64_t)actual - (int64_t)g_media_cache_bytes));
g_media_cache_bytes = actual;
CC_ID, (unsigned long long)cc->media_cache_bytes, (unsigned long long)actual,
(long long)((int64_t)actual - (int64_t)cc->media_cache_bytes));
cc->media_cache_bytes = actual;
} else {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "%s: cache reconcile ok bytes=%llu",
CC_ID, (unsigned long long)g_media_cache_bytes);
CC_ID, (unsigned long long)cc->media_cache_bytes);
}
}
@@ -1459,14 +1467,15 @@ static void orphan_walk_cb(void* arg, const char* full_path, int64_t size) {
}
}
static void chat_media_cache_cleanup_orphans(void) {
if (!g_cc.initialized || !g_cc.db || !g_cc.inst) return;
static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db || !inst) return;
char media_base[512]; chat_media_base(media_base, sizeof(media_base));
char media_base[512]; chat_media_base(cc, media_base, sizeof(media_base));
char media_dir[600]; snprintf(media_dir, sizeof(media_dir), "%s/media", media_base);
struct cache_rel_list rels = {0};
chat_media_foreach_media_message(media_base, cache_rel_collect_cb, &rels);
chat_media_foreach_media_message(cc, media_base, cache_rel_collect_cb, &rels);
if (rels.count > 1) qsort(rels.items, (size_t)rels.count, sizeof(char*), cache_str_cmp);
struct orphan_walk_ctx ow;
@@ -1482,28 +1491,29 @@ static void chat_media_cache_cleanup_orphans(void) {
u_free(rels.items);
}
static void chat_media_cache_enforce(void) {
if (!g_cc.initialized || !g_cc.db || !g_cc.inst) return;
uint64_t limit = g_cc.inst->config->global.chatserver_storage_total_size;
static void chat_media_cache_enforce(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db || !inst) return;
uint64_t limit = inst->config->global.chatserver_storage_total_size;
if (limit == 0 || g_media_cache_bytes <= limit) {
if (limit == 0 || cc->media_cache_bytes <= limit) {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "%s: cache limit ok bytes=%llu limit=%llu",
CC_ID, (unsigned long long)g_media_cache_bytes, (unsigned long long)limit);
chat_media_cache_reconcile();
CC_ID, (unsigned long long)cc->media_cache_bytes, (unsigned long long)limit);
chat_media_cache_reconcile(inst);
return;
}
char media_base[512]; chat_media_base(media_base, sizeof(media_base));
char media_base[512]; chat_media_base(cc, media_base, sizeof(media_base));
struct cache_file_list files = {0};
chat_media_foreach_media_message(media_base, cache_file_collect_cb, &files);
chat_media_foreach_media_message(cc, media_base, cache_file_collect_cb, &files);
if (files.count > 1) qsort(files.files, (size_t)files.count, sizeof(struct cache_file), cache_file_cmp_ts);
int deleted = 0; uint64_t deleted_bytes = 0;
for (int i = 0; i < files.count && g_media_cache_bytes > limit; i++) {
for (int i = 0; i < files.count && cc->media_cache_bytes > limit; i++) {
if (files.files[i].size <= 0) continue;
if (remove(files.files[i].full_path) == 0) {
cache_bytes_sub(files.files[i].size);
cache_bytes_sub(cc, files.files[i].size);
deleted++; deleted_bytes += (uint64_t)files.files[i].size;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: cache evict: %s ts=%llu size=%lld",
CC_ID, files.files[i].full_path,
@@ -1513,58 +1523,58 @@ static void chat_media_cache_enforce(void) {
}
}
if (g_media_cache_bytes > limit)
if (cc->media_cache_bytes > limit)
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "%s: cache still over limit bytes=%llu limit=%llu",
CC_ID, (unsigned long long)g_media_cache_bytes, (unsigned long long)limit);
CC_ID, (unsigned long long)cc->media_cache_bytes, (unsigned long long)limit);
else
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: cache limit enforced bytes=%llu limit=%llu deleted=%d bytes=%llu",
CC_ID, (unsigned long long)g_media_cache_bytes, (unsigned long long)limit,
CC_ID, (unsigned long long)cc->media_cache_bytes, (unsigned long long)limit,
deleted, (unsigned long long)deleted_bytes);
u_free(files.files);
chat_media_cache_reconcile();
chat_media_cache_reconcile(inst);
}
/* ─── Триггер: после первого SYNC_DONE (+ дебаунс) — анонс локальных блоков + докачка ─── */
static void* g_media_startup_backfill_timer = NULL;
static int g_media_startup_backfill_ran = 0;
static struct UTUN_INSTANCE* g_media_startup_backfill_inst = NULL;
static void media_startup_backfill_timer_cb(void* arg) {
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
g_media_startup_backfill_timer = NULL;
g_media_startup_backfill_ran = 1;
struct chat_core_ctx* cc = CC(inst);
cc->media_backfill_timer = NULL;
cc->media_backfill_ran = 1;
/* 1) переанонс локальных блоков суперузлам (block_availability),
* 2) чистка сирот, 3) докачка недостающего + вычистка старого (единый проход) */
media_delivery_announce_local_blocks(inst);
chat_media_cache_cleanup_orphans();
chat_media_autodownload_backfill();
chat_media_cache_cleanup_orphans(inst);
chat_media_autodownload_backfill(inst);
}
static void media_startup_backfill_done_cb(struct DB_SYNC_INSTANCE* si, uint64_t peer_node_id, void* arg) {
(void)si; (void)peer_node_id; (void)arg;
if (g_media_startup_backfill_ran || !g_media_startup_backfill_inst) return;
struct UTUN_INSTANCE* inst = g_media_startup_backfill_inst;
if (g_media_startup_backfill_timer) uasync_cancel_timeout(inst->ua, g_media_startup_backfill_timer);
g_media_startup_backfill_timer = uasync_set_timeout(inst->ua, 20000, inst,
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
struct chat_core_ctx* cc = CC(inst);
(void)si; (void)peer_node_id;
if (!cc || cc->media_backfill_ran || !cc->media_backfill_inst) return;
if (cc->media_backfill_timer) uasync_cancel_timeout(inst->ua, cc->media_backfill_timer);
cc->media_backfill_timer = uasync_set_timeout(inst->ua, 20000, inst,
media_startup_backfill_timer_cb, "chat_media_bl");
}
void chat_media_startup_backfill_init(struct UTUN_INSTANCE* inst) {
if (!inst) return;
g_media_startup_backfill_inst = inst;
g_media_startup_backfill_ran = 0;
db_sync_add_done_cbk(inst, media_startup_backfill_done_cb, NULL);
struct chat_core_ctx* cc = CC(inst);
if (!inst || !cc) return;
cc->media_backfill_inst = inst;
cc->media_backfill_ran = 0;
db_sync_add_done_cbk(inst, media_startup_backfill_done_cb, inst);
}
void chat_media_startup_backfill_destroy(struct UTUN_INSTANCE* inst) {
if (!inst) return;
db_sync_remove_done_cbk(inst, media_startup_backfill_done_cb, NULL);
if (g_media_startup_backfill_timer) {
uasync_cancel_timeout(inst->ua, g_media_startup_backfill_timer);
g_media_startup_backfill_timer = NULL;
struct chat_core_ctx* cc = CC(inst);
if (!inst || !cc) return;
db_sync_remove_done_cbk(inst, media_startup_backfill_done_cb, inst);
if (cc->media_backfill_timer) {
uasync_cancel_timeout(inst->ua, cc->media_backfill_timer);
cc->media_backfill_timer = NULL;
}
g_media_startup_backfill_inst = NULL;
g_media_startup_backfill_ran = 0;
cc->media_backfill_inst = NULL;
cc->media_backfill_ran = 0;
}
+37 -34
View File
@@ -23,36 +23,41 @@
#include <openssl/evp.h>
void chat_core_set_my_node_id(uint64_t node_id) {
g_cc.my_node_id = node_id;
void chat_core_set_my_node_id(struct UTUN_INSTANCE* inst, uint64_t node_id) {
struct chat_core_ctx* cc = CC(inst);
if (cc) cc->my_node_id = node_id;
}
void chat_core_update_my_name(const char* name) {
if (!g_cc.initialized || !name) return;
topo_node_sqlite_node_update_verified(g_cc.db, g_cc.my_node_id,
name, g_cc.inst->my_keys.public_key, g_cc.inst->my_ed25519_pubkey,
(uint64_t)ntp_time_get_seconds(g_cc.inst), 0);
snprintf(g_cc.inst->name, sizeof(g_cc.inst->name), "%s", name);
chat_core_update_my_member();
void chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !name) return;
topo_node_sqlite_node_update_verified(cc->db, cc->my_node_id,
name, inst->my_keys.public_key, inst->my_ed25519_pubkey,
(uint64_t)ntp_time_get_seconds(inst), 0);
snprintf(inst->name, sizeof(inst->name), "%s", name);
chat_core_update_my_member(inst);
}
void chat_core_update_my_name_trampoline(void* arg) {
chat_core_update_my_name((const char*)arg);
struct update_my_name_arg* a = (struct update_my_name_arg*)arg;
if (!a) return;
chat_core_update_my_name(a->inst, a->name);
u_free(arg);
}
/* ── Обновление своего мембера (userinfo + update_ts + подпись + синк).
Адреса больше не здесь — их синхронизирует BGP/topo_group. ── */
void chat_core_update_my_member(void) {
if (!g_cc.initialized || !g_cc.db || !g_cc.inst) return;
void chat_core_update_my_member(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db || !inst) return;
uint64_t myid = g_cc.my_node_id;
const char* my_name = g_cc.inst->name[0] ? g_cc.inst->name : "";
uint64_t myid = cc->my_node_id;
const char* my_name = inst->name[0] ? inst->name : "";
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}", my_name);
sqlite3_stmt* cs = NULL;
sqlite3_prepare_v2(g_cc.db, "SELECT channel_id FROM channels", -1, &cs, NULL);
sqlite3_prepare_v2(cc->db, "SELECT channel_id FROM channels", -1, &cs, NULL);
if (!cs) return;
while (sqlite3_step(cs) == SQLITE_ROW) {
const char* ch = (const char*)sqlite3_column_text(cs, 0);
@@ -60,26 +65,26 @@ void chat_core_update_my_member(void) {
char tbl[80]; peers_table_name(ch, tbl, sizeof(tbl));
uint8_t join_sig[64]; uint64_t join_ts = 0;
if (topo_node_sqlite_member_get_join(g_cc.db, ch, myid, join_sig, &join_ts) != 0) continue;
if (topo_node_sqlite_member_get_join(cc->db, ch, myid, join_sig, &join_ts) != 0) continue;
uint64_t local_uts = 0;
{ char q[256]; snprintf(q, sizeof(q), "SELECT update_ts FROM \"%s\" WHERE node_id=?", tbl);
sqlite3_stmt* ps = NULL;
if (sqlite3_prepare_v2(g_cc.db, q, -1, &ps, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, q, -1, &ps, NULL) == SQLITE_OK) {
sqlite3_bind_int64(ps, 1, (sqlite3_int64)myid);
if (sqlite3_step(ps) == SQLITE_ROW) local_uts = (uint64_t)sqlite3_column_int64(ps, 0);
sqlite3_finalize(ps);
}
}
uint64_t now = (uint64_t)ntp_time_get_seconds(g_cc.inst);
uint64_t now = (uint64_t)ntp_time_get_seconds(inst);
uint64_t update_ts = (now > local_uts) ? now : (local_uts + 1); /* строго больше локального */
uint8_t umsg[8192];
int ulen = member_sync_build_update_msg(join_sig, update_ts, juser, umsg, (int)sizeof(umsg));
if (ulen < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: build_update_msg failed ch=%s", CC_ID, ch); continue; }
uint8_t update_sig[64]; memset(update_sig, 0, 64);
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, g_cc.inst->my_ed25519_privkey, 32);
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, inst->my_ed25519_privkey, 32);
if (pkey) {
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
if (mdctx) {
@@ -90,11 +95,11 @@ void chat_core_update_my_member(void) {
EVP_PKEY_free(pkey);
}
int r2 = member_sync_put(g_cc.inst, ch, myid, g_cc.inst->my_keys.public_key,
g_cc.inst->my_ed25519_pubkey, join_sig, join_ts,
int r2 = member_sync_put(inst, ch, myid, inst->my_keys.public_key,
inst->my_ed25519_pubkey, join_sig, join_ts,
update_sig, update_ts, juser, NULL, NULL, 0, 0, NULL);
if (r2 < 0) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member_sync_put(self) FAILED ch=%s rc=%d", CC_ID, ch, r2);
else if (r2 & MS_APPLY_CHANGED) member_sync_broadcast_one(g_cc.inst, ch, myid);
else if (r2 & MS_APPLY_CHANGED) member_sync_broadcast_one(inst, ch, myid);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: update my member ch=%s ts=%llu", CC_ID, ch,
(unsigned long long)update_ts);
@@ -102,19 +107,17 @@ void chat_core_update_my_member(void) {
sqlite3_finalize(cs);
}
void chat_core_sync_my_addresses(void) {
/* Адреса синхронизируются BGP/topo_group через topo_node_update_my_addresses
(реестр + БД node_addresses + классификация node_type/storage + рассылка nodeinfo).
Вызывается после готовности БД, поэтому здесь гарантированно персистит свои адреса. */
if (g_cc.inst)
topo_node_update_my_addresses(g_cc.inst);
chat_core_update_my_member();
void chat_core_sync_my_addresses(struct UTUN_INSTANCE* inst) {
if (inst)
topo_node_update_my_addresses(inst);
chat_core_update_my_member(inst);
}
void chat_core_save_ui_state(const char* key, const char* value) {
if (!g_cc.initialized || !key || !value) return;
void chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !key || !value) return;
sqlite3_stmt* st = NULL;
sqlite3_prepare_v2(g_cc.db,
sqlite3_prepare_v2(cc->db,
"INSERT OR REPLACE INTO ui_state(key, value) VALUES(?, ?)", -1, &st, NULL);
if (st) {
sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC);
@@ -123,11 +126,11 @@ void chat_core_save_ui_state(const char* key, const char* value) {
}
}
struct save_ui_state_arg { char data[256]; };
void chat_core_save_ui_state_trampoline(void* arg) {
struct save_ui_state_arg* a = (struct save_ui_state_arg*)arg;
if (!a) return;
const char* key = a->data;
const char* value = key + strlen(key) + 1;
chat_core_save_ui_state(key, value);
chat_core_save_ui_state(a->inst, key, value);
u_free(arg);
}
+58 -53
View File
@@ -2,22 +2,19 @@
* chat_setting.c — chat-related settings: validation, storage, on-the-fly apply
*
* Используется и из парсера конфига (headless), и из GUI (через bridge).
* Никаких зависимостей от g_cc или instance — работает автономно.
* Состояние per-instance (struct chat_setting_state).
*/
#include "chat_setting.h"
#include "../utun_instance.h"
#include "../../lib/debug_config.h"
#ifdef DEBUG_CATEGORY_CHAT_SYNC
#define CS_ID DEBUG_CATEGORY_CHAT_SYNC
#else
#define CS_ID DEBUG_CATEGORY_GENERAL
#endif
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#define CS_ID DEBUG_CATEGORY_CHAT_SYNC
/* ─── типы настроек ─── */
#define CHAT_SETTING_INT 0
@@ -32,12 +29,6 @@ struct chat_setting_def {
int max_val;
};
/* хранилище для строковых значений */
#define CHAT_SETTING_STRING_MAX 16
static int g_chat_string_idx[CHAT_SETTING_STRING_MAX];
static char g_chat_string_val[CHAT_SETTING_STRING_MAX][256];
static int g_chat_string_count = 0;
/* ─── реестр всех известных настроек ─── */
static const struct chat_setting_def g_setting_defs[] = {
@@ -59,38 +50,32 @@ static const struct chat_setting_def g_setting_defs[] = {
#define CHAT_SETTING_COUNT (sizeof(g_setting_defs) / sizeof(g_setting_defs[0]))
/* ─── состояние (текущие значения) ─── */
/* ─── state helpers ─── */
static int g_chat_values[CHAT_SETTING_COUNT];
static int g_chat_initialized = 0;
static void chat_setting_reset_defaults(void) {
static void chat_setting_reset_defaults(struct chat_setting_state* s) {
for (size_t i = 0; i < CHAT_SETTING_COUNT; i++)
g_chat_values[i] = g_setting_defs[i].default_val;
g_chat_initialized = 1;
s->values[i] = g_setting_defs[i].default_val;
s->initialized = 1;
}
/* ─── поиск ─── */
static int chat_setting_find(const char* name) {
for (size_t i = 0; i < CHAT_SETTING_COUNT; i++)
if (strcmp(g_setting_defs[i].name, name) == 0) return (int)i;
return -1;
}
/* ─── apply: вызов специфичного для настройки кода ─── */
static void chat_setting_apply(void) {
static void chat_setting_apply(struct chat_setting_state* s) {
(void)s;
/* будет заполняться по мере появления runtime-setter'ов в модулях:
if (idx == ...) some_module_set_xxx(value); */
}
/* ─── public API ─── */
/* ─── state-level API ─── */
int chat_setting_set(const char* name, const char* value) {
if (!name || !value || !*name || !*value) return -2;
int chat_setting_set_state(struct chat_setting_state* s, const char* name, const char* value) {
if (!s || !name || !value || !*name || !*value) return -2;
if (!g_chat_initialized) chat_setting_reset_defaults();
if (!s->initialized) chat_setting_reset_defaults(s);
int idx = chat_setting_find(name);
if (idx < 0) {
@@ -102,25 +87,24 @@ int chat_setting_set(const char* name, const char* value) {
int v;
if (def->type == CHAT_SETTING_STRING) {
/* сохраняем строку в отдельном хранилище */
for (int i = 0; i < g_chat_string_count; i++) {
if (g_chat_string_idx[i] == idx) {
if (strcmp(g_chat_string_val[i], value) == 0) return 0;
snprintf(g_chat_string_val[i], sizeof(g_chat_string_val[i]), "%s", value);
for (int i = 0; i < s->string_count; i++) {
if (s->string_idx[i] == idx) {
if (strcmp(s->string_val[i], value) == 0) return 0;
snprintf(s->string_val[i], sizeof(s->string_val[i]), "%s", value);
DEBUG_INFO(CS_ID, "chat_setting: %s = %s", name, value);
chat_setting_apply();
chat_setting_apply(s);
return 0;
}
}
if (g_chat_string_count >= CHAT_SETTING_STRING_MAX) {
if (s->string_count >= CHAT_SETTING_STRING_MAX) {
DEBUG_ERROR(CS_ID, "chat_setting: string storage full for '%s'", name);
return -2;
}
g_chat_string_idx[g_chat_string_count] = idx;
snprintf(g_chat_string_val[g_chat_string_count], sizeof(g_chat_string_val[0]), "%s", value);
g_chat_string_count++;
s->string_idx[s->string_count] = idx;
snprintf(s->string_val[s->string_count], sizeof(s->string_val[0]), "%s", value);
s->string_count++;
DEBUG_INFO(CS_ID, "chat_setting: %s = %s", name, value);
chat_setting_apply();
chat_setting_apply(s);
return 0;
}
@@ -138,34 +122,55 @@ int chat_setting_set(const char* name, const char* value) {
v = (int)lv;
}
if (g_chat_values[idx] == v) return 0;
if (s->values[idx] == v) return 0;
g_chat_values[idx] = v;
s->values[idx] = v;
DEBUG_INFO(CS_ID, "chat_setting: %s = %s", name, value);
chat_setting_apply();
chat_setting_apply(s);
return 0;
}
const char* chat_setting_get(const char* name) {
if (!g_chat_initialized) chat_setting_reset_defaults();
const char* chat_setting_get_state(const struct chat_setting_state* s, const char* name) {
if (!s || !s->initialized) return NULL;
int idx = chat_setting_find(name);
if (idx < 0) return NULL;
const struct chat_setting_def* def = &g_setting_defs[idx];
if (def->type == CHAT_SETTING_STRING) {
for (int i = 0; i < g_chat_string_count; i++) {
if (g_chat_string_idx[i] == idx) return g_chat_string_val[i];
for (int i = 0; i < s->string_count; i++) {
if (s->string_idx[i] == idx) return s->string_val[i];
}
return "";
}
static char buf[16];
snprintf(buf, sizeof(buf), "%d", g_chat_values[idx]);
return buf;
snprintf((char*)s->int_buf, sizeof(s->int_buf), "%d", s->values[idx]);
return s->int_buf;
}
int chat_setting_get_int(const char* name, int def) {
if (!g_chat_initialized) chat_setting_reset_defaults();
int chat_setting_get_int_state(const struct chat_setting_state* s, const char* name, int def) {
if (!s || !s->initialized) return def;
int idx = chat_setting_find(name);
return idx >= 0 ? g_chat_values[idx] : def;
return idx >= 0 ? s->values[idx] : def;
}
/* ─── instance-level API ─── */
static struct chat_setting_state* cs_state(struct UTUN_INSTANCE* inst) {
return inst ? &inst->chat_settings : NULL;
}
int chat_setting_set(struct UTUN_INSTANCE* inst, const char* name, const char* value) {
struct chat_setting_state* s = cs_state(inst);
if (!s) return -2;
return chat_setting_set_state(s, name, value);
}
const char* chat_setting_get(struct UTUN_INSTANCE* inst, const char* name) {
struct chat_setting_state* s = cs_state(inst);
return s ? chat_setting_get_state(s, name) : NULL;
}
int chat_setting_get_int(struct UTUN_INSTANCE* inst, const char* name, int def) {
struct chat_setting_state* s = cs_state(inst);
return s ? chat_setting_get_int_state(s, name, def) : def;
}
+31 -6
View File
@@ -4,23 +4,48 @@
* Один API для всех потребителей: парсер конфига (headless) и GUI (через bridge).
* chat_setting_set(name, value) — валидация + хранение + apply к модулям.
* Возвращает 0=OK, -1=неизвестная настройка, -2=невалидное значение.
*
* Состояние — per-instance. Конфиг-парсер заполняет struct chat_setting_state
* из [chatserver] ещё до создания instance (state-уровень), затем runtime-модули
* читают через instance-уровень (chat_setting_get_int(inst, ...)).
*/
#ifndef CHAT_SETTING_H
#define CHAT_SETTING_H
#include <stdint.h>
struct UTUN_INSTANCE;
#ifdef __cplusplus
extern "C" {
#endif
/* Установить настройку. Применяет на лету если модуль уже инициализирован. */
int chat_setting_set(const char* name, const char* value);
/* ─── per-instance хранилище ─── */
/* Прочитать сохранённое значение. NULL если такой настройки нет. */
const char* chat_setting_get(const char* name);
#define CHAT_SETTING_MAX_SETTINGS 32
#define CHAT_SETTING_STRING_MAX 16
/* Прочитать как int. Возвращает def если настройка не найдена. */
int chat_setting_get_int(const char* name, int def);
struct chat_setting_state {
int values[CHAT_SETTING_MAX_SETTINGS];
int string_idx[CHAT_SETTING_STRING_MAX];
char string_val[CHAT_SETTING_STRING_MAX][256];
int string_count;
int initialized;
char int_buf[16]; /* буфер для chat_setting_get_state у int-настроек */
};
/* ─── state-уровень (конфиг-парсер, до создания instance) ─── */
int chat_setting_set_state(struct chat_setting_state* s, const char* name, const char* value);
const char* chat_setting_get_state(const struct chat_setting_state* s, const char* name);
int chat_setting_get_int_state(const struct chat_setting_state* s, const char* name, int def);
/* ─── instance-уровень (runtime) ─── */
int chat_setting_set(struct UTUN_INSTANCE* inst, const char* name, const char* value);
const char* chat_setting_get(struct UTUN_INSTANCE* inst, const char* name);
int chat_setting_get_int(struct UTUN_INSTANCE* inst, const char* name, int def);
#ifdef __cplusplus
}
+46 -44
View File
@@ -24,11 +24,12 @@ static const char* nat_type_str(uint8_t t) {
switch (t) { case 0: return "UNKNOWN"; case 1: return "EIM"; case 2: return "STRICT"; case 3: return "DIRECT"; default: return "?"; }
}
static void get_node_name(uint64_t node_id, char* out, size_t sz) {
static void get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz) {
struct chat_core_ctx* cc = CC(inst);
out[0] = '\0';
if (!g_cc.db || node_id == 0) return;
if (!cc || !cc->db || node_id == 0) return;
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL) != SQLITE_OK) return;
if (sqlite3_prepare_v2(cc->db, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL) != SQLITE_OK) return;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
const char* n = (const char*)sqlite3_column_text(st, 0);
@@ -37,16 +38,16 @@ static void get_node_name(uint64_t node_id, char* out, size_t sz) {
sqlite3_finalize(st);
}
static void chat_core_collect_status(void) {
static void chat_core_collect_status(struct UTUN_INSTANCE* inst) {
char buf[8192]; int off = 0;
if (!g_cc.initialized || !g_cc.inst) {
if (!CC(inst) || !CC(inst)->initialized || !inst) {
off = snprintf(buf, sizeof(buf), "uTun not initialized\n");
chat_event_post(CHAT_EVT_STATUS_REFRESH, (const uint8_t*)buf, off);
chat_event_post(inst, CHAT_EVT_STATUS_REFRESH, (const uint8_t*)buf, off);
return;
}
struct NTP_TIME* ntp = &g_cc.inst->ntp;
struct NTP_TIME* ntp = &inst->ntp;
off += snprintf(buf + off, sizeof(buf) - off, "=== NTP ===\n");
off += snprintf(buf + off, sizeof(buf) - off, "Enabled: %s\n", ntp->enabled ? "yes" : "no");
off += snprintf(buf + off, sizeof(buf) - off, "Synced: %s\n", ntp->synced ? "yes" : "no");
@@ -64,12 +65,12 @@ static void chat_core_collect_status(void) {
}
int conn_count = 0;
struct ll_entry* entry = g_cc.inst->connections->head;
struct ll_entry* entry = inst->connections ? inst->connections->head : NULL;
while (entry) { conn_count++; entry = entry->next; }
off += snprintf(buf + off, sizeof(buf) - off, "=== Connections (%d) ===\n", conn_count);
uint64_t my_id = g_cc.inst->node_id;
entry = g_cc.inst->connections->head;
uint64_t my_id = inst->node_id;
entry = inst->connections ? inst->connections->head : NULL;
while (entry) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
if (!ce || !ce->conn) { entry = entry->next; continue; }
@@ -88,7 +89,7 @@ static void chat_core_collect_status(void) {
snprintf(peerhex, sizeof(peerhex), "%04llX", (unsigned long long)(pid & 0xFFFF));
char peername[64];
get_node_name(pid, peername, sizeof(peername));
get_node_name(inst, pid, peername, sizeof(peername));
if (peername[0])
off += snprintf(buf + off, sizeof(buf) - off, "[%s]→[%s] %s ETCP:%s%s(%dL)\n",
@@ -117,18 +118,18 @@ static void chat_core_collect_status(void) {
entry = entry->next;
}
chat_event_post(CHAT_EVT_STATUS_REFRESH, (const uint8_t*)buf, off);
chat_event_post(inst, CHAT_EVT_STATUS_REFRESH, (const uint8_t*)buf, off);
}
/* ── Бинарный список соединений: [count:2][entry:46B]* ── */
/* entry: peer_node_id:8 flags:1 link_count:1 rtt:2 inflight_kb:2 name:32 */
#define CONN_LIST_ENTRY_SIZE 46
static void collect_conn_list(void) {
if (!g_cc.initialized || !g_cc.inst || !g_cc.inst->connections) return;
static void collect_conn_list(struct UTUN_INSTANCE* inst) {
if (!CC(inst) || !CC(inst)->initialized || !inst || !inst->connections) return;
uint16_t count = 0;
struct ll_entry* entry = g_cc.inst->connections->head;
struct ll_entry* entry = inst->connections->head;
while (entry) { count++; entry = entry->next; }
if (count > 250) count = 250;
@@ -140,7 +141,7 @@ static void collect_conn_list(void) {
*hdr = count;
uint8_t* p = buf + 2;
entry = g_cc.inst->connections->head;
entry = inst->connections->head;
for (uint16_t i = 0; i < count && entry; i++, entry = entry->next) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
if (!ce || !ce->conn) { memset(p, 0, CONN_LIST_ENTRY_SIZE); p += CONN_LIST_ENTRY_SIZE; continue; }
@@ -169,41 +170,41 @@ static void collect_conn_list(void) {
memcpy(p, &inflight_kb, 2); p += 2;
char name[32]; memset(name, 0, 32);
get_node_name(ce->peer_node_id, name, 31);
get_node_name(inst, ce->peer_node_id, name, 31);
if (!name[0]) snprintf(name, 32, "%016llX", (unsigned long long)ce->peer_node_id);
memcpy(p, name, 32); p += 32;
}
chat_event_post(CHAT_EVT_CONN_LIST, buf, (int)buf_sz);
chat_event_post(inst, CHAT_EVT_CONN_LIST, buf, (int)buf_sz);
u_free(buf);
}
/* ── Полный текстовый дамп одного соединения ── */
static void collect_conn_metrics(uint64_t peer_node_id) {
static void collect_conn_metrics(struct UTUN_INSTANCE* inst, uint64_t peer_node_id) {
char buf[8192]; int off = 0;
if (!g_cc.initialized || !g_cc.inst || !g_cc.inst->connections) {
if (!CC(inst) || !CC(inst)->initialized || !inst || !inst->connections) {
off = snprintf(buf, sizeof(buf), "uTun not initialized\n");
chat_event_post(CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off);
chat_event_post(inst, CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off);
return;
}
struct ll_entry* e = queue_find_data_by_index(g_cc.inst->connections, (const uint8_t*)&peer_node_id);
struct ll_entry* e = queue_find_data_by_index(inst->connections, (const uint8_t*)&peer_node_id);
if (!e) {
off = snprintf(buf, sizeof(buf), "Connection %016llX not found\n", (unsigned long long)peer_node_id);
chat_event_post(CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off);
chat_event_post(inst, CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off);
return;
}
struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data;
if (!ce || !ce->conn) {
off = snprintf(buf, sizeof(buf), "Connection %016llX has null data\n", (unsigned long long)peer_node_id);
chat_event_post(CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off);
chat_event_post(inst, CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off);
return;
}
struct ETCP_CONN* conn = ce->conn;
uint64_t my_id = g_cc.inst->node_id;
uint64_t my_id = inst->node_id;
char peername[64]; get_node_name(peer_node_id, peername, sizeof(peername));
char peername[64]; get_node_name(inst, peer_node_id, peername, sizeof(peername));
if (!peername[0]) snprintf(peername, sizeof(peername), "%016llX", (unsigned long long)peer_node_id);
int is_server = conn->links ? conn->links->is_server : -1;
@@ -284,24 +285,25 @@ static void collect_conn_metrics(uint64_t peer_node_id) {
link = link->next; link_idx++;
}
chat_event_post(CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off);
chat_event_post(inst, CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off);
}
void chat_core_collect_status_trampoline(void* arg) {
(void)arg;
chat_core_collect_status();
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
chat_core_collect_status(inst);
}
void chat_core_collect_conn_list_trampoline(void* arg) {
(void)arg;
collect_conn_list();
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
collect_conn_list(inst);
}
void chat_core_collect_conn_metrics_trampoline(void* arg) {
uint64_t peer_id;
memcpy(&peer_id, arg, sizeof(peer_id));
struct chat_node_arg* a = (struct chat_node_arg*)arg;
if (!a) return;
uint64_t peer_id = a->node_id;
collect_conn_metrics(a->inst, peer_id);
u_free(arg);
collect_conn_metrics(peer_id);
}
/* ── Live-снапшот для панели деталей участника ──
@@ -334,9 +336,8 @@ static void sock_ifname(const char* name, char* out, size_t out_sz) {
out[len] = '\0';
}
static void collect_member_detail(uint64_t node_id) {
if (!g_cc.initialized || !g_cc.inst) return;
struct UTUN_INSTANCE* inst = g_cc.inst;
static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id) {
if (!CC(inst) || !CC(inst)->initialized || !inst) return;
uint8_t flags = 0;
struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
@@ -469,7 +470,7 @@ static void collect_member_detail(uint64_t node_id) {
memcpy(p, ifname, 38); p += 38;
}
chat_event_post(CHAT_EVT_MEMBER_DETAIL, buf, (int)(p - buf));
chat_event_post(inst, CHAT_EVT_MEMBER_DETAIL, buf, (int)(p - buf));
u_free(buf);
/* summary text: direct RTT or BGP */
@@ -478,7 +479,7 @@ static void collect_member_detail(uint64_t node_id) {
if (rtt > 0) {
char tbuf[32];
int off = snprintf(tbuf, sizeof(tbuf), "direct, rtt:%ums", rtt / 10);
chat_event_post(CHAT_EVT_BGP_INFO, (const uint8_t*)tbuf, off);
chat_event_post(inst, CHAT_EVT_BGP_INFO, (const uint8_t*)tbuf, off);
}
} else if (conn && !conn->links_up && node_id != inst->node_id) {
struct ll_queue* gl = inst->topo_groups ? inst->topo_groups->group_list : NULL;
@@ -495,13 +496,13 @@ static void collect_member_detail(uint64_t node_id) {
int off = snprintf(tbuf, sizeof(tbuf), "rtt:%ums hop:%u via bgp:", cr / 10, hc);
for (uint8_t hi = 0; hi < hc && off < (int)sizeof(tbuf) - 24; hi++) {
char hn[32] = {0};
get_node_name(hops[hi], hn, sizeof(hn));
get_node_name(inst, hops[hi], hn, sizeof(hn));
uint16_t hid = (uint16_t)(hops[hi] & 0xFFFF);
off += snprintf(tbuf + off, sizeof(tbuf) - (size_t)off,
" [%04X]%s", hid, hn[0] ? " " : "");
if (hn[0]) off += snprintf(tbuf + off, sizeof(tbuf) - (size_t)off, "%s", hn);
}
chat_event_post(CHAT_EVT_BGP_INFO, (const uint8_t*)tbuf, off);
chat_event_post(inst, CHAT_EVT_BGP_INFO, (const uint8_t*)tbuf, off);
}
break;
}
@@ -509,8 +510,9 @@ static void collect_member_detail(uint64_t node_id) {
}
void chat_core_collect_member_detail_trampoline(void* arg) {
uint64_t node_id;
memcpy(&node_id, arg, sizeof(node_id));
struct chat_node_arg* a = (struct chat_node_arg*)arg;
if (!a) return;
uint64_t node_id = a->node_id;
collect_member_detail(a->inst, node_id);
u_free(arg);
collect_member_detail(node_id);
}
+195 -190
View File
@@ -35,7 +35,9 @@
#include <android/log.h>
#endif
static struct chat_sync* g_cs = NULL;
static inline struct chat_sync* cs_of(struct UTUN_INSTANCE* inst) {
return inst ? inst->chat_sync : NULL;
}
struct channel_cache {
char channel_id[64];
@@ -194,7 +196,8 @@ static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
}
return;
}
if (!g_cs || !g_cs->initialized) { 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;
@@ -203,7 +206,7 @@ static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
uint64_t group_id = be64toh(*(const uint64_t*)(d + 1));
uint8_t type = d[9];
char ch_id[64];
if (cs_group_id_to_ch_id(g_cs->inst, group_id, ch_id, sizeof(ch_id)) != 0) {
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_REQUEST) {
@@ -221,19 +224,19 @@ static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
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(g_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(g_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(g_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(g_cs, peer, ch_id, pl, plen); break;
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 (g_cs->info_req_timer) { uasync_cancel_timeout(g_cs->inst->ua, g_cs->info_req_timer); g_cs->info_req_timer = NULL; }
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, &g_cs->pending_invite_ch_id, 8); chat_event_post(CHAT_EVT_CONNECT_RESULT, err, 20);
g_cs->pending_invite_node_id = 0; g_cs->pending_invite_ch_id = 0;
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(g_cs, peer, ch_id, pl, plen); break;
case CS_MSG_CHANNEL_INVITE: cs_handle_channel_invite(cs, peer, ch_id, pl, plen); 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);
@@ -251,7 +254,7 @@ static void cs_info_req_timeout_cb(void* arg) {
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(CHAT_EVT_CONNECT_RESULT, err, 20);
chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, err, 20);
cs->pending_invite_node_id = 0;
cs->pending_invite_ch_id = 0;
}
@@ -266,7 +269,7 @@ static void cs_join_timeout_cb(void* arg) {
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(CHAT_EVT_CONNECT_RESULT, err, 20);
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;
@@ -282,7 +285,7 @@ static void cs_invite_send_timeout_cb(void* arg) {
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(CHAT_EVT_CONNECT_RESULT, err, 20);
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;
@@ -350,7 +353,7 @@ static void cs_post_channel_online(struct chat_sync* cs, const char* ch_id) {
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(CHAT_EVT_CHANNEL_PEERS_ONLINE, evt, 1 + (int)cl + 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);
}
@@ -367,7 +370,7 @@ static void cs_flush_sync(struct chat_sync* cs) {
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, NULL);
member_sync_start(cs->inst, pid, ch->channel_id, _on_member_sync_done, cs);
}
}
}
@@ -391,52 +394,53 @@ static void cs_schedule_sync(struct chat_sync* cs) {
/* ── Peer status change (GUI + online-бар + schedule_sync; онлайн в БД не хранится) ── */
static void cs_on_peer_status_changed(uint64_t peer, int online) {
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 < g_cs->channel_count; i++) {
for (int j = 0; j < g_cs->channels[i].peer_count; j++) {
if (g_cs->channels[i].peer_ids[j] == peer) {
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 = g_cs->channels[i].channel_id;
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(ch_id, peer, mevt + 1 + cl + 1) == 0)
chat_event_post(CHAT_EVT_MEMBER_UPDATED, mevt, 1 + (int)cl + 1 + CHAT_MEMBER_DISPLAY_SIZE);
cs_post_channel_online(g_cs, ch_id);
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, g_cs->channel_count, found_in_channel);
CS_ID, (unsigned long long)peer, online, cs->channel_count, found_in_channel);
/* 2. Запланировать member_sync_start (throttled) */
cs_schedule_sync(g_cs);
cs_schedule_sync(cs);
}
static void cs_resume_db_sync(const char* ch_id) {
if (!g_cs || !ch_id || !ch_id[0]) return;
struct DB_SYNC_INSTANCE* si = si_find(ch_id);
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) {
(void)arg;
struct chat_sync* cs = (struct chat_sync*)arg;
if (result == MT_OK) {
struct channel_cache* ch = g_cs ? cs_find(g_cs, ns) : NULL;
struct channel_cache* ch = cs ? cs_find(cs, ns) : NULL;
if (ch) ch->synced = CS_SYNC_DONE;
cs_resume_db_sync(ns);
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];
};
@@ -446,13 +450,13 @@ static void _on_invite_sync_done(uint64_t peer, const char* ns, int result, void
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 && g_cs) {
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(g_cs, sa->ch_id);
cs_resume_db_sync(sa->ch_id);
cs_post_channel_online(sa->cs, sa->ch_id);
cs_resume_db_sync(sa->cs, sa->ch_id);
}
}
u_free(sa);
@@ -482,62 +486,62 @@ static void cs_start_channel_join(struct chat_sync* cs, uint64_t peer) {
/* ── Connection callbacks ── */
static void cs_on_conn_up(struct ETCP_CONN* conn, int event, void* arg) { (void)event;
(void)arg;
if (!conn || !g_cs) return;
struct chat_sync* cs = (struct chat_sync*)arg;
if (!conn || !cs) return;
uint64_t peer = conn->peer_node_id;
if (peer == 0 || peer == g_cs->inst->node_id) return;
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)g_cs->pending_invite_ch_id, conn->links_up);
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 (g_cs->pending_invite_ch_id != 0) {
if (g_cs->pending_invite_node_id != 0 && g_cs->pending_invite_node_id != peer)
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)g_cs->pending_invite_node_id, (unsigned long long)peer);
g_cs->pending_invite_node_id = peer;
if (g_cs->pending_invite_is_inviter) {
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)g_cs->pending_invite_ch_id);
(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(g_cs->inst->topo_sqlite_db,
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(g_cs, g_cs->inst, ch_id, peer, ch_name, ch_owner, ch_x2, ch_ed2, ch_sig);
if (g_cs->invite_send_timer) { uasync_cancel_timeout(g_cs->inst->ua, g_cs->invite_send_timer); }
g_cs->invite_send_timer = uasync_set_timeout(g_cs->inst->ua,
CS_INVITE_SEND_TIMEOUT_MS * 10, g_cs, cs_invite_send_timeout_cb, "cs_invite_send");
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(g_cs, peer);
cs_start_channel_join(cs, peer);
}
cs_on_peer_status_changed(peer, 1);
cs_on_peer_status_changed(cs, peer, 1);
return;
}
cs_on_peer_status_changed(peer, 1);
cs_on_peer_status_changed(cs, peer, 1);
}
static void cs_on_conn_down(struct ETCP_CONN* conn, int event, void* arg) { (void)event;
(void)arg;
if (!conn || !g_cs) return;
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(peer, 0);
cs_on_peer_status_changed(cs, peer, 0);
cs_cancel_proto_timers(g_cs);
if (g_cs->pending_invite_node_id == peer) {
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 < g_cs->channel_count; i++) {
for (int i = 0; i < cs->channel_count; i++) {
int found = 0;
for (int j = 0; j < g_cs->channels[i].peer_count; j++)
if (g_cs->channels[i].peer_ids[j] == peer) { found = 1; break; }
for (int j = 0; j < cs->channels[i].peer_count; j++)
if (cs->channels[i].peer_ids[j] == peer) { found = 1; break; }
if (found)
g_cs->channels[i].synced = CS_SYNC_NONE;
cs->channels[i].synced = CS_SYNC_NONE;
}
}
@@ -550,21 +554,11 @@ static void cs_on_conn_status(struct ETCP_CONN* conn, int status, void* arg) {
}
}
#if 0 /* replaced by cs_on_conn_status */
static void cs_on_new_conn(struct ETCP_CONN* conn, void* arg) {
(void)arg;
if (!conn) return;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: new_conn conn=%p", CS_ID, (void*)conn);
etcp_conn_add_cbk(conn, cs_on_conn_up, NULL, ETCP_CBK_EVENT_UP);
etcp_conn_add_cbk(conn, cs_on_conn_down, NULL, ETCP_CBK_EVENT_DOWN);
}
#endif
/* ── 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(buf, sizeof(buf), &buf_len) != 0) return;
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);
@@ -584,15 +578,15 @@ static void cs_refresh_channels(struct chat_sync* cs) {
cs->channels[i].channel_id[id_len] = '\0';
p += id_len; rem -= id_len;
cs->channels[i].msg_count = chat_core_count(cs->channels[i].channel_id);
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->channels[i].channel_id,
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->channels[i].channel_id, peer_buf,
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));
@@ -607,18 +601,18 @@ static void cs_refresh_channels(struct chat_sync* cs) {
}
static void refresh_timer_cb(void* arg) {
(void)arg;
if (!g_cs || !g_cs->initialized) return;
struct chat_sync* cs = (struct chat_sync*)arg;
if (!cs || !cs->initialized) return;
#ifdef UTUN_HAVE_STANDBY
if (standby_get_sleep_tb() > 0) {
g_cs->refresh_wait = standby_wait(g_cs, refresh_timer_cb);
cs->refresh_wait = standby_wait(cs, refresh_timer_cb);
return;
}
g_cs->refresh_wait = NULL;
cs->refresh_wait = NULL;
#endif
cs_refresh_channels(g_cs);
g_cs->refresh_timer = uasync_set_timeout(g_cs->inst->ua,
30u * 10000u, g_cs, refresh_timer_cb, "cs_refresh");
cs_refresh_channels(cs);
cs->refresh_timer = uasync_set_timeout(cs->inst->ua,
30u * 10000u, cs, refresh_timer_cb, "cs_refresh");
}
/* ── TTL cleanup ── */
@@ -633,10 +627,10 @@ int chat_sync_init(struct UTUN_INSTANCE* inst) {
if (!cs) return -1;
cs->inst = inst;
cs->initialized = 1;
g_cs = cs;
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, NULL);
etcp_add_conn_status_cbk(inst, cs_on_conn_status, cs);
cs_refresh_channels(cs);
@@ -646,7 +640,7 @@ int chat_sync_init(struct UTUN_INSTANCE* inst) {
cs->join_timer = NULL;
member_sync_init(inst);
member_sync_add_apply_cbk(cs_on_member_applied, cs);
member_sync_add_apply_cbk(inst, cs_on_member_applied, cs);
chat_admin_init(inst);
chat_join_init(inst);
@@ -655,21 +649,21 @@ int chat_sync_init(struct UTUN_INSTANCE* inst) {
}
void chat_sync_destroy(struct UTUN_INSTANCE* inst) {
struct chat_sync* cs = g_cs;
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(cs_on_member_applied, cs);
member_sync_remove_apply_cbk(inst, cs_on_member_applied, cs);
cs_pending_free_all(cs);
member_sync_destroy(inst);
chat_admin_destroy(inst);
chat_join_destroy(inst);
cs->initialized = 0; g_cs = NULL;
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, NULL);
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; }
@@ -683,14 +677,15 @@ void chat_sync_destroy(struct UTUN_INSTANCE* inst) {
}
void chat_sync_retry_channels_on_socket_change(struct UTUN_INSTANCE* inst) {
if (!g_cs || !g_cs->initialized || !inst->topo_groups) return;
for (int i = 0; i < g_cs->channel_count; i++) {
uint64_t gid = strtoull(g_cs->channels[i].channel_id, NULL, 10);
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, g_cs->channels[i].channel_id);
CS_ID, cs->channels[i].channel_id);
topo_group_connect_restart(group);
}
}
@@ -701,14 +696,14 @@ void chat_sync_connect_node(struct UTUN_INSTANCE* inst, uint64_t node_id) {
}
void chat_sync_on_channel_deleted(struct UTUN_INSTANCE* inst, const char* ch_id) {
(void)inst;
if (!g_cs || !ch_id || !ch_id[0]) return;
for (int i = 0; i < g_cs->channel_count; i++) {
if (strcmp(g_cs->channels[i].channel_id, ch_id) != 0) continue;
if (g_cs->channels[i].peer_ids) u_free(g_cs->channels[i].peer_ids);
memmove(&g_cs->channels[i], &g_cs->channels[i + 1],
(size_t)(g_cs->channel_count - i - 1) * sizeof(struct channel_cache));
g_cs->channel_count--;
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;
}
@@ -721,9 +716,11 @@ struct chat_invite {
uint8_t* addrs_data;
int addrs_data_len;
int addr_count;
uint64_t join_key;
char password[128];
};
struct cm_invite_wrap { struct chat_invite inv; };
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 заполнены). */
@@ -762,13 +759,23 @@ static void invite_reality_sock_append(struct TOPO_NODE* ni, uint8_t socket_id,
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 = chat_core_get_inst();
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(w); return; }
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;
@@ -814,50 +821,52 @@ static void cm_invite_trampoline(void* arg) {
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(w); return;
u_free(ni); u_free(inv->addrs_data); u_free(w); return;
}
/* 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);
/* если линк к узлу уже есть и он 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 {
/* если уже подключены к connection-узлу — cs_on_conn_up отработал до установки
pending_invite_ch_id и повторно не сработает, поэтому запускаем join-хендшейк напрямую */
struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
if (conn && conn->links_up && conn->initialized) {
g_cs->pending_invite_node_id = node_id;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: already connected to 0x%016llx — starting join handshake directly",
CS_ID, (unsigned long long)node_id);
cs_start_channel_join(g_cs, node_id);
}
/* 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(uint64_t channel_id, uint64_t node_id,
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) {
if (!g_cs || !g_cs->inst || !g_cs->inst->ua) {
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 g_cs=%p inst=%p ua=%p",
(long long)channel_id, (long long)node_id, (void*)g_cs,
g_cs ? (void*)g_cs->inst : NULL,
(g_cs && g_cs->inst) ? (void*)g_cs->inst->ua : NULL);
__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(CHAT_EVT_CONNECT_RESULT, err, 12);
chat_event_post(inst, CHAT_EVT_CONNECT_RESULT, err, 12);
return;
}
@@ -874,18 +883,9 @@ void chat_sync_connect_from_invite(uint64_t channel_id, uint64_t node_id,
memcpy(inv->addrs_data, addrs_data, addrs_sz);
inv->addrs_data_len = (int)addrs_sz;
inv->addr_count = addr_count;
g_cs->pending_invite_ch_id = channel_id;
g_cs->pending_invite_node_id = node_id;
g_cs->pending_join_key = join_key;
if (password && password[0]) {
size_t pl = strlen(password);
if (pl >= sizeof(g_cs->pending_password)) pl = sizeof(g_cs->pending_password) - 1;
memcpy(g_cs->pending_password, password, pl);
g_cs->pending_password[pl] = '\0';
} else {
g_cs->pending_password[0] = '\0';
}
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");
@@ -895,16 +895,18 @@ void chat_sync_connect_from_invite(uint64_t channel_id, uint64_t node_id,
CS_ID, channel_id, node_id, *(const uint64_t*)pubkey_bin, addr_count, password && password[0] ? "yes" : "no");
#endif
struct cm_invite_wrap { struct chat_invite inv; }* w = u_malloc(sizeof(struct cm_invite_wrap));
struct cm_invite_wrap* w = u_malloc(sizeof(struct cm_invite_wrap));
w->inst = inst;
w->inv = *inv; u_free(inv);
uasync_post(g_cs->inst->ua,
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) {
if (!g_cs) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel: g_cs=NULL, chat_sync not initialized", CS_ID); return; }
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);
@@ -920,10 +922,10 @@ void chat_sync_join_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint6
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel ch=%s peer=0x%016llx",
CS_ID, ch_id, (unsigned long long)target_node_id);
g_cs->pending_invite_ch_id = channel_id;
g_cs->pending_invite_node_id = target_node_id;
cs_start_channel_join(g_cs, target_node_id);
cs_on_peer_status_changed(target_node_id, 1);
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 ─── */
@@ -1132,7 +1134,7 @@ static void cs_handle_join_info_resp(struct chat_sync* cs, uint64_t peer,
/* 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(ch_id);
chat_core_ensure_channel_ready(cs->inst, ch_id);
/* добавить connection-узел в мемберы (дерево приглашений из INFO_RESP) */
sqlite3* vdb = cs->inst->topo_sqlite_db;
@@ -1325,18 +1327,18 @@ static void cs_handle_join_ready(struct chat_sync* cs, uint64_t peer,
if (c) topo_group_new_conn(g, c);
}
}
chat_core_ensure_channel_ready(ch_id);
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(CHAT_EVT_CHANNEL_UPDATED, evt, 1 + 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(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(CHAT_EVT_CONNECT_RESULT, cevt, 20);
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;
}
@@ -1386,7 +1388,7 @@ static void cs_handle_channel_invite(struct chat_sync* cs, uint64_t peer,
int policy;
{
struct global_config *gcfg = &cs->inst->config->global;
policy = gcfg->chatserver_enabled ? 0 : chat_setting_get_int("join_policy", 1);
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);
@@ -1405,7 +1407,7 @@ static void cs_handle_channel_invite(struct chat_sync* cs, uint64_t peer,
ch_id, name, owner, x25519, NULL, ed_pub, NULL, ch_sig);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE channel_put rc=%d ch=%s ch_x25519=%016llx",
CS_ID, cprc, ch_id, *(const uint64_t*)x25519);
chat_core_ensure_channel_ready(ch_id);
chat_core_ensure_channel_ready(cs->inst, ch_id);
/* Initiate join: connect back and send CHANNEL_INFO_REQ */
chat_sync_join_channel(cs->inst, ch_id, peer);
return;
@@ -1432,7 +1434,7 @@ static void cs_handle_channel_invite(struct chat_sync* cs, uint64_t peer,
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(CHAT_EVT_INVITE_RECEIVED, evt, epos);
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);
}
@@ -1483,7 +1485,8 @@ static int cs_send_channel_invite(struct chat_sync* cs, struct UTUN_INSTANCE* in
/* ─── chat_sync_invite_to_channel: пригласить узел в наш канал ─── */
void chat_sync_invite_to_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id) {
if (!g_cs) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel: g_cs=NULL, chat_sync not initialized", CS_ID); return; }
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; }
@@ -1512,8 +1515,8 @@ void chat_sync_invite_to_channel(struct UTUN_INSTANCE* inst, const char* ch_id,
/* 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);
g_cs->pending_invite_ch_id = channel_id;
g_cs->pending_invite_node_id = target_node_id;
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
return;
}
}
@@ -1526,28 +1529,27 @@ void chat_sync_invite_to_channel(struct UTUN_INSTANCE* inst, const char* ch_id,
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.
But we need to distinguish this from the joiner-side invite flow.
Use a separate flag. */
g_cs->pending_invite_ch_id = channel_id;
g_cs->pending_invite_node_id = target_node_id;
g_cs->pending_invite_is_inviter = 1;
/* 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(g_cs, inst, ch_id, target_node_id, ch_name, owner, ch_x25519, ch_ed, ch_sig);
g_cs->pending_invite_ch_id = channel_id;
g_cs->pending_invite_node_id = target_node_id;
g_cs->pending_invite_is_inviter = 1;
if (g_cs->invite_send_timer) { uasync_cancel_timeout(inst->ua, g_cs->invite_send_timer); }
g_cs->invite_send_timer = uasync_set_timeout(inst->ua,
CS_INVITE_SEND_TIMEOUT_MS * 10, g_cs, cs_invite_send_timeout_cb, "cs_invite_send");
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];
@@ -1558,8 +1560,9 @@ struct cs_invite_wrap {
static void cs_invite_trampoline(void* arg) {
struct cs_invite_wrap* w = (struct cs_invite_wrap*)arg;
if (!g_cs || !g_cs->inst) { u_free(w); return; }
struct UTUN_INSTANCE* inst = g_cs->inst;
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;
@@ -1577,13 +1580,13 @@ static void cs_invite_trampoline(void* arg) {
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(g_cs, inst, ch_id, target_node_id, ch_name, ch_owner, ch_x2, ch_ed2, ch_sig);
g_cs->pending_invite_ch_id = channel_id;
g_cs->pending_invite_node_id = target_node_id;
g_cs->pending_invite_is_inviter = 1;
if (g_cs->invite_send_timer) { uasync_cancel_timeout(inst->ua, g_cs->invite_send_timer); }
g_cs->invite_send_timer = uasync_set_timeout(inst->ua,
CS_INVITE_SEND_TIMEOUT_MS * 10, g_cs, cs_invite_send_timeout_cb, "cs_invite_send");
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;
}
@@ -1629,15 +1632,15 @@ static void cs_invite_trampoline(void* arg) {
u_free(ni); u_free(w); return;
}
g_cs->pending_invite_ch_id = channel_id;
g_cs->pending_invite_node_id = target_node_id;
g_cs->pending_invite_is_inviter = 1;
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);
g_cs->pending_invite_node_id = 0; g_cs->pending_invite_ch_id = 0;
g_cs->pending_invite_is_inviter = 0;
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; }
@@ -1651,7 +1654,8 @@ void chat_sync_invite_to_channel_with_addrs(
const uint8_t* pubkey_bin, const uint8_t* addrs_data, int addr_count,
int addrs_data_len)
{
if (!g_cs || !inst || !ch_id || !ch_id[0] || !pubkey_bin || !addrs_data || addr_count <= 0 || addrs_data_len <= 0) {
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;
}
@@ -1664,6 +1668,7 @@ void chat_sync_invite_to_channel_with_addrs(
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);
@@ -1679,7 +1684,7 @@ void chat_sync_invite_to_channel_with_addrs(
/* ─── accept/deny incoming invite ─── */
void chat_sync_accept_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t inviter_node_id) {
if (!g_cs || !inst) return;
if (!cs_of(inst) || !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",
@@ -1688,13 +1693,13 @@ void chat_sync_accept_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, ui
}
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: accept_invite ch=%llu inviter=0x%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id);
chat_core_ensure_channel_ready(ch_id);
chat_core_ensure_channel_ready(inst, ch_id);
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 (!g_cs) return;
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 */
+1 -1
View File
@@ -74,7 +74,7 @@ void chat_sync_connect_node(struct UTUN_INSTANCE* inst, uint64_t node_id);
/* Подключиться к пиру по данным invite-ссылки (вызывается из uasync-потока).
join_key — 64-битный ключ входа из ссылки (регистрируется инвайтером на connection-узле).
password может быть NULL — тогда отправляется без пароля (совместимость с v1) */
void chat_sync_connect_from_invite(uint64_t channel_id, uint64_t node_id,
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,
+17 -11
View File
@@ -38,6 +38,7 @@ static int g_initialized = 0;
/* очередь транскрипций: whisper не thread-safe, обрабатываем по одной */
struct wh_job {
struct UTUN_INSTANCE* inst;
char audio_path[1024];
char channel_id[64];
uint64_t reply_to_ts;
@@ -231,12 +232,14 @@ static float* resample_16k(const float* src, int n_src, int src_rate, int* n_dst
struct wh_work_ctx;
static void chat_whisper_transcribe_async_impl(
struct UTUN_INSTANCE* inst,
struct media_async* ma, struct UASYNC* ua,
const char* audio_path, const char* channel_id,
uint64_t reply_to_ts, uint64_t reply_to_node,
chat_whisper_done_fn done_cb, void* done_arg);
static void chat_whisper_transcribe_async(
struct UTUN_INSTANCE* inst,
struct media_async* ma, struct UASYNC* ua,
const char* audio_path, const char* channel_id,
uint64_t reply_to_ts, uint64_t reply_to_node,
@@ -360,7 +363,7 @@ static void wh_done_fn(void* raw, int err) {
if (g_pending_job) {
struct wh_job* nj = g_pending_job;
g_pending_job = NULL;
chat_whisper_transcribe_async_impl(nj->ma, nj->ua, nj->audio_path, nj->channel_id,
chat_whisper_transcribe_async_impl(nj->inst, nj->ma, nj->ua, nj->audio_path, nj->channel_id,
nj->reply_to_ts, nj->reply_to_node,
nj->done_cb, nj->done_arg);
u_free(nj);
@@ -369,8 +372,8 @@ static void wh_done_fn(void* raw, int err) {
/* ─── авто-поиск модели ─── */
static int find_model_path(char* out, size_t out_sz) {
const char* setting = chat_setting_get("whisper_model_path");
static int find_model_path(struct UTUN_INSTANCE* inst, char* out, size_t out_sz) {
const char* setting = chat_setting_get(inst, "whisper_model_path");
if (setting && setting[0] != '\0') {
/* если путь относительный, не проверяем доступ — вернём как есть */
if (access(setting, R_OK) == 0) { snprintf(out, out_sz, "%s", setting); return 0; }
@@ -397,7 +400,7 @@ static int find_model_path(char* out, size_t out_sz) {
/* ─── публичный API ─── */
int chat_whisper_init(void) {
int chat_whisper_init(struct UTUN_INSTANCE* inst) {
if (g_initialized) return 0;
g_initialized = 1;
@@ -405,13 +408,13 @@ int chat_whisper_init(void) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "%s: compiled without whisper support", CW_ID);
return -1;
#else
if (!chat_setting_get_int("whisper_enabled", 0)) {
if (!chat_setting_get_int(inst, "whisper_enabled", 0)) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT, "%s: whisper disabled in settings", CW_ID);
return -1;
}
char model_path[512];
if (find_model_path(model_path, sizeof(model_path)) != 0) {
if (find_model_path(inst, model_path, sizeof(model_path)) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "%s: model not found (set whisper_model_path)", CW_ID);
return -1;
}
@@ -432,9 +435,9 @@ int chat_whisper_init(void) {
#endif /* HAVE_WHISPER */
}
int chat_whisper_available(void) {
int chat_whisper_available(struct UTUN_INSTANCE* inst) {
#ifdef HAVE_WHISPER
return g_whisper_ctx != NULL && chat_setting_get_int("whisper_enabled", 0);
return g_whisper_ctx != NULL && chat_setting_get_int(inst, "whisper_enabled", 0);
#else
return 0;
#endif
@@ -455,17 +458,19 @@ void chat_whisper_destroy(void) {
}
void chat_whisper_transcribe_async(
struct UTUN_INSTANCE* inst,
struct media_async* ma, struct UASYNC* ua,
const char* audio_path, const char* channel_id,
uint64_t reply_to_ts, uint64_t reply_to_node,
chat_whisper_done_fn done_cb, void* done_arg)
{
chat_whisper_transcribe_async_impl(ma, ua, audio_path, channel_id,
chat_whisper_transcribe_async_impl(inst, ma, ua, audio_path, channel_id,
reply_to_ts, reply_to_node,
done_cb, done_arg);
}
static void chat_whisper_transcribe_async_impl(
struct UTUN_INSTANCE* inst,
struct media_async* ma, struct UASYNC* ua,
const char* audio_path, const char* channel_id,
uint64_t reply_to_ts, uint64_t reply_to_node,
@@ -477,8 +482,8 @@ static void chat_whisper_transcribe_async_impl(
return;
}
if (!chat_whisper_available()) {
int rc = chat_whisper_init();
if (!chat_whisper_available(inst)) {
int rc = chat_whisper_init(inst);
if (rc != 0) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "%s: whisper not available, skip transcription", CW_ID);
done_cb(done_arg, channel_id, NULL, -1, reply_to_ts, reply_to_node);
@@ -491,6 +496,7 @@ static void chat_whisper_transcribe_async_impl(
if (g_pending_job) { u_free(g_pending_job); } /* заменяем */
g_pending_job = u_malloc(sizeof(*g_pending_job));
if (!g_pending_job) { done_cb(done_arg, channel_id, NULL, -1, reply_to_ts, reply_to_node); return; }
g_pending_job->inst = inst;
snprintf(g_pending_job->audio_path, sizeof(g_pending_job->audio_path), "%s", audio_path);
snprintf(g_pending_job->channel_id, sizeof(g_pending_job->channel_id), "%s", channel_id);
g_pending_job->reply_to_ts = reply_to_ts;
+4 -2
View File
@@ -24,11 +24,12 @@ extern "C" {
struct media_async;
struct UASYNC;
struct UTUN_INSTANCE;
/* ── жизненный цикл ── */
int chat_whisper_init(void); /* ленивая загрузка модели */
int chat_whisper_available(void); /* модель загружена и готова */
int chat_whisper_init(struct UTUN_INSTANCE* inst); /* ленивая загрузка модели */
int chat_whisper_available(struct UTUN_INSTANCE* inst); /* модель загружена и готова */
void chat_whisper_destroy(void); /* выгрузка модели */
/* ── асинхронная транскрипция ── */
@@ -38,6 +39,7 @@ typedef void (*chat_whisper_done_fn)(void* arg, const char* channel_id,
uint64_t reply_ts, uint64_t reply_node);
typedef void (*chat_whisper_trigger_fn)(
struct UTUN_INSTANCE* inst,
struct media_async* ma, struct UASYNC* ua,
const char* audio_path, const char* channel_id,
uint64_t reply_ts, uint64_t reply_node,
+73 -63
View File
@@ -3,7 +3,7 @@
*
* Общий модуль для всех GUI (desktop chatgui, Android, headless CLI).
* Логика выбора «лучшего узла» перенесена из tools/chatgui-android/jni_bridge.
* Работает в uasync-потоке, использует общий контекст g_cc.
* Работает в uasync-потоке, использует per-instance контекст.
*/
#include "invite_build.h"
#include "invite_link.h"
@@ -25,15 +25,16 @@
/* ── живость узла в канале (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);
static struct TOPO_GROUP* resolve_chat_group(struct UTUN_INSTANCE* inst, uint64_t channel_id) {
if (!inst || !inst->topo_groups) return NULL;
struct TOPO_GROUP* grp = topo_groups_find(inst->topo_groups, channel_id);
if (!grp || grp->group_type != TOPO_GROUP_TYPE_CHAT) return NULL;
return grp;
}
static int node_is_alive(struct TOPO_GROUP* grp, uint64_t node_id) {
if (node_id == g_cc.my_node_id) return 1; /* self всегда доступен */
static int node_is_alive(struct UTUN_INSTANCE* inst, struct TOPO_GROUP* grp, uint64_t node_id) {
struct chat_core_ctx* cc = CC(inst);
if (cc && node_id == cc->my_node_id) return 1; /* self всегда доступен */
if (!grp) return 0;
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(grp, node_id);
if (!nq) return 0;
@@ -41,13 +42,14 @@ static int node_is_alive(struct TOPO_GROUP* grp, uint64_t node_id) {
}
/* есть ли у узла публичные адреса v4 и v6 (addr_type=DIRECT) */
static int node_has_both_public(uint64_t node_id) {
static int node_has_both_public(struct UTUN_INSTANCE* inst, uint64_t node_id) {
struct chat_core_ctx* cc = CC(inst);
int has_v4 = 0, has_v6 = 0;
sqlite3_stmt* st = NULL;
char sql[256];
snprintf(sql, sizeof(sql),
"SELECT family FROM node_addresses WHERE node_id=? AND addr_type=%d", ADDR_TYPE_DIRECT);
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) return 0;
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) return 0;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
while (sqlite3_step(st) == SQLITE_ROW) {
int fam = sqlite3_column_int(st, 0);
@@ -60,12 +62,13 @@ static int node_has_both_public(uint64_t node_id) {
/* ── Фаза 1-2: лучший достижимый узел в канале ── */
static uint64_t pick_best_node(uint64_t channel_id, int* out_has_both_public, int* out_priority) {
static uint64_t pick_best_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, int* out_has_both_public, int* out_priority) {
struct chat_core_ctx* cc = CC(inst);
uint64_t best_nid = 0;
int best_priority = 4; /* 1=regular, 2=self, 3=super, 4=invalid */
int best_has_both = 0;
struct TOPO_GROUP* grp = resolve_chat_group(channel_id);
struct TOPO_GROUP* grp = resolve_chat_group(inst, channel_id);
if (!grp) {
*out_has_both_public = 0;
*out_priority = best_priority;
@@ -80,9 +83,9 @@ static uint64_t pick_best_node(uint64_t channel_id, int* out_has_both_public, in
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) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &pst, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: pick_best — query failed ch=%llu err=%s",
(unsigned long long)channel_id, sqlite3_errmsg(g_cc.db));
(unsigned long long)channel_id, sqlite3_errmsg(cc->db));
*out_has_both_public = 0;
*out_priority = best_priority;
return 0;
@@ -91,12 +94,12 @@ static uint64_t pick_best_node(uint64_t channel_id, int* out_has_both_public, in
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;
if (!node_is_alive(inst, grp, nid)) continue;
int has_both = node_has_both_public(nid);
int has_both = node_has_both_public(inst, nid);
int priority;
if (nid == g_cc.my_node_id) priority = 2; /* self */
if (nid == cc->my_node_id) priority = 2; /* self */
else if (ntype == 4) priority = 3; /* supernode */
else priority = 1; /* regular */
@@ -114,10 +117,11 @@ static uint64_t pick_best_node(uint64_t channel_id, int* out_has_both_public, in
}
/* авто-узел: pick_best_node + фолбэк на self */
static uint64_t resolve_auto_node(uint64_t channel_id, int* out_has_both, int* out_priority) {
static uint64_t resolve_auto_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, int* out_has_both, int* out_priority) {
struct chat_core_ctx* cc = CC(inst);
int has_both = 0, prio = 4;
uint64_t best = pick_best_node(channel_id, &has_both, &prio);
if (best == 0) best = g_cc.my_node_id;
uint64_t best = pick_best_node(inst, channel_id, &has_both, &prio);
if (best == 0) best = cc->my_node_id;
if (out_has_both) *out_has_both = has_both;
if (out_priority) *out_priority = prio;
return best;
@@ -125,14 +129,15 @@ static uint64_t resolve_auto_node(uint64_t channel_id, int* out_has_both, int* o
/* ── Фаза 3: pubkey узла ── */
static int get_node_pubkey(uint64_t node_id, int is_self, uint8_t pubkey_out[INVITE_PUBKEY_SIZE]) {
static int get_node_pubkey(struct UTUN_INSTANCE* inst, uint64_t node_id, int is_self, uint8_t pubkey_out[INVITE_PUBKEY_SIZE]) {
struct chat_core_ctx* cc = CC(inst);
if (is_self) {
if (!g_cc.inst) return -1;
memcpy(pubkey_out, g_cc.inst->my_keys.public_key, INVITE_PUBKEY_SIZE);
if (!inst) return -1;
memcpy(pubkey_out, inst->my_keys.public_key, INVITE_PUBKEY_SIZE);
return 0;
}
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT x25519_pubkey FROM nodes WHERE node_id=?",
if (sqlite3_prepare_v2(cc->db, "SELECT x25519_pubkey FROM nodes WHERE node_id=?",
-1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
int ok = 0;
@@ -147,13 +152,13 @@ static int get_node_pubkey(uint64_t node_id, int is_self, uint8_t pubkey_out[INV
/* ── Фаза 4a: свои адреса из сокетов ── */
static int collect_self_addrs(struct InviteAddr* out, int max_cnt) {
static int collect_self_addrs(struct UTUN_INSTANCE* inst, struct InviteAddr* out, int max_cnt) {
int cnt = 0;
if (!g_cc.inst) return 0;
if (!inst) return 0;
struct ETCP_SOCKET* s;
/* UDP */
for (s = g_cc.inst->etcp_sockets; s && cnt < max_cnt; s = s->next) {
for (s = inst->etcp_sockets; s && cnt < max_cnt; s = s->next) {
if (s->is_tcp) continue;
struct sockaddr_storage* sa = s->interface_addr.ss_family ? &s->interface_addr : &s->local_addr;
if (sa->ss_family == AF_INET) {
@@ -169,7 +174,7 @@ static int collect_self_addrs(struct InviteAddr* out, int max_cnt) {
}
}
/* TCP */
for (s = g_cc.inst->etcp_sockets; s && cnt < max_cnt; s = s->next) {
for (s = inst->etcp_sockets; s && cnt < max_cnt; s = s->next) {
if (!s->is_tcp) continue;
struct sockaddr_storage* sa = s->interface_addr.ss_family ? &s->interface_addr : &s->local_addr;
uint8_t proto = INVITE_PROTO_TCP | (s->reality_enabled ? INVITE_PROTO_REALITY : 0);
@@ -189,13 +194,13 @@ static int collect_self_addrs(struct InviteAddr* out, int max_cnt) {
}
/* reality-параметры self-узла из конфига (node-global, как в topo_node_update_my_addresses). */
static void collect_self_reality(struct InviteReality* out) {
static void collect_self_reality(struct UTUN_INSTANCE* inst, struct InviteReality* out) {
memset(out, 0, sizeof(*out));
if (!g_cc.inst || !g_cc.inst->config) return;
const struct reality_config* rc = &g_cc.inst->config->global.reality;
if (!inst || !inst->config) return;
const struct reality_config* rc = &inst->config->global.reality;
if (!rc->enabled || !rc->has_private_key) return;
int any = 0;
for (struct ETCP_SOCKET* s = g_cc.inst->etcp_sockets; s; s = s->next)
for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next)
if (s->is_tcp && s->reality_enabled) { any = 1; break; }
if (!any) return;
uint8_t pub[32];
@@ -211,8 +216,9 @@ static void collect_self_reality(struct InviteReality* out) {
/* ── Фаза 4b: адреса узла из БД ── */
static int collect_node_addrs_from_db(uint64_t node_id, struct InviteAddr* out, int max_cnt, int dir_only,
static int collect_node_addrs_from_db(struct UTUN_INSTANCE* inst, uint64_t node_id, struct InviteAddr* out, int max_cnt, int dir_only,
struct InviteReality* reality) {
struct chat_core_ctx* cc = CC(inst);
char sql[256];
if (dir_only)
snprintf(sql, sizeof(sql),
@@ -223,7 +229,7 @@ static int collect_node_addrs_from_db(uint64_t node_id, struct InviteAddr* out,
"SELECT family,socket_id,protocol,address,port,options 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;
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) return 0;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
int cnt = 0;
@@ -257,13 +263,14 @@ static int collect_node_addrs_from_db(uint64_t node_id, struct InviteAddr* out,
/* ── публичный API ── */
int chat_invite_build_link(uint64_t channel_id, uint64_t target_node_id,
int chat_invite_build_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id,
const char* password, char* out, size_t out_size) {
struct chat_core_ctx* cc = CC(inst);
if (!out || out_size == 0) return -1;
out[0] = '\0';
if (!g_cc.initialized || !g_cc.inst || !g_cc.db) {
if (!cc || !cc->initialized || !inst || !cc->db) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: not initialized (inst=%p db=%p)",
(void*)g_cc.inst, (void*)g_cc.db);
(void*)inst, (void*)(cc ? cc->db : NULL));
return -1;
}
if (channel_id == 0) {
@@ -277,23 +284,23 @@ int chat_invite_build_link(uint64_t channel_id, uint64_t target_node_id,
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;
target_nid = resolve_auto_node(inst, channel_id, &has_both_public, &prio);
use_self = (target_nid == cc->my_node_id) ? 1 : 0;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: auto ch=%llu best=0x%016llx prio=%d self=%d both=%d",
(unsigned long long)channel_id, (unsigned long long)target_nid,
prio, use_self, has_both_public);
} else {
if (target_nid == g_cc.my_node_id) {
if (target_nid == cc->my_node_id) {
use_self = 1;
} else {
struct TOPO_GROUP* grp = resolve_chat_group(channel_id);
if (!node_is_alive(grp, target_nid)) {
struct TOPO_GROUP* grp = resolve_chat_group(inst, channel_id);
if (!node_is_alive(inst, grp, target_nid)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC,
"invite_build: node 0x%016llx not reachable in ch=%llu",
(unsigned long long)target_nid, (unsigned long long)channel_id);
return -1;
}
has_both_public = node_has_both_public(target_nid);
has_both_public = node_has_both_public(inst, target_nid);
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: manual ch=%llu target=0x%016llx self=%d both=%d",
(unsigned long long)channel_id, (unsigned long long)target_nid, use_self, has_both_public);
@@ -309,7 +316,7 @@ int chat_invite_build_link(uint64_t channel_id, uint64_t target_node_id,
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: join_key generation failed");
return -1;
}
if (get_node_pubkey(target_nid, use_self, data.pubkey) != 0) {
if (get_node_pubkey(inst, target_nid, use_self, data.pubkey) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: no pubkey for node=0x%016llx",
(unsigned long long)target_nid);
return -1;
@@ -317,11 +324,11 @@ int chat_invite_build_link(uint64_t channel_id, uint64_t target_node_id,
/* ── адреса ── */
if (use_self) {
data.addrCount = (uint8_t)collect_self_addrs(data.addrs, INVITE_ADDR_MAX);
collect_self_reality(&data.reality);
data.addrCount = (uint8_t)collect_self_addrs(inst, data.addrs, INVITE_ADDR_MAX);
collect_self_reality(inst, &data.reality);
} else {
int dir_only = has_both_public ? 1 : 0;
data.addrCount = (uint8_t)collect_node_addrs_from_db(target_nid, data.addrs, INVITE_ADDR_MAX, dir_only, &data.reality);
data.addrCount = (uint8_t)collect_node_addrs_from_db(inst, target_nid, data.addrs, INVITE_ADDR_MAX, dir_only, &data.reality);
}
if (data.addrCount == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: no addrs for node=0x%016llx",
@@ -340,28 +347,30 @@ int chat_invite_build_link(uint64_t channel_id, uint64_t target_node_id,
}
/* ── регистрация join-ключа на connection-узле (или локально, если target==self) ── */
chat_join_register_key(g_cc.inst, channel_id, target_nid, data.join_key);
chat_join_register_key(inst, channel_id, target_nid, data.join_key);
return 0;
}
int chat_invite_best_node(uint64_t channel_id, uint64_t* out_node_id) {
int chat_invite_best_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t* out_node_id) {
struct chat_core_ctx* cc = CC(inst);
if (!out_node_id) return -1;
if (!g_cc.initialized || !g_cc.inst || !g_cc.db) {
if (!cc || !cc->initialized || !inst || !cc->db) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: best_node — not initialized");
return -1;
}
*out_node_id = resolve_auto_node(channel_id, NULL, NULL);
*out_node_id = resolve_auto_node(inst, channel_id, NULL, NULL);
return 0;
}
int chat_invite_candidate_nodes(uint64_t channel_id, uint64_t* out_ids, int max_ids) {
int chat_invite_candidate_nodes(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t* out_ids, int max_ids) {
struct chat_core_ctx* cc = CC(inst);
if (!out_ids || max_ids <= 0) return -1;
if (!g_cc.initialized || !g_cc.inst || !g_cc.db) {
if (!cc || !cc->initialized || !inst || !cc->db) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — not initialized");
return -1;
}
struct TOPO_GROUP* grp = resolve_chat_group(channel_id);
struct TOPO_GROUP* grp = resolve_chat_group(inst, channel_id);
if (!grp) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — group not found ch=%llu",
(unsigned long long)channel_id);
@@ -376,16 +385,16 @@ int chat_invite_candidate_nodes(uint64_t channel_id, uint64_t* out_ids, int max_
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) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — query failed ch=%llu err=%s",
(unsigned long long)channel_id, sqlite3_errmsg(g_cc.db));
(unsigned long long)channel_id, sqlite3_errmsg(cc->db));
return -1;
}
int cnt = 0;
while (sqlite3_step(st) == SQLITE_ROW && cnt < max_ids) {
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
if (node_is_alive(grp, nid)) out_ids[cnt++] = nid;
if (node_is_alive(inst, grp, nid)) out_ids[cnt++] = nid;
}
sqlite3_finalize(st);
return cnt;
@@ -394,18 +403,19 @@ int chat_invite_candidate_nodes(uint64_t channel_id, uint64_t* out_ids, int max_
/* ── трамплины для gui_bridge ── */
void chat_invite_candidates_trampoline(void* arg) {
if (!arg) return;
struct chat_invite_cand_req* req = (struct chat_invite_cand_req*)arg;
if (!req) return;
char ch_id[64];
strncpy(ch_id, (const char*)arg, sizeof(ch_id) - 1);
strncpy(ch_id, req->ch_id, 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; }
if (!CC(req->inst) || !CC(req->inst)->initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — not initialized"); return; }
uint64_t channel_id = strtoull(ch_id, NULL, 10);
uint64_t auto_id = 0;
chat_invite_best_node(channel_id, &auto_id);
chat_invite_best_node(req->inst, channel_id, &auto_id);
uint64_t ids[INVITE_ADDR_MAX];
int cnt = chat_invite_candidate_nodes(channel_id, ids, INVITE_ADDR_MAX);
int cnt = chat_invite_candidate_nodes(req->inst, channel_id, ids, INVITE_ADDR_MAX);
if (cnt < 0) cnt = 0;
size_t ch_len = strlen(ch_id);
@@ -418,7 +428,7 @@ void chat_invite_candidates_trampoline(void* arg) {
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);
chat_event_post(req->inst, CHAT_EVT_INVITE_CANDIDATES, evt, (int)evt_sz);
u_free(evt);
}
@@ -438,7 +448,7 @@ void chat_invite_build_link_trampoline(void* arg) {
}
char link[1024];
int ok = chat_invite_build_link(channel_id, target, pass, link, sizeof(link));
int ok = chat_invite_build_link(req->inst, channel_id, target, pass, link, sizeof(link));
size_t ch_len = strlen(req->ch_id);
size_t link_len = (ok == 0) ? strlen(link) : 0;
@@ -450,7 +460,7 @@ void chat_invite_build_link_trampoline(void* arg) {
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);
chat_event_post(req->inst, CHAT_EVT_INVITE_LINK_READY, evt, (int)evt_sz);
u_free(evt);
u_free(req);
}
+8 -4
View File
@@ -15,32 +15,36 @@
extern "C" {
#endif
struct UTUN_INSTANCE;
/* Собрать 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,
int chat_invite_build_link(struct UTUN_INSTANCE* inst, 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);
int chat_invite_candidate_nodes(struct UTUN_INSTANCE* inst, 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);
int chat_invite_best_node(struct UTUN_INSTANCE* inst, 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 {
struct UTUN_INSTANCE* inst;
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
/* Трамплин (arg = u_malloc(struct chat_invite_cand_req)). Результат: CHAT_EVT_INVITE_CANDIDATES
* (формат: [ch_id_len:1][ch_id][auto_node_id:8][count:2][node_id:8]*). */
struct chat_invite_cand_req { struct UTUN_INSTANCE* inst; char ch_id[64]; };
void chat_invite_candidates_trampoline(void* arg);
#ifdef __cplusplus
+23 -27
View File
@@ -240,25 +240,25 @@ static int _member_get_items(void* ctx, const char* ns, uint8_t level,
/* ── node_props_changed (adm_tags) callbacks ── */
struct ms_props_cbk { node_props_changed_fn fn; void* arg; struct ms_props_cbk* next; };
static struct ms_props_cbk* g_props_cbks = NULL;
static void _fire_props_changed(uint64_t node_id, const char* adm_tags, const char* channel_id) {
struct ms_props_cbk* pc = g_props_cbks;
static void _fire_props_changed(struct UTUN_INSTANCE* inst, uint64_t node_id, const char* adm_tags, const char* channel_id) {
if (!inst || !CC(inst)) return;
struct ms_props_cbk* pc = CC(inst)->props_cbks;
while (pc) { pc->fn(node_id, adm_tags, channel_id, pc->arg); pc = pc->next; }
}
void member_sync_add_props_cbk(node_props_changed_fn fn, void* arg) {
if (!fn) return;
void member_sync_add_props_cbk(struct UTUN_INSTANCE* inst, node_props_changed_fn fn, void* arg) {
if (!inst || !fn || !CC(inst)) return;
struct ms_props_cbk* e = u_malloc(sizeof(*e));
if (!e) return;
e->fn = fn; e->arg = arg;
e->next = g_props_cbks;
g_props_cbks = e;
e->next = CC(inst)->props_cbks;
CC(inst)->props_cbks = e;
}
void member_sync_remove_props_cbk(node_props_changed_fn fn, void* arg) {
if (!fn) return;
struct ms_props_cbk** p = &g_props_cbks;
void member_sync_remove_props_cbk(struct UTUN_INSTANCE* inst, node_props_changed_fn fn, void* arg) {
if (!inst || !fn || !CC(inst)) return;
struct ms_props_cbk** p = &CC(inst)->props_cbks;
while (*p) {
if ((*p)->fn == fn && (*p)->arg == arg) {
struct ms_props_cbk* r = *p;
@@ -272,27 +272,27 @@ void member_sync_remove_props_cbk(node_props_changed_fn fn, void* arg) {
/* ── Коллбэк применения рекорда мембера (ready-хендлинг join на connection-узле) ── */
struct ms_apply_cbk { member_apply_cbk_fn fn; void* arg; struct ms_apply_cbk* next; };
static struct ms_apply_cbk* g_apply_cbks = NULL;
static void _fire_apply_cbk(const char* ch_id, uint64_t node_id,
static void _fire_apply_cbk(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id,
const uint8_t* x25519, uint64_t signed_by,
const uint8_t* signature) {
struct ms_apply_cbk* ac = g_apply_cbks;
if (!inst || !CC(inst)) return;
struct ms_apply_cbk* ac = CC(inst)->apply_cbks;
while (ac) { ac->fn(ch_id, node_id, x25519, signed_by, signature, ac->arg); ac = ac->next; }
}
void member_sync_add_apply_cbk(member_apply_cbk_fn fn, void* arg) {
if (!fn) return;
void member_sync_add_apply_cbk(struct UTUN_INSTANCE* inst, member_apply_cbk_fn fn, void* arg) {
if (!inst || !fn || !CC(inst)) return;
struct ms_apply_cbk* e = u_malloc(sizeof(*e));
if (!e) return;
e->fn = fn; e->arg = arg;
e->next = g_apply_cbks;
g_apply_cbks = e;
e->next = CC(inst)->apply_cbks;
CC(inst)->apply_cbks = e;
}
void member_sync_remove_apply_cbk(member_apply_cbk_fn fn, void* arg) {
if (!fn) return;
struct ms_apply_cbk** p = &g_apply_cbks;
void member_sync_remove_apply_cbk(struct UTUN_INSTANCE* inst, member_apply_cbk_fn fn, void* arg) {
if (!inst || !fn || !CC(inst)) return;
struct ms_apply_cbk** p = &CC(inst)->apply_cbks;
while (*p) {
if ((*p)->fn == fn && (*p)->arg == arg) {
struct ms_apply_cbk* r = *p;
@@ -329,7 +329,7 @@ static void _ms_on_bgp_node(struct TOPO_GROUP* group, uint64_t node_id, int even
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bgp_node ev=%d nid=0x%016llx tags=%s",
MS_ID, event, (unsigned long long)node_id, tags);
_fire_props_changed(node_id, tags[0] ? tags : NULL, group->channel_id);
_fire_props_changed(inst, node_id, tags[0] ? tags : NULL, group->channel_id);
}
static int _sig_is_zero64(const uint8_t* sig) {
@@ -840,13 +840,9 @@ static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer,
if (r & MS_APPLY_CHANGED) {
changed_count++;
/* fire node_props_changed callbacks */
struct ms_props_cbk* pc = g_props_cbks;
if (pc) {
const char* tags_final = atags[0] ? atags : NULL;
while (pc) { pc->fn(nid, tags_final, ns, pc->arg); pc = pc->next; }
}
_fire_props_changed(inst, nid, atags[0] ? atags : NULL, ns);
/* fire apply callbacks (ready-хендлинг join на connection-узле) */
_fire_apply_cbk(ns, nid, x25, sb, sgn);
_fire_apply_cbk(inst, ns, nid, x25, sb, sgn);
}
if (r & MS_APPLY_STALE) {
/* у нас версия новее — отправить наш полный рекорд автору */
+4 -4
View File
@@ -100,8 +100,8 @@ int member_sync_validate_pubkey(struct UTUN_INSTANCE* inst, const char* ch_id,
typedef void (*member_apply_cbk_fn)(const char* ch_id, uint64_t node_id,
const uint8_t* x25519, uint64_t signed_by,
const uint8_t* signature, void* arg);
void member_sync_add_apply_cbk(member_apply_cbk_fn fn, void* arg);
void member_sync_remove_apply_cbk(member_apply_cbk_fn fn, void* arg);
void member_sync_add_apply_cbk(struct UTUN_INSTANCE* inst, member_apply_cbk_fn fn, void* arg);
void member_sync_remove_apply_cbk(struct UTUN_INSTANCE* inst, member_apply_cbk_fn fn, void* arg);
/*
* Инициализировать модуль: создаёт merkle_sync с коллбэками для мемберов
@@ -237,8 +237,8 @@ const uint8_t* member_sync_get_hash(struct UTUN_INSTANCE* inst,
* Многоподписочный — можно добавить несколько подписчиков.
*/
typedef void (*node_props_changed_fn)(uint64_t node_id, const char* adm_tags, const char* channel_id, void* arg);
void member_sync_add_props_cbk(node_props_changed_fn fn, void* arg);
void member_sync_remove_props_cbk(node_props_changed_fn fn, void* arg);
void member_sync_add_props_cbk(struct UTUN_INSTANCE* inst, node_props_changed_fn fn, void* arg);
void member_sync_remove_props_cbk(struct UTUN_INSTANCE* inst, node_props_changed_fn fn, void* arg);
#ifdef __cplusplus
}
+2 -2
View File
@@ -1231,7 +1231,7 @@ static struct utun_config* parse_config_internal(FILE *fp, const char *filename)
if (strcmp(key, "db_path") == 0) {
assign_string(cfg->global.db_path, sizeof(cfg->global.db_path), value);
} else if (parse_chatserver(key, value, &cfg->global, filename, line_num) < 0 &&
chat_setting_set(key, value) < 0) {
chat_setting_set_state(&cfg->global.chat_settings, key, value) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown chatserver option '%s'. Valid: db_path, join_password, allowed_groups, storage_total_size, storage_unit_size, storage_autoload, opus_codec_preset, compressor_enabled, compressor_max_gain_db, compressor_rise_rate, media_download_max_peers", filename, line_num, key);
}
break;
@@ -1391,7 +1391,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
break;
case SECTION_NETWORK: if (cur_network) parse_network(key, value, cur_network, filename, line_num); break;
case SECTION_NTP: if (strcmp(key, "enabled") == 0) cfg->global.ntp_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; else if (strcmp(key, "server") == 0) { struct CFG_NTP_SERVER *ns = u_calloc(1, sizeof(struct CFG_NTP_SERVER)); if (ns) { strncpy(ns->name, value, sizeof(ns->name) - 1); ns->name[sizeof(ns->name) - 1] = '\0'; ns->next = cfg->global.ntp_servers; cfg->global.ntp_servers = ns; cfg->global.ntp_server_count++; } } else if (strcmp(key, "interval") == 0) { cfg->global.ntp_resync_interval = atoi(value); if (cfg->global.ntp_resync_interval < 60) cfg->global.ntp_resync_interval = 60; } else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown ntp option '%s'", filename, line_num, key); break;
case SECTION_CHATSERVER: cfg->global.chatserver_enabled = 1; cfg->global.db_sync_enabled = 1; if (strcmp(key, "db_path") == 0) assign_string(cfg->global.db_path, sizeof(cfg->global.db_path), value); else if (parse_chatserver(key, value, &cfg->global, filename, line_num) < 0 && chat_setting_set(key, value) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown chatserver option '%s'. Valid: db_path, join_password, allowed_groups, storage_total_size, storage_unit_size, storage_autoload, opus_codec_preset, compressor_enabled, compressor_max_gain_db, compressor_rise_rate, media_download_max_peers", filename, line_num, key); } break;
case SECTION_CHATSERVER: cfg->global.chatserver_enabled = 1; cfg->global.db_sync_enabled = 1; if (strcmp(key, "db_path") == 0) assign_string(cfg->global.db_path, sizeof(cfg->global.db_path), value); else if (parse_chatserver(key, value, &cfg->global, filename, line_num) < 0 && chat_setting_set_state(&cfg->global.chat_settings, key, value) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown chatserver option '%s'. Valid: db_path, join_password, allowed_groups, storage_total_size, storage_unit_size, storage_autoload, opus_codec_preset, compressor_enabled, compressor_max_gain_db, compressor_rise_rate, media_download_max_peers", filename, line_num, key); } break;
case SECTION_GUI: break;
case SECTION_REALITY: parse_reality(key, value, &cfg->global.reality, filename, line_num); break;
case SECTION_SOCKS: parse_socks(key, value, &cfg->global, filename, line_num); break;
+2
View File
@@ -7,6 +7,7 @@
#include "../lib/platform_compat.h"
#include "secure_channel.h"
#include "transport_layer/reality.h"
#include "chat/chat_setting.h"
#ifdef __cplusplus
extern "C" {
@@ -221,6 +222,7 @@ struct global_config {
uint64_t chatserver_storage_total_size; // bytes, общий лимит хранилища (0 = безлимит)
uint64_t chatserver_storage_unit_size; // bytes, лимит одного файла (0 = отключено)
char headless_control_bind[64]; // ip:port для headless chat CLI (пусто = отключено)
struct chat_setting_state chat_settings; // chat-настройки из [chatserver] (per-instance)
// Reality-камуфляж ([reality] section)
struct reality_config reality;
+785
View File
@@ -0,0 +1,785 @@
/*
* dm_core.c — прямой p2p чат между двумя пользователями (DM)
*
* Отдельная подсистема (не канал): беседы хранятся в dm_conversations,
* сообщения в dm_messages, синхронизация идёт напрямую через etcp_router
* (сервис 0x34), при недоступности пира — через dm_mailbox (0x35).
*
* Два независимых направленных потока (я→пир, пир→я) с монотонным seq.
* Каноническое тело сообщения — см. dm_core.h (общее с dm_mailbox).
*/
#include "dm_core.h"
#include "dm_crypto.h"
#include "dm_mailbox.h"
#include "../chat/chat_core.h"
#include "../chat/chat_event.h"
#include "../utun_instance.h"
#include "../ntp_time.h"
#include "../routing_layer/etcp_router.h"
#include "../routing_layer/topo_node_sqlite.h"
#include "../routing_layer/topo_group.h"
#include "../transport_layer/etcp_api.h"
#include "../transport_layer/etcp.h"
#include "../transport_layer/secure_channel.h"
#include "../../lib/mem.h"
#include "../../lib/ll_queue.h"
#include "../../lib/debug_config.h"
#include "../../lib/platform_compat.h"
#include <string.h>
#include <stdlib.h>
#include <sqlite3.h>
#define DM_ID "dm_core"
#define DM_DATA_MAX 1024 /* макс размер открытого текста сообщения (v1 — текст) */
/* Подкоманды протокола DM (первый байт payload после ROUTER_SVC-заголовка). */
#define DM_SUBCMD_MSG 0x01 /* [msg] — сообщение */
#define DM_SUBCMD_ACK 0x02 /* [conv_id:8][seq:8] — подтверждение приёма */
#define DM_SUBCMD_HELLO 0x03 /* [conv_id:8][last_out_seq:8][last_in_seq:8] — catch-up при connect */
/* Per-instance состояние модуля. */
struct dm_state {
struct UTUN_INSTANCE* inst;
sqlite3* db;
int initialized;
};
/* ── вспомогательные ── */
static struct dm_state* dm_of(struct UTUN_INSTANCE* inst) {
return inst ? inst->dm : NULL;
}
/* Выполнить SQL без результата. Возвращает 0/ошибку. */
static int dm_exec(struct dm_state* dm, const char* sql) {
char* err = NULL;
if (sqlite3_exec(dm->db, sql, NULL, NULL, &err) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: sql failed: %s (%s)", DM_ID, sql, err ? err : "?");
if (err) sqlite3_free(err);
return -1;
}
return 0;
}
/* Создать таблицы бесед и сообщений (общие для всех узлов). */
static void dm_create_tables(struct dm_state* dm) {
dm_exec(dm, "CREATE TABLE IF NOT EXISTS dm_conversations ("
" conv_id TEXT PRIMARY KEY,"
" peer_node_id INTEGER NOT NULL,"
" peer_x25519 BLOB NOT NULL,"
" peer_ed25519 BLOB NOT NULL,"
" peer_name TEXT DEFAULT '',"
" group_id INTEGER DEFAULT 0,"
" last_out_seq INTEGER DEFAULT 0,"
" last_in_seq INTEGER DEFAULT 0,"
" created_at INTEGER DEFAULT 0,"
" last_ts INTEGER DEFAULT 0)");
dm_exec(dm, "CREATE TABLE IF NOT EXISTS dm_messages ("
" conv_id TEXT NOT NULL,"
" dir INTEGER NOT NULL," /* 0=входящее, 1=исходящее */
" seq INTEGER NOT NULL,"
" ts INTEGER NOT NULL,"
" author INTEGER NOT NULL,"
" ct TEXT DEFAULT '',"
" data BLOB," /* зашифрованное тело (ciphertext+tag) */
" sig BLOB," /* Ed25519 автора */
" PRIMARY KEY (conv_id, dir, seq))");
}
/* Прочитать оба pubkey узла (x25519 + ed25519) из таблицы nodes. 0=ок, <0=нет. */
static int dm_node_pubkeys(struct dm_state* dm, uint64_t node_id, uint8_t x25519[32], uint8_t ed25519[32]) {
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db,
"SELECT x25519_pubkey, ed25519_pubkey FROM nodes WHERE node_id=?",
-1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
int rc = -1;
if (sqlite3_step(st) == SQLITE_ROW) {
const uint8_t* x = (const uint8_t*)sqlite3_column_blob(st, 0);
const uint8_t* e = (const uint8_t*)sqlite3_column_blob(st, 1);
if (x && e && sqlite3_column_bytes(st, 0) >= 32 && sqlite3_column_bytes(st, 1) >= 32) {
memcpy(x25519, x, 32);
memcpy(ed25519, e, 32);
rc = 0;
}
}
sqlite3_finalize(st);
return rc;
}
/* Есть ли общая группа с пиром (anti-spam: принимаем DM только от знакомых из общих каналов). */
static int dm_share_group(struct dm_state* dm, uint64_t peer_id) {
uint64_t* ch_ids = NULL;
int ch_count = 0;
if (topo_node_sqlite_get_member_channels(dm->db, dm->inst->node_id, &ch_ids, &ch_count) != 0)
return 0;
int shared = 0;
for (int i = 0; i < ch_count; i++) {
char ch[64]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)ch_ids[i]);
if (topo_node_sqlite_member_in_channel(dm->db, ch, peer_id)) { shared = 1; break; }
}
if (ch_ids) u_free(ch_ids);
return shared;
}
/* Доступен ли пир для прямой доставки: живой ETCP-линк или присутствие в BGP-группе. */
static int dm_peer_reachable(struct dm_state* dm, uint64_t peer_id) {
struct ETCP_CONN* conn = instance_find_conn(dm->inst, peer_id);
if (conn && conn->links_up && conn->initialized) return 1;
if (dm->inst->topo_groups && dm->inst->topo_groups->group_list) {
struct ll_entry* e = dm->inst->topo_groups->group_list->head;
while (e) {
struct TOPO_GROUP* g = (struct TOPO_GROUP*)e;
if (g->group_type == TOPO_GROUP_TYPE_CHAT && topo_node_find_by_id(g, peer_id)) return 1;
e = e->next;
}
}
return 0;
}
/* Отправить payload подкомандой subcmd в dst через etcp_router (mode=0, своё E2E). */
static int dm_route_send(struct dm_state* dm, uint64_t group_id, uint64_t dst, uint8_t subcmd,
const uint8_t* body, size_t body_len) {
struct ll_entry* entry = queue_entry_new(0);
if (!entry) return -1;
entry->dgram = u_malloc(1 + 1 + body_len);
if (!entry->dgram) { queue_entry_free(entry); return -1; }
entry->dgram[0] = ETCP_RT_ID_DM;
entry->dgram[1] = subcmd;
if (body_len) memcpy(entry->dgram + 2, body, body_len);
entry->len = (uint16_t)(2 + body_len);
int rc = etcp_route_send(dm->inst, group_id, dst, entry, 1, 0);
if (rc != 0) { queue_dgram_free(entry); queue_entry_free(entry); return -1; }
return 0;
}
/* ── работа с беседой ── */
/* Загрузить беседу из БД в структуру. Возвращает 0/ошибку. */
struct dm_conv {
char conv_id[64];
uint64_t peer_node_id;
uint8_t peer_x25519[32];
uint8_t peer_ed25519[32];
char peer_name[64];
uint64_t group_id;
uint8_t content_key[DM_CONTENT_KEY_SIZE];
uint64_t last_out_seq;
uint64_t last_in_seq;
};
static int dm_conv_load(struct dm_state* dm, const char* conv_id, struct dm_conv* c) {
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db,
"SELECT peer_node_id, peer_x25519, peer_ed25519, COALESCE(peer_name,''),"
" COALESCE(group_id,0), last_out_seq, last_in_seq FROM dm_conversations WHERE conv_id=?",
-1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC);
int rc = -1;
if (sqlite3_step(st) == SQLITE_ROW) {
memset(c, 0, sizeof(*c));
snprintf(c->conv_id, sizeof(c->conv_id), "%s", conv_id);
c->peer_node_id = (uint64_t)sqlite3_column_int64(st, 0);
const uint8_t* x = (const uint8_t*)sqlite3_column_blob(st, 1);
const uint8_t* e = (const uint8_t*)sqlite3_column_blob(st, 2);
if (x && sqlite3_column_bytes(st, 1) >= 32) memcpy(c->peer_x25519, x, 32);
if (e && sqlite3_column_bytes(st, 2) >= 32) memcpy(c->peer_ed25519, e, 32);
const char* name = (const char*)sqlite3_column_text(st, 3);
if (name) snprintf(c->peer_name, sizeof(c->peer_name), "%s", name);
c->group_id = (uint64_t)sqlite3_column_int64(st, 4);
c->last_out_seq = (uint64_t)sqlite3_column_int64(st, 5);
c->last_in_seq = (uint64_t)sqlite3_column_int64(st, 6);
dm_derive_content_key(dm->inst->my_keys.private_key, c->peer_x25519, c->content_key);
rc = 0;
}
sqlite3_finalize(st);
return rc;
}
/* Вставить/обновить беседу в БД. Возвращает 0/ошибку. */
static int dm_conv_save(struct dm_state* dm, const struct dm_conv* c) {
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db,
"INSERT INTO dm_conversations(conv_id,peer_node_id,peer_x25519,peer_ed25519,"
" peer_name,group_id,last_out_seq,last_in_seq,created_at,last_ts)"
" VALUES(?,?,?,?,?,?,?,?,?,0)"
" ON CONFLICT(conv_id) DO UPDATE SET"
" peer_x25519=excluded.peer_x25519, peer_ed25519=excluded.peer_ed25519,"
" peer_name=excluded.peer_name, group_id=excluded.group_id,"
" last_out_seq=excluded.last_out_seq, last_in_seq=excluded.last_in_seq",
-1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(st, 1, c->conv_id, -1, SQLITE_STATIC);
sqlite3_bind_int64(st, 2, (sqlite3_int64)c->peer_node_id);
sqlite3_bind_blob(st, 3, c->peer_x25519, 32, SQLITE_STATIC);
sqlite3_bind_blob(st, 4, c->peer_ed25519, 32, SQLITE_STATIC);
sqlite3_bind_text(st, 5, c->peer_name, -1, SQLITE_STATIC);
sqlite3_bind_int64(st, 6, (sqlite3_int64)c->group_id);
sqlite3_bind_int64(st, 7, (sqlite3_int64)c->last_out_seq);
sqlite3_bind_int64(st, 8, (sqlite3_int64)c->last_in_seq);
sqlite3_bind_int64(st, 9, (sqlite3_int64)ntp_time_get_seconds(dm->inst));
int rc = sqlite3_step(st) == SQLITE_DONE ? 0 : -1;
sqlite3_finalize(st);
return rc;
}
/* ── сборка/разбор канонического тела ── */
/* Собрать каноническое тело сообщения (зашифровать + подписать).
* out — буфер (u_malloc), *out_len — длина. dir=1 для исходящего. */
static int dm_build_body(struct dm_state* dm, const struct dm_conv* c, uint64_t seq, uint64_t ts,
const char* ct, const uint8_t* data, uint32_t data_len,
uint8_t** out, size_t* out_len) {
if (!c || !ct || (!data && data_len) || data_len > DM_DATA_MAX) return -1;
uint8_t ct_len = (uint8_t)strnlen(ct, 255);
uint64_t conv_num = (uint64_t)strtoull(c->conv_id, NULL, 10);
uint8_t nonce[DM_NONCE_SIZE];
dm_build_nonce(conv_num, dm->inst->node_id, seq, nonce);
/* шифротекст = data_len + tag(16) */
uint8_t* enc = u_malloc(data_len + DM_TAG_SIZE);
if (!enc) return -1;
size_t enc_len = 0;
if (dm_encrypt(c->content_key, nonce, data, data_len, enc, &enc_len) != 0) {
u_free(enc);
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: encrypt failed conv=%s", DM_ID, c->conv_id);
return -1;
}
uint64_t author = dm->inst->node_id;
size_t body_len = DM_MSG_FIXED_HDR + ct_len + 2 + enc_len + DM_MSG_SIG_SIZE;
uint8_t* body = u_malloc(body_len);
if (!body) { u_free(enc); return -1; }
size_t off = 0;
memcpy(body + off, &conv_num, 8); off += 8;
memcpy(body + off, &seq, 8); off += 8;
memcpy(body + off, &ts, 8); off += 8;
memcpy(body + off, &author, 8); off += 8;
body[off++] = ct_len;
memcpy(body + off, ct, ct_len); off += ct_len;
uint16_t el = (uint16_t)enc_len;
memcpy(body + off, &el, 2); off += 2;
memcpy(body + off, enc, enc_len); off += enc_len;
/* подпись по всем полям до sig */
sc_ed25519_sign(dm->inst->my_ed25519_privkey, body, off, body + off);
u_free(enc);
*out = body;
*out_len = body_len;
return 0;
}
/* ── обработка входящего сообщения ── */
/* Обработать каноническое тело входящего сообщения (из live-доставки или mailbox):
* проверить подпись, расшифровать, dedup по seq, вставить, отослать ACK, событие GUI. */
static void dm_on_msg(struct dm_state* dm, const uint8_t* msg, size_t mlen) {
if (!msg || mlen < DM_MSG_FIXED_HDR + DM_MSG_SIG_SIZE) return;
uint64_t conv_num; memcpy(&conv_num, msg, 8);
uint64_t seq; memcpy(&seq, msg + 8, 8);
uint64_t ts; memcpy(&ts, msg + 16, 8);
uint64_t author; memcpy(&author, msg + 24, 8);
uint8_t ct_len = msg[32];
const uint8_t* ct = msg + 33;
uint16_t enc_len; memcpy(&enc_len, msg + 33 + ct_len, 2);
const uint8_t* enc = msg + 35 + ct_len;
const uint8_t* sig = msg + mlen - DM_MSG_SIG_SIZE;
if (author == dm->inst->node_id) return; /* своё (эхо) — игнор */
char conv_id[64];
snprintf(conv_id, sizeof(conv_id), "%llu", (unsigned long long)conv_num);
struct dm_conv c;
if (dm_conv_load(dm, conv_id, &c) != 0) {
/* первое сообщение: автоприём при общей группе + совпадение conv_id */
if (dm_derive_conv_id(dm->inst->node_id, author) != conv_num) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: conv_id mismatch from 0x%016llx", DM_ID, (unsigned long long)author);
return;
}
if (!dm_share_group(dm, author)) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: no shared group with 0x%016llx — reject DM", DM_ID, (unsigned long long)author);
return;
}
memset(&c, 0, sizeof(c));
snprintf(c.conv_id, sizeof(c.conv_id), "%s", conv_id);
c.peer_node_id = author;
if (dm_node_pubkeys(dm, author, c.peer_x25519, c.peer_ed25519) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: no pubkeys for 0x%016llx", DM_ID, (unsigned long long)author);
return;
}
dm_derive_content_key(dm->inst->my_keys.private_key, c.peer_x25519, c.content_key);
c.group_id = 0;
dm_conv_save(dm, &c);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: auto-created conversation conv=%s peer=0x%016llx",
DM_ID, conv_id, (unsigned long long)author);
}
/* проверка подписи автора по всем полям до sig */
if (sc_ed25519_verify(c.peer_ed25519, msg, mlen - DM_MSG_SIG_SIZE, sig) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: invalid signature conv=%s seq=%llu", DM_ID, conv_id, (unsigned long long)seq);
return;
}
/* dedup: вставляем только новое */
if (seq <= c.last_in_seq) {
/* уже есть — только подтвердить */
uint8_t ack[16]; memcpy(ack, &conv_num, 8); memcpy(ack + 8, &seq, 8);
dm_route_send(dm, c.group_id, author, DM_SUBCMD_ACK, ack, sizeof(ack));
return;
}
/* расшифровать (nonce детерминирован по conv_id+author+seq) */
uint8_t nonce[DM_NONCE_SIZE];
dm_build_nonce(conv_num, author, seq, nonce);
uint8_t* plain = u_malloc(enc_len > DM_TAG_SIZE ? enc_len - DM_TAG_SIZE : 0);
if (!plain) return;
size_t plain_len = 0;
if (dm_decrypt(c.content_key, nonce, enc, enc_len, plain, &plain_len) != 0) {
u_free(plain);
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: decrypt failed conv=%s seq=%llu", DM_ID, conv_id, (unsigned long long)seq);
return;
}
/* вставить в dm_messages (dir=0, входящее) */
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db,
"INSERT OR IGNORE INTO dm_messages(conv_id,dir,seq,ts,author,ct,data,sig)"
" VALUES(?,0,?,?,?,?,?,?)",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC);
sqlite3_bind_int64(st, 2, (sqlite3_int64)seq);
sqlite3_bind_int64(st, 3, (sqlite3_int64)ts);
sqlite3_bind_int64(st, 4, (sqlite3_int64)author);
sqlite3_bind_text(st, 5, (const char*)ct, ct_len, SQLITE_STATIC);
sqlite3_bind_blob(st, 6, enc, enc_len, SQLITE_STATIC);
sqlite3_bind_blob(st, 7, sig, DM_MSG_SIG_SIZE, SQLITE_STATIC);
sqlite3_step(st);
}
if (st) sqlite3_finalize(st);
c.last_in_seq = seq;
dm_conv_save(dm, &c);
/* подтвердить приём (нужно и для mailbox-доставки → storage удалит по ACK) */
uint8_t ack[16]; memcpy(ack, &conv_num, 8); memcpy(ack + 8, &seq, 8);
dm_route_send(dm, c.group_id, author, DM_SUBCMD_ACK, ack, sizeof(ack));
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: recv conv=%s seq=%llu author=0x%016llx ct=%.*s len=%zu",
DM_ID, conv_id, (unsigned long long)seq, (unsigned long long)author, ct_len, ct, plain_len);
/* событие GUI: [conv_id_len:1][conv_id][author_node_id:8] */
{
size_t cl = strlen(conv_id);
uint8_t evt[1 + 64 + 8];
evt[0] = (uint8_t)cl;
memcpy(evt + 1, conv_id, cl);
memcpy(evt + 1 + cl, &author, 8);
chat_event_post(dm->inst, CHAT_EVT_DM_MSG_RECEIVED, evt, 1 + (int)cl + 8);
}
u_free(plain);
}
/* ── обработка HELLO (catch-up) ── */
/* Пересобрать каноническое тело из сохранённых полей (для catch-up resend). */
static int dm_rebuild_body(uint64_t conv_num, uint64_t seq, uint64_t ts, uint64_t author,
const char* ct, const uint8_t* enc, int enc_len, const uint8_t sig[64],
uint8_t** out, size_t* out_len) {
uint8_t ctl = (uint8_t)strnlen(ct, 255);
size_t body_len = DM_MSG_FIXED_HDR + ctl + 2 + (size_t)enc_len + DM_MSG_SIG_SIZE;
uint8_t* body = u_malloc(body_len);
if (!body) return -1;
size_t off = 0;
memcpy(body + off, &conv_num, 8); off += 8;
memcpy(body + off, &seq, 8); off += 8;
memcpy(body + off, &ts, 8); off += 8;
memcpy(body + off, &author, 8); off += 8;
body[off++] = ctl;
memcpy(body + off, ct, ctl); off += ctl;
uint16_t el = (uint16_t)enc_len;
memcpy(body + off, &el, 2); off += 2;
memcpy(body + off, enc, enc_len); off += enc_len;
memcpy(body + off, sig, DM_MSG_SIG_SIZE);
*out = body;
*out_len = body_len;
return 0;
}
/* На connect обе стороны шлют HELLO со своими счётчиками; получив HELLO, досылаем
* пиру недостающие ему наши сообщения (его last_in_seq+1 .. наш last_out_seq). */
static void dm_on_hello(struct dm_state* dm, uint64_t conv_num, uint64_t from_peer, uint64_t peer_out_seq, uint64_t peer_in_seq) {
char conv_id[64];
snprintf(conv_id, sizeof(conv_id), "%llu", (unsigned long long)conv_num);
struct dm_conv c;
if (dm_conv_load(dm, conv_id, &c) != 0) return;
if (peer_in_seq < c.last_out_seq) {
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db,
"SELECT seq, ts, author, ct, data, sig FROM dm_messages"
" WHERE conv_id=? AND dir=1 AND seq>? ORDER BY seq",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC);
sqlite3_bind_int64(st, 2, (sqlite3_int64)peer_in_seq);
while (sqlite3_step(st) == SQLITE_ROW) {
uint64_t seq = (uint64_t)sqlite3_column_int64(st, 0);
uint64_t ts = (uint64_t)sqlite3_column_int64(st, 1);
uint64_t author = (uint64_t)sqlite3_column_int64(st, 2);
const char* ct = (const char*)sqlite3_column_text(st, 3);
const uint8_t* enc = (const uint8_t*)sqlite3_column_blob(st, 4);
int enc_len = sqlite3_column_bytes(st, 4);
const uint8_t* sig = (const uint8_t*)sqlite3_column_blob(st, 5);
uint8_t* body = NULL; size_t body_len = 0;
if (dm_rebuild_body(conv_num, seq, ts, author, ct, enc, enc_len, sig, &body, &body_len) == 0) {
dm_route_send(dm, c.group_id, from_peer, DM_SUBCMD_MSG, body, body_len);
u_free(body);
}
}
}
if (st) sqlite3_finalize(st);
}
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: hello from 0x%016llx conv=%s out=%llu in=%llu",
DM_ID, (unsigned long long)from_peer, conv_id,
(unsigned long long)peer_out_seq, (unsigned long long)peer_in_seq);
}
/* ── обработка ACK ── */
/* Получено подтверждение доставки от пира — логируем (надёжность гарантирует etcp_router). */
static void dm_on_ack(uint64_t conv_num, uint64_t seq) {
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: ack conv=%llu seq=%llu", DM_ID,
(unsigned long long)conv_num, (unsigned long long)seq);
}
/* ── etcp_router recv (0x34) ── */
static void dm_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (!entry || !conn || !entry->dgram || entry->len < ROUTER_SVC_PAYLOAD_OFF + 1) {
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); }
return;
}
struct dm_state* dm = dm_of(conn->instance);
if (!dm || !dm->initialized) { queue_dgram_free(entry); queue_entry_free(entry); return; }
const uint8_t* d = entry->dgram;
uint64_t from_node; memcpy(&from_node, d + ROUTER_SVC_SRC_OFF, 8);
const uint8_t* p = d + ROUTER_SVC_PAYLOAD_OFF;
size_t plen = entry->len - ROUTER_SVC_PAYLOAD_OFF;
uint8_t subcmd = p[0];
if (subcmd == DM_SUBCMD_MSG) {
size_t mlen = 0;
if (dm_msg_len(p + 1, plen - 1, &mlen))
dm_on_msg(dm, p + 1, mlen);
} else if (subcmd == DM_SUBCMD_ACK) {
if (plen >= 1 + 16) {
uint64_t conv_num; memcpy(&conv_num, p + 1, 8);
uint64_t seq; memcpy(&seq, p + 9, 8);
dm_on_ack(conv_num, seq);
}
} else if (subcmd == DM_SUBCMD_HELLO) {
if (plen >= 1 + 24) {
uint64_t conv_num; memcpy(&conv_num, p + 1, 8);
uint64_t out_seq; memcpy(&out_seq, p + 9, 8);
uint64_t in_seq; memcpy(&in_seq, p + 17, 8);
dm_on_hello(dm, conv_num, from_node, out_seq, in_seq);
}
} else {
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: unknown subcmd=%02x", DM_ID, subcmd);
}
queue_dgram_free(entry); queue_entry_free(entry);
}
/* ── connection status ── */
/* При поднятии соединения с пиром: HELLO (catch-up) + вытянуть mailbox. */
static void dm_on_conn_status(struct ETCP_CONN* conn, int status, void* arg) {
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
struct dm_state* dm = dm_of(inst);
if (!conn || status != ETCP_CONN_STATUS_UP || !dm) return;
uint64_t peer = conn->peer_node_id;
if (!peer) return;
/* На подъём ЛЮБОГО соединения: HELLO для беседы с подключившимся пиром +
* PULL mailbox для бесед, чей пир не подключён напрямую (offline-доставка). */
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db,
"SELECT conv_id, peer_node_id, last_out_seq, last_in_seq FROM dm_conversations",
-1, &st, NULL) != SQLITE_OK) return;
while (sqlite3_step(st) == SQLITE_ROW) {
const char* conv_id = (const char*)sqlite3_column_text(st, 0);
uint64_t conv_peer = (uint64_t)sqlite3_column_int64(st, 1);
uint64_t out_seq = (uint64_t)sqlite3_column_int64(st, 2);
uint64_t in_seq = (uint64_t)sqlite3_column_int64(st, 3);
uint64_t conv_num = strtoull(conv_id, NULL, 10);
if (conv_peer == peer) {
uint8_t hello[24];
memcpy(hello, &conv_num, 8);
memcpy(hello + 8, &out_seq, 8);
memcpy(hello + 16, &in_seq, 8);
/* group_id берём из строки беседы — для простоты HELLO по 0, маршрутизация глобальная */
dm_route_send(dm, 0, peer, DM_SUBCMD_HELLO, hello, sizeof(hello));
}
/* пир не подключён напрямую — тянем mailbox со storage-узлов */
struct ETCP_CONN* pc = instance_find_conn(inst, conv_peer);
if (!pc || !pc->links_up || !pc->initialized) {
dm_mailbox_pull(inst, conv_peer, in_seq);
}
}
sqlite3_finalize(st);
}
/* ── публичный API ── */
/* Начать беседу с пользователем. Создаёт запись если её нет, выводит ключи,
* обеспечивает прямое соединение (если есть общая группа). */
int dm_start(struct UTUN_INSTANCE* inst, uint64_t peer_node_id,
const uint8_t peer_x25519[32], const uint8_t peer_ed25519[32],
const char* peer_name, const char* source_ch_id) {
if (!inst || !peer_node_id || !peer_x25519 || !peer_ed25519) return -1;
if (peer_node_id == inst->node_id) return -1;
struct dm_state* dm = dm_of(inst);
if (!dm || !dm->initialized) return -1;
uint64_t conv_num = dm_derive_conv_id(inst->node_id, peer_node_id);
char conv_id[64];
snprintf(conv_id, sizeof(conv_id), "%llu", (unsigned long long)conv_num);
struct dm_conv c;
if (dm_conv_load(dm, conv_id, &c) == 0) {
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: conversation already exists conv=%s", DM_ID, conv_id);
return 0;
}
memset(&c, 0, sizeof(c));
snprintf(c.conv_id, sizeof(c.conv_id), "%s", conv_id);
c.peer_node_id = peer_node_id;
memcpy(c.peer_x25519, peer_x25519, 32);
memcpy(c.peer_ed25519, peer_ed25519, 32);
if (peer_name) snprintf(c.peer_name, sizeof(c.peer_name), "%s", peer_name);
c.group_id = source_ch_id ? strtoull(source_ch_id, NULL, 10) : 0;
dm_derive_content_key(inst->my_keys.private_key, c.peer_x25519, c.content_key);
if (dm_conv_save(dm, &c) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: conv_save failed conv=%s", DM_ID, conv_id);
return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: started conv=%s peer=0x%016llx name=%s group=%llu",
DM_ID, conv_id, (unsigned long long)peer_node_id, c.peer_name,
(unsigned long long)c.group_id);
/* событие GUI */
{
size_t cl = strlen(conv_id);
uint8_t evt[1 + 64];
evt[0] = (uint8_t)cl;
memcpy(evt + 1, conv_id, cl);
chat_event_post(inst, CHAT_EVT_DM_CONV_UPDATED, evt, 1 + (int)cl);
}
return 0;
}
/* Отправить сообщение в беседу: зашифровать, подписать, сохранить, доставить. */
int dm_send(struct UTUN_INSTANCE* inst, const char* conv_id, const char* content_type,
const uint8_t* data, uint32_t data_len) {
if (!inst || !conv_id || !content_type || (!data && data_len)) return -1;
struct dm_state* dm = dm_of(inst);
if (!dm || !dm->initialized) return -1;
struct dm_conv c;
if (dm_conv_load(dm, conv_id, &c) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: no conversation %s", DM_ID, conv_id);
return -1;
}
uint64_t seq = c.last_out_seq + 1;
uint64_t ts = (uint64_t)(ntp_time_get_us(inst) / 1000);
uint8_t* body = NULL;
size_t body_len = 0;
if (dm_build_body(dm, &c, seq, ts, content_type, data, data_len, &body, &body_len) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: build body failed", DM_ID);
return -1;
}
/* сохранить локально (dir=1, исходящее) */
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db,
"INSERT INTO dm_messages(conv_id,dir,seq,ts,author,ct,data,sig)"
" VALUES(?,1,?,?,?,?,?,?)",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC);
sqlite3_bind_int64(st, 2, (sqlite3_int64)seq);
sqlite3_bind_int64(st, 3, (sqlite3_int64)ts);
sqlite3_bind_int64(st, 4, (sqlite3_int64)inst->node_id);
sqlite3_bind_text(st, 5, content_type, -1, SQLITE_STATIC);
/* data/sig уже внутри body: data_enc по смещению 35+ct_len, sig в конце */
uint8_t ctl = (uint8_t)strnlen(content_type, 255);
const uint8_t* enc = body + 35 + ctl;
uint16_t enc_len; memcpy(&enc_len, body + 33 + ctl, 2);
sqlite3_bind_blob(st, 6, enc, enc_len, SQLITE_STATIC);
sqlite3_bind_blob(st, 7, body + body_len - DM_MSG_SIG_SIZE, DM_MSG_SIG_SIZE, SQLITE_STATIC);
sqlite3_step(st);
}
if (st) sqlite3_finalize(st);
c.last_out_seq = seq;
dm_conv_save(dm, &c);
/* доставка: доступен → прямой send, иначе → mailbox */
if (dm_peer_reachable(dm, c.peer_node_id)) {
dm_route_send(dm, c.group_id, c.peer_node_id, DM_SUBCMD_MSG, body, body_len);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: sent direct conv=%s seq=%llu → 0x%016llx",
DM_ID, conv_id, (unsigned long long)seq, (unsigned long long)c.peer_node_id);
} else {
dm_mailbox_put(inst, c.peer_node_id, inst->node_id, body, body_len);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: peer offline conv=%s seq=%llu → mailbox",
DM_ID, conv_id, (unsigned long long)seq);
}
u_free(body);
return 0;
}
/* Вставка сообщения, полученного через mailbox (вызывается dm_mailbox по PULL_RESP).
* Обрабатывается тем же путём, что и live-сообщение (verify/decrypt/dedup/insert). */
void dm_core_inject_message(const uint8_t* msg, size_t len, void* arg) {
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
struct dm_state* dm = dm_of(inst);
if (!dm) return;
dm_on_msg(dm, msg, len);
}
/* ── жизненный цикл ── */
int dm_core_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
if (inst->dm && inst->dm->initialized) return 0;
struct dm_state* dm = u_calloc(1, sizeof(*dm));
if (!dm) return -1;
inst->dm = dm;
dm->inst = inst;
dm->db = chat_core_get_db(inst);
if (!dm->db) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: no chat db", DM_ID); u_free(dm); inst->dm = NULL; return -1; }
dm_create_tables(dm);
if (etcp_router_bind(inst, ETCP_RT_ID_DM, dm_recv_cb) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: etcp_router_bind failed", DM_ID);
u_free(dm); inst->dm = NULL; return -1;
}
etcp_add_conn_status_cbk(inst, dm_on_conn_status, inst);
dm_mailbox_set_deliver_cb(inst, dm_core_inject_message, inst);
dm->initialized = 1;
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: initialized", DM_ID);
return 0;
}
void dm_core_destroy(struct UTUN_INSTANCE* inst) {
if (!inst || !inst->dm || !inst->dm->initialized) return;
etcp_remove_conn_status_cbk(inst, dm_on_conn_status, inst);
etcp_router_unbind(inst, ETCP_RT_ID_DM);
u_free(inst->dm);
inst->dm = NULL;
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: destroyed", DM_ID);
}
/* ── JSON для GUI/headless ── */
/* Список бесед в JSON. */
int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len) {
struct dm_state* dm = dm_of(inst);
if (!buf || !out_len || !dm || !dm->db) return -1;
int off = 0;
off += snprintf(buf + off, buf_size - (size_t)off, "[");
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db,
"SELECT conv_id, peer_node_id, COALESCE(peer_name,''), last_ts, last_out_seq, last_in_seq"
" FROM dm_conversations ORDER BY last_ts DESC",
-1, &st, NULL) == SQLITE_OK) {
int first = 1;
while (sqlite3_step(st) == SQLITE_ROW) {
const char* conv_id = (const char*)sqlite3_column_text(st, 0);
uint64_t peer = (uint64_t)sqlite3_column_int64(st, 1);
const char* name = (const char*)sqlite3_column_text(st, 2);
uint64_t last_ts = (uint64_t)sqlite3_column_int64(st, 3);
uint64_t out_seq = (uint64_t)sqlite3_column_int64(st, 4);
uint64_t in_seq = (uint64_t)sqlite3_column_int64(st, 5);
off += snprintf(buf + off, buf_size - (size_t)off,
"%s{\"conv_id\":\"%s\",\"peer_node_id\":%llu,\"peer_name\":\"%s\","
"\"last_ts\":%llu,\"out_seq\":%llu,\"in_seq\":%llu}",
first ? "" : ",", conv_id, (unsigned long long)peer,
name ? name : "", (unsigned long long)last_ts,
(unsigned long long)out_seq, (unsigned long long)in_seq);
first = 0;
}
}
if (st) sqlite3_finalize(st);
off += snprintf(buf + off, buf_size - (size_t)off, "]");
*out_len = (size_t)off;
return 0;
}
/* Сообщения беседы (расшифрованные) в JSON. */
int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int count, int offset,
char* buf, size_t buf_size, size_t* out_len) {
struct dm_state* dm = dm_of(inst);
if (!conv_id || !buf || !out_len || !dm || !dm->db) return -1;
struct dm_conv c;
if (dm_conv_load(dm, conv_id, &c) != 0) return -1;
char sql[256];
if (count > 0)
snprintf(sql, sizeof(sql),
"SELECT dir,seq,ts,author,ct,data FROM dm_messages WHERE conv_id=? ORDER BY seq LIMIT %d OFFSET %d",
count, offset);
else
snprintf(sql, sizeof(sql),
"SELECT dir,seq,ts,author,ct,data FROM dm_messages WHERE conv_id=? ORDER BY seq");
int off = 0;
off += snprintf(buf + off, buf_size - (size_t)off, "[");
uint64_t conv_num = (uint64_t)strtoull(conv_id, NULL, 10);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC);
int first = 1;
while (sqlite3_step(st) == SQLITE_ROW) {
int dir = sqlite3_column_int(st, 0);
uint64_t seq = (uint64_t)sqlite3_column_int64(st, 1);
uint64_t ts = (uint64_t)sqlite3_column_int64(st, 2);
uint64_t author = (uint64_t)sqlite3_column_int64(st, 3);
const char* ct = (const char*)sqlite3_column_text(st, 4);
const uint8_t* enc = (const uint8_t*)sqlite3_column_blob(st, 5);
int enc_len = sqlite3_column_bytes(st, 5);
/* расшифровать (nonce детерминирован по conv_id+author+seq) */
char text[DM_DATA_MAX + 1];
text[0] = '\0';
if (enc && enc_len >= (int)DM_TAG_SIZE) {
uint8_t nonce[DM_NONCE_SIZE];
dm_build_nonce(conv_num, author, seq, nonce);
uint8_t* plain = u_malloc((size_t)enc_len);
size_t plen = 0;
if (dm_decrypt(c.content_key, nonce, enc, (size_t)enc_len, plain, &plen) == 0) {
if (plen >= sizeof(text)) plen = sizeof(text) - 1;
memcpy(text, plain, plen);
text[plen] = '\0';
}
u_free(plain);
}
off += snprintf(buf + off, buf_size - (size_t)off,
"%s{\"dir\":%d,\"seq\":%llu,\"ts\":%llu,\"author\":%llu,\"ct\":\"%s\",\"data\":\"%s\"}",
first ? "" : ",", dir, (unsigned long long)seq, (unsigned long long)ts,
(unsigned long long)author, ct ? ct : "", text);
first = 0;
}
}
if (st) sqlite3_finalize(st);
off += snprintf(buf + off, buf_size - (size_t)off, "]");
*out_len = (size_t)off;
return 0;
}
+76
View File
@@ -0,0 +1,76 @@
/*
* dm_core.h — прямой p2p чат между двумя пользователями (DM)
*
* Отдельная подсистема (НЕ канал): без TOPO_GROUP/member_sync/merkle.
* - conv_id и content_key детерминированно выводятся обеими сторонами
* из своих ключей и pubkey пира (см. dm_crypto.h) — без переговоров.
* - сообщения идут через etcp_router (прямое соединение или релей),
* при недоступности пира — через dm_mailbox (storage-узлы).
* - содержимое шифруется E2E (AES-256-CCM), запись подписывается Ed25519.
*
* Два независимых направленных потока (я→пир и пир→я) с монотонным seq.
*/
#ifndef DM_CORE_H
#define DM_CORE_H
#include <stdint.h>
#include <stddef.h>
#include <string.h>
struct UTUN_INSTANCE;
#ifdef __cplusplus
extern "C" {
#endif
/* ── Каноническое тело DM-сообщения (общий wire-формат dm_core и dm_mailbox) ──
* [conv_id:8][seq:8][ts:8][author:8][ct_len:1][ct:ct_len][data_len:2][data_enc:data_len][sig:64]
* data_enc — AES-256-CCM (шифротекст + tag), sig — Ed25519 автора по всем полям до sig. */
#define DM_MSG_FIXED_HDR 33 /* conv_id+seq+ts+author(32) + ct_len(1) — до ct */
#define DM_MSG_SIG_SIZE 64
/* Вычислить полную длину канонического тела из его заголовка.
* Возвращает 1 и *out_len, либо 0 если буфер мал/повреждён. */
static inline int dm_msg_len(const uint8_t* msg, size_t avail, size_t* out_len) {
if (!msg || avail < DM_MSG_FIXED_HDR) return 0;
uint8_t ct_len = msg[32];
if (avail < (size_t)DM_MSG_FIXED_HDR + ct_len + 2) return 0;
uint16_t data_len;
memcpy(&data_len, msg + DM_MSG_FIXED_HDR + ct_len, 2);
size_t total = (size_t)DM_MSG_FIXED_HDR + ct_len + 2 + data_len + DM_MSG_SIG_SIZE;
if (avail < total) return 0;
*out_len = total;
return 1;
}
/* Жизненный цикл: инициализация после chat_core_init, destroy перед chat_core_destroy. */
int dm_core_init(struct UTUN_INSTANCE* inst);
void dm_core_destroy(struct UTUN_INSTANCE* inst);
/* Начать беседу с пользователем (peer). source_ch_id — канал, где нашли target
* (общая группа; нужен для маршрутизации и подключения). Создаёт беседу если её
* ещё нет, выводит conv_id/ключи, пытается установить прямое соединение.
* Возвращает 0 при успехе, <0 при ошибке. */
int dm_start(struct UTUN_INSTANCE* inst, uint64_t peer_node_id,
const uint8_t peer_x25519[32], const uint8_t peer_ed25519[32],
const char* peer_name, const char* source_ch_id);
/* Отправить сообщение в беседу. Шифрует, подписывает, сохраняет локально и
* доставляет (прямое соединение при доступности пира, иначе mailbox).
* conv_id — строковое представление (как в dm_conversations.conv_id). */
int dm_send(struct UTUN_INSTANCE* inst, const char* conv_id, const char* content_type,
const uint8_t* data, uint32_t data_len);
/* Список бесед в JSON: [{conv_id,peer_node_id,peer_name,last_ts,out_seq,in_seq}] */
int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len);
/* Сообщения беседы в JSON (расшифрованные): [{dir,seq,ts,author,ct,data}]
* count=0 → все, offset=0 → с начала. */
int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int count, int offset,
char* buf, size_t buf_size, size_t* out_len);
#ifdef __cplusplus
}
#endif
#endif /* DM_CORE_H */
+128
View File
@@ -0,0 +1,128 @@
/*
* dm_crypto.c — криптография прямого чата (DM)
*/
#include "dm_crypto.h"
#include "../../lib/debug_config.h"
#define OPENSSL_API_COMPAT 0x10100000L
#include <openssl/evp.h>
#include <openssl/sha.h>
#include <string.h>
#include <stdio.h>
#define DM_ID "dm_crypto"
uint64_t dm_derive_conv_id(uint64_t a, uint64_t b) {
uint64_t lo = a < b ? a : b;
uint64_t hi = a < b ? b : a;
SHA256_CTX ctx;
uint8_t hash[32];
SHA256_Init(&ctx);
SHA256_Update(&ctx, "utun_dm_v1", 9);
SHA256_Update(&ctx, &lo, 8);
SHA256_Update(&ctx, &hi, 8);
SHA256_Final(hash, &ctx);
uint64_t id;
memcpy(&id, hash, 8);
id &= 0x7FFFFFFFFFFFFFFFULL;
if (id == 0) id = 1;
return id;
}
int dm_derive_content_key(const uint8_t my_priv[32], const uint8_t peer_pub[32],
uint8_t out[DM_CONTENT_KEY_SIZE]) {
if (!my_priv || !peer_pub || !out) return -1;
EVP_PKEY* my_pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, my_priv, 32);
if (!my_pkey) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: EVP_PKEY_new_raw_private_key failed", DM_ID); return -1; }
EVP_PKEY* peer_pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_X25519, NULL, peer_pub, 32);
if (!peer_pkey) { EVP_PKEY_free(my_pkey); DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: EVP_PKEY_new_raw_public_key failed", DM_ID); return -1; }
EVP_PKEY_CTX* dctx = EVP_PKEY_CTX_new(my_pkey, NULL);
uint8_t shared[32];
size_t shared_len = sizeof(shared);
int rc = -1;
if (!dctx) goto out;
if (EVP_PKEY_derive_init(dctx) <= 0) goto out;
if (EVP_PKEY_derive_set_peer(dctx, peer_pkey) <= 0) goto out;
if (EVP_PKEY_derive(dctx, shared, &shared_len) <= 0 || shared_len != sizeof(shared)) goto out;
SHA256_CTX sctx;
SHA256_Init(&sctx);
SHA256_Update(&sctx, shared, sizeof(shared));
SHA256_Update(&sctx, "utun_dm_content", 15);
SHA256_Final(out, &sctx);
rc = 0;
out:
if (dctx) EVP_PKEY_CTX_free(dctx);
EVP_PKEY_free(my_pkey);
EVP_PKEY_free(peer_pkey);
if (rc != 0) DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: X25519 derive failed", DM_ID);
return rc;
}
void dm_build_nonce(uint64_t conv_id, uint64_t author, uint64_t seq, uint8_t nonce[DM_NONCE_SIZE]) {
SHA256_CTX ctx;
uint8_t hash[32];
SHA256_Init(&ctx);
SHA256_Update(&ctx, &conv_id, 8);
SHA256_Update(&ctx, &author, 8);
SHA256_Update(&ctx, &seq, 8);
SHA256_Final(hash, &ctx);
memcpy(nonce, hash, DM_NONCE_SIZE);
}
static int dm_ccm_crypt(const uint8_t key[DM_CONTENT_KEY_SIZE], const uint8_t nonce[DM_NONCE_SIZE],
const uint8_t* in, size_t in_len, uint8_t* out, size_t* out_len,
int encrypt, const uint8_t* tag_in) {
if (!key || !nonce || !in || !out || !out_len) return -1;
EVP_CIPHER_CTX* ectx = EVP_CIPHER_CTX_new();
if (!ectx) return -1;
const EVP_CIPHER* cipher = EVP_aes_256_ccm();
int rc = -1;
if (encrypt) {
if (EVP_EncryptInit_ex(ectx, cipher, NULL, NULL, NULL) != 1
|| EVP_CIPHER_CTX_ctrl(ectx, EVP_CTRL_AEAD_SET_IVLEN, DM_NONCE_SIZE, NULL) != 1
|| EVP_CIPHER_CTX_ctrl(ectx, EVP_CTRL_AEAD_SET_TAG, DM_TAG_SIZE, NULL) != 1
|| EVP_EncryptInit_ex(ectx, NULL, NULL, key, nonce) != 1) goto done;
int outl;
if (EVP_EncryptUpdate(ectx, out, &outl, in, (int)in_len) != 1 || outl != (int)in_len) goto done;
int tmpl;
if (EVP_EncryptFinal_ex(ectx, out + outl, &tmpl) != 1) goto done;
if (EVP_CIPHER_CTX_ctrl(ectx, EVP_CTRL_AEAD_GET_TAG, DM_TAG_SIZE, out + in_len) != 1) goto done;
*out_len = in_len + DM_TAG_SIZE;
} else {
if (in_len < DM_TAG_SIZE) goto done;
size_t body = in_len - DM_TAG_SIZE;
if (EVP_DecryptInit_ex(ectx, cipher, NULL, NULL, NULL) != 1
|| EVP_CIPHER_CTX_ctrl(ectx, EVP_CTRL_AEAD_SET_IVLEN, DM_NONCE_SIZE, NULL) != 1
|| EVP_CIPHER_CTX_ctrl(ectx, EVP_CTRL_AEAD_SET_TAG, DM_TAG_SIZE, (void*)(tag_in ? tag_in : in + body)) != 1
|| EVP_DecryptInit_ex(ectx, NULL, NULL, key, nonce) != 1) goto done;
int outl;
if (EVP_DecryptUpdate(ectx, out, &outl, in, (int)body) != 1 || outl != (int)body) goto done;
int tmpl;
if (EVP_DecryptFinal_ex(ectx, out + outl, &tmpl) != 1) goto done;
*out_len = body;
}
rc = 0;
done:
EVP_CIPHER_CTX_free(ectx);
return rc;
}
int dm_encrypt(const uint8_t key[DM_CONTENT_KEY_SIZE], const uint8_t nonce[DM_NONCE_SIZE],
const uint8_t* plain, size_t plain_len, uint8_t* out, size_t* out_len) {
return dm_ccm_crypt(key, nonce, plain, plain_len, out, out_len, 1, NULL);
}
int dm_decrypt(const uint8_t key[DM_CONTENT_KEY_SIZE], const uint8_t nonce[DM_NONCE_SIZE],
const uint8_t* ct, size_t ct_len, uint8_t* out, size_t* out_len) {
return dm_ccm_crypt(key, nonce, ct, ct_len, out, out_len, 0, NULL);
}
+50
View File
@@ -0,0 +1,50 @@
/*
* dm_crypto.h — криптография прямого чата (DM)
*
* Детерминированная идентичность и E2E-шифрование:
* - conv_id / content_key выводятся обеими сторонами из своих ключей и
* pubkey пира (известен из общей группы) — без переговоров.
* - шифрование AES-256-CCM с детерминированным nonce из (dir, seq).
*/
#ifndef DM_CRYPTO_H
#define DM_CRYPTO_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define DM_CONTENT_KEY_SIZE 32
#define DM_NONCE_SIZE 13 /* AES-CCM требует ровно 13 байт */
#define DM_TAG_SIZE 16
/* conv_id = SHA256("utun_dm_v1" || min(a,b) || max(a,b))[..8] & 0x7FFF...,
* 63-битное число, одинаковое на обеих сторонах (одна ветка на пару). */
uint64_t dm_derive_conv_id(uint64_t a, uint64_t b);
/* content_key = SHA256(X25519(my_priv, peer_pub) || "utun_dm_content").
* Возвращает 0 при успехе, <0 при ошибке. */
int dm_derive_content_key(const uint8_t my_priv[32], const uint8_t peer_pub[32],
uint8_t out[DM_CONTENT_KEY_SIZE]);
/* Детерминированный nonce, уникальный на каждое сообщение:
* SHA256(conv_id || author || seq)[..13]. Одинаков на обеих сторонах,
* не пересекается между направлениями (author различается). */
void dm_build_nonce(uint64_t conv_id, uint64_t author, uint64_t seq, uint8_t nonce[DM_NONCE_SIZE]);
/* AES-256-CCM. ciphertext = ciphertext(plain_len) || tag(DM_TAG_SIZE).
* out должен вмещать plain_len + DM_TAG_SIZE. 0=ок, <0=ошибка. */
int dm_encrypt(const uint8_t key[DM_CONTENT_KEY_SIZE], const uint8_t nonce[DM_NONCE_SIZE],
const uint8_t* plain, size_t plain_len, uint8_t* out, size_t* out_len);
/* Обратная операция. ct_len = plain_len + DM_TAG_SIZE. 0=ок, <0=ошибка. */
int dm_decrypt(const uint8_t key[DM_CONTENT_KEY_SIZE], const uint8_t nonce[DM_NONCE_SIZE],
const uint8_t* ct, size_t ct_len, uint8_t* out, size_t* out_len);
#ifdef __cplusplus
}
#endif
#endif /* DM_CRYPTO_H */
+353
View File
@@ -0,0 +1,353 @@
/*
* dm_mailbox.c — offline-хранение DM-сообщений на storage-узлах
*
* Storage-узел хранит зашифрованные блобы в таблице dm_mail и отдаёт их
* получателю по PULL, удаляя после ACK. Отправитель кладёт блобы по PUT.
* Содержимое для storage-узла непрозрачно (E2E-шифрование + подпись автора).
*/
#include "dm_mailbox.h"
#include "dm_core.h"
#include "../chat/chat_core.h"
#include "../utun_instance.h"
#include "../ntp_time.h"
#include "../routing_layer/etcp_router.h"
#include "../routing_layer/topo_node_sqlite.h"
#include "../transport_layer/etcp_api.h"
#include "../transport_layer/etcp.h"
#include "../../lib/mem.h"
#include "../../lib/ll_queue.h"
#include "../../lib/debug_config.h"
#include "../../lib/json_flat.h"
#include "../../lib/platform_compat.h"
#include <string.h>
#include <stdlib.h>
#include <sqlite3.h>
#define MB_ID "dm_mailbox"
/* Подкоманды протокола mailbox (первый байт payload после ROUTER_SVC-заголовка). */
#define MB_SUBCMD_PUT 0x01 /* отправитель → storage: [recipient:8][sender:8][msg] */
#define MB_SUBCMD_PULL 0x02 /* получатель → storage: [recipient:8][sender:8][since_seq:8] */
#define MB_SUBCMD_PULL_RESP 0x03 /* storage → получатель: [recipient:8][count:2][msg]* */
#define MB_SUBCMD_ACK 0x04 /* получатель → storage: [recipient:8][sender:8][up_to_seq:8] */
#define MB_TTL_DAYS 7
#define MB_MAX_BATCH 32
/* Per-instance состояние модуля. */
struct dm_mb_state {
struct UTUN_INSTANCE* inst;
sqlite3* db;
int initialized;
dm_mail_deliver_fn deliver_fn;
void* deliver_arg;
};
static struct dm_mb_state* mb_of(struct UTUN_INSTANCE* inst) {
return inst ? inst->dm_mailbox : NULL;
}
void dm_mailbox_set_deliver_cb(struct UTUN_INSTANCE* inst, dm_mail_deliver_fn fn, void* arg) {
struct dm_mb_state* mb = mb_of(inst);
if (!mb) return;
mb->deliver_fn = fn;
mb->deliver_arg = arg;
}
/* Выполнить SQL без результата. Возвращает 0/ошибку. */
static int mb_exec(struct dm_mb_state* mb, const char* sql) {
char* err = NULL;
if (sqlite3_exec(mb->db, sql, NULL, NULL, &err) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: sql failed: %s (%s)", MB_ID, sql, err ? err : "?");
if (err) sqlite3_free(err);
return -1;
}
return 0;
}
/* Прочитать seq из канонического тела сообщения (смещение 8). */
static uint64_t mb_msg_seq(const uint8_t* msg) {
uint64_t seq; memcpy(&seq, msg + 8, 8); return seq;
}
/* Создать таблицу dm_mail (на любом узле; фактически используется storage-узлами). */
static void mb_create_tables(struct dm_mb_state* mb) {
mb_exec(mb, "CREATE TABLE IF NOT EXISTS dm_mail ("
" recipient INTEGER NOT NULL,"
" sender INTEGER NOT NULL,"
" seq INTEGER NOT NULL,"
" msg BLOB NOT NULL,"
" ttl INTEGER NOT NULL,"
" PRIMARY KEY (recipient, sender, seq))");
}
/* Отправить payload подкомандой subcmd в dst через etcp_router (mode=0, своё E2E). */
static int mb_route_send(struct dm_mb_state* mb, uint64_t group_id, uint64_t dst, uint8_t subcmd,
const uint8_t* body, size_t body_len) {
struct ll_entry* entry = queue_entry_new(0);
if (!entry) return -1;
entry->dgram = u_malloc(1 + 1 + body_len);
if (!entry->dgram) { queue_entry_free(entry); return -1; }
entry->dgram[0] = ETCP_RT_ID_DM_MAILBOX;
memcpy(entry->dgram + 1, &subcmd, 1);
if (body_len) memcpy(entry->dgram + 1 + 1, body, body_len);
entry->len = (uint16_t)(1 + 1 + body_len);
int rc = etcp_route_send(mb->inst, group_id, dst, entry, 1, 0);
if (rc != 0) { queue_dgram_free(entry); queue_entry_free(entry); return -1; }
return 0;
}
/* ── обработка входящих mailbox-пакетов (etcp_router 0x35) ── */
static void mb_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (!entry || !conn || !entry->dgram || entry->len < ROUTER_SVC_PAYLOAD_OFF + 1) {
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); }
return;
}
struct UTUN_INSTANCE* inst = conn->instance;
struct dm_mb_state* mb = mb_of(inst);
if (!inst || !mb || !mb->db) { queue_dgram_free(entry); queue_entry_free(entry); return; }
const uint8_t* d = entry->dgram;
uint64_t group_id; memcpy(&group_id, d + ROUTER_SVC_GROUP_OFF, 8);
const uint8_t* p = d + ROUTER_SVC_PAYLOAD_OFF;
size_t plen = entry->len - ROUTER_SVC_PAYLOAD_OFF;
uint8_t subcmd = p[0];
if (subcmd == MB_SUBCMD_PUT) {
/* [subcmd][recipient:8][sender:8][msg] — storage: сохранить блоб */
if (plen < 1 + 8 + 8 + 16) { queue_dgram_free(entry); queue_entry_free(entry); return; }
uint64_t recipient; memcpy(&recipient, p + 1, 8);
uint64_t sender; memcpy(&sender, p + 9, 8);
const uint8_t* msg = p + 17;
size_t mlen = plen - 17;
uint64_t seq = mb_msg_seq(msg);
int64_t ttl = (int64_t)ntp_time_get_seconds(inst) + MB_TTL_DAYS * 86400;
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(mb->db,
"INSERT OR IGNORE INTO dm_mail(recipient,sender,seq,msg,ttl) VALUES(?,?,?,?,?)",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)recipient);
sqlite3_bind_int64(st, 2, (sqlite3_int64)sender);
sqlite3_bind_int64(st, 3, (sqlite3_int64)seq);
sqlite3_bind_blob(st, 4, msg, (int)mlen, SQLITE_STATIC);
sqlite3_bind_int64(st, 5, ttl);
int rc = sqlite3_step(st);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: PUT recipient=0x%016llx sender=0x%016llx seq=%llu rc=%d",
MB_ID, (unsigned long long)recipient, (unsigned long long)sender,
(unsigned long long)seq, rc);
}
if (st) sqlite3_finalize(st);
} else if (subcmd == MB_SUBCMD_PULL) {
/* [subcmd][recipient:8][sender:8][since_seq:8] — storage: вернуть очередь */
if (plen < 1 + 8 + 8 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; }
uint64_t recipient; memcpy(&recipient, p + 1, 8);
uint64_t sender; memcpy(&sender, p + 9, 8);
uint64_t since_seq; memcpy(&since_seq, p + 17, 8);
/* прочитать блобы (до MB_MAX_BATCH) в буфер: [recipient:8][sender:8][count:2][msg]* */
size_t buf_cap = 18 + (size_t)MB_MAX_BATCH * 4096;
uint8_t* body = u_malloc(buf_cap);
if (!body) { queue_dgram_free(entry); queue_entry_free(entry); return; }
memcpy(body, &recipient, 8);
memcpy(body + 8, &sender, 8);
uint16_t count = 0;
size_t boff = 18;
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(mb->db,
"SELECT msg, length(msg) FROM dm_mail WHERE recipient=? AND sender=? AND seq>? ORDER BY seq LIMIT ?",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)recipient);
sqlite3_bind_int64(st, 2, (sqlite3_int64)sender);
sqlite3_bind_int64(st, 3, (sqlite3_int64)since_seq);
sqlite3_bind_int(st, 4, MB_MAX_BATCH);
while (sqlite3_step(st) == SQLITE_ROW) {
const uint8_t* msg = (const uint8_t*)sqlite3_column_blob(st, 0);
int mlen = sqlite3_column_int(st, 1);
if (!msg || mlen <= 0) continue;
if (boff + (size_t)mlen > buf_cap) break;
memcpy(body + boff, msg, (size_t)mlen);
boff += (size_t)mlen;
count++;
}
}
if (st) sqlite3_finalize(st);
memcpy(body + 16, &count, 2);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: PULL recipient=0x%016llx sender=0x%016llx since=%llu → %u msgs",
MB_ID, (unsigned long long)recipient, (unsigned long long)sender,
(unsigned long long)since_seq, count);
if (count > 0)
mb_route_send(mb, group_id, recipient, MB_SUBCMD_PULL_RESP, body, boff);
u_free(body);
} else if (subcmd == MB_SUBCMD_PULL_RESP) {
/* [subcmd][recipient:8][sender:8][count:2][msg]* — получатель: передать в dm_core + ACK storage */
if (plen < 1 + 8 + 8 + 2) { queue_dgram_free(entry); queue_entry_free(entry); return; }
uint64_t storage; memcpy(&storage, d + ROUTER_SVC_SRC_OFF, 8);
uint64_t recipient; memcpy(&recipient, p + 1, 8);
uint64_t sender; memcpy(&sender, p + 9, 8);
uint16_t count; memcpy(&count, p + 17, 2);
const uint8_t* m = p + 19;
size_t remaining = plen - 19;
uint64_t max_seq = 0;
for (int i = 0; i < count; i++) {
size_t mlen = 0;
if (!dm_msg_len(m, remaining, &mlen)) break;
uint64_t seq = mb_msg_seq(m);
if (seq > max_seq) max_seq = seq;
if (mb->deliver_fn) mb->deliver_fn(m, mlen, mb->deliver_arg);
m += mlen; remaining -= mlen;
}
/* подтвердить storage-узлу приём — удалить доставленное (recipient/sender/up_to_seq) */
if (count > 0 && max_seq > 0) {
uint8_t ack[24];
memcpy(ack, &recipient, 8);
memcpy(ack + 8, &sender, 8);
memcpy(ack + 16, &max_seq, 8);
mb_route_send(mb, group_id, storage, MB_SUBCMD_ACK, ack, sizeof(ack));
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: PULL_RESP recipient=0x%016llx sender=0x%016llx count=%u → ACK up_to=%llu",
MB_ID, (unsigned long long)recipient, (unsigned long long)sender, count,
(unsigned long long)max_seq);
}
} else if (subcmd == MB_SUBCMD_ACK) {
/* [subcmd][recipient:8][sender:8][up_to_seq:8] — storage: удалить доставленное */
if (plen < 1 + 8 + 8 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; }
uint64_t recipient; memcpy(&recipient, p + 1, 8);
uint64_t sender; memcpy(&sender, p + 9, 8);
uint64_t up_to_seq; memcpy(&up_to_seq, p + 17, 8);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(mb->db,
"DELETE FROM dm_mail WHERE recipient=? AND sender=? AND seq<=?",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)recipient);
sqlite3_bind_int64(st, 2, (sqlite3_int64)sender);
sqlite3_bind_int64(st, 3, (sqlite3_int64)up_to_seq);
sqlite3_step(st);
}
if (st) sqlite3_finalize(st);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: ACK recipient=0x%016llx sender=0x%016llx up_to=%llu",
MB_ID, (unsigned long long)recipient, (unsigned long long)sender,
(unsigned long long)up_to_seq);
} else {
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: unknown subcmd=%02x", MB_ID, subcmd);
}
queue_dgram_free(entry); queue_entry_free(entry);
}
/* ── Жизненный цикл ── */
int dm_mailbox_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
if (inst->dm_mailbox && inst->dm_mailbox->initialized) return 0;
struct dm_mb_state* mb = u_calloc(1, sizeof(*mb));
if (!mb) return -1;
inst->dm_mailbox = mb;
mb->inst = inst;
mb->db = chat_core_get_db(inst);
if (!mb->db) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: no chat db", MB_ID); u_free(mb); inst->dm_mailbox = NULL; return -1; }
mb_create_tables(mb);
if (etcp_router_bind(inst, ETCP_RT_ID_DM_MAILBOX, mb_recv_cb) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: etcp_router_bind failed", MB_ID);
u_free(mb); inst->dm_mailbox = NULL; return -1;
}
mb->initialized = 1;
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: initialized", MB_ID);
return 0;
}
void dm_mailbox_destroy(struct UTUN_INSTANCE* inst) {
struct dm_mb_state* mb = mb_of(inst);
if (!inst || !mb || !mb->initialized) return;
etcp_router_unbind(inst, ETCP_RT_ID_DM_MAILBOX);
u_free(mb);
inst->dm_mailbox = NULL;
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: destroyed", MB_ID);
}
/* ── Публичные операции ── */
/* Собрать storage-узлы (dm_storage=yes) из общих групп без дублей. */
int dm_mailbox_get_storage_nodes(struct UTUN_INSTANCE* inst, uint64_t* out, int max) {
struct dm_mb_state* mb = mb_of(inst);
if (!inst || !out || max <= 0 || !mb || !mb->db) return 0;
uint64_t my_id = inst->node_id;
uint64_t* ch_ids = NULL;
int ch_count = 0;
if (topo_node_sqlite_get_member_channels(mb->db, my_id, &ch_ids, &ch_count) != 0) return 0;
int found = 0;
char buf[256];
for (int c = 0; c < ch_count && found < max; c++) {
char ch[64]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)ch_ids[c]);
char tbl[80]; snprintf(tbl, sizeof(tbl), "peers_%s", ch);
char sql[256]; snprintf(sql, sizeof(sql),
"SELECT node_id, COALESCE(adm_tags,'') FROM \"%s\"", tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(mb->db, sql, -1, &st, NULL) != SQLITE_OK) continue;
while (sqlite3_step(st) == SQLITE_ROW && found < max) {
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
if (nid == my_id) continue;
const char* tags = (const char*)sqlite3_column_text(st, 1);
if (tags && json_flat_get(tags, "dm_storage", buf, sizeof(buf)) == 0
&& strcmp(buf, "yes") == 0) {
int dup = 0;
for (int i = 0; i < found; i++) if (out[i] == nid) { dup = 1; break; }
if (!dup) out[found++] = nid;
}
}
sqlite3_finalize(st);
}
if (ch_ids) u_free(ch_ids);
return found;
}
/* Отправитель: разослать зашифрованное сообщение на все storage-узлы. */
int dm_mailbox_put(struct UTUN_INSTANCE* inst, uint64_t recipient, uint64_t sender,
const uint8_t* msg, size_t msg_len) {
struct dm_mb_state* mb = mb_of(inst);
if (!inst || !mb || !msg || msg_len == 0) return -1;
uint64_t nodes[8];
int n = dm_mailbox_get_storage_nodes(inst, nodes, 8);
if (n == 0) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: no storage nodes for offline delivery", MB_ID);
return -1;
}
/* body = [recipient:8][sender:8][msg] */
size_t body_len = 8 + 8 + msg_len;
uint8_t* body = u_malloc(body_len);
if (!body) return -1;
memcpy(body, &recipient, 8);
memcpy(body + 8, &sender, 8);
memcpy(body + 16, msg, msg_len);
int sent = 0;
for (int i = 0; i < n; i++) {
if (mb_route_send(mb, 0, nodes[i], MB_SUBCMD_PUT, body, body_len) == 0) sent++;
}
u_free(body);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: put → %d/%d storage nodes", MB_ID, sent, n);
return sent > 0 ? 0 : -1;
}
/* Получатель: запросить почту от отправителя со всех storage-узлов. */
void dm_mailbox_pull(struct UTUN_INSTANCE* inst, uint64_t peer_node_id, uint64_t since_seq) {
struct dm_mb_state* mb = mb_of(inst);
if (!inst || !mb) return;
uint64_t nodes[8];
int n = dm_mailbox_get_storage_nodes(inst, nodes, 8);
if (n == 0) return;
uint8_t body[24];
memcpy(body, &inst->node_id, 8);
memcpy(body + 8, &peer_node_id, 8);
memcpy(body + 16, &since_seq, 8);
for (int i = 0; i < n; i++)
mb_route_send(mb, 0, nodes[i], MB_SUBCMD_PULL, body, sizeof(body));
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: pull from %d storage nodes (peer=0x%016llx since=%llu)",
MB_ID, n, (unsigned long long)peer_node_id, (unsigned long long)since_seq);
}
+49
View File
@@ -0,0 +1,49 @@
/*
* dm_mailbox.h — offline-хранение DM-сообщений на storage-узлах
*
* Когда пир недоступен (нет прямой связи), отправитель кладёт зашифрованное
* сообщение на storage-узлы (мемберы общих групп с флагом dm_storage=yes).
* Получатель при выходе в online вытягивает накопившуюся почту, проверяет
* подпись, расшифровывает и вставляет в dm_messages (dedup по seq).
*
* Сервис ETCP_RT_ID_DM_MAILBOX (0x35), подкоманды PUT/PULL/PULL_RESP/ACK.
*/
#ifndef DM_MAILBOX_H
#define DM_MAILBOX_H
#include <stdint.h>
#include <stddef.h>
struct UTUN_INSTANCE;
#ifdef __cplusplus
extern "C" {
#endif
/* Жизненный цикл: init после chat_core_init, destroy перед chat_core_destroy. */
int dm_mailbox_init(struct UTUN_INSTANCE* inst);
void dm_mailbox_destroy(struct UTUN_INSTANCE* inst);
/* Коллбэк доставки вытянутых сообщений в dm_core (msg = каноническое тело
* DM-сообщения, включая подпись). Вызывается из uasync-потока. */
typedef void (*dm_mail_deliver_fn)(const uint8_t* msg, size_t len, void* arg);
void dm_mailbox_set_deliver_cb(struct UTUN_INSTANCE* inst, dm_mail_deliver_fn fn, void* arg);
/* Отправитель: положить зашифрованное сообщение на storage-узлы. msg — уже
* собранное каноническое тело (зашифровано + подписано). 0=ок, <0=ошибка. */
int dm_mailbox_put(struct UTUN_INSTANCE* inst, uint64_t recipient, uint64_t sender,
const uint8_t* msg, size_t msg_len);
/* Получатель: вытянуть почту от конкретного отправителя (seq > since_seq) со
* всех известных storage-узлов. Вызывается из uasync-потока. */
void dm_mailbox_pull(struct UTUN_INSTANCE* inst, uint64_t peer_node_id, uint64_t since_seq);
/* Собрать node_id storage-узлов (dm_storage=yes) из общих групп. До max штук.
* Возвращает количество найденных. out — буфер на max*8 байт. */
int dm_mailbox_get_storage_nodes(struct UTUN_INSTANCE* inst, uint64_t* out, int max);
#ifdef __cplusplus
}
#endif
#endif /* DM_MAILBOX_H */
+2 -2
View File
@@ -1378,7 +1378,7 @@ int media_delivery_init(struct UTUN_INSTANCE* inst) {
}
etcp_add_conn_status_cbk(inst, md_on_conn_status, md);
member_sync_add_props_cbk(md_on_props_changed, md);
member_sync_add_props_cbk(inst, md_on_props_changed, md);
/* determine initial supernode state and subscribe to BGP callbacks */
struct ll_entry* gle = inst->topo_groups ? inst->topo_groups->group_list->head : NULL;
@@ -1416,7 +1416,7 @@ void media_delivery_destroy(struct UTUN_INSTANCE* inst) {
etcp_router_unbind(inst, ETCP_RT_ID_MEDIA_DELIVERY);
etcp_remove_conn_status_cbk(inst, md_on_conn_status, md);
member_sync_remove_props_cbk(md_on_props_changed, md);
member_sync_remove_props_cbk(inst, md_on_props_changed, md);
/* unsubscribe from BGP callbacks */
struct ll_entry* gle = inst->topo_groups ? inst->topo_groups->group_list->head : NULL;
+3 -1
View File
@@ -184,8 +184,10 @@ static struct ETCP_CONN* router_send_conn(struct ETCP_ROUTER_CONN* rconn) {
if (c) return c;
}
}
return topo_group_find_conn_for_node(group, rconn->remote_node_id);
}
return topo_group_find_conn_for_node(group, rconn->remote_node_id);
/* group_id=0 (или группа не найдена) — глобальная маршрутизация: прямое соединение по node_id */
return instance_find_conn(inst, rconn->remote_node_id);
}
// Собрать и закодировать (подпись/шифрование) пакет в начале отправки.
+1 -1
View File
@@ -555,7 +555,7 @@ struct TOPO_GROUP* topo_groups_create_group(struct TOPO_GROUPS* g, uint64_t grou
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Group created: group_id=%016llx type=%u ch_id=%s", (unsigned long long)group_id, group_type, group->channel_id);
if (group_type == TOPO_GROUP_TYPE_CHAT && chat_setting_get_int("group_autoconnect", 1)) topo_group_connect_init(group);
if (group_type == TOPO_GROUP_TYPE_CHAT && chat_setting_get_int(group->instance, "group_autoconnect", 1)) topo_group_connect_init(group);
return group;
}
+1 -1
View File
@@ -91,7 +91,7 @@ static void tgc_notify_connecting(struct TOPO_GROUP_CONNECT* gc, const uint64_t*
*p++ = (uint8_t)cl; memcpy(p, gc->group->channel_id, cl); p += cl;
uint16_t c = (uint16_t)count; memcpy(p, &c, 2); p += 2;
for (int i = 0; i < count; i++) { memcpy(p, &ids[i], 8); p += 8; }
chat_event_post(CHAT_EVT_CONNECTING_NODES, buf, (int)sz);
chat_event_post(gc->group->instance, CHAT_EVT_CONNECTING_NODES, buf, (int)sz);
u_free(buf);
DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: notify_connecting ch=%s count=%d",
TGC_ID, gc->group->channel_id, count);
+3 -3
View File
@@ -180,7 +180,7 @@ static void tgi_handle_info_req(struct ETCP_CONN* conn, const uint8_t* data, siz
}
/* ── создаём инфраструктуру группы СРАЗУ ── */
chat_core_ensure_channel_ready(ch_id);
chat_core_ensure_channel_ready(inst, ch_id);
/* запуск BGP: узел соединён с группой, членство подтверждено (inviter, INFO_REQ) */
{
@@ -310,11 +310,11 @@ static void tgi_handle_info_resp(struct ETCP_CONN* conn, const uint8_t* data, si
if (ldb) topo_node_sqlite_channel_put(ldb, ch_id, ch_name, ch_owner,
ch_x25519, NULL, ch_ed_pub, NULL, ch_sig);
}
chat_core_ensure_channel_ready(ch_id);
chat_core_ensure_channel_ready(inv->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(CHAT_EVT_CHANNEL_UPDATED, evt, 1 + ch_id_len); }
chat_event_post(inv->inst, CHAT_EVT_CHANNEL_UPDATED, evt, 1 + ch_id_len); }
/* ── верифицируем update_sig приглашающего ── */
{
+1 -1
View File
@@ -251,7 +251,7 @@ static void auto_socket_post_sockets_changed(struct AUTO_SOCKET* as) {
need_reconnect = 1;
as->v4_addr_changed = 0; as->v6_addr_changed = 0;
chat_event_post(CHAT_EVT_LOCAL_SOCKETS, NULL, 0);
chat_event_post(as->instance, CHAT_EVT_LOCAL_SOCKETS, NULL, 0);
if (need_reconnect) {
DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] connectivity change: v4 %d→%d v6 %d→%d — triggering reconnect",
+2
View File
@@ -47,6 +47,8 @@ extern "C" {
#define ETCP_RT_ID_CONN_MGR 0x11 // Connection Manager — management connections
#define ETCP_RT_ID_NCD_CONTROL 0x12 // node_conn_direct — CLOSE / KEEP_ALIVE control
#define ETCP_RT_ID_ADMIN_KEY 0x32 // chat_admin — передача канального приватного ключа (encrypted+signed)
#define ETCP_RT_ID_DM 0x34 // dm_core — прямой p2p чат (msg/ack/hello)
#define ETCP_RT_ID_DM_MAILBOX 0x35 // dm_mailbox — offline-хранение на storage-узлах (put/pull/ack)
// Connection status events (instance-level callback)
#define ETCP_CONN_STATUS_NEW 0 // соединение создано
+9 -2
View File
@@ -18,6 +18,8 @@
#include "chat/chat_core.h"
#include "chat/chat_sync.h"
#include "chat/chat_headless_control.h"
#include "dm/dm_core.h"
#include "dm/dm_mailbox.h"
#include "broadcast.h"
#include "stcp_server.h"
#include "control_server.h"
@@ -478,8 +480,10 @@ void utun_instance_destroy(struct UTUN_INSTANCE *instance) {
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] G done — ETCP sockets");
/* Phase H: chat */
chat_headless_control_destroy();
chat_headless_control_destroy(instance);
chat_media_startup_backfill_destroy(instance);
dm_mailbox_destroy(instance);
dm_core_destroy(instance);
chat_sync_destroy(instance);
chat_core_destroy(instance);
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] H done — chat");
@@ -672,8 +676,11 @@ int utun_instance_init(struct UTUN_INSTANCE *instance) {
snprintf(db_file, sizeof(db_file), "%s/chats.db", instance->config->global.db_path);
if (chat_core_init(instance, db_file) == 0) {
chat_sync_init(instance);
chat_media_backfill();
chat_media_backfill(instance);
chat_media_startup_backfill_init(instance);
/* dm_mailbox до dm_core: dm_core_init ставит deliver-cb в mailbox */
dm_mailbox_init(instance);
dm_core_init(instance);
}
}
+17
View File
@@ -29,9 +29,16 @@ extern "C" {
#include "etcp_api.h"
#include "config_parser.h"
#include "topo_group.h"
#include "chat/chat_setting.h"
#include "chat/chat_event.h"
// Forward declarations
struct utun_config;
struct chat_core_ctx;
struct chat_sync;
struct join_key_entry; /* chat_join.c */
struct dm_state;
struct dm_mb_state;
struct uasync_s;
struct ROUTE_TABLE;
struct ETCP_CONN;
@@ -186,6 +193,16 @@ struct UTUN_INSTANCE {
struct CONN_MGR* conn_mgr; // Connection Manager (может быть NULL)
struct DB_SYNC* db_sync; // Distributed DB sync (может быть NULL)
// Chat/DM subsystem (per-instance contexts; were global singletons)
struct chat_core_ctx* chat_core; // chat_core.c (was g_cc)
struct chat_sync* chat_sync; // chat_sync.c (was g_cs)
struct join_key_entry* join_keys; // chat_join.c (was g_keys; собственный init/destroy)
struct dm_state* dm; // dm/dm_core.c (was g_dm)
struct dm_mb_state* dm_mailbox; // dm/dm_mailbox.c (was g_mb)
struct chat_setting_state chat_settings; // chat_setting.c (was globals)
chat_event_handler_fn chat_event_handler; // chat_event.c (was g_handler)
void* headless; // chat_headless_control.c (was g_hc)
// E2E encryption cache — per-peer sc_context_t with derived session key
#define E2E_CTX_CACHE_SIZE 8
struct { uint64_t peer_node_id; struct secure_channel ctx; uint8_t valid; } e2e_ctx_cache[E2E_CTX_CACHE_SIZE];