From 7c8d65db78e39878ede3a4792b7596da820b293f Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 13 Sep 2026 22:08:33 +0300 Subject: [PATCH] =?UTF-8?q?chat:=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=BE=D0=B5=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BA=D0=B0=D0=BD=D0=B0=D0=BB=D0=B0=20(Android)=20+?= =?UTF-8?q?=20db=5Fsync=20=D0=B8=D0=BD=D1=81=D1=82=D0=B0=D0=BD=D1=81=D1=8B?= =?UTF-8?q?=20=D0=B2=20=D0=BA=D1=83=D1=87=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/chat_channel.c | 74 +++++++++++++++++++ src/chat/chat_core.c | 19 +---- src/chat/chat_core.h | 12 +++ src/chat/chat_core_priv.h | 3 - src/chat/chat_event.h | 1 + src/chat/chat_sync.c | 14 ++++ src/chat/chat_sync.h | 4 + src/chat/db_sync.c | 50 ++++++++----- src/chat/db_sync.h | 2 + src/chat/member_sync.c | 4 + src/chat/member_sync.h | 6 ++ src/chat/merkle_sync.c | 20 +++++ src/chat/merkle_sync.h | 6 ++ src/media_delivery/media_delivery.c | 49 ++++++++++++ src/media_delivery/media_delivery.h | 4 + src/media_delivery/media_index.c | 39 ++++++++++ src/media_delivery/media_index.h | 4 + src/routing_layer/topo_group.c | 18 +++++ src/routing_layer/topo_group.h | 9 +++ src/routing_layer/topo_group_invite.c | 5 ++ src/routing_layer/topo_node_sqlite.c | 36 +++++++++ src/routing_layer/topo_node_sqlite.h | 4 + .../main/java/com/utun/chat/MainActivity.kt | 5 +- .../java/com/utun/chat/data/ChatRepository.kt | 2 + .../main/java/com/utun/chat/data/NativeLib.kt | 2 + .../utun/chat/ui/screens/ChannelListScreen.kt | 27 ++++++- .../com/utun/chat/viewmodel/ChatViewModel.kt | 28 +++++++ .../jni_bridge/android_jni_bridge.c | 29 ++++++++ .../jni_bridge/android_jni_bridge.h | 3 + .../libutun_lite/instance_lite.c | 29 ++++++++ 30 files changed, 466 insertions(+), 42 deletions(-) diff --git a/src/chat/chat_channel.c b/src/chat/chat_channel.c index b6a0b487..bb154067 100644 --- a/src/chat/chat_channel.c +++ b/src/chat/chat_channel.c @@ -14,7 +14,10 @@ #include "../routing_layer/topo_group.h" #include "../routing_layer/topo_group_connect.h" #include "member_sync.h" +#include "chat_sync.h" #include "../transport_layer/secure_channel.h" +#include "../media_delivery/media_delivery.h" +#include "../media_delivery/media_index.h" #include "../../lib/mem.h" #include "../../lib/ll_queue.h" #include "../../lib/platform_compat.h" @@ -169,6 +172,8 @@ void chat_core_create_channel_auto(const char* name) { if (ed_pkey) { size_t pkLen = 32, skLen = 32; EVP_PKEY_get_raw_public_key(ed_pkey, ed25519_pub, &pkLen); EVP_PKEY_get_raw_private_key(ed_pkey, ed25519_priv, &skLen); } } uint64_t chId = 0; RAND_bytes((uint8_t*)&chId, sizeof(chId)); + chId &= 0x7FFFFFFFFFFFFFFFULL; /* как node_id — всегда < 2^63 (помещается в signed Long) */ + if (chId == 0) chId = 1; char channelId[64]; snprintf(channelId, sizeof(channelId), "%llu", (unsigned long long)chId); uint8_t sig[64] = {0}; @@ -295,3 +300,72 @@ void chat_core_apply_group_autoconnect(void) { e = e->next; } } + +/* ─── локальное удаление канала ─── */ + +static const char* cc_ui_state_get(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) { + 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); + if (v) snprintf(val, sizeof(val), "%s", v); + } + sqlite3_finalize(st); + } + return val; +} + +void chat_core_delete_channel(const char* ch_id) { + if (!g_cc.initialized || !g_cc.db || !g_cc.inst) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel — not initialized", CC_ID); + return; + } + if (!ch_id || !ch_id[0]) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel — empty ch_id", CC_ID); return; } + + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel BEGIN ch=%s", CC_ID, ch_id); + uint64_t gid = strtoull(ch_id, NULL, 10); + + /* 1. отмена медиа-загрузок/релэев/суперузлов канала (держат conn_mgr handle группы) */ + media_delivery_cancel_channel(g_cc.inst, gid); + + /* 2. снять merkle-сессии этого namespace */ + member_sync_cancel_channel(g_cc.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); + + /* 4. удалить db_sync инстанс (освобождает память, отменяет таймеры) */ + { + struct DB_SYNC_INSTANCE* si = si_find(ch_id); + if (si) db_sync_instance_remove(si); + } + + /* 5. немедленно убрать канал из кеша chat_sync */ + chat_sync_on_channel_deleted(g_cc.inst, ch_id); + + /* 6. удалить медиафайлы канала на диске + строки media_files */ + media_index_delete_channel(g_cc.db, ch_id, cc_ui_state_get("media_base")); + + /* 7. удалить таблицы и метаданные канала */ + topo_node_sqlite_channel_delete(g_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); + } + + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel DONE ch=%s gid=0x%016llx", CC_ID, ch_id, (unsigned long long)gid); +} + +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); + u_free(req); +} diff --git a/src/chat/chat_core.c b/src/chat/chat_core.c index e3d5157c..40a7b9ad 100644 --- a/src/chat/chat_core.c +++ b/src/chat/chat_core.c @@ -69,22 +69,11 @@ int db_exec(const char* sql) { /* ─── db_sync registry ─── */ struct DB_SYNC_INSTANCE* si_find(const char* ch_id) { - for (int i = 0; i < g_cc.si_count; i++) - if (strcmp(g_cc.si_ch_id[i], ch_id) == 0) return g_cc.si[i]; - return NULL; + 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)); } void si_register(struct DB_SYNC_INSTANCE* si, const char* ch_id) { - if (g_cc.si_count >= g_cc.si_capacity) { - int nc = g_cc.si_capacity ? g_cc.si_capacity * 2 : 8; - g_cc.si = u_realloc(g_cc.si, nc * sizeof(void*)); - g_cc.si_ch_id = u_realloc(g_cc.si_ch_id, nc * sizeof(char*)); - g_cc.si_capacity = nc; - } - g_cc.si[g_cc.si_count] = si; - g_cc.si_ch_id[g_cc.si_count] = u_strdup(ch_id); - g_cc.si_count++; - 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); } @@ -243,10 +232,6 @@ void chat_core_destroy(struct UTUN_INSTANCE* inst) { if (!g_cc.initialized) return; g_cc.initialized = 0; - for (int i = 0; i < g_cc.si_count; i++) u_free(g_cc.si_ch_id[i]); - u_free(g_cc.si_ch_id); u_free(g_cc.si); - g_cc.si = NULL; g_cc.si_ch_id = NULL; g_cc.si_count = g_cc.si_capacity = 0; - if (g_cc.db && !g_cc.shared_db) { sqlite3_close(g_cc.db); } g_cc.db = NULL; g_cc.inst = NULL; DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: destroyed", CC_ID); diff --git a/src/chat/chat_core.h b/src/chat/chat_core.h index 3f7407a4..d6066caa 100644 --- a/src/chat/chat_core.h +++ b/src/chat/chat_core.h @@ -88,6 +88,18 @@ void chat_core_create_channel_trampoline(void* arg); void chat_core_create_channel_auto(const char* name); void chat_core_create_channel_auto_trampoline(void* arg); +/* ── Локальное удаление канала (GUI → uasync) ── */ + +struct chat_delete_channel_req { + char ch_id[64]; +}; + +/* Удалить канал с этого устройства: отмена медиа-загрузок, удаление группы + * (соединения/синк), db_sync инстанса, таблиц msg_ и peers_, медиафайлов и + * merkle-хешей. Работает в uasync-потоке. Другие узлы канал не теряют. */ +void chat_core_delete_channel(const char* ch_id); +void chat_core_delete_channel_trampoline(void* arg); + /* ── Утилиты ── */ void chat_core_set_my_node_id(uint64_t node_id); diff --git a/src/chat/chat_core_priv.h b/src/chat/chat_core_priv.h index 8c5e6587..96a7c1f7 100644 --- a/src/chat/chat_core_priv.h +++ b/src/chat/chat_core_priv.h @@ -81,9 +81,6 @@ struct chat_core_ctx { char db_path[512]; uint8_t shared_db; uint64_t my_node_id; - struct DB_SYNC_INSTANCE** si; - char** si_ch_id; - int si_count, si_capacity; uint8_t initialized; }; diff --git a/src/chat/chat_event.h b/src/chat/chat_event.h index 9b4732d3..70c94c7f 100644 --- a/src/chat/chat_event.h +++ b/src/chat/chat_event.h @@ -49,6 +49,7 @@ extern "C" { #define CHAT_EVT_ADMIN_KEY_RECEIVED 28 /* [ch_id_len:1][ch_id:var][from_node_id:8] — получен канальный ключ (стал совладельцем) */ #define CHAT_EVT_INVITE_LINK_READY 29 /* [ch_id_len:1][ch_id:var][link_len:2][link:var] — пустой link = ошибка */ #define CHAT_EVT_INVITE_CANDIDATES 30 /* [ch_id_len:1][ch_id:var][auto_node_id:8][count:2][node_id:8]* — достижимые узлы для invite */ +#define CHAT_EVT_CHANNEL_DELETED 31 /* [ch_id_len:1][ch_id:var] — канал удалён локально */ typedef void (*chat_event_handler_fn)(int type, const uint8_t* data, int len); diff --git a/src/chat/chat_sync.c b/src/chat/chat_sync.c index 1b42489b..f80872cd 100644 --- a/src/chat/chat_sync.c +++ b/src/chat/chat_sync.c @@ -679,6 +679,20 @@ void chat_sync_connect_node(struct UTUN_INSTANCE* inst, uint64_t node_id) { (void)inst; (void)node_id; } +void chat_sync_on_channel_deleted(struct UTUN_INSTANCE* inst, const char* ch_id) { + (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--; + DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: channel removed from cache ch=%s", CS_ID, ch_id); + return; + } +} + struct chat_invite { uint64_t channel_id; uint64_t node_id; diff --git a/src/chat/chat_sync.h b/src/chat/chat_sync.h index e0130d6c..efd74f62 100644 --- a/src/chat/chat_sync.h +++ b/src/chat/chat_sync.h @@ -111,6 +111,10 @@ void chat_sync_join_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint6 Вызывается из auto_socket при появлении/изменении сетевой связности. */ void chat_sync_retry_channels_on_socket_change(struct UTUN_INSTANCE* inst); +/* Немедленно убрать канал из кеша g_cs->channels (локальное удаление канала), + * чтобы cs_on_conn_status не запускал member_sync для удалённого ns до следующего refresh. */ +void chat_sync_on_channel_deleted(struct UTUN_INSTANCE* inst, const char* ch_id); + #ifdef __cplusplus } #endif diff --git a/src/chat/db_sync.c b/src/chat/db_sync.c index 95e794ba..a8be7d71 100644 --- a/src/chat/db_sync.c +++ b/src/chat/db_sync.c @@ -82,7 +82,7 @@ struct DB_SYNC { sqlite3* db; uint8_t shared_db; /* db == inst->topo_sqlite_db, do not close */ uint64_t last_connected_tb; - struct DB_SYNC_INSTANCE* instances; + struct DB_SYNC_INSTANCE** instances; int instance_count, instance_capacity; void* peer_check_timer; void* peer_check_wait; /* standby_wait handle (Android) */ @@ -225,26 +225,33 @@ static void db_chain_hash_compute(const uint8_t prev_chain_hash[32], static struct DB_SYNC_INSTANCE* db_instance_find(struct DB_SYNC* db, uint64_t group_id) { for (int i = 0; i < db->instance_count; i++) { - if (db->instances[i].group_id == group_id && db->instances[i].enabled) return &db->instances[i]; + if (db->instances[i]->group_id == group_id && db->instances[i]->enabled) return db->instances[i]; } return NULL; } -// Выделяет новый слот инстанса в массиве, расширяя его при необходимости +struct DB_SYNC_INSTANCE* db_sync_instance_find(struct UTUN_INSTANCE* inst, uint64_t group_id) +{ + if (!inst || !inst->db_sync) return NULL; + return db_instance_find(inst->db_sync, group_id); +} + +// Выделяет новый инстанс в куче (стабильный указатель) и регистрирует его в массиве static struct DB_SYNC_INSTANCE* db_instance_alloc(struct DB_SYNC* db) { DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "count=%d cap=%d", db->instance_count, db->instance_capacity); if (db->instance_count >= db->instance_capacity) { int nc = db->instance_capacity ? db->instance_capacity * 2 : 4; - struct DB_SYNC_INSTANCE* np = u_realloc(db->instances, nc * sizeof(*db->instances)); + struct DB_SYNC_INSTANCE** np = u_realloc(db->instances, nc * sizeof(*db->instances)); if (!np) return NULL; db->instances = np; db->instance_capacity = nc; } - struct DB_SYNC_INSTANCE* si = &db->instances[db->instance_count++]; - memset(si, 0, sizeof(*si)); + struct DB_SYNC_INSTANCE* si = u_calloc(1, sizeof(*si)); + if (!si) return NULL; si->db_sync = db; si->enabled = 1; + db->instances[db->instance_count++] = si; return si; } @@ -856,8 +863,10 @@ static void db_handle_init_resp(struct DB_SYNC_INSTANCE* si, uint64_t src, const struct SI_PEER* sp = si_peer_find(si, src); - // Peer empty → send ALL our data, then SYNC_DONE will be sent from DATA handler when slave responds - if (peer_ch8 == 0 && sc == 0) { + // Peer empty → send ALL our data, then SYNC_DONE will be sent from DATA handler when slave responds. + // Если мы тоже пусты (mc==0) — обе стороны пусты: не отправляем ничего, а завершаем sync через + // SYNC_DONE в ветке "exact match" ниже (иначе обе стороны вечно ждут ответа → TIMEOUT). + if (peer_ch8 == 0 && sc == 0 && mc > 0) { uint32_t vp = (uint32_t)-1; if (sp) sp->verified_pos = vp; uint32_t sent = 0; while (sent < mc) { @@ -1293,7 +1302,7 @@ static void db_sync_on_conn_up(struct ETCP_CONN* conn, void* arg) int synced = 0; char tbl_list[256] = ""; for (int i = 0; i < db->instance_count; i++) { - struct DB_SYNC_INSTANCE* si = &db->instances[i]; + struct DB_SYNC_INSTANCE* si = db->instances[i]; if (!si->enabled) continue; struct SI_PEER* p = si_peer_add(si, pid); if (!p) continue; @@ -1323,14 +1332,14 @@ static void db_sync_on_conn_down(struct ETCP_CONN* conn, void* arg) DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "peer=%016llx", (unsigned long long)pid); for (int i = 0; i < db->instance_count; i++) { - struct SI_PEER* p = si_peer_find(&db->instances[i], pid); + struct SI_PEER* p = si_peer_find(db->instances[i], pid); if (p) p->sync_state = 0; } int any = 0; for (int i = 0; i < db->instance_count; i++) { - for (int j = 0; j < db->instances[i].peer_count; j++) { - if (db->instances[i].peers[j].sync_state >= 1) { any = 1; goto cd_done; } + for (int j = 0; j < db->instances[i]->peer_count; j++) { + if (db->instances[i]->peers[j].sync_state >= 1) { any = 1; goto cd_done; } } } cd_done: @@ -1507,7 +1516,7 @@ static int db_sync_has_pending_work(struct DB_SYNC* db) { if (!db || !db->enabled) return 0; for (int i = 0; i < db->instance_count; i++) { - struct DB_SYNC_INSTANCE* si = &db->instances[i]; + struct DB_SYNC_INSTANCE* si = db->instances[i]; if (!si->enabled) continue; for (int j = 0; j < si->peer_count; j++) if (si->peers[j].sync_state != 2) return 1; @@ -1546,7 +1555,7 @@ static void db_sync_peer_check_cb(void* arg) char launched_list[256] = ""; if (g) { for (int i = 0; i < db->instance_count; i++) { - struct DB_SYNC_INSTANCE* si = &db->instances[i]; + struct DB_SYNC_INSTANCE* si = db->instances[i]; if (!si->enabled) continue; if (si->sync_gated) continue; @@ -1591,7 +1600,7 @@ static void db_sync_peer_check_cb(void* arg) uint64_t now = get_time_tb(); uint64_t to_tb = DB_SYNC_SYNC_TIMEOUT * 10000u; for (int i = 0; i < db->instance_count; i++) { - struct DB_SYNC_INSTANCE* si = &db->instances[i]; + struct DB_SYNC_INSTANCE* si = db->instances[i]; if (!si->enabled) continue; for (int j = 0; j < si->peer_count; j++) { struct SI_PEER* p = &si->peers[j]; @@ -1722,10 +1731,11 @@ void db_sync_destroy(struct UTUN_INSTANCE* inst) #endif for (int i = 0; i < db->instance_count; i++) { - struct DB_SYNC_INSTANCE* si = &db->instances[i]; + struct DB_SYNC_INSTANCE* si = db->instances[i]; if (si->ttl_timer) { uasync_cancel_timeout(inst->ua, si->ttl_timer); si->ttl_timer = NULL; } if (si->recalc.timer) { uasync_cancel_timeout(inst->ua, si->recalc.timer); si->recalc.timer = NULL; } if (si->peers) u_free(si->peers); + u_free(si); } etcp_remove_conn_status_cbk(inst, db_sync_on_conn_status, NULL); @@ -1834,13 +1844,17 @@ void db_sync_instance_remove(struct DB_SYNC_INSTANCE* si) if (si->recalc.timer) { uasync_cancel_timeout(inst->ua, si->recalc.timer); si->recalc.timer = NULL; } if (si->peers) { u_free(si->peers); si->peers = NULL; si->peer_count = si->peer_capacity = 0; } - int idx = (int)(si - db->instances); - if (idx >= 0 && idx < db->instance_count) { + int idx = -1; + for (int i = 0; i < db->instance_count; i++) { + if (db->instances[i] == si) { idx = i; break; } + } + if (idx >= 0) { memmove(&db->instances[idx], &db->instances[idx + 1], (db->instance_count - idx - 1) * sizeof(*db->instances)); db->instance_count--; } DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "instance removed tbl=%s group=%016llx", si->table_name, (unsigned long long)si->group_id); + u_free(si); } // Вставляет подписанную запись: проверяет подпись, вставляет в БД, рассылает PUSH всем синхронизированным пирам diff --git a/src/chat/db_sync.h b/src/chat/db_sync.h index d7b71df4..ef2edecf 100644 --- a/src/chat/db_sync.h +++ b/src/chat/db_sync.h @@ -98,6 +98,8 @@ void db_sync_destroy(struct UTUN_INSTANCE* inst); // Instance management struct DB_SYNC_INSTANCE* db_sync_instance_add(struct UTUN_INSTANCE* inst, const char* table_name, uint64_t group_id, int auto_sync); void db_sync_instance_remove(struct DB_SYNC_INSTANCE* si); +// Найти живой инстанс по group_id (поиск в текущем массиве db->instances — указатель всегда актуален). +struct DB_SYNC_INSTANCE* db_sync_instance_find(struct UTUN_INSTANCE* inst, uint64_t group_id); // Data operations (per-instance) // db_sync_insert_signed: sig MUST be non-NULL, 64 bytes. diff --git a/src/chat/member_sync.c b/src/chat/member_sync.c index 3703c7c3..49d2b8f6 100644 --- a/src/chat/member_sync.c +++ b/src/chat/member_sync.c @@ -813,6 +813,10 @@ void member_sync_cancel(struct UTUN_INSTANCE* inst, uint64_t peer, const char* c merkle_sync_cancel(inst, peer, ch_id); } +void member_sync_cancel_channel(struct UTUN_INSTANCE* inst, const char* ch_id) { + merkle_sync_cancel_ns(inst, ch_id); +} + /* Сериализует одного мембера в wire-формат [count:2][member...]. Возвращает 0 и *out_len, или -1 если мембер не найден/нет БД. */ static int _serialize_member(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t member_id, diff --git a/src/chat/member_sync.h b/src/chat/member_sync.h index 8fdf300c..a8b34546 100644 --- a/src/chat/member_sync.h +++ b/src/chat/member_sync.h @@ -96,6 +96,12 @@ int member_sync_start(struct UTUN_INSTANCE* inst, uint64_t peer, */ void member_sync_cancel(struct UTUN_INSTANCE* inst, uint64_t peer, const char* ch_id); +/* + * Отменить все merkle-сессии канала (локальное удаление канала). + * Делегирует в merkle_sync_cancel_ns(). + */ +void member_sync_cancel_channel(struct UTUN_INSTANCE* inst, const char* ch_id); + /* * Добавить/обновить мембера (локальная запись, доверенная) и пересчитать дерево. * Делегирует в member_sync_apply_record() с from_peer = inst->node_id. diff --git a/src/chat/merkle_sync.c b/src/chat/merkle_sync.c index 1520faf6..e7cc3033 100644 --- a/src/chat/merkle_sync.c +++ b/src/chat/merkle_sync.c @@ -910,3 +910,23 @@ void merkle_sync_cancel(struct UTUN_INSTANCE* inst, uint64_t peer, const char* n p = &(*p)->next; } } + +void merkle_sync_cancel_ns(struct UTUN_INSTANCE* inst, const char* ns) { + struct merkle_sync* ms = inst ? inst->msync : NULL; + if (!ms || !ns) return; + DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cancel_ns ns=%s", MS_ID, ns); + struct ms_session** p = &ms->sessions; + int removed = 0; + while (*p) { + struct ms_session* s = *p; + if (strcmp(s->ns, ns) == 0) { + *p = s->next; + ms_session_outq_free(s); + u_free(s); + removed++; + continue; + } + p = &(*p)->next; + } + if (removed) DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cancel_ns ns=%s removed=%d", MS_ID, ns, removed); +} diff --git a/src/chat/merkle_sync.h b/src/chat/merkle_sync.h index 6421adb4..af7c9465 100644 --- a/src/chat/merkle_sync.h +++ b/src/chat/merkle_sync.h @@ -216,6 +216,12 @@ int merkle_sync_start(struct UTUN_INSTANCE* inst, uint64_t peer, */ void merkle_sync_cancel(struct UTUN_INSTANCE* inst, uint64_t peer, const char* ns); +/* + * Отменить и удалить ВСЕ сессии для namespace ns (независимо от пира). + * Используется при локальном удалении канала. done_cb НЕ вызывается. + */ +void merkle_sync_cancel_ns(struct UTUN_INSTANCE* inst, const char* ns); + /* ── Recompute tree path after data change ── */ /* diff --git a/src/media_delivery/media_delivery.c b/src/media_delivery/media_delivery.c index e2316ecf..1e4e8317 100644 --- a/src/media_delivery/media_delivery.c +++ b/src/media_delivery/media_delivery.c @@ -1459,6 +1459,55 @@ void media_delivery_destroy(struct UTUN_INSTANCE* inst) { DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: destroyed", MD_ID); } +void media_delivery_cancel_channel(struct UTUN_INSTANCE* inst, uint64_t group_id) { + if (!inst || !inst->md.initialized) return; + struct media_delivery_ctx* md = &inst->md; + + /* 1. собрать media_id активных загрузок этого канала и отменить их */ + uint8_t media_ids[MEDIA_MAX_DOWNLOADS][16]; + int nm = 0; + if (md->downloads) { + struct ll_entry* de = md->downloads->head; + while (de) { + struct media_download* dl = (struct media_download*)de->data; + struct ll_entry* next = de->next; + if (dl->group_id == group_id) { + if (nm < MEDIA_MAX_DOWNLOADS) memcpy(media_ids[nm++], dl->media_id, 16); + media_download_cancel(inst, dl->media_id, NULL, 0); + } + de = next; + } + } + + /* 2. удалить relay-контексты этих media (остаются после отмены загрузки) */ + for (int i = 0; i < nm; i++) { + if (!md->relay_blocks) break; + struct ll_entry* re = md->relay_blocks->head; + while (re) { + struct relay_block_ctx* rc = (struct relay_block_ctx*)re->data; + struct ll_entry* next = re->next; + if (memcmp(rc->media_id, media_ids[i], 16) == 0) + md_relay_remove(md, rc->block_id); + re = next; + } + } + + /* 3. удалить super_peers этой группы */ + if (md->super_peers) { + struct ll_entry* se = md->super_peers->head; + while (se) { + struct media_super_peer* sp = (struct media_super_peer*)se->data; + struct ll_entry* next = se->next; + if (sp->group_id == group_id) + md_super_peer_remove(md, sp->peer_node_id); + se = next; + } + } + + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: cancel_channel group=%016llx downloads=%d", + MD_ID, (unsigned long long)group_id, nm); +} + int media_delivery_bind(struct UTUN_INSTANCE* inst) { return media_delivery_init(inst); } diff --git a/src/media_delivery/media_delivery.h b/src/media_delivery/media_delivery.h index f4645d4f..f44fd0bc 100644 --- a/src/media_delivery/media_delivery.h +++ b/src/media_delivery/media_delivery.h @@ -146,6 +146,10 @@ void media_delivery_send_have_block(struct UTUN_INSTANCE* inst, uint64_t group_i * block_availability для уже скачанных ранее медиа. Идемпотентно на суперузле. */ void media_delivery_announce_local_blocks(struct UTUN_INSTANCE* inst); +/* Отменить всё активное, что связано с каналом (локальное удаление канала): + * загрузки с group_id, их relay-контексты и super_peers этой группы. */ +void media_delivery_cancel_channel(struct UTUN_INSTANCE* inst, uint64_t group_id); + /* relay block context helpers (used by media_download.c) */ struct relay_block_ctx* md_relay_find(struct media_delivery_ctx* md, const uint8_t* block_id); struct relay_block_ctx* md_relay_add(struct media_delivery_ctx* md, diff --git a/src/media_delivery/media_index.c b/src/media_delivery/media_index.c index 21b57aff..c8a5bf54 100644 --- a/src/media_delivery/media_index.c +++ b/src/media_delivery/media_index.c @@ -111,6 +111,45 @@ int media_calc_block_size(uint64_t file_size) { return (int)target; } +int media_index_delete_channel(sqlite3* db, const char* chat_id, const char* media_base) { + if (!db || !chat_id || !chat_id[0] || !media_base || !media_base[0]) return -1; + + char base[512]; + size_t base_len = strlen(media_base); + while (base_len > 0 && media_base[base_len - 1] == '/') base_len--; + snprintf(base, sizeof(base), "%.*s", (int)base_len, media_base); + + int files = 0, removed = 0; + sqlite3_stmt* st = NULL; + if (sqlite3_prepare_v2(db, + "SELECT DISTINCT location FROM media_files WHERE chat_id=? AND location!=''", + -1, &st, NULL) == SQLITE_OK) { + sqlite3_bind_text(st, 1, chat_id, -1, SQLITE_STATIC); + while (sqlite3_step(st) == SQLITE_ROW) { + const char* loc = (const char*)sqlite3_column_text(st, 0); + if (!loc || !loc[0]) continue; + files++; + char full[1024]; + snprintf(full, sizeof(full), "%s/%s", base, loc); + if (remove(full) == 0) removed++; + else DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media_index: delete_channel remove failed: %s", full); + } + sqlite3_finalize(st); + } + + int rows = 0; + sqlite3_stmt* del = NULL; + if (sqlite3_prepare_v2(db, "DELETE FROM media_files WHERE chat_id=?", -1, &del, NULL) == SQLITE_OK) { + sqlite3_bind_text(del, 1, chat_id, -1, SQLITE_STATIC); + if (sqlite3_step(del) == SQLITE_DONE) rows = sqlite3_changes(db); + sqlite3_finalize(del); + } + + DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "media_index: delete_channel chat=%s files=%d removed=%d rows=%d", + chat_id, files, removed, rows); + return 0; +} + int media_index_generate_uuid(uint8_t uuid_out[16]) { ma_uuid(uuid_out); return 0; diff --git a/src/media_delivery/media_index.h b/src/media_delivery/media_index.h index 56f53065..b1c1a5bc 100644 --- a/src/media_delivery/media_index.h +++ b/src/media_delivery/media_index.h @@ -53,6 +53,10 @@ int media_index_generate_uuid(uint8_t uuid_out[16]); void media_index_result_free(struct media_index_result* result); int media_calc_block_size(uint64_t file_size); +/* Удалить медиа канала (локальное удаление канала): стереть файлы на диске + * (media_base + location) и строки media_files с chat_id. Возвращает 0/ошибка. */ +int media_index_delete_channel(sqlite3* db, const char* chat_id, const char* media_base); + #ifdef __cplusplus } #endif diff --git a/src/routing_layer/topo_group.c b/src/routing_layer/topo_group.c index 6c09fee3..75b13291 100644 --- a/src/routing_layer/topo_group.c +++ b/src/routing_layer/topo_group.c @@ -351,6 +351,24 @@ static void topo_group_destroy(struct TOPO_GROUP* group) { DEBUG_INFO(DEBUG_CATEGORY_SYS, "[GRP_DESTROY] 5 done"); } +void topo_groups_remove_group(struct TOPO_GROUPS* g, uint64_t group_id) { + if (!g || !g->group_list || group_id == TOPO_GROUP_UTUN) return; + struct TOPO_GROUP* group = topo_groups_find(g, group_id); + if (!group) { DEBUG_WARN(DEBUG_CATEGORY_BGP, "remove_group: group %016llx not found", (unsigned long long)group_id); return; } + + DEBUG_INFO(DEBUG_CATEGORY_BGP, "remove_group: id=%016llx type=%d ch=%s", + (unsigned long long)group_id, group->group_type, group->channel_id); + + /* освободить подписчиков BGP-событий (member_sync и др.), т.к. группа умирает */ + struct topo_node_cbk_entry* c = group->node_cbks; + while (c) { struct topo_node_cbk_entry* n = c->next; u_free(c); c = n; } + group->node_cbks = NULL; + + queue_remove_data(g->group_list, &group->ll); + topo_group_destroy(group); + queue_entry_free(&group->ll); +} + struct TOPO_GROUPS* topo_groups_init(struct UTUN_INSTANCE* instance) { if (!instance) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "instance is NULL"); return NULL; } DEBUG_INFO(DEBUG_CATEGORY_BGP, "node_id=%016llx", (unsigned long long)instance->node_id); diff --git a/src/routing_layer/topo_group.h b/src/routing_layer/topo_group.h index 3e1ee6bb..51fb10e0 100644 --- a/src/routing_layer/topo_group.h +++ b/src/routing_layer/topo_group.h @@ -253,6 +253,15 @@ struct TOPO_GROUP* topo_groups_find(struct TOPO_GROUPS* g, uint64_t group_id); */ struct TOPO_GROUP* topo_groups_create_group(struct TOPO_GROUPS* g, uint64_t group_id, uint8_t group_type, const char* channel_id); +/** + * @brief Удаляет одну группу по id (разрушает conn_mgr, connect-цикл, broadcast, + * recovery, узлы и подписчиков node_cbks) и извлекает её из group_list. + * + * Используется при локальном удалении чат-канала. Группу по умолчанию + * (TOPO_GROUP_UTUN) удалять нельзя — функция это игнорирует. + */ +void topo_groups_remove_group(struct TOPO_GROUPS* g, uint64_t group_id); + /** * @brief Добавляет conn в senders_list (если нет), отправляет запрос таблицы (nodeinfo). * diff --git a/src/routing_layer/topo_group_invite.c b/src/routing_layer/topo_group_invite.c index 6aa9dbcf..5bbf37cb 100644 --- a/src/routing_layer/topo_group_invite.c +++ b/src/routing_layer/topo_group_invite.c @@ -23,6 +23,7 @@ #include "../transport_layer/node_conn_direct.h" #include "ntp_time.h" #include "../chat/chat_core.h" +#include "../chat/chat_event.h" #define TGI_ID "topo_group_invite" #define TGI_TIMEOUT_MS 2000 @@ -311,6 +312,10 @@ static void tgi_handle_info_resp(struct ETCP_CONN* conn, const uint8_t* data, si } chat_core_ensure_channel_ready(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); } + /* ── верифицируем update_sig приглашающего ── */ { const uint8_t* inv_ed = conn->peer_ed25519_pubkey; diff --git a/src/routing_layer/topo_node_sqlite.c b/src/routing_layer/topo_node_sqlite.c index d507738b..2cfd521d 100644 --- a/src/routing_layer/topo_node_sqlite.c +++ b/src/routing_layer/topo_node_sqlite.c @@ -415,6 +415,42 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id, return rc == SQLITE_DONE ? 0 : -1; } +int topo_node_sqlite_channel_delete(sqlite3* db, const char* channel_id) { + if (!db || !channel_id || !channel_id[0]) return -1; + + char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); + char msg_tbl[80]; sanitize_ch_id(channel_id, msg_tbl, sizeof(msg_tbl)); + /* msg_ */ + char msg_full[80]; snprintf(msg_full, sizeof(msg_full), "msg_%s", msg_tbl); + + /* 1. drop per-channel tables (индексы дропятся вместе с таблицей) */ + char sql[128]; + snprintf(sql, sizeof(sql), "DROP TABLE IF EXISTS \"%s\"", peers_tbl); + sqlite3_exec(db, sql, NULL, NULL, NULL); + snprintf(sql, sizeof(sql), "DROP TABLE IF EXISTS \"%s\"", msg_full); + sqlite3_exec(db, sql, NULL, NULL, NULL); + + /* 2. delete channels row (вместе с канальными privkey) */ + sqlite3_stmt* st = NULL; + if (sqlite3_prepare_v2(db, "DELETE FROM channels WHERE channel_id=?", -1, &st, NULL) == SQLITE_OK) { + sqlite3_bind_text(st, 1, channel_id, -1, SQLITE_STATIC); + sqlite3_step(st); + sqlite3_finalize(st); + } + + /* 3. delete merkle tree hashes for this namespace */ + sqlite3_stmt* mh = NULL; + if (sqlite3_prepare_v2(db, "DELETE FROM merkle_tree_hash WHERE namespace=?", -1, &mh, NULL) == SQLITE_OK) { + sqlite3_bind_text(mh, 1, channel_id, -1, SQLITE_STATIC); + sqlite3_step(mh); + sqlite3_finalize(mh); + } + + DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_node_sqlite_channel_delete: ch=%s peers=%s msg=%s", + channel_id, peers_tbl, msg_full); + return 0; +} + /* Упрощённый плейсхолдер мембера от topo_group: только node_id + pubkeys, source=1. * Не участвует в merkle-хеше. Если запись уже есть (полная merkle) — не трогает её. * Позже полная merkle-запись дополняет/перезаписывает блоки и сбрасывает source=0. */ diff --git a/src/routing_layer/topo_node_sqlite.h b/src/routing_layer/topo_node_sqlite.h index 4f56c964..82e8b3a3 100644 --- a/src/routing_layer/topo_node_sqlite.h +++ b/src/routing_layer/topo_node_sqlite.h @@ -33,6 +33,10 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id, const uint8_t* ed25519_pub, const uint8_t* ed25519_priv, const uint8_t* signature); +/* Полное локальное удаление канала: DROP peers_/msg_, DELETE channels, + * DELETE merkle_tree_hash WHERE namespace=. 0=ок. */ +int topo_node_sqlite_channel_delete(sqlite3* db, const char* channel_id); + /* Упрощённый плейсхолдер мембера от topo_group (source=1, не участвует в merkle-хеше). */ int topo_node_sqlite_member_placeholder_put(sqlite3* db, const char* channel_id, uint64_t node_id, const uint8_t* x25519_pubkey, const uint8_t* ed25519_pubkey); diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt index d31c6847..fb83a7b5 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt @@ -67,7 +67,7 @@ class MainActivity : ComponentActivity() { val scope = rememberCoroutineScope() val showInvite: (Channel) -> Unit = { ch -> - if (ch.id.toLongOrNull() == null) { + if (ch.id.toULongOrNull() == null) { LogManager.addLog("ERROR", "INVITE", "channel id is not numeric: ${ch.id}") scope.launch { snackbarHostState.showSnackbar("Invalid channel id") } } else { @@ -111,9 +111,10 @@ class MainActivity : ComponentActivity() { connected = connected, isStuck = stuck, onSelectChannel = { ch -> vm.selectChannel(ch); screen = Screen.Chat }, - onCreateChannel = { vm.createChannel("general", "channel_1") }, + onCreateChannel = { name -> vm.createChannel(name, "channel_1") }, onJoinChannel = { showJoinDialog = true; joinAutoLink = null }, onInviteClick = showInvite, + onDeleteChannel = { vm.deleteChannel(it.id) }, onSettings = { screen = Screen.Settings }, onLogs = { screen = Screen.Logs }, onRefresh = { vm.requestRefresh() } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt index 16073230..da1f5bd6 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt @@ -460,6 +460,8 @@ class ChatRepository { fun transferAdmin(channelId: String, nodeId: Long) = NativeLib.transferAdmin(channelId, nodeId) + fun deleteChannel(channelId: String) = NativeLib.deleteChannel(channelId) + fun getMemberLinks(nodeId: Long): MemberLinks { try { val obj = JSONObject(NativeLib.getMemberLinks(nodeId)) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt index f6c83050..1b0e3058 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt @@ -100,6 +100,7 @@ object NativeLib { nativeSetMemberFlags(channelId, nodeId, admin, supernode, moder, storage, verified, deleted) } fun transferAdmin(channelId: String, nodeId: Long) { nativeTransferAdmin(channelId, nodeId) } + fun deleteChannel(channelId: String) { nativeDeleteChannel(channelId) } fun getMemberLinks(nodeId: Long): String = nativeGetMemberLinks(nodeId) ?: "{}" /* ── Local sockets (for auto-mode display) ── */ @@ -178,6 +179,7 @@ object NativeLib { private external fun nativeSetMemberFlags(channelId: String, nodeId: Long, admin: Boolean, supernode: Boolean, moder: Boolean, storage: Boolean, verified: Boolean, deleted: Boolean) private external fun nativeTransferAdmin(channelId: String, nodeId: Long) + private external fun nativeDeleteChannel(channelId: String) private external fun nativeGetMemberLinks(nodeId: Long): String? /* ── Local sockets JNI ── */ diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt index 0059f0aa..f3a48cb0 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt @@ -34,9 +34,10 @@ fun ChannelListScreen( connected: Boolean, isStuck: Boolean = false, onSelectChannel: (Channel) -> Unit, - onCreateChannel: () -> Unit, + onCreateChannel: (String) -> Unit, onJoinChannel: () -> Unit, onInviteClick: (Channel) -> Unit, + onDeleteChannel: (Channel) -> Unit, onSettings: () -> Unit, onLogs: () -> Unit, onRefresh: () -> Unit = {} @@ -44,6 +45,7 @@ fun ChannelListScreen( var showCreateDialog by remember { mutableStateOf(false) } var menuChannelId by remember { mutableStateOf(null) } var optionsChannel by remember { mutableStateOf(null) } + var deleteConfirmChannel by remember { mutableStateOf(null) } LaunchedEffect(Unit) { onRefresh() } @@ -108,6 +110,10 @@ fun ChannelListScreen( text = { Text("Options") }, onClick = { menuChannelId = null; optionsChannel = ch } ) + DropdownMenuItem( + text = { Text("Delete channel", color = MaterialTheme.colorScheme.error) }, + onClick = { menuChannelId = null; deleteConfirmChannel = ch } + ) } if (badgeText.isNotEmpty()) { val badgeSize = 18.dp @@ -151,7 +157,7 @@ fun ChannelListScreen( onDismiss = { showCreateDialog = false }, onCreate = { name -> showCreateDialog = false - onCreateChannel() + onCreateChannel(name) } ) } @@ -162,6 +168,23 @@ fun ChannelListScreen( onDismiss = { optionsChannel = null } ) } + + deleteConfirmChannel?.let { ch -> + AlertDialog( + onDismissRequest = { deleteConfirmChannel = null }, + title = { Text("Delete channel") }, + text = { Text("Delete \"${ch.name}\" and all its local messages and files? Other members keep the channel.") }, + confirmButton = { + TextButton(onClick = { deleteConfirmChannel = null; onDeleteChannel(ch) }, + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)) { + Text("Delete") + } + }, + dismissButton = { + TextButton(onClick = { deleteConfirmChannel = null }) { Text("Cancel") } + } + ) + } } @Composable diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt index 004052bb..557a18a1 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt @@ -403,6 +403,20 @@ class ChatViewModel : ViewModel() { _inviteLink.value = link _inviteLinkPending.value = false } + 31 -> { /* CHANNEL_DELETED: [ch_id_len:1][ch_id:var] */ + if (repo == null || data == null || data.size < 2) return + val cl = data[0].toInt() and 0xFF + if (data.size < 1 + cl) return + val chId = String(data, 1, cl) + LogManager.addLog("INFO", "VM", "CHANNEL_DELETED ch=$chId") + if (_currentChannel.value?.id == chId) { + _currentChannel.value = null + _messages.value = emptyList() + _members.value = emptyList() + _channelOwner.value = false + } + refreshChannels() + } } } @@ -556,6 +570,20 @@ class ChatViewModel : ViewModel() { refreshChannels() } + fun deleteChannel(channelId: String) { + val r = repo ?: return + LogManager.addLog("INFO", "VM", "deleteChannel ch=$channelId") + r.deleteChannel(channelId) + if (_currentChannel.value?.id == channelId) { + _currentChannel.value = null + _messages.value = emptyList() + _members.value = emptyList() + _channelOwner.value = false + } + NotificationSound.setMuted(channelId, false) + refreshChannels() + } + fun connectNode(address: String, port: Int, pubkeyHex: String) { val r = repo ?: return r.connectNode(address, port, pubkeyHex) diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index d5e08ec0..1a6053c1 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.c @@ -203,6 +203,12 @@ void utun_bridge_send_message(const char* channel_id, const char* text) { void utun_bridge_create_channel(const char* name, const char* channel_id) { bridge_log(BLEV_INFO, "create_channel name=%s id=%s", name ? name : "", channel_id ? channel_id : ""); + if (!name || !name[0]) { bridge_log(BLEV_ERROR, "create_channel: empty name"); return; } + struct UASYNC* ua = instance_lite_get_uasync(); + if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "create_channel: uasync not running"); return; } + char* n = u_strdup(name); + if (!n) { bridge_log(BLEV_ERROR, "create_channel: OOM"); return; } + uasync_post(ua, chat_core_create_channel_auto_trampoline, n); } void utun_bridge_connect_node(const char* address, int port, const char* pubkey_hex) { @@ -911,6 +917,21 @@ void utun_bridge_transfer_admin(const char* channel_id, uint64_t node_id) { bridge_log(BLEV_INFO, "transfer_admin queued ch=%s → 0x%016llx", channel_id, (unsigned long long)node_id); } +/* ────────────────────────────────────────────────────────────────── + * Удаление канала (локальное) + * ────────────────────────────────────────────────────────────────── */ + +void utun_bridge_delete_channel(const char* channel_id) { + if (!channel_id || !channel_id[0]) { bridge_log(BLEV_ERROR, "delete_channel: empty ch_id"); return; } + struct UASYNC* ua = instance_lite_get_uasync(); + if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "delete_channel: uasync not running"); return; } + struct chat_delete_channel_req* req = u_calloc(1, sizeof(*req)); + if (!req) { bridge_log(BLEV_ERROR, "delete_channel: OOM"); return; } + strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1); + uasync_post(ua, chat_core_delete_channel_trampoline, req); + bridge_log(BLEV_INFO, "delete_channel queued ch=%s", channel_id); +} + /* ────────────────────────────────────────────────────────────────── * Local sockets state (for auto-mode UI display) * ────────────────────────────────────────────────────────────────── */ @@ -1912,6 +1933,14 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeTransferAdmin( (*env)->ReleaseStringUTFChars(env, channelId, ch); } +JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDeleteChannel( + JNIEnv* env, jobject thiz, jstring channelId) { + (void)thiz; + const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); + utun_bridge_delete_channel(ch); + (*env)->ReleaseStringUTFChars(env, channelId, ch); +} + JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetMemberFlags( JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId, jboolean admin, jboolean supernode, jboolean moder, jboolean storage, diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.h b/tools/chatgui-android/jni_bridge/android_jni_bridge.h index 2369b0d2..0c08d815 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.h +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.h @@ -147,6 +147,9 @@ char* utun_bridge_get_member_links_json(uint64_t node_id); /* Передать канальный приватный ключ (права админа) другому узлу. */ void utun_bridge_transfer_admin(const char* channel_id, uint64_t node_id); +/* Локально удалить канал (drop таблиц, соединений, медиа, db_sync). */ +void utun_bridge_delete_channel(const char* channel_id); + /* ── Local sockets state (for auto-mode display) ── */ char* utun_bridge_get_local_sockets_json(void); diff --git a/tools/chatgui-android/libutun_lite/instance_lite.c b/tools/chatgui-android/libutun_lite/instance_lite.c index 0ed6c0c9..1eb9397c 100644 --- a/tools/chatgui-android/libutun_lite/instance_lite.c +++ b/tools/chatgui-android/libutun_lite/instance_lite.c @@ -7,6 +7,7 @@ * * Mirrors UtunNode::runLoop() from tools/chatgui/transport/utun_node.cpp:221-372. */ +#define _GNU_SOURCE 1 /* dladdr (Linux); no-op on Android/bionic */ #include "instance_lite.h" #include "utun_instance.h" #include "config_parser.h" @@ -34,6 +35,8 @@ #include #include #include +#include +#include #include #include #include @@ -203,10 +206,36 @@ static void on_link_status_changed(struct ETCP_CONN* conn, struct ETCP_LINK* lin static volatile int g_signal_caught = 0; static volatile int g_signal_num = 0; +struct crash_bt_state { void** frames; int count; int max; }; + +static _Unwind_Reason_Code crash_bt_cb(struct _Unwind_Context* ctx, void* arg) { + struct crash_bt_state* st = (struct crash_bt_state*)arg; + if (st->count < st->max) { + void* pc = (void*)_Unwind_GetIP(ctx); + if (pc) st->frames[st->count++] = pc; + return _URC_NO_REASON; + } + return _URC_END_OF_STACK; +} + static void crash_signal_handler(int sig, siginfo_t* info, void* ctx) { (void)ctx; g_signal_caught = 1; g_signal_num = sig; DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "CRASH signal=%d code=%d addr=%p thread still alive", sig, info->si_code, info->si_addr); + void* frames[64]; + struct crash_bt_state st = { frames, 0, 64 }; + _Unwind_Backtrace(crash_bt_cb, &st); + IL_LOGE("CRASH backtrace %d frames:", st.count); + for (int i = 0; i < st.count; i++) { + Dl_info dli; + if (dladdr(frames[i], &dli) && dli.dli_sname) { + IL_LOGE(" #%02d %p %s+%p (%s)", i, frames[i], dli.dli_sname, + (void*)((char*)frames[i] - (char*)dli.dli_saddr), + dli.dli_fname ? dli.dli_fname : "?"); + } else { + IL_LOGE(" #%02d %p (no symbol)", i, frames[i]); + } + } signal(sig, SIG_DFL); raise(sig); }