From 68df26d4a5160be37d9f65bee41bd7b3c2996cb9 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 8 Oct 2026 00:21:58 +0200 Subject: [PATCH] Read reply references and bounded history around message IDs --- tools/chatgui/db/db_manager.cpp | 111 ++++++++++++++++++-------------- tools/chatgui/db/db_manager.h | 5 +- 2 files changed, 67 insertions(+), 49 deletions(-) diff --git a/tools/chatgui/db/db_manager.cpp b/tools/chatgui/db/db_manager.cpp index 49fd748c..af4e249d 100644 --- a/tools/chatgui/db/db_manager.cpp +++ b/tools/chatgui/db/db_manager.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include "chat/chat_poll.h" /* ── helpers ── */ @@ -231,34 +233,42 @@ QString DbManager::getChannelLastMessage(const QString& channelId) const { /* ── messages (UI) ── */ -QList DbManager::getMessages(const QString& chId, int limit) const { +/* Единое чтение подписанного сообщения для истории, переходов и разрешения цитат. */ +static MessageRow readMessage(sqlite3_stmt* stmt, quint64 myId) { + MessageRow m; + m.id = sqlite3_column_int64(stmt, 0); m.authorNodeId = (quint64)sqlite3_column_int64(stmt, 1); + const auto json = QJsonDocument::fromJson(DbManager::colBlob(stmt, 2)).object(); + m.contentType = json.value("ct").toString("text/plain"); m.data = json.value("d").toString().toUtf8(); + const QByteArray reply = json.value("reply_to").toString().toLatin1(); + if (!reply.isEmpty()) { + m.replyTo = QByteArray::fromHex(reply); + if (reply.size() != 64 || m.replyTo.size() != 32 || m.replyTo.toHex() != reply) { + GUI_WARN("reply: malformed reference message_row=%lld", (long long)m.id); m.replyTo.clear(); + } + } + m.timestamp = sqlite3_column_int64(stmt, 3); m.chainHash = DbManager::colBlob(stmt, 4); + m.signature = DbManager::colBlob(stmt, 5); m.localAttrs = DbManager::colBlob(stmt, 6); + m.messageId = DbManager::colBlob(stmt, 7); m.isOutgoing = m.authorNodeId == myId; m.isRead = true; + return m; +} + +/* Окно вокруг оригинала выбирается одним SQL snapshot, включая одинаковые timestamp. */ +QList DbManager::getMessages(const QString& chId, int limit, const QByteArray& around) const { QList list; QString sql = QStringLiteral( "SELECT id, node_id, data, timestamp, chain_hash, author_signature, local_attrs, message_id" - " FROM \"%1\" ORDER BY timestamp DESC, author_signature DESC LIMIT ?").arg(msgTableName(chId)); + " FROM \"%1\" ORDER BY timestamp DESC, author_signature DESC LIMIT ?1 OFFSET " + "max(0,(SELECT count(*) FROM \"%1\" n JOIN \"%1\" a ON a.message_id=?2 " + "WHERE (n.timestamp,n.author_signature)>(a.timestamp,a.author_signature)) - ?1/2)").arg(msgTableName(chId)); sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); if (!stmt) return list; sqlite3_bind_int(stmt, 1, limit); + sqlite3_bind_blob(stmt, 2, around.constData(), around.size(), SQLITE_TRANSIENT); uint64_t myId = 0; { sqlite3_stmt* ms = prepareOrNull("SELECT node_id FROM local_identity WHERE id=1"); if (ms) { if (sqlite3_step(ms) == SQLITE_ROW) myId = (uint64_t)sqlite3_column_int64(ms, 0); sqlite3_finalize(ms); } } while (sqlite3_step(stmt) == SQLITE_ROW) { - MessageRow m; - m.id = sqlite3_column_int64(stmt, 0); - m.authorNodeId = (quint64)sqlite3_column_int64(stmt, 1); - QByteArray jdata = colBlob(stmt, 2); - m.contentType = QString::fromUtf8(parseJsonField(jdata, "ct")); - if (m.contentType.isEmpty()) m.contentType = "text/plain"; - m.data = parseJsonField(jdata, "d"); - m.timestamp = sqlite3_column_int64(stmt, 3); - m.datahash = 0; - m.chainHash = colBlob(stmt, 4); - m.signature = colBlob(stmt, 5); - m.messageId = colBlob(stmt, 7); - m.localAttrs = QByteArray((const char*)sqlite3_column_text(stmt, 6)); - m.isOutgoing = (m.authorNodeId == myId); - m.isRead = true; - list.prepend(m); + list.prepend(readMessage(stmt, myId)); } sqlite3_finalize(stmt); return list; @@ -274,22 +284,7 @@ QList DbManager::getMessagesSince(const QString& chId, qint64 sinceI sqlite3_bind_int64(stmt, 1, sinceId); uint64_t myId = m_myNodeId; while (sqlite3_step(stmt) == SQLITE_ROW) { - MessageRow m; - m.id = sqlite3_column_int64(stmt, 0); - m.authorNodeId = (quint64)sqlite3_column_int64(stmt, 1); - QByteArray jdata = colBlob(stmt, 2); - m.contentType = QString::fromUtf8(parseJsonField(jdata, "ct")); - if (m.contentType.isEmpty()) m.contentType = "text/plain"; - m.data = parseJsonField(jdata, "d"); - m.timestamp = sqlite3_column_int64(stmt, 3); - m.datahash = 0; - m.chainHash = colBlob(stmt, 4); - m.signature = colBlob(stmt, 5); - m.messageId = colBlob(stmt, 7); - m.localAttrs = QByteArray((const char*)sqlite3_column_text(stmt, 6)); - m.isOutgoing = (m.authorNodeId == myId); - m.isRead = true; - list.append(m); + list.append(readMessage(stmt, myId)); } sqlite3_finalize(stmt); return list; @@ -304,20 +299,7 @@ MessageRow DbManager::getMessageById(const QString& chId, qint64 msgId) const { if (!stmt) return m; sqlite3_bind_int64(stmt, 1, msgId); if (sqlite3_step(stmt) == SQLITE_ROW) { - m.id = sqlite3_column_int64(stmt, 0); - m.authorNodeId = (quint64)sqlite3_column_int64(stmt, 1); - QByteArray jdata = colBlob(stmt, 2); - m.contentType = QString::fromUtf8(parseJsonField(jdata, "ct")); - if (m.contentType.isEmpty()) m.contentType = "text/plain"; - m.data = parseJsonField(jdata, "d"); - m.timestamp = sqlite3_column_int64(stmt, 3); - m.datahash = 0; - m.chainHash = colBlob(stmt, 4); - m.signature = colBlob(stmt, 5); - m.messageId = colBlob(stmt, 7); - m.localAttrs = QByteArray((const char*)sqlite3_column_text(stmt, 6)); - m.isOutgoing = (m.authorNodeId == m_myNodeId); - m.isRead = true; + m = readMessage(stmt, m_myNodeId); } sqlite3_finalize(stmt); return m; @@ -325,6 +307,39 @@ MessageRow DbManager::getMessageById(const QString& chId, qint64 msgId) const { /* ── DM-беседы (UI, read-only) ── */ +/* Поиск ограничен таблицей выбранного канала; отсутствие оригинала допустимо при неполной синхронизации. */ +MessageRow DbManager::getMessageByKey(const QString& chId, const QByteArray& key) const { + MessageRow message; + if (key.size() != 32) { GUI_ERROR("reply: invalid lookup key bytes=%d", int(key.size())); return message; } + const auto sql = QStringLiteral("SELECT id,node_id,data,timestamp,chain_hash,author_signature,local_attrs,message_id " + "FROM \"%1\" WHERE message_id=?").arg(msgTableName(chId)).toUtf8(); + sqlite3_stmt* stmt = prepareOrNull(sql.constData()); + if (!stmt) return message; + sqlite3_bind_blob(stmt, 1, key.constData(), key.size(), SQLITE_TRANSIENT); + const int rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) message = readMessage(stmt, m_myNodeId); + else if (rc != SQLITE_DONE) GUI_ERROR("reply: lookup failed ch=%s rc=%d", qPrintable(chId), rc); + sqlite3_finalize(stmt); + return message; +} + +/* Короткий обычный текст без рекурсивных цитат и служебного описания медиа. */ +QString DbManager::messagePreview(const MessageRow& message) { + const auto& ct = message.contentType; + QString text; + if (ct == "poll") { + struct chat_poll poll; + const auto wire = QByteArray::fromBase64(message.data.mid(4), QByteArray::Base64UrlEncoding); + if (chat_poll_decode(reinterpret_cast(wire.constData()), wire.size(), &poll) == 0) + text = QString::fromUtf8(poll.question); + else text = QCoreApplication::translate("ChatGui", "Invalid poll"); + } else if (ct == "audio/opus") text = QCoreApplication::translate("ChatGui", "Voice message"); + else if (ct.startsWith("image/")) text = QCoreApplication::translate("ChatGui", "Photo"); + else if (ct.startsWith("video/")) text = QCoreApplication::translate("ChatGui", "Video"); + else text = mediaPreviewBody(ct, message.data); + return text.simplified().left(160); +} + QList DbManager::getDmConversations() const { QList list; if (!m_db) return list; diff --git a/tools/chatgui/db/db_manager.h b/tools/chatgui/db/db_manager.h index 8bb2b09e..2cac13af 100644 --- a/tools/chatgui/db/db_manager.h +++ b/tools/chatgui/db/db_manager.h @@ -43,6 +43,7 @@ struct MessageRow { QByteArray chainHash; QByteArray signature; QByteArray messageId; // общий ID подписанного сообщения, 32 байта + QByteArray replyTo; // общий ID оригинала в этом канале, либо пусто QByteArray localAttrs; bool isOutgoing = false; bool isRead = false; @@ -106,9 +107,11 @@ public: qint64 getUiStateInt64(const QString& key, qint64 defaultVal = 0) const; /* ── Сообщения (UI) ── */ - QList getMessages(const QString& chId, int limit = 50) const; + QList getMessages(const QString& chId, int limit = 50, const QByteArray& around = {}) const; QList getMessagesSince(const QString& chId, qint64 sinceId) const; MessageRow getMessageById(const QString& chId, qint64 msgId) const; + MessageRow getMessageByKey(const QString& chId, const QByteArray& key) const; + static QString messagePreview(const MessageRow& message); /* ── DM-беседы (UI, read-only) ── */ QList getDmConversations() const;