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

885 lines
34 KiB
C++
Raw Normal View History

/*
Copyright © 2015-2019 by The qTox Project Contributors
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>
#include "history.h"
#include "profile.h"
#include "settings.h"
#include "db/rawdatabase.h"
2019-05-01 15:50:19 +08:00
namespace {
static constexpr int SCHEMA_VERSION = 2;
2019-05-01 15:50:19 +08:00
bool createCurrentSchema(RawDatabase& db)
2019-05-01 15:50:19 +08:00
{
QVector<RawDatabase::Query> queries;
2019-05-01 15:50:19 +08:00
queries += RawDatabase::Query(QStringLiteral(
"CREATE TABLE peers (id INTEGER PRIMARY KEY, "
"public_key TEXT NOT NULL UNIQUE);"
"CREATE TABLE aliases (id INTEGER PRIMARY KEY, "
"owner INTEGER, "
"display_name BLOB NOT NULL, "
"UNIQUE(owner, display_name));"
"CREATE TABLE history "
"(id INTEGER PRIMARY KEY, "
"timestamp INTEGER NOT NULL, "
"chat_id INTEGER NOT NULL, "
"sender_alias INTEGER NOT NULL, "
// even though technically a message can be null for file transfer, we've opted
// to just insert an empty string when there's no content, this moderately simplifies
// implementating to leakon as currently our database doesn't have support for optional
// fields. We would either have to insert "?" or "null" based on if message exists and then
// ensure that our blob vector always has the right number of fields. Better to just
// leave this as NOT NULL for now.
"message BLOB NOT NULL, "
"file_id INTEGER);"
"CREATE TABLE file_transfers "
"(id INTEGER PRIMARY KEY, "
"chat_id INTEGER NOT NULL, "
"file_restart_id BLOB NOT NULL, "
"file_name BLOB NOT NULL, "
"file_path BLOB NOT NULL, "
"file_hash BLOB NOT NULL, "
"file_size INTEGER NOT NULL, "
"direction INTEGER NOT NULL, "
"file_state INTEGER NOT NULL);"
"CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY);"
"CREATE TABLE broken_messages (id INTEGER PRIMARY KEY);"));
queries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = %1;").arg(SCHEMA_VERSION));
return db.execNow(queries);
2019-05-01 15:50:19 +08:00
}
bool isNewDb(std::shared_ptr<RawDatabase>& db, bool& success)
2019-05-01 15:50:19 +08:00
{
bool newDb;
if (!db->execNow(RawDatabase::Query("SELECT COUNT(*) FROM sqlite_master;",
[&](const QVector<QVariant>& row) {
newDb = row[0].toLongLong() == 0;
}))) {
db.reset();
success = false;
return false;
2019-05-01 15:50:19 +08:00
}
success = true;
2019-05-01 15:50:19 +08:00
return newDb;
}
bool dbSchema0to1(RawDatabase& db)
2019-05-01 15:50:19 +08:00
{
QVector<RawDatabase::Query> queries;
2019-05-01 15:50:19 +08:00
queries +=
RawDatabase::Query(QStringLiteral(
"CREATE TABLE file_transfers "
"(id INTEGER PRIMARY KEY, "
"chat_id INTEGER NOT NULL, "
"file_restart_id BLOB NOT NULL, "
"file_name BLOB NOT NULL, "
"file_path BLOB NOT NULL, "
"file_hash BLOB NOT NULL, "
"file_size INTEGER NOT NULL, "
"direction INTEGER NOT NULL, "
"file_state INTEGER NOT NULL);"));
queries +=
RawDatabase::Query(QStringLiteral("ALTER TABLE history ADD file_id INTEGER;"));
queries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 1;"));
return db.execNow(queries);
2019-05-01 15:50:19 +08:00
}
bool dbSchema1to2(RawDatabase& db)
{
// Any faux_offline_pending message, in a chat that has newer delivered
// message is decided to be broken. It must be moved from
// faux_offline_pending to broken_messages
// the last non-pending message in each chat
QString lastDeliveredQuery = QString(
"SELECT chat_id, MAX(history.id) FROM "
"history JOIN peers chat ON chat_id = chat.id "
"LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
"WHERE faux_offline_pending.id IS NULL "
"GROUP BY chat_id;");
QVector<RawDatabase::Query> upgradeQueries;
upgradeQueries +=
RawDatabase::Query(QStringLiteral(
"CREATE TABLE broken_messages "
"(id INTEGER PRIMARY KEY);"));
auto rowCallback = [&upgradeQueries](const QVector<QVariant>& row) {
auto chatId = row[0].toLongLong();
auto lastDeliveredHistoryId = row[1].toLongLong();
upgradeQueries += QString("INSERT INTO broken_messages "
"SELECT faux_offline_pending.id FROM "
"history JOIN faux_offline_pending "
"ON faux_offline_pending.id = history.id "
"WHERE history.chat_id=%1 "
"AND history.id < %2;").arg(chatId).arg(lastDeliveredHistoryId);
};
// note this doesn't modify the db, just generate new queries, so is safe
// to run outside of our upgrade transaction
if (!db.execNow({lastDeliveredQuery, rowCallback})) {
return false;
}
upgradeQueries += QString(
"DELETE FROM faux_offline_pending "
"WHERE id in ("
"SELECT id FROM broken_messages);");
upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 2;"));
return db.execNow(upgradeQueries);
}
2019-05-01 15:50:19 +08:00
/**
* @brief Upgrade the db schema
* @note On future alterations of the database all you have to do is bump the SCHEMA_VERSION
* variable and add another case to the switch statement below. Make sure to fall through on each case.
*/
void dbSchemaUpgrade(std::shared_ptr<RawDatabase>& db)
2019-05-01 15:50:19 +08:00
{
int64_t databaseSchemaVersion;
if (!db->execNow(RawDatabase::Query("PRAGMA user_version", [&](const QVector<QVariant>& row) {
databaseSchemaVersion = row[0].toLongLong();
}))) {
qCritical() << "History failed to read user_version";
db.reset();
return;
}
if (databaseSchemaVersion > SCHEMA_VERSION) {
qWarning() << "Database version is newer than we currently support. Please upgrade qTox";
// We don't know what future versions have done, we have to disable db access until we re-upgrade
db.reset();
return;
} else if (databaseSchemaVersion == SCHEMA_VERSION) {
// No work to do
return;
}
switch (databaseSchemaVersion) {
case 0: {
2019-05-01 15:50:19 +08:00
// Note: 0 is a special version that is actually two versions.
// possibility 1) it is a newly created database and it neesds the current schema to be created.
// possibility 2) it is a old existing database, before version 1 and before we saved schema version,
// and needs to be updated.
bool success = false;
const bool newDb = isNewDb(db, success);
if (!success) {
qCritical() << "Failed to create current db schema";
db.reset();
return;
}
if (newDb) {
if (!createCurrentSchema(*db)) {
qCritical() << "Failed to create current db schema";
db.reset();
return;
}
2019-05-01 15:50:19 +08:00
qDebug() << "Database created at schema version" << SCHEMA_VERSION;
break; // new db is the only case where we don't incrementally upgrade through each version
} else {
if (!dbSchema0to1(*db)) {
qCritical() << "Failed to upgrade db to schema version 1, aborting";
db.reset();
return;
}
qDebug() << "Database upgraded incrementally to schema version 1";
2019-05-01 15:50:19 +08:00
}
}
2019-05-01 15:50:19 +08:00
// fallthrough
case 1:
if (!dbSchema1to2(*db)) {
qCritical() << "Failed to upgrade db to schema version 2, aborting";
db.reset();
return;
}
qDebug() << "Database upgraded incrementally to schema version 2";
//fallthrough
2019-05-01 15:50:19 +08:00
// etc.
default:
qInfo() << "Database upgrade finished (databaseSchemaVersion" << databaseSchemaVersion
2019-05-01 15:50:19 +08:00
<< "->" << SCHEMA_VERSION << ")";
}
}
} // namespace
/**
2016-08-01 16:21:23 +08:00
* @class History
* @brief Interacts with the profile database to save the chat history.
*
* @var QHash<QString, int64_t> History::peers
* @brief Maps friend public keys to unique IDs by index.
* Caches mappings to speed up message saving.
*/
static constexpr int NUM_MESSAGES_DEFAULT =
100; // arbitrary number of messages loaded when not loading by date
FileDbInsertionData::FileDbInsertionData()
{
static int id = qRegisterMetaType<FileDbInsertionData>();
(void)id;
}
/**
* @brief Prepares the database to work with the history.
* @param db This database will be prepared for use with the history.
2016-08-01 16:21:23 +08:00
*/
History::History(std::shared_ptr<RawDatabase> db)
: db(db)
{
if (!isValid()) {
qWarning() << "Database not open, init failed";
return;
}
2019-05-01 15:50:19 +08:00
dbSchemaUpgrade(db);
// dbSchemaUpgrade may have put us in an invalid state
if (!isValid()) {
return;
}
connect(this, &History::fileInsertionReady, this, &History::onFileInsertionReady);
connect(this, &History::fileInserted, this, &History::onFileInserted);
// 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();
}});
}
History::~History()
{
if (!isValid()) {
return;
}
// We could have execLater requests pending with a lambda attached,
// so clear the pending transactions first
db->sync();
}
/**
2016-08-01 16:21:23 +08:00
* @brief Checks if the database was opened successfully
* @return True if database if opened, false otherwise.
*/
bool History::isValid()
{
return db && db->isOpen();
2015-12-19 23:51:15 +08:00
}
/**
* @brief Checks if a friend has chat history
* @param friendPk
* @return True if has, false otherwise.
*/
refactor(chatform): Remove message handling logic from gui path Functional changes * Offline messages are still sent when the chat log is cleared * Spinner now does not wait for history to be complete, just a receipt from our friend * Export chat and load chat history are now available in group chats * Merged save chat log and export chat log * Note that we lost the info messages in the process NonFunctional Changes * FileTransferWidget slots only called for correct file * Settings::getEnableGroupChatsColor now embedded in GenericChatForm::colorizeNames * Settings::setEnableGroupChatscolor now emits signal connected to GenericChatForm::setColorizedNames to keep state in sync * Chatlog history not reloaded on setPassword() * I am pretty sure this had no purpose * Removed a lot of responsibility from ChatForm * History moved to ChatHistory implementation of IChatLog * OfflineMsgEngine moved to FriendMessageDispatcher * Export chat and load chat history moved to GenericChatLog * Backed by IChatLog so can be used generically * Message processing moved to FriendMessageDispatcher * The action of sending files to coreFile is still handled by ChatForm, but displaying of the sent messages is done through IChatLog -> GenericChatForm * Search moved to ChatHistory/SessionChatLog * All insertion of chat log elements should be handled by GenericChatForm now * Removed overlapping responsibilities from GroupChatForm * Search and message sending goes through ichatlog/messagedispatcher too * Lots of search functionality pushed down into IChatLog * Some of the file logic was moved into Widget. This is mostly to avoid scope increase of this PR even further. * History APIs removed that were no longer used
2019-05-26 08:11:44 +08:00
bool History::historyExists(const ToxPk& friendPk)
{
refactor(chatform): Remove message handling logic from gui path Functional changes * Offline messages are still sent when the chat log is cleared * Spinner now does not wait for history to be complete, just a receipt from our friend * Export chat and load chat history are now available in group chats * Merged save chat log and export chat log * Note that we lost the info messages in the process NonFunctional Changes * FileTransferWidget slots only called for correct file * Settings::getEnableGroupChatsColor now embedded in GenericChatForm::colorizeNames * Settings::setEnableGroupChatscolor now emits signal connected to GenericChatForm::setColorizedNames to keep state in sync * Chatlog history not reloaded on setPassword() * I am pretty sure this had no purpose * Removed a lot of responsibility from ChatForm * History moved to ChatHistory implementation of IChatLog * OfflineMsgEngine moved to FriendMessageDispatcher * Export chat and load chat history moved to GenericChatLog * Backed by IChatLog so can be used generically * Message processing moved to FriendMessageDispatcher * The action of sending files to coreFile is still handled by ChatForm, but displaying of the sent messages is done through IChatLog -> GenericChatForm * Search moved to ChatHistory/SessionChatLog * All insertion of chat log elements should be handled by GenericChatForm now * Removed overlapping responsibilities from GroupChatForm * Search and message sending goes through ichatlog/messagedispatcher too * Lots of search functionality pushed down into IChatLog * Some of the file logic was moved into Widget. This is mostly to avoid scope increase of this PR even further. * History APIs removed that were no longer used
2019-05-26 08:11:44 +08:00
return !getMessagesForFriend(friendPk, 0, 1).empty();
}
/**
2016-08-01 16:21:23 +08:00
* @brief Erases all the chat history from the database.
*/
void History::eraseHistory()
{
if (!isValid()) {
return;
}
db->execNow("DELETE FROM faux_offline_pending;"
"DELETE FROM history;"
"DELETE FROM aliases;"
"DELETE FROM peers;"
"DELETE FROM file_transfers;"
"DELETE FROM broken_messages;"
"VACUUM;");
}
/**
2016-08-01 16:21:23 +08:00
* @brief Erases the chat history with one friend.
* @param friendPk Friend public key to erase.
*/
void History::removeFriendHistory(const QString& friendPk)
{
if (!isValid()) {
return;
}
if (!peers.contains(friendPk)) {
return;
}
int64_t id = peers[friendPk];
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 "
" WHERE chat_id=%1 "
"); "
"DELETE FROM broken_messages "
"WHERE broken_messages.id IN ( "
" SELECT broken_messages.id FROM broken_messages "
" LEFT JOIN history ON broken_messages.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; "
"DELETE FROM file_transfers WHERE chat_id=%1;"
"VACUUM;")
.arg(id);
if (db->execNow(queryText)) {
peers.remove(friendPk);
} else {
qWarning() << "Failed to remove friend's history";
}
}
/**
2016-08-01 16:21:23 +08:00
* @brief Generate query to insert new message in database
* @param friendPk Friend publick key to save.
* @param message Message to save.
* @param sender Sender to save.
* @param time Time of message sending.
* @param isDelivered True if message was already delivered.
2016-08-01 16:21:23 +08:00
* @param dispName Name, which should be displayed.
* @param insertIdCallback Function, called after query execution.
*/
QVector<RawDatabase::Query>
History::generateNewMessageQueries(const QString& friendPk, const QString& message,
const QString& sender, const QDateTime& time, bool isDelivered,
QString dispName, std::function<void(RowId)> 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 = *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));
}
// 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 = *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(
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 (!isDelivered) {
queries += RawDatabase::Query{"INSERT INTO faux_offline_pending (id) VALUES ("
" last_insert_rowid()"
");"};
}
return queries;
}
void History::onFileInsertionReady(FileDbInsertionData data)
{
QVector<RawDatabase::Query> queries;
std::weak_ptr<History> weakThis = shared_from_this();
// peerId is guaranteed to be inserted since we just used it in addNewMessage
auto peerId = peers[data.friendPk];
// Copy to pass into labmda for later
auto fileId = data.fileId;
queries +=
RawDatabase::Query(QStringLiteral(
"INSERT INTO file_transfers (chat_id, file_restart_id, "
"file_path, file_name, file_hash, file_size, direction, file_state) "
"VALUES (%1, ?, ?, ?, ?, %2, %3, %4);")
.arg(peerId)
.arg(data.size)
.arg(static_cast<int>(data.direction))
.arg(ToxFile::CANCELED),
{data.fileId.toUtf8(), data.filePath.toUtf8(), data.fileName.toUtf8(), QByteArray()},
[weakThis, fileId](RowId id) {
auto pThis = weakThis.lock();
if (pThis) {
emit pThis->fileInserted(id, fileId);
}
});
queries += RawDatabase::Query(QStringLiteral("UPDATE history "
"SET file_id = (last_insert_rowid()) "
"WHERE id = %1")
.arg(data.historyId.get()));
db->execLater(queries);
}
void History::onFileInserted(RowId dbId, QString fileId)
{
auto& fileInfo = fileInfos[fileId];
if (fileInfo.finished) {
db->execLater(
generateFileFinished(dbId, fileInfo.success, fileInfo.filePath, fileInfo.fileHash));
fileInfos.remove(fileId);
} else {
fileInfo.finished = false;
fileInfo.fileId = dbId;
}
}
RawDatabase::Query History::generateFileFinished(RowId id, bool success, const QString& filePath,
const QByteArray& fileHash)
{
auto file_state = success ? ToxFile::FINISHED : ToxFile::CANCELED;
if (filePath.length()) {
return RawDatabase::Query(QStringLiteral("UPDATE file_transfers "
"SET file_state = %1, file_path = ?, file_hash = ?"
"WHERE id = %2")
.arg(file_state)
.arg(id.get()),
{filePath.toUtf8(), fileHash});
} else {
return RawDatabase::Query(QStringLiteral("UPDATE file_transfers "
"SET finished = %1 "
"WHERE id = %2")
.arg(file_state)
.arg(id.get()));
}
}
void History::addNewFileMessage(const QString& friendPk, const QString& fileId,
const QString& fileName, const QString& filePath, int64_t size,
const QString& sender, const QDateTime& time, QString const& dispName)
{
// This is an incredibly far from an optimal way of implementing this,
// but given the frequency that people are going to be initiating a file
// transfer we can probably live with it.
// Since both inserting an alias for a user and inserting a file transfer
// will generate new ids, there is no good way to inject both new ids into the
// history query without refactoring our RawDatabase::Query and processor loops.
// What we will do instead is chain callbacks to try to get reasonable behavior.
// We can call the generateNewMessageQueries() fn to insert a message with an empty
// message in it, and get the id with the callbck. Once we have the id we can ammend
// the data to have our newly inserted file_id as well
ToxFile::FileDirection direction;
if (sender == friendPk) {
direction = ToxFile::RECEIVING;
} else {
direction = ToxFile::SENDING;
}
std::weak_ptr<History> weakThis = shared_from_this();
FileDbInsertionData insertionData;
insertionData.friendPk = friendPk;
insertionData.fileId = fileId;
insertionData.fileName = fileName;
insertionData.filePath = filePath;
insertionData.size = size;
insertionData.direction = direction;
auto insertFileTransferFn = [weakThis, insertionData](RowId messageId) {
auto insertionDataRw = std::move(insertionData);
insertionDataRw.historyId = messageId;
auto thisPtr = weakThis.lock();
if (thisPtr)
emit thisPtr->fileInsertionReady(std::move(insertionDataRw));
};
addNewMessage(friendPk, "", sender, time, true, dispName, insertFileTransferFn);
}
/**
2016-08-01 16:21:23 +08:00
* @brief Saves a chat message in the database.
* @param friendPk Friend publick key to save.
* @param message Message to save.
* @param sender Sender to save.
* @param time Time of message sending.
* @param isDelivered True if message was already delivered.
2016-08-01 16:21:23 +08:00
* @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 isDelivered, QString dispName,
const std::function<void(RowId)>& insertIdCallback)
{
if (!Settings::getInstance().getEnableLogging()) {
qWarning() << "Blocked a message from being added to database while history is disabled";
return;
}
if (!isValid()) {
return;
}
db->execLater(generateNewMessageQueries(friendPk, message, sender, time, isDelivered, dispName,
insertIdCallback));
}
void History::setFileFinished(const QString& fileId, bool success, const QString& filePath,
const QByteArray& fileHash)
{
auto& fileInfo = fileInfos[fileId];
if (fileInfo.fileId.get() == -1) {
fileInfo.finished = true;
fileInfo.success = success;
fileInfo.filePath = filePath;
fileInfo.fileHash = fileHash;
} else {
db->execLater(generateFileFinished(fileInfo.fileId, success, filePath, fileHash));
}
fileInfos.remove(fileId);
}
size_t History::getNumMessagesForFriend(const ToxPk& friendPk)
{
return getNumMessagesForFriendBeforeDate(friendPk, QDateTime());
}
size_t History::getNumMessagesForFriendBeforeDate(const ToxPk& friendPk, const QDateTime& date)
{
QString queryText = QString("SELECT COUNT(history.id) "
"FROM history "
"JOIN peers chat ON chat_id = chat.id "
"WHERE chat.public_key='%1'")
.arg(friendPk.toString());
if (date.isNull()) {
queryText += ";";
} else {
queryText += QString(" AND timestamp < %1;").arg(date.toMSecsSinceEpoch());
}
size_t numMessages = 0;
auto rowCallback = [&numMessages](const QVector<QVariant>& row) {
numMessages = row[0].toLongLong();
};
db->execNow({queryText, rowCallback});
return numMessages;
}
QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk, size_t firstIdx,
size_t lastIdx)
{
QList<HistMessage> messages;
// Don't forget to update the rowCallback if you change the selected columns!
QString queryText =
QString("SELECT history.id, faux_offline_pending.id, timestamp, "
"chat.public_key, aliases.display_name, sender.public_key, "
"message, file_transfers.file_restart_id, "
"file_transfers.file_path, file_transfers.file_name, "
"file_transfers.file_size, file_transfers.direction, "
"file_transfers.file_state, broken_messages.id FROM history "
"LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
"JOIN peers chat ON history.chat_id = chat.id "
"JOIN aliases ON sender_alias = aliases.id "
"JOIN peers sender ON aliases.owner = sender.id "
"LEFT JOIN file_transfers ON history.file_id = file_transfers.id "
"LEFT JOIN broken_messages ON history.id = broken_messages.id "
"WHERE chat.public_key='%1' "
"LIMIT %2 OFFSET %3;")
.arg(friendPk.toString())
.arg(lastIdx - firstIdx)
.arg(firstIdx);
auto rowCallback = [&messages](const QVector<QVariant>& row) {
// dispName and message could have null bytes, QString::fromUtf8
// truncates on null bytes so we strip them
auto id = RowId{row[0].toLongLong()};
auto isPending = !row[1].isNull();
auto timestamp = QDateTime::fromMSecsSinceEpoch(row[2].toLongLong());
auto friend_key = row[3].toString();
auto display_name = QString::fromUtf8(row[4].toByteArray().replace('\0', ""));
auto sender_key = row[5].toString();
auto isBroken = !row[13].isNull();
if (row[7].isNull()) {
messages += {id, isPending, isBroken, timestamp, friend_key,
display_name, sender_key, row[6].toString()};
} else {
ToxFile file;
file.fileKind = TOX_FILE_KIND_DATA;
file.resumeFileId = row[7].toString().toUtf8();
file.filePath = row[8].toString();
file.fileName = row[9].toString();
file.filesize = row[10].toLongLong();
file.direction = static_cast<ToxFile::FileDirection>(row[11].toLongLong());
file.status = static_cast<ToxFile::FileStatus>(row[12].toInt());
messages +=
{id, isPending, isBroken, timestamp, friend_key, display_name, sender_key, file};
}
};
db->execNow({queryText, rowCallback});
return messages;
}
QList<History::HistMessage> History::getUndeliveredMessagesForFriend(const ToxPk& friendPk)
{
auto queryText =
QString("SELECT history.id, faux_offline_pending.id, timestamp, chat.public_key, "
"aliases.display_name, sender.public_key, message, broken_messages.id "
"FROM history "
"JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
"JOIN peers chat on history.chat_id = chat.id "
"JOIN aliases on sender_alias = aliases.id "
"JOIN peers sender on aliases.owner = sender.id "
"LEFT JOIN broken_messages ON history.id = broken_messages.id "
"WHERE chat.public_key='%1';")
.arg(friendPk.toString());
QList<History::HistMessage> ret;
auto rowCallback = [&ret](const QVector<QVariant>& row) {
// dispName and message could have null bytes, QString::fromUtf8
// truncates on null bytes so we strip them
auto id = RowId{row[0].toLongLong()};
auto isPending = !row[1].isNull();
auto timestamp = QDateTime::fromMSecsSinceEpoch(row[2].toLongLong());
auto friend_key = row[3].toString();
auto display_name = QString::fromUtf8(row[4].toByteArray().replace('\0', ""));
auto sender_key = row[5].toString();
auto isBroken = !row[7].isNull();
ret += {id, isPending, isBroken, timestamp, friend_key,
display_name, sender_key, row[6].toString()};
};
db->execNow({queryText, rowCallback});
return ret;
}
/**
* @brief Search phrase in chat messages
* @param friendPk Friend public key
* @param from a date message where need to start a search
* @param phrase what need to find
* @param parameter for search
* @return date of the message where the phrase was found
*/
QDateTime History::getDateWhereFindPhrase(const QString& friendPk, const QDateTime& from,
QString phrase, const ParameterSearch& parameter)
2018-02-10 23:50:48 +08:00
{
QDateTime result;
auto rowCallback = [&result](const QVector<QVariant>& row) {
result = QDateTime::fromMSecsSinceEpoch(row[0].toLongLong());
2018-02-10 23:50:48 +08:00
};
phrase.replace("'", "''");
2018-02-14 17:30:38 +08:00
2018-06-25 02:11:20 +08:00
QString message;
switch (parameter.filter) {
case FilterSearch::Register:
message = QStringLiteral("message LIKE '%%1%'").arg(phrase);
2018-06-25 02:11:20 +08:00
break;
case FilterSearch::WordsOnly:
message = QStringLiteral("message REGEXP '%1'")
.arg(SearchExtraFunctions::generateFilterWordsOnly(phrase).toLower());
2018-06-25 02:11:20 +08:00
break;
case FilterSearch::RegisterAndWordsOnly:
message = QStringLiteral("REGEXPSENSITIVE(message, '%1')")
.arg(SearchExtraFunctions::generateFilterWordsOnly(phrase));
2018-06-25 02:11:20 +08:00
break;
case FilterSearch::Regular:
message = QStringLiteral("message REGEXP '%1'").arg(phrase);
2018-06-25 02:11:20 +08:00
break;
case FilterSearch::RegisterAndRegular:
message = QStringLiteral("REGEXPSENSITIVE(message '%1')").arg(phrase);
2018-06-25 02:11:20 +08:00
break;
default:
message = QStringLiteral("LOWER(message) LIKE '%%1%'").arg(phrase.toLower());
2018-06-25 02:11:20 +08:00
break;
}
2019-05-26 03:16:47 +08:00
QDateTime time = from;
2019-05-26 03:16:47 +08:00
if (!time.isValid()) {
time = QDateTime::currentDateTime();
}
2018-08-09 02:35:28 +08:00
if (parameter.period == PeriodSearch::AfterDate || parameter.period == PeriodSearch::BeforeDate) {
2019-05-26 03:16:47 +08:00
time = parameter.time;
2018-06-25 02:11:20 +08:00
}
QString period;
switch (parameter.period) {
case PeriodSearch::WithTheFirst:
period = QStringLiteral("ORDER BY timestamp ASC LIMIT 1;");
2018-06-25 02:11:20 +08:00
break;
case PeriodSearch::AfterDate:
period = QStringLiteral("AND timestamp > '%1' ORDER BY timestamp ASC LIMIT 1;")
2019-05-26 03:16:47 +08:00
.arg(time.toMSecsSinceEpoch());
2018-06-25 02:11:20 +08:00
break;
case PeriodSearch::BeforeDate:
period = QStringLiteral("AND timestamp < '%1' ORDER BY timestamp DESC LIMIT 1;")
2019-05-26 03:16:47 +08:00
.arg(time.toMSecsSinceEpoch());
2018-06-25 02:11:20 +08:00
break;
default:
period = QStringLiteral("AND timestamp < '%1' ORDER BY timestamp DESC LIMIT 1;")
2019-05-26 03:16:47 +08:00
.arg(time.toMSecsSinceEpoch());
2018-06-25 02:11:20 +08:00
break;
}
2018-02-10 23:50:48 +08:00
QString queryText =
QStringLiteral("SELECT timestamp "
"FROM history "
"LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
"JOIN peers chat ON chat_id = chat.id "
"WHERE chat.public_key='%1' "
"AND %2 "
"%3")
2018-02-12 07:02:28 +08:00
.arg(friendPk)
2018-06-25 02:11:20 +08:00
.arg(message)
.arg(period);
db->execNow({queryText, rowCallback});
return result;
2018-06-25 02:11:20 +08:00
}
2018-02-12 07:02:28 +08:00
/**
* @brief Gets date boundaries in conversation with friendPk. History doesn't model conversation indexes,
* but we can count messages between us and friendPk to effectively give us an index. This function
* returns how many messages have happened between us <-> friendPk each time the date changes
* @param[in] friendPk ToxPk of conversation to retrieve
* @param[in] from Start date to look from
* @param[in] maxNum Maximum number of date boundaries to retrieve
* @note This API may seem a little strange, why not use QDate from and QDate to? The intent is to
* have an API that can be used to get the first item after a date (for search) and to get a list
* of date changes (for loadHistory). We could write two separate queries but the query is fairly
* intricate compared to our other ones so reducing duplication of it is preferable.
*/
QList<History::DateIdx> History::getNumMessagesForFriendBeforeDateBoundaries(const ToxPk& friendPk,
const QDate& from,
size_t maxNum)
{
auto friendPkString = friendPk.toString();
// No guarantee that this is the most efficient way to do this...
// We want to count messages that happened for a friend before a
// certain date. We do this by re-joining our table a second time
// but this time with the only filter being that our id is less than
// the ID of the corresponding row in the table that is grouped by day
auto countMessagesForFriend =
QString("SELECT COUNT(*) - 1 " // Count - 1 corresponds to 0 indexed message id for friend
"FROM history countHistory " // Import unfiltered table as countHistory
"JOIN peers chat ON chat_id = chat.id " // link chat_id to chat.id
"WHERE chat.public_key = '%1'" // filter this conversation
"AND countHistory.id <= history.id") // and filter that our unfiltered table history id only has elements up to history.id
.arg(friendPkString);
auto limitString = (maxNum) ? QString("LIMIT %1").arg(maxNum) : QString("");
auto queryString = QString("SELECT (%1), (timestamp / 1000 / 60 / 60 / 24) AS day "
"FROM history "
"JOIN peers chat ON chat_id = chat.id "
"WHERE chat.public_key = '%2' "
"AND timestamp >= %3 "
"GROUP by day "
"%4;")
.arg(countMessagesForFriend)
.arg(friendPkString)
.arg(QDateTime(from).toMSecsSinceEpoch())
.arg(limitString);
QList<DateIdx> dateIdxs;
auto rowCallback = [&dateIdxs](const QVector<QVariant>& row) {
DateIdx dateIdx;
dateIdx.numMessagesIn = row[0].toLongLong();
dateIdx.date =
QDateTime::fromMSecsSinceEpoch(row[1].toLongLong() * 24 * 60 * 60 * 1000).date();
dateIdxs.append(dateIdx);
};
db->execNow({queryString, rowCallback});
return dateIdxs;
}
/**
* @brief Marks a message as delivered.
2016-08-01 16:21:23 +08:00
* Removing message from the faux-offline pending messages list.
*
* @param id Message ID.
*/
void History::markAsDelivered(RowId messageId)
{
if (!isValid()) {
return;
}
db->execLater(QString("DELETE FROM faux_offline_pending WHERE id=%1;").arg(messageId.get()));
}