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

Merge branch 'v1.17-dev'

This commit is contained in:
Anthony Bilinski 2019-10-21 23:49:36 -07:00
commit f7d82a425d
No known key found for this signature in database
GPG Key ID: 2AA8E0DA1B31FB3C
18 changed files with 533 additions and 160 deletions

View File

@ -290,6 +290,8 @@ set(${PROJECT_NAME}_SOURCES
src/chatlog/content/text.h src/chatlog/content/text.h
src/chatlog/content/timestamp.cpp src/chatlog/content/timestamp.cpp
src/chatlog/content/timestamp.h src/chatlog/content/timestamp.h
src/chatlog/content/broken.cpp
src/chatlog/content/broken.h
src/chatlog/customtextdocument.cpp src/chatlog/customtextdocument.cpp
src/chatlog/customtextdocument.h src/chatlog/customtextdocument.h
src/chatlog/documentcache.cpp src/chatlog/documentcache.cpp

View File

@ -26,6 +26,7 @@
#include "content/spinner.h" #include "content/spinner.h"
#include "content/text.h" #include "content/text.h"
#include "content/timestamp.h" #include "content/timestamp.h"
#include "content/broken.h"
#include "src/widget/style.h" #include "src/widget/style.h"
#include <QDebug> #include <QDebug>
@ -33,6 +34,7 @@
#include "src/persistence/settings.h" #include "src/persistence/settings.h"
#include "src/persistence/smileypack.h" #include "src/persistence/smileypack.h"
#include "src/persistence/history.h"
#define NAME_COL_WIDTH 90.0 #define NAME_COL_WIDTH 90.0
#define TIME_COL_WIDTH 90.0 #define TIME_COL_WIDTH 90.0
@ -43,7 +45,8 @@ ChatMessage::ChatMessage()
} }
ChatMessage::Ptr ChatMessage::createChatMessage(const QString& sender, const QString& rawMessage, ChatMessage::Ptr ChatMessage::createChatMessage(const QString& sender, const QString& rawMessage,
MessageType type, bool isMe, const QDateTime& date, bool colorizeName) MessageType type, bool isMe, MessageState state,
const QDateTime& date, bool colorizeName)
{ {
ChatMessage::Ptr msg = ChatMessage::Ptr(new ChatMessage); ChatMessage::Ptr msg = ChatMessage::Ptr(new ChatMessage);
@ -105,12 +108,21 @@ ChatMessage::Ptr ChatMessage::createChatMessage(const QString& sender, const QSt
? QString("%1 %2").arg(sender, rawMessage) ? QString("%1 %2").arg(sender, rawMessage)
: rawMessage), : rawMessage),
ColumnFormat(1.0, ColumnFormat::VariableSize)); ColumnFormat(1.0, ColumnFormat::VariableSize));
msg->addColumn(new Spinner(Style::getImagePath("chatArea/spinner.svg"), QSize(16, 16), 360.0 / 1.6),
ColumnFormat(TIME_COL_WIDTH, ColumnFormat::FixedSize, ColumnFormat::Right));
if (!date.isNull())
msg->markAsSent(date);
switch (state) {
case MessageState::complete:
msg->addColumn(new Timestamp(date, Settings::getInstance().getTimestampFormat(), baseFont),
ColumnFormat(TIME_COL_WIDTH, ColumnFormat::FixedSize, ColumnFormat::Right));
break;
case MessageState::pending:
msg->addColumn(new Spinner(Style::getImagePath("chatArea/spinner.svg"), QSize(16, 16), 360.0 / 1.6),
ColumnFormat(TIME_COL_WIDTH, ColumnFormat::FixedSize, ColumnFormat::Right));
break;
case MessageState::broken:
msg->addColumn(new Broken(Style::getImagePath("chatArea/error.svg"), QSize(16, 16)),
ColumnFormat(TIME_COL_WIDTH, ColumnFormat::FixedSize, ColumnFormat::Right));
break;
}
return msg; return msg;
} }
@ -207,7 +219,7 @@ ChatMessage::Ptr ChatMessage::createBusyNotification()
return msg; return msg;
} }
void ChatMessage::markAsSent(const QDateTime& time) void ChatMessage::markAsDelivered(const QDateTime& time)
{ {
QFont baseFont = Settings::getInstance().getChatMessageFont(); QFont baseFont = Settings::getInstance().getChatMessageFont();

View File

@ -22,6 +22,8 @@
#include "chatline.h" #include "chatline.h"
#include "src/core/toxfile.h" #include "src/core/toxfile.h"
#include "src/persistence/history.h"
#include <QDateTime> #include <QDateTime>
class QGraphicsScene; class QGraphicsScene;
@ -48,8 +50,8 @@ public:
ChatMessage(); ChatMessage();
static ChatMessage::Ptr createChatMessage(const QString& sender, const QString& rawMessage, static ChatMessage::Ptr createChatMessage(const QString& sender, const QString& rawMessage,
MessageType type, bool isMe, MessageType type, bool isMe, MessageState state,
const QDateTime& date = QDateTime(), bool colorizeName = false); const QDateTime& date, bool colorizeName = false);
static ChatMessage::Ptr createChatInfoMessage(const QString& rawMessage, SystemMessageType type, static ChatMessage::Ptr createChatInfoMessage(const QString& rawMessage, SystemMessageType type,
const QDateTime& date); const QDateTime& date);
static ChatMessage::Ptr createFileTransferMessage(const QString& sender, ToxFile file, static ChatMessage::Ptr createFileTransferMessage(const QString& sender, ToxFile file,
@ -57,7 +59,7 @@ public:
static ChatMessage::Ptr createTypingNotification(); static ChatMessage::Ptr createTypingNotification();
static ChatMessage::Ptr createBusyNotification(); static ChatMessage::Ptr createBusyNotification();
void markAsSent(const QDateTime& time); void markAsDelivered(const QDateTime& time);
QString toString() const; QString toString() const;
bool isAction() const; bool isAction() const;
void setAsAction(); void setAsAction();

View File

@ -0,0 +1,61 @@
/*
Copyright © 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 "broken.h"
#include "src/chatlog/pixmapcache.h"
#include <QPainter>
class QStyleOptionGraphicsItem;
Broken::Broken(const QString& img, QSize size)
: pmap{PixmapCache::getInstance().get(img, size)}
, size{size}
{
}
QRectF Broken::boundingRect() const
{
return QRectF(QPointF(-size.width() / 2.0, -size.height() / 2.0), size);
}
void Broken::paint(QPainter* painter, const QStyleOptionGraphicsItem* option,
QWidget* widget)
{
painter->setRenderHint(QPainter::SmoothPixmapTransform);
painter->drawPixmap(0, 0, pmap);
Q_UNUSED(option)
Q_UNUSED(widget)
}
void Broken::setWidth(qreal width)
{
Q_UNUSED(width);
}
void Broken::visibilityChanged(bool visible)
{
Q_UNUSED(visible);
}
qreal Broken::getAscent() const
{
return 0.0;
}

View File

@ -0,0 +1,45 @@
/*
Copyright © 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/>.
*/
#ifndef BROKEN_H
#define BROKEN_H
#include "../chatlinecontent.h"
#include <QObject>
#include <QPixmap>
class Broken : public ChatLineContent
{
Q_OBJECT
public:
Broken(const QString& img, QSize size);
QRectF boundingRect() const override;
void paint(QPainter* painter, const QStyleOptionGraphicsItem* option,
QWidget* widget) override;
void setWidth(qreal width) override;
void visibilityChanged(bool visible) override;
qreal getAscent() const override;
private:
QSize size;
QPixmap pmap;
};
#endif // BROKEN_H

View File

@ -391,8 +391,8 @@ int main(int argc, char* argv[])
} else { } else {
nexus.setParser(&parser); nexus.setParser(&parser);
int returnval = nexus.showLogin(profileName); int returnval = nexus.showLogin(profileName);
if (returnval != 0) { if (returnval == QDialog::Rejected) {
return returnval; return -1;
} }
} }

View File

@ -94,9 +94,10 @@ ChatHistory::ChatHistory(Friend& f_, History* history_, const ICoreIdHandler& co
, settings(settings_) , settings(settings_)
, coreIdHandler(coreIdHandler) , coreIdHandler(coreIdHandler)
{ {
connect(&messageDispatcher, &IMessageDispatcher::messageSent, this, &ChatHistory::onMessageSent);
connect(&messageDispatcher, &IMessageDispatcher::messageComplete, this, connect(&messageDispatcher, &IMessageDispatcher::messageComplete, this,
&ChatHistory::onMessageComplete); &ChatHistory::onMessageComplete);
connect(&messageDispatcher, &IMessageDispatcher::messageReceived, this,
&ChatHistory::onMessageReceived);
if (canUseHistory()) { if (canUseHistory()) {
// Defer messageSent callback until we finish firing off all our unsent messages. // Defer messageSent callback until we finish firing off all our unsent messages.
@ -105,8 +106,7 @@ ChatHistory::ChatHistory(Friend& f_, History* history_, const ICoreIdHandler& co
} }
// Now that we've fired off our unsent messages we can connect the message // Now that we've fired off our unsent messages we can connect the message
connect(&messageDispatcher, &IMessageDispatcher::messageReceived, this, connect(&messageDispatcher, &IMessageDispatcher::messageSent, this, &ChatHistory::onMessageSent);
&ChatHistory::onMessageReceived);
// NOTE: this has to be done _after_ sending all sent messages since initial // NOTE: this has to be done _after_ sending all sent messages since initial
// state of the message has to be marked according to our dispatch state // state of the message has to be marked according to our dispatch state
@ -385,15 +385,23 @@ void ChatHistory::loadHistoryIntoSessionChatLog(ChatLogIdx start) const
std::find_if(dispatchedMessageRowIdMap.begin(), dispatchedMessageRowIdMap.end(), std::find_if(dispatchedMessageRowIdMap.begin(), dispatchedMessageRowIdMap.end(),
[&](RowId dispatchedId) { return dispatchedId == message.id; }); [&](RowId dispatchedId) { return dispatchedId == message.id; });
bool isComplete = dispatchedMessageIt == dispatchedMessageRowIdMap.end(); assert((message.state != MessageState::pending && dispatchedMessageIt == dispatchedMessageRowIdMap.end()) ||
(message.state == MessageState::pending && dispatchedMessageIt != dispatchedMessageRowIdMap.end()));
if (isComplete) { auto chatLogMessage = ChatLogMessage{message.state, processedMessage};
auto chatLogMessage = ChatLogMessage{true, processedMessage}; switch (message.state) {
sessionChatLog.insertMessageAtIdx(currentIdx, sender, message.dispName, chatLogMessage); case MessageState::complete:
} else { sessionChatLog.insertCompleteMessageAtIdx(currentIdx, sender, message.dispName,
// If the message is incomplete we have to pretend we sent it to ensure chatLogMessage);
// sessionChatLog state is correct break;
sessionChatLog.onMessageSent(dispatchedMessageIt.key(), processedMessage); case MessageState::pending:
sessionChatLog.insertIncompleteMessageAtIdx(currentIdx, sender, message.dispName,
chatLogMessage, dispatchedMessageIt.key());
break;
case MessageState::broken:
sessionChatLog.insertBrokenMessageAtIdx(currentIdx, sender, message.dispName,
chatLogMessage);
break;
} }
break; break;
} }
@ -409,7 +417,7 @@ void ChatHistory::loadHistoryIntoSessionChatLog(ChatLogIdx start) const
*/ */
void ChatHistory::dispatchUnsentMessages(IMessageDispatcher& messageDispatcher) void ChatHistory::dispatchUnsentMessages(IMessageDispatcher& messageDispatcher)
{ {
auto unsentMessages = history->getUnsentMessagesForFriend(f.getPublicKey()); auto unsentMessages = history->getUndeliveredMessagesForFriend(f.getPublicKey());
for (auto& message : unsentMessages) { for (auto& message : unsentMessages) {
// We should only store messages as unsent, if this changes in the // We should only store messages as unsent, if this changes in the
// future we need to extend this logic // future we need to extend this logic
@ -443,7 +451,7 @@ void ChatHistory::handleDispatchedMessage(DispatchedMessageId dispatchId, RowId
if (completedMessageIt == completedMessages.end()) { if (completedMessageIt == completedMessages.end()) {
dispatchedMessageRowIdMap.insert(dispatchId, historyId); dispatchedMessageRowIdMap.insert(dispatchId, historyId);
} else { } else {
history->markAsSent(historyId); history->markAsDelivered(historyId);
completedMessages.erase(completedMessageIt); completedMessages.erase(completedMessageIt);
} }
} }
@ -455,7 +463,7 @@ void ChatHistory::completeMessage(DispatchedMessageId id)
if (dispatchedMessageIt == dispatchedMessageRowIdMap.end()) { if (dispatchedMessageIt == dispatchedMessageRowIdMap.end()) {
completedMessages.insert(id); completedMessages.insert(id);
} else { } else {
history->markAsSent(*dispatchedMessageIt); history->markAsDelivered(*dispatchedMessageIt);
dispatchedMessageRowIdMap.erase(dispatchedMessageIt); dispatchedMessageRowIdMap.erase(dispatchedMessageIt);
} }
} }

View File

@ -23,12 +23,13 @@
#include "src/core/toxfile.h" #include "src/core/toxfile.h"
#include "src/core/toxpk.h" #include "src/core/toxpk.h"
#include "src/model/message.h" #include "src/model/message.h"
#include "src/persistence/history.h"
#include <memory> #include <memory>
struct ChatLogMessage struct ChatLogMessage
{ {
bool isComplete; MessageState state;
Message message; Message message;
}; };

View File

@ -289,8 +289,8 @@ std::vector<IChatLog::DateChatLogIdxPair> SessionChatLog::getDateIdxs(const QDat
return ret; return ret;
} }
void SessionChatLog::insertMessageAtIdx(ChatLogIdx idx, ToxPk sender, QString senderName, void SessionChatLog::insertCompleteMessageAtIdx(ChatLogIdx idx, const ToxPk& sender, const QString& senderName,
ChatLogMessage message) const ChatLogMessage& message)
{ {
auto item = ChatLogItem(sender, message); auto item = ChatLogItem(sender, message);
@ -298,10 +298,42 @@ void SessionChatLog::insertMessageAtIdx(ChatLogIdx idx, ToxPk sender, QString se
item.setDisplayName(senderName); item.setDisplayName(senderName);
} }
assert(message.state == MessageState::complete);
items.emplace(idx, std::move(item)); items.emplace(idx, std::move(item));
} }
void SessionChatLog::insertFileAtIdx(ChatLogIdx idx, ToxPk sender, QString senderName, ChatLogFile file) void SessionChatLog::insertIncompleteMessageAtIdx(ChatLogIdx idx, const ToxPk& sender, const QString& senderName,
const ChatLogMessage& message,
DispatchedMessageId dispatchId)
{
auto item = ChatLogItem(sender, message);
if (!senderName.isEmpty()) {
item.setDisplayName(senderName);
}
assert(message.state == MessageState::pending);
items.emplace(idx, std::move(item));
outgoingMessages.insert(dispatchId, idx);
}
void SessionChatLog::insertBrokenMessageAtIdx(ChatLogIdx idx, const ToxPk& sender, const QString& senderName,
const ChatLogMessage& message)
{
auto item = ChatLogItem(sender, message);
if (!senderName.isEmpty()) {
item.setDisplayName(senderName);
}
assert(message.state == MessageState::broken);
items.emplace(idx, std::move(item));
}
void SessionChatLog::insertFileAtIdx(ChatLogIdx idx, const ToxPk& sender, const QString& senderName, const ChatLogFile& file)
{ {
auto item = ChatLogItem(sender, file); auto item = ChatLogItem(sender, file);
@ -321,7 +353,7 @@ void SessionChatLog::onMessageReceived(const ToxPk& sender, const Message& messa
auto messageIdx = nextIdx++; auto messageIdx = nextIdx++;
ChatLogMessage chatLogMessage; ChatLogMessage chatLogMessage;
chatLogMessage.isComplete = true; chatLogMessage.state = MessageState::complete;
chatLogMessage.message = message; chatLogMessage.message = message;
items.emplace(messageIdx, ChatLogItem(sender, chatLogMessage)); items.emplace(messageIdx, ChatLogItem(sender, chatLogMessage));
@ -337,7 +369,7 @@ void SessionChatLog::onMessageSent(DispatchedMessageId id, const Message& messag
auto messageIdx = nextIdx++; auto messageIdx = nextIdx++;
ChatLogMessage chatLogMessage; ChatLogMessage chatLogMessage;
chatLogMessage.isComplete = false; chatLogMessage.state = MessageState::pending;
chatLogMessage.message = message; chatLogMessage.message = message;
items.emplace(messageIdx, ChatLogItem(coreIdHandler.getSelfPublicKey(), chatLogMessage)); items.emplace(messageIdx, ChatLogItem(coreIdHandler.getSelfPublicKey(), chatLogMessage));
@ -367,7 +399,7 @@ void SessionChatLog::onMessageComplete(DispatchedMessageId id)
return; return;
} }
messageIt->second.getContentAsMessage().isComplete = true; messageIt->second.getContentAsMessage().state = MessageState::complete;
emit this->itemUpdated(messageIt->first); emit this->itemUpdated(messageIt->first);
} }

View File

@ -46,8 +46,13 @@ public:
ChatLogIdx getNextIdx() const override; ChatLogIdx getNextIdx() const override;
std::vector<DateChatLogIdxPair> getDateIdxs(const QDate& startDate, size_t maxDates) const override; std::vector<DateChatLogIdxPair> getDateIdxs(const QDate& startDate, size_t maxDates) const override;
void insertMessageAtIdx(ChatLogIdx idx, ToxPk sender, QString senderName, ChatLogMessage message); void insertCompleteMessageAtIdx(ChatLogIdx idx, const ToxPk& sender, const QString& senderName,
void insertFileAtIdx(ChatLogIdx idx, ToxPk sender, QString senderName, ChatLogFile file); const ChatLogMessage& message);
void insertIncompleteMessageAtIdx(ChatLogIdx idx, const ToxPk& sender, const QString& senderName,
const ChatLogMessage& message, DispatchedMessageId dispatchId);
void insertBrokenMessageAtIdx(ChatLogIdx idx, const ToxPk& sender, const QString& senderName,
const ChatLogMessage& message);
void insertFileAtIdx(ChatLogIdx idx, const ToxPk& sender, const QString& senderName, const ChatLogFile& file);
public slots: public slots:
void onMessageReceived(const ToxPk& sender, const Message& message); void onMessageReceived(const ToxPk& sender, const Message& message);

View File

@ -165,6 +165,11 @@ int Nexus::showLogin(const QString& profileName)
// The connection order ensures profile will be ready for bootstrap for now // The connection order ensures profile will be ready for bootstrap for now
connect(this, &Nexus::currentProfileChanged, this, &Nexus::bootstrapWithProfile); connect(this, &Nexus::currentProfileChanged, this, &Nexus::bootstrapWithProfile);
int returnval = loginScreen.exec(); int returnval = loginScreen.exec();
if (returnval == QDialog::Rejected) {
// Kriby: This will terminate the main application loop, necessary until we refactor
// away the split startup/return to login behavior.
qApp->quit();
}
disconnect(this, &Nexus::currentProfileChanged, this, &Nexus::bootstrapWithProfile); disconnect(this, &Nexus::currentProfileChanged, this, &Nexus::bootstrapWithProfile);
return returnval; return returnval;
} }

View File

@ -721,7 +721,8 @@ void RawDatabase::process()
&stmt, &compileTail)) &stmt, &compileTail))
!= SQLITE_OK) { != SQLITE_OK) {
qWarning() << "Failed to prepare statement" << anonymizeQuery(query.query) qWarning() << "Failed to prepare statement" << anonymizeQuery(query.query)
<< "with error" << r; << "and returned" << r;
qWarning("The full error is %d: %s", sqlite3_errcode(sqlite), sqlite3_errmsg(sqlite));
goto cleanupStatements; goto cleanupStatements;
} }
query.statements += stmt; query.statements += stmt;

View File

@ -26,10 +26,11 @@
#include "db/rawdatabase.h" #include "db/rawdatabase.h"
namespace { namespace {
static constexpr int SCHEMA_VERSION = 1; static constexpr int SCHEMA_VERSION = 2;
void generateCurrentSchema(QVector<RawDatabase::Query>& queries) bool createCurrentSchema(RawDatabase& db)
{ {
QVector<RawDatabase::Query> queries;
queries += RawDatabase::Query(QStringLiteral( queries += RawDatabase::Query(QStringLiteral(
"CREATE TABLE peers (id INTEGER PRIMARY KEY, " "CREATE TABLE peers (id INTEGER PRIMARY KEY, "
"public_key TEXT NOT NULL UNIQUE);" "public_key TEXT NOT NULL UNIQUE);"
@ -60,10 +61,13 @@ void generateCurrentSchema(QVector<RawDatabase::Query>& queries)
"file_size INTEGER NOT NULL, " "file_size INTEGER NOT NULL, "
"direction INTEGER NOT NULL, " "direction INTEGER NOT NULL, "
"file_state INTEGER NOT NULL);" "file_state INTEGER NOT NULL);"
"CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY);")); "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);
} }
bool isNewDb(std::shared_ptr<RawDatabase> db) bool isNewDb(std::shared_ptr<RawDatabase>& db, bool& success)
{ {
bool newDb; bool newDb;
if (!db->execNow(RawDatabase::Query("SELECT COUNT(*) FROM sqlite_master;", if (!db->execNow(RawDatabase::Query("SELECT COUNT(*) FROM sqlite_master;",
@ -71,13 +75,16 @@ bool isNewDb(std::shared_ptr<RawDatabase> db)
newDb = row[0].toLongLong() == 0; newDb = row[0].toLongLong() == 0;
}))) { }))) {
db.reset(); db.reset();
return false; // TODO: propogate error success = false;
return false;
} }
success = true;
return newDb; return newDb;
} }
void dbSchema0to1(std::shared_ptr<RawDatabase> db, QVector<RawDatabase::Query>& queries) bool dbSchema0to1(RawDatabase& db)
{ {
QVector<RawDatabase::Query> queries;
queries += queries +=
RawDatabase::Query(QStringLiteral( RawDatabase::Query(QStringLiteral(
"CREATE TABLE file_transfers " "CREATE TABLE file_transfers "
@ -92,6 +99,55 @@ void dbSchema0to1(std::shared_ptr<RawDatabase> db, QVector<RawDatabase::Query>&
"file_state INTEGER NOT NULL);")); "file_state INTEGER NOT NULL);"));
queries += queries +=
RawDatabase::Query(QStringLiteral("ALTER TABLE history ADD file_id INTEGER;")); RawDatabase::Query(QStringLiteral("ALTER TABLE history ADD file_id INTEGER;"));
queries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 1;"));
return db.execNow(queries);
}
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);
} }
/** /**
@ -99,7 +155,7 @@ void dbSchema0to1(std::shared_ptr<RawDatabase> db, QVector<RawDatabase::Query>&
* @note On future alterations of the database all you have to do is bump the SCHEMA_VERSION * @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. * 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) void dbSchemaUpgrade(std::shared_ptr<RawDatabase>& db)
{ {
int64_t databaseSchemaVersion; int64_t databaseSchemaVersion;
@ -121,35 +177,65 @@ void dbSchemaUpgrade(std::shared_ptr<RawDatabase> db)
return; return;
} }
QVector<RawDatabase::Query> queries;
// Make sure to handle the un-created case as well in the following upgrade code
switch (databaseSchemaVersion) { switch (databaseSchemaVersion) {
case 0: case 0: {
// Note: 0 is a special version that is actually two versions. // 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 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, // possibility 2) it is a old existing database, before version 1 and before we saved schema version,
// and need to be updated. // and needs to be updated.
if (isNewDb(db)) { bool success = false;
generateCurrentSchema(queries); const bool newDb = isNewDb(db, success);
queries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = %1;").arg(SCHEMA_VERSION)); if (!success) {
db->execLater(queries); 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;
}
qDebug() << "Database created at schema version" << SCHEMA_VERSION; qDebug() << "Database created at schema version" << SCHEMA_VERSION;
break; // new db is the only case where we don't incrementally upgrade through each version break; // new db is the only case where we don't incrementally upgrade through each version
} else { } else {
dbSchema0to1(db, queries); if (!dbSchema0to1(*db)) {
qCritical() << "Failed to upgrade db to schema version 1, aborting";
db.reset();
return;
}
qDebug() << "Database upgraded incrementally to schema version 1";
} }
}
// fallthrough // fallthrough
// case 1: case 1:
// dbSchema1to2(queries); if (!dbSchema1to2(*db)) {
// //fallthrough qCritical() << "Failed to upgrade db to schema version 2, aborting";
db.reset();
return;
}
qDebug() << "Database upgraded incrementally to schema version 2";
//fallthrough
// etc. // etc.
default: default:
queries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = %1;").arg(SCHEMA_VERSION)); qInfo() << "Database upgrade finished (databaseSchemaVersion" << databaseSchemaVersion
db->execLater(queries);
qDebug() << "Database upgrade finished (databaseSchemaVersion" << databaseSchemaVersion
<< "->" << SCHEMA_VERSION << ")"; << "->" << SCHEMA_VERSION << ")";
} }
} }
MessageState getMessageState(bool isPending, bool isBroken)
{
assert(!(isPending && isBroken));
MessageState messageState;
if (isPending) {
messageState = MessageState::pending;
} else if (isBroken) {
messageState = MessageState::broken;
} else {
messageState = MessageState::complete;
}
return messageState;
}
} // namespace } // namespace
/** /**
@ -243,6 +329,7 @@ void History::eraseHistory()
"DELETE FROM aliases;" "DELETE FROM aliases;"
"DELETE FROM peers;" "DELETE FROM peers;"
"DELETE FROM file_transfers;" "DELETE FROM file_transfers;"
"DELETE FROM broken_messages;"
"VACUUM;"); "VACUUM;");
} }
@ -268,6 +355,12 @@ void History::removeFriendHistory(const QString& friendPk)
" LEFT JOIN history ON faux_offline_pending.id = history.id " " LEFT JOIN history ON faux_offline_pending.id = history.id "
" WHERE chat_id=%1 " " 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 history WHERE chat_id=%1; "
"DELETE FROM aliases WHERE owner=%1; " "DELETE FROM aliases WHERE owner=%1; "
"DELETE FROM peers WHERE id=%1; " "DELETE FROM peers WHERE id=%1; "
@ -288,13 +381,13 @@ void History::removeFriendHistory(const QString& friendPk)
* @param message Message to save. * @param message Message to save.
* @param sender Sender to save. * @param sender Sender to save.
* @param time Time of message sending. * @param time Time of message sending.
* @param isSent True if message was already sent. * @param isDelivered True if message was already delivered.
* @param dispName Name, which should be displayed. * @param dispName Name, which should be displayed.
* @param insertIdCallback Function, called after query execution. * @param insertIdCallback Function, called after query execution.
*/ */
QVector<RawDatabase::Query> QVector<RawDatabase::Query>
History::generateNewMessageQueries(const QString& friendPk, const QString& message, History::generateNewMessageQueries(const QString& friendPk, const QString& message,
const QString& sender, const QDateTime& time, bool isSent, const QString& sender, const QDateTime& time, bool isDelivered,
QString dispName, std::function<void(RowId)> insertIdCallback) QString dispName, std::function<void(RowId)> insertIdCallback)
{ {
QVector<RawDatabase::Query> queries; QVector<RawDatabase::Query> queries;
@ -355,7 +448,7 @@ History::generateNewMessageQueries(const QString& friendPk, const QString& messa
.arg(senderId), .arg(senderId),
{message.toUtf8(), dispName.toUtf8()}, insertIdCallback); {message.toUtf8(), dispName.toUtf8()}, insertIdCallback);
if (!isSent) { if (!isDelivered) {
queries += RawDatabase::Query{"INSERT INTO faux_offline_pending (id) VALUES (" queries += RawDatabase::Query{"INSERT INTO faux_offline_pending (id) VALUES ("
" last_insert_rowid()" " last_insert_rowid()"
");"}; ");"};
@ -485,12 +578,12 @@ void History::addNewFileMessage(const QString& friendPk, const QString& fileId,
* @param message Message to save. * @param message Message to save.
* @param sender Sender to save. * @param sender Sender to save.
* @param time Time of message sending. * @param time Time of message sending.
* @param isSent True if message was already sent. * @param isDelivered True if message was already delivered.
* @param dispName Name, which should be displayed. * @param dispName Name, which should be displayed.
* @param insertIdCallback Function, called after query execution. * @param insertIdCallback Function, called after query execution.
*/ */
void History::addNewMessage(const QString& friendPk, const QString& message, const QString& sender, void History::addNewMessage(const QString& friendPk, const QString& message, const QString& sender,
const QDateTime& time, bool isSent, QString dispName, const QDateTime& time, bool isDelivered, QString dispName,
const std::function<void(RowId)>& insertIdCallback) const std::function<void(RowId)>& insertIdCallback)
{ {
if (!Settings::getInstance().getEnableLogging()) { if (!Settings::getInstance().getEnableLogging()) {
@ -501,7 +594,7 @@ void History::addNewMessage(const QString& friendPk, const QString& message, con
return; return;
} }
db->execLater(generateNewMessageQueries(friendPk, message, sender, time, isSent, dispName, db->execLater(generateNewMessageQueries(friendPk, message, sender, time, isDelivered, dispName,
insertIdCallback)); insertIdCallback));
} }
@ -562,12 +655,13 @@ QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk,
"message, file_transfers.file_restart_id, " "message, file_transfers.file_restart_id, "
"file_transfers.file_path, file_transfers.file_name, " "file_transfers.file_path, file_transfers.file_name, "
"file_transfers.file_size, file_transfers.direction, " "file_transfers.file_size, file_transfers.direction, "
"file_transfers.file_state FROM history " "file_transfers.file_state, broken_messages.id FROM history "
"LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id " "LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
"JOIN peers chat ON history.chat_id = chat.id " "JOIN peers chat ON history.chat_id = chat.id "
"JOIN aliases ON sender_alias = aliases.id " "JOIN aliases ON sender_alias = aliases.id "
"JOIN peers sender ON aliases.owner = sender.id " "JOIN peers sender ON aliases.owner = sender.id "
"LEFT JOIN file_transfers ON history.file_id = file_transfers.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' " "WHERE chat.public_key='%1' "
"LIMIT %2 OFFSET %3;") "LIMIT %2 OFFSET %3;")
.arg(friendPk.toString()) .arg(friendPk.toString())
@ -578,14 +672,18 @@ QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk,
// dispName and message could have null bytes, QString::fromUtf8 // dispName and message could have null bytes, QString::fromUtf8
// truncates on null bytes so we strip them // truncates on null bytes so we strip them
auto id = RowId{row[0].toLongLong()}; auto id = RowId{row[0].toLongLong()};
auto isOfflineMessage = row[1].isNull(); auto isPending = !row[1].isNull();
auto timestamp = QDateTime::fromMSecsSinceEpoch(row[2].toLongLong()); auto timestamp = QDateTime::fromMSecsSinceEpoch(row[2].toLongLong());
auto friend_key = row[3].toString(); auto friend_key = row[3].toString();
auto display_name = QString::fromUtf8(row[4].toByteArray().replace('\0', "")); auto display_name = QString::fromUtf8(row[4].toByteArray().replace('\0', ""));
auto sender_key = row[5].toString(); auto sender_key = row[5].toString();
auto isBroken = !row[13].isNull();
MessageState messageState = getMessageState(isPending, isBroken);
if (row[7].isNull()) { if (row[7].isNull()) {
messages += {id, isOfflineMessage, timestamp, friend_key, messages += {id, messageState, timestamp, friend_key,
display_name, sender_key, row[6].toString()}; display_name, sender_key, row[6].toString()};
} else { } else {
ToxFile file; ToxFile file;
file.fileKind = TOX_FILE_KIND_DATA; file.fileKind = TOX_FILE_KIND_DATA;
@ -596,7 +694,7 @@ QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk,
file.direction = static_cast<ToxFile::FileDirection>(row[11].toLongLong()); file.direction = static_cast<ToxFile::FileDirection>(row[11].toLongLong());
file.status = static_cast<ToxFile::FileStatus>(row[12].toInt()); file.status = static_cast<ToxFile::FileStatus>(row[12].toInt());
messages += messages +=
{id, isOfflineMessage, timestamp, friend_key, display_name, sender_key, file}; {id, messageState, timestamp, friend_key, display_name, sender_key, file};
} }
}; };
@ -605,16 +703,17 @@ QList<History::HistMessage> History::getMessagesForFriend(const ToxPk& friendPk,
return messages; return messages;
} }
QList<History::HistMessage> History::getUnsentMessagesForFriend(const ToxPk& friendPk) QList<History::HistMessage> History::getUndeliveredMessagesForFriend(const ToxPk& friendPk)
{ {
auto queryText = auto queryText =
QString("SELECT history.id, faux_offline_pending.id, timestamp, chat.public_key, " QString("SELECT history.id, faux_offline_pending.id, timestamp, chat.public_key, "
"aliases.display_name, sender.public_key, message " "aliases.display_name, sender.public_key, message, broken_messages.id "
"FROM history " "FROM history "
"JOIN faux_offline_pending ON history.id = faux_offline_pending.id " "JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
"JOIN peers chat on history.chat_id = chat.id " "JOIN peers chat on history.chat_id = chat.id "
"JOIN aliases on sender_alias = aliases.id " "JOIN aliases on sender_alias = aliases.id "
"JOIN peers sender on aliases.owner = sender.id " "JOIN peers sender on aliases.owner = sender.id "
"LEFT JOIN broken_messages ON history.id = broken_messages.id "
"WHERE chat.public_key='%1';") "WHERE chat.public_key='%1';")
.arg(friendPk.toString()); .arg(friendPk.toString());
@ -623,15 +722,17 @@ QList<History::HistMessage> History::getUnsentMessagesForFriend(const ToxPk& fri
// dispName and message could have null bytes, QString::fromUtf8 // dispName and message could have null bytes, QString::fromUtf8
// truncates on null bytes so we strip them // truncates on null bytes so we strip them
auto id = RowId{row[0].toLongLong()}; auto id = RowId{row[0].toLongLong()};
auto isOfflineMessage = row[1].isNull(); auto isPending = !row[1].isNull();
auto timestamp = QDateTime::fromMSecsSinceEpoch(row[2].toLongLong()); auto timestamp = QDateTime::fromMSecsSinceEpoch(row[2].toLongLong());
auto friend_key = row[3].toString(); auto friend_key = row[3].toString();
auto display_name = QString::fromUtf8(row[4].toByteArray().replace('\0', "")); auto display_name = QString::fromUtf8(row[4].toByteArray().replace('\0', ""));
auto sender_key = row[5].toString(); auto sender_key = row[5].toString();
if (row[6].isNull()) { auto isBroken = !row[7].isNull();
ret += {id, isOfflineMessage, timestamp, friend_key,
display_name, sender_key, row[6].toString()}; MessageState messageState = getMessageState(isPending, isBroken);
}
ret += {id, messageState, timestamp, friend_key,
display_name, sender_key, row[6].toString()};
}; };
db->execNow({queryText, rowCallback}); db->execNow({queryText, rowCallback});
@ -788,12 +889,12 @@ QList<History::DateIdx> History::getNumMessagesForFriendBeforeDateBoundaries(con
} }
/** /**
* @brief Marks a message as sent. * @brief Marks a message as delivered.
* Removing message from the faux-offline pending messages list. * Removing message from the faux-offline pending messages list.
* *
* @param id Message ID. * @param id Message ID.
*/ */
void History::markAsSent(RowId messageId) void History::markAsDelivered(RowId messageId)
{ {
if (!isValid()) { if (!isValid()) {
return; return;

View File

@ -105,31 +105,38 @@ struct FileDbInsertionData
}; };
Q_DECLARE_METATYPE(FileDbInsertionData); Q_DECLARE_METATYPE(FileDbInsertionData);
enum class MessageState
{
complete,
pending,
broken
};
class History : public QObject, public std::enable_shared_from_this<History> class History : public QObject, public std::enable_shared_from_this<History>
{ {
Q_OBJECT Q_OBJECT
public: public:
struct HistMessage struct HistMessage
{ {
HistMessage(RowId id, bool isSent, QDateTime timestamp, QString chat, QString dispName, HistMessage(RowId id, MessageState state, QDateTime timestamp, QString chat, QString dispName,
QString sender, QString message) QString sender, QString message)
: chat{chat} : chat{chat}
, sender{sender} , sender{sender}
, dispName{dispName} , dispName{dispName}
, timestamp{timestamp} , timestamp{timestamp}
, id{id} , id{id}
, isSent{isSent} , state{state}
, content(std::move(message)) , content(std::move(message))
{} {}
HistMessage(RowId id, bool isSent, QDateTime timestamp, QString chat, QString dispName, HistMessage(RowId id, MessageState state, QDateTime timestamp, QString chat, QString dispName,
QString sender, ToxFile file) QString sender, ToxFile file)
: chat{chat} : chat{chat}
, sender{sender} , sender{sender}
, dispName{dispName} , dispName{dispName}
, timestamp{timestamp} , timestamp{timestamp}
, id{id} , id{id}
, isSent{isSent} , state{state}
, content(std::move(file)) , content(std::move(file))
{} {}
@ -139,7 +146,7 @@ public:
QString dispName; QString dispName;
QDateTime timestamp; QDateTime timestamp;
RowId id; RowId id;
bool isSent; MessageState state;
HistMessageContent content; HistMessageContent content;
}; };
@ -160,7 +167,7 @@ public:
void eraseHistory(); void eraseHistory();
void removeFriendHistory(const QString& friendPk); void removeFriendHistory(const QString& friendPk);
void addNewMessage(const QString& friendPk, const QString& message, const QString& sender, void addNewMessage(const QString& friendPk, const QString& message, const QString& sender,
const QDateTime& time, bool isSent, QString dispName, const QDateTime& time, bool isDelivered, QString dispName,
const std::function<void(RowId)>& insertIdCallback = {}); const std::function<void(RowId)>& insertIdCallback = {});
void addNewFileMessage(const QString& friendPk, const QString& fileId, void addNewFileMessage(const QString& friendPk, const QString& fileId,
@ -171,18 +178,18 @@ public:
size_t getNumMessagesForFriend(const ToxPk& friendPk); size_t getNumMessagesForFriend(const ToxPk& friendPk);
size_t getNumMessagesForFriendBeforeDate(const ToxPk& friendPk, const QDateTime& date); size_t getNumMessagesForFriendBeforeDate(const ToxPk& friendPk, const QDateTime& date);
QList<HistMessage> getMessagesForFriend(const ToxPk& friendPk, size_t firstIdx, size_t lastIdx); QList<HistMessage> getMessagesForFriend(const ToxPk& friendPk, size_t firstIdx, size_t lastIdx);
QList<HistMessage> getUnsentMessagesForFriend(const ToxPk& friendPk); QList<HistMessage> getUndeliveredMessagesForFriend(const ToxPk& friendPk);
QDateTime getDateWhereFindPhrase(const QString& friendPk, const QDateTime& from, QString phrase, QDateTime getDateWhereFindPhrase(const QString& friendPk, const QDateTime& from, QString phrase,
const ParameterSearch& parameter); const ParameterSearch& parameter);
QList<DateIdx> getNumMessagesForFriendBeforeDateBoundaries(const ToxPk& friendPk, QList<DateIdx> getNumMessagesForFriendBeforeDateBoundaries(const ToxPk& friendPk,
const QDate& from, size_t maxNum); const QDate& from, size_t maxNum);
void markAsSent(RowId messageId); void markAsDelivered(RowId messageId);
protected: protected:
QVector<RawDatabase::Query> QVector<RawDatabase::Query>
generateNewMessageQueries(const QString& friendPk, const QString& message, generateNewMessageQueries(const QString& friendPk, const QString& message,
const QString& sender, const QDateTime& time, bool isSent, const QString& sender, const QDateTime& time, bool isDelivered,
QString dispName, std::function<void(RowId)> insertIdCallback = {}); QString dispName, std::function<void(RowId)> insertIdCallback = {});
signals: signals:

View File

@ -178,11 +178,9 @@ ChatMessage::Ptr createMessage(const QString& displayName, bool isSelf, bool col
messageType = ChatMessage::MessageType::ALERT; messageType = ChatMessage::MessageType::ALERT;
} }
// Spinner is displayed by passing in an empty date const auto timestamp = chatLogMessage.message.timestamp;
auto timestamp = chatLogMessage.isComplete ? chatLogMessage.message.timestamp : QDateTime();
return ChatMessage::createChatMessage(displayName, chatLogMessage.message.content, messageType, return ChatMessage::createChatMessage(displayName, chatLogMessage.message.content, messageType,
isSelf, timestamp, colorizeNames); isSelf, chatLogMessage.state, timestamp, colorizeNames);
} }
void renderMessage(const QString& displayName, bool isSelf, bool colorizeNames, void renderMessage(const QString& displayName, bool isSelf, bool colorizeNames,
@ -190,8 +188,8 @@ void renderMessage(const QString& displayName, bool isSelf, bool colorizeNames,
{ {
if (chatMessage) { if (chatMessage) {
if (chatLogMessage.isComplete) { if (chatLogMessage.state == MessageState::complete) {
chatMessage->markAsSent(chatLogMessage.message.timestamp); chatMessage->markAsDelivered(chatLogMessage.message.timestamp);
} }
} else { } else {
chatMessage = createMessage(displayName, isSelf, colorizeNames, chatLogMessage); chatMessage = createMessage(displayName, isSelf, colorizeNames, chatLogMessage);
@ -543,9 +541,8 @@ void GenericChatForm::onSendTriggered()
/** /**
* @brief Show, is it needed to hide message author name or not * @brief Show, is it needed to hide message author name or not
* @param messageAuthor Author of the sent message * @param idx ChatLogIdx of the message
* @oaran messageTime DateTime of the sent message * @return True if the name should be hidden, false otherwise
* @return True if it's needed to hide name, false otherwise
*/ */
bool GenericChatForm::needsToHideName(ChatLogIdx idx) const bool GenericChatForm::needsToHideName(ChatLogIdx idx) const
{ {

View File

@ -72,15 +72,6 @@ LoginScreen::~LoginScreen()
delete ui; delete ui;
} }
void LoginScreen::closeEvent(QCloseEvent* event)
{
// If we are in the bootstrap, returning -1 will give us something to exit with in main.cpp
this->setResult(-1);
// If we are in application exec, we can quit by closing it, instead.
qApp->quit();
}
/** /**
* @brief Resets the UI, clears all fields. * @brief Resets the UI, clears all fields.
*/ */
@ -110,10 +101,11 @@ void LoginScreen::reset(const QString& initialProfileName)
void LoginScreen::onProfileLoaded() void LoginScreen::onProfileLoaded()
{ {
done(0); done(QDialog::Accepted);
} }
void LoginScreen::onProfileLoadFailed() { void LoginScreen::onProfileLoadFailed()
{
QMessageBox::critical(this, tr("Couldn't load this profile"), tr("Wrong password.")); QMessageBox::critical(this, tr("Couldn't load this profile"), tr("Wrong password."));
ui->loginPassword->setFocus(); ui->loginPassword->setFocus();
ui->loginPassword->selectAll(); ui->loginPassword->selectAll();

View File

@ -21,9 +21,9 @@
#ifndef LOGINSCREEN_H #ifndef LOGINSCREEN_H
#define LOGINSCREEN_H #define LOGINSCREEN_H
#include <QDialog>
#include <QShortcut> #include <QShortcut>
#include <QToolButton> #include <QToolButton>
#include <QDialog>
class Profile; class Profile;
@ -47,9 +47,6 @@ signals:
void createNewProfile(QString name, const QString& pass); void createNewProfile(QString name, const QString& pass);
void loadProfile(QString name, const QString& pass); void loadProfile(QString name, const QString& pass);
protected:
void closeEvent(QCloseEvent* event) final;
public slots: public slots:
void onProfileLoaded(); void onProfileLoaded();
void onProfileLoadFailed(); void onProfileLoadFailed();

View File

@ -35,9 +35,11 @@ private slots:
void testCreation(); void testCreation();
void testIsNewDb(); void testIsNewDb();
void test0to1(); void test0to1();
void test1to2();
void cleanupTestCase(); void cleanupTestCase();
private: private:
bool initSucess{false}; bool initSucess{false};
void createSchemaAtVersion(std::shared_ptr<RawDatabase>, const QMap<QString, QString>& schema);
void verifyDb(std::shared_ptr<RawDatabase> db, const QMap<QString, QString>& expectedSql); void verifyDb(std::shared_ptr<RawDatabase> db, const QMap<QString, QString>& expectedSql);
}; };
@ -45,7 +47,34 @@ const QString testFileList[] = {
"testCreation.db", "testCreation.db",
"testIsNewDbTrue.db", "testIsNewDbTrue.db",
"testIsNewDbFalse.db", "testIsNewDbFalse.db",
"test0to1.db" "test0to1.db",
"test1to2.db"
};
const QMap<QString, QString> schema0 {
{"aliases", "CREATE TABLE aliases (id INTEGER PRIMARY KEY, owner INTEGER, display_name BLOB NOT NULL, UNIQUE(owner, display_name))"},
{"faux_offline_pending", "CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY)"},
{"history", "CREATE TABLE history (id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, chat_id INTEGER NOT NULL, sender_alias INTEGER NOT NULL, message BLOB NOT NULL)"},
{"peers", "CREATE TABLE peers (id INTEGER PRIMARY KEY, public_key TEXT NOT NULL UNIQUE)"}
};
// added file transfer history
const QMap<QString, QString> schema1 {
{"aliases", "CREATE TABLE aliases (id INTEGER PRIMARY KEY, owner INTEGER, display_name BLOB NOT NULL, UNIQUE(owner, display_name))"},
{"faux_offline_pending", "CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY)"},
{"file_transfers", "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)"},
{"history", "CREATE TABLE history (id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, chat_id INTEGER NOT NULL, sender_alias INTEGER NOT NULL, message BLOB NOT NULL, file_id INTEGER)"},
{"peers", "CREATE TABLE peers (id INTEGER PRIMARY KEY, public_key TEXT NOT NULL UNIQUE)"}
};
// move stuck faux offline messages do a table of "broken" messages
const QMap<QString, QString> schema2 {
{"aliases", "CREATE TABLE aliases (id INTEGER PRIMARY KEY, owner INTEGER, display_name BLOB NOT NULL, UNIQUE(owner, display_name))"},
{"faux_offline_pending", "CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY)"},
{"file_transfers", "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)"},
{"history", "CREATE TABLE history (id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, chat_id INTEGER NOT NULL, sender_alias INTEGER NOT NULL, message BLOB NOT NULL, file_id INTEGER)"},
{"peers", "CREATE TABLE peers (id INTEGER PRIMARY KEY, public_key TEXT NOT NULL UNIQUE)"},
{"broken_messages", "CREATE TABLE broken_messages (id INTEGER PRIMARY KEY)"}
}; };
void TestDbSchema::initTestCase() void TestDbSchema::initTestCase()
@ -69,77 +98,153 @@ void TestDbSchema::cleanupTestCase()
void TestDbSchema::verifyDb(std::shared_ptr<RawDatabase> db, const QMap<QString, QString>& expectedSql) void TestDbSchema::verifyDb(std::shared_ptr<RawDatabase> db, const QMap<QString, QString>& expectedSql)
{ {
QVERIFY(db->execNow(RawDatabase::Query(QStringLiteral("SELECT name, sql FROM sqlite_master " QVERIFY(db->execNow(RawDatabase::Query(QStringLiteral(
"WHERE type='table' " "SELECT name, sql FROM sqlite_master "
"ORDER BY name;"), "WHERE type='table';"),
[&](const QVector<QVariant>& row) { [&](const QVector<QVariant>& row) {
const QString tableName = row[0].toString(); const QString tableName = row[0].toString();
const QString tableSql = row[1].toString(); QString tableSql = row[1].toString();
QVERIFY(expectedSql.contains(tableName)); QVERIFY(expectedSql.contains(tableName));
QVERIFY(expectedSql.value(tableName) == tableSql); // table and column names can be quoted. UPDATE TEABLE automatically quotes the new names, but this
// has no functional impact on the schema. Strip quotes for comparison so that our created schema
// matches schema made from UPDATE TABLEs.
const QString unquotedTableSql = tableSql.remove("\"");
QVERIFY(expectedSql.value(tableName) == unquotedTableSql);
}))); })));
} }
void TestDbSchema::createSchemaAtVersion(std::shared_ptr<RawDatabase> db, const QMap<QString, QString>& schema)
{
QVector<RawDatabase::Query> queries;
for (auto const& tableCreation : schema.values()) {
queries += tableCreation;
}
QVERIFY(db->execNow(queries));
}
void TestDbSchema::testCreation() void TestDbSchema::testCreation()
{ {
QVector<RawDatabase::Query> queries; QVector<RawDatabase::Query> queries;
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"testCreation.db", {}, {}}}; auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"testCreation.db", {}, {}}};
generateCurrentSchema(queries); QVERIFY(createCurrentSchema(*db));
QVERIFY(db->execNow(queries)); verifyDb(db, schema2);
const QMap<QString, QString> expectedSql {
{"aliases", "CREATE TABLE aliases (id INTEGER PRIMARY KEY, owner INTEGER, display_name BLOB NOT NULL, UNIQUE(owner, display_name))"},
{"faux_offline_pending", "CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY)"},
{"file_transfers", "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)"},
{"history", "CREATE TABLE history (id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, chat_id INTEGER NOT NULL, sender_alias INTEGER NOT NULL, message BLOB NOT NULL, file_id INTEGER)"},
{"peers", "CREATE TABLE peers (id INTEGER PRIMARY KEY, public_key TEXT NOT NULL UNIQUE)"}
};
verifyDb(db, expectedSql);
} }
void TestDbSchema::testIsNewDb() void TestDbSchema::testIsNewDb()
{ {
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"testIsNewDbTrue.db", {}, {}}}; auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"testIsNewDbTrue.db", {}, {}}};
QVERIFY(isNewDb(db) == true); bool success = false;
bool newDb = isNewDb(db, success);
QVERIFY(success);
QVERIFY(newDb == true);
db = std::shared_ptr<RawDatabase>{new RawDatabase{"testIsNewDbFalse.db", {}, {}}}; db = std::shared_ptr<RawDatabase>{new RawDatabase{"testIsNewDbFalse.db", {}, {}}};
QVector<RawDatabase::Query> queries; createSchemaAtVersion(db, schema0);
generateCurrentSchema(queries); newDb = isNewDb(db, success);
QVERIFY(db->execNow(queries)); QVERIFY(success);
QVERIFY(isNewDb(db) == false); QVERIFY(newDb == false);
} }
void TestDbSchema::test0to1() void TestDbSchema::test0to1()
{ {
const QMap<QString, QString> expectedSql {
{"aliases", "CREATE TABLE aliases (id INTEGER PRIMARY KEY, owner INTEGER, display_name BLOB NOT NULL, UNIQUE(owner, display_name))"},
{"faux_offline_pending", "CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY)"},
{"file_transfers", "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)"},
{"history", "CREATE TABLE history (id INTEGER PRIMARY KEY, timestamp INTEGER NOT NULL, chat_id INTEGER NOT NULL, sender_alias INTEGER NOT NULL, message BLOB NOT NULL, file_id INTEGER)"},
{"peers", "CREATE TABLE peers (id INTEGER PRIMARY KEY, public_key TEXT NOT NULL UNIQUE)"}
};
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"test0to1.db", {}, {}}}; auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"test0to1.db", {}, {}}};
createSchemaAtVersion(db, schema0);
QVERIFY(dbSchema0to1(*db));
verifyDb(db, schema1);
}
void TestDbSchema::test1to2()
{
/*
Due to a long standing bug, faux offline message have been able to become stuck
going back years. Because of recent fixes to history loading, faux offline
messages will correctly all be sent on connection, but this causes an issue of
long stuck messages suddenly being delivered to a friend, out of context,
creating a confusing interaction. To work around this, this upgrade moves any
faux offline messages in a chat that are older than the last successfully
delivered message, indicating they were stuck, to a new table,
`broken_messages`, preventing them from ever being sent in the future.
https://github.com/qTox/qTox/issues/5776
*/
auto db = std::shared_ptr<RawDatabase>{new RawDatabase{"test1to2.db", {}, {}}};
createSchemaAtVersion(db, schema1);
const QString myPk = "AC18841E56CCDEE16E93E10E6AB2765BE54277D67F1372921B5B418A6B330D3D";
const QString friend1Pk = "FE34BC6D87B66E958C57BBF205F9B79B62BE0AB8A4EFC1F1BB9EC4D0D8FB0663";
const QString friend2Pk = "2A1CBCE227549459C0C20F199DB86AD9BCC436D35BAA1825FFD4B9CA3290D200";
QVector<RawDatabase::Query> queries; QVector<RawDatabase::Query> queries;
queries += RawDatabase::Query(QStringLiteral( queries += QString("INSERT INTO peers (id, public_key) VALUES (%1, '%2')").arg(0).arg(myPk);
"CREATE TABLE peers " queries += QString("INSERT INTO peers (id, public_key) VALUES (%1, '%2')").arg(1).arg(friend1Pk);
"(id INTEGER PRIMARY KEY, " queries += QString("INSERT INTO peers (id, public_key) VALUES (%1, '%2')").arg(2).arg(friend2Pk);
"public_key TEXT NOT NULL UNIQUE);"
"CREATE TABLE aliases " // friend 1
"(id INTEGER PRIMARY KEY, " // first message in chat is pending - but the second is delivered. This message is "broken"
"owner INTEGER, " queries += RawDatabase::Query{
"display_name BLOB NOT NULL, " "INSERT INTO history (id, timestamp, chat_id, message, sender_alias) VALUES (1, 1, 1, ?, 0)",
"UNIQUE(owner, display_name));" {"first message in chat, pending and stuck"}};
"CREATE TABLE history " queries += {"INSERT INTO faux_offline_pending (id) VALUES ("
"(id INTEGER PRIMARY KEY, " " last_insert_rowid()"
"timestamp INTEGER NOT NULL, " ");"};
"chat_id INTEGER NOT NULL, " // second message is delivered, causing the first to be considered broken
"sender_alias INTEGER NOT NULL, " queries += RawDatabase::Query{
"message BLOB NOT NULL);" "INSERT INTO history (id, timestamp, chat_id, message, sender_alias) VALUES (2, 2, 1, ?, 0)",
"CREATE TABLE faux_offline_pending " {"second message in chat, delivered"}};
"(id INTEGER PRIMARY KEY);"));
// third message is pending - this is a normal pending message. It should be untouched.
queries += RawDatabase::Query{
"INSERT INTO history (id, timestamp, chat_id, message, sender_alias) VALUES (3, 3, 1, ?, 0)",
{"third message in chat, pending"}};
queries += {"INSERT INTO faux_offline_pending (id) VALUES ("
" last_insert_rowid()"
");"};
// friend 2
// first message is delivered.
queries += RawDatabase::Query{
"INSERT INTO history (id, timestamp, chat_id, message, sender_alias) VALUES (4, 4, 2, ?, 2)",
{"first message by friend in chat, delivered"}};
// second message is also delivered.
queries += RawDatabase::Query{
"INSERT INTO history (id, timestamp, chat_id, message, sender_alias) VALUES (5, 5, 2, ?, 0)",
{"first message by us in chat, delivered"}};
// third message is pending, but not broken since there are no delivered messages after it.
queries += RawDatabase::Query{
"INSERT INTO history (id, timestamp, chat_id, message, sender_alias) VALUES (6, 6, 2, ?, 0)",
{"last message in chat, by us, pending"}};
queries += {"INSERT INTO faux_offline_pending (id) VALUES ("
" last_insert_rowid()"
");"};
QVERIFY(db->execNow(queries)); QVERIFY(db->execNow(queries));
queries.clear(); QVERIFY(dbSchema1to2(*db));
dbSchema0to1(db, queries); verifyDb(db, schema2);
QVERIFY(db->execNow(queries));
verifyDb(db, expectedSql); long brokenCount = -1;
RawDatabase::Query brokenCountQuery = {"SELECT COUNT(*) FROM broken_messages;", [&](const QVector<QVariant>& row) {
brokenCount = row[0].toLongLong();
}};
QVERIFY(db->execNow(brokenCountQuery));
QVERIFY(brokenCount == 1); // only friend 1's first message is "broken"
int fauxOfflineCount = -1;
RawDatabase::Query fauxOfflineCountQuery = {"SELECT COUNT(*) FROM faux_offline_pending;", [&](const QVector<QVariant>& row) {
fauxOfflineCount = row[0].toLongLong();
}};
QVERIFY(db->execNow(fauxOfflineCountQuery));
// both friend 1's third message and friend 2's third message should still be pending.
//The broken message should no longer be pending.
QVERIFY(fauxOfflineCount == 2);
int totalHisoryCount = -1;
RawDatabase::Query totalHistoryCountQuery = {"SELECT COUNT(*) FROM history;", [&](const QVector<QVariant>& row) {
totalHisoryCount = row[0].toLongLong();
}};
QVERIFY(db->execNow(totalHistoryCountQuery));
QVERIFY(totalHisoryCount == 6); // all messages should still be in history.
} }
QTEST_GUILESS_MAIN(TestDbSchema) QTEST_GUILESS_MAIN(TestDbSchema)