chatgui: выпилить все записи в БД из GUI потока (DbManager)
- DbManager теперь read-only, получает sqlite3* handle через setDb() - chat_core — единственный владелец БД (SQLITE_OPEN_FULLMUTEX + WAL) - initTables/seedStubData/createChannel/insertMessage/setUiState удалены из DbManager - seed 5 stub-аккаунтов перенесён в chat_core_init() - GUI_EVT_DB_READY — уведомление о готовности БД - setUiState заменён на QSettings - каналы загружаются по DB_READY коллбэку - убран двойной sqlite3_open (один файл, один хендл)
This commit is contained in:
1 parent
409a99f56b
commit
371208d489
9 files changed
+82
-359
No files matched your search
@@ -7,8 +7,6 @@
|
||||
|
||||
/* ── helpers ── */
|
||||
|
||||
static const char kZero64[64] = {};
|
||||
|
||||
QString DbManager::sanitizeTableName(const QString& chId) {
|
||||
QString s;
|
||||
s.reserve(chId.size());
|
||||
@@ -59,36 +57,15 @@ QByteArray DbManager::getLastChainHash(const QString& chId) const {
|
||||
|
||||
/* ── lifecycle ── */
|
||||
|
||||
DbManager::DbManager(const QString& path, QObject* parent)
|
||||
DbManager::DbManager(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);
|
||||
|
||||
initTables();
|
||||
|
||||
m_myNodeId = 100;
|
||||
sqlite3_stmt* st = prepareOrNull("SELECT node_id FROM local_identity WHERE id=1");
|
||||
if (st) {
|
||||
if (sqlite3_step(st) == SQLITE_ROW) m_myNodeId = (quint64)sqlite3_column_int64(st, 0);
|
||||
sqlite3_finalize(st);
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -112,70 +89,6 @@ QByteArray DbManager::colBlob(sqlite3_stmt* stmt, int col) {
|
||||
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 accounts ("
|
||||
" node_id INTEGER PRIMARY KEY,"
|
||||
" 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 ui_state ("
|
||||
" key TEXT PRIMARY KEY,"
|
||||
" value TEXT"
|
||||
");"
|
||||
|
||||
/* таблицы создаваемые uTun-потоком — дублируем IF NOT EXISTS чтобы GUI мог читать независимо */
|
||||
"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,"
|
||||
" online INTEGER DEFAULT 0,"
|
||||
" 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())"
|
||||
");";
|
||||
|
||||
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 {
|
||||
@@ -206,50 +119,6 @@ QList<ChannelRow> DbManager::getChannels() const {
|
||||
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 (msg table only): chId='%s' name='%s'", chId.toUtf8().constData(), name.toUtf8().constData());
|
||||
(void)ownerNodeId; (void)x25519pub; (void)x25519priv; (void)ed25519pub; (void)ed25519priv;
|
||||
|
||||
QString msgt = msgTableName(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);
|
||||
|
||||
GUI_INFO("Message table ensured: %s", msgt.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);
|
||||
GUI_INFO("Channel deleted: %s", chId.toUtf8().constData());
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── messages (UI) ── */
|
||||
|
||||
QList<MessageRow> DbManager::getMessages(const QString& chId, int limit) const {
|
||||
@@ -279,44 +148,6 @@ QList<MessageRow> DbManager::getMessages(const QString& chId, int limit) const {
|
||||
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) {
|
||||
QString tbl = msgTableName(chId);
|
||||
|
||||
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(tbl);
|
||||
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
|
||||
if (!stmt) {
|
||||
qWarning("DbManager: insertMessage prepare failed: %s", sqlite3_errmsg(m_db));
|
||||
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 (UI) ── */
|
||||
|
||||
int DbManager::getChannelMemberCount(const QString& chId) const {
|
||||
@@ -374,20 +205,6 @@ QString DbManager::getDisplayName(quint64 nodeId) const {
|
||||
return QString("node_%1").arg(nodeId);
|
||||
}
|
||||
|
||||
/* ── nodes (запись теперь через topo_node_sqlite) ── */
|
||||
|
||||
bool DbManager::upsertNode(quint64 nodeId, const QString& name,
|
||||
const QByteArray& x25519pub, const QByteArray& ed25519pub) {
|
||||
(void)nodeId; (void)name; (void)x25519pub; (void)ed25519pub;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DbManager::addNodeAddress(quint64 nodeId, int family, int protocol,
|
||||
const QByteArray& addr, quint16 port, bool isNat) {
|
||||
(void)nodeId; (void)family; (void)protocol; (void)addr; (void)port; (void)isNat;
|
||||
return true;
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -530,16 +347,7 @@ QString DbManager::getUiState(const QString& key) const {
|
||||
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) ── */
|
||||
/* ── sync operations (read-only, for gui_bridge) ── */
|
||||
|
||||
QByteArray DbManager::syncListChannels() const {
|
||||
/* binary: [count:2]([chId_len:1][chId:var])... */
|
||||
@@ -578,55 +386,6 @@ QByteArray DbManager::syncChainHashAt(const QString& chId, uint32_t pos) const {
|
||||
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"
|
||||
@@ -668,32 +427,6 @@ void DbManager::syncCursorClose(uint32_t 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));
|
||||
@@ -710,48 +443,4 @@ QByteArray DbManager::syncListPeers(const QString& chId) const {
|
||||
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 (accounts only)...");
|
||||
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 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);
|
||||
}
|
||||
|
||||
sqlite3_exec(m_db, "COMMIT", nullptr, nullptr, nullptr);
|
||||
GUI_INFO("Seed data inserted (5 accounts)");
|
||||
}
|
||||
/* ── syncListPeers ── */
|
||||
Reference in new issue
Block a user