From e5786f240d317fdf9e46b4979a3b0a625477d49e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 10 Nov 2019 19:29:08 +0900 Subject: [PATCH] Support event id(s) in RedactionEvent content (MSC2244) This implements MSC2174 and MSC2244. The implementation does not validate the redaction event contents against a particular room version, accepting both singular event ids and lists of ids in any room. (In fact, Quotient cannot create objects of different classes for the same event type depending on the room version - see #362.) Also, this commit doesn't change Room::redactEvent() - that still gets a single event id, pending MSC4084 (and, respectively, implementation of UIA in the library). --- Quotient/events/redactionevent.h | 11 +++ Quotient/room.cpp | 134 +++++++++++++++++-------------- 2 files changed, 83 insertions(+), 62 deletions(-) diff --git a/Quotient/events/redactionevent.h b/Quotient/events/redactionevent.h index a2e0b73b5..2830dd947 100644 --- a/Quotient/events/redactionevent.h +++ b/Quotient/events/redactionevent.h @@ -12,10 +12,21 @@ class QUOTIENT_API RedactionEvent : public RoomEvent { using RoomEvent::RoomEvent; + [[deprecated("Use redactedEvents() instead")]] QString redactedEvent() const { return fullJson()["redacts"_ls].toString(); } + QStringList redactedEvents() const + { + const auto evtIdJson = contentJson()["redacts"_ls]; + if (evtIdJson.isArray()) + return fromJson(evtIdJson); // MSC2244: a list of ids + if (evtIdJson.isString()) + return { fromJson(evtIdJson) }; // MSC2174: id in content + return { fullJson()["redacts"_ls].toString() }; // legacy fallback + } + QUO_CONTENT_GETTER(QString, reason) }; } // namespace Quotient diff --git a/Quotient/room.cpp b/Quotient/room.cpp index 6b27fa3ad..422594b48 100644 --- a/Quotient/room.cpp +++ b/Quotient/room.cpp @@ -343,11 +343,10 @@ class Q_DECL_HIDDEN Room::Private { /*! Apply redaction to the timeline * - * Tries to find an event in the timeline and redact it; deletes the - * redaction event whether the redacted event was found or not. - * \return true if the event has been found and redacted; false otherwise + * Tries to find events in the timeline and redact them. + * \return the list of event ids that were NOT found and redacted */ - bool processRedaction(const RedactionEvent& redaction); + QStringList processRedaction(const RedactionEvent& redaction); /*! Apply a new revision of the event to the timeline * @@ -2846,59 +2845,67 @@ RoomEventPtr makeRedacted(const RoomEvent& target, return loadEvent(originalJson); } -bool Room::Private::processRedaction(const RedactionEvent& redaction) +QStringList Room::Private::processRedaction(const RedactionEvent& redaction) { + QStringList unredactedIds; // Can't use findInTimeline because it returns a const iterator, and // we need to change the underlying TimelineItem. - const auto pIdx = eventsIndex.constFind(redaction.redactedEvent()); - if (pIdx == eventsIndex.cend()) - return false; - - Q_ASSERT(q->isValidIndex(*pIdx)); + const auto& eventIds = redaction.redactedEvents(); + for (const auto& evtId: eventIds) { + const auto pIdx = eventsIndex.constFind(evtId); + if (pIdx == eventsIndex.cend()) { + unredactedIds.push_back(evtId); + continue; + } - auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; - if (ti->isRedacted() && ti->redactedBecause()->id() == redaction.id()) { - qCDebug(EVENTS) << "Redaction" << redaction.id() << "of event" - << ti->id() << "already done, skipping"; - return true; - } - if (ti->is()) - FileMetadataMap::remove(id, ti->id()); + Q_ASSERT(q->isValidIndex(*pIdx)); - // Make a new event from the redacted JSON and put it in the timeline - // instead of the redacted one. oldEvent will be deleted on return. - auto oldEvent = ti.replaceEvent(makeRedacted(*ti, redaction)); - qCDebug(EVENTS) << "Redacted" << oldEvent->id() << "with" << redaction.id(); - if (oldEvent->isStateEvent()) { - // Check whether the old event was a part of current state; if it was, - // update the current state to the redacted event object. - const auto currentStateEvt = - currentState.get(oldEvent->matrixType(), oldEvent->stateKey()); - Q_ASSERT(currentStateEvt); - if (currentStateEvt == oldEvent.get()) { - // Historical states can't be in currentState - Q_ASSERT(ti.index() >= 0); - qCDebug(STATE).nospace() - << "Redacting state " << oldEvent->matrixType() << "/" - << oldEvent->stateKey(); - // Retarget the current state to the newly made event. - if (q->processStateEvent(*ti)) - emit q->namesChanged(q); - updateDisplayname(); + auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; + if (ti->isRedacted() && ti->redactedBecause()->id() == redaction.id()) { + qCDebug(EVENTS) << "Redaction" << redaction.id() << "of event" + << ti->id() << "already done, skipping"; + continue; } - } - if (const auto* reaction = eventCast(oldEvent)) { - const auto& content = reaction->content().value; - const std::pair lookupKey { content.eventId, content.type }; - if (relations.contains(lookupKey)) { - relations[lookupKey].removeOne(reaction); - emit q->updatedEvent(content.eventId); + if (ti->is()) + FileMetadataMap::remove(id, ti->id()); + + + // Make a new event from the redacted JSON and put it in the timeline + // instead of the redacted one. oldEvent will be deleted on return. + auto oldEvent = ti.replaceEvent(makeRedacted(*ti, redaction)); + qCDebug(EVENTS) << "Redacted" << oldEvent->id() << "with" + << redaction.id(); + if (oldEvent->isStateEvent()) { + // Check whether the old event was a part of current state; if it was, + // update the current state to the redacted event object. + const auto currentStateEvt = + currentState.get(oldEvent->matrixType(), oldEvent->stateKey()); + Q_ASSERT(currentStateEvt); + if (currentStateEvt == oldEvent.get()) { + // Historical states can't be in currentState + Q_ASSERT(ti.index() >= 0); + qCDebug(STATE).nospace() + << "Redacting state " << oldEvent->matrixType() << "/" + << oldEvent->stateKey(); + // Retarget the current state to the newly made event. + if (q->processStateEvent(*ti)) + emit q->namesChanged(q); + updateDisplayname(); + } + } + if (const auto* reaction = eventCast(oldEvent)) { + const auto& content = reaction->content().value; + const std::pair lookupKey { content.eventId, content.type }; + if (relations.contains(lookupKey)) { + relations[lookupKey].removeOne(reaction); + emit q->updatedEvent(content.eventId); + } } + q->onRedaction(*oldEvent, *ti); + emit q->replacedEvent(ti.event(), std::to_address(oldEvent)); + // By now, all references to oldEvent must have been updated to ti.event() } - q->onRedaction(*oldEvent, *ti); - emit q->replacedEvent(ti.event(), std::to_address(oldEvent)); - // By now, all references to oldEvent must have been updated to ti.event() - return true; + return unredactedIds; } /** Make a replaced event @@ -3021,19 +3028,22 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) auto it = std::find_if(events.begin(), events.end(), isEditing); for (const auto& eptr : RoomEventsRange(it, events.end())) { if (auto* r = eventCast(eptr)) { - // Try to find the target in the timeline, then in the batch. - if (processRedaction(*r)) - continue; - if (auto targetIt = std::find_if(events.begin(), events.end(), - [id = r->redactedEvent()](const RoomEventPtr& ep) { - return ep->id() == id; - }); targetIt != events.end()) - *targetIt = makeRedacted(**targetIt, *r); - else - qCDebug(STATE) - << "Redaction" << r->id() << "ignored: target event" - << r->redactedEvent() << "is not found"; - // If the target event comes later, it comes already redacted. + // Try to find the targets in the timeline, then in the batch. + const auto unredactedIds = processRedaction(*r); + for (const auto& idToRedact: unredactedIds) { + if (auto targetIt = + std::find_if(events.begin(), it, + [&idToRedact](const RoomEventPtr& ep) { + return ep->id() == idToRedact; + }); + targetIt != it) + *targetIt = makeRedacted(**targetIt, *r); + else + qCDebug(EVENTS) + << "Target event" << idToRedact << "in redaction" + << r->id() << "is not found"; + // If the target event comes later, it comes already redacted. + } } if (auto* msg = eventCast(eptr); msg && !msg->replacedEvent().isEmpty()) {