aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKitsune Ral <Kitsune-Ral@users.sf.net>2017-12-10 16:10:54 +0900
committerKitsune Ral <Kitsune-Ral@users.sf.net>2017-12-10 16:22:26 +0900
commitdf7f9bd2c26ca67fb3d221f9d3a68e34fb25a235 (patch)
treea9b4609a85c310c7b270b8f703b84ab3ec992899
parent6ecdefd8463c0b393dc51535ddb4b4283b5891d8 (diff)
downloadlibquotient-df7f9bd2c26ca67fb3d221f9d3a68e34fb25a235.tar.gz
libquotient-df7f9bd2c26ca67fb3d221f9d3a68e34fb25a235.zip
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.
-rw-r--r--examples/qmc-example.cpp22
-rw-r--r--room.cpp170
-rw-r--r--room.h22
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<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());
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<RoomEvent> 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;