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,' ', ' ')")); 0445 Database::exec(removeHtml); 0446 removeHtml.prepare(SL("UPDATE Messages SET text = REPLACE(text, '<', '<')")); 0447 Database::exec(removeHtml); 0448 removeHtml.prepare(SL("UPDATE Messages SET text = REPLACE(text, '>', '>')")); 0449 Database::exec(removeHtml); 0450 removeHtml.prepare(SL("UPDATE Messages SET text = REPLACE(text, '"', '\"')")); 0451 Database::exec(removeHtml); 0452 removeHtml.prepare(SL("UPDATE Messages SET text = REPLACE(text, '&', '&')")); 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 }