Return complete channel snapshots and distinguish database errors from empty history

This commit is contained in:
evgeny committed 2026-10-29 13:35:41 +01:00
1 parent 57227b331d
commit 05ab3b8054
2 files changed
+60 -87

No files matched your search

@@ -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);
}
@@ -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.