#include "db_manager.h" #include "../src/debug_ui.h" #include #include #include #include /* ── helpers ── */ static const char kZero64[64] = {}; static const char kZero32[32] = {}; 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(const QString& path, QObject* parent) : QObject(parent) { int rc = sqlite3_open(path.toUtf8().constData(), &m_db); if (rc != SQLITE_OK) { GUI_ERROR("Cannot open database: %s", sqlite3_errmsg(m_db)); sqlite3_close(m_db); m_db = nullptr; return; } sqlite3_exec(m_db, "PRAGMA journal_mode=WAL", nullptr, nullptr, nullptr); sqlite3_exec(m_db, "PRAGMA foreign_keys=ON", nullptr, nullptr, nullptr); m_myNodeId = 100; initTables(); if (getChannels().isEmpty()) seedStubData(); GUI_INFO("Database opened: %s (my_node_id=%llu)", path.toUtf8().constData(), (unsigned long long)m_myNodeId); } DbManager::~DbManager() { for (auto it = m_cursors.begin(); it != m_cursors.end(); ++it) sqlite3_finalize(it.value()); m_cursors.clear(); if (m_db) { sqlite3_close(m_db); m_db = nullptr; } } sqlite3_stmt* DbManager::prepareOrNull(const char* sql) const { sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(m_db, sql, -1, &stmt, nullptr) != SQLITE_OK) { GUI_ERROR("prepare failed: %s — %s", sql, sqlite3_errmsg(m_db)); 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); } /* ── init ── */ void DbManager::initTables() { const char* sql = "CREATE TABLE IF NOT EXISTS local_identity (" " id INTEGER PRIMARY KEY CHECK (id = 1)," " node_id INTEGER NOT NULL UNIQUE," " name TEXT NOT NULL," " x25519_pubkey BLOB NOT NULL," " x25519_privkey BLOB," " ed25519_pubkey BLOB," " created_at INTEGER DEFAULT (unixepoch())," " updated_at INTEGER DEFAULT (unixepoch())" ");" "CREATE TABLE IF NOT EXISTS nodes (" " node_id INTEGER PRIMARY KEY," " name TEXT," " x25519_pubkey BLOB NOT NULL," " ed25519_pubkey BLOB," " last_seen_at INTEGER," " created_at INTEGER DEFAULT (unixepoch())" ");" "CREATE TABLE IF NOT EXISTS node_addresses (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " node_id INTEGER NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE," " family INTEGER NOT NULL CHECK(family IN (4, 6))," " protocol INTEGER NOT NULL DEFAULT 1," " address BLOB NOT NULL," " port INTEGER NOT NULL CHECK(port > 0 AND port <= 65535)," " rtt INTEGER," " is_nat INTEGER DEFAULT 0," " created_at INTEGER DEFAULT (unixepoch())" ");" "CREATE INDEX IF NOT EXISTS idx_na_node ON node_addresses(node_id);" "CREATE TABLE IF NOT EXISTS accounts (" " node_id INTEGER PRIMARY KEY REFERENCES nodes(node_id)," " display_name TEXT NOT NULL," " avatar_color TEXT DEFAULT '#4A90E2'," " avatar_letter TEXT NOT NULL," " is_contact INTEGER DEFAULT 1," " created_at INTEGER DEFAULT (unixepoch())" ");" "CREATE TABLE IF NOT EXISTS channels (" " channel_id TEXT PRIMARY KEY," " name TEXT NOT NULL," " owner_node_id INTEGER," " is_dm INTEGER DEFAULT 0," " x25519_pubkey BLOB NOT NULL," " x25519_privkey BLOB," " ed25519_pubkey BLOB NOT NULL," " ed25519_privkey BLOB," " signature BLOB NOT NULL," " last_read_msg_id INTEGER," " last_pos_msg_id INTEGER," " created_at INTEGER DEFAULT (unixepoch())" ");" "CREATE TABLE IF NOT EXISTS ui_state (" " key TEXT PRIMARY KEY," " value TEXT" ");"; char* err = nullptr; if (sqlite3_exec(m_db, sql, nullptr, nullptr, &err) != SQLITE_OK) { GUI_ERROR("initTables: %s", err); sqlite3_free(err); } else { GUI_INFO("Database tables initialized"); } } /* ── 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; } bool DbManager::createChannel(const QString& chId, const QString& name, quint64 ownerNodeId, const QByteArray& x25519pub, const QByteArray& x25519priv, const QByteArray& ed25519pub, const QByteArray& ed25519priv) { GUI_INFO("createChannel: chId='%s' name='%s'", chId.toUtf8().constData(), name.toUtf8().constData()); QByteArray sig = kZero64; Q_UNUSED(sig); QString msgt = msgTableName(chId), pt = peersTableName(chId); /* per-channel message table (DDL — до INSERT, чтобы не ломать транзакцию) */ QString csql = QStringLiteral( "CREATE TABLE IF NOT EXISTS \"%1\" (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " node_id INTEGER NOT NULL," " content_type TEXT NOT NULL," " data BLOB NOT NULL," " timestamp INTEGER NOT NULL," " datahash INTEGER NOT NULL," " chain_hash BLOB NOT NULL," " signature BLOB," " is_outgoing INTEGER DEFAULT 0," " is_read INTEGER DEFAULT 0," " sync_flags INTEGER DEFAULT 0," " UNIQUE(timestamp, datahash))").arg(msgt); sqlite3_exec(m_db, csql.toUtf8().constData(), nullptr, nullptr, nullptr); csql = QStringLiteral("CREATE INDEX IF NOT EXISTS idx_%1_ts ON \"%1\"(timestamp, datahash)").arg(msgt); sqlite3_exec(m_db, csql.toUtf8().constData(), nullptr, nullptr, nullptr); /* per-channel peers table */ csql = QStringLiteral( "CREATE TABLE IF NOT EXISTS \"%1\" (" " node_id INTEGER NOT NULL," " join_sig BLOB NOT NULL," " creator_sig BLOB," " comment TEXT," " joined_at INTEGER DEFAULT (unixepoch())," " PRIMARY KEY (node_id))").arg(pt); sqlite3_exec(m_db, csql.toUtf8().constData(), nullptr, nullptr, nullptr); /* вставка записи channels (после DDL, корректно внутри транзакции) */ sqlite3_stmt* stmt = prepareOrNull( "INSERT INTO channels(channel_id,name,owner_node_id,is_dm," " x25519_pubkey,x25519_privkey,ed25519_pubkey,ed25519_privkey,signature)" " VALUES(?,?,?,0,?,?,?,?,?)"); if (!stmt) return false; sqlite3_bind_text(stmt, 1, chId.toUtf8().constData(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, name.toUtf8().constData(), -1, SQLITE_TRANSIENT); sqlite3_bind_int64(stmt, 3, (sqlite3_int64)ownerNodeId); sqlite3_bind_blob(stmt, 4, x25519pub.isEmpty() ? kZero32 : x25519pub.constData(), x25519pub.isEmpty() ? 32 : x25519pub.size(), SQLITE_TRANSIENT); sqlite3_bind_blob(stmt, 5, x25519priv.isEmpty() ? nullptr : x25519priv.constData(), x25519priv.size(), SQLITE_TRANSIENT); sqlite3_bind_blob(stmt, 6, ed25519pub.isEmpty() ? kZero32 : ed25519pub.constData(), ed25519pub.isEmpty() ? 32 : ed25519pub.size(), SQLITE_TRANSIENT); sqlite3_bind_blob(stmt, 7, ed25519priv.isEmpty() ? nullptr : ed25519priv.constData(), ed25519priv.size(), SQLITE_TRANSIENT); sqlite3_bind_blob(stmt, 8, sig.constData(), sig.size(), SQLITE_TRANSIENT); bool ok = sqlite3_step(stmt) == SQLITE_DONE; sqlite3_finalize(stmt); if (!ok) { GUI_ERROR("createChannel %s failed: %s", chId.toUtf8().constData(), sqlite3_errmsg(m_db)); return false; } GUI_INFO("Channel created: %s (msg=%s, peers=%s)", chId.toUtf8().constData(), msgt.toUtf8().constData(), pt.toUtf8().constData()); return true; } bool DbManager::deleteChannel(const QString& chId) { QString msgt = msgTableName(chId), pt = peersTableName(chId); sqlite3_exec(m_db, QStringLiteral("DROP TABLE IF EXISTS \"%1\"").arg(msgt).toUtf8().constData(), nullptr, nullptr, nullptr); sqlite3_exec(m_db, QStringLiteral("DROP TABLE IF EXISTS \"%1\"").arg(pt).toUtf8().constData(), nullptr, nullptr, nullptr); sqlite3_stmt* stmt = prepareOrNull("DELETE FROM channels WHERE channel_id=?"); if (!stmt) return false; sqlite3_bind_text(stmt, 1, chId.toUtf8().constData(), -1, SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_finalize(stmt); return true; } /* ── 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; } bool DbManager::insertMessage(const QString& chId, quint64 nodeId, const QString& contentType, const QByteArray& data, qint64 timestamp, bool isOutgoing, uint64_t* outDatahash, QByteArray* outChainHash) { uint64_t dh = computeDatahash(data); QByteArray prevCh = getLastChainHash(chId); QByteArray ch = computeChainHash(prevCh, timestamp, dh); QString sql = QStringLiteral( "INSERT OR IGNORE INTO \"%1\"" " (node_id, content_type, data, timestamp, datahash, chain_hash," " signature, is_outgoing, is_read)" " VALUES(?,?,?,?,?,?,?,?,1)").arg(msgTableName(chId)); sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); if (!stmt) return false; sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId); sqlite3_bind_text(stmt, 2, contentType.toUtf8().constData(), -1, SQLITE_TRANSIENT); sqlite3_bind_blob(stmt, 3, data.constData(), data.size(), SQLITE_TRANSIENT); sqlite3_bind_int64(stmt, 4, timestamp); sqlite3_bind_int64(stmt, 5, (sqlite3_int64)dh); sqlite3_bind_blob(stmt, 6, ch.constData(), 32, SQLITE_TRANSIENT); sqlite3_bind_blob(stmt, 7, kZero64, 64, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 8, isOutgoing ? 1 : 0); bool ok = sqlite3_step(stmt) == SQLITE_DONE; sqlite3_finalize(stmt); if (ok) { if (outDatahash) *outDatahash = dh; if (outChainHash) *outChainHash = ch; } return ok; } /* ── peers ── */ bool DbManager::addPeer(const QString& chId, quint64 nodeId, const QByteArray& joinSig, const QByteArray& creatorSig, const QString& comment) { QString sql = QStringLiteral( "INSERT OR REPLACE INTO \"%1\"(node_id, join_sig, creator_sig, comment)" " VALUES(?,?,?,?)").arg(peersTableName(chId)); sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); if (!stmt) return false; sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId); sqlite3_bind_blob(stmt, 2, joinSig.constData(), joinSig.size(), SQLITE_TRANSIENT); sqlite3_bind_blob(stmt, 3, creatorSig.isEmpty() ? nullptr : creatorSig.constData(), creatorSig.size(), SQLITE_TRANSIENT); if (!comment.isEmpty()) sqlite3_bind_text(stmt, 4, comment.toUtf8().constData(), -1, SQLITE_TRANSIENT); else sqlite3_bind_null(stmt, 4); bool ok = sqlite3_step(stmt) == SQLITE_DONE; sqlite3_finalize(stmt); return ok; } bool DbManager::removePeer(const QString& chId, quint64 nodeId) { QString sql = QStringLiteral( "DELETE FROM \"%1\" WHERE node_id=?").arg(peersTableName(chId)); sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); if (!stmt) return false; sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId); sqlite3_step(stmt); sqlite3_finalize(stmt); return true; } /* ── nodes ── */ bool DbManager::upsertNode(quint64 nodeId, const QString& name, const QByteArray& x25519pub, const QByteArray& ed25519pub) { sqlite3_stmt* stmt = prepareOrNull( "INSERT OR REPLACE INTO nodes(node_id, name, x25519_pubkey, ed25519_pubkey, last_seen_at)" " VALUES(?,?,?,?,unixepoch())"); if (!stmt) return false; sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId); sqlite3_bind_text(stmt, 2, name.toUtf8().constData(), -1, SQLITE_TRANSIENT); sqlite3_bind_blob(stmt, 3, x25519pub.constData(), x25519pub.size(), SQLITE_TRANSIENT); sqlite3_bind_blob(stmt, 4, ed25519pub.isEmpty() ? nullptr : ed25519pub.constData(), ed25519pub.size(), SQLITE_TRANSIENT); bool ok = sqlite3_step(stmt) == SQLITE_DONE; sqlite3_finalize(stmt); return ok; } bool DbManager::addNodeAddress(quint64 nodeId, int family, int protocol, const QByteArray& addr, quint16 port, bool isNat) { sqlite3_stmt* stmt = prepareOrNull( "INSERT OR IGNORE INTO node_addresses(node_id, family, protocol, address, port, is_nat)" " VALUES(?,?,?,?,?,?)"); if (!stmt) return false; sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId); sqlite3_bind_int(stmt, 2, family); sqlite3_bind_int(stmt, 3, protocol); sqlite3_bind_blob(stmt, 4, addr.constData(), addr.size(), SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 5, port); sqlite3_bind_int(stmt, 6, isNat ? 1 : 0); bool ok = sqlite3_step(stmt) == SQLITE_DONE; sqlite3_finalize(stmt); return ok; } 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; } void DbManager::setUiState(const QString& key, const QString& value) { sqlite3_stmt* stmt = prepareOrNull( "INSERT OR REPLACE INTO ui_state(key,value) VALUES(?,?)"); if (!stmt) return; sqlite3_bind_text(stmt, 1, key.toUtf8().constData(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, value.toUtf8().constData(), -1, SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_finalize(stmt); } /* ── sync operations (called from gui_bridge, in GUI thread) ── */ 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::syncInsert(const QString& chId, const QByteArray& recData) { /* recData: [timestamp:8][datahash:8][node_id:8][ct_len:1][ct:var][data_len:4][data:var][chain_hash:32] */ if (recData.size() < 29) return QByteArray(1, (char)-1); const uint8_t* p = (const uint8_t*)recData.constData(); int rem = recData.size(); qint64 ts; uint64_t dh, nid; memcpy(&ts, p, 8); p += 8; rem -= 8; memcpy(&dh, p, 8); p += 8; rem -= 8; memcpy(&nid, p, 8); p += 8; rem -= 8; if (rem < 1) return QByteArray(1, (char)-1); uint8_t ctLen = *p; p++; rem--; if (rem < ctLen) return QByteArray(1, (char)-1); QString ct = QString::fromUtf8((const char*)p, ctLen); p += ctLen; rem -= ctLen; if (rem < 4) return QByteArray(1, (char)-1); uint32_t dLen; memcpy(&dLen, p, 4); p += 4; rem -= 4; if (rem < (int)dLen) return QByteArray(1, (char)-1); QByteArray data((const char*)p, dLen); p += dLen; rem -= dLen; if (rem < 32) return QByteArray(1, (char)-1); QByteArray peerCH((const char*)p, 32); /* verify chain_hash */ QByteArray prev = getLastChainHash(chId); QByteArray expected = computeChainHash(prev, ts, dh); if (expected != peerCH) { GUI_ERROR("syncInsert: chain_hash mismatch for %s ts=%lld dh=%llx", chId.toUtf8().constData(), (long long)ts, (unsigned long long)dh); return QByteArray(1, (char)-1); } QString sql = QStringLiteral( "INSERT OR IGNORE INTO \"%1\"" " (node_id, content_type, data, timestamp, datahash, chain_hash, signature, is_outgoing, is_read)" " VALUES(?,?,?,?,?,?,?,0,1)").arg(msgTableName(chId)); sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); if (!stmt) return QByteArray(1, (char)-1); sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nid); sqlite3_bind_text(stmt, 2, ct.toUtf8().constData(), -1, SQLITE_TRANSIENT); sqlite3_bind_blob(stmt, 3, data.constData(), data.size(), SQLITE_TRANSIENT); sqlite3_bind_int64(stmt, 4, ts); sqlite3_bind_int64(stmt, 5, (sqlite3_int64)dh); sqlite3_bind_blob(stmt, 6, peerCH.constData(), 32, SQLITE_TRANSIENT); sqlite3_bind_blob(stmt, 7, kZero64, 64, SQLITE_TRANSIENT); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc == SQLITE_DONE) return QByteArray(1, '\0'); if (rc == SQLITE_CONSTRAINT) return QByteArray(1, (char)1); /* duplicate */ return QByteArray(1, (char)-1); } 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::syncMarkSent(const QString& chId, quint64 ts, quint64 dh, quint64 myNodeId) { QString sql = QStringLiteral( "UPDATE \"%1\" SET sync_flags=sync_flags|1" " WHERE timestamp=? AND datahash=? AND node_id=? AND is_outgoing=1") .arg(msgTableName(chId)); sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); if (!stmt) return {}; sqlite3_bind_int64(stmt, 1, (sqlite3_int64)ts); sqlite3_bind_int64(stmt, 2, (sqlite3_int64)dh); sqlite3_bind_int64(stmt, 3, (sqlite3_int64)myNodeId); sqlite3_step(stmt); sqlite3_finalize(stmt); return {}; } QByteArray DbManager::syncTtlDelete(const QString& chId, quint64 myNodeId, quint64 cutoffUs) { QString sql = QStringLiteral( "DELETE FROM \"%1\" WHERE node_id=? AND is_outgoing=1 AND (sync_flags&1)=0 AND timestamp 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; } /* ── gui notifications ── */ void DbManager::onSyncMessageReceived(const QString& chId) { Q_UNUSED(chId) /* Обновление last_message в channels не нужно (убран). Вызывается emitter'ом из gui_bridge_impl */ } void DbManager::setNodeOnline(quint64 nodeId, bool online) { Q_UNUSED(nodeId); Q_UNUSED(online) /* is_online убран из nodes. Обновление статуса через conn_mgr — позже */ } /* ── seed stub data ── */ void DbManager::seedStubData() { GUI_INFO("Seeding stub data..."); sqlite3_exec(m_db, "BEGIN", nullptr, nullptr, nullptr); unsigned char pub_dummy[32] = {}; sqlite3_stmt* ins = prepareOrNull( "INSERT OR IGNORE INTO local_identity(id,node_id,name,x25519_pubkey,ed25519_pubkey)" " VALUES(1,?,'MyDevice',?,?)"); sqlite3_bind_int64(ins, 1, m_myNodeId); sqlite3_bind_blob(ins, 2, pub_dummy, 32, SQLITE_TRANSIENT); sqlite3_bind_blob(ins, 3, pub_dummy, 32, SQLITE_TRANSIENT); sqlite3_step(ins); sqlite3_finalize(ins); struct { quint64 id; const char* name; const char* letter; const char* color; } accs[] = { {1, "Alice", "A", "#4A90E2"}, {2, "Bob", "B", "#7ED321"}, {3, "Carol", "C", "#F5A623"}, {4, "Dave", "D", "#9013FE"}, {5, "Eve", "E", "#00B894"}, }; for (auto& a : accs) { ins = prepareOrNull("INSERT OR IGNORE INTO nodes(node_id,name,x25519_pubkey,ed25519_pubkey,last_seen_at)" " VALUES(?,?,?,?,unixepoch())"); sqlite3_bind_int64(ins, 1, (sqlite3_int64)a.id); sqlite3_bind_text(ins, 2, a.name, -1, SQLITE_TRANSIENT); sqlite3_bind_blob(ins, 3, pub_dummy, 32, SQLITE_TRANSIENT); sqlite3_bind_blob(ins, 4, pub_dummy, 32, SQLITE_TRANSIENT); sqlite3_step(ins); sqlite3_finalize(ins); ins = prepareOrNull("INSERT OR IGNORE INTO accounts(node_id,display_name,avatar_color,avatar_letter)" " VALUES(?,?,?,?)"); sqlite3_bind_int64(ins, 1, (sqlite3_int64)a.id); sqlite3_bind_text(ins, 2, a.name, -1, SQLITE_TRANSIENT); sqlite3_bind_text(ins, 3, a.color, -1, SQLITE_TRANSIENT); sqlite3_bind_text(ins, 4, a.letter, -1, SQLITE_TRANSIENT); sqlite3_step(ins); sqlite3_finalize(ins); } /* channels (1 демо-канал) */ QByteArray dummyPub(32, '\0'), dummySig(64, '\0'), dummyCH(32, '\0'); createChannel("ch:general", "#general", 0, dummyPub, QByteArray(), dummyPub, QByteArray()); /* peers */ for (auto& a : accs) { addPeer("ch:general", a.id, dummySig, dummySig, a.name); } /* messages — 0 для простоты, добавим позже в seed v2 */ sqlite3_exec(m_db, "COMMIT", nullptr, nullptr, nullptr); GUI_INFO("Seed data inserted (5 accounts, 1 channel)"); }