aboutsummaryrefslogtreecommitdiff
path: root/room.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'room.cpp')
-rw-r--r--room.cpp170
1 files changed, 142 insertions, 28 deletions
diff --git a/room.cpp b/room.cpp
index 764f4d23..48486ab8 100644
--- a/room.cpp
+++ b/room.cpp
@@ -29,6 +29,7 @@
#include "events/roommemberevent.h"
#include "events/typingevent.h"
#include "events/receiptevent.h"
+#include "events/redactionevent.h"
#include "jobs/sendeventjob.h"
#include "jobs/roommessagesjob.h"
#include "avatar.h"
@@ -99,8 +100,9 @@ class Room::Private
bool isEventNotable(const RoomEvent* e) const
{
- return e->senderId() != connection->userId() &&
- e->type() == EventType::RoomMessage;
+ return !e->isRedacted() &&
+ e->senderId() != connection->userId() &&
+ e->type() == EventType::RoomMessage;
}
void appendEvent(RoomEvent* e)
@@ -118,11 +120,14 @@ class Room::Private
* Removes events from the passed container that are already in the timeline
*/
void dropDuplicateEvents(RoomEvents* events) const;
+ void checkUnreadMessages(RoomEventsView events);
void setLastReadEvent(User* u, const QString& eventId);
rev_iter_pair_t promoteReadMarker(User* u, rev_iter_t newMarker,
bool force = false);
+ void processRedaction(const RedactionEvent* redaction);
+
QJsonObject toJson() const;
private:
@@ -677,6 +682,83 @@ void Room::Private::dropDuplicateEvents(RoomEvents* events) const
events->erase(dupsBegin, events->end());
}
+void Room::Private::processRedaction(const RedactionEvent* redaction)
+{
+ Q_ASSERT(redaction && redaction->type() == EventType::Redaction);
+
+ const auto pIdx = eventsIndex.find(redaction->redactedEvent());
+ if (pIdx == eventsIndex.end())
+ {
+ qCDebug(MAIN) << "Redaction" << redaction->id()
+ << "ignored: target event not found";
+ return; // If the target events comes later, it comes already redacted.
+ }
+ Q_ASSERT(q->isValidIndex(*pIdx));
+
+ auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())];
+
+ // Apply the redaction procedure from chapter 6.5 of The Spec
+ auto originalJson = ti->originalJsonObject();
+ if (originalJson.value("unsigned").toObject()
+ .value("redacted_because").toObject()
+ .value("event_id") == redaction->id())
+ {
+ qCDebug(MAIN) << "Redaction" << redaction->id()
+ << "of event" << ti.event()->id() << "already done, skipping";
+ return;
+ }
+ static const QStringList keepKeys =
+ { "event_id", "type", "room_id", "sender", "state_key",
+ "prev_content", "content" };
+ static const
+ std::vector<std::pair<EventType, QStringList>> keepContentKeysMap
+ { { Event::Type::RoomMember, { "membership" } }
+ , { Event::Type::RoomCreate, { "creator" } }
+ , { Event::Type::RoomJoinRules, { "join_rule" } }
+ , { Event::Type::RoomPowerLevels,
+ { "ban", "events", "events_default", "kick", "redact",
+ "state_default", "users", "users_default" } }
+ , { Event::Type::RoomAliases, { "alias" } }
+ };
+ for (auto it = originalJson.begin(); it != originalJson.end();)
+ {
+ if (!keepKeys.contains(it.key()))
+ it = originalJson.erase(it); // TODO: shred the value
+ else
+ ++it;
+ }
+ auto keepContentKeys =
+ find_if(keepContentKeysMap.begin(), keepContentKeysMap.end(),
+ [&](const std::pair<EventType,QStringList>& t)
+ { return ti->type() == t.first; } );
+ if (keepContentKeys == keepContentKeysMap.end())
+ {
+ originalJson.remove("content");
+ originalJson.remove("prev_content");
+ } else {
+ auto content = originalJson.take("content").toObject();
+ for (auto it = content.begin(); it != content.end(); )
+ {
+ if (!keepContentKeys->second.contains(it.key()))
+ it = content.erase(it);
+ else
+ ++it;
+ }
+ originalJson.insert("content", content);
+ }
+ auto unsignedData = originalJson.take("unsigned").toObject();
+ unsignedData["redacted_because"] = redaction->originalJsonObject();
+ originalJson.insert("unsigned", unsignedData);
+
+ // Make a new event from the redacted JSON, exchange events,
+ // notify everyone and delete the old event
+ auto oldEvent = ti.replaceEvent(RoomEvent::fromJson(originalJson));
+ q->onRedaction(oldEvent, ti);
+ qCDebug(MAIN) << "Redacted" << oldEvent->id() << "with" << redaction->id();
+ emit q->replacedEvent(oldEvent, ti.event());
+ delete oldEvent;
+}
+
Connection* Room::connection() const
{
Q_ASSERT(d->connection);
@@ -688,62 +770,94 @@ User* Room::localUser() const
return connection()->user();
}
+inline bool isRedaction(Event* e)
+{
+ return e->type() == EventType::Redaction;
+}
+
void Room::addNewMessageEvents(RoomEvents events)
{
+ auto timelineSize = d->timeline.size();
+
d->dropDuplicateEvents(&events);
- if (events.empty())
- return;
- emit aboutToAddNewMessages(events);
- doAddNewMessageEvents(events);
- emit addedMessages();
+ // We want to process redactions in the order of arrival (covering the
+ // case of one redaction superseding another one), hence stable partition.
+ const auto normalsBegin =
+ std::stable_partition(events.begin(), events.end(), isRedaction);
+ RoomEventsView redactions { events.begin(), normalsBegin },
+ normalEvents { normalsBegin, events.end() };
+ if (!normalEvents.empty())
+ {
+ emit aboutToAddNewMessages(normalEvents);
+ doAddNewMessageEvents(normalEvents);
+ }
+ for (auto* r: redactions)
+ d->processRedaction(static_cast<const RedactionEvent*>(r));
+ if (!normalEvents.empty())
+ {
+ d->checkUnreadMessages(normalEvents);
+ emit addedMessages();
+ }
+
+ Q_ASSERT(d->timeline.size() == timelineSize + normalEvents.size());
}
-void Room::doAddNewMessageEvents(const RoomEvents& events)
+void Room::doAddNewMessageEvents(RoomEventsView events)
{
Q_ASSERT(!events.empty());
-
- Timeline::size_type newUnreadMessages = 0;
for (auto e: events)
- {
d->appendEvent(e);
- newUnreadMessages += d->isEventNotable(e);
- }
- qCDebug(MAIN) << "Room" << displayName() << "received" << events.size()
- << "(with" << newUnreadMessages << "notable)"
- << "new events; the last event is now" << d->timeline.back();
+ qCDebug(MAIN)
+ << "Room" << displayName() << "received" << events.size()
+ << "new events; the last event is now" << d->timeline.back();
+}
+
+void Room::Private::checkUnreadMessages(RoomEventsView events)
+{
+ auto newUnreadMessages =
+ count_if(events.from, events.to,
+ [=] (const RoomEvent* e) { return isEventNotable(e); });
// The first event in the batch defines whose read marker can possibly be
// promoted any further over the same author's events newly arrived.
// Others will need explicit read receipts from the server (or, for
// the local user, markMessagesAsRead() invocation) to promote their
// read markers over the new message events.
- User* firstWriter = connection()->user(events.front()->senderId());
- if (readMarker(firstWriter) != timelineEdge())
+ User* firstWriter = connection->user((*events.from)->senderId());
+ if (q->readMarker(firstWriter) != timeline.crend())
{
- d->promoteReadMarker(firstWriter, findInTimeline(events.front()->id()));
+ promoteReadMarker(firstWriter, q->findInTimeline((*events.from)->id()));
qCDebug(MAIN) << "Auto-promoted read marker for" << firstWriter->id()
- << "to" << *readMarker(firstWriter);
+ << "to" << *q->readMarker(firstWriter);
}
- if( !d->unreadMessages && newUnreadMessages > 0)
+ if(!unreadMessages && newUnreadMessages > 0)
{
- d->unreadMessages = true;
- emit unreadMessagesChanged(this);
- qCDebug(MAIN) << "Room" << displayName() << "has unread messages";
+ unreadMessages = true;
+ emit q->unreadMessagesChanged(q);
+ qCDebug(MAIN) << "Room" << displayname << "has unread messages";
}
}
void Room::addHistoricalMessageEvents(RoomEvents events)
{
+ auto timelineSize = d->timeline.size();
+
d->dropDuplicateEvents(&events);
- if (events.empty())
+ auto redactionsBegin =
+ std::remove_if(events.begin(), events.end(), isRedaction);
+ RoomEventsView normalEvents { events.begin(), redactionsBegin };
+ if (normalEvents.empty())
return;
- emit aboutToAddHistoricalMessages(events);
- doAddHistoricalMessageEvents(events);
+
+ emit aboutToAddHistoricalMessages(normalEvents);
+ doAddHistoricalMessageEvents(normalEvents);
emit addedMessages();
+
+ Q_ASSERT(d->timeline.size() == timelineSize + normalEvents.size());
}
-void Room::doAddHistoricalMessageEvents(const RoomEvents& events)
+void Room::doAddHistoricalMessageEvents(RoomEventsView events)
{
Q_ASSERT(!events.empty());