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"