From b5cdfb3dcee084160a39dc4dfd9239318443ad2f Mon Sep 17 00:00:00 2001 From: tux3 Date: Thu, 17 Dec 2015 11:24:01 +0100 Subject: [PATCH] Implement new SQLCipher based database and history qTox will automatically import the old history on startup. This new database code is much more robust. It is very resilient and will not corrupt or disappear after a crash or power failure, unlike the old code. The on-disk database format is also much more compact now. The database sync option in the advanced settings has been removed, we know run many database operations asynchronously so performance should not be a problem anymore, but we always ensure resiliency in case of abrupt termination, so there is no tradeoff anymore. --- qtox.pro | 16 +- src/core/core.cpp | 8 +- src/core/coreencryption.cpp | 4 +- src/core/toxid.cpp | 2 +- src/core/toxid.h | 2 +- src/friend.cpp | 5 +- src/group.cpp | 2 +- src/persistence/db/rawdatabase.cpp | 466 +++++++++++++++++++ src/persistence/db/rawdatabase.h | 115 +++++ src/persistence/history.cpp | 231 +++++++++ src/persistence/history.h | 77 +++ src/persistence/historykeeper.cpp | 309 +----------- src/persistence/historykeeper.h | 38 +- src/persistence/offlinemsgengine.cpp | 17 +- src/persistence/offlinemsgengine.h | 6 +- src/persistence/profile.cpp | 49 +- src/persistence/profile.h | 16 +- src/persistence/settings.cpp | 1 - src/widget/about/aboutuser.cpp | 5 +- src/widget/form/addfriendform.cpp | 2 +- src/widget/form/chatform.cpp | 29 +- src/widget/form/genericchatform.cpp | 4 +- src/widget/form/groupchatform.cpp | 1 - src/widget/form/profileform.cpp | 1 - src/widget/form/settings/advancedform.cpp | 33 +- src/widget/form/settings/advancedform.h | 1 - src/widget/form/settings/advancedsettings.ui | 37 +- src/widget/form/settings/privacyform.cpp | 6 +- src/widget/friendlistwidget.cpp | 1 - src/widget/gui.cpp | 7 +- src/widget/widget.cpp | 7 +- 31 files changed, 1044 insertions(+), 454 deletions(-) create mode 100644 src/persistence/db/rawdatabase.cpp create mode 100644 src/persistence/db/rawdatabase.h create mode 100644 src/persistence/history.cpp create mode 100644 src/persistence/history.h diff --git a/qtox.pro b/qtox.pro index 2c8b487da..9a11f24cf 100644 --- a/qtox.pro +++ b/qtox.pro @@ -144,7 +144,7 @@ win32 { LIBS += -L$$PWD/libs/lib -ltoxav -ltoxcore -ltoxencryptsave -ltoxdns -lsodium -lvpx -lpthread LIBS += -L$$PWD/libs/lib -lavdevice -lavformat -lavcodec -lavutil -lswscale -lOpenAL32 -lopus LIBS += -lopengl32 -lole32 -loleaut32 -lvfw32 -lws2_32 -liphlpapi -lgdi32 -lshlwapi -luuid - LIBS += -lqrencode + LIBS += -lqrencode -lsqlcipher LIBS += -lstrmiids # For DirectShow contains(DEFINES, QTOX_FILTER_AUDIO) { contains(STATICPKG, YES) { @@ -160,7 +160,7 @@ win32 { QMAKE_INFO_PLIST = osx/info.plist QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.7 LIBS += -L$$PWD/libs/lib/ -ltoxcore -ltoxav -ltoxencryptsave -ltoxdns -lsodium -lvpx -lopus -framework OpenAL -lavformat -lavdevice -lavcodec -lavutil -lswscale -mmacosx-version-min=10.7 - LIBS += -lqrencode + LIBS += -lqrencode -lsqlcipher contains(DEFINES, QTOX_PLATFORM_EXT) { LIBS += -framework IOKit -framework CoreFoundation } contains(DEFINES, QTOX_FILTER_AUDIO) { LIBS += -lfilteraudio } } else { @@ -181,10 +181,10 @@ win32 { LIBS += -L$$PWD/libs/lib/ -lopus -lvpx -lopenal -Wl,-Bstatic -ltoxcore -ltoxav -ltoxencryptsave -ltoxdns -lsodium -lavformat -lavdevice -lavcodec -lavutil -lswscale -lz -Wl,-Bdynamic LIBS += -Wl,-Bstatic -ljpeg -ltiff -lpng -ljasper -lIlmImf -lIlmThread -lIex -ldc1394 -lraw1394 -lHalf -lz -llzma -ljbig LIBS += -Wl,-Bdynamic -lv4l1 -lv4l2 -lavformat -lavcodec -lavutil -lswscale -lusb-1.0 - LIBS += -lqrencode + LIBS += -lqrencode -lsqlcipher } else { LIBS += -L$$PWD/libs/lib/ -ltoxcore -ltoxav -ltoxencryptsave -ltoxdns -lvpx -lsodium -lopenal -lavformat -lavdevice -lavcodec -lavutil -lswscale - LIBS += -lqrencode + LIBS += -lqrencode -lsqlcipher } contains(DEFINES, QTOX_PLATFORM_EXT) { @@ -515,7 +515,9 @@ SOURCES += \ src/widget/tool/removefrienddialog.cpp \ src/video/groupnetcamview.cpp \ src/core/toxcall.cpp \ - src/widget/about/aboutuser.cpp + src/widget/about/aboutuser.cpp \ + src/persistence/db/rawdatabase.cpp \ + src/persistence/history.cpp HEADERS += \ src/audio/audio.h \ @@ -569,4 +571,6 @@ HEADERS += \ src/video/groupnetcamview.h \ src/core/indexedlist.h \ src/core/toxcall.h \ - src/widget/about/aboutuser.h + src/widget/about/aboutuser.h \ + src/persistence/db/rawdatabase.h \ + src/persistence/history.h diff --git a/src/core/core.cpp b/src/core/core.cpp index 361cc67d0..0f18b63f5 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -25,7 +25,6 @@ #include "src/core/coreav.h" #include "src/persistence/settings.h" #include "src/widget/gui.h" -#include "src/persistence/historykeeper.h" #include "src/audio/audio.h" #include "src/persistence/profilelocker.h" #include "src/net/avatarbroadcaster.h" @@ -270,7 +269,8 @@ void Core::start() if (!id.isEmpty()) emit idSet(id); - // tox core is already decrypted + /// TODO: NOTE: This is a backwards compatibility check, + /// once most people have been upgraded away from the old HistoryKeeper, remove this if (Nexus::getProfile()->isEncrypted()) checkEncryptedHistory(); @@ -593,7 +593,9 @@ void Core::requestFriendship(const QString& friendAddress, const QString& messag if (message.length()) inviteStr = tr("/me offers friendship, \"%1\"").arg(message); - HistoryKeeper::getInstance()->addChatEntry(userId, inviteStr, getSelfId().publicKey, QDateTime::currentDateTime(), true, QString()); + Profile* profile = Nexus::getProfile(); + if (profile->isHistoryEnabled()) + profile->getHistory()->addNewMessage(userId, inviteStr, getSelfId().publicKey, QDateTime::currentDateTime(), true, QString()); emit friendAdded(friendId, userId); emit friendshipChanged(friendId); } diff --git a/src/core/coreencryption.cpp b/src/core/coreencryption.cpp index ae1813e86..50ed20f99 100644 --- a/src/core/coreencryption.cpp +++ b/src/core/coreencryption.cpp @@ -25,9 +25,9 @@ #include "src/widget/gui.h" #include "src/persistence/settings.h" #include "src/core/cstring.h" -#include "src/persistence/historykeeper.h" #include "src/nexus.h" #include "src/persistence/profile.h" +#include "src/persistence/historykeeper.h" #include #include #include @@ -118,6 +118,8 @@ void Core::checkEncryptedHistory() { QString path = HistoryKeeper::getHistoryPath(); bool exists = QFile::exists(path) && QFile(path).size()>0; + if (!exists) + return; QByteArray salt = getSaltFromFile(path); if (exists && salt.size() == 0) diff --git a/src/core/toxid.cpp b/src/core/toxid.cpp index 5b4b7410c..a20de0b80 100644 --- a/src/core/toxid.cpp +++ b/src/core/toxid.cpp @@ -62,7 +62,7 @@ bool ToxId::operator!=(const ToxId &other) const return publicKey != other.publicKey; } -bool ToxId::isActiveProfile() const +bool ToxId::isSelf() const { return *this == Core::getInstance()->getSelfId(); } diff --git a/src/core/toxid.h b/src/core/toxid.h index 65551df9e..10fc3c728 100644 --- a/src/core/toxid.h +++ b/src/core/toxid.h @@ -38,7 +38,7 @@ public: bool operator==(const ToxId& other) const; ///< Compares only publicKey. bool operator!=(const ToxId& other) const; ///< Compares only publicKey. - bool isActiveProfile() const; ///< Returns true if this Tox ID is equals to + bool isSelf() const; ///< Returns true if this Tox ID is equals to /// the Tox ID of the currently active profile. QString toString() const; ///< Returns the Tox ID as QString. void clear(); ///< Clears all elements of the Tox ID. diff --git a/src/friend.cpp b/src/friend.cpp index 8d2b5b22b..a2db70fb7 100644 --- a/src/friend.cpp +++ b/src/friend.cpp @@ -25,7 +25,8 @@ #include "widget/gui.h" #include "src/core/core.h" #include "src/persistence/settings.h" -#include "src/persistence/historykeeper.h" +#include "src/persistence/profile.h" +#include "src/nexus.h" Friend::Friend(uint32_t FriendId, const ToxId &UserId) : userName{Core::getInstance()->getPeerName(UserId)}, @@ -50,7 +51,7 @@ Friend::~Friend() void Friend::loadHistory() { - if (Settings::getInstance().getEnableLogging()) + if (Nexus::getProfile()->isHistoryEnabled()) { chatForm->loadHistory(QDateTime::currentDateTime().addDays(-7), true); widget->historyLoaded = true; diff --git a/src/group.cpp b/src/group.cpp index cdd3c375d..3497263de 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -86,7 +86,7 @@ void Group::regeneratePeerList() for (int i = 0; i < nPeers; i++) { ToxId id = Core::getInstance()->getGroupPeerToxId(groupId, i); - if (id.isActiveProfile()) + if (id.isSelf()) selfPeerNum = i; QString toxid = id.publicKey; diff --git a/src/persistence/db/rawdatabase.cpp b/src/persistence/db/rawdatabase.cpp new file mode 100644 index 000000000..cc533e7e8 --- /dev/null +++ b/src/persistence/db/rawdatabase.cpp @@ -0,0 +1,466 @@ +#include "rawdatabase.h" +#include +#include +#include +#include +#include +#include +#include + +/// The two following defines are required to use SQLCipher +/// They are used by the sqlite3.h header +#define SQLITE_HAS_CODEC +#define SQLITE_TEMP_STORE 2 + +#include + +RawDatabase::RawDatabase(const QString &path, const QString& password) + : workerThread{new QThread}, path{path}, currentHexKey{deriveKey(password)} +{ + workerThread->setObjectName("qTox Database"); + moveToThread(workerThread.get()); + workerThread->start(); + + if (!open(path, currentHexKey)) + return; +} + +RawDatabase::~RawDatabase() +{ + close(); + workerThread->exit(0); + while (workerThread->isRunning()) + workerThread->wait(50); +} + +bool RawDatabase::open(const QString& path, const QString &hexKey) +{ + if (QThread::currentThread() != workerThread.get()) + { + bool ret; + QMetaObject::invokeMethod(this, "open", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, ret), + Q_ARG(const QString&, path), Q_ARG(const QString&, hexKey)); + return ret; + } + + if (!QFile::exists(path) && QFile::exists(path+".tmp")) + { + qWarning() << "Restoring database from temporary export file! Did we crash while changing the password?"; + QFile::rename(path+".tmp", path); + } + + if (sqlite3_open_v2(path.toUtf8().data(), &sqlite, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX, nullptr) != SQLITE_OK) + { + qWarning() << "Failed to open database"<{statement}); +} + +bool RawDatabase::execNow(const QVector &statements) +{ + if (!sqlite) + { + qWarning() << "Trying to exec, but the database is not open"; + return false; + } + + std::atomic_bool done{false}; + std::atomic_bool success{false}; + + Transaction trans; + trans.queries = statements; + trans.done = &done; + trans.success = &success; + { + QMutexLocker locker{&transactionsMutex}; + pendingTransactions.enqueue(trans); + } + + // We can't use blocking queued here, otherwise we might process future transactions + // before returning, but we only want to wait until this transaction is done. + QMetaObject::invokeMethod(this, "process"); + while (!done.load(std::memory_order_acquire)) + QThread::msleep(10); + + return success.load(std::memory_order_acquire); +} + +void RawDatabase::execLater(const QString &statement) +{ + execLater(Query{statement}); +} + +void RawDatabase::execLater(const RawDatabase::Query &statement) +{ + execLater(QVector{statement}); +} + +void RawDatabase::execLater(const QVector &statements) +{ + if (!sqlite) + { + qWarning() << "Trying to exec, but the database is not open"; + return; + } + + Transaction trans; + trans.queries = statements; + { + QMutexLocker locker{&transactionsMutex}; + pendingTransactions.enqueue(trans); + } + + QMetaObject::invokeMethod(this, "process"); +} + +void RawDatabase::sync() +{ + QMetaObject::invokeMethod(this, "process", Qt::BlockingQueuedConnection); +} + +bool RawDatabase::setPassword(const QString& password) +{ + if (!sqlite) + { + qWarning() << "Trying to change the password, but the database is not open"; + return false; + } + + if (QThread::currentThread() != workerThread.get()) + { + bool ret; + QMetaObject::invokeMethod(this, "setPassword", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, ret), Q_ARG(const QString&, password)); + return ret; + } + + // If we need to decrypt or encrypt, we'll need to sync and close, + // so we always process the pending queue before rekeying for consistency + process(); + + if (QFile::exists(path+".tmp")) + { + qWarning() << "Found old temporary export file while rekeying, deleting it"; + QFile::remove(path+".tmp"); + } + + if (!password.isEmpty()) + { + QString newHexKey = deriveKey(password); + if (!currentHexKey.isEmpty()) + { + if (!execNow("PRAGMA rekey = \"x'"+newHexKey+"'\"")) + { + qWarning() << "Failed to change encryption key"; + close(); + return false; + } + } + else + { + // Need to encrypt the database + if (!execNow("ATTACH DATABASE '"+path+".tmp' AS encrypted KEY \"x'"+newHexKey+"'\";" + "SELECT sqlcipher_export('encrypted');" + "DETACH DATABASE encrypted;")) + { + qWarning() << "Failed to export encrypted database"; + close(); + return false; + } + + // This is racy as hell, but nobody will race with us since we hold the profile lock + // If we crash or die here, the rename should be atomic, so we can recover no matter what + close(); + QFile::remove(path); + QFile::rename(path+".tmp", path); + currentHexKey = newHexKey; + if (!open(path, currentHexKey)) + { + qWarning() << "Failed to open encrypted database"; + return false; + } + } + } + else + { + if (currentHexKey.isEmpty()) + return true; + + // Need to decrypt the database + if (!execNow("ATTACH DATABASE '"+path+".tmp' AS plaintext KEY '';" + "SELECT sqlcipher_export('plaintext');" + "DETACH DATABASE plaintext;")) + { + qWarning() << "Failed to export decrypted database"; + close(); + return false; + } + + // This is racy as hell, but nobody will race with us since we hold the profile lock + // If we crash or die here, the rename should be atomic, so we can recover no matter what + close(); + QFile::remove(path); + QFile::rename(path+".tmp", path); + currentHexKey.clear(); + if (!open(path)) + { + qCritical() << "Failed to open decrypted database"; + return false; + } + } + return true; +} + +bool RawDatabase::rename(const QString &newPath) +{ + if (!sqlite) + { + qWarning() << "Trying to change the password, but the database is not open"; + return false; + } + + if (QThread::currentThread() != workerThread.get()) + { + bool ret; + QMetaObject::invokeMethod(this, "rename", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, ret), Q_ARG(const QString&, newPath)); + return ret; + } + + process(); + + if (path == newPath) + return true; + + if (QFile::exists(newPath)) + return false; + + close(); + if (!QFile::rename(path, newPath)) + return false; + path = newPath; + return open(path, currentHexKey); +} + + +QString RawDatabase::deriveKey(QString password) +{ + if (password.isEmpty()) + return {}; + + QByteArray passData = password.toUtf8(); + + static_assert(TOX_PASS_KEY_LENGTH >= 32, "toxcore must provide 256bit or longer keys"); + + static const uint8_t expandConstant[TOX_PASS_SALT_LENGTH+1] = "L'ignorance est le pire des maux"; + TOX_PASS_KEY key; + tox_derive_key_with_salt((uint8_t*)passData.data(), passData.size(), expandConstant, &key, nullptr); + return QByteArray((char*)key.key, 32).toHex(); +} + +void RawDatabase::process() +{ + assert(QThread::currentThread() == workerThread.get()); + + if (!sqlite) + return; + + forever + { + // Fetch the next transaction + Transaction trans; + { + QMutexLocker locker{&transactionsMutex}; + if (pendingTransactions.isEmpty()) + return; + trans = pendingTransactions.dequeue(); + } + + // In case we exit early, prepare to signal errors + if (trans.success != nullptr) + trans.success->store(false, std::memory_order_release); + + // Add transaction commands if necessary + if (trans.queries.size() > 1) + { + trans.queries.prepend(Query{"BEGIN;"}); + trans.queries.append({"COMMIT;"}); + } + + // Compile queries + for (Query& query : trans.queries) + { + assert(query.statements.isEmpty()); + // sqlite3_prepare_v2 only compiles one statement at a time in the query, we need to loop over them all + int curParam=0; + const char* compileTail = query.query.data(); + do { + // Compile the next statement + sqlite3_stmt* stmt; + int r; + if ((r = sqlite3_prepare_v2(sqlite, compileTail, + query.query.size() - static_cast(compileTail - query.query.data()), + &stmt, &compileTail)) != SQLITE_OK) + { + qWarning() << "Failed to prepare statement"< row; + for (int i=0; istore(true, std::memory_order_release); + + // Free our statements + cleanupStatements: + for (Query& query : trans.queries) + { + for (sqlite3_stmt* stmt : query.statements) + sqlite3_finalize(stmt); + query.statements.clear(); + } + + // Signal transaction results + if (trans.done != nullptr) + trans.done->store(true, std::memory_order_release); + } +} + +QVariant RawDatabase::extractData(sqlite3_stmt *stmt, int col) +{ + int type = sqlite3_column_type(stmt, col); + if (type == SQLITE_INTEGER) + { + return sqlite3_column_int64(stmt, col); + } + else if (type == SQLITE_TEXT) + { + const char* str = reinterpret_cast(sqlite3_column_text(stmt, col)); + int len = sqlite3_column_bytes(stmt, col); + return QString::fromUtf8(str, len); + } + else if (type == SQLITE_NULL) + { + return QVariant{}; + } + else + { + const char* data = reinterpret_cast(sqlite3_column_blob(stmt, col)); + int len = sqlite3_column_bytes(stmt, col); + return QByteArray::fromRawData(data, len); + } +} diff --git a/src/persistence/db/rawdatabase.h b/src/persistence/db/rawdatabase.h new file mode 100644 index 000000000..558738bde --- /dev/null +++ b/src/persistence/db/rawdatabase.h @@ -0,0 +1,115 @@ +#ifndef RAWDATABASE_H +#define RAWDATABASE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct sqlite3; +struct sqlite3_stmt; + +/// Implements a low level RAII interface to a SQLCipher (SQlite3) database +/// Thread-safe, does all database operations on a worker thread +/// The queries must not contain transaction commands (BEGIN/COMMIT/...) or the behavior is undefined +class RawDatabase : QObject +{ + Q_OBJECT + +public: + /// A query to be executed by the database. Can be composed of one or more SQL statements in the query, + /// optional BLOB parameters to be bound, and callbacks fired when the query is executed + /// Calling any database method from a query callback is undefined behavior + class Query + { + public: + Query(QString query, QVector blobs = {}, std::function insertCallback={}) + : query{query.toUtf8()}, blobs{blobs}, insertCallback{insertCallback} {} + Query(QString query, std::function insertCallback) + : query{query.toUtf8()}, insertCallback{insertCallback} {} + Query(QString query, std::function&)> rowCallback) + : query{query.toUtf8()}, rowCallback{rowCallback} {} + Query() = default; + private: + QByteArray query; ///< UTF-8 query string + QVector blobs; ///< Bound data blobs + std::function insertCallback; ///< Called after execution with the last insert rowid + std::function&)> rowCallback; ///< Called during execution for each row + QVector statements; ///< Statements to be compiled from the query + + friend class RawDatabase; + }; + +public: + /// Tries to open a database + /// If password is empty, the database will be opened unencrypted + /// Otherwise we will use toxencryptsave to derive a key and encrypt the database + RawDatabase(const QString& path, const QString& password); + ~RawDatabase(); + bool isOpen(); ///< Returns true if the database was opened successfully + /// Executes a SQL transaction synchronously. + /// Returns whether the transaction was successful. + bool execNow(const QString& statement); + bool execNow(const Query& statement); + bool execNow(const QVector& statements); + /// Executes a SQL transaction asynchronously. + void execLater(const QString& statement); + void execLater(const Query& statement); + void execLater(const QVector& statements); + /// Waits until all the pending transactions are executed + void sync(); + +public slots: + /// Changes the database password, encrypting or decrypting if necessary + /// If password is empty, the database will be decrypted + /// Will process all transactions before changing the password + bool setPassword(const QString& password); + /// Moves the database file on disk to match the new path + /// /// Will process all transactions before renaming + bool rename(const QString& newPath); + +protected slots: + /// Should only be called from the constructor, runs on the caller's thread + bool open(const QString& path, const QString& hexKey = {}); + /// Should only be called from the destructor, runs on the caller's thread + void close(); + /// Implements the actual processing of pending transactions + /// Unqueues, compiles, binds and executes queries, then notifies of results + /// MUST only be called from the worker thread + void process(); + /// Extracts a variant from one column of a result row depending on the column type + QVariant extractData(sqlite3_stmt* stmt, int col); + +protected: + /// Derives a 256bit key from the password and returns it hex-encoded + static QString deriveKey(QString password); + +private: + /// SQL transactions to be processed + /// A transaction is made of queries, which can have bound BLOBs + struct Transaction + { + QVector queries; + /// If not a nullptr, the result of the transaction will be set + std::atomic_bool* success = nullptr; + /// If not a nullptr, will be set to true when the transaction has been executed + std::atomic_bool* done = nullptr; + }; + +private: + sqlite3* sqlite; + std::unique_ptr workerThread; + QQueue pendingTransactions; + /// Protects pendingTransactions + QMutex transactionsMutex; + QString path; + QString currentHexKey; +}; + +#endif // RAWDATABASE_H diff --git a/src/persistence/history.cpp b/src/persistence/history.cpp new file mode 100644 index 000000000..0e9ac9ec3 --- /dev/null +++ b/src/persistence/history.cpp @@ -0,0 +1,231 @@ +#include "history.h" +#include "src/persistence/profile.h" +#include "src/persistence/settings.h" +#include "src/persistence/db/rawdatabase.h" +#include "src/persistence/historykeeper.h" +#include +#include + +using namespace std; + +History::History(const QString &profileName, const QString &password) + : db{getDbPath(profileName), password} +{ + init(); +} + +History::History(const QString &profileName, const QString &password, const HistoryKeeper &oldHistory) + : History{profileName, password} +{ + import(oldHistory); +} + +History::~History() +{ + // We could have execLater requests pending with a lambda attached, + // so clear the pending transactions first + db.sync(); +} + +bool History::isValid() +{ + return db.isOpen(); +} + +void History::setPassword(const QString& password) +{ + db.setPassword(password); +} + +void History::rename(const QString &newName) +{ + db.rename(getDbPath(newName)); +} + +void History::eraseHistory() +{ + db.execNow("DELETE FROM faux_offline_pending;" + "DELETE FROM history;" + "DELETE FROM aliases;" + "DELETE FROM peers;" + "VACUUM;"); +} + +void History::removeFriendHistory(const QString &friendPk) +{ + if (!peers.contains(friendPk)) + return; + int64_t id = peers[friendPk]; + + if (db.execNow(QString("DELETE FROM faux_offline_pending " + "WHERE faux_offline_pending.id IN ( " + "SELECT faux_offline_pending.id FROM faux_offline_pending " + "LEFT JOIN history ON faux_offline_pending.id = history.id " + "WHERE chat_id=%1 " + "); " + "DELETE FROM history WHERE chat_id=%1; " + "DELETE FROM aliases WHERE owner=%1; " + "DELETE FROM peers WHERE id=%1; " + "VACUUM;").arg(id))) + { + peers.remove(friendPk); + } + else + { + qWarning() << "Failed to remove friend's history"; + } +} + +QVector History::generateNewMessageQueries(const QString &friendPk, const QString &message, + const QString &sender, const QDateTime &time, bool isSent, QString dispName, + std::function insertIdCallback) +{ + QVector queries; + + // Get the db id of the peer we're chatting with + int64_t peerId; + if (peers.contains(friendPk)) + { + peerId = peers[friendPk]; + } + else + { + if (peers.isEmpty()) + peerId = 0; + else + peerId = *max_element(begin(peers), end(peers))+1; + peers[friendPk] = peerId; + queries += RawDatabase::Query{("INSERT INTO peers (id, public_key) VALUES (%1, '"+friendPk+"');").arg(peerId)}; + } + + // Get the db id of the sender of the message + int64_t senderId; + if (peers.contains(sender)) + { + senderId = peers[sender]; + } + else + { + if (peers.isEmpty()) + senderId = 0; + else + senderId = *max_element(begin(peers), end(peers))+1; + peers[sender] = senderId; + queries += RawDatabase::Query{("INSERT INTO peers (id, public_key) VALUES (%1, '"+sender+"');").arg(senderId)}; + } + + queries += RawDatabase::Query(QString("INSERT OR IGNORE INTO aliases (owner, display_name) VALUES (%1, ?);") + .arg(senderId), {dispName.toUtf8()}); + + // If the alias already existed, the insert will ignore the conflict and last_insert_rowid() will return garbage, + // so we have to check changes() and manually fetch the row ID in this case + queries += RawDatabase::Query(QString("INSERT INTO history (timestamp, chat_id, message, sender_alias) " + "VALUES (%1, %2, ?, (" + " CASE WHEN changes() IS 0 THEN (" + " SELECT id FROM aliases WHERE owner=%3 AND display_name=?)" + " ELSE last_insert_rowid() END" + "));") + .arg(time.toMSecsSinceEpoch()).arg(peerId).arg(senderId), + {message.toUtf8(), dispName.toUtf8()}, insertIdCallback); + + if (!isSent) + queries += RawDatabase::Query{"INSERT INTO faux_offline_pending (id) VALUES (last_insert_rowid());"}; + + return queries; +} + +void History::addNewMessage(const QString &friendPk, const QString &message, const QString &sender, + const QDateTime &time, bool isSent, QString dispName, std::function insertIdCallback) +{ + db.execLater(generateNewMessageQueries(friendPk, message, sender, time, isSent, dispName, insertIdCallback)); +} + +QList History::getChatHistory(const QString &friendPk, const QDateTime &from, const QDateTime &to) +{ + QList messages; + + auto rowCallback = [&messages](const QVector& row) + { + messages += {row[0].toLongLong(), + row[1].isNull(), + QDateTime::fromMSecsSinceEpoch(row[2].toLongLong()), + row[3].toString(), + row[4].toString(), + row[5].toString(), + row[6].toString()}; + }; + + // Don't forget to update the rowCallback if you change the selected columns! + db.execNow({QString("SELECT history.id, faux_offline_pending.id, timestamp, chat.public_key, " + "aliases.display_name, sender.public_key, message FROM history " + "LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id " + "JOIN peers chat ON chat_id = chat.id " + "JOIN aliases ON sender_alias = aliases.id " + "JOIN peers sender ON aliases.owner = sender.id " + "WHERE timestamp BETWEEN %1 AND %2 AND chat.public_key='%3';") + .arg(from.toMSecsSinceEpoch()).arg(to.toMSecsSinceEpoch()).arg(friendPk), rowCallback}); + + return messages; +} + +void History::markAsSent(qint64 id) +{ + db.execLater(QString("DELETE FROM faux_offline_pending WHERE id=%1;").arg(id)); +} + +QString History::getDbPath(const QString &profileName) +{ + return Settings::getInstance().getSettingsDirPath() + profileName + ".db"; +} + +void History::init() +{ + if (!isValid()) + { + qWarning() << "Database not open, init failed"; + return; + } + + db.execLater("CREATE TABLE IF NOT EXISTS peers (id INTEGER PRIMARY KEY, public_key TEXT NOT NULL UNIQUE);" + "CREATE TABLE IF NOT EXISTS aliases (id INTEGER PRIMARY KEY, owner INTEGER," + "display_name BLOB NOT NULL, UNIQUE(owner, display_name));" + "CREATE TABLE IF NOT EXISTS history (id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, " + "chat_id INTEGER NOT NULL, sender_alias INTEGER NOT NULL, " + "message BLOB NOT NULL);" + "CREATE TABLE IF NOT EXISTS faux_offline_pending (id INTEGER PRIMARY KEY);"); + + // Cache our current peers + db.execLater(RawDatabase::Query{"SELECT id, public_key FROM peers;", [this](const QVector& row) + { + peers[row[1].toString()] = row[0].toInt(); + }}); +} + +void History::import(const HistoryKeeper &oldHistory) +{ + if (!isValid()) + { + qWarning() << "New database not open, import failed"; + return; + } + + qDebug() << "Importing old database..."; + QTime t=QTime::currentTime(); + t.start(); + QVector queries; + constexpr int batchSize = 1000; + queries.reserve(batchSize); + QList oldMessages = oldHistory.exportMessagesDeleteFile(); + for (const HistoryKeeper::HistMessage& msg : oldMessages) + { + queries += generateNewMessageQueries(msg.chat, msg.message, msg.sender, msg.timestamp, true, msg.dispName); + if (queries.size() == batchSize) + { + db.execLater(queries); + queries.clear(); + } + } + db.execLater(queries); + db.sync(); + qDebug() << "Imported old database in"< +#include +#include +#include +#include +#include "src/persistence/db/rawdatabase.h" + +class Profile; +class HistoryKeeper; +class RawDatabase; + +/// Interacts with the profile database to save the chat history +class History +{ +public: + struct HistMessage + { + HistMessage(qint64 id, bool isSent, QDateTime timestamp, QString chat, QString dispName, QString sender, QString message) : + chat{chat}, sender{sender}, message{message}, dispName{dispName}, timestamp{timestamp}, id{id}, isSent{isSent} {} + + QString chat; + QString sender; + QString message; + QString dispName; + QDateTime timestamp; + qint64 id; + bool isSent; + }; + +public: + /// Opens the profile database and prepares to work with the history + /// If password is empty, the database will be opened unencrypted + History(const QString& profileName, const QString& password); + /// Opens the profile database, and import from the old database + /// If password is empty, the database will be opened unencrypted + History(const QString& profileName, const QString& password, const HistoryKeeper& oldHistory); + ~History(); + /// Checks if the database was opened successfully + bool isValid(); + /// Imports messages from the old history file + void import(const HistoryKeeper& oldHistory); + /// Changes the database password, will encrypt or decrypt if necessary + void setPassword(const QString& password); + /// Moves the database file on disk to match the new name + void rename(const QString& newName); + + /// Erases all the chat history from the database + void eraseHistory(); + /// Erases the chat history with one friend + void removeFriendHistory(const QString& friendPk); + /// Saves a chat message in the database + void addNewMessage(const QString& friendPk, const QString& message, const QString& sender, + const QDateTime &time, bool isSent, QString dispName, + std::function insertIdCallback={}); + /// Fetches chat messages from the database + QList getChatHistory(const QString& friendPk, const QDateTime &from, const QDateTime &to); + /// Marks a message as sent, removing it from the faux-offline pending messages list + void markAsSent(qint64 id); + +protected: + /// Makes sure the history tables are created + void init(); + static QString getDbPath(const QString& profileName); + QVector generateNewMessageQueries(const QString& friendPk, const QString& message, + const QString& sender, const QDateTime &time, bool isSent, QString dispName, + std::function insertIdCallback={}); + +private: + RawDatabase db; + // Cached mappings to speed up message saving + QHash peers; ///< Maps friend public keys to unique IDs by index +}; + +#endif // HISTORY_H diff --git a/src/persistence/historykeeper.cpp b/src/persistence/historykeeper.cpp index a8fe1e318..381bfe97d 100644 --- a/src/persistence/historykeeper.cpp +++ b/src/persistence/historykeeper.cpp @@ -38,7 +38,7 @@ static HistoryKeeper *historyInstance = nullptr; QMutex HistoryKeeper::historyMutex; -HistoryKeeper *HistoryKeeper::getInstance() +HistoryKeeper *HistoryKeeper::getInstance(const Profile& profile) { historyMutex.lock(); if (historyInstance == nullptr) @@ -53,9 +53,9 @@ HistoryKeeper *HistoryKeeper::getInstance() QString path(":memory:"); GenericDdInterface *dbIntf; - if (Nexus::getProfile()->isEncrypted()) + if (profile.isEncrypted()) { - path = getHistoryPath(); + path = getHistoryPath({}, 1); dbIntf = new EncryptedDb(path, initLst); historyInstance = new HistoryKeeper(dbIntf); @@ -64,7 +64,7 @@ HistoryKeeper *HistoryKeeper::getInstance() } else { - path = getHistoryPath(); + path = getHistoryPath({}, 0); } dbIntf = new PlainDb(path, initLst); @@ -87,7 +87,7 @@ bool HistoryKeeper::checkPassword(const TOX_PASS_KEY &passkey, int encrypted) } HistoryKeeper::HistoryKeeper(GenericDdInterface *db_) : - db(db_) + oldDb(db_) { /* DB format @@ -114,11 +114,11 @@ HistoryKeeper::HistoryKeeper(GenericDdInterface *db_) : */ // for old tables: - QSqlQuery ans = db->exec("SELECT seq FROM sqlite_sequence WHERE name=\"history\";"); + QSqlQuery ans = oldDb->exec("SELECT seq FROM sqlite_sequence WHERE name=\"history\";"); if (ans.first()) { int idMax = ans.value(0).toInt(); - QSqlQuery ret = db->exec("SELECT seq FROM sqlite_sequence WHERE name=\"sent_status\";"); + QSqlQuery ret = oldDb->exec("SELECT seq FROM sqlite_sequence WHERE name=\"sent_status\";"); int idCur = 0; if (ret.first()) idCur = ret.value(0).toInt(); @@ -126,131 +126,29 @@ HistoryKeeper::HistoryKeeper(GenericDdInterface *db_) : if (idCur != idMax) { QString cmd = QString("INSERT INTO sent_status (id, status) VALUES (%1, 1);").arg(idMax); - db->exec(cmd); + oldDb->exec(cmd); } } //check table stuct - ans = db->exec("PRAGMA table_info (\"history\")"); + ans = oldDb->exec("PRAGMA table_info (\"history\")"); ans.seek(5); if (!ans.value(1).toString().contains("alias")) { //add collum in table - db->exec("ALTER TABLE history ADD COLUMN alias TEXT"); + oldDb->exec("ALTER TABLE history ADD COLUMN alias TEXT"); qDebug() << "Struct DB updated: Added column alias in table history."; } - - ans.clear(); - ans = db->exec("PRAGMA table_info('aliases')"); - ans.seek(2); - if (!ans.value(1).toString().contains("av_hash")) - { - //add collum in table - db->exec("ALTER TABLE aliases ADD COLUMN av_hash BLOB"); - qDebug() << "Struct DB updated: Added column av_hash in table aliases."; - } - - ans.seek(3); - if (!ans.value(1).toString().contains("avatar")) - { - //add collum in table - needImport = true; - db->exec("ALTER TABLE aliases ADD COLUMN avatar BLOB"); - qDebug() << "Struct DB updated: Added column avatar in table aliases."; - } - - updateChatsID(); - updateAliases(); - - setSyncType(Settings::getInstance().getDbSyncType()); - - messageID = 0; - QSqlQuery sqlAnswer = db->exec("SELECT seq FROM sqlite_sequence WHERE name=\"history\";"); - if (sqlAnswer.first()) - messageID = sqlAnswer.value(0).toLongLong(); } HistoryKeeper::~HistoryKeeper() { - delete db; -} - -void HistoryKeeper::removeFriendHistory(const QString& chat) -{ - int chat_id = getChatID(chat, ctSingle).first; - - db->exec("BEGIN TRANSACTION;"); - - QString cmd = QString("DELETE FROM chats WHERE name = '%1';").arg(chat); - db->exec(cmd); - cmd = QString("DELETE FROM aliases WHERE user_id = '%1';").arg(chat); - db->exec(cmd); - cmd = QString("DELETE FROM sent_status WHERE id IN (SELECT id FROM history WHERE chat_id = '%1');").arg(chat_id); - db->exec(cmd); - cmd = QString("DELETE FROM history WHERE chat_id = '%1';").arg(chat_id); - db->exec(cmd); - - db->exec("COMMIT TRANSACTION;"); -} - -qint64 HistoryKeeper::addChatEntry(const QString& chat, const QString& message, const QString& sender, const QDateTime &dt, bool isSent, QString dispName) -{ - QList cmds = generateAddChatEntryCmd(chat, message, sender, dt, isSent, dispName); - - db->exec("BEGIN TRANSACTION;"); - for (auto &it : cmds) - db->exec(it); - - db->exec("COMMIT TRANSACTION;"); - - messageID++; - return messageID; -} - -QList HistoryKeeper::getChatHistory(HistoryKeeper::ChatType ct, const QString &chat, - const QDateTime &time_from, const QDateTime &time_to) -{ - QList res; - - qint64 time64_from = time_from.toMSecsSinceEpoch(); - qint64 time64_to = time_to.toMSecsSinceEpoch(); - - int chat_id = getChatID(chat, ct).first; - - QSqlQuery dbAnswer; - if (ct == ctSingle) - { - dbAnswer = db->exec(QString("SELECT history.id, timestamp, user_id, message, status, alias FROM history LEFT JOIN sent_status ON history.id = sent_status.id ") + - QString("INNER JOIN aliases ON history.sender = aliases.id AND timestamp BETWEEN %1 AND %2 AND chat_id = %3;") - .arg(time64_from).arg(time64_to).arg(chat_id)); - } - else - { - // no groupchats yet - } - - while (dbAnswer.next()) - { - qint64 id = dbAnswer.value(0).toLongLong(); - qint64 timeInt = dbAnswer.value(1).toLongLong(); - QString sender = dbAnswer.value(2).toString(); - QString senderName = dbAnswer.value(5).toString(); - QString message = unWrapMessage(dbAnswer.value(3).toString()); - bool isSent = true; - if (!dbAnswer.value(4).isNull()) - isSent = dbAnswer.value(4).toBool(); - - QDateTime time = QDateTime::fromMSecsSinceEpoch(timeInt); - - res.push_back(HistMessage(id, "", sender, message, time, isSent, senderName)); - } - - return res; + delete oldDb; } QList HistoryKeeper::exportMessages() { QSqlQuery dbAnswer; - dbAnswer = db->exec(QString("SELECT history.id, timestamp, user_id, message, status, name, alias FROM history LEFT JOIN sent_status ON history.id = sent_status.id ") + + dbAnswer = oldDb->exec(QString("SELECT history.id, timestamp, user_id, message, status, name, alias FROM history LEFT JOIN sent_status ON history.id = sent_status.id ") + QString("INNER JOIN aliases ON history.sender = aliases.id INNER JOIN chats ON history.chat_id = chats.id;")); QList res; @@ -274,41 +172,6 @@ QList HistoryKeeper::exportMessages() return res; } -void HistoryKeeper::importMessages(const QList &lst) -{ - db->exec("BEGIN TRANSACTION;"); - for (const HistMessage &msg : lst) - { - QList cmds = generateAddChatEntryCmd(msg.chat, msg.message, msg.sender, msg.timestamp, msg.isSent, QString()); //!!! - for (auto &it : cmds) - db->exec(it); - - messageID++; - } - db->exec("COMMIT TRANSACTION;"); -} - -QList HistoryKeeper::generateAddChatEntryCmd(const QString& chat, const QString& message, const QString& sender, const QDateTime &dt, bool isSent, QString dispName) -{ - QList cmds; - - int chat_id = getChatID(chat, ctSingle).first; - int sender_id = getAliasID(sender); - - cmds.push_back(QString("INSERT INTO history (timestamp, chat_id, sender, message, alias) VALUES (%1, %2, %3, '%4', '%5');") - .arg(dt.toMSecsSinceEpoch()).arg(chat_id).arg(sender_id).arg(wrapMessage(message.toUtf8())).arg(QString(dispName.toUtf8()))); - cmds.push_back(QString("INSERT INTO sent_status (status) VALUES (%1);").arg(isSent)); - - return cmds; -} - -QString HistoryKeeper::wrapMessage(const QString &str) -{ - QString wrappedMessage(str); - wrappedMessage.replace("'", "''"); - return wrappedMessage; -} - QString HistoryKeeper::unWrapMessage(const QString &str) { QString unWrappedMessage(str); @@ -316,59 +179,6 @@ QString HistoryKeeper::unWrapMessage(const QString &str) return unWrappedMessage; } -void HistoryKeeper::updateChatsID() -{ - auto dbAnswer = db->exec(QString("SELECT * FROM chats;")); - - chats.clear(); - while (dbAnswer.next()) - { - QString name = dbAnswer.value(1).toString(); - int id = dbAnswer.value(0).toInt(); - ChatType ctype = convertToChatType(dbAnswer.value(2).toInt()); - - chats[name] = {id, ctype}; - } -} - -void HistoryKeeper::updateAliases() -{ - auto dbAnswer = db->exec(QString("SELECT * FROM aliases;")); - - aliases.clear(); - while (dbAnswer.next()) - { - QString user_id = dbAnswer.value(1).toString(); - int id = dbAnswer.value(0).toInt(); - - aliases[user_id] = id; - } -} - -QPair HistoryKeeper::getChatID(const QString &id_str, ChatType ct) -{ - auto it = chats.find(id_str); - if (it != chats.end()) - return it.value(); - - db->exec(QString("INSERT INTO chats (name, ctype) VALUES ('%1', '%2');").arg(id_str).arg(ct)); - updateChatsID(); - - return getChatID(id_str, ct); -} - -int HistoryKeeper::getAliasID(const QString &id_str) -{ - auto it = aliases.find(id_str); - if (it != aliases.end()) - return it.value(); - - db->exec(QString("INSERT INTO aliases (user_id) VALUES ('%1');").arg(id_str)); - updateAliases(); - - return getAliasID(id_str); -} - void HistoryKeeper::resetInstance() { if (historyInstance == nullptr) @@ -378,25 +188,6 @@ void HistoryKeeper::resetInstance() historyInstance = nullptr; } -qint64 HistoryKeeper::addGroupChatEntry(const QString &chat, const QString &message, const QString &sender, const QDateTime &dt) -{ - Q_UNUSED(chat) - Q_UNUSED(message) - Q_UNUSED(sender) - Q_UNUSED(dt) - // no groupchats yet - - return -1; -} - -HistoryKeeper::ChatType HistoryKeeper::convertToChatType(int ct) -{ - if (ct < 0 || ct > 1) - return ctSingle; - - return static_cast(ct); -} - QString HistoryKeeper::getHistoryPath(QString currentProfile, int encrypted) { QDir baseDir(Settings::getInstance().getSettingsDirPath()); @@ -409,70 +200,9 @@ QString HistoryKeeper::getHistoryPath(QString currentProfile, int encrypted) return baseDir.filePath(currentProfile + ".qtox_history"); } -void HistoryKeeper::renameHistory(QString from, QString to) +bool HistoryKeeper::isFileExist(bool encrypted) { - resetInstance(); - - QFile fileEnc(QDir(Settings::getInstance().getSettingsDirPath()).filePath(from + ".qtox_history.encrypted")); - if (fileEnc.exists()) - fileEnc.rename(QDir(Settings::getInstance().getSettingsDirPath()).filePath(to + ".qtox_history.encrypted")); - - QFile filePlain(QDir(Settings::getInstance().getSettingsDirPath()).filePath(from + ".qtox_history")); - if (filePlain.exists()) - filePlain.rename(QDir(Settings::getInstance().getSettingsDirPath()).filePath(to + ".qtox_history")); -} - -void HistoryKeeper::markAsSent(int m_id) -{ - db->exec(QString("UPDATE sent_status SET status = 1 WHERE id = %1;").arg(m_id)); -} - -QDate HistoryKeeper::getLatestDate(const QString &chat) -{ - int chat_id = getChatID(chat, ctSingle).first; - - QSqlQuery dbAnswer; - dbAnswer = db->exec(QString("SELECT MAX(timestamp) FROM history LEFT JOIN sent_status ON history.id = sent_status.id ") + - QString("INNER JOIN aliases ON history.sender = aliases.id AND chat_id = %3;") - .arg(chat_id)); - - if (dbAnswer.first()) - { - qint64 timeInt = dbAnswer.value(0).toLongLong(); - - if (timeInt != 0) - return QDateTime::fromMSecsSinceEpoch(timeInt).date(); - } - - return QDate(); -} - -void HistoryKeeper::setSyncType(Db::syncType sType) -{ - QString syncCmd; - - switch (sType) - { - case Db::syncType::stFull: - syncCmd = "FULL"; - break; - case Db::syncType::stNormal: - syncCmd = "NORMAL"; - break; - case Db::syncType::stOff: - syncCmd = "OFF"; - break; - default: - syncCmd = "FULL"; - break; - } - - db->exec(QString("PRAGMA synchronous=%1;").arg(syncCmd)); -} - -bool HistoryKeeper::isFileExist() -{ - QString path = getHistoryPath(); + QString path = getHistoryPath({}, encrypted ? 1 : 0); QFile file(path); return file.exists(); @@ -480,17 +210,16 @@ bool HistoryKeeper::isFileExist() void HistoryKeeper::removeHistory() { - db->exec("BEGIN TRANSACTION;"); - db->exec("DELETE FROM sent_status;"); - db->exec("DELETE FROM history;"); - db->exec("COMMIT TRANSACTION;"); + resetInstance(); + QFile::remove(getHistoryPath({}, 0)); + QFile::remove(getHistoryPath({}, 1)); } QList HistoryKeeper::exportMessagesDeleteFile() { - auto msgs = getInstance()->exportMessages(); + auto msgs = getInstance(*Nexus::getProfile())->exportMessages(); qDebug() << "Messages exported"; - getInstance()->removeHistory(); + getInstance(*Nexus::getProfile())->removeHistory(); return msgs; } diff --git a/src/persistence/historykeeper.h b/src/persistence/historykeeper.h index 4af427eef..6315a7249 100644 --- a/src/persistence/historykeeper.h +++ b/src/persistence/historykeeper.h @@ -27,13 +27,19 @@ #include #include +/** + * THIS IS A LEGACY CLASS KEPT FOR BACKWARDS COMPATIBILITY + * DO NOT USE! + * See the History class instead + */ + +class Profile; class GenericDdInterface; namespace Db { enum class syncType; } class HistoryKeeper { public: - enum ChatType {ctSingle = 0, ctGroup}; static QMutex historyMutex; struct HistMessage @@ -52,47 +58,23 @@ public: virtual ~HistoryKeeper(); - static HistoryKeeper* getInstance(); + static HistoryKeeper* getInstance(const Profile& profile); static void resetInstance(); static QString getHistoryPath(QString currentProfile = QString(), int encrypted = -1); // -1 defaults to checking settings, 0 or 1 to specify static bool checkPassword(const TOX_PASS_KEY& passkey, int encrypted = -1); - static bool isFileExist(); - static void renameHistory(QString from, QString to); + static bool isFileExist(bool encrypted); void removeHistory(); static QList exportMessagesDeleteFile(); - - void removeFriendHistory(const QString& chat); - qint64 addChatEntry(const QString& chat, const QString& message, const QString& sender, const QDateTime &dt, bool isSent, QString dispName); - qint64 addGroupChatEntry(const QString& chat, const QString& message, const QString& sender, const QDateTime &dt); - QList getChatHistory(ChatType ct, const QString &chat, const QDateTime &time_from, const QDateTime &time_to); - void markAsSent(int m_id); - QDate getLatestDate(const QString& chat); - QList exportMessages(); - void importMessages(const QList &lst); - - void setSyncType(Db::syncType sType); private: HistoryKeeper(GenericDdInterface *db_); HistoryKeeper(HistoryKeeper &hk) = delete; HistoryKeeper& operator=(const HistoryKeeper&) = delete; - - void updateChatsID(); - void updateAliases(); - QPair getChatID(const QString &id_str, ChatType ct); - int getAliasID(const QString &id_str); - QString wrapMessage(const QString &str); QString unWrapMessage(const QString &str); - QList generateAddChatEntryCmd(const QString& chat, const QString& message, const QString& sender, const QDateTime &dt, bool isSent, QString dispName); - ChatType convertToChatType(int); - bool needImport = false; // must be deleted with "importAvatarToDatabase" - GenericDdInterface *db; - QMap aliases; - QMap> chats; - qint64 messageID; + GenericDdInterface *oldDb; }; #endif // HISTORYKEEPER_H diff --git a/src/persistence/offlinemsgengine.cpp b/src/persistence/offlinemsgengine.cpp index 54f20d4b6..48e911cb0 100644 --- a/src/persistence/offlinemsgengine.cpp +++ b/src/persistence/offlinemsgengine.cpp @@ -19,9 +19,10 @@ #include "offlinemsgengine.h" #include "src/friend.h" -#include "src/persistence/historykeeper.h" #include "src/persistence/settings.h" #include "src/core/core.h" +#include "src/nexus.h" +#include "src/persistence/profile.h" #include #include @@ -42,6 +43,7 @@ void OfflineMsgEngine::dischargeReceipt(int receipt) { QMutexLocker ml(&mutex); + Profile* profile = Nexus::getProfile(); auto it = receipts.find(receipt); if (it != receipts.end()) { @@ -49,7 +51,8 @@ void OfflineMsgEngine::dischargeReceipt(int receipt) auto msgIt = undeliveredMsgs.find(mID); if (msgIt != undeliveredMsgs.end()) { - HistoryKeeper::getInstance()->markAsSent(mID); + if (profile->isHistoryEnabled()) + profile->getHistory()->markAsSent(mID); msgIt.value().msg->markAsSent(QDateTime::currentDateTime()); undeliveredMsgs.erase(msgIt); } @@ -57,7 +60,7 @@ void OfflineMsgEngine::dischargeReceipt(int receipt) } } -void OfflineMsgEngine::registerReceipt(int receipt, int messageID, ChatMessage::Ptr msg, const QDateTime ×tamp) +void OfflineMsgEngine::registerReceipt(int receipt, int64_t messageID, ChatMessage::Ptr msg, const QDateTime ×tamp) { QMutexLocker ml(&mutex); @@ -78,13 +81,13 @@ void OfflineMsgEngine::deliverOfflineMsgs() if (undeliveredMsgs.size() == 0) return; - QMap msgs = undeliveredMsgs; + QMap msgs = undeliveredMsgs; removeAllReciepts(); for (auto iter = msgs.begin(); iter != msgs.end(); ++iter) - { - auto val = iter.value(); - auto key = iter.key(); + { + auto val = iter.value(); + auto key = iter.key(); if (val.timestamp.msecsTo(QDateTime::currentDateTime()) < offlineTimeout) { diff --git a/src/persistence/offlinemsgengine.h b/src/persistence/offlinemsgengine.h index 6cce8c919..386262a5c 100644 --- a/src/persistence/offlinemsgengine.h +++ b/src/persistence/offlinemsgengine.h @@ -39,7 +39,7 @@ public: static QMutex globalMutex; void dischargeReceipt(int receipt); - void registerReceipt(int receipt, int messageID, ChatMessage::Ptr msg, const QDateTime ×tamp = QDateTime::currentDateTime()); + void registerReceipt(int receipt, int64_t messageID, ChatMessage::Ptr msg, const QDateTime ×tamp = QDateTime::currentDateTime()); public slots: void deliverOfflineMsgs(); @@ -54,8 +54,8 @@ private: QMutex mutex; Friend* f; - QHash receipts; - QMap undeliveredMsgs; + QHash receipts; + QMap undeliveredMsgs; static const int offlineTimeout; }; diff --git a/src/persistence/profile.cpp b/src/persistence/profile.cpp index fb9023025..587af3f8c 100644 --- a/src/persistence/profile.cpp +++ b/src/persistence/profile.cpp @@ -21,8 +21,8 @@ #include "profile.h" #include "profilelocker.h" #include "src/persistence/settings.h" -#include "src/core/core.h" #include "src/persistence/historykeeper.h" +#include "src/core/core.h" #include "src/widget/gui.h" #include "src/widget/widget.h" #include "src/nexus.h" @@ -47,7 +47,16 @@ Profile::Profile(QString name, QString password, bool isNewProfile) Settings& s = Settings::getInstance(); s.setCurrentProfile(name); s.saveGlobal(); - HistoryKeeper::resetInstance(); + + // At this point it's too early to load the personnal settings (Nexus will do it), so we always load + // the history, and if it fails we can't change the setting now, but we keep a nullptr + history.reset(new History{name, password}); + if (!history->isValid()) + { + qWarning() << "Failed to open history for profile"<setObjectName("qTox Core"); @@ -127,7 +136,10 @@ Profile* Profile::loadProfile(QString name, QString password) } } - return new Profile(name, password, false); + Profile* p = new Profile(name, password, false); + if (p->history && HistoryKeeper::isFileExist(!password.isEmpty())) + p->history->import(*HistoryKeeper::getInstance(*p)); + return p; } Profile* Profile::createProfile(QString name, QString password) @@ -211,7 +223,7 @@ Core* Profile::getCore() return core; } -QString Profile::getName() +QString Profile::getName() const { return name; } @@ -379,7 +391,7 @@ QByteArray Profile::loadAvatarData(const QString &ownerId) void Profile::saveAvatar(QByteArray pic, const QString &ownerId) { - if (!password.isEmpty()) + if (!password.isEmpty() && !pic.isEmpty()) pic = core->encryptData(pic, passkey); QString path = avatarPath(ownerId); @@ -407,6 +419,16 @@ void Profile::removeAvatar() removeAvatar(core->getSelfId().publicKey); } +bool Profile::isHistoryEnabled() +{ + return Settings::getInstance().getEnableLogging() && history; +} + +History *Profile::getHistory() +{ + return history.get(); +} + void Profile::removeAvatar(const QString &ownerId) { QFile::remove(avatarPath(ownerId)); @@ -419,7 +441,7 @@ bool Profile::exists(QString name) return QFile::exists(path+".tox") && QFile::exists(path+".ini"); } -bool Profile::isEncrypted() +bool Profile::isEncrypted() const { return !password.isEmpty(); } @@ -478,7 +500,8 @@ bool Profile::rename(QString newName) QFile::rename(path+".tox", newPath+".tox"); QFile::rename(path+".ini", newPath+".ini"); - HistoryKeeper::renameHistory(name, newName); + if (history) + history->rename(newName); bool resetAutorun = Settings::getInstance().getAutorun(); Settings::getInstance().setAutorun(false); Settings::getInstance().setCurrentProfile(newName); @@ -497,12 +520,12 @@ bool Profile::checkPassword() return !loadToxSave().isEmpty(); } -QString Profile::getPassword() +QString Profile::getPassword() const { return password; } -const TOX_PASS_KEY& Profile::getPasskey() +const TOX_PASS_KEY& Profile::getPasskey() const { return passkey; } @@ -517,14 +540,16 @@ void Profile::restartCore() void Profile::setPassword(QString newPassword) { - QList oldMessages = HistoryKeeper::exportMessagesDeleteFile(); QByteArray avatar = loadAvatarData(core->getSelfId().publicKey); password = newPassword; passkey = *core->createPasskey(password); saveToxSave(); - HistoryKeeper::getInstance()->importMessages(oldMessages); - Nexus::getDesktopGUI()->reloadHistory(); + if (history) + { + history->setPassword(newPassword); + Nexus::getDesktopGUI()->reloadHistory(); + } saveAvatar(avatar, core->getSelfId().publicKey); } diff --git a/src/persistence/profile.h b/src/persistence/profile.h index b177a5152..4f4e454d4 100644 --- a/src/persistence/profile.h +++ b/src/persistence/profile.h @@ -26,6 +26,8 @@ #include #include #include +#include +#include "src/persistence/history.h" class Core; class QThread; @@ -44,16 +46,16 @@ public: ~Profile(); Core* getCore(); - QString getName(); + QString getName() const; void startCore(); ///< Starts the Core thread void restartCore(); ///< Delete core and restart a new one bool isNewProfile(); - bool isEncrypted(); ///< Returns true if we have a password set (doesn't check the actual file on disk) + bool isEncrypted() const; ///< Returns true if we have a password set (doesn't check the actual file on disk) bool checkPassword(); ///< Checks whether the password is valid - QString getPassword(); + QString getPassword() const; void setPassword(QString newPassword); ///< Changes the encryption password and re-saves everything with it - const TOX_PASS_KEY& getPasskey(); + const TOX_PASS_KEY& getPasskey() const; QByteArray loadToxSave(); ///< Loads the profile's .tox save from file, unencrypted void saveToxSave(); ///< Saves the profile's .tox save, encrypted if needed. Invalid on deleted profiles. @@ -67,6 +69,11 @@ public: void removeAvatar(const QString& ownerId); ///< Removes a cached avatar void removeAvatar(); ///< Removes our own avatar + /// Returns true if the history is enabled in the settings, and loaded successfully for this profile + bool isHistoryEnabled(); + /// May return a nullptr if the history failed to load + History* getHistory(); + /// Removes the profile permanently /// It is invalid to call loadToxSave or saveToxSave on a deleted profile /// Updates the profiles vector @@ -100,6 +107,7 @@ private: QThread* coreThread; QString name, password; TOX_PASS_KEY passkey; + std::unique_ptr history; bool newProfile; ///< True if this is a newly created profile, with no .tox save file yet. bool isRemoved; ///< True if the profile has been removed by remove() static QVector profiles; diff --git a/src/persistence/settings.cpp b/src/persistence/settings.cpp index 3aa214a45..da06b74ff 100644 --- a/src/persistence/settings.cpp +++ b/src/persistence/settings.cpp @@ -26,7 +26,6 @@ #include "src/widget/gui.h" #include "src/persistence/profilelocker.h" #include "src/persistence/settingsserializer.h" -#include "src/persistence/historykeeper.h" #include "src/nexus.h" #include "src/persistence/profile.h" #ifdef QTOX_PLATFORM_EXT diff --git a/src/widget/about/aboutuser.cpp b/src/widget/about/aboutuser.cpp index 1b6ba9bc3..c71f21075 100644 --- a/src/widget/about/aboutuser.cpp +++ b/src/widget/about/aboutuser.cpp @@ -1,7 +1,6 @@ #include "aboutuser.h" #include "ui_aboutuser.h" #include "src/persistence/settings.h" -#include "src/persistence/historykeeper.h" #include "src/persistence/profile.h" #include "src/nexus.h" @@ -97,7 +96,9 @@ void AboutUser::onAcceptedClicked() void AboutUser::onRemoveHistoryClicked() { - HistoryKeeper::getInstance()->removeFriendHistory(toxId.publicKey); + History* history = Nexus::getProfile()->getHistory(); + if (history) + history->removeFriendHistory(toxId.publicKey); QMessageBox::StandardButton reply; reply = QMessageBox::information(this, tr("History removed"), diff --git a/src/widget/form/addfriendform.cpp b/src/widget/form/addfriendform.cpp index 90a57098d..b55aa8389 100644 --- a/src/widget/form/addfriendform.cpp +++ b/src/widget/form/addfriendform.cpp @@ -177,7 +177,7 @@ void AddFriendForm::setIdFromClipboard() QString id = clipboard->text().trimmed(); if (Core::getInstance()->isReady() && !id.isEmpty() && ToxId::isToxId(id)) { - if (!ToxId(id).isActiveProfile()) + if (!ToxId(id).isSelf()) toxId.setText(id); } } diff --git a/src/widget/form/chatform.cpp b/src/widget/form/chatform.cpp index 1319a352e..5a482863b 100644 --- a/src/widget/form/chatform.cpp +++ b/src/widget/form/chatform.cpp @@ -38,7 +38,6 @@ #include "src/core/core.h" #include "src/core/coreav.h" #include "src/friend.h" -#include "src/persistence/historykeeper.h" #include "src/widget/style.h" #include "src/persistence/settings.h" #include "src/core/cstring.h" @@ -61,6 +60,8 @@ #include "src/widget/translator.h" #include "src/video/videosource.h" #include "src/video/camerasource.h" +#include "src/nexus.h" +#include "src/persistence/profile.h" ChatForm::ChatForm(Friend* chatFriend) : f(chatFriend) @@ -208,7 +209,7 @@ void ChatForm::startFileSend(ToxFile file) return; QString name; - if (!previousId.isActiveProfile()) + if (!previousId.isSelf()) { Core* core = Core::getInstance(); name = core->getUsername(); @@ -694,7 +695,7 @@ void ChatForm::loadHistory(QDateTime since, bool processUndelivered) } } - auto msgs = HistoryKeeper::getInstance()->getChatHistory(HistoryKeeper::ctSingle, f->getToxId().publicKey, since, now); + auto msgs = Nexus::getProfile()->getHistory()->getChatHistory(f->getToxId().publicKey, since, now); ToxId storedPrevId = previousId; ToxId prevId; @@ -716,13 +717,13 @@ void ChatForm::loadHistory(QDateTime since, bool processUndelivered) // Show each messages ToxId authorId = ToxId(it.sender); - QString authorStr = !it.dispName.isEmpty() ? it.dispName : (authorId.isActiveProfile() ? Core::getInstance()->getUsername() : resolveToxId(authorId)); + QString authorStr = !it.dispName.isEmpty() ? it.dispName : (authorId.isSelf() ? Core::getInstance()->getUsername() : resolveToxId(authorId)); bool isAction = it.message.startsWith("/me ", Qt::CaseInsensitive); ChatMessage::Ptr msg = ChatMessage::createChatMessage(authorStr, - isAction ? it.message.right(it.message.length() - 4) : it.message, + isAction ? it.message.mid(4) : it.message, isAction ? ChatMessage::ACTION : ChatMessage::NORMAL, - authorId.isActiveProfile(), + authorId.isSelf(), QDateTime()); if (!isAction && (prevId == authorId) && (prevMsgDateTime.secsTo(msgDateTime) < getChatLog()->repNameAfter) ) @@ -731,7 +732,7 @@ void ChatForm::loadHistory(QDateTime since, bool processUndelivered) prevId = authorId; prevMsgDateTime = msgDateTime; - if (it.isSent || !authorId.isActiveProfile()) + if (it.isSent || !authorId.isSelf()) { msg->markAsSent(msgDateTime); } @@ -810,6 +811,9 @@ void ChatForm::onScreenshotTaken(const QPixmap &pixmap) { void ChatForm::onLoadHistory() { + if (!Nexus::getProfile()->isHistoryEnabled()) + return; + LoadHistoryDialog dlg; if (dlg.exec()) @@ -937,9 +941,6 @@ void ChatForm::SendMessageStr(QString msg) bool status = !Settings::getInstance().getFauxOfflineMessaging(); - int id = HistoryKeeper::getInstance()->addChatEntry(f->getToxId().publicKey, qt_msg_hist, - Core::getInstance()->getSelfId().publicKey, timestamp, status, Core::getInstance()->getUsername()); - ChatMessage::Ptr ma = addSelfMessage(qt_msg, isAction, timestamp, false); int rec; @@ -948,7 +949,13 @@ void ChatForm::SendMessageStr(QString msg) else rec = Core::getInstance()->sendMessage(f->getFriendID(), qt_msg); - getOfflineMsgEngine()->registerReceipt(rec, id, ma); + auto* offMsgEngine = getOfflineMsgEngine(); + Nexus::getProfile()->getHistory()->addNewMessage(f->getToxId().publicKey, qt_msg_hist, + Core::getInstance()->getSelfId().publicKey, timestamp, status, Core::getInstance()->getUsername(), + [offMsgEngine,rec,ma](int64_t id) + { + offMsgEngine->registerReceipt(rec, id, ma); + }); msgEdit->setLastMessage(msg); //set last message only when sending it diff --git a/src/widget/form/genericchatform.cpp b/src/widget/form/genericchatform.cpp index 0d12e725f..55a8a05fb 100644 --- a/src/widget/form/genericchatform.cpp +++ b/src/widget/form/genericchatform.cpp @@ -312,7 +312,7 @@ void GenericChatForm::onChatContextMenuRequested(QPoint pos) ChatMessage::Ptr GenericChatForm::addMessage(const ToxId& author, const QString &message, bool isAction, const QDateTime &datetime, bool isSent) { - bool authorIsActiveProfile = author.isActiveProfile(); + bool authorIsActiveProfile = author.isSelf(); QString authorStr = authorIsActiveProfile ? Core::getInstance()->getUsername() : resolveToxId(author); ChatMessage::Ptr msg; @@ -347,7 +347,7 @@ ChatMessage::Ptr GenericChatForm::addSelfMessage(const QString &message, bool is void GenericChatForm::addAlertMessage(const ToxId &author, QString message, QDateTime datetime) { QString authorStr = resolveToxId(author); - ChatMessage::Ptr msg = ChatMessage::createChatMessage(authorStr, message, ChatMessage::ALERT, author.isActiveProfile(), datetime); + ChatMessage::Ptr msg = ChatMessage::createChatMessage(authorStr, message, ChatMessage::ALERT, author.isSelf(), datetime); insertChatMessage(msg); if ((author == previousId) && (prevMsgDateTime.secsTo(QDateTime::currentDateTime()) < getChatLog()->repNameAfter)) diff --git a/src/widget/form/groupchatform.cpp b/src/widget/form/groupchatform.cpp index 51e6c6114..e837d01ec 100644 --- a/src/widget/form/groupchatform.cpp +++ b/src/widget/form/groupchatform.cpp @@ -27,7 +27,6 @@ #include "src/core/core.h" #include "src/core/coreav.h" #include "src/widget/style.h" -#include "src/persistence/historykeeper.h" #include "src/widget/flowlayout.h" #include "src/widget/translator.h" #include "src/video/groupnetcamview.h" diff --git a/src/widget/form/profileform.cpp b/src/widget/form/profileform.cpp index 7d8a71fc1..f03747b5a 100644 --- a/src/widget/form/profileform.cpp +++ b/src/widget/form/profileform.cpp @@ -30,7 +30,6 @@ #include "src/widget/widget.h" #include "src/widget/gui.h" #include "src/widget/style.h" -#include "src/persistence/historykeeper.h" #include "src/persistence/profilelocker.h" #include "src/persistence/profile.h" #include "src/widget/translator.h" diff --git a/src/widget/form/settings/advancedform.cpp b/src/widget/form/settings/advancedform.cpp index dbd035928..58d5e39c9 100644 --- a/src/widget/form/settings/advancedform.cpp +++ b/src/widget/form/settings/advancedform.cpp @@ -20,7 +20,6 @@ #include "ui_advancedsettings.h" #include "advancedform.h" -#include "src/persistence/historykeeper.h" #include "src/persistence/settings.h" #include "src/persistence/db/plaindb.h" #include "src/widget/translator.h" @@ -31,28 +30,11 @@ AdvancedForm::AdvancedForm() : bodyUI = new Ui::AdvancedSettings; bodyUI->setupUi(this); - bodyUI->dbLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); - bodyUI->dbLabel->setOpenExternalLinks(true); - bodyUI->cbMakeToxPortable->setChecked(Settings::getInstance().getMakeToxPortable()); - bodyUI->syncTypeComboBox->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLength); - bodyUI->syncTypeComboBox->addItems({tr("Synchronized - safe (recommended)"), - tr("Partially async - risky (20% faster)"), - tr("Asynchronous - dangerous (fastest)") - }); - int index = 2 - static_cast(Settings::getInstance().getDbSyncType()); - bodyUI->syncTypeComboBox->setCurrentIndex(index); connect(bodyUI->cbMakeToxPortable, &QCheckBox::stateChanged, this, &AdvancedForm::onMakeToxPortableUpdated); - connect(bodyUI->syncTypeComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(onDbSyncTypeUpdated())); connect(bodyUI->resetButton, SIGNAL(clicked()), this, SLOT(resetToDefault())); - for (QComboBox* cb : findChildren()) - { - cb->installEventFilter(this); - cb->setFocusPolicy(Qt::StrongFocus); - } - for (QCheckBox *cb : findChildren()) // this one is to allow scrolling on checkboxes { cb->installEventFilter(this); @@ -72,24 +54,14 @@ void AdvancedForm::onMakeToxPortableUpdated() Settings::getInstance().setMakeToxPortable(bodyUI->cbMakeToxPortable->isChecked()); } -void AdvancedForm::onDbSyncTypeUpdated() -{ - int index = 2 - bodyUI->syncTypeComboBox->currentIndex(); - Settings::getInstance().setDbSyncType(index); - HistoryKeeper::getInstance()->setSyncType(Settings::getInstance().getDbSyncType()); -} - void AdvancedForm::resetToDefault() { - int index = 2 - static_cast(Db::syncType::stFull); - bodyUI->syncTypeComboBox->setCurrentIndex(index); - onDbSyncTypeUpdated(); } bool AdvancedForm::eventFilter(QObject *o, QEvent *e) { if ((e->type() == QEvent::Wheel) && - (qobject_cast(o) || qobject_cast(o) || qobject_cast(o))) + (qobject_cast(o) || qobject_cast(o))) { e->ignore(); return true; @@ -100,7 +72,4 @@ bool AdvancedForm::eventFilter(QObject *o, QEvent *e) void AdvancedForm::retranslateUi() { bodyUI->retranslateUi(this); - bodyUI->syncTypeComboBox->setItemText(0, tr("Synchronized - safe (recommended)")); - bodyUI->syncTypeComboBox->setItemText(1, tr("Partially async - risky (20% faster)")); - bodyUI->syncTypeComboBox->setItemText(2, tr("Asynchronous - dangerous (fastest)")); } diff --git a/src/widget/form/settings/advancedform.h b/src/widget/form/settings/advancedform.h index 1fb871f6f..8eea447f0 100644 --- a/src/widget/form/settings/advancedform.h +++ b/src/widget/form/settings/advancedform.h @@ -41,7 +41,6 @@ protected: private slots: void onMakeToxPortableUpdated(); - void onDbSyncTypeUpdated(); void resetToDefault(); private: diff --git a/src/widget/form/settings/advancedsettings.ui b/src/widget/form/settings/advancedsettings.ui index ff14b7950..abe13c920 100644 --- a/src/widget/form/settings/advancedsettings.ui +++ b/src/widget/form/settings/advancedsettings.ui @@ -24,8 +24,8 @@ 0 0 - 396 - 454 + 398 + 456 @@ -62,39 +62,6 @@ - - - - Chat history - - - - - - - - <html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_synchronous"><span style=" text-decoration: underline; color:#0000ff;">Writing to DB</span></a></p></body></html> - - - Qt::RichText - - - - - - - - 0 - 0 - - - - - - - - - diff --git a/src/widget/form/settings/privacyform.cpp b/src/widget/form/settings/privacyform.cpp index 8a337939d..6afd7d756 100644 --- a/src/widget/form/settings/privacyform.cpp +++ b/src/widget/form/settings/privacyform.cpp @@ -21,12 +21,14 @@ #include "ui_privacysettings.h" #include "src/widget/form/settingswidget.h" #include "src/persistence/settings.h" -#include "src/persistence/historykeeper.h" #include "src/core/core.h" #include "src/widget/widget.h" #include "src/widget/gui.h" #include "src/widget/form/setpassworddialog.h" #include "src/widget/translator.h" +#include "src/nexus.h" +#include "src/persistence/profile.h" +#include "src/persistence/history.h" #include #include #include @@ -63,7 +65,7 @@ void PrivacyForm::onEnableLoggingUpdated() QMessageBox::Yes|QMessageBox::No); if (dialogDelHistory == QMessageBox::Yes) { - HistoryKeeper::getInstance()->removeHistory(); + Nexus::getProfile()->getHistory()->eraseHistory(); } } } diff --git a/src/widget/friendlistwidget.cpp b/src/widget/friendlistwidget.cpp index f74fbda18..707091ad7 100644 --- a/src/widget/friendlistwidget.cpp +++ b/src/widget/friendlistwidget.cpp @@ -26,7 +26,6 @@ #include "groupwidget.h" #include "circlewidget.h" #include "widget.h" -#include "src/persistence/historykeeper.h" #include #include #include diff --git a/src/widget/gui.cpp b/src/widget/gui.cpp index 8b4362e28..9112e99b3 100644 --- a/src/widget/gui.cpp +++ b/src/widget/gui.cpp @@ -143,7 +143,12 @@ void GUI::showError(const QString& title, const QString& msg) { if (QThread::currentThread() == qApp->thread()) { - getInstance()._showError(title, msg); + // If the GUI hasn't started yet and we're on the main thread, + // we still want to be able to show error messages + if (!Nexus::getDesktopGUI()) + QMessageBox::critical(nullptr, title, msg); + else + getInstance()._showError(title, msg); } else { diff --git a/src/widget/widget.cpp b/src/widget/widget.cpp index 452edb7a9..4c4ba6c51 100644 --- a/src/widget/widget.cpp +++ b/src/widget/widget.cpp @@ -37,7 +37,6 @@ #include "friendlistwidget.h" #include "form/chatform.h" #include "maskablepixmapwidget.h" -#include "src/persistence/historykeeper.h" #include "src/net/autoupdate.h" #include "src/audio/audio.h" #include "src/platform/timer.h" @@ -1081,7 +1080,7 @@ void Widget::onFriendMessageReceived(int friendId, const QString& message, bool QDateTime timestamp = QDateTime::currentDateTime(); f->getChatForm()->addMessage(f->getToxId(), message, isAction, timestamp, true); - HistoryKeeper::getInstance()->addChatEntry(f->getToxId().publicKey, isAction ? "/me " + f->getDisplayedName() + " " + message : message, + Nexus::getProfile()->getHistory()->addNewMessage(f->getToxId().publicKey, isAction ? "/me " + f->getDisplayedName() + " " + message : message, f->getToxId().publicKey, timestamp, true, f->getDisplayedName()); newFriendMessageAlert(friendId); @@ -1281,7 +1280,7 @@ void Widget::removeFriend(Friend* f, bool fake) if (!ask.accepted()) return; else if (ask.removeHistory()) - HistoryKeeper::getInstance()->removeFriendHistory(f->getToxId().publicKey); + Nexus::getProfile()->getHistory()->removeFriendHistory(f->getToxId().publicKey); } f->getFriendWidget()->setAsInactiveChatroom(); @@ -1444,7 +1443,7 @@ void Widget::onGroupMessageReceived(int groupnumber, int peernumber, const QStri return; ToxId author = Core::getInstance()->getGroupPeerToxId(groupnumber, peernumber); - bool targeted = !author.isActiveProfile() && (message.contains(nameMention) || message.contains(sanitizedNameMention)); + bool targeted = !author.isSelf() && (message.contains(nameMention) || message.contains(sanitizedNameMention)); if (targeted && !isAction) g->getChatForm()->addAlertMessage(author, message, QDateTime::currentDateTime()); else