From df7f9bd2c26ca67fb3d221f9d3a68e34fb25a235 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 10 Dec 2017 16:10:54 +0900 Subject: Process incoming redactions This only applies to new messages; historical redaction events are just skipped because historical events are already redacted on the server side. Closes #117. --- examples/qmc-example.cpp | 22 +++--- room.cpp | 170 +++++++++++++++++++++++++++++++++++++++-------- room.h | 22 ++++-- 3 files changed, 173 insertions(+), 41 deletions(-) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index dc0c94e4..62ae8310 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -15,15 +15,21 @@ void onNewRoom(Room* r) { cout << "New room: " << r->id().toStdString() << endl; QObject::connect(r, &Room::namesChanged, [=] { - cout << "Room " << r->id().toStdString() << ", name(s) changed:" << endl; - cout << " Name: " << r->name().toStdString() << endl; - cout << " Canonical alias: " << r->canonicalAlias().toStdString() << endl; + cout << "Room " << r->id().toStdString() << ", name(s) changed:" << endl + << " Name: " << r->name().toStdString() << endl + << " Canonical alias: " << r->canonicalAlias().toStdString() + << endl << endl; }); - QObject::connect(r, &Room::aboutToAddNewMessages, [=] (RoomEvents evs) { - cout << "New events in room " << r->id().toStdString() << ":" << endl; - for (auto e: evs) + QObject::connect(r, &Room::aboutToAddNewMessages, [=] (RoomEventsView events) { + cout << events.size() << " new event(s) in room " + << r->id().toStdString() << ":" << endl; + for (auto e: events) { - cout << string(e->originalJson()) << endl; + cout << "From: " + << r->roomMembername(e->senderId()).toStdString() + << endl << "Timestamp:" + << e->timestamp().toString().toStdString() << endl + << "JSON:" << endl << e->originalJson().toStdString() << endl; } }); } @@ -36,7 +42,7 @@ int main(int argc, char* argv[]) auto conn = new Connection(QUrl("https://matrix.org")); conn->connectToServer(argv[1], argv[2], "QMatrixClient example application"); - QObject::connect(conn, &Connection::connected, [=] { + auto c = QObject::connect(conn, &Connection::connected, [=] { cout << "Connected" << endl; conn->sync(); }); 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> 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& 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(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()); diff --git a/room.h b/room.h index 702c22f3..bef66b86 100644 --- a/room.h +++ b/room.h @@ -38,6 +38,8 @@ namespace QMatrixClient class User; class MemberSorter; class LeaveRoomJob; + class RedactEventJob; + class Room; class TimelineItem { @@ -49,9 +51,17 @@ namespace QMatrixClient TimelineItem(RoomEvent* e, index_t number) : evt(e), idx(number) { } RoomEvent* event() const { return evt.get(); } - RoomEvent* operator->() const { return event(); } //< Synonym for event() + RoomEvent* operator->() const { return event(); } //< Synonym for event()-> index_t index() const { return idx; } + // Used for event redaction + RoomEvent* replaceEvent(RoomEvent* other) + { + auto* old = evt.release(); + evt.reset(other); + return old; + } + private: std::unique_ptr evt; index_t idx; @@ -176,8 +186,8 @@ namespace QMatrixClient void markAllMessagesAsRead(); signals: - void aboutToAddHistoricalMessages(const RoomEvents& events); - void aboutToAddNewMessages(const RoomEvents& events); + void aboutToAddHistoricalMessages(RoomEventsView events); + void aboutToAddNewMessages(RoomEventsView events); void addedMessages(); /** @@ -200,12 +210,14 @@ namespace QMatrixClient void lastReadEventChanged(User* user); void readMarkerMoved(); void unreadMessagesChanged(Room* room); + void replacedEvent(RoomEvent* before, RoomEvent* after); protected: - virtual void doAddNewMessageEvents(const RoomEvents& events); - virtual void doAddHistoricalMessageEvents(const RoomEvents& events); + virtual void doAddNewMessageEvents(RoomEventsView events); + virtual void doAddHistoricalMessageEvents(RoomEventsView events); virtual void processStateEvents(const RoomEvents& events); virtual void processEphemeralEvent(Event* event); + virtual void onRedaction(RoomEvent*, TimelineItem&) { } private: class Private; -- cgit v1.2.3