From 1cdead18de1931541dd0dcdf7f3baf49c974d2c2 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 1 Oct 2026 15:59:38 +0300 Subject: [PATCH] Expose bounded DM history snapshots with text and media delivery state --- src/dm/dm_core.c | 108 ++++++++++++++++++++++++----------------------- 1 file changed, 56 insertions(+), 52 deletions(-) diff --git a/src/dm/dm_core.c b/src/dm/dm_core.c index fe48f544..fcc7f351 100644 --- a/src/dm/dm_core.c +++ b/src/dm/dm_core.c @@ -26,6 +26,7 @@ #include "../../lib/ll_queue.h" #include "../../lib/debug_config.h" #include "../../lib/platform_compat.h" +#include "../../lib/strbuf.h" #include #include @@ -563,6 +564,7 @@ int dm_accept_receipt(struct UTUN_INSTANCE* inst, const uint8_t receipt[DM_RECEI if (rc != SQLITE_DONE) goto fail; sqlite3_finalize(st); DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: delivery confirmed conv=%s seq=%llu", DM_ID, conv_id, (unsigned long long)seq); + dm_media_notify(inst, conv); return 0; fail: DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: receipt database error rc=%d sql=%s", DM_ID, rc, sqlite3_errmsg(dm->db)); @@ -892,64 +894,66 @@ int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int count, int offset, char* buf, size_t buf_size, size_t* out_len) { struct dm_state* dm = dm_of(inst); - if (!conv_id || !buf || !out_len || !dm || !dm->db) return -1; - + if (!conv_id || !buf || !out_len || !dm || !dm->db || offset < 0) { + DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: invalid messages snapshot request", DM_ID); + return -1; + } + *out_len = 0; struct dm_conv c; if (dm_conv_load(dm, conv_id, &c) != 0) return -1; - - char sql[256]; - if (count > 0) - snprintf(sql, sizeof(sql), - "SELECT dir,seq,ts,author,ct,data FROM dm_messages WHERE conv_id=? ORDER BY ts, seq LIMIT %d OFFSET %d", - count, offset); - else - snprintf(sql, sizeof(sql), - "SELECT dir,seq,ts,author,ct,data FROM dm_messages WHERE conv_id=? ORDER BY ts, seq"); - - int off = 0; - off += snprintf(buf + off, buf_size - (size_t)off, "["); + /* Ограниченный снимок не блокирует uasync на длинной истории. */ + if (count <= 0 || count > 200) count = 200; + const char* sql = "SELECT * FROM (SELECT m.dir,m.seq,m.ts,m.author,m.ct,m.data,o.seq IS NOT NULL AS pending," + "COALESCE(f.ready,0) AS ready,COALESCE(f.downloading,0) AS downloading,f.receipt," + "COALESCE(f.local_path,'') AS path,COALESCE(f.plain_size,0) AS size" + " FROM dm_messages m LEFT JOIN dm_outbox o ON m.dir=1 AND o.conv_id=m.conv_id AND o.seq=m.seq" + " LEFT JOIN dm_files f ON f.conv_id=m.conv_id AND f.dir=m.dir AND f.seq=m.seq" + " WHERE m.conv_id=? ORDER BY m.ts DESC,m.seq DESC,m.dir DESC LIMIT ? OFFSET ?) ORDER BY ts,seq,dir"; + struct strbuf sb = strbuf_new(); uint64_t conv_num = (uint64_t)strtoull(conv_id, NULL, 10); sqlite3_stmt* st = NULL; - if (sqlite3_prepare_v2(dm->db, sql, -1, &st, NULL) == SQLITE_OK) { - sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC); - int first = 1; - while (sqlite3_step(st) == SQLITE_ROW) { - int dir = sqlite3_column_int(st, 0); - uint64_t seq = (uint64_t)sqlite3_column_int64(st, 1); - uint64_t ts = (uint64_t)sqlite3_column_int64(st, 2); - uint64_t author = (uint64_t)sqlite3_column_int64(st, 3); - const char* ct = (const char*)sqlite3_column_text(st, 4); - const uint8_t* enc = (const uint8_t*)sqlite3_column_blob(st, 5); - int enc_len = sqlite3_column_bytes(st, 5); - /* расшифровать (nonce детерминирован по conv_id+author+seq) */ - char text[DM_DATA_MAX + 1]; - text[0] = '\0'; - if (enc && enc_len >= (int)DM_TAG_SIZE) { - uint8_t nonce[DM_NONCE_SIZE]; - dm_build_nonce(conv_num, author, seq, nonce); - uint8_t* plain = u_malloc((size_t)enc_len); - size_t plen = 0; - if (dm_decrypt(c.content_key, nonce, enc, (size_t)enc_len, plain, &plen) == 0) { - if (plen >= sizeof(text)) plen = sizeof(text) - 1; - memcpy(text, plain, plen); - text[plen] = '\0'; - } - u_free(plain); - } - char esc_ct[64], esc_data[DM_DATA_MAX * 2 + 8]; - json_escape(ct ? ct : "", esc_ct, sizeof(esc_ct)); - json_escape(text, esc_data, sizeof(esc_data)); - off += snprintf(buf + off, buf_size - (size_t)off, - "%s{\"dir\":%d,\"seq\":%llu,\"ts\":%llu,\"author\":%llu,\"ct\":\"%s\",\"data\":\"%s\"}", - first ? "" : ",", dir, (unsigned long long)seq, (unsigned long long)ts, - (unsigned long long)author, esc_ct, esc_data); - first = 0; - } + if (sqlite3_prepare_v2(dm->db, sql, -1, &st, NULL) != SQLITE_OK || strbuf_addf(&sb, "[") < 0) goto fail; + sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC); + sqlite3_bind_int(st, 2, count); sqlite3_bind_int(st, 3, offset); + int first = 1, rc; + while ((rc = sqlite3_step(st)) == SQLITE_ROW) { + int dir = sqlite3_column_int(st, 0); + uint64_t seq = (uint64_t)sqlite3_column_int64(st, 1), ts = (uint64_t)sqlite3_column_int64(st, 2); + uint64_t author = (uint64_t)sqlite3_column_int64(st, 3); + const char* ct = (const char*)sqlite3_column_text(st, 4); + const uint8_t* enc = sqlite3_column_blob(st, 5); + int enc_len = sqlite3_column_bytes(st, 5); + char text[DM_DATA_MAX + 1]; + size_t plen = 0; + uint8_t nonce[DM_NONCE_SIZE]; + dm_build_nonce(conv_num, author, seq, nonce); + if (enc_len < DM_TAG_SIZE || enc_len > DM_DATA_MAX + DM_TAG_SIZE || + dm_decrypt(c.content_key, nonce, enc, (size_t)enc_len, (uint8_t*)text, &plen)) goto fail; + text[plen] = '\0'; + int ready = sqlite3_column_int(st, 7), downloading = sqlite3_column_int(st, 8); + int delivered = sqlite3_column_bytes(st, 9) == DM_MEDIA_RECEIPT_SIZE; + const char* path = ready == 1 ? (const char*)sqlite3_column_text(st, 10) : ""; + const char* status = delivered && dir ? "delivered" : ready == 1 ? "ready" : downloading ? "downloading" : + ready < 0 ? "expired" : "pending"; + char esc_ct[512], esc_data[DM_DATA_MAX * 2 + 8], esc_path[2048]; + json_escape(ct ? ct : "", esc_ct, sizeof(esc_ct)); json_escape(text, esc_data, sizeof(esc_data)); + json_escape(path, esc_path, sizeof(esc_path)); + if (strbuf_addf(&sb, "%s{\"dir\":%d,\"seq\":%llu,\"ts\":%llu,\"author\":\"%llu\",\"ct\":\"%s\",\"data\":\"%s\"," + "\"pending\":%s,\"media_status\":\"%s\",\"media_path\":\"%s\",\"media_size\":%llu}", first ? "" : ",", dir, + (unsigned long long)seq, (unsigned long long)ts, (unsigned long long)author, esc_ct, esc_data, + sqlite3_column_int(st, 6) ? "true" : "false", status, esc_path, + (unsigned long long)sqlite3_column_int64(st, 11)) < 0) goto fail; + first = 0; } - if (st) sqlite3_finalize(st); - off += snprintf(buf + off, buf_size - (size_t)off, "]"); - *out_len = (size_t)off; + if (rc != SQLITE_DONE || strbuf_addf(&sb, "]") < 0 || sb.len + 1 > buf_size) goto fail; + memcpy(buf, strbuf_str(&sb), sb.len + 1); *out_len = sb.len; + sqlite3_finalize(st); strbuf_free(&sb); return 0; +fail: + DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: message snapshot failed conv=%s buffer=%zu required=%zu sql=%s", DM_ID, + conv_id, buf_size, sb.len + 1, sqlite3_errmsg(dm->db)); + sqlite3_finalize(st); strbuf_free(&sb); + return -1; } /* ── трамплины для gui_bridge (GUI → uasync) ── */