- gui_bridge.h/cpp — кросс-поточные асинхронные коллбэки uasync↔GUI - chat_sync.h/c — протокол синхронизации (INIT_SYNC/RESP, SEND_DATA, PUSH/ACK, SYNC_DONE) - DbManager полностью переписан: per-channel таблицы msg_<ch> + peers_<ch>, 4 ключа в channels, signature, protocol/rtt в node_addresses, sync-методы - ChatPropagator удалён, заменён на chat_sync - db_sync_stub.c — заглушки для сборки chatgui без LMDB-версии db_sync - db_schema.md актуализирован под новую схему
804 lines
33 KiB
C++
804 lines
33 KiB
C++
#include "db_manager.h"
|
|
#include "../src/debug_ui.h"
|
|
#include <openssl/sha.h>
|
|
#include <openssl/evp.h>
|
|
#include <cstring>
|
|
#include <QSet>
|
|
|
|
/* ── 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<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);
|
|
}
|
|
|
|
/* ── 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<ChannelRow> DbManager::getChannels() const {
|
|
QList<ChannelRow> 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) {
|
|
QByteArray sig = kZero64; Q_UNUSED(sig);
|
|
|
|
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_STATIC);
|
|
sqlite3_bind_text(stmt, 2, name.toUtf8().constData(), -1, SQLITE_STATIC);
|
|
sqlite3_bind_int64(stmt, 3, (sqlite3_int64)ownerNodeId);
|
|
sqlite3_bind_blob(stmt, 4, x25519pub.isEmpty() ? kZero32 : x25519pub.constData(),
|
|
x25519pub.isEmpty() ? 32 : x25519pub.size(), SQLITE_STATIC);
|
|
sqlite3_bind_blob(stmt, 5, x25519priv.isEmpty() ? nullptr : x25519priv.constData(),
|
|
x25519priv.size(), SQLITE_STATIC);
|
|
sqlite3_bind_blob(stmt, 6, ed25519pub.isEmpty() ? kZero32 : ed25519pub.constData(),
|
|
ed25519pub.isEmpty() ? 32 : ed25519pub.size(), SQLITE_STATIC);
|
|
sqlite3_bind_blob(stmt, 7, ed25519priv.isEmpty() ? nullptr : ed25519priv.constData(),
|
|
ed25519priv.size(), SQLITE_STATIC);
|
|
sqlite3_bind_blob(stmt, 8, sig.constData(), sig.size(), SQLITE_STATIC);
|
|
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; }
|
|
|
|
/* per-channel message table */
|
|
QString msgt = msgTableName(chId), pt = peersTableName(chId);
|
|
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);
|
|
|
|
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_STATIC);
|
|
sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
return true;
|
|
}
|
|
|
|
/* ── messages (UI) ── */
|
|
|
|
QList<MessageRow> DbManager::getMessages(const QString& chId, int limit) const {
|
|
QList<MessageRow> 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_STATIC);
|
|
sqlite3_bind_blob(stmt, 3, data.constData(), data.size(), SQLITE_STATIC);
|
|
sqlite3_bind_int64(stmt, 4, timestamp);
|
|
sqlite3_bind_int64(stmt, 5, (sqlite3_int64)dh);
|
|
sqlite3_bind_blob(stmt, 6, ch.constData(), 32, SQLITE_STATIC);
|
|
sqlite3_bind_blob(stmt, 7, kZero64, 64, SQLITE_STATIC);
|
|
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_STATIC);
|
|
sqlite3_bind_blob(stmt, 3, creatorSig.isEmpty() ? nullptr : creatorSig.constData(),
|
|
creatorSig.size(), SQLITE_STATIC);
|
|
if (!comment.isEmpty())
|
|
sqlite3_bind_text(stmt, 4, comment.toUtf8().constData(), -1, SQLITE_STATIC);
|
|
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_STATIC);
|
|
sqlite3_bind_blob(stmt, 3, x25519pub.constData(), x25519pub.size(), SQLITE_STATIC);
|
|
sqlite3_bind_blob(stmt, 4, ed25519pub.isEmpty() ? nullptr : ed25519pub.constData(),
|
|
ed25519pub.size(), SQLITE_STATIC);
|
|
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_STATIC);
|
|
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<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.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<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_STATIC);
|
|
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_STATIC);
|
|
sqlite3_bind_text(stmt, 2, value.toUtf8().constData(), -1, SQLITE_STATIC);
|
|
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<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, 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_STATIC);
|
|
sqlite3_bind_blob(stmt, 3, data.constData(), data.size(), SQLITE_STATIC);
|
|
sqlite3_bind_int64(stmt, 4, ts);
|
|
sqlite3_bind_int64(stmt, 5, (sqlite3_int64)dh);
|
|
sqlite3_bind_blob(stmt, 6, peerCH.constData(), 32, SQLITE_STATIC);
|
|
sqlite3_bind_blob(stmt, 7, kZero64, 64, SQLITE_STATIC);
|
|
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<?")
|
|
.arg(msgTableName(chId));
|
|
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
|
|
if (!stmt) return {};
|
|
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)myNodeId);
|
|
sqlite3_bind_int64(stmt, 2, (sqlite3_int64)cutoffUs);
|
|
sqlite3_step(stmt); sqlite3_finalize(stmt);
|
|
return {};
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/* ── 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_STATIC);
|
|
sqlite3_bind_blob(ins, 3, pub_dummy, 32, SQLITE_STATIC);
|
|
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_STATIC);
|
|
sqlite3_bind_blob(ins, 3, pub_dummy, 32, SQLITE_STATIC);
|
|
sqlite3_bind_blob(ins, 4, pub_dummy, 32, SQLITE_STATIC);
|
|
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_STATIC);
|
|
sqlite3_bind_text(ins, 3, a.color, -1, SQLITE_STATIC);
|
|
sqlite3_bind_text(ins, 4, a.letter, -1, SQLITE_STATIC);
|
|
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)");
|
|
}
|