diff options
Diffstat (limited to 'lib/room.cpp')
-rw-r--r-- | lib/room.cpp | 273 |
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()); |