Browse Source

chat: локальное удаление канала (Android) + db_sync инстансы в куче

v2
evgeny 3 weeks ago
parent
commit
7c8d65db78
  1. 74
      src/chat/chat_channel.c
  2. 19
      src/chat/chat_core.c
  3. 12
      src/chat/chat_core.h
  4. 3
      src/chat/chat_core_priv.h
  5. 1
      src/chat/chat_event.h
  6. 14
      src/chat/chat_sync.c
  7. 4
      src/chat/chat_sync.h
  8. 50
      src/chat/db_sync.c
  9. 2
      src/chat/db_sync.h
  10. 4
      src/chat/member_sync.c
  11. 6
      src/chat/member_sync.h
  12. 20
      src/chat/merkle_sync.c
  13. 6
      src/chat/merkle_sync.h
  14. 49
      src/media_delivery/media_delivery.c
  15. 4
      src/media_delivery/media_delivery.h
  16. 39
      src/media_delivery/media_index.c
  17. 4
      src/media_delivery/media_index.h
  18. 18
      src/routing_layer/topo_group.c
  19. 9
      src/routing_layer/topo_group.h
  20. 5
      src/routing_layer/topo_group_invite.c
  21. 36
      src/routing_layer/topo_node_sqlite.c
  22. 4
      src/routing_layer/topo_node_sqlite.h
  23. 5
      tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt
  24. 2
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt
  25. 2
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt
  26. 27
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt
  27. 28
      tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt
  28. 29
      tools/chatgui-android/jni_bridge/android_jni_bridge.c
  29. 3
      tools/chatgui-android/jni_bridge/android_jni_bridge.h
  30. 29
      tools/chatgui-android/libutun_lite/instance_lite.c

74
src/chat/chat_channel.c

@ -14,7 +14,10 @@
#include "../routing_layer/topo_group.h" #include "../routing_layer/topo_group.h"
#include "../routing_layer/topo_group_connect.h" #include "../routing_layer/topo_group_connect.h"
#include "member_sync.h" #include "member_sync.h"
#include "chat_sync.h"
#include "../transport_layer/secure_channel.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/mem.h"
#include "../../lib/ll_queue.h" #include "../../lib/ll_queue.h"
#include "../../lib/platform_compat.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); } } 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)); 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); char channelId[64]; snprintf(channelId, sizeof(channelId), "%llu", (unsigned long long)chId);
uint8_t sig[64] = {0}; uint8_t sig[64] = {0};
@ -295,3 +300,72 @@ void chat_core_apply_group_autoconnect(void) {
e = e->next; 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);
}

19
src/chat/chat_core.c

@ -69,22 +69,11 @@ int db_exec(const char* sql) {
/* ─── db_sync registry ─── */ /* ─── db_sync registry ─── */
struct DB_SYNC_INSTANCE* si_find(const char* ch_id) { struct DB_SYNC_INSTANCE* si_find(const char* ch_id) {
for (int i = 0; i < g_cc.si_count; i++) if (!g_cc.inst || !g_cc.inst->db_sync || !ch_id || !ch_id[0]) return NULL;
if (strcmp(g_cc.si_ch_id[i], ch_id) == 0) return g_cc.si[i]; return db_sync_instance_find(g_cc.inst, strtoull(ch_id, NULL, 10));
return NULL;
} }
void si_register(struct DB_SYNC_INSTANCE* si, const char* ch_id) { 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); 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); 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; if (!g_cc.initialized) return;
g_cc.initialized = 0; 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); } if (g_cc.db && !g_cc.shared_db) { sqlite3_close(g_cc.db); }
g_cc.db = NULL; g_cc.inst = NULL; g_cc.db = NULL; g_cc.inst = NULL;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: destroyed", CC_ID); DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: destroyed", CC_ID);

12
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(const char* name);
void chat_core_create_channel_auto_trampoline(void* arg); 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); void chat_core_set_my_node_id(uint64_t node_id);

3
src/chat/chat_core_priv.h

@ -81,9 +81,6 @@ struct chat_core_ctx {
char db_path[512]; char db_path[512];
uint8_t shared_db; uint8_t shared_db;
uint64_t my_node_id; uint64_t my_node_id;
struct DB_SYNC_INSTANCE** si;
char** si_ch_id;
int si_count, si_capacity;
uint8_t initialized; uint8_t initialized;
}; };

1
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_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_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_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); typedef void (*chat_event_handler_fn)(int type, const uint8_t* data, int len);

14
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)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 { struct chat_invite {
uint64_t channel_id; uint64_t channel_id;
uint64_t node_id; uint64_t node_id;

4
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 при появлении/изменении сетевой связности. */ Вызывается из auto_socket при появлении/изменении сетевой связности. */
void chat_sync_retry_channels_on_socket_change(struct UTUN_INSTANCE* inst); 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 #ifdef __cplusplus
} }
#endif #endif

50
src/chat/db_sync.c

@ -82,7 +82,7 @@ struct DB_SYNC {
sqlite3* db; sqlite3* db;
uint8_t shared_db; /* db == inst->topo_sqlite_db, do not close */ uint8_t shared_db; /* db == inst->topo_sqlite_db, do not close */
uint64_t last_connected_tb; uint64_t last_connected_tb;
struct DB_SYNC_INSTANCE* instances; struct DB_SYNC_INSTANCE** instances;
int instance_count, instance_capacity; int instance_count, instance_capacity;
void* peer_check_timer; void* peer_check_timer;
void* peer_check_wait; /* standby_wait handle (Android) */ 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) 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++) { 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; 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) 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); DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "count=%d cap=%d", db->instance_count, db->instance_capacity);
if (db->instance_count >= db->instance_capacity) { if (db->instance_count >= db->instance_capacity) {
int nc = db->instance_capacity ? db->instance_capacity * 2 : 4; 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; if (!np) return NULL;
db->instances = np; db->instances = np;
db->instance_capacity = nc; db->instance_capacity = nc;
} }
struct DB_SYNC_INSTANCE* si = &db->instances[db->instance_count++]; struct DB_SYNC_INSTANCE* si = u_calloc(1, sizeof(*si));
memset(si, 0, sizeof(*si)); if (!si) return NULL;
si->db_sync = db; si->db_sync = db;
si->enabled = 1; si->enabled = 1;
db->instances[db->instance_count++] = si;
return 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); 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 // Peer empty → send ALL our data, then SYNC_DONE will be sent from DATA handler when slave responds.
if (peer_ch8 == 0 && sc == 0) { // Если мы тоже пусты (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 vp = (uint32_t)-1; if (sp) sp->verified_pos = vp;
uint32_t sent = 0; uint32_t sent = 0;
while (sent < mc) { while (sent < mc) {
@ -1293,7 +1302,7 @@ static void db_sync_on_conn_up(struct ETCP_CONN* conn, void* arg)
int synced = 0; int synced = 0;
char tbl_list[256] = ""; char tbl_list[256] = "";
for (int i = 0; i < db->instance_count; i++) { 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->enabled) continue;
struct SI_PEER* p = si_peer_add(si, pid); struct SI_PEER* p = si_peer_add(si, pid);
if (!p) continue; 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); DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "peer=%016llx", (unsigned long long)pid);
for (int i = 0; i < db->instance_count; i++) { 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; if (p) p->sync_state = 0;
} }
int any = 0; int any = 0;
for (int i = 0; i < db->instance_count; i++) { for (int i = 0; i < db->instance_count; i++) {
for (int j = 0; j < db->instances[i].peer_count; j++) { 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; } if (db->instances[i]->peers[j].sync_state >= 1) { any = 1; goto cd_done; }
} }
} }
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; if (!db || !db->enabled) return 0;
for (int i = 0; i < db->instance_count; i++) { 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->enabled) continue;
for (int j = 0; j < si->peer_count; j++) for (int j = 0; j < si->peer_count; j++)
if (si->peers[j].sync_state != 2) return 1; 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] = ""; char launched_list[256] = "";
if (g) { if (g) {
for (int i = 0; i < db->instance_count; i++) { 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->enabled) continue;
if (si->sync_gated) 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 now = get_time_tb();
uint64_t to_tb = DB_SYNC_SYNC_TIMEOUT * 10000u; uint64_t to_tb = DB_SYNC_SYNC_TIMEOUT * 10000u;
for (int i = 0; i < db->instance_count; i++) { 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->enabled) continue;
for (int j = 0; j < si->peer_count; j++) { for (int j = 0; j < si->peer_count; j++) {
struct SI_PEER* p = &si->peers[j]; struct SI_PEER* p = &si->peers[j];
@ -1722,10 +1731,11 @@ void db_sync_destroy(struct UTUN_INSTANCE* inst)
#endif #endif
for (int i = 0; i < db->instance_count; i++) { 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->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->recalc.timer) { uasync_cancel_timeout(inst->ua, si->recalc.timer); si->recalc.timer = NULL; }
if (si->peers) u_free(si->peers); if (si->peers) u_free(si->peers);
u_free(si);
} }
etcp_remove_conn_status_cbk(inst, db_sync_on_conn_status, NULL); 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->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; } if (si->peers) { u_free(si->peers); si->peers = NULL; si->peer_count = si->peer_capacity = 0; }
int idx = (int)(si - db->instances); int idx = -1;
if (idx >= 0 && idx < db->instance_count) { 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], memmove(&db->instances[idx], &db->instances[idx + 1],
(db->instance_count - idx - 1) * sizeof(*db->instances)); (db->instance_count - idx - 1) * sizeof(*db->instances));
db->instance_count--; db->instance_count--;
} }
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "instance removed tbl=%s group=%016llx", si->table_name, (unsigned long long)si->group_id); 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 всем синхронизированным пирам // Вставляет подписанную запись: проверяет подпись, вставляет в БД, рассылает PUSH всем синхронизированным пирам

2
src/chat/db_sync.h

@ -98,6 +98,8 @@ void db_sync_destroy(struct UTUN_INSTANCE* inst);
// Instance management // 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); 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); 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) // Data operations (per-instance)
// db_sync_insert_signed: sig MUST be non-NULL, 64 bytes. // db_sync_insert_signed: sig MUST be non-NULL, 64 bytes.

4
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); 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...]. /* Сериализует одного мембера в wire-формат [count:2][member...].
Возвращает 0 и *out_len, или -1 если мембер не найден/нет БД. */ Возвращает 0 и *out_len, или -1 если мембер не найден/нет БД. */
static int _serialize_member(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t member_id, static int _serialize_member(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t member_id,

6
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); 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. * Делегирует в member_sync_apply_record() с from_peer = inst->node_id.

20
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; 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);
}

6
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); 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 ── */ /* ── Recompute tree path after data change ── */
/* /*

49
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); 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) { int media_delivery_bind(struct UTUN_INSTANCE* inst) {
return media_delivery_init(inst); return media_delivery_init(inst);
} }

4
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 для уже скачанных ранее медиа. Идемпотентно на суперузле. */ * block_availability для уже скачанных ранее медиа. Идемпотентно на суперузле. */
void media_delivery_announce_local_blocks(struct UTUN_INSTANCE* inst); 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) */ /* 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_find(struct media_delivery_ctx* md, const uint8_t* block_id);
struct relay_block_ctx* md_relay_add(struct media_delivery_ctx* md, struct relay_block_ctx* md_relay_add(struct media_delivery_ctx* md,

39
src/media_delivery/media_index.c

@ -111,6 +111,45 @@ int media_calc_block_size(uint64_t file_size) {
return (int)target; 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]) { int media_index_generate_uuid(uint8_t uuid_out[16]) {
ma_uuid(uuid_out); ma_uuid(uuid_out);
return 0; return 0;

4
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); void media_index_result_free(struct media_index_result* result);
int media_calc_block_size(uint64_t file_size); 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 #ifdef __cplusplus
} }
#endif #endif

18
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"); 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) { struct TOPO_GROUPS* topo_groups_init(struct UTUN_INSTANCE* instance) {
if (!instance) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "instance is NULL"); return NULL; } 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); DEBUG_INFO(DEBUG_CATEGORY_BGP, "node_id=%016llx", (unsigned long long)instance->node_id);

9
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); 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). * @brief Добавляет conn в senders_list (если нет), отправляет запрос таблицы (nodeinfo).
* *

5
src/routing_layer/topo_group_invite.c

@ -23,6 +23,7 @@
#include "../transport_layer/node_conn_direct.h" #include "../transport_layer/node_conn_direct.h"
#include "ntp_time.h" #include "ntp_time.h"
#include "../chat/chat_core.h" #include "../chat/chat_core.h"
#include "../chat/chat_event.h"
#define TGI_ID "topo_group_invite" #define TGI_ID "topo_group_invite"
#define TGI_TIMEOUT_MS 2000 #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); 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 приглашающего ── */ /* ── верифицируем update_sig приглашающего ── */
{ {
const uint8_t* inv_ed = conn->peer_ed25519_pubkey; const uint8_t* inv_ed = conn->peer_ed25519_pubkey;

36
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; 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_<san> */
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. /* Упрощённый плейсхолдер мембера от topo_group: только node_id + pubkeys, source=1.
* Не участвует в merkle-хеше. Если запись уже есть (полная merkle) — не трогает её. * Не участвует в merkle-хеше. Если запись уже есть (полная merkle) — не трогает её.
* Позже полная merkle-запись дополняет/перезаписывает блоки и сбрасывает source=0. */ * Позже полная merkle-запись дополняет/перезаписывает блоки и сбрасывает source=0. */

4
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* ed25519_pub, const uint8_t* ed25519_priv,
const uint8_t* signature); const uint8_t* signature);
/* Полное локальное удаление канала: DROP peers_<id>/msg_<id>, DELETE channels,
* DELETE merkle_tree_hash WHERE namespace=<channel_id>. 0=ок. */
int topo_node_sqlite_channel_delete(sqlite3* db, const char* channel_id);
/* Упрощённый плейсхолдер мембера от topo_group (source=1, не участвует в merkle-хеше). */ /* Упрощённый плейсхолдер мембера от topo_group (source=1, не участвует в merkle-хеше). */
int topo_node_sqlite_member_placeholder_put(sqlite3* db, const char* channel_id, uint64_t node_id, 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); const uint8_t* x25519_pubkey, const uint8_t* ed25519_pubkey);

5
tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt

@ -67,7 +67,7 @@ class MainActivity : ComponentActivity() {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val showInvite: (Channel) -> Unit = { ch -> 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}") LogManager.addLog("ERROR", "INVITE", "channel id is not numeric: ${ch.id}")
scope.launch { snackbarHostState.showSnackbar("Invalid channel id") } scope.launch { snackbarHostState.showSnackbar("Invalid channel id") }
} else { } else {
@ -111,9 +111,10 @@ class MainActivity : ComponentActivity() {
connected = connected, connected = connected,
isStuck = stuck, isStuck = stuck,
onSelectChannel = { ch -> vm.selectChannel(ch); screen = Screen.Chat }, 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 }, onJoinChannel = { showJoinDialog = true; joinAutoLink = null },
onInviteClick = showInvite, onInviteClick = showInvite,
onDeleteChannel = { vm.deleteChannel(it.id) },
onSettings = { screen = Screen.Settings }, onSettings = { screen = Screen.Settings },
onLogs = { screen = Screen.Logs }, onLogs = { screen = Screen.Logs },
onRefresh = { vm.requestRefresh() } onRefresh = { vm.requestRefresh() }

2
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 transferAdmin(channelId: String, nodeId: Long) = NativeLib.transferAdmin(channelId, nodeId)
fun deleteChannel(channelId: String) = NativeLib.deleteChannel(channelId)
fun getMemberLinks(nodeId: Long): MemberLinks { fun getMemberLinks(nodeId: Long): MemberLinks {
try { try {
val obj = JSONObject(NativeLib.getMemberLinks(nodeId)) val obj = JSONObject(NativeLib.getMemberLinks(nodeId))

2
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) nativeSetMemberFlags(channelId, nodeId, admin, supernode, moder, storage, verified, deleted)
} }
fun transferAdmin(channelId: String, nodeId: Long) { nativeTransferAdmin(channelId, nodeId) } fun transferAdmin(channelId: String, nodeId: Long) { nativeTransferAdmin(channelId, nodeId) }
fun deleteChannel(channelId: String) { nativeDeleteChannel(channelId) }
fun getMemberLinks(nodeId: Long): String = nativeGetMemberLinks(nodeId) ?: "{}" fun getMemberLinks(nodeId: Long): String = nativeGetMemberLinks(nodeId) ?: "{}"
/* ── Local sockets (for auto-mode display) ── */ /* ── 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, private external fun nativeSetMemberFlags(channelId: String, nodeId: Long, admin: Boolean, supernode: Boolean, moder: Boolean, storage: Boolean,
verified: Boolean, deleted: Boolean) verified: Boolean, deleted: Boolean)
private external fun nativeTransferAdmin(channelId: String, nodeId: Long) private external fun nativeTransferAdmin(channelId: String, nodeId: Long)
private external fun nativeDeleteChannel(channelId: String)
private external fun nativeGetMemberLinks(nodeId: Long): String? private external fun nativeGetMemberLinks(nodeId: Long): String?
/* ── Local sockets JNI ── */ /* ── Local sockets JNI ── */

27
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt

@ -34,9 +34,10 @@ fun ChannelListScreen(
connected: Boolean, connected: Boolean,
isStuck: Boolean = false, isStuck: Boolean = false,
onSelectChannel: (Channel) -> Unit, onSelectChannel: (Channel) -> Unit,
onCreateChannel: () -> Unit, onCreateChannel: (String) -> Unit,
onJoinChannel: () -> Unit, onJoinChannel: () -> Unit,
onInviteClick: (Channel) -> Unit, onInviteClick: (Channel) -> Unit,
onDeleteChannel: (Channel) -> Unit,
onSettings: () -> Unit, onSettings: () -> Unit,
onLogs: () -> Unit, onLogs: () -> Unit,
onRefresh: () -> Unit = {} onRefresh: () -> Unit = {}
@ -44,6 +45,7 @@ fun ChannelListScreen(
var showCreateDialog by remember { mutableStateOf(false) } var showCreateDialog by remember { mutableStateOf(false) }
var menuChannelId by remember { mutableStateOf<String?>(null) } var menuChannelId by remember { mutableStateOf<String?>(null) }
var optionsChannel by remember { mutableStateOf<Channel?>(null) } var optionsChannel by remember { mutableStateOf<Channel?>(null) }
var deleteConfirmChannel by remember { mutableStateOf<Channel?>(null) }
LaunchedEffect(Unit) { onRefresh() } LaunchedEffect(Unit) { onRefresh() }
@ -108,6 +110,10 @@ fun ChannelListScreen(
text = { Text("Options") }, text = { Text("Options") },
onClick = { menuChannelId = null; optionsChannel = ch } onClick = { menuChannelId = null; optionsChannel = ch }
) )
DropdownMenuItem(
text = { Text("Delete channel", color = MaterialTheme.colorScheme.error) },
onClick = { menuChannelId = null; deleteConfirmChannel = ch }
)
} }
if (badgeText.isNotEmpty()) { if (badgeText.isNotEmpty()) {
val badgeSize = 18.dp val badgeSize = 18.dp
@ -151,7 +157,7 @@ fun ChannelListScreen(
onDismiss = { showCreateDialog = false }, onDismiss = { showCreateDialog = false },
onCreate = { name -> onCreate = { name ->
showCreateDialog = false showCreateDialog = false
onCreateChannel() onCreateChannel(name)
} }
) )
} }
@ -162,6 +168,23 @@ fun ChannelListScreen(
onDismiss = { optionsChannel = null } 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 @Composable

28
tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt

@ -403,6 +403,20 @@ class ChatViewModel : ViewModel() {
_inviteLink.value = link _inviteLink.value = link
_inviteLinkPending.value = false _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() 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) { fun connectNode(address: String, port: Int, pubkeyHex: String) {
val r = repo ?: return val r = repo ?: return
r.connectNode(address, port, pubkeyHex) r.connectNode(address, port, pubkeyHex)

29
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) { 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 : ""); 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) { 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); 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) * 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); (*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( JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetMemberFlags(
JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId, JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId,
jboolean admin, jboolean supernode, jboolean moder, jboolean storage, jboolean admin, jboolean supernode, jboolean moder, jboolean storage,

3
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); 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) ── */ /* ── Local sockets state (for auto-mode display) ── */
char* utun_bridge_get_local_sockets_json(void); char* utun_bridge_get_local_sockets_json(void);

29
tools/chatgui-android/libutun_lite/instance_lite.c

@ -7,6 +7,7 @@
* *
* Mirrors UtunNode::runLoop() from tools/chatgui/transport/utun_node.cpp:221-372. * 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 "instance_lite.h"
#include "utun_instance.h" #include "utun_instance.h"
#include "config_parser.h" #include "config_parser.h"
@ -34,6 +35,8 @@
#include <stdlib.h> #include <stdlib.h>
#include <unistd.h> #include <unistd.h>
#include <signal.h> #include <signal.h>
#include <unwind.h>
#include <dlfcn.h>
#include <time.h> #include <time.h>
#include <errno.h> #include <errno.h>
#include <openssl/evp.h> #include <openssl/evp.h>
@ -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_caught = 0;
static volatile int g_signal_num = 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) { static void crash_signal_handler(int sig, siginfo_t* info, void* ctx) {
(void)ctx; (void)ctx;
g_signal_caught = 1; g_signal_num = sig; 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); 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); signal(sig, SIG_DFL);
raise(sig); raise(sig);
} }

Loading…
Cancel
Save