File indexing completed on 2024-10-06 07:36:06
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"