Skip to content

Commit

Permalink
Support multiple event ids in RedactionEvent (MSC2244)
Browse files Browse the repository at this point in the history
Cross-ref: matrix-org/matrix-spec-proposals#2244.
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() - it still only gets a single event id, pending
MSC4084 (and, respectively, implementation of UIA in the library).
  • Loading branch information
KitsuneRal authored and Kitsune Ral committed Mar 9, 2024
1 parent 8d1edd5 commit da5ab4c
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 62 deletions.
11 changes: 11 additions & 0 deletions Quotient/events/redactionevent.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<QStringList>(evtIdJson); // MSC2244: a list of ids
if (evtIdJson.isString())
return { fromJson<QString>(evtIdJson) }; // MSC2174: id in content
return { fullJson()["redacts"_ls].toString() }; // legacy fallback
}

QUO_CONTENT_GETTER(QString, reason)
};
} // namespace Quotient
134 changes: 72 additions & 62 deletions Quotient/room.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -2836,59 +2835,67 @@ RoomEventPtr makeRedacted(const RoomEvent& target,
return loadEvent<RoomEvent>(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<RoomMessageEvent>())
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<ReactionEvent>(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<RoomMessageEvent>())
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<ReactionEvent>(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
Expand Down Expand Up @@ -3011,19 +3018,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<RedactionEvent>(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<RoomMessageEvent>(eptr);
msg && !msg->replacedEvent().isEmpty()) {
Expand Down

0 comments on commit da5ab4c

Please sign in to comment.