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

238 lines
8.4 KiB
C++
Raw Normal View History

#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));
}
bool History::remove()
2015-12-19 23:51:15 +08:00
{
return db.remove();
2015-12-19 23:51:15 +08:00
}
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)
{
2015-12-19 12:00:23 +08:00
// dispName and message could have null bytes, QString::fromUtf8 truncates on null bytes so we strip them
messages += {row[0].toLongLong(),
row[1].isNull(),
QDateTime::fromMSecsSinceEpoch(row[2].toLongLong()),
row[3].toString(),
2015-12-19 12:00:23 +08:00
QString::fromUtf8(row[4].toByteArray().replace('\0',"")),
row[5].toString(),
2015-12-19 12:00:23 +08:00
QString::fromUtf8(row[6].toByteArray().replace('\0',""))};
};
// 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 public_key, id FROM peers;", [this](const QVector<QVariant>& row)
{
peers[row[0].toString()] = row[1].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";
}