|
|
|
@ -1,12 +1,30 @@
@@ -1,12 +1,30 @@
|
|
|
|
|
#include "history.h" |
|
|
|
|
#include "src/persistence/profile.h" |
|
|
|
|
#include "src/persistence/settings.h" |
|
|
|
|
#include "src/persistence/db/rawdatabase.h" |
|
|
|
|
#include "src/persistence/historykeeper.h" |
|
|
|
|
/*
|
|
|
|
|
Copyright © 2015-2016 by The qTox Project |
|
|
|
|
|
|
|
|
|
This file is part of qTox, a Qt-based graphical interface for Tox. |
|
|
|
|
|
|
|
|
|
qTox is libre software: you can redistribute it and/or modify |
|
|
|
|
it under the terms of the GNU General Public License as published by |
|
|
|
|
the Free Software Foundation, either version 3 of the License, or |
|
|
|
|
(at your option) any later version. |
|
|
|
|
|
|
|
|
|
qTox is distributed in the hope that it will be useful, |
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
|
|
GNU General Public License for more details. |
|
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License |
|
|
|
|
along with qTox. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
#include <QDebug> |
|
|
|
|
#include <cassert> |
|
|
|
|
|
|
|
|
|
using namespace std; |
|
|
|
|
#include "db/rawdatabase.h" |
|
|
|
|
#include "history.h" |
|
|
|
|
#include "historykeeper.h" |
|
|
|
|
#include "profile.h" |
|
|
|
|
#include "settings.h" |
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @class History |
|
|
|
@ -18,33 +36,43 @@ using namespace std;
@@ -18,33 +36,43 @@ using namespace std;
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Opens the profile database and prepares to work with the history. |
|
|
|
|
* @param profileName Profile name to load. |
|
|
|
|
* @param password If empty, the database will be opened unencrypted. |
|
|
|
|
* @brief Prepares the database to work with the history. |
|
|
|
|
* @param db This database will be prepared for use with the history. |
|
|
|
|
*/ |
|
|
|
|
History::History(const QString &profileName, const QString &password) |
|
|
|
|
: db{getDbPath(profileName), password} |
|
|
|
|
History::History(std::shared_ptr<RawDatabase> db) |
|
|
|
|
: db(db) |
|
|
|
|
{ |
|
|
|
|
if (!isValid()) |
|
|
|
|
{ |
|
|
|
|
init(); |
|
|
|
|
qWarning() << "Database not open, init failed"; |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Opens the profile database, and import from the old database. |
|
|
|
|
* @param profileName Profile name to load. |
|
|
|
|
* @param password If empty, the database will be opened unencrypted. |
|
|
|
|
* @param oldHistory Old history to import. |
|
|
|
|
*/ |
|
|
|
|
History::History(const QString &profileName, const QString &password, const HistoryKeeper &oldHistory) |
|
|
|
|
: History{profileName, password} |
|
|
|
|
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) |
|
|
|
|
{ |
|
|
|
|
import(oldHistory); |
|
|
|
|
peers[row[0].toString()] = row[1].toInt(); |
|
|
|
|
}}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
History::~History() |
|
|
|
|
{ |
|
|
|
|
if (!isValid()) |
|
|
|
|
{ |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// We could have execLater requests pending with a lambda attached,
|
|
|
|
|
// so clear the pending transactions first
|
|
|
|
|
db.sync(); |
|
|
|
|
db->sync(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -53,42 +81,20 @@ History::~History()
@@ -53,42 +81,20 @@ History::~History()
|
|
|
|
|
*/ |
|
|
|
|
bool History::isValid() |
|
|
|
|
{ |
|
|
|
|
return db.isOpen(); |
|
|
|
|
return db && db->isOpen(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Changes the database password, will encrypt or decrypt if necessary. |
|
|
|
|
* @param password Password to set. |
|
|
|
|
*/ |
|
|
|
|
void History::setPassword(const QString& password) |
|
|
|
|
{ |
|
|
|
|
db.setPassword(password); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Moves the database file on disk to match the new name. |
|
|
|
|
* @param newName New name. |
|
|
|
|
* @brief Erases all the chat history from the database. |
|
|
|
|
*/ |
|
|
|
|
void History::rename(const QString &newName) |
|
|
|
|
void History::eraseHistory() |
|
|
|
|
{ |
|
|
|
|
db.rename(getDbPath(newName)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Deletes the on-disk database file. |
|
|
|
|
* @return True if success, false otherwise. |
|
|
|
|
*/ |
|
|
|
|
bool History::remove() |
|
|
|
|
if (!isValid()) |
|
|
|
|
{ |
|
|
|
|
return db.remove(); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Erases all the chat history from the database. |
|
|
|
|
*/ |
|
|
|
|
void History::eraseHistory() |
|
|
|
|
{ |
|
|
|
|
db.execNow("DELETE FROM faux_offline_pending;" |
|
|
|
|
db->execNow("DELETE FROM faux_offline_pending;" |
|
|
|
|
"DELETE FROM history;" |
|
|
|
|
"DELETE FROM aliases;" |
|
|
|
|
"DELETE FROM peers;" |
|
|
|
@ -101,11 +107,20 @@ void History::eraseHistory()
@@ -101,11 +107,20 @@ void History::eraseHistory()
|
|
|
|
|
*/ |
|
|
|
|
void History::removeFriendHistory(const QString& friendPk) |
|
|
|
|
{ |
|
|
|
|
if (!isValid()) |
|
|
|
|
{ |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!peers.contains(friendPk)) |
|
|
|
|
{ |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
int64_t id = peers[friendPk]; |
|
|
|
|
|
|
|
|
|
if (db.execNow(QString("DELETE FROM faux_offline_pending " |
|
|
|
|
QString queryText = 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 " |
|
|
|
@ -114,7 +129,9 @@ void History::removeFriendHistory(const QString &friendPk)
@@ -114,7 +129,9 @@ void History::removeFriendHistory(const QString &friendPk)
|
|
|
|
|
"DELETE FROM history WHERE chat_id=%1; " |
|
|
|
|
"DELETE FROM aliases WHERE owner=%1; " |
|
|
|
|
"DELETE FROM peers WHERE id=%1; " |
|
|
|
|
"VACUUM;").arg(id))) |
|
|
|
|
"VACUUM;").arg(id); |
|
|
|
|
|
|
|
|
|
if (db->execNow(queryText)) |
|
|
|
|
{ |
|
|
|
|
peers.remove(friendPk); |
|
|
|
|
} |
|
|
|
@ -149,11 +166,18 @@ QVector<RawDatabase::Query> History::generateNewMessageQueries(const QString &fr
@@ -149,11 +166,18 @@ QVector<RawDatabase::Query> History::generateNewMessageQueries(const QString &fr
|
|
|
|
|
else |
|
|
|
|
{ |
|
|
|
|
if (peers.isEmpty()) |
|
|
|
|
{ |
|
|
|
|
peerId = 0; |
|
|
|
|
} |
|
|
|
|
else |
|
|
|
|
peerId = *max_element(begin(peers), end(peers))+1; |
|
|
|
|
{ |
|
|
|
|
peerId = *std::max_element(peers.begin(), peers.end()) + 1; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
peers[friendPk] = peerId; |
|
|
|
|
queries += RawDatabase::Query{("INSERT INTO peers (id, public_key) VALUES (%1, '"+friendPk+"');").arg(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
|
|
|
|
@ -165,11 +189,18 @@ QVector<RawDatabase::Query> History::generateNewMessageQueries(const QString &fr
@@ -165,11 +189,18 @@ QVector<RawDatabase::Query> History::generateNewMessageQueries(const QString &fr
|
|
|
|
|
else |
|
|
|
|
{ |
|
|
|
|
if (peers.isEmpty()) |
|
|
|
|
{ |
|
|
|
|
senderId = 0; |
|
|
|
|
} |
|
|
|
|
else |
|
|
|
|
senderId = *max_element(begin(peers), end(peers))+1; |
|
|
|
|
{ |
|
|
|
|
senderId = *std::max_element(peers.begin(), peers.end()) + 1; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
peers[sender] = senderId; |
|
|
|
|
queries += RawDatabase::Query{("INSERT INTO peers (id, public_key) VALUES (%1, '"+sender+"');").arg(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, ?);") |
|
|
|
@ -187,7 +218,12 @@ QVector<RawDatabase::Query> History::generateNewMessageQueries(const QString &fr
@@ -187,7 +218,12 @@ QVector<RawDatabase::Query> History::generateNewMessageQueries(const QString &fr
|
|
|
|
|
{message.toUtf8(), dispName.toUtf8()}, insertIdCallback); |
|
|
|
|
|
|
|
|
|
if (!isSent) |
|
|
|
|
queries += RawDatabase::Query{"INSERT INTO faux_offline_pending (id) VALUES (last_insert_rowid());"}; |
|
|
|
|
{ |
|
|
|
|
queries += RawDatabase::Query{ |
|
|
|
|
"INSERT INTO faux_offline_pending (id) VALUES (" |
|
|
|
|
" last_insert_rowid()" |
|
|
|
|
");"}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return queries; |
|
|
|
|
} |
|
|
|
@ -202,10 +238,18 @@ QVector<RawDatabase::Query> History::generateNewMessageQueries(const QString &fr
@@ -202,10 +238,18 @@ QVector<RawDatabase::Query> History::generateNewMessageQueries(const QString &fr
|
|
|
|
|
* @param dispName Name, which should be displayed. |
|
|
|
|
* @param insertIdCallback Function, called after query execution. |
|
|
|
|
*/ |
|
|
|
|
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) |
|
|
|
|
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)); |
|
|
|
|
if (!isValid()) |
|
|
|
|
{ |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
db->execLater(generateNewMessageQueries(friendPk, message, sender, time, |
|
|
|
|
isSent, dispName, insertIdCallback)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -215,13 +259,21 @@ void History::addNewMessage(const QString &friendPk, const QString &message, con
@@ -215,13 +259,21 @@ void History::addNewMessage(const QString &friendPk, const QString &message, con
|
|
|
|
|
* @param to End of period to fetch. |
|
|
|
|
* @return List of messages. |
|
|
|
|
*/ |
|
|
|
|
QList<History::HistMessage> History::getChatHistory(const QString &friendPk, const QDateTime &from, const QDateTime &to) |
|
|
|
|
QList<History::HistMessage> History::getChatHistory(const QString& friendPk, |
|
|
|
|
const QDateTime& from, |
|
|
|
|
const QDateTime& to) |
|
|
|
|
{ |
|
|
|
|
if (!isValid()) |
|
|
|
|
{ |
|
|
|
|
return {}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
QList<HistMessage> messages; |
|
|
|
|
|
|
|
|
|
auto rowCallback = [&messages](const QVector<QVariant>& row) |
|
|
|
|
{ |
|
|
|
|
// dispName and message could have null bytes, QString::fromUtf8 truncates on null bytes so we strip them
|
|
|
|
|
// 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()), |
|
|
|
@ -232,14 +284,18 @@ QList<History::HistMessage> History::getChatHistory(const QString &friendPk, con
@@ -232,14 +284,18 @@ QList<History::HistMessage> History::getChatHistory(const QString &friendPk, con
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
// 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 " |
|
|
|
|
QString queryText = 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}); |
|
|
|
|
.arg(from.toMSecsSinceEpoch()).arg(to.toMSecsSinceEpoch()).arg(friendPk); |
|
|
|
|
|
|
|
|
|
db->execNow({queryText, rowCallback}); |
|
|
|
|
|
|
|
|
|
return messages; |
|
|
|
|
} |
|
|
|
@ -250,45 +306,15 @@ QList<History::HistMessage> History::getChatHistory(const QString &friendPk, con
@@ -250,45 +306,15 @@ QList<History::HistMessage> History::getChatHistory(const QString &friendPk, con
|
|
|
|
|
* |
|
|
|
|
* @param id Message ID. |
|
|
|
|
*/ |
|
|
|
|
void History::markAsSent(qint64 id) |
|
|
|
|
{ |
|
|
|
|
db.execLater(QString("DELETE FROM faux_offline_pending WHERE id=%1;").arg(id)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Retrieves the path to the database file for a given profile. |
|
|
|
|
* @param profileName Profile name. |
|
|
|
|
* @return Path to database. |
|
|
|
|
*/ |
|
|
|
|
QString History::getDbPath(const QString &profileName) |
|
|
|
|
{ |
|
|
|
|
return Settings::getInstance().getSettingsDirPath() + profileName + ".db"; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @brief Makes sure the history tables are created |
|
|
|
|
*/ |
|
|
|
|
void History::init() |
|
|
|
|
void History::markAsSent(qint64 messageId) |
|
|
|
|
{ |
|
|
|
|
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(); |
|
|
|
|
}}); |
|
|
|
|
db->execLater(QString("DELETE FROM faux_offline_pending WHERE id=%1;") |
|
|
|
|
.arg(messageId)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -312,14 +338,15 @@ void History::import(const HistoryKeeper &oldHistory)
@@ -312,14 +338,15 @@ void History::import(const HistoryKeeper &oldHistory)
|
|
|
|
|
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); |
|
|
|
|
queries += generateNewMessageQueries(msg.chat, msg.message, msg.sender, |
|
|
|
|
msg.timestamp, true, msg.dispName); |
|
|
|
|
if (queries.size() >= batchSize) |
|
|
|
|
{ |
|
|
|
|
db.execLater(queries); |
|
|
|
|
db->execLater(queries); |
|
|
|
|
queries.clear(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
db.execLater(queries); |
|
|
|
|
db.sync(); |
|
|
|
|
db->execLater(queries); |
|
|
|
|
db->sync(); |
|
|
|
|
qDebug() << "Imported old database in" << t.elapsed() << "ms"; |
|
|
|
|
} |
|
|
|
|