#include "db_manager.h" #include "../src/debug_ui.h" #include "../../lib/debug_config.h" #include #include #include #include #include /* ── 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_DEBUG, "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(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(sqlite3_column_blob(stmt, col)), len); } /* ── channels ── */ QList DbManager::getChannels() const { QList list; sqlite3_stmt* stmt = prepareOrNull( "SELECT channel_id, name, owner_node_id," " x25519_pubkey, x25519_privkey, ed25519_pubkey, ed25519_privkey, signature," " last_read_msg_id, last_pos_msg_id, created_at" " FROM channels ORDER BY created_at ASC"); 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.createdAt = sqlite3_column_int64(stmt, 10); 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; } /* ── messages (UI) ── */ QList DbManager::getMessages(const QString& chId, int limit) const { QList list; QString sql = QStringLiteral( "SELECT id, node_id, data, timestamp, chain_hash, author_signature" " 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.isOutgoing = (m.authorNodeId == myId); m.isRead = true; list.prepend(m); } sqlite3_finalize(stmt); return list; } QList DbManager::getMessagesSince(const QString& chId, qint64 sinceId) const { QList list; QString sql = QStringLiteral( "SELECT id, node_id, data, timestamp, chain_hash, author_signature" " 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.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.isOutgoing = (m.authorNodeId == myId); m.isRead = true; list.append(m); } sqlite3_finalize(stmt); return list; } /* ── peers (UI) ── */ int DbManager::getChannelMemberCount(const QString& chId) const { QString sql = QStringLiteral("SELECT COUNT(*) FROM \"%1\"").arg(peersTableName(chId)); sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); if (!stmt) return 0; int c = 0; if (sqlite3_step(stmt) == SQLITE_ROW) c = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); return c; } QList DbManager::getChannelMembers(const QString& chId, int offset, int limit) const { QList list; QString sql = QStringLiteral( "SELECT p.node_id, COALESCE(n.online,0), COALESCE(n.name,''), COALESCE(p.local_nick,'')" " FROM \"%1\" p LEFT JOIN nodes n ON p.node_id=n.node_id" " ORDER BY n.online DESC, p.node_id ASC LIMIT ? OFFSET ?" ).arg(peersTableName(chId)); sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); if (!stmt) return list; sqlite3_bind_int(stmt, 1, limit); sqlite3_bind_int(stmt, 2, offset); while (sqlite3_step(stmt) == SQLITE_ROW) { ChannelMember m; m.nodeId = (quint64)sqlite3_column_int64(stmt, 0); m.online = sqlite3_column_int(stmt, 1) != 0; m.name = colText(stmt, 2); m.localNick = colText(stmt, 3); list.append(m); } sqlite3_finalize(stmt); return list; } QMap DbManager::getLocalNicknames(const QString& chId) const { QMap 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 DbManager::getOnlinePeers() const { QList 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 DbManager::getOnlinePeerAddresses(quint64 excludeNodeId) const { QList 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 DbManager::getAccounts(bool contactsOnly) const { QList 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 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 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(this), "PRAGMA quick_check"); } IntegrityResult DbManager::integrityCheckFull() const { return runPragma(const_cast(this), "PRAGMA integrity_check"); } IntegrityResult DbManager::integrityOptimize() const { return runPragma(const_cast(this), "PRAGMA optimize"); }