File indexing completed on 2024-05-05 17:32:28

0001 // SPDX-FileCopyrightText: 2020 Jonah BrĂ¼chert <jbb@kaidan.im>
0002 //
0003 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 
0005 #include "database.h"
0006 
0007 #include <QDebug>
0008 #include <QDir>
0009 #include <QSqlDatabase>
0010 #include <QSqlError>
0011 #include <QSqlQuery>
0012 #include <QStandardPaths>
0013 
0014 #include <QCoroFuture>
0015 #include <QCoroTask>
0016 #include <random>
0017 
0018 #include <global.h>
0019 #include <phonenumberlist.h>
0020 
0021 constexpr auto ID_LEN = 10;
0022 constexpr auto DATABASE_REVISION = 8; // Keep MIGRATE_TO_LATEST_FROM in sync
0023 #define MIGRATE_TO(n, current)                                                                                                                                 \
0024     if (current < n) {                                                                                                                                         \
0025         qDebug() << "Running migration" << #n;                                                                                                                 \
0026         migrationV##n(db, current);                                                                                                                            \
0027     }
0028 #define MIGRATE_TO_LATEST_FROM(current) MIGRATE_TO(8, current)
0029 
0030 template<typename T, typename Func>
0031 std::optional<std::invoke_result_t<Func, T>> map(const std::optional<T> &&in, Func &&f)
0032 {
0033     if (in.has_value()) {
0034         return f(std::move(*in));
0035     } else {
0036         return {};
0037     }
0038 }
0039 
0040 Database::Database(QObject *parent)
0041     : QObject(parent)
0042 {
0043     const QString databaseLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + SL("/spacebar");
0044     if (!QDir().mkpath(databaseLocation)) {
0045         qDebug() << "Could not create the database directory at" << databaseLocation;
0046     }
0047 
0048     DatabaseConfiguration config;
0049     config.setDatabaseName(databaseLocation + SL("/messages.sqlite"));
0050     config.setType(DatabaseType::SQLite);
0051     m_database = ThreadedDatabase::establishConnection(config);
0052 }
0053 
0054 QFuture<std::vector<Message>> Database::messagesForNumber(const PhoneNumberList &phoneNumberList, const QString &id, const int limit) const
0055 {
0056     QString sql = SL(R"(
0057         SELECT
0058             id,
0059             phoneNumber,
0060             text,
0061             time,
0062             read,
0063             delivered,
0064             sentByMe,
0065             attachments,
0066             smil,
0067             fromNumber,
0068             messageId,
0069             deliveryReport,
0070             readReport,
0071             pendingDownload,
0072             contentLocation,
0073             expires,
0074             size,
0075             tapbacks
0076         FROM Messages
0077     )");
0078 
0079     if (id.isEmpty()) {
0080         sql.append(SL("WHERE phoneNumber == ? ORDER BY time DESC"));
0081 
0082         if (limit == 0) {
0083             sql.append(SL(" LIMIT 30"));
0084         } else if (limit == 1) {
0085             sql.append(SL(" LIMIT 1"));
0086         } else {
0087             sql.append(SL(" LIMIT -1 OFFSET 30"));
0088         }
0089 
0090         return m_database->getResults<Message>(sql, phoneNumberList.toString());
0091     } else {
0092         sql.append(SL("WHERE id == ?"));
0093         return m_database->getResults<Message>(sql, id);
0094     }
0095 }
0096 
0097 QFuture<void> Database::updateMessageDeliveryState(const QString &id, const MessageState state)
0098 {
0099     return m_database->execute(SL("UPDATE Messages SET delivered = ? WHERE id == ?"), state, id);
0100 }
0101 
0102 QFuture<void> Database::updateMessageSent(const QString &id, const QString &messageId, const QString &contentLocation)
0103 {
0104     return m_database->execute(SL("UPDATE Messages SET messageId = ?, contentLocation = ? WHERE id == ?"), messageId, contentLocation, id);
0105 }
0106 
0107 QFuture<void> Database::updateMessageDeliveryReport(const QString &messageId)
0108 {
0109     return m_database->execute(SL("UPDATE Messages SET deliveryReport = IFNULL(deliveryReport, 0) + 1 WHERE messageId == ?"), messageId);
0110 }
0111 
0112 QFuture<void> Database::updateMessageReadReport(const QString &messageId, const PhoneNumber &fromNumber)
0113 {
0114     return m_database->execute(SL("UPDATE Messages SET readReport = IFNULL(readReport, '') || ? WHERE messageId == ?"),
0115                                fromNumber.toInternational(),
0116                                messageId);
0117 }
0118 
0119 QFuture<void> Database::markMessageRead(const int id)
0120 {
0121     return m_database->execute(SL("UPDATE Messages SET read = True WHERE id == ? AND NOT read = True"), id);
0122 }
0123 
0124 QFuture<void> Database::updateMessageTapbacks(const QString &id, const QString tapbacks)
0125 {
0126     return m_database->execute(SL("UPDATE Messages SET tapbacks = ? WHERE id == ?"), tapbacks, id);
0127 }
0128 
0129 QCoro::Task<std::optional<QString>> Database::lastMessageWithText(const PhoneNumberList &phoneNumberList, const QString &text)
0130 {
0131     auto id =
0132         co_await m_database->getResult<SingleValue<QString>>(SL("SELECT id FROM Messages WHERE phoneNumber == ? AND text == ? ORDER BY time DESC LIMIT 1"),
0133                                                              phoneNumberList.toString(),
0134                                                              text);
0135     co_return map(std::move(id), [](auto &&v) {
0136         return v.value;
0137     });
0138 }
0139 
0140 QCoro::Task<std::optional<QString>> Database::lastMessageWithAttachment(const PhoneNumberList &phoneNumberList)
0141 {
0142     auto id = co_await m_database->getResult<SingleValue<QString>>(
0143         SL("SELECT id FROM Messages WHERE phoneNumber == ? AND attachments IS NOT NULL ORDER BY time DESC LIMIT 1"),
0144         phoneNumberList.toString());
0145     co_return map(std::move(id), [](auto &&v) {
0146         return v.value;
0147     });
0148 }
0149 
0150 QCoro::Task<QVector<Chat>> Database::chats(const PhoneNumberList &phoneNumberList) const
0151 {
0152     QVector<Chat> chats;
0153 
0154     if (!phoneNumberList.empty()) {
0155         Chat chat;
0156         chat.phoneNumberList = phoneNumberList;
0157         chat.unreadMessages = (co_await unreadMessagesForNumber(chat.phoneNumberList)).value_or(0);
0158 
0159         std::vector<Message> messages = co_await messagesForNumber(chat.phoneNumberList, QString(), 1);
0160 
0161         if (!messages.empty()) {
0162             auto message = messages.front();
0163             chat.lastMessage = message.text;
0164             chat.lastDateTime = message.datetime;
0165             chat.lastSentByMe = message.sentByMe;
0166             chat.lastAttachment = message.attachments;
0167         }
0168 
0169         chats.push_back(std::move(chat));
0170     } else {
0171         auto numbers = co_await m_database->getResults<SingleValue<QString>>(SL(R"(
0172             WITH Numbers AS (
0173                 SELECT
0174                     MAX(time) AS maxTime,
0175                     phoneNumber
0176                 FROM Messages
0177                 GROUP BY phoneNumber
0178             )
0179             SELECT phoneNumber
0180             FROM  Numbers
0181             ORDER BY maxTime desc
0182         )"));
0183 
0184         for (auto &&number : numbers) {
0185             Chat chat;
0186             chat.phoneNumberList = PhoneNumberList(number);
0187             chats.push_back(std::move(chat));
0188         }
0189     }
0190 
0191     co_return chats;
0192 }
0193 
0194 QCoro::Task<std::optional<int>> Database::unreadMessagesForNumber(const PhoneNumberList &phoneNumberList) const
0195 {
0196     auto n = co_await m_database->getResult<SingleValue<int>>(SL("SELECT Count(*) FROM Messages WHERE phoneNumber == ? AND read == False"),
0197                                                               phoneNumberList.toString());
0198     co_return map(std::move(n), [](auto &&v) {
0199         return v.value;
0200     });
0201 }
0202 
0203 QFuture<void> Database::markChatAsRead(const PhoneNumberList &phoneNumberList)
0204 {
0205     return m_database->execute(SL("UPDATE Messages SET read = True WHERE phoneNumber = ? AND NOT read == True"), phoneNumberList.toString());
0206 }
0207 
0208 QFuture<void> Database::deleteChat(const PhoneNumberList &phoneNumberList)
0209 {
0210     return m_database->execute(SL("DELETE FROM Messages WHERE phoneNumber = ?"), phoneNumberList.toString());
0211 }
0212 
0213 QCoro::Task<> Database::addMessage(const Message &message)
0214 {
0215     co_await m_database->execute(SL(R"(
0216         INSERT INTO Messages (
0217             id,
0218             phoneNumber,
0219             text,
0220             time,
0221             read,
0222             delivered,
0223             sentByMe,
0224             attachments,
0225             smil,
0226             fromNumber,
0227             messageId,
0228             deliveryReport,
0229             readReport,
0230             pendingDownload,
0231             contentLocation,
0232             expires,
0233             size)
0234         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
0235         )"),
0236                                  message.id,
0237                                  message.phoneNumberList.toString(),
0238                                  message.text,
0239                                  message.datetime.toMSecsSinceEpoch(),
0240                                  message.read,
0241                                  message.deliveryStatus,
0242                                  message.sentByMe,
0243                                  message.attachments,
0244                                  message.smil,
0245                                  message.fromNumber,
0246                                  message.messageId,
0247                                  message.deliveryReport,
0248                                  message.readReport,
0249                                  message.pendingDownload,
0250                                  message.contentLocation,
0251                                  message.expires.isNull() ? QVariant() : message.expires.toMSecsSinceEpoch(),
0252                                  message.size);
0253 }
0254 
0255 QFuture<void> Database::deleteMessage(const QString &id)
0256 {
0257     return m_database->execute(SL("DELETE FROM Messages WHERE id == ?"), id);
0258 }
0259 
0260 QString Database::generateRandomId()
0261 {
0262     QString intermediateId = SL("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890");
0263     std::shuffle(intermediateId.begin(), intermediateId.end(), std::mt19937(std::random_device()()));
0264     intermediateId.truncate(ID_LEN);
0265 
0266     return intermediateId;
0267 }
0268 
0269 QCoro::Task<> Database::mergeChats(const QString &fromNumbers, const QString toNumbers)
0270 {
0271     co_await m_database->execute(SL("UPDATE Messages SET phoneNumber = ? WHERE phoneNumber = ?"), fromNumbers, toNumbers);
0272 
0273     // need to move files to correct chat attachment subdirectory
0274     QDir attachments(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + SL("/spacebar/attachments"));
0275     const QString folderFromHash = QString::number(hash(fromNumbers));
0276     const QString folderToHash = QString::number(hash(toNumbers));
0277 
0278     // if folder already exists, just move the files
0279     if (attachments.exists(folderToHash) && attachments.exists(folderFromHash)) {
0280         const QString folderPathFrom = attachments.path() + QDir::separator() + folderFromHash;
0281         const QString folderPathTo = attachments.path() + QDir::separator() + folderToHash;
0282         const QStringList files = QDir(folderPathFrom).entryList();
0283         for (auto &file : files) {
0284             QFile::copy(folderPathFrom + QDir::separator() + file, folderPathTo + QDir::separator() + file);
0285             QFile::remove(folderPathFrom + QDir::separator() + file);
0286         }
0287         attachments.rmdir(folderFromHash);
0288     } else if (attachments.exists(folderFromHash)) {
0289         attachments.rename(folderFromHash, folderToHash);
0290     }
0291 }
0292 
0293 QCoro::Task<> Database::migrate()
0294 {
0295     // Check whether the database uses the old migration system
0296     auto revision = co_await m_database->runOnThread([](auto &db) -> std::optional<int> {
0297         // Find out current revision
0298         QSqlQuery currentRevision(db);
0299         currentRevision.prepare(SL("SELECT migrationId FROM Metadata ORDER BY migrationId DESC LIMIT 1"));
0300         exec(currentRevision);
0301         currentRevision.first();
0302 
0303         if (currentRevision.isValid()) {
0304             return currentRevision.value(0).toUInt();
0305         } else {
0306             return {};
0307         }
0308     });
0309 
0310     // If it does, first run all pending migrations of the old system
0311     if (revision) {
0312         qDebug() << "current (old) database revision" << *revision;
0313         m_database->runOnThread([this, revision = *revision](auto &db) {
0314             // Run migration if necessary
0315             if (revision >= DATABASE_REVISION) {
0316                 return;
0317             }
0318 
0319             MIGRATE_TO_LATEST_FROM(revision);
0320 
0321             // Update migration info if necessary
0322             QSqlQuery update(db);
0323             update.prepare(SL("INSERT INTO Metadata (migrationId) VALUES (:migrationId)"));
0324             update.bindValue(SL(":migrationId"), DATABASE_REVISION);
0325             exec(update);
0326         });
0327 
0328         // Mark an equivalent migration level in the new system.
0329         m_database->setCurrentMigrationLevel(QStringLiteral("2023-05-07-102621_init"));
0330     }
0331 
0332     // Finally, run all migrations of the new system on top of that
0333     m_database->runMigrations(SL(":/migrations"));
0334 }
0335 
0336 void Database::exec(QSqlQuery &query)
0337 {
0338     if (query.lastQuery().isEmpty()) {
0339         // Sending empty queries doesn't make sense
0340         Q_UNREACHABLE();
0341     }
0342     if (!query.exec()) {
0343         qWarning() << "Query" << query.lastQuery() << "resulted in" << query.lastError();
0344     }
0345 }
0346 
0347 void Database::migrationV1(const QSqlDatabase &db, uint)
0348 {
0349     QSqlQuery createTable(db);
0350     createTable.prepare(
0351         SL("CREATE TABLE IF NOT EXISTS Messages (id INTEGER, phoneNumber TEXT, text TEXT, time DATETIME, read BOOLEAN, delivered BOOLEAN, sentByMe BOOLEAN)"));
0352     Database::exec(createTable);
0353 }
0354 
0355 void Database::migrationV2(const QSqlDatabase &db, uint current)
0356 {
0357     MIGRATE_TO(1, current);
0358 
0359     QSqlQuery tempTable(db);
0360     tempTable.prepare(SL("CREATE TABLE temp_table AS SELECT * FROM Messages"));
0361     Database::exec(tempTable);
0362 
0363     QSqlQuery dropOld(db);
0364     dropOld.prepare(SL("DROP TABLE Messages"));
0365     Database::exec(dropOld);
0366 
0367     QSqlQuery createNew(db);
0368     createNew.prepare(
0369         SL("CREATE TABLE IF NOT EXISTS Messages (id TEXT, phoneNumber TEXT, text TEXT, time DATETIME, read BOOLEAN, delivered INTEGER, sentByMe BOOLEAN)"));
0370     Database::exec(createNew);
0371 
0372     QSqlQuery copyTemp(db);
0373     copyTemp.prepare(SL("INSERT INTO Messages SELECT * FROM temp_table"));
0374     Database::exec(copyTemp);
0375 
0376     QSqlQuery dropTemp(db);
0377     dropTemp.prepare(SL("DROP TABLE temp_table"));
0378     Database::exec(dropTemp);
0379 }
0380 
0381 void Database::migrationV3(const QSqlDatabase &db, uint current)
0382 {
0383     MIGRATE_TO(2, current);
0384 
0385     QSqlQuery getPhoneNumbers(db);
0386     getPhoneNumbers.prepare(SL("SELECT DISTINCT phoneNumber FROM Messages"));
0387     Database::exec(getPhoneNumbers);
0388 
0389     while (getPhoneNumbers.next()) {
0390         const auto phoneNumber = getPhoneNumbers.value(0).toString();
0391         qDebug() << "updating phone number" << phoneNumber;
0392         auto normalized = PhoneNumberList(phoneNumber).toString();
0393         qDebug() << "to" << normalized;
0394 
0395         QSqlQuery normalizePhoneNumbers(db);
0396         normalizePhoneNumbers.prepare(SL("UPDATE Messages SET phoneNumber = :normalized WHERE phoneNumber == :phoneNumber"));
0397         normalizePhoneNumbers.bindValue(SL(":normalized"), normalized);
0398         normalizePhoneNumbers.bindValue(SL(":phoneNumber"), phoneNumber);
0399         Database::exec(normalizePhoneNumbers);
0400     }
0401 }
0402 
0403 void Database::migrationV4(const QSqlDatabase &db, uint current)
0404 {
0405     MIGRATE_TO(3, current);
0406 
0407     QSqlQuery addMmsColumns(db);
0408     addMmsColumns.prepare(SL("ALTER TABLE Messages ADD COLUMN attachments TEXT;"));
0409     Database::exec(addMmsColumns);
0410     addMmsColumns.prepare(SL("ALTER TABLE Messages ADD COLUMN smil TEXT;"));
0411     Database::exec(addMmsColumns);
0412     addMmsColumns.prepare(SL("ALTER TABLE Messages ADD COLUMN fromNumber TEXT;"));
0413     Database::exec(addMmsColumns);
0414     addMmsColumns.prepare(SL("ALTER TABLE Messages ADD COLUMN messageId TEXT;"));
0415     Database::exec(addMmsColumns);
0416     addMmsColumns.prepare(SL("ALTER TABLE Messages ADD COLUMN deliveryReport INTEGER;"));
0417     Database::exec(addMmsColumns);
0418     addMmsColumns.prepare(SL("ALTER TABLE Messages ADD COLUMN readReport TEXT;"));
0419     Database::exec(addMmsColumns);
0420     addMmsColumns.prepare(SL("ALTER TABLE Messages ADD COLUMN pendingDownload BOOLEAN;"));
0421     Database::exec(addMmsColumns);
0422     addMmsColumns.prepare(SL("ALTER TABLE Messages ADD COLUMN contentLocation TEXT;"));
0423     Database::exec(addMmsColumns);
0424     addMmsColumns.prepare(SL("ALTER TABLE Messages ADD COLUMN expires DATETIME;"));
0425     Database::exec(addMmsColumns);
0426     addMmsColumns.prepare(SL("ALTER TABLE Messages ADD COLUMN size INTEGER;"));
0427     Database::exec(addMmsColumns);
0428 }
0429 
0430 void Database::migrationV5(const QSqlDatabase &db, uint current)
0431 {
0432     MIGRATE_TO(4, current);
0433 
0434     QSqlQuery fixDuplicateIds(db);
0435     fixDuplicateIds.prepare(SL("UPDATE Messages SET id = ROWID WHERE LENGTH(id) <> 10;"));
0436     Database::exec(fixDuplicateIds);
0437 }
0438 
0439 void Database::migrationV6(const QSqlDatabase &db, uint current)
0440 {
0441     MIGRATE_TO(5, current);
0442 
0443     QSqlQuery removeHtml(db);
0444     removeHtml.prepare(SL("UPDATE Messages SET text = REPLACE(text,'&nbsp;', ' ')"));
0445     Database::exec(removeHtml);
0446     removeHtml.prepare(SL("UPDATE Messages SET text = REPLACE(text, '&lt;', '<')"));
0447     Database::exec(removeHtml);
0448     removeHtml.prepare(SL("UPDATE Messages SET text = REPLACE(text, '&gt;', '>')"));
0449     Database::exec(removeHtml);
0450     removeHtml.prepare(SL("UPDATE Messages SET text = REPLACE(text, '&quot;', '\"')"));
0451     Database::exec(removeHtml);
0452     removeHtml.prepare(SL("UPDATE Messages SET text = REPLACE(text, '&amp;', '&')"));
0453     Database::exec(removeHtml);
0454     removeHtml.prepare(SL("UPDATE Messages SET text = REPLACE(text, '<br />', CHAR(13))"));
0455     Database::exec(removeHtml);
0456     removeHtml.prepare(SL("UPDATE Messages SET text = REPLACE(text, '</a>', '')"));
0457     Database::exec(removeHtml);
0458     removeHtml.prepare(
0459         SL("UPDATE Messages SET text = REPLACE(text, SUBSTR(text, INSTR(text, '<a href='), INSTR(text, '>http') - INSTR(text, '<a href=') + CASE WHEN "
0460            "INSTR(text, '>http') > 0 THEN 1 ELSE 0 END), '')"));
0461     Database::exec(removeHtml);
0462 }
0463 
0464 void Database::migrationV7(const QSqlDatabase &db, const uint current)
0465 {
0466     MIGRATE_TO(6, current);
0467 
0468     QSqlQuery sql(db);
0469     sql.prepare(SL("ALTER TABLE Messages ADD COLUMN tapbacks TEXT"));
0470     Database::exec(sql);
0471 }
0472 
0473 void Database::migrationV8(const QSqlDatabase &db, uint current)
0474 {
0475     MIGRATE_TO(7, current);
0476 
0477     QSqlQuery fetch(db);
0478     fetch.prepare(SL("SELECT DISTINCT phoneNumber FROM Messages"));
0479     exec(fetch);
0480 
0481     while (fetch.next()) {
0482         const QString original = fetch.value(0).toString();
0483 
0484         // now using the modem instead of locale to format numbers
0485         // fixes any numbers that were not formatted for cases where the locale was unset
0486         QString formatted = PhoneNumberList(fetch.value(0).toString().replace(u';', u'~')).toString();
0487 
0488         if (formatted.contains(u'~')) {
0489             QStringList numbers = formatted.split(u'~');
0490             numbers.sort();
0491             formatted = numbers.join(u'~');
0492         }
0493 
0494         if (original != formatted) {
0495             mergeChats(original, formatted);
0496         }
0497     }
0498 
0499     QSqlQuery sql(db);
0500     sql.prepare(SL("UPDATE Messages SET phoneNumber = REPLACE(phoneNumber,';', '~')"));
0501     Database::exec(sql);
0502 }
0503 
0504 Message Message::fromSql(ColumnTypes &&tuple)
0505 {
0506     auto [id,
0507           phoneNumberList,
0508           text,
0509           datetime,
0510           read,
0511           deliveryStatus,
0512           sentByMe,
0513           attachments,
0514           smil,
0515           fromNumber,
0516           messageId,
0517           deliveryReport,
0518           readReport,
0519           pendingDownload,
0520           contentLocation,
0521           expires,
0522           size,
0523           tapbacks] = tuple;
0524 
0525     return Message{id,
0526                    PhoneNumberList(phoneNumberList),
0527                    text,
0528                    QDateTime::fromMSecsSinceEpoch(datetime),
0529                    read,
0530                    MessageState(deliveryStatus),
0531                    sentByMe,
0532                    attachments,
0533                    smil,
0534                    fromNumber,
0535                    messageId,
0536                    deliveryReport,
0537                    readReport,
0538                    pendingDownload,
0539                    contentLocation,
0540                    QDateTime::fromMSecsSinceEpoch(expires),
0541                    size,
0542                    tapbacks};
0543 }