From 05ab3b805413bee28b4b3da42659cd0447316879 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 29 Oct 2026 12:03:47 +0100 Subject: [PATCH] Return complete channel snapshots and distinguish database errors from empty history --- .../jni_bridge/android_jni_bridge.c | 144 +++++++----------- .../jni_bridge/android_jni_bridge.h | 3 +- 2 files changed, 60 insertions(+), 87 deletions(-) diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index cdb2630f3..b7eb9aab5 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.c @@ -433,98 +433,70 @@ char* utun_bridge_get_channels_json(void) { struct UTUN_INSTANCE* inst = instance_lite_get_instance(); sqlite3* db = chat_core_get_db(inst); if (!chat_core_is_initialized(inst) || !db) { - bridge_log(BLEV_WARN, "get_channels: not initialized db=%p initialized=%d", - (void*)db, chat_core_is_initialized(inst)); - return u_strdup("[]"); + bridge_log(BLEV_WARN, "get_channels: chat/database unavailable"); return NULL; } - size_t cap = 4096, pos = 1; - char* json = u_malloc(cap); - if (!json) return u_strdup("[]"); - json[0] = '['; - - sqlite3_stmt* st = NULL; - int rc = sqlite3_prepare_v2(db, "SELECT channel_id, name, last_msg_at FROM channels ORDER BY last_msg_at DESC, name ASC", -1, &st, NULL); - if (rc != SQLITE_OK) { u_free(json); return u_strdup("[]"); } - - int first = 1; - while (sqlite3_step(st) == SQLITE_ROW) { - const char* ch_id = (const char*)sqlite3_column_text(st, 0); - const char* name = (const char*)sqlite3_column_text(st, 1); - int64_t last_msg_at = sqlite3_column_int64(st, 2); - if (!ch_id) continue; - - char tbl_msg[80], tbl_peers[80], last_msg[160]; - int last_encrypted = 0; - msg_table_name(ch_id, tbl_msg, sizeof(tbl_msg)); - peers_table_name(ch_id, tbl_peers, sizeof(tbl_peers)); - last_msg[0] = '\0'; - - sqlite3_stmt* lm = NULL; - char sql[256]; - snprintf(sql, sizeof(sql), "SELECT node_id, data FROM \"%s\" ORDER BY id DESC LIMIT 1", tbl_msg); - if (sqlite3_prepare_v2(db, sql, -1, &lm, NULL) == SQLITE_OK) { - if (sqlite3_step(lm) == SQLITE_ROW) { - int64_t author = sqlite3_column_int64(lm, 0); - const uint8_t* dptr = sqlite3_column_blob(lm, 1); - int dlen = sqlite3_column_bytes(lm, 1); - char body[160] = ""; - if (dptr && dlen > 0) { - const char* dp = (const char*)dptr; - char ct[32] = "text/plain"; - const char* cts = strstr(dp, "\"ct\":\""); - if (cts) { cts += 6; char* cte = strchr((char*)cts, '"'); if (cte) { size_t tlc = (size_t)(cte - cts); if (tlc < sizeof(ct)) { memcpy(ct, cts, tlc); ct[tlc] = '\0'; } } } - last_encrypted = !strcmp(ct, ENCRYPTED_ATTACHMENT_TYPE); - const char* ds = strstr(dp, "\"d\":\""); - if (ds) { ds += 5; char* de = strchr((char*)ds, '"'); if (de) { size_t tl = (size_t)(de - ds); if (tl > 511) tl = 511; char d[512]; memcpy(d, ds, tl); d[tl] = '\0'; bridge_format_preview_body(ct, d, body, sizeof(body)); } } - } - char author_name[128]; - bridge_get_node_name(db, (uint64_t)author, ch_id, author_name, sizeof(author_name)); - if (!author_name[0]) - snprintf(author_name, sizeof(author_name), "0x%016llx", (unsigned long long)author); - snprintf(last_msg, sizeof(last_msg), "%s: %s", author_name, body); - } - sqlite3_finalize(lm); + struct strbuf out = strbuf_new(); + sqlite3_stmt *st = NULL, *last = NULL, *peers = NULL; + int rc = sqlite3_prepare_v2(db, "SELECT channel_id,name,last_msg_at FROM channels ORDER BY last_msg_at DESC,name ASC", -1, &st, NULL); + if (rc != SQLITE_OK || strbuf_addf(&out, "[") < 0) goto failed; + unsigned count = 0; + while ((rc = sqlite3_step(st)) == SQLITE_ROW) { + const char* channel = (const char*)sqlite3_column_text(st, 0); + const char* name = (const char*)sqlite3_column_text(st, 1); + if (!channel || !channel[0]) { bridge_log(BLEV_ERROR, "get_channels: invalid stored channel ID"); goto failed; } + char messages_table[80], peers_table[80], sql[512], preview[160] = ""; + msg_table_name(channel, messages_table, sizeof(messages_table)); + peers_table_name(channel, peers_table, sizeof(peers_table)); + snprintf(sql, sizeof(sql), "SELECT node_id," + "CASE WHEN json_valid(CAST(data AS TEXT)) THEN json_extract(CAST(data AS TEXT),'$.ct') END," + "CASE WHEN json_valid(CAST(data AS TEXT)) THEN json_extract(CAST(data AS TEXT),'$.d') END" + " FROM \"%s\" ORDER BY id DESC LIMIT 1", messages_table); + if (sqlite3_prepare_v2(db, sql, -1, &last, NULL) != SQLITE_OK) goto failed; + int last_rc = sqlite3_step(last), encrypted = 0; + if (last_rc == SQLITE_ROW) { + uint64_t author = (uint64_t)sqlite3_column_int64(last, 0); + const char* type = (const char*)sqlite3_column_text(last, 1); + const char* data = (const char*)sqlite3_column_text(last, 2); + char body[160] = "", author_name[128]; + encrypted = type && !strcmp(type, ENCRYPTED_ATTACHMENT_TYPE); + bridge_format_preview_body(type, data ? data : "", body, sizeof(body)); + bridge_get_node_name(db, author, channel, author_name, sizeof(author_name)); + if (!author_name[0]) snprintf(author_name, sizeof(author_name), "0x%016llx", (unsigned long long)author); + snprintf(preview, sizeof(preview), "%s: %s", author_name, body); + } else if (last_rc != SQLITE_DONE) goto failed; + sqlite3_finalize(last); last = NULL; + snprintf(sql, sizeof(sql), "SELECT DISTINCT node_id FROM \"%s\"", peers_table); + if (sqlite3_prepare_v2(db, sql, -1, &peers, NULL) != SQLITE_OK) goto failed; + int online = 0, peer_rc; + while ((peer_rc = sqlite3_step(peers)) == SQLITE_ROW) { + uint64_t node = (uint64_t)sqlite3_column_int64(peers, 0); + if (node == inst->node_id) continue; + struct ETCP_CONN* connection = instance_find_conn(inst, node); + if (connection && connection->initialized && connection->links_up) online++; } - - int online = 0; - uint64_t my_id = inst->node_id; - sqlite3_stmt* pc = NULL; - snprintf(sql, sizeof(sql), "SELECT DISTINCT node_id FROM \"%s\"", tbl_peers); - if (sqlite3_prepare_v2(db, sql, -1, &pc, NULL) == SQLITE_OK) { - while (sqlite3_step(pc) == SQLITE_ROW) { - uint64_t nid = (uint64_t)sqlite3_column_int64(pc, 0); - if (nid == my_id) continue; - if (inst) { struct ETCP_CONN* c = instance_find_conn(inst, nid); if (c && c->initialized && c->links_up) online++; } - else online++; - } - sqlite3_finalize(pc); - } - - char* esc_name = json_escape_alloc(name ? name : ch_id); - char* esc_msg = json_escape_alloc(last_msg); - const char* sep = first ? "" : ","; - first = 0; - - size_t needed = snprintf(NULL, 0, "%s{\"id\":\"%s\",\"name\":\"%s\",\"lastMsgAt\":%lld,\"lastMsg\":\"%s\"," - "\"peersOnline\":%d,\"lastMsgEncrypted\":%s}", sep, ch_id, esc_name, (long long)last_msg_at, esc_msg, online, - last_encrypted ? "true" : "false"); - while (pos + needed + 2 > cap) { char* tmp = u_realloc(json, cap * 2); if (!tmp) break; json = tmp; cap *= 2; } - if (pos + needed + 2 <= cap) - pos += (size_t)snprintf(json + pos, cap - pos, "%s{\"id\":\"%s\",\"name\":\"%s\",\"lastMsgAt\":%lld,\"lastMsg\":\"%s\"," - "\"peersOnline\":%d,\"lastMsgEncrypted\":%s}", sep, ch_id, esc_name, (long long)last_msg_at, esc_msg, online, - last_encrypted ? "true" : "false"); - u_free(esc_msg); - u_free(esc_name); + if (peer_rc != SQLITE_DONE) goto failed; + sqlite3_finalize(peers); peers = NULL; + if (strbuf_addf(&out, "%s{\"id\":", count++ ? "," : "") < 0 || + strbuf_add_json_string(&out, channel) < 0 || strbuf_addf(&out, ",\"name\":") < 0 || + strbuf_add_json_string(&out, name ? name : channel) < 0 || + strbuf_addf(&out, ",\"lastMsgAt\":%lld,\"lastMsg\":", (long long)sqlite3_column_int64(st, 2)) < 0 || + strbuf_add_json_string(&out, preview) < 0 || + strbuf_addf(&out, ",\"peersOnline\":%d,\"lastMsgEncrypted\":%s}", online, encrypted ? "true" : "false") < 0) goto failed; } + if (rc != SQLITE_DONE || strbuf_addf(&out, "]") < 0) goto failed; sqlite3_finalize(st); - - json[pos++] = ']'; json[pos] = '\0'; - return json; + return strbuf_detach(&out); +failed: + bridge_log(BLEV_ERROR, "get_channels: complete snapshot failed: %s", sqlite3_errmsg(db)); + sqlite3_finalize(last); sqlite3_finalize(peers); sqlite3_finalize(st); strbuf_free(&out); + return NULL; } char* utun_bridge_get_messages_json(const char* channel_id, int limit, const uint8_t* anchor, int exact) { struct UTUN_INSTANCE* inst = instance_lite_get_instance(); sqlite3* db = chat_core_get_db(inst); - if (!chat_core_is_initialized(inst) || !db || !channel_id || !channel_id[0]) return u_strdup("[]"); + if (!chat_core_is_initialized(inst) || !db || !channel_id || !channel_id[0]) { + bridge_log(BLEV_WARN, "get_messages: chat/database/channel unavailable"); return NULL; + } char table[80], sql[2048], selection[1024]; msg_table_name(channel_id, table, sizeof(table)); if (exact && anchor) snprintf(selection, sizeof(selection), "SELECT * FROM \"%s\" WHERE message_id=?2", table); else if (anchor) snprintf(selection, sizeof(selection), @@ -544,7 +516,7 @@ char* utun_bridge_get_messages_json(const char* channel_id, int limit, const uin " ORDER BY m.timestamp,m.message_id", selection); sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) { - DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: messages query ch=%s: %s", channel_id, sqlite3_errmsg(db)); return u_strdup("[]"); + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: messages query ch=%s: %s", channel_id, sqlite3_errmsg(db)); return NULL; } sqlite3_bind_int(st, 1, limit > 0 ? limit : 50); if (anchor) sqlite3_bind_blob(st, 2, anchor, 32, SQLITE_STATIC); @@ -656,7 +628,7 @@ char* utun_bridge_get_messages_json(const char* channel_id, int limit, const uin sqlite3_finalize(st); if (result < 0 || step != SQLITE_DONE || strbuf_addf(&out, "]") < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: messages snapshot failed ch=%s: %s", channel_id, sqlite3_errmsg(db)); - strbuf_free(&out); return u_strdup("[]"); + strbuf_free(&out); return NULL; } return strbuf_detach(&out); } diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.h b/tools/chatgui-android/jni_bridge/android_jni_bridge.h index 11dc157ad..f5c8bb444 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.h +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.h @@ -86,7 +86,8 @@ void utun_bridge_connect_channel(const char* channel_id); /* Однократная попытка подключения к конкретному узлу канала (ручной "Connect"). */ void utun_bridge_connect_member_node(const char* channel_id, uint64_t node_id); -/* Синхронные JSON-снимки; память освобождать u_free. */ +/* Синхронные JSON-снимки в потоке ядра; память освобождать u_free. + * [] — подтверждённый пустой снимок, NULL — ошибка SQL/памяти/неинициализированное ядро; частичного результата нет. */ char* utun_bridge_get_channels_json(void); /* limit<=0 → 50; anchor=NULL — последние сообщения, иначе окно вокруг 32-байтового message_id.