|
|
|
|
@ -8,6 +8,8 @@
|
|
|
|
|
#include <QElapsedTimer> |
|
|
|
|
#include <QJsonDocument> |
|
|
|
|
#include <QJsonObject> |
|
|
|
|
#include <QCoreApplication> |
|
|
|
|
#include "chat/chat_poll.h" |
|
|
|
|
|
|
|
|
|
/* ── helpers ── */ |
|
|
|
|
|
|
|
|
|
@ -231,34 +233,42 @@ QString DbManager::getChannelLastMessage(const QString& channelId) const {
|
|
|
|
|
|
|
|
|
|
/* ── messages (UI) ── */ |
|
|
|
|
|
|
|
|
|
QList<MessageRow> 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<MessageRow> DbManager::getMessages(const QString& chId, int limit, const QByteArray& around) const { |
|
|
|
|
QList<MessageRow> 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<MessageRow> 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<const uint8_t*>(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<DmConversation> DbManager::getDmConversations() const { |
|
|
|
|
QList<DmConversation> list; |
|
|
|
|
if (!m_db) return list; |
|
|
|
|
|