File indexing completed on 2024-04-28 05:26:03

0001 // SPDX-FileCopyrightText: 2020 Jonah Brüchert <jbb@kaidan.im>
0002 // SPDX-FileCopyrightText: 2021 Michael Lang <criticaltemp@protonmail.com>
0003 //
0004 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 
0006 #include "channellogger.h"
0007 #include "modemcontroller.h"
0008 #include "settingsmanager.h"
0009 
0010 #include <QCoroFuture>
0011 #include <QDBusConnection>
0012 #include <QDBusReply>
0013 #include <QLocale>
0014 #include <QTimer>
0015 #include <QtConcurrent>
0016 
0017 #include <KIO/CommandLauncherJob>
0018 #include <KLocalizedString>
0019 #include <KNotification>
0020 #include <KPeople/PersonData>
0021 
0022 #include <contactphonenumbermapper.h>
0023 #include <global.h>
0024 
0025 #include <QCoroDBusPendingReply>
0026 #include <QCoroFuture>
0027 
0028 static bool isScreenSaverActive()
0029 {
0030     bool active = false;
0031     QDBusMessage request = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"),
0032                                                           QStringLiteral("/ScreenSaver"),
0033                                                           QStringLiteral("org.freedesktop.ScreenSaver"),
0034                                                           QStringLiteral("GetActive"));
0035     const QDBusReply<bool> response = QDBusConnection::sessionBus().call(request);
0036     active = response.isValid() ? response.value() : false;
0037     return active;
0038 }
0039 
0040 ChannelLogger::ChannelLogger(std::optional<QString> &modemPath, QObject *parent)
0041     : QObject(parent)
0042 {
0043     QDBusConnection::sessionBus().registerObject(QStringLiteral("/Daemon"), this, QDBusConnection::ExportScriptableContents);
0044 
0045     ModemController::instance().init(modemPath);
0046 
0047     PhoneNumber::setCountryCode(countryCode());
0048 
0049     m_database.migrate();
0050 
0051     connect(&ModemController::instance(), &ModemController::messageAdded, this, [this](ModemManager::Sms::Ptr msg) {
0052         handleIncomingMessage(msg);
0053     });
0054 
0055     connect(&ModemController::instance(), &ModemController::modemConnected, this, &ChannelLogger::checkMessages);
0056 
0057     connect(&ModemController::instance(), &ModemController::modemDataConnectedChanged, this, [this](const bool isConnected) {
0058         m_dataConnected = isConnected;
0059 
0060         if (isConnected) {
0061             for (const auto &indicator : m_deferredIndicators) {
0062                 sendNotifyResponse(indicator, SL("deferred"));
0063             }
0064             m_deferredIndicators.clear();
0065         }
0066     });
0067 
0068     connect(&ModemController::instance(), &ModemController::countryCodeChanged, this, [](const QString &countryCode) {
0069         PhoneNumber::setCountryCode(countryCode);
0070     });
0071 }
0072 
0073 void ChannelLogger::checkMessages()
0074 {
0075     // update own number
0076     m_ownNumber = PhoneNumber(ownNumber());
0077 
0078     // for any unhandled messages
0079     const ModemManager::Sms::List messages = ModemController::instance().messages();
0080     for (const ModemManager::Sms::Ptr &msg : messages) {
0081         if (msg->state() == MMSmsState::MM_SMS_STATE_RECEIVED) {
0082             handleIncomingMessage(msg);
0083         }
0084     }
0085 }
0086 
0087 QString ChannelLogger::ownNumber()
0088 {
0089     return ModemController::instance().ownNumber();
0090 }
0091 
0092 QString ChannelLogger::countryCode()
0093 {
0094     QString countryCode = ModemController::instance().countryCode;
0095 
0096     if (countryCode.isEmpty()) {
0097         const QLocale locale;
0098         const QStringList qcountry = locale.name().split(u'_');
0099         countryCode = qcountry.constLast();
0100     }
0101 
0102     return countryCode;
0103 }
0104 
0105 void ChannelLogger::handleIncomingMessage(ModemManager::Sms::Ptr msg)
0106 {
0107     const QString number = msg->number();
0108     const QString text = msg->text();
0109     const QDateTime datetime = msg->timestamp();
0110     const QByteArray data = msg->data();
0111     ModemController::instance().deleteMessage(msg->uni());
0112 
0113     const PhoneNumberList phoneNumberList = PhoneNumberList(number);
0114 
0115     // TODO check if blocked number
0116     //  use phonebook for blocked number storage so can be shared with dialer?
0117 
0118     // text and data are not valid at the same time
0119     if (!text.isEmpty()) {
0120         saveMessage(phoneNumberList, datetime, text);
0121     } else if (!data.isEmpty()) {
0122         MmsMessage mmsMessage;
0123         m_mms.decodeNotification(mmsMessage, data);
0124 
0125         // should be in the notification itself, but adding here for redundancy
0126         if (mmsMessage.from.isEmpty()) {
0127             mmsMessage.from = number;
0128         }
0129         if (mmsMessage.date.isNull()) {
0130             mmsMessage.date = datetime;
0131         }
0132 
0133         if (mmsMessage.messageType == SL("m-notification-ind")) {
0134             if (mmsMessage.contentType == SL("application/vnd.wap.mms-message")) {
0135                 // give modem data an opportunity to connect if not currently connected
0136                 QTimer::singleShot(1000 * (m_dataConnected ? 0 : 10), this, [this, mmsMessage]() {
0137                     bool autoDownload = SettingsManager::self()->autoDownload();
0138                     const bool autoDownloadContactsOnly = SettingsManager::self()->autoDownloadContactsOnly();
0139                     if (autoDownload && autoDownloadContactsOnly) {
0140                         const QString name = KPeople::PersonData(ContactPhoneNumberMapper::instance().uriForNumber(PhoneNumber(mmsMessage.from)), this).name();
0141                         if (name.isEmpty()) {
0142                             autoDownload = false;
0143                         }
0144                     }
0145                     if (autoDownload && m_dataConnected) {
0146                         downloadMessage(mmsMessage);
0147                     } else {
0148                         // manually download later
0149                         createDownloadNotification(mmsMessage);
0150                     }
0151                 });
0152             } else {
0153                 qDebug() << "Unknown content type:" << mmsMessage.contentType;
0154             }
0155         } else if (mmsMessage.messageType == SL("m-delivery-ind")) {
0156             if (!mmsMessage.messageId.isEmpty()) {
0157                 m_database.updateMessageDeliveryReport(mmsMessage.messageId);
0158             }
0159         } else if (mmsMessage.messageType == SL("m-read-orig-ind")) {
0160             if (!mmsMessage.messageId.isEmpty()) {
0161                 m_database.updateMessageReadReport(mmsMessage.messageId, PhoneNumber(mmsMessage.from));
0162             }
0163         } else if (mmsMessage.messageType == SL("m-cancel-req")) {
0164             sendCancelResponse(mmsMessage.transactionId);
0165         } else {
0166             qDebug() << "Unknown message type:" << mmsMessage.messageType;
0167             sendNotifyResponse(mmsMessage.transactionId, SL("unrecognized"));
0168         }
0169     } else {
0170         saveMessage(phoneNumberList, datetime);
0171     }
0172 }
0173 
0174 void ChannelLogger::createDownloadNotification(const MmsMessage &mmsMessage)
0175 {
0176     saveMessage(PhoneNumberList(mmsMessage.from),
0177                 mmsMessage.date,
0178                 QString(), // text
0179                 QString(), // attachments
0180                 QString(), // smil
0181                 QString(), // fromNumber
0182                 QString(), // messageId
0183                 true, // pendingDownload
0184                 mmsMessage.contentLocation,
0185                 mmsMessage.expiry,
0186                 mmsMessage.messageSize);
0187 
0188     // this is important, otherwise an MMSC server may send repeated notifications
0189     if (m_dataConnected) {
0190         sendNotifyResponse(mmsMessage.transactionId, SL("deferred"));
0191     } else {
0192         m_deferredIndicators.append(mmsMessage.transactionId);
0193     }
0194 }
0195 
0196 void ChannelLogger::manualDownload(const QString &id, const QString &url, const QDateTime &expires)
0197 {
0198     MmsMessage mmsMessage;
0199     mmsMessage.databaseId = id;
0200     mmsMessage.contentLocation = url;
0201     mmsMessage.expiry = expires;
0202     mmsMessage.transactionId = m_mms.generateTransactionId();
0203 
0204     if (m_dataConnected) {
0205         downloadMessage(mmsMessage);
0206     } else {
0207         Q_EMIT manualDownloadFinished(id, true);
0208     }
0209 }
0210 
0211 void ChannelLogger::handleDownloadedMessage(const QByteArray &response, const QString &url, const QDateTime &expires)
0212 {
0213     MmsMessage mmsMessage;
0214     mmsMessage.ownNumber = m_ownNumber;
0215     m_mms.decodeMessage(mmsMessage, response);
0216 
0217     // fromNumber is only useful to know in group conversations
0218     const QString fromNumber = mmsMessage.to.length() > 1 ? mmsMessage.from : QString();
0219 
0220     saveMessage(mmsMessage.phoneNumberList,
0221                 mmsMessage.date,
0222                 mmsMessage.text,
0223                 mmsMessage.attachments,
0224                 mmsMessage.smil,
0225                 fromNumber,
0226                 mmsMessage.messageId,
0227                 false,
0228                 url,
0229                 expires,
0230                 response.size());
0231 }
0232 
0233 QCoro::Task<void> ChannelLogger::addMessage(const Message &message)
0234 {
0235     // save to database
0236     co_await m_database.addMessage(message);
0237 
0238     // add message to open conversation
0239     if (!message.sentByMe) {
0240         Q_EMIT messageAdded(message.phoneNumberList.toString(), message.id);
0241     }
0242 }
0243 
0244 void ChannelLogger::updateMessage(const Message &message)
0245 {
0246     // update message in open conversation
0247     Q_EMIT messageUpdated(message.phoneNumberList.toString(), message.id);
0248 }
0249 
0250 QCoro::Task<void> ChannelLogger::saveMessage(const PhoneNumberList &phoneNumberList,
0251                                              const QDateTime &datetime,
0252                                              const QString &text,
0253                                              const QString &attachments,
0254                                              const QString &smil,
0255                                              const QString &fromNumber,
0256                                              const QString &messageId,
0257                                              const bool pendingDownload,
0258                                              const QString &contentLocation,
0259                                              const QDateTime &expires,
0260                                              const int size)
0261 {
0262     Message message;
0263     message.text = text;
0264     message.sentByMe = false; // SMS doesn't have any kind of synchronization, so received messages are always from the chat partner.
0265     message.datetime = datetime;
0266     message.deliveryStatus = MessageState::Received; // It arrived, soo
0267     message.phoneNumberList = phoneNumberList;
0268     message.id = Database::generateRandomId();
0269     message.read = message.phoneNumberList == m_disabledNotificationNumber;
0270     message.attachments = attachments;
0271     message.smil = smil;
0272     message.fromNumber = fromNumber;
0273     message.messageId = messageId;
0274     message.pendingDownload = pendingDownload;
0275     message.contentLocation = contentLocation;
0276     message.expires = expires;
0277     message.size = size;
0278 
0279     // prevent chronologically misordered chat history
0280     if (message.read && message.datetime.secsTo(QDateTime::currentDateTime()) < 60) {
0281         // adjust for small delays if conversation is currently open
0282         message.datetime = QDateTime::currentDateTime();
0283     } else if (message.datetime.daysTo(QDateTime::currentDateTime()) > 7) {
0284         // probably an invalid date if more than a week old
0285         message.datetime = QDateTime::currentDateTime();
0286     } else if (message.datetime > QDateTime::currentDateTime() && QDateTime::currentSecsSinceEpoch() > 31536000) {
0287         // future datetimes do not make sense
0288         message.datetime = QDateTime::currentDateTime();
0289     }
0290 
0291     if (co_await handleTapbackReaction(message, message.fromNumber.isEmpty() ? message.phoneNumberList.toString() : message.fromNumber)) {
0292         updateMessage(message);
0293 
0294         if (SettingsManager::self()->ignoreTapbacks()) {
0295             co_return;
0296         }
0297     } else {
0298         co_await addMessage(message);
0299     }
0300 
0301     // TODO add setting to turn off notifications for multiple chats in addition to current chat
0302     if (message.phoneNumberList == m_disabledNotificationNumber) {
0303         if (!isScreenSaverActive()) {
0304             co_return;
0305         }
0306     }
0307 
0308     createNotification(message);
0309 }
0310 
0311 void ChannelLogger::sendMessage(const QString &numbers, const QString &id, const QString &text, const QStringList &files, const qint64 &totalSize)
0312 {
0313     PhoneNumberList phoneNumberList = PhoneNumberList(numbers);
0314 
0315     [this, phoneNumberList, id, text, files, totalSize]() -> QCoro::Task<void> {
0316         QString result;
0317         // check if it is a MMS message
0318         if (phoneNumberList.size() > 1 || files.length() > 0) {
0319             if (SettingsManager::self()->groupConversation()) {
0320                 sendMessageMMS(phoneNumberList, id, text, files, totalSize);
0321             } else {
0322                 // send as individual messages
0323                 for (const auto &phoneNumber : phoneNumberList) {
0324                     if (files.length() > 0) {
0325                         sendMessageMMS(PhoneNumberList(phoneNumber.toInternational()), Database::generateRandomId(), text, files, totalSize);
0326                     } else {
0327                         result = co_await sendMessageSMS(phoneNumber, Database::generateRandomId(), text);
0328                     }
0329                 }
0330 
0331                 // update delivery status of original message
0332                 Message message;
0333                 message.id = id;
0334                 message.phoneNumberList = phoneNumberList;
0335                 message.datetime = QDateTime::currentDateTime();
0336                 message.deliveryStatus = MessageState::Sent;
0337                 updateMessage(message);
0338             }
0339         } else {
0340             result = co_await sendMessageSMS(phoneNumberList.first(), id, text);
0341         }
0342 
0343         if (result.isEmpty()) {
0344             qDebug() << "Message sent successfully";
0345         } else {
0346             qDebug() << "Failed successfully" << result;
0347         }
0348     }();
0349 }
0350 
0351 void ChannelLogger::sendTapback(const QString &numbers, const QString &id, const QString &tapback, const bool &isRemoved)
0352 {
0353     sendTapbackHandler(numbers, id, tapback, isRemoved);
0354 }
0355 
0356 QCoro::Task<QString> ChannelLogger::sendMessageSMS(const PhoneNumber &phoneNumber, const QString &id, const QString &text)
0357 {
0358     ModemManager::ModemMessaging::Message m;
0359     m.number = phoneNumber.toE164();
0360     m.text = text;
0361 
0362     Message message;
0363     message.id = id;
0364     message.phoneNumberList = PhoneNumberList(phoneNumber.toInternational());
0365     message.text = text;
0366     message.datetime = QDateTime::currentDateTime();
0367     message.read = true;
0368     message.sentByMe = true;
0369     message.deliveryStatus = MessageState::Pending;
0370 
0371     // add message to database
0372     co_await addMessage(message);
0373 
0374     auto maybeReply = ModemController::instance().createMessage(m);
0375 
0376     if (!maybeReply) {
0377         m_database.updateMessageDeliveryState(message.id, MessageState::Failed);
0378         updateMessage(message);
0379         co_return QStringLiteral("No modem");
0380     }
0381 
0382     const QDBusReply<QDBusObjectPath> msgPathResult = co_await *maybeReply;
0383 
0384     if (!msgPathResult.isValid()) {
0385         m_database.updateMessageDeliveryState(message.id, MessageState::Failed);
0386         updateMessage(message);
0387         co_return msgPathResult.error().message();
0388     }
0389 
0390     ModemManager::Sms::Ptr mmMessage = QSharedPointer<ModemManager::Sms>::create(msgPathResult.value().path());
0391 
0392     connect(mmMessage.get(), &ModemManager::Sms::stateChanged, this, [mmMessage, message, this] {
0393         qDebug() << "state changed" << mmMessage->state();
0394 
0395         switch (mmMessage->state()) {
0396         case MM_SMS_STATE_SENT:
0397             // The message was successfully sent
0398             m_database.updateMessageDeliveryState(message.id, MessageState::Sent);
0399             updateMessage(message);
0400             break;
0401         case MM_SMS_STATE_RECEIVED:
0402             // The message has been completely received
0403             // Should not happen
0404             qWarning() << "Received a message we sent";
0405             break;
0406         case MM_SMS_STATE_RECEIVING:
0407             // The message is being received but is not yet complete
0408             // Should not happen
0409             qWarning() << "Receiving a message we sent";
0410             break;
0411         case MM_SMS_STATE_SENDING:
0412             // The message is queued for delivery
0413             m_database.updateMessageDeliveryState(message.id, MessageState::Pending);
0414             updateMessage(message);
0415             break;
0416         case MM_SMS_STATE_STORED:
0417             // The message has been neither received nor yet sent
0418             m_database.updateMessageDeliveryState(message.id, MessageState::Pending);
0419             updateMessage(message);
0420             break;
0421         case MM_SMS_STATE_UNKNOWN:
0422             // State unknown or not reportable
0423             m_database.updateMessageDeliveryState(message.id, MessageState::Unknown);
0424             updateMessage(message);
0425             break;
0426         }
0427     });
0428 
0429     connect(mmMessage.get(), &ModemManager::Sms::deliveryStateChanged, this, [=] {
0430         MMSmsDeliveryState state = mmMessage->deliveryState();
0431         // This is only applicable if the PDU type is MM_SMS_PDU_TYPE_STATUS_REPORT
0432         // TODO handle and store message delivery report state
0433         qDebug() << "deliverystate changed" << state;
0434     });
0435 
0436     QDBusReply<void> sendResult = co_await mmMessage->send();
0437 
0438     if (!sendResult.isValid()) {
0439         m_database.updateMessageDeliveryState(message.id, MessageState::Failed);
0440         updateMessage(message);
0441 
0442         co_return sendResult.error().message();
0443     }
0444 
0445     co_return QString();
0446 }
0447 
0448 QCoro::Task<QString>
0449 ChannelLogger::sendMessageMMS(const PhoneNumberList &phoneNumberList, const QString &id, const QString &text, const QStringList &files, const qint64 totalSize)
0450 {
0451     Message message;
0452     message.phoneNumberList = phoneNumberList;
0453     message.text = text;
0454     message.datetime = QDateTime::currentDateTime();
0455     message.read = true;
0456     message.sentByMe = true;
0457     message.deliveryStatus = MessageState::Pending;
0458 
0459     MmsMessage mmsMessage;
0460     mmsMessage.ownNumber = m_ownNumber;
0461     mmsMessage.from = m_ownNumber.toInternational();
0462     mmsMessage.to = phoneNumberList.toString().split(u'~');
0463     mmsMessage.text = message.text;
0464     QByteArray data;
0465     m_mms.encodeMessage(mmsMessage, data, files, totalSize);
0466 
0467     // update message with encoded content parts
0468     message.id = id;
0469     message.text = mmsMessage.text;
0470     message.attachments = mmsMessage.attachments;
0471     message.smil = mmsMessage.smil;
0472     updateMessage(message);
0473 
0474     // add message to database
0475     co_await addMessage(message);
0476 
0477     // send message
0478     const QByteArray response = co_await uploadMessage(data);
0479     if (response.isEmpty()) {
0480         m_database.updateMessageDeliveryState(message.id, MessageState::Failed);
0481         updateMessage(message);
0482     } else {
0483         MmsMessage mmsMessage;
0484         m_mms.decodeConfirmation(mmsMessage, response);
0485         if (mmsMessage.responseStatus == 0) {
0486             m_database.updateMessageDeliveryState(message.id, MessageState::Sent);
0487             updateMessage(message);
0488 
0489             if (!mmsMessage.messageId.isEmpty()) {
0490                 m_database.updateMessageSent(message.id, mmsMessage.messageId, mmsMessage.contentLocation);
0491             }
0492         } else {
0493             m_database.updateMessageDeliveryState(message.id, MessageState::Failed);
0494             updateMessage(message);
0495             qDebug() << mmsMessage.responseText;
0496         }
0497     }
0498 
0499     co_return QString();
0500 }
0501 
0502 QCoro::Task<void> ChannelLogger::sendCancelResponse(const QString &transactionId)
0503 {
0504     const QByteArray data = m_mms.encodeCancelResponse(transactionId);
0505     const QByteArray response = co_await uploadMessage(data);
0506 }
0507 
0508 QCoro::Task<void> ChannelLogger::sendDeliveryAcknowledgement(const QString &transactionId)
0509 {
0510     const QByteArray data = m_mms.encodeDeliveryAcknowledgement(transactionId);
0511     const QByteArray response = co_await uploadMessage(data);
0512 }
0513 
0514 QCoro::Task<void> ChannelLogger::sendNotifyResponse(const QString &transactionId, const QString &status)
0515 {
0516     const QByteArray data = m_mms.encodeNotifyResponse(transactionId, status);
0517     const QByteArray response = co_await uploadMessage(data);
0518 }
0519 
0520 QCoro::Task<void> ChannelLogger::sendReadReport(const QString &messageId)
0521 {
0522     const QByteArray data = m_mms.encodeReadReport(messageId);
0523     const QByteArray response = co_await uploadMessage(data);
0524 }
0525 
0526 void ChannelLogger::createNotification(Message &message)
0527 {
0528     auto *notification = new KNotification(QStringLiteral("incomingMessage"));
0529     notification->setComponentName(SL("spacebar"));
0530     notification->setIconName(SL("message-new"));
0531 
0532     QString title = i18n("New message");
0533     if (SettingsManager::self()->showSenderInfo()) {
0534         const PhoneNumber from = message.fromNumber.isEmpty() ? message.phoneNumberList.first() : PhoneNumber(message.fromNumber);
0535         title = KPeople::PersonData(ContactPhoneNumberMapper::instance().uriForNumber(from), this).name();
0536         if (title.isEmpty()) {
0537             title = from.toNational();
0538         }
0539         title = i18n("Message from %1", title);
0540     }
0541     notification->setTitle(title);
0542 
0543     if (SettingsManager::self()->showMessageContent()) {
0544         QString notificationText = message.text;
0545         notificationText.truncate(200);
0546         if (!message.attachments.isEmpty()) {
0547             QJsonArray items = QJsonDocument::fromJson(message.attachments.toUtf8()).array();
0548 
0549             int count = static_cast<int>(items.count());
0550             notificationText = i18ncp("Number of files attached", "%1 Attachment", "%1 Attachments", count);
0551             notification->setText(notificationText);
0552 
0553             if (SettingsManager::self()->showAttachments()) {
0554                 QList<QUrl> urls;
0555                 for (const auto &item : items) {
0556                     const QString local = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
0557                     const QString folder = QString::number(hash(message.phoneNumberList.toString()));
0558                     const QString fileName = item.toObject()[SL("fileName")].toString();
0559                     urls.append(QUrl::fromLocalFile(local + SL("/spacebar/attachments/") + folder + SL("/") + fileName));
0560                 }
0561                 notification->setUrls(urls);
0562             }
0563         } else {
0564             notification->setText(notificationText);
0565         }
0566     }
0567 
0568     // copy current pointer to notification, otherwise this would just close the most recent one.
0569     auto openApp = [notification, message]() {
0570         notification->close();
0571         auto *job = new KIO::CommandLauncherJob(SL("spacebar"), {message.phoneNumberList.toString()});
0572         job->setStartupId(notification->xdgActivationToken().toUtf8());
0573         job->setDesktopName(SL("org.kde.spacebar"));
0574         job->start();
0575     };
0576 
0577     auto defaultAction = notification->addDefaultAction(i18nc("@action open message in application", "Open"));
0578     connect(defaultAction, &KNotificationAction::activated, this, openApp);
0579 
0580     notification->sendEvent();
0581 }
0582 
0583 void ChannelLogger::disableNotificationsForNumber(const QString &numbers)
0584 {
0585     m_disabledNotificationNumber = PhoneNumberList(numbers);
0586 }
0587 
0588 QCoro::Task<bool> ChannelLogger::handleTapbackReaction(Message &message, const QString &reactNumber)
0589 {
0590     for (const auto &tapback : TAPBACK_REMOVED) {
0591         if (message.text.startsWith(tapback)) {
0592             co_return co_await saveTapback(message, reactNumber, tapback, TAPBACK_REMOVED, false, false);
0593         } else if (message.text == tapback.left(tapback.length() - 1) + SL("an image")) {
0594             co_return co_await saveTapback(message, reactNumber, tapback, TAPBACK_REMOVED, false, true);
0595         }
0596     }
0597 
0598     for (const auto &tapback : TAPBACK_ADDED) {
0599         if (message.text.startsWith(tapback)) {
0600             co_return co_await saveTapback(message, reactNumber, tapback, TAPBACK_ADDED, true, false);
0601         } else if (message.text == tapback.left(tapback.length() - 1) + SL("an image")) {
0602             co_return co_await saveTapback(message, reactNumber, tapback, TAPBACK_ADDED, true, true);
0603         }
0604     }
0605 
0606     co_return false;
0607 }
0608 
0609 QCoro::Task<bool> ChannelLogger::saveTapback(Message &message,
0610                                              const QString &reactNumber,
0611                                              const QStringView &tapback,
0612                                              std::span<const QStringView> list,
0613                                              const bool &isAdd,
0614                                              const bool &isImage)
0615 {
0616     const QString searchText = isImage ? SL("") : message.text.mid(tapback.length(), message.text.length() - tapback.length() - 1);
0617     const auto id = isImage ? co_await m_database.lastMessageWithAttachment(message.phoneNumberList)
0618                             : co_await m_database.lastMessageWithText(message.phoneNumberList, searchText);
0619 
0620     if (id) {
0621         Message msg = (co_await m_database.messagesForNumber(message.phoneNumberList, *id)).front();
0622         QJsonObject reactions = QJsonDocument::fromJson(msg.tapbacks.toUtf8()).object();
0623         QJsonArray numbers;
0624         const QJsonValue number = QJsonValue(reactNumber);
0625 
0626         // limits tapbacks to one per message per number
0627         for (const auto &keyToRemove : TAPBACK_KEYS) {
0628             if (reactions.contains(keyToRemove)) {
0629                 numbers = reactions[keyToRemove].toArray();
0630 
0631                 for (int i = 0; i < numbers.size(); ++i) {
0632                     if (numbers.at(i) == number) {
0633                         numbers.removeAt(i);
0634 
0635                         if (numbers.isEmpty()) {
0636                             reactions.remove(keyToRemove);
0637                         } else {
0638                             reactions[keyToRemove] = numbers;
0639                         }
0640                     }
0641                 }
0642             }
0643         }
0644 
0645         if (isAdd) {
0646             const int idx = std::find(list.begin(), list.end(), tapback) - list.begin();
0647 
0648             numbers = reactions[TAPBACK_KEYS[idx]].toArray();
0649 
0650             if (!numbers.contains(number)) {
0651                 numbers.append(number);
0652             }
0653 
0654             reactions.insert(TAPBACK_KEYS[idx], numbers);
0655         }
0656 
0657         if (reactions.isEmpty()) {
0658             msg.tapbacks = QString();
0659         } else {
0660             QJsonDocument jsonDoc;
0661             jsonDoc.setObject(reactions);
0662             msg.tapbacks = QString::fromUtf8(jsonDoc.toJson(QJsonDocument::Compact));
0663         }
0664 
0665         m_database.updateMessageTapbacks(*id, msg.tapbacks);
0666 
0667         message.id = msg.id;
0668         co_return true;
0669     }
0670 
0671     co_return false;
0672 }
0673 
0674 QCoro::Task<void> ChannelLogger::sendTapbackHandler(const QString &numbers, const QString &id, const QString &tapback, const bool &isRemoved)
0675 {
0676     Message message = (co_await m_database.messagesForNumber(PhoneNumberList(numbers), id)).front();
0677     const int idx = std::find(TAPBACK_KEYS.cbegin(), TAPBACK_KEYS.cend(), tapback) - TAPBACK_KEYS.cbegin();
0678 
0679     if (message.attachments.isEmpty()) {
0680         if (isRemoved) {
0681             message.text = TAPBACK_REMOVED[idx] + message.text + SL("”");
0682         } else {
0683             message.text = TAPBACK_ADDED[idx] + message.text + SL("”");
0684         }
0685     } else {
0686         if (isRemoved) {
0687             message.text = TAPBACK_REMOVED[idx].left(TAPBACK_REMOVED[idx].length() - 1) + SL("an image");
0688         } else {
0689             message.text = TAPBACK_ADDED[idx].left(TAPBACK_ADDED[idx].length() - 1) + SL("an image");
0690         }
0691     }
0692 
0693     handleTapbackReaction(message, m_ownNumber.toInternational());
0694     Q_EMIT messageUpdated(numbers, message.id);
0695 
0696     for (const auto &phoneNumber : PhoneNumberList(numbers)) {
0697         ModemManager::ModemMessaging::Message m;
0698         m.number = phoneNumber.toE164();
0699         m.text = message.text;
0700 
0701         auto maybeReply = ModemController::instance().createMessage(m);
0702 
0703         if (!maybeReply) {
0704             qDebug() << "No modem";
0705             co_return;
0706         }
0707 
0708         const QDBusReply<QDBusObjectPath> msgPathResult = *maybeReply;
0709 
0710         if (!msgPathResult.isValid()) {
0711             co_return;
0712         }
0713 
0714         ModemManager::Sms::Ptr mmMessage = QSharedPointer<ModemManager::Sms>::create(msgPathResult.value().path());
0715 
0716         QDBusReply<void> sendResult = mmMessage->send();
0717 
0718         if (!sendResult.isValid()) {
0719             qDebug() << sendResult.error().message();
0720             co_return;
0721         }
0722     }
0723 }
0724 
0725 void ChannelLogger::syncSettings()
0726 {
0727     SettingsManager::self()->load();
0728 }
0729 
0730 QCoro::Task<QByteArray> ChannelLogger::uploadMessage(const QByteArray &data)
0731 {
0732     const QString url = SettingsManager::self()->mmsc();
0733     if (url.length() < 10) {
0734         qDebug() << "Invalid URL provided";
0735         co_return BL("");
0736     }
0737 
0738 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0739     const QByteArray response = co_await QtConcurrent::run(&ECurl::networkRequest, &m_curl, url, data);
0740 #else
0741     const QByteArray response = co_await QtConcurrent::run(&m_curl, &ECurl::networkRequest, url, data);
0742 #endif
0743 
0744     if (response.isNull()) {
0745         co_return QByteArray();
0746     } else {
0747         co_return response;
0748     }
0749 }
0750 
0751 QCoro::Task<void> ChannelLogger::downloadMessage(const MmsMessage message)
0752 {
0753     const QString url = message.contentLocation;
0754 
0755 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0756     const QByteArray response = co_await QtConcurrent::run(&ECurl::networkRequest, &m_curl, url, BL(""));
0757 #else
0758     const QByteArray response = co_await QtConcurrent::run(&m_curl, &ECurl::networkRequest, url, BL(""));
0759 #endif
0760 
0761     if (response.isNull()) {
0762         if (!message.databaseId.isEmpty()) {
0763             Q_EMIT manualDownloadFinished(message.databaseId, true);
0764         } else {
0765             createDownloadNotification(message);
0766         }
0767     } else {
0768         // if message exists, do not create a new download notification
0769         if (!message.databaseId.isEmpty()) {
0770             Q_EMIT manualDownloadFinished(message.databaseId, response.isEmpty());
0771         } else if (response.isEmpty()) {
0772             createDownloadNotification(message);
0773         }
0774 
0775         if (!response.isEmpty()) {
0776             handleDownloadedMessage(response, message.contentLocation, message.expiry);
0777 
0778             if (!message.transactionId.isEmpty()) {
0779                 sendDeliveryAcknowledgement(message.transactionId); // acknowledge download
0780             }
0781         }
0782     }
0783 }