#include "dm_history.h" #include "dm_core.h" #include "dm_crypto.h" #include "../utun_instance.h" #include "../../lib/debug_config.h" #include #include #include #include #include static int history_error(struct UTUN_INSTANCE* inst, const char* operation, const char* conv) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM history: %s conv=%s sql=%s", operation, conv ? conv : "", inst && inst->topo_sqlite_db ? sqlite3_errmsg(inst->topo_sqlite_db) : "database unavailable"); return -1; } int dm_history_init(struct UTUN_INSTANCE* inst) { const char* sql = "CREATE TABLE IF NOT EXISTS dm_history(conv_id TEXT PRIMARY KEY,generation INTEGER NOT NULL,hidden INTEGER NOT NULL);" "CREATE TABLE IF NOT EXISTS dm_deleted_messages(conv_id TEXT NOT NULL,dir INTEGER NOT NULL,seq INTEGER NOT NULL," "body_hash BLOB NOT NULL,media_id BLOB,media_receipt BLOB,PRIMARY KEY(conv_id,dir,seq));" "CREATE TABLE IF NOT EXISTS dm_local_files(path TEXT PRIMARY KEY,conv_id TEXT NOT NULL,generation INTEGER NOT NULL," "busy INTEGER NOT NULL DEFAULT 0,retry_at INTEGER NOT NULL DEFAULT 0);" "UPDATE dm_local_files SET busy=0;"; if (sqlite3_exec(inst->topo_sqlite_db, sql, NULL, NULL, NULL) != SQLITE_OK) return history_error(inst, "init", NULL); DEBUG_INFO(DEBUG_CATEGORY_DM, "DM history: initialized persistent generations, replay records and file cleanup"); return 0; } uint64_t dm_history_generation(struct UTUN_INSTANCE* inst, const char* conv) { sqlite3_stmt* st = NULL; if (!inst || !inst->topo_sqlite_db || !conv || sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT COALESCE(h.generation,1) FROM dm_conversations c LEFT JOIN dm_history h USING(conv_id) WHERE c.conv_id=?", -1, &st, NULL) != SQLITE_OK) { history_error(inst, "generation prepare", conv); return 0; } sqlite3_bind_text(st, 1, conv, -1, SQLITE_STATIC); int rc = sqlite3_step(st); uint64_t generation = rc == SQLITE_ROW ? (uint64_t)sqlite3_column_int64(st, 0) : 0; if (rc != SQLITE_ROW && rc != SQLITE_DONE) history_error(inst, "generation read", conv); sqlite3_finalize(st); return generation; } int dm_history_current(struct UTUN_INSTANCE* inst, const char* conv, uint64_t generation) { sqlite3_stmt* st = NULL; if (!inst || !inst->topo_sqlite_db || !conv || !generation || sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT 1 FROM dm_conversations c LEFT JOIN dm_history h USING(conv_id)" " WHERE c.conv_id=? AND COALESCE(h.generation,1)=? AND COALESCE(h.hidden,0)=0", -1, &st, NULL) != SQLITE_OK) { history_error(inst, "current prepare", conv); return 0; } sqlite3_bind_text(st, 1, conv, -1, SQLITE_STATIC); sqlite3_bind_int64(st, 2, (sqlite3_int64)generation); int rc = sqlite3_step(st), current = rc == SQLITE_ROW; if (rc != SQLITE_ROW && rc != SQLITE_DONE) history_error(inst, "current read", conv); sqlite3_finalize(st); if (!current) DEBUG_DEBUG(DEBUG_CATEGORY_DM, "DM history: stale/hidden target conv=%s generation=%llu", conv, (unsigned long long)generation); return current; } int dm_history_show(struct UTUN_INSTANCE* inst, const char* conv) { sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(inst->topo_sqlite_db, "UPDATE dm_history SET hidden=0 WHERE conv_id=?", -1, &st, NULL) != SQLITE_OK) return history_error(inst, "show prepare", conv); sqlite3_bind_text(st, 1, conv, -1, SQLITE_STATIC); int rc = sqlite3_step(st); sqlite3_finalize(st); if (rc != SQLITE_DONE) return history_error(inst, "show", conv); return 0; } int dm_history_deleted(struct UTUN_INSTANCE* inst, const uint8_t* body, size_t len, int dir, uint8_t* media_receipt) { uint64_t conv, seq; char id[64]; if (media_receipt) memset(media_receipt, 0, DM_MEDIA_RECEIPT_SIZE); size_t actual; if (!dm_msg_len(body, len, &actual) || actual != len) return history_error(inst, "invalid replay body", NULL); memcpy(&conv, body, 8); memcpy(&seq, body + 8, 8); snprintf(id, sizeof(id), "%llu", (unsigned long long)conv); sqlite3_stmt* st = NULL; if (sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT body_hash,media_receipt FROM dm_deleted_messages WHERE conv_id=? AND dir=? AND seq=?", -1, &st, NULL) != SQLITE_OK) return history_error(inst, "replay prepare", id); sqlite3_bind_text(st, 1, id, -1, SQLITE_STATIC); sqlite3_bind_int(st, 2, dir); sqlite3_bind_int64(st, 3, (sqlite3_int64)seq); int rc = sqlite3_step(st), result = rc == SQLITE_DONE ? 0 : -1; if (rc == SQLITE_ROW) { uint8_t hash[32]; SHA256(body, len, hash); if (sqlite3_column_bytes(st, 0) == sizeof(hash) && !memcmp(hash, sqlite3_column_blob(st, 0), sizeof(hash))) { result = 1; if (media_receipt && sqlite3_column_bytes(st, 1) == DM_MEDIA_RECEIPT_SIZE) memcpy(media_receipt, sqlite3_column_blob(st, 1), DM_MEDIA_RECEIPT_SIZE); DEBUG_DEBUG(DEBUG_CATEGORY_DM, "DM history: deleted replay conv=%s dir=%d seq=%llu", id, dir, (unsigned long long)seq); } else DEBUG_WARN(DEBUG_CATEGORY_DM, "DM history: conflicting deleted message conv=%s dir=%d seq=%llu", id, dir, (unsigned long long)seq); } else if (rc != SQLITE_DONE) history_error(inst, "replay read", id); sqlite3_finalize(st); return result; } /* Каноническое тело восстанавливается из сохранённых полей; у file оно уже есть в dm_files. */ static int history_remember(struct UTUN_INSTANCE* inst, const char* conv) { sqlite3_stmt* rows = NULL; sqlite3_stmt* put = NULL; const char* sql = "SELECT m.dir,m.seq,m.ts,m.author,m.ct,m.data,m.sig,f.body,f.receipt" " FROM dm_messages m 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=?"; if (sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &rows, NULL) != SQLITE_OK || sqlite3_prepare_v2(inst->topo_sqlite_db, "INSERT INTO dm_deleted_messages(conv_id,dir,seq,body_hash,media_id,media_receipt) VALUES(?,?,?,?,?,?)", -1, &put, NULL) != SQLITE_OK) goto fail; sqlite3_bind_text(rows, 1, conv, -1, SQLITE_STATIC); int rc; while ((rc = sqlite3_step(rows)) == SQLITE_ROW) { uint8_t body[DM_MSG_FIXED_HDR + 255 + 2 + DM_DATA_MAX + 16 + DM_MEDIA_DESC_SIZE + DM_MSG_SIG_SIZE], hash[32]; size_t length = 0; uint64_t number = strtoull(conv, NULL, 10), seq = (uint64_t)sqlite3_column_int64(rows, 1); uint64_t ts = (uint64_t)sqlite3_column_int64(rows, 2), author = (uint64_t)sqlite3_column_int64(rows, 3); int ct = sqlite3_column_bytes(rows, 4), data = sqlite3_column_bytes(rows, 5), dir = sqlite3_column_int(rows, 0); const char* type = (const char*)sqlite3_column_text(rows, 4); if (ct == 4 && type && !memcmp(type, "file", 4)) { int bytes = sqlite3_column_bytes(rows, 7); if (bytes <= 0 || bytes > sizeof(body)) goto fail; length = (size_t)bytes; memcpy(body, sqlite3_column_blob(rows, 7), length); } else { if (ct > 255 || data < 16 || data > DM_DATA_MAX + 16 || sqlite3_column_bytes(rows, 6) != 64) goto fail; memcpy(body, &number, 8); memcpy(body + 8, &seq, 8); memcpy(body + 16, &ts, 8); memcpy(body + 24, &author, 8); body[32] = (uint8_t)ct; memcpy(body + 33, type, (size_t)ct); uint16_t size = (uint16_t)data; memcpy(body + 33 + ct, &size, 2); memcpy(body + 35 + ct, sqlite3_column_blob(rows, 5), (size_t)data); memcpy(body + 35 + ct + data, sqlite3_column_blob(rows, 6), 64); length = (size_t)(35 + ct + data + 64); } size_t actual; if (!dm_msg_len(body, length, &actual) || actual != length) goto fail; SHA256(body, length, hash); sqlite3_reset(put); sqlite3_clear_bindings(put); sqlite3_bind_text(put, 1, conv, -1, SQLITE_STATIC); sqlite3_bind_int(put, 2, dir); sqlite3_bind_int64(put, 3, (sqlite3_int64)seq); sqlite3_bind_blob(put, 4, hash, sizeof(hash), SQLITE_TRANSIENT); const uint8_t* media = dm_message_media(body, length); if (media) sqlite3_bind_blob(put, 5, media, 16, SQLITE_TRANSIENT); if (sqlite3_column_bytes(rows, 8) == DM_MEDIA_RECEIPT_SIZE) sqlite3_bind_blob(put, 6, sqlite3_column_blob(rows, 8), DM_MEDIA_RECEIPT_SIZE, SQLITE_TRANSIENT); if (sqlite3_step(put) != SQLITE_DONE) goto fail; } if (rc != SQLITE_DONE) goto fail; sqlite3_finalize(rows); sqlite3_finalize(put); return 0; fail: sqlite3_finalize(rows); sqlite3_finalize(put); return history_error(inst, "remember deleted bodies", conv); } int dm_history_delete(struct UTUN_INSTANCE* inst, const char* conv, uint64_t generation) { if (!inst || !inst->dm || !inst->dm_media || generation >= INT64_MAX || !dm_history_current(inst, conv, generation)) { DEBUG_WARN(DEBUG_CATEGORY_DM, "DM history: delete rejected conv=%s generation=%llu", conv ? conv : "", (unsigned long long)generation); return -1; } sqlite3* db = inst->topo_sqlite_db; sqlite3_stmt* st = NULL; if (sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) return history_error(inst, "begin delete", conv); if (history_remember(inst, conv) || dm_media_forget_prepare(inst, conv, generation)) goto fail; const char* statements[] = { "DELETE FROM dm_outbox WHERE conv_id=?", "DELETE FROM dm_messages WHERE conv_id=?", "INSERT INTO dm_history(conv_id,generation,hidden) VALUES(?,?,1)" " ON CONFLICT(conv_id) DO UPDATE SET generation=excluded.generation,hidden=1" }; for (size_t i = 0; i < sizeof(statements) / sizeof(statements[0]); i++) { if (sqlite3_prepare_v2(db, statements[i], -1, &st, NULL) != SQLITE_OK) goto fail; sqlite3_bind_text(st, 1, conv, -1, SQLITE_STATIC); if (i == 2) sqlite3_bind_int64(st, 2, (sqlite3_int64)(generation + 1)); int rc = sqlite3_step(st); sqlite3_finalize(st); st = NULL; if (rc != SQLITE_DONE) goto fail; } if (sqlite3_exec(db, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) goto fail; DEBUG_INFO(DEBUG_CATEGORY_DM, "DM history: deleted conv=%s generation=%llu -> %llu; counters retained", conv, (unsigned long long)generation, (unsigned long long)(generation + 1)); dm_media_forget_committed(inst, conv); dm_cancel_pending_history(inst, conv); return 0; fail: history_error(inst, "delete rolled back", conv); sqlite3_finalize(st); if (sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL) != SQLITE_OK) history_error(inst, "rollback delete", conv); return -1; }