File indexing completed on 2024-05-05 05:28:46

0001 // SPDX-FileCopyrightText: 2022 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 "messagemodel.h"
0007 #include "channelhandler.h"
0008 #include "utils.h"
0009 
0010 #include <QJsonObject>
0011 #include <QMimeDatabase>
0012 #include <QMimeType>
0013 
0014 #include <contactphonenumbermapper.h>
0015 #include <database.h>
0016 #include <global.h>
0017 #include <phonenumberlist.h>
0018 
0019 #include <QCoroFuture>
0020 
0021 MessageModel::MessageModel(ChannelHandler &handler, const PhoneNumberList &phoneNumberList, QObject *parent)
0022     : QAbstractListModel(parent)
0023     , m_handler(handler)
0024     , m_phoneNumberList(phoneNumberList)
0025 {
0026     disableNotifications(m_phoneNumberList);
0027 
0028     for (const auto &number : phoneNumberList) {
0029         Person person;
0030         person.m_name = KPeople::PersonData(ContactPhoneNumberMapper::instance().uriForNumber(number)).name();
0031         person.m_phoneNumber = number.toInternational();
0032         m_peopleData.append(person);
0033     }
0034 
0035     connect(m_handler.interface(), &OrgKdeSpacebarDaemonInterface::messageAdded, this, &MessageModel::messageAdded);
0036 
0037     connect(m_handler.interface(), &OrgKdeSpacebarDaemonInterface::messageUpdated, this, &MessageModel::messageUpdated);
0038 
0039     connect(m_handler.interface(), &OrgKdeSpacebarDaemonInterface::manualDownloadFinished, this, [this](const QString &id, const bool isEmpty) {
0040         if (isEmpty) {
0041             updateMessageState(id, MessageState::Failed, true);
0042         } else {
0043             const auto idx = getMessageIndex(id);
0044             deleteMessage(id, idx.second, QStringList());
0045         }
0046     });
0047 
0048     fetchMessages(QString());
0049 }
0050 
0051 QCoro::Task<void> MessageModel::fetchMessages(const QString &id, const int limit)
0052 {
0053     const auto messages = co_await m_handler.database().messagesForNumber(m_phoneNumberList, id, limit);
0054 
0055     if (limit == -1) {
0056         beginInsertRows({}, 0, messages.size() - 1);
0057         for (auto &&message : messages) {
0058             m_messages.push_back(message);
0059         }
0060         endInsertRows();
0061     } else {
0062         if (messages.size() == 1) {
0063             beginInsertRows({}, m_messages.size(), m_messages.size());
0064             m_messages.insert(m_messages.begin(), messages.at(0));
0065             endInsertRows();
0066         } else {
0067             beginResetModel();
0068             m_messages = messages;
0069             endResetModel();
0070         }
0071     }
0072 
0073     Q_EMIT messagesFetched();
0074 }
0075 
0076 QCoro::Task<void> MessageModel::fetchUpdatedMessage(const QString &id)
0077 {
0078     const auto &[message, i] = getMessageIndex(id);
0079     const auto messages = co_await m_handler.database().messagesForNumber(m_phoneNumberList, id);
0080 
0081     if (!messages.empty()) {
0082         message->text = messages.front().text;
0083         message->datetime = messages.front().datetime;
0084         message->deliveryStatus = messages.front().deliveryStatus;
0085         message->attachments = messages.front().attachments;
0086         message->smil = messages.front().smil;
0087         message->deliveryReport = messages.front().deliveryReport;
0088         message->readReport = messages.front().readReport;
0089         message->tapbacks = messages.front().tapbacks;
0090 
0091         Q_EMIT dataChanged(index(i), index(i));
0092     }
0093 }
0094 
0095 void MessageModel::fetchAllMessages()
0096 {
0097     fetchMessages(QString(), -1);
0098 }
0099 
0100 QHash<int, QByteArray> MessageModel::roleNames() const
0101 {
0102     return {{Role::TextRole, BL("text")},
0103             {Role::TimeRole, BL("time")},
0104             {Role::DateRole, BL("date")},
0105             {Role::SentByMeRole, BL("sentByMe")},
0106             {Role::ReadRole, BL("read")},
0107             {Role::DeliveryStateRole, BL("deliveryState")},
0108             {Role::IdRole, BL("id")},
0109             {Role::AttachmentsRole, BL("attachments")},
0110             {Role::SmilRole, BL("smil")},
0111             {Role::FromNumberRole, BL("fromNumber")},
0112             {Role::MessageIdRole, BL("messageId")},
0113             {Role::DeliveryReportRole, BL("deliveryReport")},
0114             {Role::ReadReportRole, BL("readReport")},
0115             {Role::PendingDownloadRole, BL("pendingDownload")},
0116             {Role::ContentLocationRole, BL("contentLocation")},
0117             {Role::ExpiresRole, BL("expires")},
0118             {Role::ExpiresDateTimeRole, BL("expiresDateTime")},
0119             {Role::SizeRole, BL("size")},
0120             {Role::TapbacksRole, BL("tapbacks")}};
0121 }
0122 
0123 QVariant MessageModel::data(const QModelIndex &index, int role) const
0124 {
0125     if (!index.isValid() || index.row() < 0 || size_t(index.row()) >= m_messages.size()) {
0126         return false;
0127     }
0128 
0129     // message order is reversed from the C++ side instead of in QML since sectioning doesn't work right otherwise
0130     Message message = m_messages.at((m_messages.size() - 1) - index.row());
0131     switch (role) {
0132     case Role::TextRole:
0133         return message.text;
0134     case Role::TimeRole:
0135         return message.datetime.time();
0136     case Role::DateRole:
0137         return message.datetime.date();
0138     case Role::SentByMeRole:
0139         return message.sentByMe;
0140     case Role::ReadRole:
0141         return message.read;
0142     case Role::DeliveryStateRole:
0143         return DeliveryState(message.deliveryStatus);
0144     case Role::IdRole:
0145         return message.id;
0146     case Role::AttachmentsRole:
0147         return message.attachments;
0148     case Role::SmilRole:
0149         return message.smil;
0150     case Role::FromNumberRole:
0151         return message.fromNumber;
0152     case Role::MessageIdRole:
0153         return message.messageId;
0154     case Role::DeliveryReportRole:
0155         return message.deliveryReport;
0156     case Role::ReadReportRole:
0157         return message.readReport;
0158     case Role::PendingDownloadRole:
0159         return message.pendingDownload;
0160     case Role::ContentLocationRole:
0161         return message.contentLocation;
0162     case Role::ExpiresRole:
0163         return message.expires;
0164     case Role::ExpiresDateTimeRole:
0165         return message.expires.toLocalTime().toString(SL("MMM d, h:mm ap"));
0166     case Role::SizeRole:
0167         return message.size;
0168     case Role::TapbacksRole:
0169         return message.tapbacks;
0170     }
0171 
0172     return {};
0173 }
0174 
0175 int MessageModel::rowCount(const QModelIndex &index) const
0176 {
0177     return index.isValid() ? 0 : m_messages.size();
0178 }
0179 
0180 QVector<Person> MessageModel::people() const
0181 {
0182     return m_peopleData;
0183 }
0184 
0185 PhoneNumberList MessageModel::phoneNumberList() const
0186 {
0187     return m_phoneNumberList;
0188 }
0189 
0190 QString MessageModel::sendingNumber() const
0191 {
0192     QString number = m_handler.interface()->ownNumber();
0193     if (number == SL("The name org.kde.Spacebar was not provided by any .service files")) {
0194         return QString();
0195     } else {
0196         return PhoneNumber(number).toInternational();
0197     }
0198 }
0199 
0200 QString MessageModel::attachmentsFolder() const
0201 {
0202     const QString folder = QString::number(hash(m_phoneNumberList.toString()));
0203     return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + SL("/spacebar/attachments/") + folder;
0204 }
0205 
0206 QVariant MessageModel::fileInfo(const QUrl &path)
0207 {
0208     QFile file(path.toLocalFile());
0209     file.open(QIODevice::ReadOnly);
0210     QByteArray data = file.readAll();
0211     QMimeDatabase db;
0212     QMimeType mime = db.mimeTypeForData(data);
0213     QString fileName = Database::generateRandomId() + SL(".") + mime.preferredSuffix();
0214 
0215     QJsonObject object{{SL("filePath"), path.toString()},
0216                        {SL("name"), path.fileName()},
0217                        {SL("fileName"), fileName},
0218                        {SL("size"), data.length()},
0219                        {SL("mimeType"), mime.name()},
0220                        {SL("iconName"), mime.iconName()}};
0221 
0222     return object;
0223 }
0224 
0225 void MessageModel::messageAdded(const QString &numbers, const QString &id)
0226 {
0227     if (PhoneNumberList(numbers) != m_phoneNumberList) {
0228         return; // Message is not for this model
0229     }
0230 
0231     fetchMessages(id);
0232 }
0233 
0234 void MessageModel::messageUpdated(const QString &numbers, const QString &id)
0235 {
0236     if (PhoneNumberList(numbers) != m_phoneNumberList) {
0237         return; // Message is not for this model
0238     }
0239 
0240     fetchUpdatedMessage(id);
0241 }
0242 
0243 void MessageModel::sendTapback(const QString &id, const QString &tapback, const bool &isRemoved)
0244 {
0245     m_handler.interface()->sendTapback(m_phoneNumberList.toString(), id, tapback, isRemoved);
0246 }
0247 
0248 void MessageModel::sendMessage(const QString &text, const QStringList &files, const qint64 &totalSize)
0249 {
0250     Message message;
0251     message.id = Database::generateRandomId();
0252     message.phoneNumberList = m_phoneNumberList;
0253     message.text = text;
0254     message.sentByMe = true;
0255     message.deliveryStatus = MessageState::Pending;
0256 
0257     beginInsertRows({}, m_messages.size(), m_messages.size());
0258     m_messages.insert(m_messages.begin(), message);
0259     endInsertRows();
0260 
0261     m_handler.interface()->sendMessage(m_phoneNumberList.toString(), message.id, text, files, totalSize);
0262 
0263     // update chat list
0264     Q_EMIT m_handler.messagesChanged(m_phoneNumberList);
0265 }
0266 
0267 QPair<Message *, int> MessageModel::getMessageIndex(const QString &id)
0268 {
0269     auto modelIt = std::find_if(m_messages.begin(), m_messages.end(), [&](const Message &message) {
0270         return message.id == id;
0271     });
0272 
0273     Q_ASSERT(modelIt != m_messages.cend());
0274 
0275     const size_t i = (m_messages.size() - 1) - std::distance(m_messages.begin(), modelIt);
0276     return qMakePair(&(*modelIt.base()), i);
0277 }
0278 
0279 void MessageModel::updateMessageState(const QString &id, MessageState state, const bool temp)
0280 {
0281     const auto &[message, i] = getMessageIndex(id);
0282 
0283     message->deliveryStatus = state;
0284 
0285     if (!temp) {
0286         m_handler.database().updateMessageDeliveryState(id, state);
0287     }
0288 
0289     Q_EMIT dataChanged(index(i), index(i), {Role::DeliveryStateRole});
0290 }
0291 
0292 void MessageModel::markMessageRead(const int id)
0293 {
0294     m_handler.database().markMessageRead(id);
0295 }
0296 
0297 void MessageModel::downloadMessage(const QString &id, const QString &url, const QDateTime &expires)
0298 {
0299     updateMessageState(id, MessageState::Pending, true);
0300     m_handler.interface()->manualDownload(id, url, expires);
0301 }
0302 
0303 void MessageModel::deleteMessage(const QString &id, const int index, const QStringList &files)
0304 {
0305     m_handler.database().deleteMessage(id);
0306 
0307     // delete attachments
0308     const QString sourceFolder = attachmentsFolder();
0309     for (const auto &file : files) {
0310         if (!file.isEmpty()) {
0311             QFile::remove(sourceFolder + SL("/") + file);
0312         }
0313     }
0314 
0315     beginRemoveRows(QModelIndex(), index, index);
0316     m_messages.erase(m_messages.begin() + (m_messages.size() - index - 1));
0317     endRemoveRows();
0318 
0319     // update chat list only if it was the most recent message
0320     if (size_t(index) == m_messages.size()) {
0321         Q_EMIT m_handler.messagesChanged(m_phoneNumberList);
0322     }
0323 }
0324 
0325 void MessageModel::saveAttachments(const QStringList &attachments)
0326 {
0327     const QString sourceFolder = attachmentsFolder();
0328     const QString targetFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
0329     for (const auto &i : attachments) {
0330         QFile::copy(sourceFolder + QStringLiteral("/") + i, targetFolder + QStringLiteral("/") + i);
0331     }
0332 }
0333 
0334 void MessageModel::disableNotifications(const PhoneNumberList &phoneNumberList)
0335 {
0336     m_handler.interface()->disableNotificationsForNumber(phoneNumberList.toString().isNull() ? SL("") : phoneNumberList.toString());
0337 }