#include "db_manager.h" #include "../src/debug_ui.h" #include #include #include #include /* ── helpers ── */ 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, datahash 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(); } 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, is_dm," " 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.isDm = sqlite3_column_int(stmt, 3) != 0; r.x25519_pubkey = colBlob(stmt, 4); r.x25519_privkey = colBlob(stmt, 5); r.ed25519_pubkey = colBlob(stmt, 6); r.ed25519_privkey = colBlob(stmt, 7); r.signature = colBlob(stmt, 8); r.lastReadMsgId = sqlite3_column_int64(stmt, 9); r.lastPosMsgId = sqlite3_column_int64(stmt, 10); r.createdAt = sqlite3_column_int64(stmt, 11); list.append(r); } sqlite3_finalize(stmt); return list; } /* ── messages (UI) ── */ QList DbManager::getMessages(const QString& chId, int limit) const { QList list; QString sql = QStringLiteral( "SELECT id, node_id, content_type, data, timestamp, datahash, chain_hash," " signature, is_outgoing, is_read" " FROM \"%1\" ORDER BY timestamp, datahash ASC LIMIT ?").arg(msgTableName(chId)); sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); if (!stmt) return list; sqlite3_bind_int(stmt, 1, limit); while (sqlite3_step(stmt) == SQLITE_ROW) { MessageRow m; m.id = sqlite3_column_int64(stmt, 0); m.authorNodeId = (quint64)sqlite3_column_int64(stmt, 1); m.contentType = colText(stmt, 2); m.data = colBlob(stmt, 3); m.timestamp = sqlite3_column_int64(stmt, 4); m.datahash = (uint64_t)sqlite3_column_int64(stmt, 5); m.chainHash = colBlob(stmt, 6); m.signature = colBlob(stmt, 7); m.isOutgoing = sqlite3_column_int(stmt, 8) != 0; m.isRead = sqlite3_column_int(stmt, 9) != 0; 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,'')" " 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); list.append(m); } sqlite3_finalize(stmt); return list; } 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.is_nat" " FROM node_addresses na JOIN nodes n ON na.node_id = n.node_id" " WHERE na.is_nat=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.isNat = sqlite3_column_int(stmt, 6) != 0; 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; } /* ── 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, datahash 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, datahash, node_id, content_type, data" " FROM \"%1\" ORDER BY timestamp, datahash 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 dh = (uint64_t)sqlite3_column_int64(it.value(), 1); uint64_t nid = (uint64_t)sqlite3_column_int64(it.value(), 2); QString ct = colText(it.value(), 3); QByteArray data = colBlob(it.value(), 4); 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)data.size(); out.append((const char*)&dLen, 4); out.append(data); 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; } /* ── syncListPeers ── */