Browse Source

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 (один файл, один хендл)
topo_upd
Evgeny 3 months ago
parent
commit
371208d489
  1. 317
      tools/chatgui/db/db_manager.cpp
  2. 35
      tools/chatgui/db/db_manager.h
  3. 2
      tools/chatgui/src/main.cpp
  4. 30
      tools/chatgui/src/mainwindow.cpp
  5. 1
      tools/chatgui/src/mainwindow.h
  6. 40
      tools/chatgui/transport/chat_core.c
  7. 2
      tools/chatgui/transport/chat_core.h
  8. 5
      tools/chatgui/transport/gui_bridge.h
  9. 9
      tools/chatgui/transport/gui_bridge_impl.cpp

317
tools/chatgui/db/db_manager.cpp

@ -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 ── */

35
tools/chatgui/db/db_manager.h

@ -65,27 +65,19 @@ struct NodeAddr {
class DbManager : public QObject {
Q_OBJECT
public:
explicit DbManager(const QString& path, QObject* parent = nullptr);
explicit DbManager(QObject* parent = nullptr);
~DbManager() override;
bool isOpen() const { return m_db != nullptr; }
void setDb(sqlite3* db) { m_db = db; }
quint64 myNodeId() const { return m_myNodeId; }
void setMyNodeId(quint64 nodeId) { m_myNodeId = nodeId; }
/* ── Каналы ── */
QList<ChannelRow> getChannels() const;
bool createChannel(const QString& chId, const QString& name,
quint64 ownerNodeId = 0,
const QByteArray& x25519pub = {}, const QByteArray& x25519priv = {},
const QByteArray& ed25519pub = {}, const QByteArray& ed25519priv = {});
bool deleteChannel(const QString& chId);
/* ── Сообщения (UI) ── */
QList<MessageRow> getMessages(const QString& chId, int limit = 50) const;
bool insertMessage(const QString& chId, quint64 nodeId,
const QString& contentType, const QByteArray& data,
qint64 timestamp, bool isOutgoing,
uint64_t* outDatahash, QByteArray* outChainHash);
/* ── Участники канала (UI) ── */
int getChannelMemberCount(const QString& chId) const;
@ -93,10 +85,6 @@ public:
QString getDisplayName(quint64 nodeId) const;
/* ── Узлы ── */
bool upsertNode(quint64 nodeId, const QString& name,
const QByteArray& x25519pub, const QByteArray& ed25519pub);
bool addNodeAddress(quint64 nodeId, int family, int protocol,
const QByteArray& addr, quint16 port, bool isNat);
QByteArray loadNodeInfo(quint64 nodeId) const;
QList<quint64> getOnlinePeers() const;
QByteArray getNodeEdPub(quint64 nodeId) const;
@ -106,33 +94,19 @@ public:
QList<AccountRow> getAccounts(bool contactsOnly = false) const;
AccountRow getAccount(quint64 nodeId) const;
/* ── UI state ── */
/* ── UI state (read-only используем QSettings, оставлен для совместимости) ── */
QString getUiState(const QString& key) const;
void setUiState(const QString& key, const QString& value);
/* ── Sync-операции (вызываются gui_bridge, в GUI-потоке) ── */
/* ── Sync-операции (read-only, для gui_bridge/inline) ── */
QByteArray syncListChannels() const;
QByteArray syncCount(const QString& chId) const;
QByteArray syncChainHashAt(const QString& chId, uint32_t pos) const;
QByteArray syncInsert(const QString& chId, const QByteArray& recData);
QByteArray syncCursorOpen(const QString& chId);
QByteArray syncCursorNext(uint32_t cursorId);
void syncCursorClose(uint32_t cursorId);
QByteArray syncMarkSent(const QString& chId, quint64 ts, quint64 dh, quint64 myNodeId);
QByteArray syncTtlDelete(const QString& chId, quint64 myNodeId, quint64 cutoffUs);
QByteArray syncListPeers(const QString& chId) const;
/* ── Уведомления из uasync ── */
void onSyncMessageReceived(const QString& chId);
void setNodeOnline(quint64 nodeId, bool online);
void setUasyncInstance(void* ua) { m_ua = ua; }
void* uasyncInstance() const { return m_ua; }
/* ── seed ── */
void seedStubData();
private:
void initTables();
sqlite3_stmt* prepareOrNull(const char* sql) const;
static QString colText(sqlite3_stmt* stmt, int col);
static QByteArray colBlob(sqlite3_stmt* stmt, int col);
@ -147,7 +121,6 @@ private:
sqlite3* m_db = nullptr;
quint64 m_myNodeId = 0;
void* m_ua = nullptr;
/* Кэш открытых курсоров для sync-операций */
mutable QMap<uint32_t, sqlite3_stmt*> m_cursors;

2
tools/chatgui/src/main.cpp

@ -70,7 +70,7 @@ int main(int argc, char *argv[]) {
if (!QDir::isAbsolutePath(dbPath))
dbPath = QCoreApplication::applicationDirPath() + "/" + dbPath;
DbManager db(dbPath);
DbManager db;
MainWindow w(nullptr, &db, cfgPath, dbPath, cfg.debugFile(),
cfg.debugLevel(), cfg.debugCategories());
w.show();

30
tools/chatgui/src/mainwindow.cpp

@ -16,6 +16,7 @@
#include <QStyle>
#include <QMessageBox>
#include <QDebug>
#include <QSettings>
extern "C" {
#include "../transport/chat_core.h"
@ -78,7 +79,7 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath,
setupUi();
setupTray();
if (m_db && m_db->isOpen()) {
if (m_db) {
s_mainWindow = this;
gui_bridge_set_msg_received_cb(onMsgReceivedCallback);
gui_bridge_set_channel_updated_cb(onChannelUpdatedCallback);
@ -89,13 +90,25 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath,
});
gui_bridge_set_auto_connect_status_cb(onAutoConnectStatusCallback);
gui_bridge_set_channel_peers_online_cb(onChannelPeersOnlineCallback);
gui_bridge_set_db_ready_cb([]() {
if (!s_mainWindow || !s_mainWindow->m_db) return;
s_mainWindow->m_db->setDb(chat_core_get_db());
s_mainWindow->m_channelList->loadChannels();
QString lastCid = QSettings().value("last_channel_id").toString();
if (!lastCid.isEmpty()) {
s_mainWindow->m_channelList->selectChannel(lastCid);
s_mainWindow->m_accountList->setChannel(lastCid);
}
});
setupNode();
setupMessaging();
connect(m_channelList, &ChannelList::channelSelected,
this, [this](const QString& channelId) {
m_db->setUiState("last_channel_id", channelId);
QSettings().setValue("last_channel_id", channelId);
m_currentChannelId = channelId;
m_messageList->loadChannel(channelId);
m_accountList->setChannel(channelId);
});
@ -108,14 +121,6 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath,
this, &MainWindow::onCreateGroupRequested);
connect(m_channelList, &ChannelList::settingsRequested,
this, &MainWindow::showSettings);
m_channelList->loadChannels();
QString lastCid = m_db->getUiState("last_channel_id");
if (!lastCid.isEmpty()) {
m_channelList->selectChannel(lastCid);
m_accountList->setChannel(lastCid);
}
}
}
@ -266,7 +271,7 @@ void MainWindow::onMessageReceived(const char* ch_id) {
}
void MainWindow::onMembersChanged(const char* ch_id) {
QString cur = m_db->getUiState("last_channel_id");
QString cur = QSettings().value("last_channel_id").toString();
if (cur == QString::fromUtf8(ch_id))
m_accountList->refresh();
}
@ -292,7 +297,8 @@ void MainWindow::reloadChannels() {
m_channelList->selectChannel(m_pendingChannelSelect);
m_accountList->setChannel(m_pendingChannelSelect);
m_messageList->loadChannel(m_pendingChannelSelect);
m_db->setUiState("last_channel_id", m_pendingChannelSelect);
QSettings().setValue("last_channel_id", m_pendingChannelSelect);
m_currentChannelId = m_pendingChannelSelect;
m_pendingChannelSelect.clear();
}
}

1
tools/chatgui/src/mainwindow.h

@ -56,4 +56,5 @@ private:
QString m_cfgDebugLevel;
QString m_cfgDebugCategories;
QString m_pendingChannelSelect;
QString m_currentChannelId;
};

40
tools/chatgui/transport/chat_core.c

@ -131,7 +131,8 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
if (!inst || !db_path) return -1;
memset(&g_cc, 0, sizeof(g_cc));
int rc = sqlite3_open(db_path, &g_cc.db);
int rc = sqlite3_open_v2(db_path, &g_cc.db,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL);
if (rc != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: cannot open DB %s: %s",
CC_ID, db_path, sqlite3_errmsg(g_cc.db));
@ -200,6 +201,36 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
");"
);
/* seed stub accounts if empty */
{
sqlite3_stmt* cnt_st = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT COUNT(*) FROM accounts",
-1, &cnt_st, NULL) == SQLITE_OK) {
if (sqlite3_step(cnt_st) == SQLITE_ROW && sqlite3_column_int(cnt_st, 0) == 0) {
DEBUG_INFO(DEBUG_CATEGORY_CONNECTIVITY, "%s: seeding 5 stub accounts", CC_ID);
struct { int64_t 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 (int i = 0; i < 5; i++) {
sqlite3_stmt* ins = NULL;
sqlite3_prepare_v2(g_cc.db,
"INSERT OR IGNORE INTO accounts(node_id,display_name,avatar_color,avatar_letter)"
" VALUES(?,?,?,?)", -1, &ins, NULL);
if (ins) {
sqlite3_bind_int64(ins, 1, accs[i].id);
sqlite3_bind_text(ins, 2, accs[i].name, -1, SQLITE_STATIC);
sqlite3_bind_text(ins, 3, accs[i].color, -1, SQLITE_STATIC);
sqlite3_bind_text(ins, 4, accs[i].letter, -1, SQLITE_STATIC);
sqlite3_step(ins); sqlite3_finalize(ins);
}
}
}
sqlite3_finalize(cnt_st);
}
}
g_cc.initialized = 1;
/* ensure infrastructure for all existing channels (survives restart) */
@ -220,9 +251,16 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
{ uint8_t nid[8]; memcpy(nid, &g_cc.my_node_id, 8); gui_bridge_post(GUI_EVT_MY_NODE_ID, nid, 8); }
/* notify GUI: DB is ready (tables created, data seeded) */
gui_bridge_post(GUI_EVT_DB_READY, NULL, 0);
return 0;
}
sqlite3* chat_core_get_db(void) {
return g_cc.db;
}
void chat_core_destroy(struct UTUN_INSTANCE* inst) {
(void)inst;
if (!g_cc.initialized) return;

2
tools/chatgui/transport/chat_core.h

@ -19,6 +19,8 @@ struct UTUN_INSTANCE;
int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path);
void chat_core_destroy(struct UTUN_INSTANCE* inst);
struct sqlite3* chat_core_get_db(void);
/* ── Отправка сообщения (GUI → uasync) ── */
struct chat_msg_submit {

5
tools/chatgui/transport/gui_bridge.h

@ -20,6 +20,7 @@ struct UASYNC;
#define GUI_EVT_MY_NODE_ID 6 /* data: [node_id:8] */
#define GUI_EVT_AUTO_CONNECT_STATUS 7 /* data: [status:1][total_tried:2][node_count:2][success:2] */
#define GUI_EVT_CHANNEL_PEERS_ONLINE 8 /* data: [ch_id_len:1][ch_id:var][online_count:2] */
#define GUI_EVT_DB_READY 9 /* data: none — DB is open, tables created */
/* ── API ── */
@ -67,6 +68,10 @@ void gui_bridge_set_auto_connect_status_cb(gui_auto_connect_status_fn cb);
typedef void (*gui_channel_peers_online_fn)(const char* ch_id, int ch_id_len, uint16_t online_count);
void gui_bridge_set_channel_peers_online_cb(gui_channel_peers_online_fn cb);
/* Callback: DB открыта, таблицы созданы, можно загружать данные (вызывается из GUI-потока) */
typedef void (*gui_db_ready_fn)(void);
void gui_bridge_set_db_ready_cb(gui_db_ready_fn cb);
#ifdef __cplusplus
}
#endif

9
tools/chatgui/transport/gui_bridge_impl.cpp

@ -31,6 +31,7 @@ static gui_members_changed_fn g_members_changed_cb = nullptr;
static gui_my_node_id_fn g_my_node_id_cb = nullptr;
static gui_auto_connect_status_fn g_auto_connect_status_cb = nullptr;
static gui_channel_peers_online_fn g_channel_peers_online_cb = nullptr;
static gui_db_ready_fn g_db_ready_cb = nullptr;
static struct UASYNC* g_ua = nullptr;
/* ── GuiBridgeReceiver implementation ── */
@ -121,6 +122,10 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) {
}
}
break;
case GUI_EVT_DB_READY:
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "gui_bridge: DB_READY");
if (g_db_ready_cb) g_db_ready_cb();
break;
default:
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: unknown event type %d", eventType);
break;
@ -195,6 +200,10 @@ void gui_bridge_set_channel_peers_online_cb(gui_channel_peers_online_fn cb) {
g_channel_peers_online_cb = cb;
}
void gui_bridge_set_db_ready_cb(gui_db_ready_fn cb) {
g_db_ready_cb = cb;
}
} /* extern "C" */
#include "gui_bridge_impl.moc"

Loading…
Cancel
Save