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:
commit
7319df10e8
16
qtox.pro
16
qtox.pro
|
@ -144,7 +144,7 @@ win32 {
|
|||
LIBS += -L$$PWD/libs/lib -ltoxav -ltoxcore -ltoxencryptsave -ltoxdns -lsodium -lvpx -lpthread
|
||||
LIBS += -L$$PWD/libs/lib -lavdevice -lavformat -lavcodec -lavutil -lswscale -lOpenAL32 -lopus
|
||||
LIBS += -lopengl32 -lole32 -loleaut32 -lvfw32 -lws2_32 -liphlpapi -lgdi32 -lshlwapi -luuid
|
||||
LIBS += -lqrencode
|
||||
LIBS += -lqrencode -lsqlcipher
|
||||
LIBS += -lstrmiids # For DirectShow
|
||||
contains(DEFINES, QTOX_FILTER_AUDIO) {
|
||||
contains(STATICPKG, YES) {
|
||||
|
@ -160,7 +160,7 @@ win32 {
|
|||
QMAKE_INFO_PLIST = osx/info.plist
|
||||
QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.7
|
||||
LIBS += -L$$PWD/libs/lib/ -ltoxcore -ltoxav -ltoxencryptsave -ltoxdns -lsodium -lvpx -lopus -framework OpenAL -lavformat -lavdevice -lavcodec -lavutil -lswscale -mmacosx-version-min=10.7
|
||||
LIBS += -lqrencode
|
||||
LIBS += -lqrencode -lsqlcipher
|
||||
contains(DEFINES, QTOX_PLATFORM_EXT) { LIBS += -framework IOKit -framework CoreFoundation }
|
||||
contains(DEFINES, QTOX_FILTER_AUDIO) { LIBS += -lfilteraudio }
|
||||
} else {
|
||||
|
@ -181,10 +181,10 @@ win32 {
|
|||
LIBS += -L$$PWD/libs/lib/ -lopus -lvpx -lopenal -Wl,-Bstatic -ltoxcore -ltoxav -ltoxencryptsave -ltoxdns -lsodium -lavformat -lavdevice -lavcodec -lavutil -lswscale -lz -Wl,-Bdynamic
|
||||
LIBS += -Wl,-Bstatic -ljpeg -ltiff -lpng -ljasper -lIlmImf -lIlmThread -lIex -ldc1394 -lraw1394 -lHalf -lz -llzma -ljbig
|
||||
LIBS += -Wl,-Bdynamic -lv4l1 -lv4l2 -lavformat -lavcodec -lavutil -lswscale -lusb-1.0
|
||||
LIBS += -lqrencode
|
||||
LIBS += -lqrencode -lsqlcipher
|
||||
} else {
|
||||
LIBS += -L$$PWD/libs/lib/ -ltoxcore -ltoxav -ltoxencryptsave -ltoxdns -lvpx -lsodium -lopenal -lavformat -lavdevice -lavcodec -lavutil -lswscale
|
||||
LIBS += -lqrencode
|
||||
LIBS += -lqrencode -lsqlcipher
|
||||
}
|
||||
|
||||
contains(DEFINES, QTOX_PLATFORM_EXT) {
|
||||
|
@ -515,7 +515,9 @@ SOURCES += \
|
|||
src/widget/tool/removefrienddialog.cpp \
|
||||
src/video/groupnetcamview.cpp \
|
||||
src/core/toxcall.cpp \
|
||||
src/widget/about/aboutuser.cpp
|
||||
src/widget/about/aboutuser.cpp \
|
||||
src/persistence/db/rawdatabase.cpp \
|
||||
src/persistence/history.cpp
|
||||
|
||||
HEADERS += \
|
||||
src/audio/audio.h \
|
||||
|
@ -569,4 +571,6 @@ HEADERS += \
|
|||
src/video/groupnetcamview.h \
|
||||
src/core/indexedlist.h \
|
||||
src/core/toxcall.h \
|
||||
src/widget/about/aboutuser.h
|
||||
src/widget/about/aboutuser.h \
|
||||
src/persistence/db/rawdatabase.h \
|
||||
src/persistence/history.h
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
466
src/persistence/db/rawdatabase.cpp
Normal file
466
src/persistence/db/rawdatabase.cpp
Normal 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);
|
||||
}
|
||||
}
|
115
src/persistence/db/rawdatabase.h
Normal file
115
src/persistence/db/rawdatabase.h
Normal 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
231
src/persistence/history.cpp
Normal 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
77
src/persistence/history.h
Normal 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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ×tamp)
|
||||
void OfflineMsgEngine::registerReceipt(int receipt, int64_t messageID, ChatMessage::Ptr msg, const QDateTime ×tamp)
|
||||
{
|
||||
QMutexLocker ml(&mutex);
|
||||
|
||||
|
@ -78,7 +81,7 @@ 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)
|
||||
|
|
|
@ -39,7 +39,7 @@ public:
|
|||
static QMutex globalMutex;
|
||||
|
||||
void dischargeReceipt(int receipt);
|
||||
void registerReceipt(int receipt, int messageID, ChatMessage::Ptr msg, const QDateTime ×tamp = QDateTime::currentDateTime());
|
||||
void registerReceipt(int receipt, int64_t messageID, ChatMessage::Ptr msg, const QDateTime ×tamp = QDateTime::currentDateTime());
|
||||
|
||||
public slots:
|
||||
void deliverOfflineMsgs();
|
||||
|
@ -54,8 +54,8 @@ private:
|
|||
|
||||
QMutex mutex;
|
||||
Friend* f;
|
||||
QHash<int, int> receipts;
|
||||
QMap<int, MsgPtr> undeliveredMsgs;
|
||||
QHash<int, int64_t> receipts;
|
||||
QMap<int64_t, MsgPtr> undeliveredMsgs;
|
||||
|
||||
static const int offlineTimeout;
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
if (history)
|
||||
{
|
||||
history->setPassword(newPassword);
|
||||
Nexus::getDesktopGUI()->reloadHistory();
|
||||
}
|
||||
saveAvatar(avatar, core->getSelfId().publicKey);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)"));
|
||||
}
|
||||
|
|
|
@ -41,7 +41,6 @@ protected:
|
|||
|
||||
private slots:
|
||||
void onMakeToxPortableUpdated();
|
||||
void onDbSyncTypeUpdated();
|
||||
void resetToDefault();
|
||||
|
||||
private:
|
||||
|
|
|
@ -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><html><head/><body><p><a href="http://www.sqlite.org/pragma.html#pragma_synchronous"><span style=" text-decoration: underline; color:#0000ff;">Writing to DB</span></a></p></body></html></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">
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@
|
|||
#include "groupwidget.h"
|
||||
#include "circlewidget.h"
|
||||
#include "widget.h"
|
||||
#include "src/persistence/historykeeper.h"
|
||||
#include <QGridLayout>
|
||||
#include <QMimeData>
|
||||
#include <QDragEnterEvent>
|
||||
|
|
|
@ -143,6 +143,11 @@ void GUI::showError(const QString& title, const QString& msg)
|
|||
{
|
||||
if (QThread::currentThread() == qApp->thread())
|
||||
{
|
||||
// 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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user