File indexing completed on 2024-04-28 16:43:06
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 }