684 lines
26 KiB
C++
684 lines
26 KiB
C++
#include "db_manager.h"
|
|
#include "../src/debug_ui.h"
|
|
#include "../../lib/debug_config.h"
|
|
#include <openssl/sha.h>
|
|
#include <openssl/evp.h>
|
|
#include <cstring>
|
|
#include <QSet>
|
|
#include <QElapsedTimer>
|
|
|
|
/* ── helpers ── */
|
|
|
|
static QByteArray parseJsonField(const QByteArray& json, const char* key) {
|
|
QByteArray pat = "\"" + QByteArray(key) + "\":\"";
|
|
int idx = json.indexOf(pat);
|
|
if (idx < 0) return {};
|
|
idx += pat.size();
|
|
int end = idx;
|
|
while (end < json.size() && !(json[end] == '"' && (end == idx || json[end - 1] != '\\'))) end++;
|
|
if (end > idx) return QByteArray(json.constData() + idx, end - idx);
|
|
return {};
|
|
}
|
|
|
|
QString DbManager::sanitizeTableName(const QString& chId) {
|
|
QString s;
|
|
s.reserve(chId.size());
|
|
for (int i = 0; i < chId.size(); i++) {
|
|
QChar c = chId[i];
|
|
if (c.isLetterOrNumber() || c == '_') s += c;
|
|
else s += '_';
|
|
}
|
|
return s;
|
|
}
|
|
|
|
QString DbManager::msgTableName(const QString& chId) const {
|
|
return QStringLiteral("msg_") + sanitizeTableName(chId);
|
|
}
|
|
QString DbManager::peersTableName(const QString& chId) const {
|
|
return QStringLiteral("peers_") + sanitizeTableName(chId);
|
|
}
|
|
|
|
uint64_t DbManager::computeDatahash(const QByteArray& data) {
|
|
uint8_t hash[32];
|
|
SHA256((const unsigned char*)data.constData(), data.size(), hash);
|
|
uint64_t dh; memcpy(&dh, hash, 8); return dh;
|
|
}
|
|
|
|
QByteArray DbManager::computeChainHash(const QByteArray& prevChain,
|
|
qint64 timestamp, uint64_t datahash) {
|
|
uint8_t buf[32 + 8 + 8];
|
|
memcpy(buf, prevChain.constData(), 32);
|
|
memcpy(buf + 32, ×tamp, 8);
|
|
memcpy(buf + 40, &datahash, 8);
|
|
uint8_t hash[32];
|
|
SHA256(buf, 48, hash);
|
|
return QByteArray((const char*)hash, 32);
|
|
}
|
|
|
|
QByteArray DbManager::getLastChainHash(const QString& chId) const {
|
|
QString tbl = msgTableName(chId);
|
|
QByteArray sql = QStringLiteral(
|
|
"SELECT chain_hash FROM \"%1\" ORDER BY timestamp, author_signature DESC LIMIT 1")
|
|
.arg(tbl).toUtf8();
|
|
sqlite3_stmt* stmt = prepareOrNull(sql.constData());
|
|
if (!stmt) return QByteArray(32, '\0');
|
|
QByteArray r = (sqlite3_step(stmt) == SQLITE_ROW) ? colBlob(stmt, 0)
|
|
: QByteArray(32, '\0');
|
|
sqlite3_finalize(stmt);
|
|
return r.size() == 32 ? r : QByteArray(32, '\0');
|
|
}
|
|
|
|
/* ── lifecycle ── */
|
|
|
|
DbManager::DbManager(QObject* parent)
|
|
: QObject(parent)
|
|
{
|
|
}
|
|
|
|
DbManager::~DbManager() {
|
|
for (auto it = m_cursors.begin(); it != m_cursors.end(); ++it)
|
|
sqlite3_finalize(it.value());
|
|
m_cursors.clear();
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "DbManager: destroyed");
|
|
}
|
|
|
|
sqlite3_stmt* DbManager::prepareOrNull(const char* sql) const {
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(m_db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
|
const char* err = sqlite3_errmsg(m_db);
|
|
if (!strstr(err, "no such table")) GUI_ERROR("prepare failed: %s — %s", sql, err);
|
|
return nullptr;
|
|
}
|
|
return stmt;
|
|
}
|
|
|
|
QString DbManager::colText(sqlite3_stmt* stmt, int col) {
|
|
return QString::fromUtf8(
|
|
reinterpret_cast<const char*>(sqlite3_column_text(stmt, col)));
|
|
}
|
|
|
|
QByteArray DbManager::colBlob(sqlite3_stmt* stmt, int col) {
|
|
int len = sqlite3_column_bytes(stmt, col);
|
|
if (len <= 0) return {};
|
|
return QByteArray(static_cast<const char*>(sqlite3_column_blob(stmt, col)), len);
|
|
}
|
|
|
|
/* ── channels ── */
|
|
|
|
QList<ChannelRow> DbManager::getChannels(ChannelSortMode sort) const {
|
|
QList<ChannelRow> list;
|
|
const char* orderBy = (sort == ChannelSortMode::Recent)
|
|
? " ORDER BY last_msg_at DESC, name COLLATE NOCASE ASC"
|
|
: " ORDER BY name COLLATE NOCASE ASC";
|
|
char sql[512]; snprintf(sql, sizeof(sql),
|
|
"SELECT channel_id, name, owner_node_id,"
|
|
" x25519_pubkey, x25519_privkey, ed25519_pubkey, ed25519_privkey, signature,"
|
|
" last_read_msg_id, last_pos_msg_id, last_msg_at, created_at"
|
|
" FROM channels%s", orderBy);
|
|
sqlite3_stmt* stmt = prepareOrNull(sql);
|
|
if (!stmt) return list;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
ChannelRow r;
|
|
r.channelId = colText(stmt, 0);
|
|
r.name = colText(stmt, 1);
|
|
r.ownerNodeId = (quint64)sqlite3_column_int64(stmt, 2);
|
|
r.x25519_pubkey = colBlob(stmt, 3);
|
|
r.x25519_privkey = colBlob(stmt, 4);
|
|
r.ed25519_pubkey = colBlob(stmt, 5);
|
|
r.ed25519_privkey = colBlob(stmt, 6);
|
|
r.signature = colBlob(stmt, 7);
|
|
r.lastReadMsgId = sqlite3_column_int64(stmt, 8);
|
|
r.lastPosMsgId = sqlite3_column_int64(stmt, 9);
|
|
r.lastMsgAt = sqlite3_column_int64(stmt, 10);
|
|
r.createdAt = sqlite3_column_int64(stmt, 11);
|
|
list.append(r);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
for (auto& ch : list) {
|
|
ch.lastReadMsgId = getUiStateInt64(
|
|
QStringLiteral("ch_%1_last_read").arg(ch.channelId), 0);
|
|
ch.lastPosMsgId = getUiStateInt64(
|
|
QStringLiteral("ch_%1_last_pos").arg(ch.channelId), 0);
|
|
ch.unreadCount = getUnreadCount(ch.channelId, ch.lastReadMsgId);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
/* Формирует человекочитаемое тело превью последнего сообщения по content_type и "d"-полю. */
|
|
static QString mediaPreviewBody(const QString& ct, const QByteArray& d) {
|
|
bool isText = ct.isEmpty() || ct == QStringLiteral("text/plain") ||
|
|
ct == QStringLiteral("voice_transcription");
|
|
if (isText) {
|
|
QString t = QString::fromUtf8(d);
|
|
if (t.size() > 60) { t = t.left(60); t += QStringLiteral("..."); }
|
|
return t;
|
|
}
|
|
if (ct == QStringLiteral("audio/opus")) {
|
|
int durMs = 0;
|
|
if (d.startsWith('*')) {
|
|
int pipe = d.indexOf('|');
|
|
if (pipe > 1) {
|
|
QByteArray b64 = d.mid(1, pipe - 1);
|
|
b64.replace('-', '+');
|
|
b64.replace('_', '/');
|
|
QString raw = QString::fromUtf8(QByteArray::fromBase64(b64));
|
|
int durPos = raw.indexOf(QStringLiteral(";dur="));
|
|
if (durPos >= 0) {
|
|
int end = raw.indexOf(';', durPos + 5);
|
|
QString ds = (end > 0) ? raw.mid(durPos + 5, end - durPos - 5)
|
|
: raw.mid(durPos + 5);
|
|
durMs = ds.toInt();
|
|
}
|
|
}
|
|
}
|
|
if (durMs > 0) {
|
|
int s = durMs / 1000;
|
|
return QStringLiteral("Voice message (%1:%2)")
|
|
.arg(s / 60).arg(s % 60, 2, 10, QLatin1Char('0'));
|
|
}
|
|
return QStringLiteral("Voice message");
|
|
}
|
|
if (ct.startsWith(QStringLiteral("video/"))) return QStringLiteral("Video");
|
|
if (ct.startsWith(QStringLiteral("image/"))) return QStringLiteral("Photo");
|
|
/* файл и прочие нетекстовые: base64-префикс — имя файла */
|
|
if (d.startsWith('*')) {
|
|
int pipe = d.indexOf('|');
|
|
QByteArray b64 = d.mid(1, (pipe > 1) ? pipe - 1 : d.size() - 1);
|
|
b64.replace('-', '+');
|
|
b64.replace('_', '/');
|
|
QString fname = QString::fromUtf8(QByteArray::fromBase64(b64));
|
|
if (!fname.isEmpty()) return fname;
|
|
}
|
|
return QStringLiteral("File");
|
|
}
|
|
|
|
QString DbManager::getChannelLastMessage(const QString& channelId) const {
|
|
QString sql = QStringLiteral(
|
|
"SELECT node_id, data FROM \"%1\" ORDER BY id DESC LIMIT 1")
|
|
.arg(msgTableName(channelId));
|
|
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
|
|
if (!stmt) return {};
|
|
QString result;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
quint64 author = (quint64)sqlite3_column_int64(stmt, 0);
|
|
QByteArray jdata = colBlob(stmt, 1);
|
|
QString ct = QString::fromUtf8(parseJsonField(jdata, "ct"));
|
|
QByteArray d = parseJsonField(jdata, "d");
|
|
QString nick = getDisplayName(author);
|
|
result = nick + QStringLiteral(": ") + mediaPreviewBody(ct, d);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return result;
|
|
}
|
|
|
|
/* ── messages (UI) ── */
|
|
|
|
QList<MessageRow> DbManager::getMessages(const QString& chId, int limit) const {
|
|
QList<MessageRow> list;
|
|
QString sql = QStringLiteral(
|
|
"SELECT id, node_id, data, timestamp, chain_hash, author_signature, local_attrs"
|
|
" FROM \"%1\" ORDER BY timestamp DESC, author_signature DESC LIMIT ?").arg(msgTableName(chId));
|
|
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
|
|
if (!stmt) return list;
|
|
sqlite3_bind_int(stmt, 1, limit);
|
|
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.localAttrs = QByteArray((const char*)sqlite3_column_text(stmt, 6));
|
|
m.isOutgoing = (m.authorNodeId == myId);
|
|
m.isRead = true;
|
|
list.prepend(m);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return list;
|
|
}
|
|
|
|
QList<MessageRow> DbManager::getMessagesSince(const QString& chId, qint64 sinceId) const {
|
|
QList<MessageRow> list;
|
|
QString sql = QStringLiteral(
|
|
"SELECT id, node_id, data, timestamp, chain_hash, author_signature, local_attrs"
|
|
" FROM \"%1\" WHERE id > ? ORDER BY id ASC").arg(msgTableName(chId));
|
|
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
|
|
if (!stmt) return list;
|
|
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.localAttrs = QByteArray((const char*)sqlite3_column_text(stmt, 6));
|
|
m.isOutgoing = (m.authorNodeId == myId);
|
|
m.isRead = true;
|
|
list.append(m);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return list;
|
|
}
|
|
|
|
MessageRow DbManager::getMessageById(const QString& chId, qint64 msgId) const {
|
|
MessageRow m;
|
|
QString sql = QStringLiteral(
|
|
"SELECT id, node_id, data, timestamp, chain_hash, author_signature, local_attrs"
|
|
" FROM \"%1\" WHERE id = ?").arg(msgTableName(chId));
|
|
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
|
|
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.localAttrs = QByteArray((const char*)sqlite3_column_text(stmt, 6));
|
|
m.isOutgoing = (m.authorNodeId == m_myNodeId);
|
|
m.isRead = true;
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return m;
|
|
}
|
|
|
|
/* ── peers (UI) ── */
|
|
|
|
QMap<quint64, QString> DbManager::getLocalNicknames(const QString& chId) const {
|
|
QMap<quint64, QString> m;
|
|
if (!m_db) return m;
|
|
QString sql = QStringLiteral(
|
|
"SELECT node_id, local_nick FROM \"%1\" WHERE local_nick IS NOT NULL AND local_nick != ''"
|
|
).arg(peersTableName(chId));
|
|
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
|
|
if (!stmt) return m;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW)
|
|
m.insert((quint64)sqlite3_column_int64(stmt, 0), colText(stmt, 1));
|
|
sqlite3_finalize(stmt);
|
|
return m;
|
|
}
|
|
|
|
void DbManager::cleanOrphanedPeers(const QString& chId) const {
|
|
if (!m_db) return;
|
|
QString sql = QStringLiteral(
|
|
"DELETE FROM \"%1\" WHERE node_id NOT IN (SELECT node_id FROM nodes)"
|
|
).arg(peersTableName(chId));
|
|
char* err = nullptr;
|
|
int rc = sqlite3_exec(m_db, sql.toUtf8().constData(), nullptr, nullptr, &err);
|
|
if (rc != SQLITE_OK) {
|
|
qWarning("cleanOrphanedPeers: %s", err ? err : "?");
|
|
sqlite3_free(err);
|
|
}
|
|
}
|
|
|
|
QString DbManager::getDisplayName(quint64 nodeId) const {
|
|
sqlite3_stmt* st = prepareOrNull(
|
|
"SELECT display_name FROM accounts WHERE node_id=?");
|
|
if (st) {
|
|
sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId);
|
|
if (sqlite3_step(st) == SQLITE_ROW) {
|
|
QString s = colText(st, 0);
|
|
sqlite3_finalize(st);
|
|
if (!s.isEmpty()) return s;
|
|
} else sqlite3_finalize(st);
|
|
}
|
|
st = prepareOrNull("SELECT name FROM nodes WHERE node_id=?");
|
|
if (st) {
|
|
sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId);
|
|
if (sqlite3_step(st) == SQLITE_ROW) {
|
|
QString s = colText(st, 0);
|
|
sqlite3_finalize(st);
|
|
if (!s.isEmpty()) return s;
|
|
} else sqlite3_finalize(st);
|
|
}
|
|
return QString("node_%1").arg(nodeId);
|
|
}
|
|
|
|
QByteArray DbManager::loadNodeInfo(quint64 nodeId) const {
|
|
/* binary format: [x25519:32][ed25519:32][addr_count:1]([family:1][proto:1][addr_len:1][addr:4|16][port:2][rtt:2])... */
|
|
sqlite3_stmt* stmt = prepareOrNull(
|
|
"SELECT x25519_pubkey, ed25519_pubkey FROM nodes WHERE node_id=?");
|
|
if (!stmt) return {};
|
|
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId);
|
|
if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return {}; }
|
|
QByteArray x25519 = colBlob(stmt, 0);
|
|
QByteArray ed25519 = colBlob(stmt, 1);
|
|
sqlite3_finalize(stmt);
|
|
|
|
QByteArray result;
|
|
result.append(x25519.isEmpty() ? QByteArray(32, '\0') : x25519);
|
|
result.append(ed25519.isEmpty() ? QByteArray(32, '\0') : ed25519);
|
|
|
|
/* запросить адреса */
|
|
stmt = prepareOrNull(
|
|
"SELECT family, protocol, address, port, rtt FROM node_addresses WHERE node_id=?");
|
|
if (!stmt) { result.append('\0'); return result; }
|
|
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId);
|
|
QByteArray addrsPart;
|
|
uint8_t cnt = 0;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW && cnt < 255) {
|
|
int family = sqlite3_column_int(stmt, 0);
|
|
int proto = sqlite3_column_int(stmt, 1);
|
|
QByteArray addr = colBlob(stmt, 2);
|
|
uint16_t port = (uint16_t)sqlite3_column_int(stmt, 3);
|
|
int16_t rtt = (int16_t)(sqlite3_column_int(stmt, 4));
|
|
addrsPart.append((char)family);
|
|
addrsPart.append((char)proto);
|
|
addrsPart.append((char)addr.size());
|
|
addrsPart.append(addr);
|
|
addrsPart.append((const char*)&port, 2);
|
|
addrsPart.append((const char*)&rtt, 2);
|
|
cnt++;
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
result.append((char)cnt);
|
|
result.append(addrsPart);
|
|
return result;
|
|
}
|
|
|
|
QList<quint64> DbManager::getOnlinePeers() const {
|
|
QList<quint64> peers;
|
|
/* без is_online: получаем всех кроме себя */
|
|
sqlite3_stmt* stmt = prepareOrNull("SELECT node_id FROM nodes WHERE node_id != ?");
|
|
if (!stmt) return peers;
|
|
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)m_myNodeId);
|
|
while (sqlite3_step(stmt) == SQLITE_ROW)
|
|
peers.append((quint64)sqlite3_column_int64(stmt, 0));
|
|
sqlite3_finalize(stmt);
|
|
return peers;
|
|
}
|
|
|
|
QByteArray DbManager::getNodeEdPub(quint64 nodeId) const {
|
|
sqlite3_stmt* stmt = prepareOrNull("SELECT ed25519_pubkey FROM nodes WHERE node_id=?");
|
|
if (!stmt) return {};
|
|
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId);
|
|
QByteArray r;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) r = colBlob(stmt, 0);
|
|
sqlite3_finalize(stmt);
|
|
return r;
|
|
}
|
|
|
|
QList<NodeAddr> DbManager::getOnlinePeerAddresses(quint64 excludeNodeId) const {
|
|
QList<NodeAddr> list;
|
|
sqlite3_stmt* stmt = prepareOrNull(
|
|
"SELECT na.node_id, na.family, na.protocol, na.address, na.port, na.rtt, na.addr_type"
|
|
" FROM node_addresses na JOIN nodes n ON na.node_id = n.node_id"
|
|
" WHERE na.addr_type=0 AND na.node_id != ?"
|
|
" ORDER BY RANDOM()");
|
|
if (!stmt) return list;
|
|
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)excludeNodeId);
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
NodeAddr a;
|
|
a.nodeId = (quint64)sqlite3_column_int64(stmt, 0);
|
|
a.family = sqlite3_column_int(stmt, 1);
|
|
a.protocol = sqlite3_column_int(stmt, 2);
|
|
a.address = colBlob(stmt, 3);
|
|
a.port = (quint16)sqlite3_column_int(stmt, 4);
|
|
a.rtt = sqlite3_column_int(stmt, 5);
|
|
a.addrType = sqlite3_column_int(stmt, 6);
|
|
list.append(a);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return list;
|
|
}
|
|
|
|
/* ── accounts ── */
|
|
|
|
QList<AccountRow> DbManager::getAccounts(bool contactsOnly) const {
|
|
QList<AccountRow> list;
|
|
const char* sql = contactsOnly
|
|
? "SELECT node_id, display_name, avatar_color, avatar_letter, is_contact"
|
|
" FROM accounts WHERE is_contact=1 ORDER BY display_name"
|
|
: "SELECT node_id, display_name, avatar_color, avatar_letter, is_contact"
|
|
" FROM accounts ORDER BY display_name";
|
|
sqlite3_stmt* stmt = prepareOrNull(sql);
|
|
if (!stmt) return list;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
AccountRow a;
|
|
a.nodeId = (quint64)sqlite3_column_int64(stmt, 0);
|
|
a.displayName = colText(stmt, 1);
|
|
a.avatarColor = colText(stmt, 2);
|
|
a.avatarLetter = colText(stmt, 3);
|
|
a.isContact = sqlite3_column_int(stmt, 4) != 0;
|
|
list.append(a);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return list;
|
|
}
|
|
|
|
AccountRow DbManager::getAccount(quint64 nodeId) const {
|
|
AccountRow a;
|
|
sqlite3_stmt* stmt = prepareOrNull(
|
|
"SELECT node_id, display_name, avatar_color, avatar_letter, is_contact"
|
|
" FROM accounts WHERE node_id=?");
|
|
if (!stmt) return a;
|
|
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId);
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
a.nodeId = (quint64)sqlite3_column_int64(stmt, 0);
|
|
a.displayName = colText(stmt, 1);
|
|
a.avatarColor = colText(stmt, 2);
|
|
a.avatarLetter = colText(stmt, 3);
|
|
a.isContact = sqlite3_column_int(stmt, 4) != 0;
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
return a;
|
|
}
|
|
|
|
/* ── ui_state ── */
|
|
|
|
QString DbManager::getUiState(const QString& key) const {
|
|
sqlite3_stmt* stmt = prepareOrNull("SELECT value FROM ui_state WHERE key=?");
|
|
if (!stmt) return {};
|
|
sqlite3_bind_text(stmt, 1, key.toUtf8().constData(), -1, SQLITE_TRANSIENT);
|
|
QString val;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) val = colText(stmt, 0);
|
|
sqlite3_finalize(stmt);
|
|
return val;
|
|
}
|
|
|
|
int DbManager::getUiStateInt(const QString& key, int defaultVal) const {
|
|
QString val = getUiState(key);
|
|
if (val.isEmpty()) return defaultVal;
|
|
bool ok = false;
|
|
int v = val.toInt(&ok);
|
|
return ok ? v : defaultVal;
|
|
}
|
|
|
|
qint64 DbManager::getUiStateInt64(const QString& key, qint64 defaultVal) const {
|
|
QString val = getUiState(key);
|
|
if (val.isEmpty()) return defaultVal;
|
|
bool ok = false;
|
|
qint64 v = val.toLongLong(&ok);
|
|
return ok ? v : defaultVal;
|
|
}
|
|
|
|
int DbManager::getUnreadCount(const QString& channelId, qint64 lastReadMsgId) const {
|
|
QString sql = QStringLiteral(
|
|
"SELECT COUNT(*) FROM \"%1\" WHERE id > ?").arg(msgTableName(channelId));
|
|
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
|
|
if (!stmt) return 0;
|
|
sqlite3_bind_int64(stmt, 1, lastReadMsgId);
|
|
int count = 0;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) count = sqlite3_column_int(stmt, 0);
|
|
sqlite3_finalize(stmt);
|
|
return count;
|
|
}
|
|
|
|
/* ── sync operations (read-only, for gui_bridge) ── */
|
|
|
|
QByteArray DbManager::syncListChannels() const {
|
|
/* binary: [count:2]([chId_len:1][chId:var])... */
|
|
QList<ChannelRow> chs = getChannels();
|
|
QByteArray out;
|
|
uint16_t cnt = (uint16_t)chs.size();
|
|
out.append((const char*)&cnt, 2);
|
|
for (auto& ch : chs) {
|
|
QByteArray id = ch.channelId.toUtf8();
|
|
out.append((char)(uint8_t)id.size());
|
|
out.append(id);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
QByteArray DbManager::syncCount(const QString& chId) const {
|
|
QString sql = QStringLiteral("SELECT COUNT(*) FROM \"%1\"").arg(msgTableName(chId));
|
|
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
|
|
if (!stmt) return {};
|
|
uint32_t cnt = 0;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) cnt = (uint32_t)sqlite3_column_int64(stmt, 0);
|
|
sqlite3_finalize(stmt);
|
|
QByteArray out; out.append((const char*)&cnt, 4); return out;
|
|
}
|
|
|
|
QByteArray DbManager::syncChainHashAt(const QString& chId, uint32_t pos) const {
|
|
QString sql = QStringLiteral(
|
|
"SELECT chain_hash FROM \"%1\" ORDER BY timestamp, author_signature ASC LIMIT 1 OFFSET ?")
|
|
.arg(msgTableName(chId));
|
|
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
|
|
if (!stmt) return QByteArray(32, '\0');
|
|
sqlite3_bind_int64(stmt, 1, pos);
|
|
QByteArray r;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) r = colBlob(stmt, 0);
|
|
sqlite3_finalize(stmt);
|
|
return r.size() == 32 ? r : QByteArray(32, '\0');
|
|
}
|
|
|
|
QByteArray DbManager::syncCursorOpen(const QString& chId) {
|
|
QString sql = QStringLiteral(
|
|
"SELECT timestamp, node_id, data, author_signature"
|
|
" FROM \"%1\" ORDER BY timestamp, author_signature ASC").arg(msgTableName(chId));
|
|
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
|
|
if (!stmt) { QByteArray r(4, '\0'); return r; }
|
|
uint32_t id = m_nextCursorId++;
|
|
m_cursors[id] = stmt;
|
|
QByteArray r; r.append((const char*)&id, 4); return r;
|
|
}
|
|
|
|
QByteArray DbManager::syncCursorNext(uint32_t cursorId) {
|
|
auto it = m_cursors.find(cursorId);
|
|
if (it == m_cursors.end()) return {};
|
|
if (sqlite3_step(it.value()) != SQLITE_ROW) return {};
|
|
|
|
/* return: [ts:8][dh:8][nid:8][ct_len:1][ct:var][data_len:4][data:var] */
|
|
qint64 ts = sqlite3_column_int64(it.value(), 0);
|
|
uint64_t nid = (uint64_t)sqlite3_column_int64(it.value(), 1);
|
|
QByteArray jdata = colBlob(it.value(), 2);
|
|
/* parse JSON from data column */
|
|
auto jField = [](const QByteArray& j, const char* k) -> QByteArray {
|
|
QByteArray p = "\"" + QByteArray(k) + "\":\"";
|
|
int i = j.indexOf(p); if (i < 0) return {};
|
|
i += p.size(); int e = i;
|
|
while (e < j.size() && !(j[e] == '"' && (e == i || j[e - 1] != '\\'))) e++;
|
|
return (e > i) ? QByteArray(j.constData() + i, e - i) : QByteArray();
|
|
};
|
|
QByteArray ct_raw = jField(jdata, "ct");
|
|
QString ct = QString::fromUtf8(ct_raw);
|
|
if (ct.isEmpty()) ct = "text/plain";
|
|
QByteArray ddata = jField(jdata, "d");
|
|
/* compute datahash for wire format compat */
|
|
uint8_t dh_buf[32]; SHA256((const uint8_t*)ddata.constData(), ddata.size(), dh_buf);
|
|
uint64_t dh; memcpy(&dh, dh_buf, 8);
|
|
|
|
QByteArray out;
|
|
out.append((const char*)&ts, 8);
|
|
out.append((const char*)&dh, 8);
|
|
out.append((const char*)&nid, 8);
|
|
QByteArray ctb = ct.toUtf8();
|
|
out.append((char)(uint8_t)ctb.size());
|
|
out.append(ctb);
|
|
uint32_t dLen = (uint32_t)ddata.size();
|
|
out.append((const char*)&dLen, 4);
|
|
out.append(ddata);
|
|
return out;
|
|
}
|
|
|
|
void DbManager::syncCursorClose(uint32_t cursorId) {
|
|
auto it = m_cursors.find(cursorId);
|
|
if (it != m_cursors.end()) { sqlite3_finalize(it.value()); m_cursors.erase(it); }
|
|
}
|
|
|
|
QByteArray DbManager::syncListPeers(const QString& chId) const {
|
|
/* binary: [count:2]([node_id:8])... */
|
|
QString sql = QStringLiteral("SELECT node_id FROM \"%1\"").arg(peersTableName(chId));
|
|
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
|
|
if (!stmt) { QByteArray r(2, '\0'); return r; }
|
|
QList<uint64_t> ids;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW)
|
|
ids.append((uint64_t)sqlite3_column_int64(stmt, 0));
|
|
sqlite3_finalize(stmt);
|
|
QByteArray out;
|
|
uint16_t cnt = (uint16_t)ids.size();
|
|
out.append((const char*)&cnt, 2);
|
|
for (auto id : ids) out.append((const char*)&id, 8);
|
|
return out;
|
|
}
|
|
|
|
/* ── integrity checks ── */
|
|
|
|
static int integrityCallback(void* data, int nCols, char** values, char** /*names*/) {
|
|
auto* result = (IntegrityResult*)data;
|
|
for (int i = 0; i < nCols; i++) {
|
|
if (values[i]) {
|
|
QString line = QString::fromUtf8(values[i]);
|
|
if (result->message.isEmpty()) result->message = line;
|
|
else if (line != "ok") result->errors.append(line);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static IntegrityResult runPragma(DbManager* self, const char* pragma) {
|
|
IntegrityResult result;
|
|
if (!self->m_db) {
|
|
result.ok = false;
|
|
result.message = "Database not open";
|
|
return result;
|
|
}
|
|
QElapsedTimer timer;
|
|
timer.start();
|
|
char* errMsg = nullptr;
|
|
int rc = sqlite3_exec(self->m_db, pragma, integrityCallback, &result, &errMsg);
|
|
result.elapsedMs = (int)timer.elapsed();
|
|
if (rc != SQLITE_OK) {
|
|
result.ok = false;
|
|
result.message = QString::fromUtf8(errMsg ? errMsg : "unknown error");
|
|
sqlite3_free(errMsg);
|
|
return result;
|
|
}
|
|
if (errMsg) sqlite3_free(errMsg);
|
|
result.ok = (result.message == "ok" && result.errors.isEmpty());
|
|
return result;
|
|
}
|
|
|
|
IntegrityResult DbManager::integrityCheckQuick() const {
|
|
return runPragma(const_cast<DbManager*>(this), "PRAGMA quick_check");
|
|
}
|
|
|
|
IntegrityResult DbManager::integrityCheckFull() const {
|
|
return runPragma(const_cast<DbManager*>(this), "PRAGMA integrity_check");
|
|
}
|
|
|
|
IntegrityResult DbManager::integrityOptimize() const {
|
|
return runPragma(const_cast<DbManager*>(this), "PRAGMA optimize");
|
|
}
|