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 -ltoxav -ltoxcore -ltoxencryptsave -ltoxdns -lsodium -lvpx -lpthread
|
||||||
LIBS += -L$$PWD/libs/lib -lavdevice -lavformat -lavcodec -lavutil -lswscale -lOpenAL32 -lopus
|
LIBS += -L$$PWD/libs/lib -lavdevice -lavformat -lavcodec -lavutil -lswscale -lOpenAL32 -lopus
|
||||||
LIBS += -lopengl32 -lole32 -loleaut32 -lvfw32 -lws2_32 -liphlpapi -lgdi32 -lshlwapi -luuid
|
LIBS += -lopengl32 -lole32 -loleaut32 -lvfw32 -lws2_32 -liphlpapi -lgdi32 -lshlwapi -luuid
|
||||||
LIBS += -lqrencode
|
LIBS += -lqrencode -lsqlcipher
|
||||||
LIBS += -lstrmiids # For DirectShow
|
LIBS += -lstrmiids # For DirectShow
|
||||||
contains(DEFINES, QTOX_FILTER_AUDIO) {
|
contains(DEFINES, QTOX_FILTER_AUDIO) {
|
||||||
contains(STATICPKG, YES) {
|
contains(STATICPKG, YES) {
|
||||||
|
@ -160,7 +160,7 @@ win32 {
|
||||||
QMAKE_INFO_PLIST = osx/info.plist
|
QMAKE_INFO_PLIST = osx/info.plist
|
||||||
QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.7
|
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 += -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_PLATFORM_EXT) { LIBS += -framework IOKit -framework CoreFoundation }
|
||||||
contains(DEFINES, QTOX_FILTER_AUDIO) { LIBS += -lfilteraudio }
|
contains(DEFINES, QTOX_FILTER_AUDIO) { LIBS += -lfilteraudio }
|
||||||
} else {
|
} 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 += -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,-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 += -Wl,-Bdynamic -lv4l1 -lv4l2 -lavformat -lavcodec -lavutil -lswscale -lusb-1.0
|
||||||
LIBS += -lqrencode
|
LIBS += -lqrencode -lsqlcipher
|
||||||
} else {
|
} else {
|
||||||
LIBS += -L$$PWD/libs/lib/ -ltoxcore -ltoxav -ltoxencryptsave -ltoxdns -lvpx -lsodium -lopenal -lavformat -lavdevice -lavcodec -lavutil -lswscale
|
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) {
|
contains(DEFINES, QTOX_PLATFORM_EXT) {
|
||||||
|
@ -515,7 +515,9 @@ SOURCES += \
|
||||||
src/widget/tool/removefrienddialog.cpp \
|
src/widget/tool/removefrienddialog.cpp \
|
||||||
src/video/groupnetcamview.cpp \
|
src/video/groupnetcamview.cpp \
|
||||||
src/core/toxcall.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 += \
|
HEADERS += \
|
||||||
src/audio/audio.h \
|
src/audio/audio.h \
|
||||||
|
@ -569,4 +571,6 @@ HEADERS += \
|
||||||
src/video/groupnetcamview.h \
|
src/video/groupnetcamview.h \
|
||||||
src/core/indexedlist.h \
|
src/core/indexedlist.h \
|
||||||
src/core/toxcall.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/core/coreav.h"
|
||||||
#include "src/persistence/settings.h"
|
#include "src/persistence/settings.h"
|
||||||
#include "src/widget/gui.h"
|
#include "src/widget/gui.h"
|
||||||
#include "src/persistence/historykeeper.h"
|
|
||||||
#include "src/audio/audio.h"
|
#include "src/audio/audio.h"
|
||||||
#include "src/persistence/profilelocker.h"
|
#include "src/persistence/profilelocker.h"
|
||||||
#include "src/net/avatarbroadcaster.h"
|
#include "src/net/avatarbroadcaster.h"
|
||||||
|
@ -270,7 +269,8 @@ void Core::start()
|
||||||
if (!id.isEmpty())
|
if (!id.isEmpty())
|
||||||
emit idSet(id);
|
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())
|
if (Nexus::getProfile()->isEncrypted())
|
||||||
checkEncryptedHistory();
|
checkEncryptedHistory();
|
||||||
|
|
||||||
|
@ -593,7 +593,9 @@ void Core::requestFriendship(const QString& friendAddress, const QString& messag
|
||||||
if (message.length())
|
if (message.length())
|
||||||
inviteStr = tr("/me offers friendship, \"%1\"").arg(message);
|
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 friendAdded(friendId, userId);
|
||||||
emit friendshipChanged(friendId);
|
emit friendshipChanged(friendId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,9 +25,9 @@
|
||||||
#include "src/widget/gui.h"
|
#include "src/widget/gui.h"
|
||||||
#include "src/persistence/settings.h"
|
#include "src/persistence/settings.h"
|
||||||
#include "src/core/cstring.h"
|
#include "src/core/cstring.h"
|
||||||
#include "src/persistence/historykeeper.h"
|
|
||||||
#include "src/nexus.h"
|
#include "src/nexus.h"
|
||||||
#include "src/persistence/profile.h"
|
#include "src/persistence/profile.h"
|
||||||
|
#include "src/persistence/historykeeper.h"
|
||||||
#include <tox/tox.h>
|
#include <tox/tox.h>
|
||||||
#include <tox/toxencryptsave.h>
|
#include <tox/toxencryptsave.h>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
@ -118,6 +118,8 @@ void Core::checkEncryptedHistory()
|
||||||
{
|
{
|
||||||
QString path = HistoryKeeper::getHistoryPath();
|
QString path = HistoryKeeper::getHistoryPath();
|
||||||
bool exists = QFile::exists(path) && QFile(path).size()>0;
|
bool exists = QFile::exists(path) && QFile(path).size()>0;
|
||||||
|
if (!exists)
|
||||||
|
return;
|
||||||
|
|
||||||
QByteArray salt = getSaltFromFile(path);
|
QByteArray salt = getSaltFromFile(path);
|
||||||
if (exists && salt.size() == 0)
|
if (exists && salt.size() == 0)
|
||||||
|
|
|
@ -62,7 +62,7 @@ bool ToxId::operator!=(const ToxId &other) const
|
||||||
return publicKey != other.publicKey;
|
return publicKey != other.publicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ToxId::isActiveProfile() const
|
bool ToxId::isSelf() const
|
||||||
{
|
{
|
||||||
return *this == Core::getInstance()->getSelfId();
|
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 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.
|
/// the Tox ID of the currently active profile.
|
||||||
QString toString() const; ///< Returns the Tox ID as QString.
|
QString toString() const; ///< Returns the Tox ID as QString.
|
||||||
void clear(); ///< Clears all elements of the Tox ID.
|
void clear(); ///< Clears all elements of the Tox ID.
|
||||||
|
|
|
@ -25,7 +25,8 @@
|
||||||
#include "widget/gui.h"
|
#include "widget/gui.h"
|
||||||
#include "src/core/core.h"
|
#include "src/core/core.h"
|
||||||
#include "src/persistence/settings.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)
|
Friend::Friend(uint32_t FriendId, const ToxId &UserId)
|
||||||
: userName{Core::getInstance()->getPeerName(UserId)},
|
: userName{Core::getInstance()->getPeerName(UserId)},
|
||||||
|
@ -50,7 +51,7 @@ Friend::~Friend()
|
||||||
|
|
||||||
void Friend::loadHistory()
|
void Friend::loadHistory()
|
||||||
{
|
{
|
||||||
if (Settings::getInstance().getEnableLogging())
|
if (Nexus::getProfile()->isHistoryEnabled())
|
||||||
{
|
{
|
||||||
chatForm->loadHistory(QDateTime::currentDateTime().addDays(-7), true);
|
chatForm->loadHistory(QDateTime::currentDateTime().addDays(-7), true);
|
||||||
widget->historyLoaded = true;
|
widget->historyLoaded = true;
|
||||||
|
|
|
@ -86,7 +86,7 @@ void Group::regeneratePeerList()
|
||||||
for (int i = 0; i < nPeers; i++)
|
for (int i = 0; i < nPeers; i++)
|
||||||
{
|
{
|
||||||
ToxId id = Core::getInstance()->getGroupPeerToxId(groupId, i);
|
ToxId id = Core::getInstance()->getGroupPeerToxId(groupId, i);
|
||||||
if (id.isActiveProfile())
|
if (id.isSelf())
|
||||||
selfPeerNum = i;
|
selfPeerNum = i;
|
||||||
|
|
||||||
QString toxid = id.publicKey;
|
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;
|
static HistoryKeeper *historyInstance = nullptr;
|
||||||
QMutex HistoryKeeper::historyMutex;
|
QMutex HistoryKeeper::historyMutex;
|
||||||
|
|
||||||
HistoryKeeper *HistoryKeeper::getInstance()
|
HistoryKeeper *HistoryKeeper::getInstance(const Profile& profile)
|
||||||
{
|
{
|
||||||
historyMutex.lock();
|
historyMutex.lock();
|
||||||
if (historyInstance == nullptr)
|
if (historyInstance == nullptr)
|
||||||
|
@ -53,9 +53,9 @@ HistoryKeeper *HistoryKeeper::getInstance()
|
||||||
QString path(":memory:");
|
QString path(":memory:");
|
||||||
GenericDdInterface *dbIntf;
|
GenericDdInterface *dbIntf;
|
||||||
|
|
||||||
if (Nexus::getProfile()->isEncrypted())
|
if (profile.isEncrypted())
|
||||||
{
|
{
|
||||||
path = getHistoryPath();
|
path = getHistoryPath({}, 1);
|
||||||
dbIntf = new EncryptedDb(path, initLst);
|
dbIntf = new EncryptedDb(path, initLst);
|
||||||
|
|
||||||
historyInstance = new HistoryKeeper(dbIntf);
|
historyInstance = new HistoryKeeper(dbIntf);
|
||||||
|
@ -64,7 +64,7 @@ HistoryKeeper *HistoryKeeper::getInstance()
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
path = getHistoryPath();
|
path = getHistoryPath({}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
dbIntf = new PlainDb(path, initLst);
|
dbIntf = new PlainDb(path, initLst);
|
||||||
|
@ -87,7 +87,7 @@ bool HistoryKeeper::checkPassword(const TOX_PASS_KEY &passkey, int encrypted)
|
||||||
}
|
}
|
||||||
|
|
||||||
HistoryKeeper::HistoryKeeper(GenericDdInterface *db_) :
|
HistoryKeeper::HistoryKeeper(GenericDdInterface *db_) :
|
||||||
db(db_)
|
oldDb(db_)
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
DB format
|
DB format
|
||||||
|
@ -114,11 +114,11 @@ HistoryKeeper::HistoryKeeper(GenericDdInterface *db_) :
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// for old tables:
|
// 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())
|
if (ans.first())
|
||||||
{
|
{
|
||||||
int idMax = ans.value(0).toInt();
|
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;
|
int idCur = 0;
|
||||||
if (ret.first())
|
if (ret.first())
|
||||||
idCur = ret.value(0).toInt();
|
idCur = ret.value(0).toInt();
|
||||||
|
@ -126,131 +126,29 @@ HistoryKeeper::HistoryKeeper(GenericDdInterface *db_) :
|
||||||
if (idCur != idMax)
|
if (idCur != idMax)
|
||||||
{
|
{
|
||||||
QString cmd = QString("INSERT INTO sent_status (id, status) VALUES (%1, 1);").arg(idMax);
|
QString cmd = QString("INSERT INTO sent_status (id, status) VALUES (%1, 1);").arg(idMax);
|
||||||
db->exec(cmd);
|
oldDb->exec(cmd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//check table stuct
|
//check table stuct
|
||||||
ans = db->exec("PRAGMA table_info (\"history\")");
|
ans = oldDb->exec("PRAGMA table_info (\"history\")");
|
||||||
ans.seek(5);
|
ans.seek(5);
|
||||||
if (!ans.value(1).toString().contains("alias"))
|
if (!ans.value(1).toString().contains("alias"))
|
||||||
{
|
{
|
||||||
//add collum in table
|
//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.";
|
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()
|
HistoryKeeper::~HistoryKeeper()
|
||||||
{
|
{
|
||||||
delete db;
|
delete oldDb;
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<HistoryKeeper::HistMessage> HistoryKeeper::exportMessages()
|
QList<HistoryKeeper::HistMessage> HistoryKeeper::exportMessages()
|
||||||
{
|
{
|
||||||
QSqlQuery dbAnswer;
|
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;"));
|
QString("INNER JOIN aliases ON history.sender = aliases.id INNER JOIN chats ON history.chat_id = chats.id;"));
|
||||||
|
|
||||||
QList<HistMessage> res;
|
QList<HistMessage> res;
|
||||||
|
@ -274,41 +172,6 @@ QList<HistoryKeeper::HistMessage> HistoryKeeper::exportMessages()
|
||||||
return res;
|
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 HistoryKeeper::unWrapMessage(const QString &str)
|
||||||
{
|
{
|
||||||
QString unWrappedMessage(str);
|
QString unWrappedMessage(str);
|
||||||
|
@ -316,59 +179,6 @@ QString HistoryKeeper::unWrapMessage(const QString &str)
|
||||||
return unWrappedMessage;
|
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()
|
void HistoryKeeper::resetInstance()
|
||||||
{
|
{
|
||||||
if (historyInstance == nullptr)
|
if (historyInstance == nullptr)
|
||||||
|
@ -378,25 +188,6 @@ void HistoryKeeper::resetInstance()
|
||||||
historyInstance = nullptr;
|
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)
|
QString HistoryKeeper::getHistoryPath(QString currentProfile, int encrypted)
|
||||||
{
|
{
|
||||||
QDir baseDir(Settings::getInstance().getSettingsDirPath());
|
QDir baseDir(Settings::getInstance().getSettingsDirPath());
|
||||||
|
@ -409,70 +200,9 @@ QString HistoryKeeper::getHistoryPath(QString currentProfile, int encrypted)
|
||||||
return baseDir.filePath(currentProfile + ".qtox_history");
|
return baseDir.filePath(currentProfile + ".qtox_history");
|
||||||
}
|
}
|
||||||
|
|
||||||
void HistoryKeeper::renameHistory(QString from, QString to)
|
bool HistoryKeeper::isFileExist(bool encrypted)
|
||||||
{
|
{
|
||||||
resetInstance();
|
QString path = getHistoryPath({}, encrypted ? 1 : 0);
|
||||||
|
|
||||||
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();
|
|
||||||
QFile file(path);
|
QFile file(path);
|
||||||
|
|
||||||
return file.exists();
|
return file.exists();
|
||||||
|
@ -480,17 +210,16 @@ bool HistoryKeeper::isFileExist()
|
||||||
|
|
||||||
void HistoryKeeper::removeHistory()
|
void HistoryKeeper::removeHistory()
|
||||||
{
|
{
|
||||||
db->exec("BEGIN TRANSACTION;");
|
resetInstance();
|
||||||
db->exec("DELETE FROM sent_status;");
|
QFile::remove(getHistoryPath({}, 0));
|
||||||
db->exec("DELETE FROM history;");
|
QFile::remove(getHistoryPath({}, 1));
|
||||||
db->exec("COMMIT TRANSACTION;");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<HistoryKeeper::HistMessage> HistoryKeeper::exportMessagesDeleteFile()
|
QList<HistoryKeeper::HistMessage> HistoryKeeper::exportMessagesDeleteFile()
|
||||||
{
|
{
|
||||||
auto msgs = getInstance()->exportMessages();
|
auto msgs = getInstance(*Nexus::getProfile())->exportMessages();
|
||||||
qDebug() << "Messages exported";
|
qDebug() << "Messages exported";
|
||||||
getInstance()->removeHistory();
|
getInstance(*Nexus::getProfile())->removeHistory();
|
||||||
|
|
||||||
return msgs;
|
return msgs;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,13 +27,19 @@
|
||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
#include <tox/toxencryptsave.h>
|
#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;
|
class GenericDdInterface;
|
||||||
namespace Db { enum class syncType; }
|
namespace Db { enum class syncType; }
|
||||||
|
|
||||||
class HistoryKeeper
|
class HistoryKeeper
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
enum ChatType {ctSingle = 0, ctGroup};
|
|
||||||
static QMutex historyMutex;
|
static QMutex historyMutex;
|
||||||
|
|
||||||
struct HistMessage
|
struct HistMessage
|
||||||
|
@ -52,47 +58,23 @@ public:
|
||||||
|
|
||||||
virtual ~HistoryKeeper();
|
virtual ~HistoryKeeper();
|
||||||
|
|
||||||
static HistoryKeeper* getInstance();
|
static HistoryKeeper* getInstance(const Profile& profile);
|
||||||
static void resetInstance();
|
static void resetInstance();
|
||||||
|
|
||||||
static QString getHistoryPath(QString currentProfile = QString(), int encrypted = -1); // -1 defaults to checking settings, 0 or 1 to specify
|
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 checkPassword(const TOX_PASS_KEY& passkey, int encrypted = -1);
|
||||||
static bool isFileExist();
|
static bool isFileExist(bool encrypted);
|
||||||
static void renameHistory(QString from, QString to);
|
|
||||||
void removeHistory();
|
void removeHistory();
|
||||||
static QList<HistMessage> exportMessagesDeleteFile();
|
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();
|
QList<HistMessage> exportMessages();
|
||||||
void importMessages(const QList<HistoryKeeper::HistMessage> &lst);
|
|
||||||
|
|
||||||
void setSyncType(Db::syncType sType);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
HistoryKeeper(GenericDdInterface *db_);
|
HistoryKeeper(GenericDdInterface *db_);
|
||||||
HistoryKeeper(HistoryKeeper &hk) = delete;
|
HistoryKeeper(HistoryKeeper &hk) = delete;
|
||||||
HistoryKeeper& operator=(const HistoryKeeper&) = 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);
|
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);
|
GenericDdInterface *oldDb;
|
||||||
bool needImport = false; // must be deleted with "importAvatarToDatabase"
|
|
||||||
GenericDdInterface *db;
|
|
||||||
QMap<QString, int> aliases;
|
|
||||||
QMap<QString, QPair<int, ChatType>> chats;
|
|
||||||
qint64 messageID;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // HISTORYKEEPER_H
|
#endif // HISTORYKEEPER_H
|
||||||
|
|
|
@ -19,9 +19,10 @@
|
||||||
|
|
||||||
#include "offlinemsgengine.h"
|
#include "offlinemsgengine.h"
|
||||||
#include "src/friend.h"
|
#include "src/friend.h"
|
||||||
#include "src/persistence/historykeeper.h"
|
|
||||||
#include "src/persistence/settings.h"
|
#include "src/persistence/settings.h"
|
||||||
#include "src/core/core.h"
|
#include "src/core/core.h"
|
||||||
|
#include "src/nexus.h"
|
||||||
|
#include "src/persistence/profile.h"
|
||||||
#include <QMutexLocker>
|
#include <QMutexLocker>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
|
@ -42,6 +43,7 @@ void OfflineMsgEngine::dischargeReceipt(int receipt)
|
||||||
{
|
{
|
||||||
QMutexLocker ml(&mutex);
|
QMutexLocker ml(&mutex);
|
||||||
|
|
||||||
|
Profile* profile = Nexus::getProfile();
|
||||||
auto it = receipts.find(receipt);
|
auto it = receipts.find(receipt);
|
||||||
if (it != receipts.end())
|
if (it != receipts.end())
|
||||||
{
|
{
|
||||||
|
@ -49,7 +51,8 @@ void OfflineMsgEngine::dischargeReceipt(int receipt)
|
||||||
auto msgIt = undeliveredMsgs.find(mID);
|
auto msgIt = undeliveredMsgs.find(mID);
|
||||||
if (msgIt != undeliveredMsgs.end())
|
if (msgIt != undeliveredMsgs.end())
|
||||||
{
|
{
|
||||||
HistoryKeeper::getInstance()->markAsSent(mID);
|
if (profile->isHistoryEnabled())
|
||||||
|
profile->getHistory()->markAsSent(mID);
|
||||||
msgIt.value().msg->markAsSent(QDateTime::currentDateTime());
|
msgIt.value().msg->markAsSent(QDateTime::currentDateTime());
|
||||||
undeliveredMsgs.erase(msgIt);
|
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);
|
QMutexLocker ml(&mutex);
|
||||||
|
|
||||||
|
@ -78,13 +81,13 @@ void OfflineMsgEngine::deliverOfflineMsgs()
|
||||||
if (undeliveredMsgs.size() == 0)
|
if (undeliveredMsgs.size() == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
QMap<int, MsgPtr> msgs = undeliveredMsgs;
|
QMap<int64_t, MsgPtr> msgs = undeliveredMsgs;
|
||||||
removeAllReciepts();
|
removeAllReciepts();
|
||||||
|
|
||||||
for (auto iter = msgs.begin(); iter != msgs.end(); ++iter)
|
for (auto iter = msgs.begin(); iter != msgs.end(); ++iter)
|
||||||
{
|
{
|
||||||
auto val = iter.value();
|
auto val = iter.value();
|
||||||
auto key = iter.key();
|
auto key = iter.key();
|
||||||
|
|
||||||
if (val.timestamp.msecsTo(QDateTime::currentDateTime()) < offlineTimeout)
|
if (val.timestamp.msecsTo(QDateTime::currentDateTime()) < offlineTimeout)
|
||||||
{
|
{
|
||||||
|
|
|
@ -39,7 +39,7 @@ public:
|
||||||
static QMutex globalMutex;
|
static QMutex globalMutex;
|
||||||
|
|
||||||
void dischargeReceipt(int receipt);
|
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:
|
public slots:
|
||||||
void deliverOfflineMsgs();
|
void deliverOfflineMsgs();
|
||||||
|
@ -54,8 +54,8 @@ private:
|
||||||
|
|
||||||
QMutex mutex;
|
QMutex mutex;
|
||||||
Friend* f;
|
Friend* f;
|
||||||
QHash<int, int> receipts;
|
QHash<int, int64_t> receipts;
|
||||||
QMap<int, MsgPtr> undeliveredMsgs;
|
QMap<int64_t, MsgPtr> undeliveredMsgs;
|
||||||
|
|
||||||
static const int offlineTimeout;
|
static const int offlineTimeout;
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,8 +21,8 @@
|
||||||
#include "profile.h"
|
#include "profile.h"
|
||||||
#include "profilelocker.h"
|
#include "profilelocker.h"
|
||||||
#include "src/persistence/settings.h"
|
#include "src/persistence/settings.h"
|
||||||
#include "src/core/core.h"
|
|
||||||
#include "src/persistence/historykeeper.h"
|
#include "src/persistence/historykeeper.h"
|
||||||
|
#include "src/core/core.h"
|
||||||
#include "src/widget/gui.h"
|
#include "src/widget/gui.h"
|
||||||
#include "src/widget/widget.h"
|
#include "src/widget/widget.h"
|
||||||
#include "src/nexus.h"
|
#include "src/nexus.h"
|
||||||
|
@ -47,7 +47,16 @@ Profile::Profile(QString name, QString password, bool isNewProfile)
|
||||||
Settings& s = Settings::getInstance();
|
Settings& s = Settings::getInstance();
|
||||||
s.setCurrentProfile(name);
|
s.setCurrentProfile(name);
|
||||||
s.saveGlobal();
|
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 = new QThread();
|
||||||
coreThread->setObjectName("qTox Core");
|
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)
|
Profile* Profile::createProfile(QString name, QString password)
|
||||||
|
@ -211,7 +223,7 @@ Core* Profile::getCore()
|
||||||
return core;
|
return core;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Profile::getName()
|
QString Profile::getName() const
|
||||||
{
|
{
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
@ -379,7 +391,7 @@ QByteArray Profile::loadAvatarData(const QString &ownerId)
|
||||||
|
|
||||||
void Profile::saveAvatar(QByteArray pic, const QString &ownerId)
|
void Profile::saveAvatar(QByteArray pic, const QString &ownerId)
|
||||||
{
|
{
|
||||||
if (!password.isEmpty())
|
if (!password.isEmpty() && !pic.isEmpty())
|
||||||
pic = core->encryptData(pic, passkey);
|
pic = core->encryptData(pic, passkey);
|
||||||
|
|
||||||
QString path = avatarPath(ownerId);
|
QString path = avatarPath(ownerId);
|
||||||
|
@ -407,6 +419,16 @@ void Profile::removeAvatar()
|
||||||
removeAvatar(core->getSelfId().publicKey);
|
removeAvatar(core->getSelfId().publicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Profile::isHistoryEnabled()
|
||||||
|
{
|
||||||
|
return Settings::getInstance().getEnableLogging() && history;
|
||||||
|
}
|
||||||
|
|
||||||
|
History *Profile::getHistory()
|
||||||
|
{
|
||||||
|
return history.get();
|
||||||
|
}
|
||||||
|
|
||||||
void Profile::removeAvatar(const QString &ownerId)
|
void Profile::removeAvatar(const QString &ownerId)
|
||||||
{
|
{
|
||||||
QFile::remove(avatarPath(ownerId));
|
QFile::remove(avatarPath(ownerId));
|
||||||
|
@ -419,7 +441,7 @@ bool Profile::exists(QString name)
|
||||||
return QFile::exists(path+".tox") && QFile::exists(path+".ini");
|
return QFile::exists(path+".tox") && QFile::exists(path+".ini");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Profile::isEncrypted()
|
bool Profile::isEncrypted() const
|
||||||
{
|
{
|
||||||
return !password.isEmpty();
|
return !password.isEmpty();
|
||||||
}
|
}
|
||||||
|
@ -478,7 +500,8 @@ bool Profile::rename(QString newName)
|
||||||
|
|
||||||
QFile::rename(path+".tox", newPath+".tox");
|
QFile::rename(path+".tox", newPath+".tox");
|
||||||
QFile::rename(path+".ini", newPath+".ini");
|
QFile::rename(path+".ini", newPath+".ini");
|
||||||
HistoryKeeper::renameHistory(name, newName);
|
if (history)
|
||||||
|
history->rename(newName);
|
||||||
bool resetAutorun = Settings::getInstance().getAutorun();
|
bool resetAutorun = Settings::getInstance().getAutorun();
|
||||||
Settings::getInstance().setAutorun(false);
|
Settings::getInstance().setAutorun(false);
|
||||||
Settings::getInstance().setCurrentProfile(newName);
|
Settings::getInstance().setCurrentProfile(newName);
|
||||||
|
@ -497,12 +520,12 @@ bool Profile::checkPassword()
|
||||||
return !loadToxSave().isEmpty();
|
return !loadToxSave().isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Profile::getPassword()
|
QString Profile::getPassword() const
|
||||||
{
|
{
|
||||||
return password;
|
return password;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOX_PASS_KEY& Profile::getPasskey()
|
const TOX_PASS_KEY& Profile::getPasskey() const
|
||||||
{
|
{
|
||||||
return passkey;
|
return passkey;
|
||||||
}
|
}
|
||||||
|
@ -517,14 +540,16 @@ void Profile::restartCore()
|
||||||
|
|
||||||
void Profile::setPassword(QString newPassword)
|
void Profile::setPassword(QString newPassword)
|
||||||
{
|
{
|
||||||
QList<HistoryKeeper::HistMessage> oldMessages = HistoryKeeper::exportMessagesDeleteFile();
|
|
||||||
QByteArray avatar = loadAvatarData(core->getSelfId().publicKey);
|
QByteArray avatar = loadAvatarData(core->getSelfId().publicKey);
|
||||||
|
|
||||||
password = newPassword;
|
password = newPassword;
|
||||||
passkey = *core->createPasskey(password);
|
passkey = *core->createPasskey(password);
|
||||||
saveToxSave();
|
saveToxSave();
|
||||||
|
|
||||||
HistoryKeeper::getInstance()->importMessages(oldMessages);
|
if (history)
|
||||||
Nexus::getDesktopGUI()->reloadHistory();
|
{
|
||||||
|
history->setPassword(newPassword);
|
||||||
|
Nexus::getDesktopGUI()->reloadHistory();
|
||||||
|
}
|
||||||
saveAvatar(avatar, core->getSelfId().publicKey);
|
saveAvatar(avatar, core->getSelfId().publicKey);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,8 @@
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <tox/toxencryptsave.h>
|
#include <tox/toxencryptsave.h>
|
||||||
|
#include <memory>
|
||||||
|
#include "src/persistence/history.h"
|
||||||
|
|
||||||
class Core;
|
class Core;
|
||||||
class QThread;
|
class QThread;
|
||||||
|
@ -44,16 +46,16 @@ public:
|
||||||
~Profile();
|
~Profile();
|
||||||
|
|
||||||
Core* getCore();
|
Core* getCore();
|
||||||
QString getName();
|
QString getName() const;
|
||||||
|
|
||||||
void startCore(); ///< Starts the Core thread
|
void startCore(); ///< Starts the Core thread
|
||||||
void restartCore(); ///< Delete core and restart a new one
|
void restartCore(); ///< Delete core and restart a new one
|
||||||
bool isNewProfile();
|
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
|
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
|
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
|
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.
|
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(const QString& ownerId); ///< Removes a cached avatar
|
||||||
void removeAvatar(); ///< Removes our own 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
|
/// Removes the profile permanently
|
||||||
/// It is invalid to call loadToxSave or saveToxSave on a deleted profile
|
/// It is invalid to call loadToxSave or saveToxSave on a deleted profile
|
||||||
/// Updates the profiles vector
|
/// Updates the profiles vector
|
||||||
|
@ -100,6 +107,7 @@ private:
|
||||||
QThread* coreThread;
|
QThread* coreThread;
|
||||||
QString name, password;
|
QString name, password;
|
||||||
TOX_PASS_KEY passkey;
|
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 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()
|
bool isRemoved; ///< True if the profile has been removed by remove()
|
||||||
static QVector<QString> profiles;
|
static QVector<QString> profiles;
|
||||||
|
|
|
@ -26,7 +26,6 @@
|
||||||
#include "src/widget/gui.h"
|
#include "src/widget/gui.h"
|
||||||
#include "src/persistence/profilelocker.h"
|
#include "src/persistence/profilelocker.h"
|
||||||
#include "src/persistence/settingsserializer.h"
|
#include "src/persistence/settingsserializer.h"
|
||||||
#include "src/persistence/historykeeper.h"
|
|
||||||
#include "src/nexus.h"
|
#include "src/nexus.h"
|
||||||
#include "src/persistence/profile.h"
|
#include "src/persistence/profile.h"
|
||||||
#ifdef QTOX_PLATFORM_EXT
|
#ifdef QTOX_PLATFORM_EXT
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#include "aboutuser.h"
|
#include "aboutuser.h"
|
||||||
#include "ui_aboutuser.h"
|
#include "ui_aboutuser.h"
|
||||||
#include "src/persistence/settings.h"
|
#include "src/persistence/settings.h"
|
||||||
#include "src/persistence/historykeeper.h"
|
|
||||||
#include "src/persistence/profile.h"
|
#include "src/persistence/profile.h"
|
||||||
#include "src/nexus.h"
|
#include "src/nexus.h"
|
||||||
|
|
||||||
|
@ -97,7 +96,9 @@ void AboutUser::onAcceptedClicked()
|
||||||
|
|
||||||
void AboutUser::onRemoveHistoryClicked()
|
void AboutUser::onRemoveHistoryClicked()
|
||||||
{
|
{
|
||||||
HistoryKeeper::getInstance()->removeFriendHistory(toxId.publicKey);
|
History* history = Nexus::getProfile()->getHistory();
|
||||||
|
if (history)
|
||||||
|
history->removeFriendHistory(toxId.publicKey);
|
||||||
QMessageBox::StandardButton reply;
|
QMessageBox::StandardButton reply;
|
||||||
reply = QMessageBox::information(this,
|
reply = QMessageBox::information(this,
|
||||||
tr("History removed"),
|
tr("History removed"),
|
||||||
|
|
|
@ -177,7 +177,7 @@ void AddFriendForm::setIdFromClipboard()
|
||||||
QString id = clipboard->text().trimmed();
|
QString id = clipboard->text().trimmed();
|
||||||
if (Core::getInstance()->isReady() && !id.isEmpty() && ToxId::isToxId(id))
|
if (Core::getInstance()->isReady() && !id.isEmpty() && ToxId::isToxId(id))
|
||||||
{
|
{
|
||||||
if (!ToxId(id).isActiveProfile())
|
if (!ToxId(id).isSelf())
|
||||||
toxId.setText(id);
|
toxId.setText(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,6 @@
|
||||||
#include "src/core/core.h"
|
#include "src/core/core.h"
|
||||||
#include "src/core/coreav.h"
|
#include "src/core/coreav.h"
|
||||||
#include "src/friend.h"
|
#include "src/friend.h"
|
||||||
#include "src/persistence/historykeeper.h"
|
|
||||||
#include "src/widget/style.h"
|
#include "src/widget/style.h"
|
||||||
#include "src/persistence/settings.h"
|
#include "src/persistence/settings.h"
|
||||||
#include "src/core/cstring.h"
|
#include "src/core/cstring.h"
|
||||||
|
@ -61,6 +60,8 @@
|
||||||
#include "src/widget/translator.h"
|
#include "src/widget/translator.h"
|
||||||
#include "src/video/videosource.h"
|
#include "src/video/videosource.h"
|
||||||
#include "src/video/camerasource.h"
|
#include "src/video/camerasource.h"
|
||||||
|
#include "src/nexus.h"
|
||||||
|
#include "src/persistence/profile.h"
|
||||||
|
|
||||||
ChatForm::ChatForm(Friend* chatFriend)
|
ChatForm::ChatForm(Friend* chatFriend)
|
||||||
: f(chatFriend)
|
: f(chatFriend)
|
||||||
|
@ -208,7 +209,7 @@ void ChatForm::startFileSend(ToxFile file)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
QString name;
|
QString name;
|
||||||
if (!previousId.isActiveProfile())
|
if (!previousId.isSelf())
|
||||||
{
|
{
|
||||||
Core* core = Core::getInstance();
|
Core* core = Core::getInstance();
|
||||||
name = core->getUsername();
|
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 storedPrevId = previousId;
|
||||||
ToxId prevId;
|
ToxId prevId;
|
||||||
|
@ -716,13 +717,13 @@ void ChatForm::loadHistory(QDateTime since, bool processUndelivered)
|
||||||
|
|
||||||
// Show each messages
|
// Show each messages
|
||||||
ToxId authorId = ToxId(it.sender);
|
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);
|
bool isAction = it.message.startsWith("/me ", Qt::CaseInsensitive);
|
||||||
|
|
||||||
ChatMessage::Ptr msg = ChatMessage::createChatMessage(authorStr,
|
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,
|
isAction ? ChatMessage::ACTION : ChatMessage::NORMAL,
|
||||||
authorId.isActiveProfile(),
|
authorId.isSelf(),
|
||||||
QDateTime());
|
QDateTime());
|
||||||
|
|
||||||
if (!isAction && (prevId == authorId) && (prevMsgDateTime.secsTo(msgDateTime) < getChatLog()->repNameAfter) )
|
if (!isAction && (prevId == authorId) && (prevMsgDateTime.secsTo(msgDateTime) < getChatLog()->repNameAfter) )
|
||||||
|
@ -731,7 +732,7 @@ void ChatForm::loadHistory(QDateTime since, bool processUndelivered)
|
||||||
prevId = authorId;
|
prevId = authorId;
|
||||||
prevMsgDateTime = msgDateTime;
|
prevMsgDateTime = msgDateTime;
|
||||||
|
|
||||||
if (it.isSent || !authorId.isActiveProfile())
|
if (it.isSent || !authorId.isSelf())
|
||||||
{
|
{
|
||||||
msg->markAsSent(msgDateTime);
|
msg->markAsSent(msgDateTime);
|
||||||
}
|
}
|
||||||
|
@ -810,6 +811,9 @@ void ChatForm::onScreenshotTaken(const QPixmap &pixmap) {
|
||||||
|
|
||||||
void ChatForm::onLoadHistory()
|
void ChatForm::onLoadHistory()
|
||||||
{
|
{
|
||||||
|
if (!Nexus::getProfile()->isHistoryEnabled())
|
||||||
|
return;
|
||||||
|
|
||||||
LoadHistoryDialog dlg;
|
LoadHistoryDialog dlg;
|
||||||
|
|
||||||
if (dlg.exec())
|
if (dlg.exec())
|
||||||
|
@ -937,9 +941,6 @@ void ChatForm::SendMessageStr(QString msg)
|
||||||
|
|
||||||
bool status = !Settings::getInstance().getFauxOfflineMessaging();
|
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);
|
ChatMessage::Ptr ma = addSelfMessage(qt_msg, isAction, timestamp, false);
|
||||||
|
|
||||||
int rec;
|
int rec;
|
||||||
|
@ -948,7 +949,13 @@ void ChatForm::SendMessageStr(QString msg)
|
||||||
else
|
else
|
||||||
rec = Core::getInstance()->sendMessage(f->getFriendID(), qt_msg);
|
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
|
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,
|
ChatMessage::Ptr GenericChatForm::addMessage(const ToxId& author, const QString &message, bool isAction,
|
||||||
const QDateTime &datetime, bool isSent)
|
const QDateTime &datetime, bool isSent)
|
||||||
{
|
{
|
||||||
bool authorIsActiveProfile = author.isActiveProfile();
|
bool authorIsActiveProfile = author.isSelf();
|
||||||
QString authorStr = authorIsActiveProfile ? Core::getInstance()->getUsername() : resolveToxId(author);
|
QString authorStr = authorIsActiveProfile ? Core::getInstance()->getUsername() : resolveToxId(author);
|
||||||
|
|
||||||
ChatMessage::Ptr msg;
|
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)
|
void GenericChatForm::addAlertMessage(const ToxId &author, QString message, QDateTime datetime)
|
||||||
{
|
{
|
||||||
QString authorStr = resolveToxId(author);
|
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);
|
insertChatMessage(msg);
|
||||||
|
|
||||||
if ((author == previousId) && (prevMsgDateTime.secsTo(QDateTime::currentDateTime()) < getChatLog()->repNameAfter))
|
if ((author == previousId) && (prevMsgDateTime.secsTo(QDateTime::currentDateTime()) < getChatLog()->repNameAfter))
|
||||||
|
|
|
@ -27,7 +27,6 @@
|
||||||
#include "src/core/core.h"
|
#include "src/core/core.h"
|
||||||
#include "src/core/coreav.h"
|
#include "src/core/coreav.h"
|
||||||
#include "src/widget/style.h"
|
#include "src/widget/style.h"
|
||||||
#include "src/persistence/historykeeper.h"
|
|
||||||
#include "src/widget/flowlayout.h"
|
#include "src/widget/flowlayout.h"
|
||||||
#include "src/widget/translator.h"
|
#include "src/widget/translator.h"
|
||||||
#include "src/video/groupnetcamview.h"
|
#include "src/video/groupnetcamview.h"
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
#include "src/widget/widget.h"
|
#include "src/widget/widget.h"
|
||||||
#include "src/widget/gui.h"
|
#include "src/widget/gui.h"
|
||||||
#include "src/widget/style.h"
|
#include "src/widget/style.h"
|
||||||
#include "src/persistence/historykeeper.h"
|
|
||||||
#include "src/persistence/profilelocker.h"
|
#include "src/persistence/profilelocker.h"
|
||||||
#include "src/persistence/profile.h"
|
#include "src/persistence/profile.h"
|
||||||
#include "src/widget/translator.h"
|
#include "src/widget/translator.h"
|
||||||
|
|
|
@ -20,7 +20,6 @@
|
||||||
#include "ui_advancedsettings.h"
|
#include "ui_advancedsettings.h"
|
||||||
|
|
||||||
#include "advancedform.h"
|
#include "advancedform.h"
|
||||||
#include "src/persistence/historykeeper.h"
|
|
||||||
#include "src/persistence/settings.h"
|
#include "src/persistence/settings.h"
|
||||||
#include "src/persistence/db/plaindb.h"
|
#include "src/persistence/db/plaindb.h"
|
||||||
#include "src/widget/translator.h"
|
#include "src/widget/translator.h"
|
||||||
|
@ -31,28 +30,11 @@ AdvancedForm::AdvancedForm() :
|
||||||
bodyUI = new Ui::AdvancedSettings;
|
bodyUI = new Ui::AdvancedSettings;
|
||||||
bodyUI->setupUi(this);
|
bodyUI->setupUi(this);
|
||||||
|
|
||||||
bodyUI->dbLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
|
|
||||||
bodyUI->dbLabel->setOpenExternalLinks(true);
|
|
||||||
|
|
||||||
bodyUI->cbMakeToxPortable->setChecked(Settings::getInstance().getMakeToxPortable());
|
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->cbMakeToxPortable, &QCheckBox::stateChanged, this, &AdvancedForm::onMakeToxPortableUpdated);
|
||||||
connect(bodyUI->syncTypeComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(onDbSyncTypeUpdated()));
|
|
||||||
connect(bodyUI->resetButton, SIGNAL(clicked()), this, SLOT(resetToDefault()));
|
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
|
for (QCheckBox *cb : findChildren<QCheckBox*>()) // this one is to allow scrolling on checkboxes
|
||||||
{
|
{
|
||||||
cb->installEventFilter(this);
|
cb->installEventFilter(this);
|
||||||
|
@ -72,24 +54,14 @@ void AdvancedForm::onMakeToxPortableUpdated()
|
||||||
Settings::getInstance().setMakeToxPortable(bodyUI->cbMakeToxPortable->isChecked());
|
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()
|
void AdvancedForm::resetToDefault()
|
||||||
{
|
{
|
||||||
int index = 2 - static_cast<int>(Db::syncType::stFull);
|
|
||||||
bodyUI->syncTypeComboBox->setCurrentIndex(index);
|
|
||||||
onDbSyncTypeUpdated();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AdvancedForm::eventFilter(QObject *o, QEvent *e)
|
bool AdvancedForm::eventFilter(QObject *o, QEvent *e)
|
||||||
{
|
{
|
||||||
if ((e->type() == QEvent::Wheel) &&
|
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();
|
e->ignore();
|
||||||
return true;
|
return true;
|
||||||
|
@ -100,7 +72,4 @@ bool AdvancedForm::eventFilter(QObject *o, QEvent *e)
|
||||||
void AdvancedForm::retranslateUi()
|
void AdvancedForm::retranslateUi()
|
||||||
{
|
{
|
||||||
bodyUI->retranslateUi(this);
|
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:
|
private slots:
|
||||||
void onMakeToxPortableUpdated();
|
void onMakeToxPortableUpdated();
|
||||||
void onDbSyncTypeUpdated();
|
|
||||||
void resetToDefault();
|
void resetToDefault();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -24,8 +24,8 @@
|
||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>396</width>
|
<width>398</width>
|
||||||
<height>454</height>
|
<height>456</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
|
@ -62,39 +62,6 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</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>
|
<item>
|
||||||
<spacer name="verticalSpacer">
|
<spacer name="verticalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
|
|
|
@ -21,12 +21,14 @@
|
||||||
#include "ui_privacysettings.h"
|
#include "ui_privacysettings.h"
|
||||||
#include "src/widget/form/settingswidget.h"
|
#include "src/widget/form/settingswidget.h"
|
||||||
#include "src/persistence/settings.h"
|
#include "src/persistence/settings.h"
|
||||||
#include "src/persistence/historykeeper.h"
|
|
||||||
#include "src/core/core.h"
|
#include "src/core/core.h"
|
||||||
#include "src/widget/widget.h"
|
#include "src/widget/widget.h"
|
||||||
#include "src/widget/gui.h"
|
#include "src/widget/gui.h"
|
||||||
#include "src/widget/form/setpassworddialog.h"
|
#include "src/widget/form/setpassworddialog.h"
|
||||||
#include "src/widget/translator.h"
|
#include "src/widget/translator.h"
|
||||||
|
#include "src/nexus.h"
|
||||||
|
#include "src/persistence/profile.h"
|
||||||
|
#include "src/persistence/history.h"
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
@ -63,7 +65,7 @@ void PrivacyForm::onEnableLoggingUpdated()
|
||||||
QMessageBox::Yes|QMessageBox::No);
|
QMessageBox::Yes|QMessageBox::No);
|
||||||
if (dialogDelHistory == QMessageBox::Yes)
|
if (dialogDelHistory == QMessageBox::Yes)
|
||||||
{
|
{
|
||||||
HistoryKeeper::getInstance()->removeHistory();
|
Nexus::getProfile()->getHistory()->eraseHistory();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@
|
||||||
#include "groupwidget.h"
|
#include "groupwidget.h"
|
||||||
#include "circlewidget.h"
|
#include "circlewidget.h"
|
||||||
#include "widget.h"
|
#include "widget.h"
|
||||||
#include "src/persistence/historykeeper.h"
|
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
#include <QMimeData>
|
#include <QMimeData>
|
||||||
#include <QDragEnterEvent>
|
#include <QDragEnterEvent>
|
||||||
|
|
|
@ -143,7 +143,12 @@ void GUI::showError(const QString& title, const QString& msg)
|
||||||
{
|
{
|
||||||
if (QThread::currentThread() == qApp->thread())
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -37,7 +37,6 @@
|
||||||
#include "friendlistwidget.h"
|
#include "friendlistwidget.h"
|
||||||
#include "form/chatform.h"
|
#include "form/chatform.h"
|
||||||
#include "maskablepixmapwidget.h"
|
#include "maskablepixmapwidget.h"
|
||||||
#include "src/persistence/historykeeper.h"
|
|
||||||
#include "src/net/autoupdate.h"
|
#include "src/net/autoupdate.h"
|
||||||
#include "src/audio/audio.h"
|
#include "src/audio/audio.h"
|
||||||
#include "src/platform/timer.h"
|
#include "src/platform/timer.h"
|
||||||
|
@ -1081,7 +1080,7 @@ void Widget::onFriendMessageReceived(int friendId, const QString& message, bool
|
||||||
QDateTime timestamp = QDateTime::currentDateTime();
|
QDateTime timestamp = QDateTime::currentDateTime();
|
||||||
f->getChatForm()->addMessage(f->getToxId(), message, isAction, timestamp, true);
|
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());
|
f->getToxId().publicKey, timestamp, true, f->getDisplayedName());
|
||||||
|
|
||||||
newFriendMessageAlert(friendId);
|
newFriendMessageAlert(friendId);
|
||||||
|
@ -1281,7 +1280,7 @@ void Widget::removeFriend(Friend* f, bool fake)
|
||||||
if (!ask.accepted())
|
if (!ask.accepted())
|
||||||
return;
|
return;
|
||||||
else if (ask.removeHistory())
|
else if (ask.removeHistory())
|
||||||
HistoryKeeper::getInstance()->removeFriendHistory(f->getToxId().publicKey);
|
Nexus::getProfile()->getHistory()->removeFriendHistory(f->getToxId().publicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
f->getFriendWidget()->setAsInactiveChatroom();
|
f->getFriendWidget()->setAsInactiveChatroom();
|
||||||
|
@ -1444,7 +1443,7 @@ void Widget::onGroupMessageReceived(int groupnumber, int peernumber, const QStri
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ToxId author = Core::getInstance()->getGroupPeerToxId(groupnumber, peernumber);
|
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)
|
if (targeted && !isAction)
|
||||||
g->getChatForm()->addAlertMessage(author, message, QDateTime::currentDateTime());
|
g->getChatForm()->addAlertMessage(author, message, QDateTime::currentDateTime());
|
||||||
else
|
else
|
||||||
|
|
Loading…
Reference in New Issue
Block a user