1
0
mirror of https://github.com/qTox/qTox.git synced 2024-03-22 14:00:36 +08:00

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.
This commit is contained in:
tux3 2015-12-17 11:24:01 +01:00
parent 3717b5c98d
commit b5cdfb3dce
No known key found for this signature in database
GPG Key ID: 7E086DD661263264
31 changed files with 1044 additions and 454 deletions

View File

@ -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

View File

@ -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);
}

View File

@ -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 <tox/tox.h>
#include <tox/toxencryptsave.h>
#include <QApplication>
@ -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)

View File

@ -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();
}

View File

@ -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.

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,466 @@
#include "rawdatabase.h"
#include <QDebug>
#include <QMetaObject>
#include <QMutexLocker>
#include <QCoreApplication>
#include <QFile>
#include <cassert>
#include <tox/toxencryptsave.h>
/// 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 <sqlcipher/sqlite3.h>
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"<<path<<"with error:"<<sqlite3_errmsg(sqlite);
return false;
}
if (!hexKey.isEmpty())
{
if (!execNow("PRAGMA key = \"x'"+hexKey+"'\""))
{
qWarning() << "Failed to set encryption key";
close();
return false;
}
if (!execNow("SELECT count(*) FROM sqlite_master"))
{
qWarning() << "Database is unusable, check that the password is correct";
close();
return false;
}
}
return true;
}
void RawDatabase::close()
{
if (QThread::currentThread() != workerThread.get())
return (void)QMetaObject::invokeMethod(this, "close", Qt::BlockingQueuedConnection);
// We assume we're in the ctor or dtor, so we just need to finish processing our transactions
process();
if (sqlite3_close(sqlite) == SQLITE_OK)
sqlite = nullptr;
else
qWarning() << "Error closing database:"<<sqlite3_errmsg(sqlite);
}
bool RawDatabase::isOpen()
{
// We don't need thread safety since only the ctor/dtor can write this pointer
return sqlite != nullptr;
}
bool RawDatabase::execNow(const QString& statement)
{
return execNow(Query{statement});
}
bool RawDatabase::execNow(const RawDatabase::Query &statement)
{
return execNow(QVector<Query>{statement});
}
bool RawDatabase::execNow(const QVector<RawDatabase::Query> &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<Query>{statement});
}
void RawDatabase::execLater(const QVector<RawDatabase::Query> &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<int>(compileTail - query.query.data()),
&stmt, &compileTail)) != SQLITE_OK)
{
qWarning() << "Failed to prepare statement"<<query.query<<"with error"<<r;
goto cleanupStatements;
}
query.statements += stmt;
// Now we can bind our params to this statement
int nParams = sqlite3_bind_parameter_count(stmt);
if (query.blobs.size() < curParam+nParams)
{
qWarning() << "Not enough parameters to bind to query "<<query.query;
goto cleanupStatements;
}
for (int i=0; i<nParams; ++i)
{
const QByteArray& blob = query.blobs[curParam+i];
if (sqlite3_bind_blob(stmt, i+1, blob.data(), blob.size(), SQLITE_STATIC) != SQLITE_OK)
{
qWarning() << "Failed to bind param"<<curParam+i<<"to query "<<query.query;
goto cleanupStatements;
}
}
curParam += nParams;
} while (compileTail != query.query.data()+query.query.size());
}
// Execute each statement of each query of our transaction
for (Query& query : trans.queries)
{
for (sqlite3_stmt* stmt : query.statements)
{
int column_count = sqlite3_column_count(stmt);
int result;
do {
result = sqlite3_step(stmt);
// Execute our row callback
if (result == SQLITE_ROW && query.rowCallback)
{
QVector<QVariant> row;
for (int i=0; i<column_count; ++i)
row += extractData(stmt, i);
query.rowCallback(row);
}
} while (result == SQLITE_ROW);
if (result == SQLITE_ERROR)
{
qWarning() << "Error executing query "<<query.query;
goto cleanupStatements;
}
else if (result == SQLITE_MISUSE)
{
qWarning() << "Misuse executing query "<<query.query;
goto cleanupStatements;
}
else if (result == SQLITE_CONSTRAINT)
{
qWarning() << "Constraint error executing query "<<query.query;
goto cleanupStatements;
}
else if (result != SQLITE_DONE)
{
qWarning() << "Unknown error"<<result<<"executing query "<<query.query;
goto cleanupStatements;
}
}
if (query.insertCallback)
query.insertCallback(sqlite3_last_insert_rowid(sqlite));
}
if (trans.success != nullptr)
trans.success->store(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<const char*>(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<const char*>(sqlite3_column_blob(stmt, col));
int len = sqlite3_column_bytes(stmt, col);
return QByteArray::fromRawData(data, len);
}
}

View File

@ -0,0 +1,115 @@
#ifndef RAWDATABASE_H
#define RAWDATABASE_H
#include <QString>
#include <QByteArray>
#include <QThread>
#include <QQueue>
#include <QVector>
#include <QPair>
#include <QMutex>
#include <QVariant>
#include <memory>
#include <atomic>
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<QByteArray> blobs = {}, std::function<void(int64_t)> insertCallback={})
: query{query.toUtf8()}, blobs{blobs}, insertCallback{insertCallback} {}
Query(QString query, std::function<void(int64_t)> insertCallback)
: query{query.toUtf8()}, insertCallback{insertCallback} {}
Query(QString query, std::function<void(const QVector<QVariant>&)> rowCallback)
: query{query.toUtf8()}, rowCallback{rowCallback} {}
Query() = default;
private:
QByteArray query; ///< UTF-8 query string
QVector<QByteArray> blobs; ///< Bound data blobs
std::function<void(int64_t)> insertCallback; ///< Called after execution with the last insert rowid
std::function<void(const QVector<QVariant>&)> rowCallback; ///< Called during execution for each row
QVector<sqlite3_stmt*> 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<Query>& statements);
/// Executes a SQL transaction asynchronously.
void execLater(const QString& statement);
void execLater(const Query& statement);
void execLater(const QVector<Query>& 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<Query> 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<QThread> workerThread;
QQueue<Transaction> pendingTransactions;
/// Protects pendingTransactions
QMutex transactionsMutex;
QString path;
QString currentHexKey;
};
#endif // RAWDATABASE_H

231
src/persistence/history.cpp Normal file
View File

@ -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 <QDebug>
#include <cassert>
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<RawDatabase::Query> History::generateNewMessageQueries(const QString &friendPk, const QString &message,
const QString &sender, const QDateTime &time, bool isSent, QString dispName,
std::function<void(int64_t)> insertIdCallback)
{
QVector<RawDatabase::Query> 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<void(int64_t)> insertIdCallback)
{
db.execLater(generateNewMessageQueries(friendPk, message, sender, time, isSent, dispName, insertIdCallback));
}
QList<History::HistMessage> History::getChatHistory(const QString &friendPk, const QDateTime &from, const QDateTime &to)
{
QList<HistMessage> messages;
auto rowCallback = [&messages](const QVector<QVariant>& 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<QVariant>& 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<RawDatabase::Query> queries;
constexpr int batchSize = 1000;
queries.reserve(batchSize);
QList<HistoryKeeper::HistMessage> 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"<<t.elapsed()<<"ms";
}

77
src/persistence/history.h Normal file
View File

@ -0,0 +1,77 @@
#ifndef HISTORY_H
#define HISTORY_H
#include <tox/toxencryptsave.h>
#include <QDateTime>
#include <QVector>
#include <QHash>
#include <cstdint>
#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<void(int64_t)> insertIdCallback={});
/// Fetches chat messages from the database
QList<HistMessage> 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<RawDatabase::Query> generateNewMessageQueries(const QString& friendPk, const QString& message,
const QString& sender, const QDateTime &time, bool isSent, QString dispName,
std::function<void(int64_t)> insertIdCallback={});
private:
RawDatabase db;
// Cached mappings to speed up message saving
QHash<QString, int64_t> peers; ///< Maps friend public keys to unique IDs by index
};
#endif // HISTORY_H

View File

@ -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<QString> 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::HistMessage> HistoryKeeper::getChatHistory(HistoryKeeper::ChatType ct, const QString &chat,
const QDateTime &time_from, const QDateTime &time_to)
{
QList<HistMessage> 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::HistMessage> 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<HistMessage> res;
@ -274,41 +172,6 @@ QList<HistoryKeeper::HistMessage> HistoryKeeper::exportMessages()
return res;
}
void HistoryKeeper::importMessages(const QList<HistoryKeeper::HistMessage> &lst)
{
db->exec("BEGIN TRANSACTION;");
for (const HistMessage &msg : lst)
{
QList<QString> 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<QString> HistoryKeeper::generateAddChatEntryCmd(const QString& chat, const QString& message, const QString& sender, const QDateTime &dt, bool isSent, QString dispName)
{
QList<QString> 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<int, HistoryKeeper::ChatType> 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<ChatType>(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::HistMessage> HistoryKeeper::exportMessagesDeleteFile()
{
auto msgs = getInstance()->exportMessages();
auto msgs = getInstance(*Nexus::getProfile())->exportMessages();
qDebug() << "Messages exported";
getInstance()->removeHistory();
getInstance(*Nexus::getProfile())->removeHistory();
return msgs;
}

View File

@ -27,13 +27,19 @@
#include <QMutex>
#include <tox/toxencryptsave.h>
/**
* 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<HistMessage> 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<HistMessage> 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<HistMessage> exportMessages();
void importMessages(const QList<HistoryKeeper::HistMessage> &lst);
void setSyncType(Db::syncType sType);
private:
HistoryKeeper(GenericDdInterface *db_);
HistoryKeeper(HistoryKeeper &hk) = delete;
HistoryKeeper& operator=(const HistoryKeeper&) = delete;
void updateChatsID();
void updateAliases();
QPair<int, ChatType> getChatID(const QString &id_str, ChatType ct);
int getAliasID(const QString &id_str);
QString wrapMessage(const QString &str);
QString unWrapMessage(const QString &str);
QList<QString> 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<QString, int> aliases;
QMap<QString, QPair<int, ChatType>> chats;
qint64 messageID;
GenericDdInterface *oldDb;
};
#endif // HISTORYKEEPER_H

View File

@ -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 <QMutexLocker>
#include <QTimer>
@ -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 &timestamp)
void OfflineMsgEngine::registerReceipt(int receipt, int64_t messageID, ChatMessage::Ptr msg, const QDateTime &timestamp)
{
QMutexLocker ml(&mutex);
@ -78,13 +81,13 @@ void OfflineMsgEngine::deliverOfflineMsgs()
if (undeliveredMsgs.size() == 0)
return;
QMap<int, MsgPtr> msgs = undeliveredMsgs;
QMap<int64_t, MsgPtr> 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)
{

View File

@ -39,7 +39,7 @@ public:
static QMutex globalMutex;
void dischargeReceipt(int receipt);
void registerReceipt(int receipt, int messageID, ChatMessage::Ptr msg, const QDateTime &timestamp = QDateTime::currentDateTime());
void registerReceipt(int receipt, int64_t messageID, ChatMessage::Ptr msg, const QDateTime &timestamp = QDateTime::currentDateTime());
public slots:
void deliverOfflineMsgs();
@ -54,8 +54,8 @@ private:
QMutex mutex;
Friend* f;
QHash<int, int> receipts;
QMap<int, MsgPtr> undeliveredMsgs;
QHash<int, int64_t> receipts;
QMap<int64_t, MsgPtr> undeliveredMsgs;
static const int offlineTimeout;
};

View File

@ -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"<<name;
GUI::showError(QObject::tr("Error"), QObject::tr("qTox couldn't open your chat logs, they will be disabled."));
history.release();
}
coreThread = new QThread();
coreThread->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<HistoryKeeper::HistMessage> 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);
}

View File

@ -26,6 +26,8 @@
#include <QByteArray>
#include <QPixmap>
#include <tox/toxencryptsave.h>
#include <memory>
#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> 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<QString> profiles;

View File

@ -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

View File

@ -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"),

View File

@ -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);
}
}

View File

@ -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

View File

@ -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))

View File

@ -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"

View File

@ -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"

View File

@ -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<int>(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<QComboBox*>())
{
cb->installEventFilter(this);
cb->setFocusPolicy(Qt::StrongFocus);
}
for (QCheckBox *cb : findChildren<QCheckBox*>()) // 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<int>(Db::syncType::stFull);
bodyUI->syncTypeComboBox->setCurrentIndex(index);
onDbSyncTypeUpdated();
}
bool AdvancedForm::eventFilter(QObject *o, QEvent *e)
{
if ((e->type() == QEvent::Wheel) &&
(qobject_cast<QComboBox*>(o) || qobject_cast<QAbstractSpinBox*>(o) || qobject_cast<QCheckBox*>(o)))
(qobject_cast<QAbstractSpinBox*>(o) || qobject_cast<QCheckBox*>(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)"));
}

View File

@ -41,7 +41,6 @@ protected:
private slots:
void onMakeToxPortableUpdated();
void onDbSyncTypeUpdated();
void resetToDefault();
private:

View File

@ -24,8 +24,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>396</width>
<height>454</height>
<width>398</width>
<height>456</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
@ -62,39 +62,6 @@
</property>
</widget>
</item>
<item alignment="Qt::AlignTop">
<widget class="QGroupBox" name="historyGroup">
<property name="title">
<string>Chat history</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="dbLabel">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;http://www.sqlite.org/pragma.html#pragma_synchronous&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;Writing to DB&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="syncTypeComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">

View File

@ -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 <QMessageBox>
#include <QFile>
#include <QDebug>
@ -63,7 +65,7 @@ void PrivacyForm::onEnableLoggingUpdated()
QMessageBox::Yes|QMessageBox::No);
if (dialogDelHistory == QMessageBox::Yes)
{
HistoryKeeper::getInstance()->removeHistory();
Nexus::getProfile()->getHistory()->eraseHistory();
}
}
}

View File

@ -26,7 +26,6 @@
#include "groupwidget.h"
#include "circlewidget.h"
#include "widget.h"
#include "src/persistence/historykeeper.h"
#include <QGridLayout>
#include <QMimeData>
#include <QDragEnterEvent>

View File

@ -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
{

View File

@ -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