File indexing completed on 2024-12-08 04:32:55

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