diff --git a/src/model/chathistory.cpp b/src/model/chathistory.cpp index c3e206427..884c1bf6e 100644 --- a/src/model/chathistory.cpp +++ b/src/model/chathistory.cpp @@ -361,9 +361,9 @@ void ChatHistory::loadHistoryIntoSessionChatLog(ChatLogIdx start) const // Note that message.id is _not_ a valid conversion here since it is a // global id not a per-chat id like the ChatLogIdx auto currentIdx = nextIdx++; - auto sender = ToxPk(message.sender); switch (message.content.getType()) { case HistMessageContentType::file: { + auto sender = ToxPk(message.sender); const auto date = message.timestamp; const auto file = message.content.asFile(); const auto chatLogFile = ChatLogFile{date, file}; @@ -371,6 +371,7 @@ void ChatHistory::loadHistoryIntoSessionChatLog(ChatLogIdx start) const break; } case HistMessageContentType::message: { + auto sender = ToxPk(message.sender); auto messageContent = message.content.asMessage(); auto isAction = handleActionPrefix(messageContent); diff --git a/src/persistence/history.cpp b/src/persistence/history.cpp index fe6c04600..325a3fc71 100644 --- a/src/persistence/history.cpp +++ b/src/persistence/history.cpp @@ -27,7 +27,7 @@ #include "src/core/toxpk.h" namespace { -static constexpr int SCHEMA_VERSION = 8; +static constexpr int SCHEMA_VERSION = 9; bool createCurrentSchema(RawDatabase& db) { @@ -408,6 +408,109 @@ bool dbSchema7to8(RawDatabase& db) return db.execNow(upgradeQueries); } +struct BadEntry { + BadEntry(int64_t row, QString toxId) : + row{row}, + toxId{toxId} {} + RowId row; + QString toxId; +}; + +std::vector getInvalidPeers(RawDatabase& db) +{ + std::vector badPeerIds; + db.execNow(RawDatabase::Query("SELECT id, public_key FROM peers WHERE LENGTH(public_key) != 64", [&](const QVector& row) { + badPeerIds.emplace_back(BadEntry{row[0].toInt(), row[1].toString()}); + })); + return badPeerIds; +} + +RowId getValidPeerRow(RawDatabase& db, const ToxPk& friendPk) +{ + bool validPeerExists{false}; + RowId validPeerRow; + db.execNow(RawDatabase::Query(QStringLiteral("SELECT id FROM peers WHERE public_key='%1';") + .arg(friendPk.toString()), [&](const QVector& row) { + validPeerRow = RowId{row[0].toLongLong()}; + validPeerExists = true; + })); + if (validPeerExists) { + return validPeerRow; + } + + db.execNow(RawDatabase::Query(("SELECT id FROM peers ORDER BY id DESC LIMIT 1;"), [&](const QVector& row) { + int64_t maxPeerId = row[0].toInt(); + validPeerRow = RowId{maxPeerId + 1}; + })); + db.execNow(RawDatabase::Query(QStringLiteral("INSERT INTO peers (id, public_key) VALUES (%1, '%2');").arg(validPeerRow.get()).arg(friendPk.toString()))); + return validPeerRow; +} + +struct DuplicateAlias { + DuplicateAlias(RowId goodAliasRow, std::vector badAliasRows) : + goodAliasRow{goodAliasRow}, + badAliasRows{badAliasRows} {} + DuplicateAlias() {}; + RowId goodAliasRow{-1}; + std::vector badAliasRows; +}; + +DuplicateAlias getDuplicateAliasRows(RawDatabase& db, RowId goodPeerRow, RowId badPeerRow) +{ + std::vector badAliasRows; + RowId goodAliasRow; + bool hasGoodEntry{false}; + db.execNow(RawDatabase::Query(QStringLiteral("SELECT good.id, bad.id FROM aliases good INNER JOIN aliases bad ON good.display_name=bad.display_name WHERE good.owner=%1 AND bad.owner=%2;").arg(goodPeerRow.get()).arg(badPeerRow.get()), + [&](const QVector& row) { + hasGoodEntry = true; + goodAliasRow = RowId{row[0].toInt()}; + badAliasRows.emplace_back(RowId{row[1].toLongLong()}); + })); + + if (hasGoodEntry) { + return {goodAliasRow, badAliasRows}; + } else { + return {}; + } +} + +void mergeAndDeleteAlias(QVector& upgradeQueries, RowId goodAlias, std::vector badAliases) +{ + for (const auto badAliasId : badAliases) { + upgradeQueries += RawDatabase::Query(QStringLiteral("UPDATE text_messages SET sender_alias = %1 WHERE sender_alias = %2;").arg(goodAlias.get()).arg(badAliasId.get())); + upgradeQueries += RawDatabase::Query(QStringLiteral("UPDATE file_transfers SET sender_alias = %1 WHERE sender_alias = %2;").arg(goodAlias.get()).arg(badAliasId.get())); + upgradeQueries += RawDatabase::Query(QStringLiteral("DELETE FROM aliases WHERE id = %1;").arg(badAliasId.get())); + } +} + +void mergeAndDeletePeer(QVector& upgradeQueries, RowId goodPeerId, RowId badPeerId) +{ + upgradeQueries += RawDatabase::Query(QStringLiteral("UPDATE aliases SET owner = %1 WHERE owner = %2").arg(goodPeerId.get()).arg(badPeerId.get())); + upgradeQueries += RawDatabase::Query(QStringLiteral("UPDATE history SET chat_id = %1 WHERE chat_id = %2;").arg(goodPeerId.get()).arg(badPeerId.get())); + upgradeQueries += RawDatabase::Query(QStringLiteral("DELETE FROM peers WHERE id = %1").arg(badPeerId.get())); +} + +void mergeDuplicatePeers(QVector& upgradeQueries, RawDatabase& db, std::vector badPeers) +{ + for (const auto& badPeer : badPeers) { + const RowId goodPeerId = getValidPeerRow(db, ToxPk{badPeer.toxId.left(64)}); + const auto aliasDuplicates = getDuplicateAliasRows(db, goodPeerId, badPeer.row); + mergeAndDeleteAlias(upgradeQueries, aliasDuplicates.goodAliasRow, aliasDuplicates.badAliasRows); + mergeAndDeletePeer(upgradeQueries, goodPeerId, badPeer.row); + } +} + +bool dbSchema8to9(RawDatabase& db) +{ + // not technically a schema update, but still a database version update based on healing invalid user data + // we added ourself in the peers table by ToxId isntead of ToxPk. Heal this over-length entry. + QVector upgradeQueries; + const auto badPeers = getInvalidPeers(db); + mergeDuplicatePeers(upgradeQueries, db, badPeers); + upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 9;")); + return db.execNow(upgradeQueries); +} + /** * @brief Upgrade the db schema * @note On future alterations of the database all you have to do is bump the SCHEMA_VERSION @@ -457,7 +560,7 @@ bool dbSchemaUpgrade(std::shared_ptr& db) using DbSchemaUpgradeFn = bool (*)(RawDatabase&); std::vector upgradeFns = {dbSchema0to1, dbSchema1to2, dbSchema2to3, dbSchema3to4, dbSchema4to5, dbSchema5to6, - dbSchema6to7, dbSchema7to8}; + dbSchema6to7, dbSchema7to8, dbSchema8to9}; assert(databaseSchemaVersion < static_cast(upgradeFns.size())); assert(upgradeFns.size() == SCHEMA_VERSION); diff --git a/test/persistence/dbschema_test.cpp b/test/persistence/dbschema_test.cpp index 10a41fc90..f7862ea4b 100644 --- a/test/persistence/dbschema_test.cpp +++ b/test/persistence/dbschema_test.cpp @@ -54,6 +54,7 @@ private slots: void test5to6(); void test6to7(); // test7to8 omitted, version only upgrade, versions are not verified in this + // test8to9 omitted, data corruption correction upgrade with no schema change // test suite void cleanupTestCase() const;