dm: expose delivery facts and archived conversation state to clients

This commit is contained in:
evgeny committed 2026-11-03 13:21:41 +01:00
1 parent 445ead109c
commit d0f7b2a364
3 files changed
+39 -8

No files matched your search

+23 -7
View File
@@ -1144,7 +1144,7 @@ int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf
int failed = strbuf_addf(&json, "[") < 0;
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db,
"SELECT c.conv_id,peer_node_id,COALESCE(peer_name,''),last_ts,last_out_seq,last_in_seq,COALESCE(h.generation,1),mode,closed"
"SELECT c.conv_id,peer_node_id,COALESCE(peer_name,''),last_ts,last_out_seq,last_in_seq,COALESCE(h.generation,1),mode,closed,peer_party"
" FROM dm_conversations c LEFT JOIN dm_history h USING(conv_id) WHERE COALESCE(h.hidden,0)=0"
" ORDER BY last_ts DESC LIMIT 200",
-1, &st, NULL) == SQLITE_OK) {
@@ -1158,6 +1158,14 @@ int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf
uint64_t out_seq = (uint64_t)sqlite3_column_int64(st, 4);
uint64_t in_seq = (uint64_t)sqlite3_column_int64(st, 5);
char resolved[128];
struct dm_party party;
struct devgroup_record record;
struct devgroup_profile profile;
if (sqlite3_column_bytes(st, 9) == DM_PARTY_WIRE_SIZE &&
!dm_party_decode(sqlite3_column_blob(st, 9), &party) && party.kind == DM_PARTY_GROUP &&
devgroup_latest(dm->db, party.ed, &record, &profile) == 1) {
snprintf(resolved, sizeof(resolved), "%s", profile.name); name = resolved; name_len = strlen(resolved);
}
if (!name || !name_len) {
resolved[0] = '\0';
chat_core_get_node_name(inst, peer, resolved, sizeof(resolved));
@@ -1208,7 +1216,12 @@ int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int c
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"
"COALESCE(f.local_path,'') AS path,COALESCE(f.plain_size,0) AS size,"
"(SELECT count(*) FROM dm_delivery d WHERE d.conv_id=m.conv_id AND d.author=m.author AND d.seq=m.seq) AS targets,"
"(SELECT count(*) FROM dm_delivery d WHERE d.conv_id=m.conv_id AND d.author=m.author AND d.seq=m.seq AND d.receipt IS NOT NULL) AS delivered,"
"(SELECT count(*) FROM dm_file_targets t WHERE t.id=f.id) AS file_targets,"
"(SELECT count(*) FROM dm_file_targets t WHERE t.id=f.id AND t.received IS NOT NULL) AS received,"
"(SELECT count(*) FROM dm_file_targets t WHERE t.id=f.id AND t.taken IS NOT NULL) AS opened"
" FROM dm_messages m LEFT JOIN dm_outbox o ON m.author=?4 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.author=m.author AND f.seq=m.seq"
" WHERE m.conv_id=?1 ORDER BY m.ts DESC,m.seq DESC,m.author DESC LIMIT ?2 OFFSET ?3) ORDER BY ts,seq,author";
@@ -1254,10 +1267,11 @@ int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int c
snprintf(text, sizeof(text), "%s", attachment.name);
}
int ready = sqlite3_column_int(st, 7), downloading = sqlite3_column_int(st, 8);
int delivered = sqlite3_column_bytes(st, 9) == DM_MEDIA_RECEIPT_SIZE;
int file_total = sqlite3_column_int(st, 14), file_received = sqlite3_column_int(st, 15);
int delivered = file_total > 0 && file_received == file_total && author == inst->node_id;
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";
const char* status = delivered ? "delivered" : ready == 1 ? "ready" : c.closed ? "unavailable" :
downloading ? "downloading" : ready < 0 ? "expired" : "pending";
char esc_ct[512], esc_data[DM_DATA_MAX * 2 + 8], esc_group[DM_DATA_MAX * 2 + 8], esc_path[2048];
json_escape(group_name, esc_group, sizeof(esc_group));
const char* display_data = invite ? text + strlen(group_name) + 1 : text;
@@ -1265,11 +1279,13 @@ int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int c
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,"
"\"tags\":{\"group_name\":\"%s\"},"
"\"delivery_total\":%d,\"delivery_received\":%d,\"file_total\":%d,\"file_received\":%d,\"file_opened\":%d,"
"\"source\":%s,\"tags\":{\"group_name\":\"%s\"},"
"\"content_type\":\"%s\",\"duration_ms\":%u,\"width\":%u,\"height\":%u,\"waveform\":\"%s\"}", 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), esc_group,
(unsigned long long)sqlite3_column_int64(st, 11), sqlite3_column_int(st, 12), sqlite3_column_int(st, 13),
file_total, file_received, sqlite3_column_int(st, 16), author == inst->node_id ? "true" : "false", esc_group,
file ? attachment_content_type(attachment.kind) : "text/plain",
attachment.duration_ms, attachment.width, attachment.height, attachment.waveform) < 0) goto fail;
first = 0;
+13 -1
View File
@@ -9,6 +9,8 @@
#include <QCoreApplication>
#include "chat/chat_poll.h"
#include "chat/group_avatar.h"
#include "dm/dm_identity.h"
#include "routing_layer/devgroup.h"
/* ── helpers ── */
@@ -386,7 +388,8 @@ QList<DmConversation> DbManager::getDmConversations() const {
QList<DmConversation> list;
if (!m_db) return list;
sqlite3_stmt* stmt = prepareOrNull(
"SELECT c.conv_id, peer_node_id, COALESCE(peer_name,''), last_ts, last_out_seq, last_in_seq, COALESCE(h.generation,1)"
"SELECT c.conv_id, peer_node_id, COALESCE(peer_name,''), last_ts, last_out_seq, last_in_seq, COALESCE(h.generation,1),"
" c.mode,c.closed,c.peer_party"
" FROM dm_conversations c LEFT JOIN dm_history h USING(conv_id) WHERE COALESCE(h.hidden,0)=0 ORDER BY last_ts DESC");
if (!stmt) return list;
int rc;
@@ -399,6 +402,15 @@ QList<DmConversation> DbManager::getDmConversations() const {
c.lastOutSeq = sqlite3_column_int64(stmt, 4);
c.lastInSeq = sqlite3_column_int64(stmt, 5);
c.generation = (quint64)sqlite3_column_int64(stmt, 6);
c.mode = sqlite3_column_int(stmt, 7); c.closed = sqlite3_column_int(stmt, 8) != 0;
struct dm_party party;
if (sqlite3_column_bytes(stmt, 9) == DM_PARTY_WIRE_SIZE &&
!dm_party_decode(static_cast<const uint8_t*>(sqlite3_column_blob(stmt, 9)), &party)) {
c.peerGroup = party.kind == DM_PARTY_GROUP;
struct devgroup_record record;
struct devgroup_profile profile;
if (c.peerGroup && devgroup_latest(m_db, party.ed, &record, &profile) == 1) c.peerName = QString::fromUtf8(profile.name);
}
list.append(c);
}
return finishRead(stmt, rc) ? list : QList<DmConversation>();
+3
View File
@@ -81,6 +81,9 @@ struct DmConversation {
quint64 generation = 0; /* Поколение видимой истории: фиксируется в действиях PM до передачи ядру. */
quint64 peerNodeId = 0;
QString peerName;
int mode = 0; // 0 — конкретное устройство, 1 — закреплённые стороны с подгруппами.
bool closed = false; // После выхода доступна только сохранённая история.
bool peerGroup = false; // Имя подгруппы не подменяется именем устройства-представителя.
qint64 lastTs = 0; // Время последнего сообщения для сортировки UI.
qint64 lastOutSeq = 0; // Последний исходящий seq.
qint64 lastInSeq = 0; // Последний входящий seq; пространства направлений независимы.