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

Merge branch 'sqlcipher'

This commit is contained in:
tux3 2015-12-19 04:18:08 +01:00
commit 7319df10e8
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