aboutsummaryrefslogtreecommitdiff
path: root/lib/room.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'lib/room.cpp')
-rw-r--r--lib/room.cpp273
1 files changed, 245 insertions, 28 deletions
diff --git a/lib/room.cpp b/lib/room.cpp
index 045cc7bc..cf58f3c0 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -21,6 +21,7 @@
#include "avatar.h"
#include "connection.h"
#include "converters.h"
+#include "e2ee.h"
#include "syncdata.h"
#include "user.h"
@@ -42,6 +43,7 @@
#include "events/callhangupevent.h"
#include "events/callinviteevent.h"
#include "events/encryptionevent.h"
+#include "events/reactionevent.h"
#include "events/receiptevent.h"
#include "events/redactionevent.h"
#include "events/roomavatarevent.h"
@@ -65,6 +67,9 @@
#include <array>
#include <cmath>
#include <functional>
+#include <groupsession.h> // QtOlm
+#include <message.h> // QtOlm
+#include <session.h> // QtOlm
using namespace QMatrixClient;
using namespace std::placeholders;
@@ -114,7 +119,10 @@ public:
Timeline timeline;
PendingEvents unsyncedEvents;
QHash<QString, TimelineItem::index_t> eventsIndex;
-
+ // A map from evtId to a map of relation type to a vector of event
+ // pointers. Not using QMultiHash, because we want to quickly return
+ // a number of relations for a given event without enumerating them.
+ QHash<QPair<QString, QString>, RelatedEvents> relations;
QString displayname;
Avatar avatar;
int highlightCount = 0;
@@ -314,14 +322,22 @@ public:
return requestSetState(EvT(std::forward<ArgTs>(args)...));
}
- /**
- * @brief Apply redaction to the timeline
+ /*! 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
*/
bool processRedaction(const RedactionEvent& redaction);
+ /*! Apply a new revision of the event to the timeline
+ *
+ * Tries to find an event in the timeline and replace it with the new
+ * content passed in \p newMessage.
+ * \return true if the event has been found and replaced; false otherwise
+ */
+ bool processReplacement(const RoomMessageEvent& newMessage);
+
void setTags(TagsMap newTags);
QJsonObject toJson() const;
@@ -679,10 +695,10 @@ Room::rev_iter_t Room::findInTimeline(const QString& evtId) const
{
if (!d->timeline.empty() && d->eventsIndex.contains(evtId)) {
auto it = findInTimeline(d->eventsIndex.value(evtId));
- Q_ASSERT((*it)->id() == evtId);
+ Q_ASSERT(it != historyEdge() && (*it)->id() == evtId);
return it;
}
- return timelineEdge();
+ return historyEdge();
}
Room::PendingEvents::iterator Room::findPendingEvent(const QString& txnId)
@@ -702,6 +718,18 @@ Room::findPendingEvent(const QString& txnId) const
});
}
+const Room::RelatedEvents Room::relatedEvents(const QString& evtId,
+ const char* relType) const
+{
+ return d->relations.value({ evtId, relType });
+}
+
+const Room::RelatedEvents Room::relatedEvents(const RoomEvent& evt,
+ const char* relType) const
+{
+ return relatedEvents(evt.id(), relType);
+}
+
void Room::Private::getAllMembers()
{
// If already loaded or already loading, there's nothing to do here.
@@ -1094,6 +1122,92 @@ bool Room::usesEncryption() const
return !d->getCurrentState<EncryptionEvent>()->algorithm().isEmpty();
}
+const RoomEvent* Room::decryptMessage(EncryptedEvent* encryptedEvent) const
+{
+ if (encryptedEvent->algorithm() == OlmV1Curve25519AesSha2AlgoKey) {
+ QString identityKey = connection()->olmAccount()->curve25519IdentityKey();
+ QJsonObject personalCipherObject =
+ encryptedEvent->ciphertext(identityKey);
+ if (personalCipherObject.isEmpty()) {
+ qCDebug(EVENTS) << "Encrypted event is not for the current device";
+ return nullptr;
+ }
+ return makeEvent<RoomMessageEvent>(
+ decryptMessage(personalCipherObject,
+ encryptedEvent->senderKey().toLatin1()))
+ .get();
+ }
+ if (encryptedEvent->algorithm() == MegolmV1AesSha2AlgoKey) {
+ return makeEvent<RoomMessageEvent>(
+ decryptMessage(encryptedEvent->ciphertext(),
+ encryptedEvent->senderKey(),
+ encryptedEvent->deviceId(),
+ encryptedEvent->sessionId()))
+ .get();
+ }
+ return nullptr;
+}
+
+const QString Room::decryptMessage(QJsonObject personalCipherObject,
+ QByteArray senderKey) const
+{
+ QString decrypted;
+
+ using namespace QtOlm;
+ // TODO: new objects to private fields:
+ InboundSession* session;
+
+ int type = personalCipherObject.value(TypeKeyL).toInt(-1);
+ QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1();
+
+ PreKeyMessage* preKeyMessage = new PreKeyMessage(body);
+ session = new InboundSession(connection()->olmAccount(), preKeyMessage,
+ senderKey);
+ if (type == 0) {
+ if (!session->matches(preKeyMessage, senderKey)) {
+ connection()->olmAccount()->removeOneTimeKeys(session);
+ }
+ try {
+ decrypted = session->decrypt(preKeyMessage);
+ } catch (std::runtime_error& e) {
+ qWarning(EVENTS) << "Decrypt failed:" << e.what();
+ }
+ } else if (type == 1) {
+ Message* message = new Message(body);
+ if (!session->matches(preKeyMessage, senderKey)) {
+ qWarning(EVENTS) << "Invalid encrypted message";
+ }
+ try {
+ decrypted = session->decrypt(message);
+ } catch (std::runtime_error& e) {
+ qWarning(EVENTS) << "Decrypt failed:" << e.what();
+ }
+ }
+
+ return decrypted;
+}
+
+const QString Room::sessionKey(const QString& senderKey, const QString& deviceId,
+ const QString& sessionId) const
+{
+ // TODO: handling an m.room_key event
+ return "";
+}
+
+const QString Room::decryptMessage(QByteArray cipher, const QString& senderKey,
+ const QString& deviceId,
+ const QString& sessionId) const
+{
+ QString decrypted;
+ using namespace QtOlm;
+ InboundGroupSession* groupSession;
+ groupSession = new InboundGroupSession(
+ sessionKey(senderKey, deviceId, sessionId).toLatin1());
+ groupSession->decrypt(cipher);
+ // TODO: avoid replay attacks
+ return decrypted;
+}
+
int Room::joinedCount() const
{
return d->summary.joinedMemberCount.omitted()
@@ -1468,6 +1582,11 @@ QString Room::postHtmlText(const QString& plainText, const QString& html)
return postHtmlMessage(plainText, html);
}
+QString Room::postReaction(const QString& eventId, const QString& key)
+{
+ return d->sendEvent<ReactionEvent>(EventRelation::annotate(eventId, key));
+}
+
QString Room::postFile(const QString& plainText, const QUrl& localPath,
bool asGenericFile)
{
@@ -1915,11 +2034,64 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction)
updateDisplayname();
}
}
+ if (const auto* reaction = eventCast<ReactionEvent>(oldEvent)) {
+ const auto& targetEvtId = reaction->relation().eventId;
+ const auto lookupKey = qMakePair(targetEvtId,
+ EventRelation::Annotation());
+ if (relations.contains(lookupKey)) {
+ relations[lookupKey].removeOne(reaction);
+ }
+ }
q->onRedaction(*oldEvent, *ti);
emit q->replacedEvent(ti.event(), rawPtr(oldEvent));
return true;
}
+/** Make a replaced event
+ *
+ * Takes \p target and returns a copy of it with content taken from
+ * \p replacement. Disposal of the original event after that is on the caller.
+ */
+RoomEventPtr makeReplaced(const RoomEvent& target,
+ const RoomMessageEvent& replacement)
+{
+ auto originalJson = target.originalJsonObject();
+ originalJson[ContentKeyL] = replacement.contentJson();
+
+ auto unsignedData = originalJson.take(UnsignedKeyL).toObject();
+ auto relations = unsignedData.take("m.relations"_ls).toObject();
+ relations["m.replace"_ls] = replacement.id();
+ unsignedData.insert(QStringLiteral("m.relations"), relations);
+ originalJson.insert(UnsignedKey, unsignedData);
+
+ return loadEvent<RoomEvent>(originalJson);
+}
+
+bool Room::Private::processReplacement(const RoomMessageEvent& newEvent)
+{
+ // Can't use findInTimeline because it returns a const iterator, and
+ // we need to change the underlying TimelineItem.
+ const auto pIdx = eventsIndex.find(newEvent.replacedEvent());
+ if (pIdx == eventsIndex.end())
+ return false;
+
+ Q_ASSERT(q->isValidIndex(*pIdx));
+
+ auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())];
+ if (ti->replacedBy() == newEvent.id()) {
+ qCDebug(MAIN) << "Event" << ti->id() << "is already replaced with"
+ << newEvent.id();
+ return true;
+ }
+
+ // 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(makeReplaced(*ti, newEvent));
+ qCDebug(MAIN) << "Replaced" << oldEvent->id() << "with" << newEvent.id();
+ emit q->replacedEvent(ti.event(), rawPtr(oldEvent));
+ return true;
+}
+
Connection* Room::connection() const
{
Q_ASSERT(d->connection);
@@ -1928,10 +2100,16 @@ Connection* Room::connection() const
User* Room::localUser() const { return connection()->user(); }
-inline bool isRedaction(const RoomEventPtr& ep)
+/// Whether the event is a redaction or a replacement
+inline bool isEditing(const RoomEventPtr& ep)
{
Q_ASSERT(ep);
- return is<RedactionEvent>(*ep);
+ if (is<RedactionEvent>(*ep))
+ return true;
+ if (auto* msgEvent = eventCast<RoomMessageEvent>(ep))
+ return msgEvent->replacedEvent().isEmpty();
+
+ return false;
}
Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
@@ -1940,28 +2118,52 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
if (events.empty())
return Change::NoChange;
- // Pre-process redactions so that events that get redacted in the same
- // batch landed in the timeline already redacted.
- // NB: We have to store redaction events to the timeline too - see #220.
- auto redactionIt = std::find_if(events.begin(), events.end(), isRedaction);
- for (const auto& eptr : RoomEventsRange(redactionIt, events.end()))
- if (auto* r = eventCast<RedactionEvent>(eptr)) {
- // Try to find the target in the timeline, then in the batch.
- if (processRedaction(*r))
- continue;
- auto targetIt =
- std::find_if(events.begin(), redactionIt,
- [id = r->redactedEvent()](const RoomEventPtr& ep) {
- return ep->id() == id;
- });
- if (targetIt != redactionIt)
- *targetIt = makeRedacted(**targetIt, *r);
- else
- qCDebug(MAIN)
- << "Redaction" << r->id() << "ignored: target event"
- << r->redactedEvent() << "is not found";
- // If the target event comes later, it comes already redacted.
+ {
+ // Pre-process redactions and edits so that events that get
+ // redacted/replaced in the same batch landed in the timeline already
+ // treated.
+ // NB: We have to store redacting/replacing events to the timeline too -
+ // see #220.
+ 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;
+ auto targetIt = std::find_if(events.begin(), it,
+ [id = r->redactedEvent()](
+ const RoomEventPtr& ep) {
+ return ep->id() == id;
+ });
+ if (targetIt != it)
+ *targetIt = makeRedacted(**targetIt, *r);
+ else
+ qCDebug(MAIN)
+ << "Redaction" << r->id() << "ignored: target event"
+ << r->redactedEvent() << "is not found";
+ // If the target event comes later, it comes already redacted.
+ }
+ if (auto* msg = eventCast<RoomMessageEvent>(eptr)) {
+ if (!msg->replacedEvent().isEmpty()) {
+ if (processReplacement(*msg))
+ continue;
+ auto targetIt = std::find_if(events.begin(), it,
+ [id = msg->replacedEvent()](
+ const RoomEventPtr& ep) {
+ return ep->id() == id;
+ });
+ if (targetIt != it)
+ *targetIt = makeReplaced(**targetIt, *msg);
+ else // FIXME: don't ignore, just show it wherever it arrived
+ qCDebug(MAIN) << "Replacing event" << msg->id()
+ << "ignored: replaced event"
+ << msg->replacedEvent() << "is not found";
+ // Same as with redactions above, the replaced event coming
+ // later will come already with the new content.
+ }
+ }
}
+ }
// State changes arrive as a part of timeline; the current room state gets
// updated before merging events to the timeline because that's what
@@ -2026,6 +2228,14 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
emit q->callEvent(q, evt);
if (totalInserted > 0) {
+ for (auto it = from; it != timeline.cend(); ++it) {
+ if (const auto* reaction = it->viewAs<ReactionEvent>()) {
+ const auto& relation = reaction->relation();
+ relations[{ relation.eventId, relation.type }] << reaction;
+ emit q->updatedEvent(relation.eventId);
+ }
+ }
+
qCDebug(MAIN) << "Room" << q->objectName() << "received" << totalInserted
<< "new events; the last event is now" << timeline.back();
@@ -2081,6 +2291,13 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
q->onAddHistoricalTimelineEvents(from);
emit q->addedMessages(timeline.front().index(), from->index());
+ for (auto it = from; it != timeline.crend(); ++it) {
+ if (const auto* reaction = it->viewAs<ReactionEvent>()) {
+ const auto& relation = reaction->relation();
+ relations[{ relation.eventId, relation.type }] << reaction;
+ emit q->updatedEvent(relation.eventId);
+ }
+ }
if (from <= q->readMarker())
updateUnreadCount(from, timeline.crend());