File indexing completed on 2024-05-12 09:02:17

0001 // SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
0002 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0003 
0004 #include "eventhandler.h"
0005 
0006 #include <QMovie>
0007 
0008 #include <KLocalizedString>
0009 
0010 #include <Quotient/eventitem.h>
0011 #include <Quotient/events/encryptionevent.h>
0012 #include <Quotient/events/reactionevent.h>
0013 #include <Quotient/events/redactionevent.h>
0014 #include <Quotient/events/roomavatarevent.h>
0015 #include <Quotient/events/roomcanonicalaliasevent.h>
0016 #include <Quotient/events/roommemberevent.h>
0017 #include <Quotient/events/roompowerlevelsevent.h>
0018 #include <Quotient/events/simplestateevents.h>
0019 #include <Quotient/events/stickerevent.h>
0020 #include <Quotient/quotient_common.h>
0021 
0022 #include "delegatetype.h"
0023 #include "eventhandler_logging.h"
0024 #include "events/pollevent.h"
0025 #include "linkpreviewer.h"
0026 #include "models/reactionmodel.h"
0027 #include "neochatconfig.h"
0028 #include "neochatroom.h"
0029 #include "texthandler.h"
0030 #include "utils.h"
0031 
0032 using namespace Quotient;
0033 
0034 const NeoChatRoom *EventHandler::getRoom() const
0035 {
0036     return m_room;
0037 }
0038 
0039 void EventHandler::setRoom(const NeoChatRoom *room)
0040 {
0041     if (room == m_room) {
0042         return;
0043     }
0044     m_room = room;
0045 }
0046 
0047 const Quotient::Event *EventHandler::getEvent() const
0048 {
0049     return m_event;
0050 }
0051 
0052 void EventHandler::setEvent(const Quotient::RoomEvent *event)
0053 {
0054     if (m_room == nullptr) {
0055         qCWarning(EventHandling) << "cannot setEvent when m_room is set to nullptr.";
0056         return;
0057     }
0058     if (event == m_event) {
0059         return;
0060     }
0061     m_event = event;
0062 }
0063 
0064 QString EventHandler::getId() const
0065 {
0066     if (m_event == nullptr) {
0067         qCWarning(EventHandling) << "getId called with m_event set to nullptr.";
0068         return {};
0069     }
0070 
0071     return !m_event->id().isEmpty() ? m_event->id() : m_event->transactionId();
0072 }
0073 
0074 DelegateType::Type EventHandler::getDelegateTypeForEvent(const Quotient::RoomEvent *event) const
0075 {
0076     if (auto e = eventCast<const RoomMessageEvent>(event)) {
0077         switch (e->msgtype()) {
0078         case MessageEventType::Emote:
0079             return DelegateType::Emote;
0080         case MessageEventType::Notice:
0081             return DelegateType::Notice;
0082         case MessageEventType::Image:
0083             return DelegateType::Image;
0084         case MessageEventType::Audio:
0085             return DelegateType::Audio;
0086         case MessageEventType::Video:
0087             return DelegateType::Video;
0088         case MessageEventType::Location:
0089             return DelegateType::Location;
0090         default:
0091             break;
0092         }
0093         if (e->hasFileContent()) {
0094             return DelegateType::File;
0095         }
0096 
0097         return DelegateType::Message;
0098     }
0099     if (is<const StickerEvent>(*event)) {
0100         return DelegateType::Sticker;
0101     }
0102     if (event->isStateEvent()) {
0103         if (event->matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
0104             return DelegateType::LiveLocation;
0105         }
0106         return DelegateType::State;
0107     }
0108     if (is<const EncryptedEvent>(*event)) {
0109         return DelegateType::Encrypted;
0110     }
0111     if (is<PollStartEvent>(*event)) {
0112         const auto pollEvent = eventCast<const PollStartEvent>(event);
0113         if (pollEvent->isRedacted()) {
0114             return DelegateType::Message;
0115         }
0116         return DelegateType::Poll;
0117     }
0118 
0119     return DelegateType::Other;
0120 }
0121 
0122 DelegateType::Type EventHandler::getDelegateType() const
0123 {
0124     if (m_event == nullptr) {
0125         qCWarning(EventHandling) << "getDelegateType called with m_event set to nullptr.";
0126         return DelegateType::Other;
0127     }
0128 
0129     return getDelegateTypeForEvent(m_event);
0130 }
0131 
0132 QVariantMap EventHandler::getAuthor(bool isPending) const
0133 {
0134     if (m_room == nullptr) {
0135         qCWarning(EventHandling) << "getAuthor called with m_room set to nullptr.";
0136         return {};
0137     }
0138     // If we have a room we can return an empty user by handing nullptr to m_room->getUser.
0139     if (m_event == nullptr) {
0140         qCWarning(EventHandling) << "getAuthor called with m_event set to nullptr. Returning empty user.";
0141         return m_room->getUser(nullptr);
0142     }
0143 
0144     const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
0145     return m_room->getUser(author);
0146 }
0147 
0148 QString EventHandler::getAuthorDisplayName(bool isPending) const
0149 {
0150     if (m_room == nullptr) {
0151         qCWarning(EventHandling) << "getAuthorDisplayName called with m_room set to nullptr.";
0152         return {};
0153     }
0154     if (m_event == nullptr) {
0155         qCWarning(EventHandling) << "getAuthorDisplayName called with m_event set to nullptr.";
0156         return {};
0157     }
0158 
0159     if (is<RoomMemberEvent>(*m_event) && !m_event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].isNull()
0160         && m_event->stateKey() == m_event->senderId()) {
0161         auto previousDisplayName = m_event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].toString().toHtmlEscaped();
0162         if (previousDisplayName.isEmpty()) {
0163             previousDisplayName = m_event->senderId();
0164         }
0165         return previousDisplayName;
0166     } else {
0167         const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
0168         return m_room->htmlSafeMemberName(author->id());
0169     }
0170 }
0171 
0172 QString EventHandler::singleLineAuthorDisplayname(bool isPending) const
0173 {
0174     if (m_room == nullptr) {
0175         qCWarning(EventHandling) << "getAuthorDisplayName called with m_room set to nullptr.";
0176         return {};
0177     }
0178     if (m_event == nullptr) {
0179         qCWarning(EventHandling) << "getAuthorDisplayName called with m_event set to nullptr.";
0180         return {};
0181     }
0182 
0183     const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
0184     auto displayName = m_room->safeMemberName(author->id());
0185     displayName.replace(QStringLiteral("<br>\n"), QStringLiteral(" "));
0186     displayName.replace(QStringLiteral("<br>"), QStringLiteral(" "));
0187     displayName.replace(QStringLiteral("<br />\n"), QStringLiteral(" "));
0188     displayName.replace(QStringLiteral("<br />"), QStringLiteral(" "));
0189     displayName.replace(u'\n', QStringLiteral(" "));
0190     displayName.replace(u'\u2028', QStringLiteral(" "));
0191     return displayName;
0192 }
0193 
0194 QDateTime EventHandler::getTime(bool isPending, QDateTime lastUpdated) const
0195 {
0196     if (m_event == nullptr) {
0197         qCWarning(EventHandling) << "getTime called with m_event set to nullptr.";
0198         return {};
0199     }
0200     if (isPending && lastUpdated == QDateTime()) {
0201         qCWarning(EventHandling) << "a value must be provided for lastUpdated for a pending event.";
0202         return {};
0203     }
0204 
0205     return isPending ? lastUpdated : m_event->originTimestamp();
0206 }
0207 
0208 QString EventHandler::getTimeString(bool relative, QLocale::FormatType format, bool isPending, QDateTime lastUpdated) const
0209 {
0210     if (m_event == nullptr) {
0211         qCWarning(EventHandling) << "getTimeString called with m_event set to nullptr.";
0212         return {};
0213     }
0214     if (isPending && lastUpdated == QDateTime()) {
0215         qCWarning(EventHandling) << "a value must be provided for lastUpdated for a pending event.";
0216         return {};
0217     }
0218 
0219     auto ts = getTime(isPending, lastUpdated);
0220     if (ts.isValid()) {
0221         if (relative) {
0222             return m_format.formatRelativeDate(ts.toLocalTime().date(), format);
0223         } else {
0224             return QLocale().toString(ts.toLocalTime().time(), format);
0225         }
0226     }
0227     return {};
0228 }
0229 
0230 bool EventHandler::isHighlighted()
0231 {
0232     if (m_room == nullptr) {
0233         qCWarning(EventHandling) << "isHighlighted called with m_room set to nullptr.";
0234         return false;
0235     }
0236     if (m_event == nullptr) {
0237         qCWarning(EventHandling) << "isHighlighted called with m_event set to nullptr.";
0238         return false;
0239     }
0240 
0241     return !m_room->isDirectChat() && m_room->isEventHighlighted(m_event);
0242 }
0243 
0244 bool EventHandler::isHidden()
0245 {
0246     if (m_room == nullptr) {
0247         qCWarning(EventHandling) << "isHidden called with m_room set to nullptr.";
0248         return false;
0249     }
0250     if (m_event == nullptr) {
0251         qCWarning(EventHandling) << "isHidden called with m_event set to nullptr.";
0252         return false;
0253     }
0254 
0255     if (m_event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
0256         return true;
0257     }
0258 
0259     if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(m_event)) {
0260         if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
0261             return true;
0262         } else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
0263             return true;
0264         } else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
0265                    && !NeoChatConfig::self()->showAvatarUpdate()) {
0266             return true;
0267         }
0268     }
0269 
0270     if (m_event->isStateEvent() && eventCast<const StateEvent>(m_event)->repeatsState()) {
0271         return true;
0272     }
0273 
0274     // isReplacement?
0275     if (auto e = eventCast<const RoomMessageEvent>(m_event)) {
0276         if (!e->replacedEvent().isEmpty()) {
0277             return true;
0278         }
0279     }
0280 
0281     if (is<RedactionEvent>(*m_event) || is<ReactionEvent>(*m_event)) {
0282         return true;
0283     }
0284 
0285     if (auto e = eventCast<const RoomMessageEvent>(m_event)) {
0286         if (!e->replacedEvent().isEmpty() && e->replacedEvent() != e->id()) {
0287             return true;
0288         }
0289     }
0290 
0291     if (m_room->connection()->isIgnored(m_room->user(m_event->senderId()))) {
0292         return true;
0293     }
0294 
0295     // hide ending live location beacons
0296     if (m_event->isStateEvent() && m_event->matrixType() == "org.matrix.msc3672.beacon_info"_ls && !m_event->contentJson()["live"_ls].toBool()) {
0297         return true;
0298     }
0299 
0300     return false;
0301 }
0302 
0303 QString EventHandler::getRichBody(bool stripNewlines) const
0304 {
0305     if (m_event == nullptr) {
0306         qCWarning(EventHandling) << "getRichBody called with m_event set to nullptr.";
0307         return {};
0308     }
0309     return getBody(m_event, Qt::RichText, stripNewlines);
0310 }
0311 
0312 QString EventHandler::getPlainBody(bool stripNewlines) const
0313 {
0314     if (m_event == nullptr) {
0315         qCWarning(EventHandling) << "getPlainBody called with m_event set to nullptr.";
0316         return {};
0317     }
0318     return getBody(m_event, Qt::PlainText, stripNewlines);
0319 }
0320 
0321 QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const
0322 {
0323     if (event->isRedacted()) {
0324         auto reason = event->redactedBecause()->reason();
0325         return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>") : i18n("<i>[This message was deleted: %1]</i>", reason);
0326     }
0327 
0328     const bool prettyPrint = (format == Qt::RichText);
0329 
0330     return switchOnType(
0331         *event,
0332         [this, format, stripNewlines](const RoomMessageEvent &event) {
0333             return getMessageBody(event, format, stripNewlines);
0334         },
0335         [](const StickerEvent &e) {
0336             return e.body();
0337         },
0338         [this, prettyPrint](const RoomMemberEvent &e) {
0339             // FIXME: Rewind to the name that was at the time of this event
0340             auto subjectName = m_room->htmlSafeMemberName(e.userId());
0341             if (e.membership() == Membership::Leave) {
0342                 if (e.prevContent() && e.prevContent()->displayName) {
0343                     subjectName = sanitized(*e.prevContent()->displayName).toHtmlEscaped();
0344                 }
0345             }
0346 
0347             if (prettyPrint) {
0348                 subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
0349                                   .arg(e.userId(), Utils::getUserColor(m_room->user(e.userId())->hueF()).name(), subjectName);
0350             }
0351 
0352             // The below code assumes senderName output in AuthorRole
0353             switch (e.membership()) {
0354             case Membership::Invite:
0355                 if (e.repeatsState()) {
0356                     auto text = i18n("reinvited %1 to the room", subjectName);
0357                     if (!e.reason().isEmpty()) {
0358                         text += i18nc("Optional reason for an invitation", ": %1") + (prettyPrint ? e.reason().toHtmlEscaped() : e.reason());
0359                     }
0360                     return text;
0361                 }
0362                 Q_FALLTHROUGH();
0363             case Membership::Join: {
0364                 QString text{};
0365                 // Part 1: invites and joins
0366                 if (e.repeatsState()) {
0367                     text = i18n("joined the room (repeated)");
0368                 } else if (e.changesMembership()) {
0369                     text = e.membership() == Membership::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
0370                 }
0371                 if (!text.isEmpty()) {
0372                     if (!e.reason().isEmpty()) {
0373                         text += i18n(": %1", e.reason().toHtmlEscaped());
0374                     }
0375                     return text;
0376                 }
0377                 // Part 2: profile changes of joined members
0378                 if (e.isRename()) {
0379                     if (!e.newDisplayName()) {
0380                         text = i18nc("their refers to a singular user", "cleared their display name");
0381                     } else {
0382                         text = i18nc("their refers to a singular user",
0383                                      "changed their display name to %1",
0384                                      prettyPrint ? e.newDisplayName()->toHtmlEscaped() : *e.newDisplayName());
0385                     }
0386                 }
0387                 if (e.isAvatarUpdate()) {
0388                     if (!text.isEmpty()) {
0389                         text += i18n(" and ");
0390                     }
0391                     if (!e.newAvatarUrl()) {
0392                         text += i18nc("their refers to a singular user", "cleared their avatar");
0393                     } else if (!e.prevContent()->avatarUrl) {
0394                         text += i18n("set an avatar");
0395                     } else {
0396                         text += i18nc("their refers to a singular user", "updated their avatar");
0397                     }
0398                 }
0399                 if (text.isEmpty()) {
0400                     text = i18nc("<user> changed nothing", "changed nothing");
0401                 }
0402                 return text;
0403             }
0404             case Membership::Leave:
0405                 if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
0406                     return (e.senderId() != e.userId()) ? i18n("withdrew %1's invitation", subjectName) : i18n("rejected the invitation");
0407                 }
0408 
0409                 if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
0410                     return (e.senderId() != e.userId()) ? i18n("unbanned %1", subjectName) : i18n("self-unbanned");
0411                 }
0412                 return (e.senderId() != e.userId())
0413                     ? i18n("has put %1 out of the room: %2", subjectName, e.contentJson()["reason"_ls].toString().toHtmlEscaped())
0414                     : i18n("left the room");
0415             case Membership::Ban:
0416                 if (e.senderId() != e.userId()) {
0417                     if (e.reason().isEmpty()) {
0418                         return i18n("banned %1 from the room", subjectName);
0419                     } else {
0420                         return i18n("banned %1 from the room: %2", subjectName, prettyPrint ? e.reason().toHtmlEscaped() : e.reason());
0421                     }
0422                 } else {
0423                     return i18n("self-banned from the room");
0424                 }
0425             case Membership::Knock: {
0426                 QString reason(e.contentJson()["reason"_ls].toString().toHtmlEscaped());
0427                 return reason.isEmpty() ? i18n("requested an invite") : i18n("requested an invite with reason: %1", reason);
0428             }
0429             default:;
0430             }
0431             return i18n("made something unknown");
0432         },
0433         [](const RoomCanonicalAliasEvent &e) {
0434             return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias to: %1", e.alias());
0435         },
0436         [prettyPrint](const RoomNameEvent &e) {
0437             return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name to: %1", prettyPrint ? e.name().toHtmlEscaped() : e.name());
0438         },
0439         [prettyPrint, stripNewlines](const RoomTopicEvent &e) {
0440             return (e.topic().isEmpty()) ? i18n("cleared the topic")
0441                                          : i18n("set the topic to: %1",
0442                                                 prettyPrint         ? Quotient::prettyPrint(e.topic())
0443                                                     : stripNewlines ? e.topic().replace(u'\n', u' ')
0444                                                                     : e.topic());
0445         },
0446         [](const RoomAvatarEvent &) {
0447             return i18n("changed the room avatar");
0448         },
0449         [](const EncryptionEvent &) {
0450             return i18n("activated End-to-End Encryption");
0451         },
0452         [prettyPrint](const RoomCreateEvent &e) {
0453             return e.isUpgrade()
0454                 ? i18n("upgraded the room to version %1", e.version().isEmpty() ? "1"_ls : (prettyPrint ? e.version().toHtmlEscaped() : e.version()))
0455                 : i18n("created the room, version %1", e.version().isEmpty() ? "1"_ls : (prettyPrint ? e.version().toHtmlEscaped() : e.version()));
0456         },
0457         [](const RoomPowerLevelsEvent &) {
0458             return i18nc("'power level' means permission level", "changed the power levels for this room");
0459         },
0460         [prettyPrint](const StateEvent &e) {
0461             if (e.matrixType() == QLatin1String("m.room.server_acl")) {
0462                 return i18n("changed the server access control lists for this room");
0463             }
0464             if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
0465                 if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
0466                     return i18nc("[User] added <name> widget", "added %1 widget", e.contentJson()["name"_ls].toString());
0467                 }
0468                 if (e.contentJson().isEmpty()) {
0469                     return i18nc("[User] removed <name> widget", "removed %1 widget", e.fullJson()["unsigned"_ls]["prev_content"_ls]["name"_ls].toString());
0470                 }
0471                 return i18nc("[User] configured <name> widget", "configured %1 widget", e.contentJson()["name"_ls].toString());
0472             }
0473             if (e.matrixType() == "org.matrix.msc3672.beacon_info"_ls) {
0474                 return e.contentJson()["description"_ls].toString();
0475             }
0476             return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
0477                                           : i18n("updated %1 state for %2", e.matrixType(), prettyPrint ? e.stateKey().toHtmlEscaped() : e.stateKey());
0478         },
0479         [](const PollStartEvent &e) {
0480             return e.question();
0481         },
0482         i18n("Unknown event"));
0483 }
0484 
0485 QString EventHandler::getMessageBody(const RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const
0486 {
0487     TextHandler textHandler;
0488 
0489     if (event.hasFileContent()) {
0490         auto fileCaption = event.content()->fileInfo()->originalName;
0491         if (fileCaption.isEmpty()) {
0492             fileCaption = event.plainBody();
0493         } else if (event.content()->fileInfo()->originalName != event.plainBody()) {
0494             fileCaption = event.plainBody() + " | "_ls + fileCaption;
0495         }
0496         textHandler.setData(fileCaption);
0497         return !fileCaption.isEmpty() ? textHandler.handleRecievePlainText(Qt::PlainText, stripNewlines) : i18n("a file");
0498     }
0499 
0500     QString body;
0501     if (event.hasTextContent() && event.content()) {
0502         body = static_cast<const MessageEventContent::TextContent *>(event.content())->body;
0503     } else {
0504         body = event.plainBody();
0505     }
0506 
0507     textHandler.setData(body);
0508 
0509     Qt::TextFormat inputFormat;
0510     if (event.mimeType().name() == "text/plain"_ls) {
0511         inputFormat = Qt::PlainText;
0512     } else {
0513         inputFormat = Qt::RichText;
0514     }
0515 
0516     if (format == Qt::RichText) {
0517         return textHandler.handleRecieveRichText(inputFormat, m_room, &event, stripNewlines);
0518     } else {
0519         return textHandler.handleRecievePlainText(inputFormat, stripNewlines);
0520     }
0521 }
0522 
0523 QString EventHandler::getGenericBody() const
0524 {
0525     if (m_event == nullptr) {
0526         qCWarning(EventHandling) << "getGenericBody called with m_event set to nullptr.";
0527         return {};
0528     }
0529     if (m_event->isRedacted()) {
0530         return i18n("<i>[This message was deleted]</i>");
0531     }
0532 
0533     return switchOnType(
0534         *m_event,
0535         [](const RoomMessageEvent &e) {
0536             Q_UNUSED(e)
0537             return i18n("sent a message");
0538         },
0539         [](const StickerEvent &e) {
0540             Q_UNUSED(e)
0541             return i18n("sent a sticker");
0542         },
0543         [](const RoomMemberEvent &e) {
0544             switch (e.membership()) {
0545             case Membership::Invite:
0546                 if (e.repeatsState()) {
0547                     return i18n("reinvited someone to the room");
0548                 }
0549                 Q_FALLTHROUGH();
0550             case Membership::Join: {
0551                 QString text{};
0552                 // Part 1: invites and joins
0553                 if (e.repeatsState()) {
0554                     text = i18n("joined the room (repeated)");
0555                 } else if (e.changesMembership()) {
0556                     text = e.membership() == Membership::Invite ? i18n("invited someone to the room") : i18n("joined the room");
0557                 }
0558                 if (!text.isEmpty()) {
0559                     return text;
0560                 }
0561                 // Part 2: profile changes of joined members
0562                 if (e.isRename()) {
0563                     if (!e.newDisplayName()) {
0564                         text = i18nc("their refers to a singular user", "cleared their display name");
0565                     } else {
0566                         text = i18nc("their refers to a singular user", "changed their display name");
0567                     }
0568                 }
0569                 if (e.isAvatarUpdate()) {
0570                     if (!text.isEmpty()) {
0571                         text += i18n(" and ");
0572                     }
0573                     if (!e.newAvatarUrl()) {
0574                         text += i18nc("their refers to a singular user", "cleared their avatar");
0575                     } else if (!e.prevContent()->avatarUrl) {
0576                         text += i18n("set an avatar");
0577                     } else {
0578                         text += i18nc("their refers to a singular user", "updated their avatar");
0579                     }
0580                 }
0581                 if (text.isEmpty()) {
0582                     text = i18nc("<user> changed nothing", "changed nothing");
0583                 }
0584                 return text;
0585             }
0586             case Membership::Leave:
0587                 if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
0588                     return (e.senderId() != e.userId()) ? i18n("withdrew a user's invitation") : i18n("rejected the invitation");
0589                 }
0590 
0591                 if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
0592                     return (e.senderId() != e.userId()) ? i18n("unbanned a user") : i18n("self-unbanned");
0593                 }
0594                 return (e.senderId() != e.userId()) ? i18n("put a user out of the room") : i18n("left the room");
0595             case Membership::Ban:
0596                 if (e.senderId() != e.userId()) {
0597                     return i18n("banned a user from the room");
0598                 } else {
0599                     return i18n("self-banned from the room");
0600                 }
0601             case Membership::Knock: {
0602                 return i18n("requested an invite");
0603             }
0604             default:;
0605             }
0606             return i18n("made something unknown");
0607         },
0608         [](const RoomCanonicalAliasEvent &e) {
0609             return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias");
0610         },
0611         [](const RoomNameEvent &e) {
0612             return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name");
0613         },
0614         [](const RoomTopicEvent &e) {
0615             return (e.topic().isEmpty()) ? i18n("cleared the topic") : i18n("set the topic");
0616         },
0617         [](const RoomAvatarEvent &) {
0618             return i18n("changed the room avatar");
0619         },
0620         [](const EncryptionEvent &) {
0621             return i18n("activated End-to-End Encryption");
0622         },
0623         [](const RoomCreateEvent &e) {
0624             return e.isUpgrade() ? i18n("upgraded the room version") : i18n("created the room");
0625         },
0626         [](const RoomPowerLevelsEvent &) {
0627             return i18nc("'power level' means permission level", "changed the power levels for this room");
0628         },
0629         [](const StateEvent &e) {
0630             if (e.matrixType() == QLatin1String("m.room.server_acl")) {
0631                 return i18n("changed the server access control lists for this room");
0632             }
0633             if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
0634                 if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
0635                     return i18n("added a widget");
0636                 }
0637                 if (e.contentJson().isEmpty()) {
0638                     return i18n("removed a widget");
0639                 }
0640                 return i18n("configured a widget");
0641             }
0642             return i18n("updated the state");
0643         },
0644         [](const PollStartEvent &e) {
0645             Q_UNUSED(e);
0646             return i18n("started a poll");
0647         },
0648         i18n("Unknown event"));
0649 }
0650 
0651 QString EventHandler::subtitleText() const
0652 {
0653     if (m_event == nullptr) {
0654         qCWarning(EventHandling) << "subtitleText called with m_event set to nullptr.";
0655         return {};
0656     }
0657     return singleLineAuthorDisplayname() + (m_event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": ")) + getPlainBody(true);
0658 }
0659 
0660 QVariantMap EventHandler::getMediaInfo() const
0661 {
0662     if (m_room == nullptr) {
0663         qCWarning(EventHandling) << "getMediaInfo called with m_room set to nullptr.";
0664         return {};
0665     }
0666     if (m_event == nullptr) {
0667         qCWarning(EventHandling) << "getMediaInfo called with m_event set to nullptr.";
0668         return {};
0669     }
0670     return getMediaInfoForEvent(m_event);
0671 }
0672 
0673 QVariantMap EventHandler::getMediaInfoForEvent(const Quotient::RoomEvent *event) const
0674 {
0675     QString eventId = event->id();
0676 
0677     // Get the file info for the event.
0678     const EventContent::FileInfo *fileInfo;
0679     if (event->is<RoomMessageEvent>()) {
0680         auto roomMessageEvent = eventCast<const RoomMessageEvent>(event);
0681         if (!roomMessageEvent->hasFileContent()) {
0682             return {};
0683         }
0684         fileInfo = roomMessageEvent->content()->fileInfo();
0685     } else if (event->is<StickerEvent>()) {
0686         auto stickerEvent = eventCast<const StickerEvent>(event);
0687         fileInfo = &stickerEvent->image();
0688     } else {
0689         return {};
0690     }
0691 
0692     return getMediaInfoFromFileInfo(fileInfo, eventId);
0693 }
0694 
0695 QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail) const
0696 {
0697     QVariantMap mediaInfo;
0698 
0699     // Get the mxc URL for the media.
0700     if (!fileInfo->url().isValid() || fileInfo->url().scheme() != QStringLiteral("mxc") || eventId.isEmpty()) {
0701         mediaInfo["source"_ls] = QUrl();
0702     } else {
0703         QUrl source = m_room->makeMediaUrl(eventId, fileInfo->url());
0704 
0705         if (source.isValid()) {
0706             mediaInfo["source"_ls] = source;
0707         } else {
0708             mediaInfo["source"_ls] = QUrl();
0709         }
0710     }
0711 
0712     auto mimeType = fileInfo->mimeType;
0713     // Add the MIME type for the media if available.
0714     mediaInfo["mimeType"_ls] = mimeType.name();
0715 
0716     // Add the MIME type icon if available.
0717     mediaInfo["mimeIcon"_ls] = mimeType.iconName();
0718 
0719     // Add media size if available.
0720     mediaInfo["size"_ls] = fileInfo->payloadSize;
0721 
0722     // Add parameter depending on media type.
0723     if (mimeType.name().contains(QStringLiteral("image"))) {
0724         if (auto castInfo = static_cast<const EventContent::ImageContent *>(fileInfo)) {
0725             mediaInfo["width"_ls] = castInfo->imageSize.width();
0726             mediaInfo["height"_ls] = castInfo->imageSize.height();
0727 
0728             // TODO: Images in certain formats (e.g. WebP) will be erroneously marked as animated, even if they are static.
0729             mediaInfo["animated"_ls] = QMovie::supportedFormats().contains(mimeType.preferredSuffix().toUtf8());
0730 
0731             if (!isThumbnail) {
0732                 QVariantMap tempInfo;
0733                 auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
0734                 if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
0735                     tempInfo = thumbnailInfo;
0736                 } else {
0737                     QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
0738                     if (blurhash.isEmpty()) {
0739                         tempInfo["source"_ls] = QUrl();
0740                     } else {
0741                         tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
0742                     }
0743                 }
0744                 mediaInfo["tempInfo"_ls] = tempInfo;
0745             }
0746         }
0747     }
0748     if (mimeType.name().contains(QStringLiteral("video"))) {
0749         if (auto castInfo = static_cast<const EventContent::VideoContent *>(fileInfo)) {
0750             mediaInfo["width"_ls] = castInfo->imageSize.width();
0751             mediaInfo["height"_ls] = castInfo->imageSize.height();
0752             mediaInfo["duration"_ls] = castInfo->duration;
0753 
0754             if (!isThumbnail) {
0755                 QVariantMap tempInfo;
0756                 auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
0757                 if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
0758                     tempInfo = thumbnailInfo;
0759                 } else {
0760                     QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
0761                     if (blurhash.isEmpty()) {
0762                         tempInfo["source"_ls] = QUrl();
0763                     } else {
0764                         tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
0765                     }
0766                 }
0767                 mediaInfo["tempInfo"_ls] = tempInfo;
0768             }
0769         }
0770     }
0771     if (mimeType.name().contains(QStringLiteral("audio"))) {
0772         if (auto castInfo = static_cast<const EventContent::AudioContent *>(fileInfo)) {
0773             mediaInfo["duration"_ls] = castInfo->duration;
0774         }
0775     }
0776 
0777     return mediaInfo;
0778 }
0779 
0780 bool EventHandler::hasReply() const
0781 {
0782     if (m_event == nullptr) {
0783         qCWarning(EventHandling) << "hasReply called with m_event set to nullptr.";
0784         return false;
0785     }
0786     return !m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
0787 }
0788 
0789 QString EventHandler::getReplyId() const
0790 {
0791     if (m_event == nullptr) {
0792         qCWarning(EventHandling) << "getReplyId called with m_event set to nullptr.";
0793         return {};
0794     }
0795     return m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
0796 }
0797 
0798 DelegateType::Type EventHandler::getReplyDelegateType() const
0799 {
0800     if (m_room == nullptr) {
0801         qCWarning(EventHandling) << "getReplyDelegateType called with m_room set to nullptr.";
0802         return DelegateType::Other;
0803     }
0804     if (m_event == nullptr) {
0805         qCWarning(EventHandling) << "getReplyDelegateType called with m_event set to nullptr.";
0806         return DelegateType::Other;
0807     }
0808 
0809     auto replyEvent = m_room->getReplyForEvent(*m_event);
0810     if (replyEvent == nullptr) {
0811         return DelegateType::Other;
0812     }
0813     return getDelegateTypeForEvent(replyEvent);
0814 }
0815 
0816 QVariantMap EventHandler::getReplyAuthor() const
0817 {
0818     if (m_room == nullptr) {
0819         qCWarning(EventHandling) << "getReplyAuthor called with m_room set to nullptr.";
0820         return {};
0821     }
0822     // If we have a room we can return an empty user by handing nullptr to m_room->getUser.
0823     if (m_event == nullptr) {
0824         qCWarning(EventHandling) << "getReplyAuthor called with m_event set to nullptr. Returning empty user.";
0825         return m_room->getUser(nullptr);
0826     }
0827 
0828     auto replyPtr = m_room->getReplyForEvent(*m_event);
0829 
0830     if (replyPtr) {
0831         auto replyUser = m_room->user(replyPtr->senderId());
0832         return m_room->getUser(replyUser);
0833     } else {
0834         return m_room->getUser(nullptr);
0835     }
0836 }
0837 
0838 QString EventHandler::getReplyRichBody(bool stripNewlines) const
0839 {
0840     if (m_room == nullptr) {
0841         qCWarning(EventHandling) << "getReplyRichBody called with m_room set to nullptr.";
0842         return {};
0843     }
0844     if (m_event == nullptr) {
0845         qCWarning(EventHandling) << "getReplyRichBody called with m_event set to nullptr.";
0846         return {};
0847     }
0848 
0849     auto replyEvent = m_room->getReplyForEvent(*m_event);
0850     if (replyEvent == nullptr) {
0851         return {};
0852     }
0853 
0854     return getBody(replyEvent, Qt::RichText, stripNewlines);
0855 }
0856 
0857 QString EventHandler::getReplyPlainBody(bool stripNewlines) const
0858 {
0859     if (m_room == nullptr) {
0860         qCWarning(EventHandling) << "getReplyPlainBody called with m_room set to nullptr.";
0861         return {};
0862     }
0863     if (m_event == nullptr) {
0864         qCWarning(EventHandling) << "getReplyPlainBody called with m_event set to nullptr.";
0865         return {};
0866     }
0867 
0868     auto replyEvent = m_room->getReplyForEvent(*m_event);
0869     if (replyEvent == nullptr) {
0870         return {};
0871     }
0872 
0873     return getBody(replyEvent, Qt::PlainText, stripNewlines);
0874 }
0875 
0876 QVariantMap EventHandler::getReplyMediaInfo() const
0877 {
0878     if (m_room == nullptr) {
0879         qCWarning(EventHandling) << "getReplyMediaInfo called with m_room set to nullptr.";
0880         return {};
0881     }
0882     if (m_event == nullptr) {
0883         qCWarning(EventHandling) << "getReplyMediaInfo called with m_event set to nullptr.";
0884         return {};
0885     }
0886 
0887     auto replyPtr = m_room->getReplyForEvent(*m_event);
0888     if (!replyPtr) {
0889         return {};
0890     }
0891     return getMediaInfoForEvent(replyPtr);
0892 }
0893 
0894 bool EventHandler::isThreaded() const
0895 {
0896     if (m_event == nullptr) {
0897         qCWarning(EventHandling) << "isThreaded called with m_event set to nullptr.";
0898         return false;
0899     }
0900 
0901     return (m_event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
0902             && m_event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls)
0903         || (!m_event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && m_event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls));
0904 }
0905 
0906 QString EventHandler::threadRoot() const
0907 {
0908     if (m_event == nullptr) {
0909         qCWarning(EventHandling) << "threadRoot called with m_event set to nullptr.";
0910         return {};
0911     }
0912 
0913     // Get the thread root ID from m.relates_to if it exists.
0914     if (m_event->contentPart<QJsonObject>("m.relates_to"_ls).contains("rel_type"_ls)
0915         && m_event->contentPart<QJsonObject>("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls) {
0916         return m_event->contentPart<QJsonObject>("m.relates_to"_ls)["event_id"_ls].toString();
0917     }
0918     // For thread root events they have an m.relations in the unsigned part with a m.thread object.
0919     // If so return the event ID as it is the root.
0920     if (!m_event->unsignedPart<QJsonObject>("m.relations"_ls).isEmpty() && m_event->unsignedPart<QJsonObject>("m.relations"_ls).contains("m.thread"_ls)) {
0921         return getId();
0922     }
0923     return {};
0924 }
0925 
0926 float EventHandler::getLatitude() const
0927 {
0928     if (m_event == nullptr) {
0929         qCWarning(EventHandling) << "getLatitude called with m_event set to nullptr.";
0930         return -100.0;
0931     }
0932 
0933     const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
0934     if (geoUri.isEmpty()) {
0935         return -100.0; // latitude runs from -90deg to +90deg so -100 is out of range.
0936     }
0937     const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[0];
0938     return latitude.toFloat();
0939 }
0940 
0941 float EventHandler::getLongitude() const
0942 {
0943     if (m_event == nullptr) {
0944         qCWarning(EventHandling) << "getLongitude called with m_event set to nullptr.";
0945         return -200.0;
0946     }
0947 
0948     const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
0949     if (geoUri.isEmpty()) {
0950         return -200.0; // longitude runs from -180deg to +180deg so -200 is out of range.
0951     }
0952     const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[1];
0953     return latitude.toFloat();
0954 }
0955 
0956 QString EventHandler::getLocationAssetType() const
0957 {
0958     if (m_event == nullptr) {
0959         qCWarning(EventHandling) << "getLocationAssetType called with m_event set to nullptr.";
0960         return {};
0961     }
0962 
0963     const auto assetType = m_event->contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
0964     if (assetType.isEmpty()) {
0965         return {};
0966     }
0967     return assetType;
0968 }
0969 
0970 bool EventHandler::hasReadMarkers() const
0971 {
0972     if (m_room == nullptr) {
0973         qCWarning(EventHandling) << "hasReadMarkers called with m_room set to nullptr.";
0974         return false;
0975     }
0976     if (m_event == nullptr) {
0977         qCWarning(EventHandling) << "hasReadMarkers called with m_event set to nullptr.";
0978         return false;
0979     }
0980 
0981     auto userIds = m_room->userIdsAtEvent(m_event->id());
0982     userIds.remove(m_room->localUser()->id());
0983     return userIds.size() > 0;
0984 }
0985 
0986 QVariantList EventHandler::getReadMarkers(int maxMarkers) const
0987 {
0988     if (m_room == nullptr) {
0989         qCWarning(EventHandling) << "getReadMarkers called with m_room set to nullptr.";
0990         return {};
0991     }
0992     if (m_event == nullptr) {
0993         qCWarning(EventHandling) << "getReadMarkers called with m_event set to nullptr.";
0994         return {};
0995     }
0996 
0997     auto userIds_temp = m_room->userIdsAtEvent(m_event->id());
0998     userIds_temp.remove(m_room->localUser()->id());
0999 
1000     auto userIds = userIds_temp.values();
1001     if (userIds.count() > maxMarkers) {
1002         userIds = userIds.mid(0, maxMarkers);
1003     }
1004 
1005     QVariantList users;
1006     users.reserve(userIds.size());
1007     for (const auto &userId : userIds) {
1008         auto user = m_room->user(userId);
1009         users += m_room->getUser(user);
1010     }
1011 
1012     return users;
1013 }
1014 
1015 QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const
1016 {
1017     if (m_room == nullptr) {
1018         qCWarning(EventHandling) << "getNumberExcessReadMarkers called with m_room set to nullptr.";
1019         return {};
1020     }
1021     if (m_event == nullptr) {
1022         qCWarning(EventHandling) << "getNumberExcessReadMarkers called with m_event set to nullptr.";
1023         return {};
1024     }
1025 
1026     auto userIds = m_room->userIdsAtEvent(m_event->id());
1027     userIds.remove(m_room->localUser()->id());
1028 
1029     if (userIds.count() > maxMarkers) {
1030         return QStringLiteral("+ ") + QString::number(userIds.count() - maxMarkers);
1031     } else {
1032         return QString();
1033     }
1034 }
1035 
1036 QString EventHandler::getReadMarkersString() const
1037 {
1038     if (m_room == nullptr) {
1039         qCWarning(EventHandling) << "getReadMarkersString called with m_room set to nullptr.";
1040         return {};
1041     }
1042     if (m_event == nullptr) {
1043         qCWarning(EventHandling) << "getReadMarkersString called with m_event set to nullptr.";
1044         return {};
1045     }
1046 
1047     auto userIds = m_room->userIdsAtEvent(m_event->id());
1048     userIds.remove(m_room->localUser()->id());
1049 
1050     /**
1051      * The string ends up in the form
1052      * "x users: user1DisplayName, user2DisplayName, etc."
1053      */
1054     QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
1055     for (const auto &userId : userIds) {
1056         auto user = m_room->user(userId);
1057         readMarkersString += user->displayname(m_room) + i18nc("list separator", ", ");
1058     }
1059     readMarkersString.chop(2);
1060     return readMarkersString;
1061 }
1062 
1063 #include "moc_eventhandler.cpp"