From a9984d39e5186e2cfd105cf2ffb4148d9a8573a2 Mon Sep 17 00:00:00 2001 From: Black Hat Date: Thu, 4 Jul 2019 21:52:02 +0800 Subject: Connection.cpp: Add .well-known parsing in resolveServer() --- lib/connection.cpp | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) (limited to 'lib') diff --git a/lib/connection.cpp b/lib/connection.cpp index cd02f6d7..4391a1b2 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -32,6 +32,7 @@ #include "csapi/joining.h" #include "csapi/to_device.h" #include "csapi/room_send.h" +#include "csapi/wellknown.h" #include "jobs/syncjob.h" #include "jobs/mediathumbnailjob.h" #include "jobs/downloadfilejob.h" @@ -194,37 +195,24 @@ void Connection::resolveServer(const QString& mxidOrDomain) } setHomeserver(maybeBaseUrl); - emit resolved(); - return; - // FIXME, #178: The below code is incorrect and is no more executed. The - // correct server resolution should be done from .well-known/matrix/client auto domain = maybeBaseUrl.host(); qCDebug(MAIN) << "Finding the server" << domain; - // Check if the Matrix server has a dedicated service record. - auto* dns = new QDnsLookup(); - dns->setType(QDnsLookup::SRV); - dns->setName("_matrix._tcp." + domain); - - connect(dns, &QDnsLookup::finished, [this,dns,maybeBaseUrl]() { - QUrl baseUrl { maybeBaseUrl }; - if (dns->error() == QDnsLookup::NoError && - dns->serviceRecords().isEmpty()) - { - auto record = dns->serviceRecords().front(); - baseUrl.setHost(record.target()); - baseUrl.setPort(record.port()); - qCDebug(MAIN) << "SRV record for" << maybeBaseUrl.host() - << "is" << baseUrl.authority(); + + auto job = callApi(); + connect(job, &BaseJob::finished, [this, job, maybeBaseUrl] { + QUrl baseUrl(job->data().homeserver.baseUrl); + if (!baseUrl.isValid() || job->status() != BaseJob::Success) { + baseUrl = maybeBaseUrl; + qCDebug(MAIN) << maybeBaseUrl.host() << "doesn't have .well-known record" << "- using the hostname as is"; } else { - qCDebug(MAIN) << baseUrl.host() << "doesn't have SRV record" - << dns->name() << "- using the hostname as is"; + qCDebug(MAIN) << ".well-known for" << maybeBaseUrl.host() << "is" << baseUrl.authority(); } + setHomeserver(baseUrl); emit resolved(); - dns->deleteLater(); + job->deleteLater(); }); - dns->lookup(); } void Connection::connectToServer(const QString& user, const QString& password, -- cgit v1.2.3 From 5d9d56a4fe9f3178c8bc9b3839ec608659c0c371 Mon Sep 17 00:00:00 2001 From: Black Hat Date: Fri, 5 Jul 2019 10:15:54 +0800 Subject: Clean up .well-known logic --- lib/connection.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/connection.cpp b/lib/connection.cpp index 4391a1b2..98555104 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -203,13 +203,12 @@ void Connection::resolveServer(const QString& mxidOrDomain) connect(job, &BaseJob::finished, [this, job, maybeBaseUrl] { QUrl baseUrl(job->data().homeserver.baseUrl); if (!baseUrl.isValid() || job->status() != BaseJob::Success) { - baseUrl = maybeBaseUrl; - qCDebug(MAIN) << maybeBaseUrl.host() << "doesn't have .well-known record" << "- using the hostname as is"; + qCDebug(MAIN) << maybeBaseUrl.host() << "doesn't have valid .well-known file" << "- using the hostname as is"; } else { qCDebug(MAIN) << ".well-known for" << maybeBaseUrl.host() << "is" << baseUrl.authority(); + setHomeserver(baseUrl); } - setHomeserver(baseUrl); emit resolved(); job->deleteLater(); }); -- cgit v1.2.3 From f40d511917e7ac1e44106b24bfa3cdf93bd6f664 Mon Sep 17 00:00:00 2001 From: Black Hat Date: Tue, 9 Jul 2019 20:46:49 +0800 Subject: Add more checks according to the spec --- lib/connection.cpp | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/connection.cpp b/lib/connection.cpp index 98555104..511a0411 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -33,6 +33,7 @@ #include "csapi/to_device.h" #include "csapi/room_send.h" #include "csapi/wellknown.h" +#include "csapi/versions.h" #include "jobs/syncjob.h" #include "jobs/mediathumbnailjob.h" #include "jobs/downloadfilejob.h" @@ -199,18 +200,40 @@ void Connection::resolveServer(const QString& mxidOrDomain) auto domain = maybeBaseUrl.host(); qCDebug(MAIN) << "Finding the server" << domain; - auto job = callApi(); - connect(job, &BaseJob::finished, [this, job, maybeBaseUrl] { - QUrl baseUrl(job->data().homeserver.baseUrl); - if (!baseUrl.isValid() || job->status() != BaseJob::Success) { - qCDebug(MAIN) << maybeBaseUrl.host() << "doesn't have valid .well-known file" << "- using the hostname as is"; + auto getWellKnownJob = callApi(); + connect(getWellKnownJob, &BaseJob::finished, [this, getWellKnownJob, maybeBaseUrl] { + if (getWellKnownJob->status() == BaseJob::NotFoundError) { + qCDebug(MAIN) << "No .well-known file, IGNORE"; + } else if (getWellKnownJob->status() != BaseJob::Success) { + qCDebug(MAIN) << "Fetching .well-known file failed, FAIL_PROMPT"; + emit resolveError(tr("Fetching .well-known file failed")); + return; + } else if (getWellKnownJob->data().homeserver.baseUrl.isEmpty()) { + qCDebug(MAIN) << "base_url not provided, FAIL_PROMPT"; + emit resolveError(tr("base_url not provided")); + return; + } else if (!QUrl(getWellKnownJob->data().homeserver.baseUrl).isValid()) { + qCDebug(MAIN) << "base_url invalid, FAIL_ERROR"; + emit resolveError(tr("base_url invalid")); + return; } else { + QUrl baseUrl(getWellKnownJob->data().homeserver.baseUrl); + qCDebug(MAIN) << ".well-known for" << maybeBaseUrl.host() << "is" << baseUrl.authority(); setHomeserver(baseUrl); } - emit resolved(); - job->deleteLater(); + auto getVersionsJob = callApi(); + + connect(getVersionsJob, &BaseJob::finished, [this, getVersionsJob] { + if (getVersionsJob->status() == BaseJob::Success) { + qCDebug(MAIN) << "homeserver url is valid"; + emit resolved(); + } else { + qCDebug(MAIN) << "homeserver url invalid"; + emit resolveError(tr("homeserver url invalid")); + } + }); }); } -- cgit v1.2.3 From 74caea2669b8f76ca76507bc40321fdcd23dc522 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 12 Jul 2019 18:02:27 +0900 Subject: Minor polish --- lib/connection.cpp | 2 +- lib/jobs/basejob.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/connection.cpp b/lib/connection.cpp index 79411dce..1bd2e32e 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -200,7 +200,7 @@ void Connection::resolveServer(const QString& mxid) } else { QUrl baseUrl(getWellKnownJob->data().homeserver.baseUrl); - qCDebug(MAIN) << ".well-known for" << maybeBaseUrl.host() << "is" << baseUrl.authority(); + qCDebug(MAIN) << ".well-known for" << maybeBaseUrl.host() << "is" << baseUrl.toString(); setHomeserver(baseUrl); } diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 6ad8d971..b3cfc527 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -300,7 +300,7 @@ void BaseJob::gotReply() QJsonDocument::fromJson(d->rawResponse).object())); } - if (error() != TooManyRequestsError) + if (status().code != TooManyRequestsError) finishJob(); else { stop(); -- cgit v1.2.3 From f1546e894b2a7550bce3e8d84067968bf5cf7087 Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Sun, 14 Jul 2019 02:33:39 +0300 Subject: E2EE: provide a shared header with encryption standard key names --- lib/e2ee.h | 21 +++++++++++++++++++++ lib/encryptionmanager.cpp | 26 ++++++++++---------------- lib/events/encryptionevent.cpp | 15 ++++++++------- 3 files changed, 39 insertions(+), 23 deletions(-) create mode 100644 lib/e2ee.h (limited to 'lib') diff --git a/lib/e2ee.h b/lib/e2ee.h new file mode 100644 index 00000000..f663ddc3 --- /dev/null +++ b/lib/e2ee.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace QMatrixClient +{ + static const auto AlgorithmKeyL = "algorithm"_ls; + static const auto RotationPeriodMsKeyL = "rotation_period_ms"_ls; + static const auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls; + + static const auto AlgorithmKey = QStringLiteral("algorithm"); + static const auto RotationPeriodMsKey = QStringLiteral("rotation_period_ms"); + static const auto RotationPeriodMsgsKey = QStringLiteral("rotation_period_msgs"); + + static const auto Ed25519Key = QStringLiteral("ed25519"); + static const auto Curve25519Key = QStringLiteral("curve25519"); + static const auto SignedCurve25519Key = QStringLiteral("signed_curve25519"); + static const auto OlmV1Curve25519AesSha2AlgoKey = QStringLiteral("m.olm.v1.curve25519-aes-sha2"); + static const auto MegolmV1AesSha2AlgoKey = QStringLiteral("m.megolm.v1.aes-sha2"); + static const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoKey, MegolmV1AesSha2AlgoKey }; +} // namespace QMatrixClient diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 50db9889..08b68911 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -8,18 +8,12 @@ #include "csapi/keys.h" #include "connection.h" +#include "e2ee.h" using namespace QMatrixClient; using namespace QtOlm; using std::move; -static const auto ed25519Name = QStringLiteral("ed25519"); -static const auto Curve25519Name = QStringLiteral("curve25519"); -static const auto SignedCurve25519Name = QStringLiteral("signed_curve25519"); -static const auto OlmV1Curve25519AesSha2AlgoName = QStringLiteral("m.olm.v1.curve25519-aes-sha2"); -static const auto MegolmV1AesSha2AlgoName = QStringLiteral("m.megolm.v1.aes-sha2"); -static const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoName, MegolmV1AesSha2AlgoName }; - class EncryptionManager::Private { public: @@ -49,8 +43,8 @@ class EncryptionManager::Private targetKeysNumber = olmAccount->maxOneTimeKeys(); // 2 // see note below targetOneTimeKeyCounts = { - {SignedCurve25519Name, qRound(signedKeysProportion * targetKeysNumber)}, - {Curve25519Name, qRound((1-signedKeysProportion) * targetKeysNumber)} + {SignedCurve25519Key, qRound(signedKeysProportion * targetKeysNumber)}, + {Curve25519Key, qRound((1-signedKeysProportion) * targetKeysNumber)} }; } ~Private() = default; @@ -104,11 +98,11 @@ void EncryptionManager::uploadIdentityKeys(Connection* connection) */ { { - Curve25519Name + QStringLiteral(":") + connection->deviceId(), + Curve25519Key + QStringLiteral(":") + connection->deviceId(), d->olmAccount->curve25519IdentityKey() }, { - ed25519Name + QStringLiteral(":") + connection->deviceId(), + Ed25519Key + QStringLiteral(":") + connection->deviceId(), d->olmAccount->ed25519IdentityKey() } }, @@ -133,7 +127,7 @@ void EncryptionManager::uploadIdentityKeys(Connection* connection) connection->userId(), { { - ed25519Name + QStringLiteral(":") + connection->deviceId(), + Ed25519Key + QStringLiteral(":") + connection->deviceId(), d->olmAccount->sign(deviceKeysJsonObject) } } @@ -158,8 +152,8 @@ void EncryptionManager::uploadOneTimeKeys(Connection* connection, bool forceUpda } - int signedKeysToUploadCount = d->oneTimeKeysToUploadCounts.value(SignedCurve25519Name, 0); - int unsignedKeysToUploadCount = d->oneTimeKeysToUploadCounts.value(Curve25519Name, 0); + int signedKeysToUploadCount = d->oneTimeKeysToUploadCounts.value(SignedCurve25519Key, 0); + int unsignedKeysToUploadCount = d->oneTimeKeysToUploadCounts.value(Curve25519Key, 0); d->olmAccount->generateOneTimeKeys(signedKeysToUploadCount + unsignedKeysToUploadCount); @@ -179,11 +173,11 @@ void EncryptionManager::uploadOneTimeKeys(Connection* connection, bool forceUpda {QStringLiteral("key"), it.value().toString()} }; key = d->olmAccount->sign(message); - keyType = SignedCurve25519Name; + keyType = SignedCurve25519Key; } else { key = it.value(); - keyType = Curve25519Name; + keyType = Curve25519Key; } ++oneTimeKeysCounter; oneTimeKeys.insert(QString("%1:%2").arg(keyType).arg(keyId), key); diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index b8e2b575..ee6c92b1 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -7,11 +7,12 @@ #include "converters.h" #include "logging.h" +#include "e2ee.h" #include static const std::array encryptionStrings = { { - QStringLiteral("m.megolm.v1.aes-sha2") + QMatrixClient::MegolmV1AesSha2AlgoKey } }; namespace QMatrixClient { @@ -36,9 +37,9 @@ using namespace QMatrixClient; EncryptionEventContent::EncryptionEventContent(const QJsonObject& json) : encryption(fromJson(json["algorithm"_ls])) - , algorithm(sanitized(json["algorithm"_ls].toString())) - , rotationPeriodMs(json["rotation_period_ms"_ls].toInt(604800000)) - , rotationPeriodMsgs(json["rotation_period_msgs"_ls].toInt(100)) + , algorithm(sanitized(json[AlgorithmKeyL].toString())) + , rotationPeriodMs(json[RotationPeriodMsKeyL].toInt(604800000)) + , rotationPeriodMsgs(json[RotationPeriodMsgsKeyL].toInt(100)) { } void EncryptionEventContent::fillJson(QJsonObject* o) const @@ -47,7 +48,7 @@ void EncryptionEventContent::fillJson(QJsonObject* o) const Q_ASSERT_X(encryption != EncryptionType::Undefined, __FUNCTION__, "The key 'algorithm' must be explicit in EncryptionEventContent"); if (encryption != EncryptionType::Undefined) - o->insert(QStringLiteral("algorithm"), algorithm); - o->insert(QStringLiteral("rotation_period_ms"), rotationPeriodMs); - o->insert(QStringLiteral("rotation_period_msgs"), rotationPeriodMsgs); + o->insert(AlgorithmKey, algorithm); + o->insert(RotationPeriodMsKey, rotationPeriodMs); + o->insert(RotationPeriodMsgsKey, rotationPeriodMsgs); } -- cgit v1.2.3 From 2737dc00334ad3a56c1b311435dbe84453ee389e Mon Sep 17 00:00:00 2001 From: Alexey Andreyev Date: Sun, 14 Jul 2019 03:54:19 +0300 Subject: E2EE: introduce EncryptedEvent --- lib/connection.cpp | 5 +++ lib/connection.h | 5 +++ lib/e2ee.h | 5 +++ lib/encryptionmanager.cpp | 5 +++ lib/encryptionmanager.h | 6 +++ lib/events/encryptedevent.cpp | 29 ++++++++++++++ lib/events/encryptedevent.h | 66 +++++++++++++++++++++++++++++++ lib/events/event.h | 1 + lib/events/roommessageevent.cpp | 7 ++-- lib/room.cpp | 87 +++++++++++++++++++++++++++++++++++++++++ lib/room.h | 6 +++ 11 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 lib/events/encryptedevent.cpp create mode 100644 lib/events/encryptedevent.h (limited to 'lib') diff --git a/lib/connection.cpp b/lib/connection.cpp index 1bd2e32e..b9ab5147 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -979,6 +979,11 @@ QByteArray Connection::accessToken() const return d->data->accessToken(); } +QtOlm::Account* Connection::olmAccount() const +{ + return d->encryptionManager->account(); +} + SyncJob* Connection::syncJob() const { return d->syncJob; diff --git a/lib/connection.h b/lib/connection.h index 11499a6e..199803d7 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -30,6 +30,10 @@ #include +namespace QtOlm { + class Account; +} + namespace QMatrixClient { class Room; @@ -264,6 +268,7 @@ namespace QMatrixClient QString userId() const; QString deviceId() const; QByteArray accessToken() const; + QtOlm::Account* olmAccount() const; Q_INVOKABLE SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; diff --git a/lib/e2ee.h b/lib/e2ee.h index f663ddc3..4a42809d 100644 --- a/lib/e2ee.h +++ b/lib/e2ee.h @@ -4,6 +4,11 @@ namespace QMatrixClient { + static const auto CiphertextKeyL = "ciphertext"_ls; + static const auto SenderKeyKeyL = "sender_key"_ls; + static const auto DeviceIdKeyL = "device_id"_ls; + static const auto SessionIdKeyL = "session_id"_ls; + static const auto AlgorithmKeyL = "algorithm"_ls; static const auto RotationPeriodMsKeyL = "rotation_period_ms"_ls; static const auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls; diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp index 08b68911..3533d791 100644 --- a/lib/encryptionmanager.cpp +++ b/lib/encryptionmanager.cpp @@ -194,6 +194,11 @@ QByteArray EncryptionManager::olmAccountPickle() return d->olmAccount->pickle(); // TODO: passphrase even with qtkeychain? } +QtOlm::Account* EncryptionManager::account() const +{ + return d->olmAccount.data(); +} + void EncryptionManager::Private::updateKeysToUpload() { for (auto it = targetOneTimeKeyCounts.cbegin(); it != targetOneTimeKeyCounts.cend(); ++it) diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h index 40fe7383..0225969d 100644 --- a/lib/encryptionmanager.h +++ b/lib/encryptionmanager.h @@ -4,6 +4,10 @@ #include #include +namespace QtOlm { + class Account; +} + namespace QMatrixClient { class Connection; @@ -23,6 +27,8 @@ namespace QMatrixClient void uploadOneTimeKeys(Connection* connection, bool forceUpdate = false); QByteArray olmAccountPickle(); + QtOlm::Account* account() const; + private: class Private; std::unique_ptr d; diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp new file mode 100644 index 00000000..6942738a --- /dev/null +++ b/lib/events/encryptedevent.cpp @@ -0,0 +1,29 @@ +#include "encryptedevent.h" +#include "room.h" + +using namespace QMatrixClient; +using namespace QtOlm; + +EncryptedEvent::EncryptedEvent(const QJsonObject &ciphertext, const QString &senderKey) + : RoomEvent(typeId(), matrixTypeId(), + { { AlgorithmKeyL , OlmV1Curve25519AesSha2AlgoKey }, + { CiphertextKeyL , ciphertext }, + { SenderKeyKeyL, senderKey } + }) +{ } + +EncryptedEvent::EncryptedEvent(QByteArray ciphertext, const QString &senderKey, const QString& deviceId, const QString& sessionId) + : RoomEvent(typeId(), matrixTypeId(), + { { AlgorithmKeyL , MegolmV1AesSha2AlgoKey }, + { CiphertextKeyL , QString(ciphertext) }, + { DeviceIdKeyL, deviceId }, + { SenderKeyKeyL, senderKey }, + { SessionIdKeyL, sessionId }, + }) +{ } + +EncryptedEvent::EncryptedEvent(const QJsonObject &obj) + : RoomEvent(typeId(), obj) +{ + qCDebug(EVENTS) << "Encrypted event" << id(); +} diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h new file mode 100644 index 00000000..2f9e4422 --- /dev/null +++ b/lib/events/encryptedevent.h @@ -0,0 +1,66 @@ +#pragma once + +#include "roomevent.h" +#include "e2ee.h" + +namespace QMatrixClient +{ + class Room; + /* + * While the specification states: + * + * "This event type is used when sending encrypted events. + * It can be used either within a room + * (in which case it will have all of the Room Event fields), + * or as a to-device event." + * "The encrypted payload can contain any message event." + * https://matrix.org/docs/spec/client_server/latest#id493 + * + * -- for most of the cases the message event is the room message event. + * And even for the to-device events the context is for the room. + * + * So, to simplify integration to the timeline, EncryptedEvent is a RoomEvent inheritor. + * Strictly speaking though, it's not always a RoomEvent, but an Event in general. + * It's possible, because RoomEvent interface is similar to Event's one + * and doesn't add new restrictions, just provides additional features. + */ + class EncryptedEvent : public RoomEvent + { + Q_GADGET + public: + DEFINE_EVENT_TYPEID("m.room.encrypted", EncryptedEvent) + + /* In case with Olm, the encrypted content of the event is + * a map from the recipient Curve25519 identity key to ciphertext information */ + explicit EncryptedEvent(const QJsonObject& ciphertext, + const QString& senderKey); + /* In case with Megolm, device_id and session_id are required */ + explicit EncryptedEvent(QByteArray ciphertext, + const QString& senderKey, + const QString& deviceId, + const QString& sessionId); + explicit EncryptedEvent(const QJsonObject& obj); + + QString algorithm() const + { + QString algo = content(AlgorithmKeyL); + if (!SupportedAlgorithms.contains(algo)) { + qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo + << "is not supported"; + } + return algo; + } + QByteArray ciphertext() const { return content(CiphertextKeyL).toLatin1(); } + QJsonObject ciphertext(const QString& identityKey) const + { + return content(CiphertextKeyL).value(identityKey).toObject(); + } + QString senderKey() const { return content(SenderKeyKeyL); } + + /* device_id and session_id are required with Megolm */ + QString deviceId() const { return content(DeviceIdKeyL); } + QString sessionId() const { return content(SessionIdKeyL); } + }; + REGISTER_EVENT_TYPE(EncryptedEvent) + +} // namespace QMatrixClient diff --git a/lib/events/event.h b/lib/events/event.h index b3a58806..6f28c4fa 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -62,6 +62,7 @@ namespace QMatrixClient static const auto UnsignedKey = QStringLiteral("unsigned"); static const auto StateKeyKey = QStringLiteral("state_key"); static const auto TypeKeyL = "type"_ls; + static const auto BodyKeyL = "body"_ls; static const auto ContentKeyL = "content"_ls; static const auto EventIdKeyL = "event_id"_ls; static const auto UnsignedKeyL = "unsigned"_ls; diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 8f4e0ebc..ec18e962 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -32,7 +32,6 @@ using MsgType = RoomMessageEvent::MsgType; static const auto RelatesToKey = "m.relates_to"_ls; static const auto MsgTypeKey = "msgtype"_ls; -static const auto BodyKey = "body"_ls; static const auto FormattedBodyKey = "formatted_body"_ls; static const auto TextTypeKey = "m.text"; @@ -159,7 +158,7 @@ RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) if (isRedacted()) return; const QJsonObject content = contentJson(); - if ( content.contains(MsgTypeKey) && content.contains(BodyKey) ) + if ( content.contains(MsgTypeKey) && content.contains(BodyKeyL) ) { auto msgtype = content[MsgTypeKey].toString(); bool msgTypeFound = false; @@ -196,7 +195,7 @@ QString RoomMessageEvent::rawMsgtype() const QString RoomMessageEvent::plainBody() const { - return contentJson()[BodyKey].toString(); + return contentJson()[BodyKeyL].toString(); } QMimeType RoomMessageEvent::mimeType() const @@ -267,7 +266,7 @@ TextContent::TextContent(const QJsonObject& json) // Falling back to plain text, as there's no standard way to describe // rich text in messages. mimeType = PlainTextMimeType; - body = json[BodyKey].toString(); + body = json[BodyKeyL].toString(); } const auto replyJson = json[RelatesToKey].toObject() .value(RelatesTo::ReplyTypeId()).toObject(); diff --git a/lib/room.cpp b/lib/room.cpp index cb368d9e..0402ce67 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -52,6 +52,12 @@ #include "converters.h" #include "syncdata.h" +#include "e2ee.h" + +#include // QtOlm +#include // QtOlm +#include // QtOlm + #include #include // for efficient string concats (operator%) #include @@ -1187,6 +1193,87 @@ bool Room::usesEncryption() const return !d->getCurrentState()->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(decryptMessage(personalCipherObject, encryptedEvent->senderKey().toLatin1())).get(); + } + if (encryptedEvent->algorithm() == MegolmV1AesSha2AlgoKey) { + return makeEvent(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() diff --git a/lib/room.h b/lib/room.h index c79ca1e0..e09556b6 100644 --- a/lib/room.h +++ b/lib/room.h @@ -24,8 +24,10 @@ #include "events/accountdataevents.h" #include "eventitem.h" #include "joinstate.h" +#include "events/encryptedevent.h" #include +#include #include #include @@ -179,6 +181,10 @@ namespace QMatrixClient int memberCount() const; int timelineSize() const; bool usesEncryption() const; + const RoomEvent *decryptMessage(EncryptedEvent* encryptedEvent) const; + const QString decryptMessage(QJsonObject personalCipherObject, QByteArray senderKey) const; + const QString sessionKey(const QString &senderKey, const QString &deviceId, const QString &sessionId) const; + const QString decryptMessage(QByteArray cipher, const QString& senderKey, const QString& deviceId, const QString& sessionId) const; int joinedCount() const; int invitedCount() const; int totalMemberCount() const; -- cgit v1.2.3 From b38d48e5bd2764568539089b248d0c3683783aa8 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 13 Jul 2019 19:54:08 +0900 Subject: converters.h: more comments; documented addParam<>() [skip ci] --- lib/converters.h | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/converters.h b/lib/converters.h index af2be645..5f00dc43 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -107,6 +107,10 @@ namespace QMatrixClient return JsonConverter::load(jd); } + // Convenience fromJson() overloads that deduce T instead of requiring + // the coder to explicitly type it. They still enforce the + // overwrite-everything semantics of fromJson(), unlike fillFromJson() + template inline void fromJson(const QJsonValue& jv, T& pod) { @@ -357,7 +361,8 @@ namespace QMatrixClient q.addQueryItem(it.key(), it.value().toString()); } - // This one is for types that don't have isEmpty() + // This one is for types that don't have isEmpty() and for all types + // when Force is true template struct AddNode { @@ -369,7 +374,7 @@ namespace QMatrixClient } }; - // This one is for types that have isEmpty() + // This one is for types that have isEmpty() when Force is false template struct AddNode().isEmpty())> @@ -420,6 +425,29 @@ namespace QMatrixClient static constexpr bool IfNotEmpty = false; + /*! Add a key-value pair to QJsonObject or QUrlQuery + * + * Adds a key-value pair(s) specified by \p key and \p value to + * \p container, optionally (in case IfNotEmpty is passed for the first + * template parameter) taking into account the value "emptiness". + * With IfNotEmpty, \p value is NOT added to the container if and only if: + * - it has a method `isEmpty()` and `value.isEmpty() == true`, or + * - it's an `Omittable<>` and `value.omitted() == true`. + * + * If \p container is a QUrlQuery, an attempt to fit \p value into it is + * made as follows: + * - if \p value is a QJsonObject, \p key is ignored and pairs from \p value + * are copied to \p container, assuming that the value in each pair + * is a string; + * - if \p value is a QStringList, it is "exploded" into a list of key-value + * pairs with key equal to \p key and value taken from each list item; + * - if \p value is a bool, its OpenAPI (i.e. JSON) representation is added + * to the query (`true` or `false`, respectively). + * + * \tparam Force add the pair even if the value is empty. This is true + * by default; passing IfNotEmpty or false for this parameter + * enables emptiness checks as described above + */ template inline void addParam(ContT& container, const QString& key, ValT&& value) { -- cgit v1.2.3 From 26bc529ec86dce5478ab37222a27902af7f0dd5a Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 13 Jul 2019 19:57:46 +0900 Subject: converters.h: fallback to intrusive toJson() for JsonObjectConverter; general improvements Single-argument fromJson>() now works as well. --- lib/converters.h | 53 +++++++++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 24 deletions(-) (limited to 'lib') diff --git a/lib/converters.h b/lib/converters.h index 5f00dc43..36b7ff15 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -60,8 +60,8 @@ namespace QMatrixClient template struct JsonObjectConverter { - static void dumpTo(QJsonObject& jo, const T& pod) { jo = pod; } - static void fillFrom(const QJsonObject& jo, T& pod) { pod = jo; } + static void dumpTo(QJsonObject& jo, const T& pod) { jo = pod.toJson(); } + static void fillFrom(const QJsonObject& jo, T& pod) { pod = T(jo); } }; template @@ -89,14 +89,16 @@ namespace QMatrixClient return JsonConverter::dump(pod); } + inline auto toJson(const QJsonObject& jo) { return jo; } + template - inline auto fillJson(QJsonObject& json, const T& data) + inline void fillJson(QJsonObject& json, const T& data) { JsonObjectConverter::dumpTo(json, data); } template - inline auto fromJson(const QJsonValue& jv) + inline T fromJson(const QJsonValue& jv) { return JsonConverter::load(jv); } @@ -114,8 +116,7 @@ namespace QMatrixClient template inline void fromJson(const QJsonValue& jv, T& pod) { - if (!jv.isUndefined()) - pod = fromJson(jv); + pod = jv.isUndefined() ? T() : fromJson(jv); } template @@ -124,21 +125,13 @@ namespace QMatrixClient pod = fromJson(jd); } - // Unfolds Omittable<> - template - inline void fromJson(const QJsonValue& jv, Omittable& pod) - { - if (jv.isUndefined()) - pod = none; - else - pod = fromJson(jv); - } - template inline void fillFromJson(const QJsonValue& jv, T& pod) { if (jv.isObject()) JsonObjectConverter::fillFrom(jv.toObject(), pod); + else if (!jv.isUndefined()) + pod = fromJson(jv); } // JsonConverter<> specialisations @@ -224,6 +217,21 @@ namespace QMatrixClient static QVariant load(const QJsonValue& jv); }; + template + struct JsonConverter> + { + static QJsonValue dump(const Omittable& from) + { + return from.omitted() ? QJsonValue() : toJson(from.value()); + } + static Omittable load(const QJsonValue& jv) + { + if (jv.isUndefined()) + return none; + return fromJson(jv); + } + }; + template struct JsonArrayConverter @@ -384,23 +392,20 @@ namespace QMatrixClient ForwardedT&& value) { if (!value.isEmpty()) - AddNode::impl(container, - key, std::forward(value)); + addTo(container, key, std::forward(value)); } }; - // This is a special one that unfolds Omittable<> - template - struct AddNode, Force> + // This one unfolds Omittable<> (also only when Force is false) + template + struct AddNode, false> { template static void impl(ContT& container, const QString& key, const OmittableT& value) { if (!value.omitted()) - AddNode::impl(container, key, value.value()); - else if (Force) // Edge case, no value but must put something - AddNode::impl(container, key, QString{}); + addTo(container, key, value.value()); } }; -- cgit v1.2.3 From b87097866f38b90f36fb216b7516a135227930a1 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 13 Jul 2019 22:43:47 +0900 Subject: Initial support for edited messages (receive only) --- lib/events/roomevent.cpp | 14 ++++++ lib/events/roomevent.h | 2 + lib/events/roommessageevent.cpp | 91 ++++++++++++++++++++++++++++++-------- lib/events/roommessageevent.h | 3 ++ lib/room.cpp | 96 +++++++++++++++++++++++++++++++++++++---- 5 files changed, 180 insertions(+), 26 deletions(-) (limited to 'lib') diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index f1e563ff..0143826a 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -66,6 +66,20 @@ QString RoomEvent::senderId() const return fullJson()["sender"_ls].toString(); } +bool RoomEvent::isReplaced() const +{ + return unsignedJson()["m.relations"_ls].toObject().contains("m.replace"); +} + +QString RoomEvent::replacedBy() const +{ + // clang-format off + return unsignedJson()["m.relations"_ls].toObject() + .value("m.replace").toObject() + .value(EventIdKeyL).toString(); + // clang-format on +} + QString RoomEvent::redactionReason() const { return isRedacted() ? _redactedBecause->reason() : QString{}; diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index e26a7135..0e6f730d 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -51,6 +51,8 @@ namespace QMatrixClient QDateTime timestamp() const; QString roomId() const; QString senderId() const; + bool isReplaced() const; + QString replacedBy() const; bool isRedacted() const { return bool(_redactedBecause); } const event_ptr_tt& redactedBecause() const { diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 8f4e0ebc..d4b0d812 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -36,6 +36,7 @@ static const auto BodyKey = "body"_ls; static const auto FormattedBodyKey = "formatted_body"_ls; static const auto TextTypeKey = "m.text"; +static const auto EmoteTypeKey = "m.emote"; static const auto NoticeTypeKey = "m.notice"; static const auto HtmlContentTypeId = QStringLiteral("org.matrix.custom.html"); @@ -62,7 +63,7 @@ struct MsgTypeDesc const std::vector msgTypes = { { TextTypeKey, MsgType::Text, make } - , { QStringLiteral("m.emote"), MsgType::Emote, make } + , { EmoteTypeKey, MsgType::Emote, make } , { NoticeTypeKey, MsgType::Notice, make } , { QStringLiteral("m.image"), MsgType::Image, make } , { QStringLiteral("m.file"), MsgType::File, make } @@ -95,12 +96,25 @@ QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) { auto json = content ? content->toJson() : QJsonObject(); - if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey && - json.contains(RelatesToKey)) - { - json.remove(RelatesToKey); - qCWarning(EVENTS) << RelatesToKey << "cannot be used in" << jsonMsgType - << "messages; the relation has been stripped off"; + if (json.contains(RelatesToKey)) { + if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey + && jsonMsgType != EmoteTypeKey) { + json.remove(RelatesToKey); + qCWarning(EVENTS) + << RelatesToKey << "cannot be used in" << jsonMsgType + << "messages; the relation has been stripped off"; + } else { + // After the above, we know for sure that the content is TextContent + // and that its RelatesTo structure is not omitted + auto* textContent = static_cast(content); + if (textContent->relatesTo->type == RelatesTo::ReplacementTypeId()) { + auto newContentJson = json.take("m.new_content"_ls).toObject(); + newContentJson.insert(BodyKey, plainBody); + newContentJson.insert(TypeKey, jsonMsgType); + json.insert(QStringLiteral("m.new_content"), newContentJson); + json[BodyKey] = "* " + plainBody; + } + } } json.insert(QStringLiteral("msgtype"), jsonMsgType); json.insert(QStringLiteral("body"), plainBody); @@ -223,6 +237,16 @@ bool RoomMessageEvent::hasThumbnail() const return content() && content()->thumbnailInfo(); } +QString RoomMessageEvent::replacedEvent() const +{ + if (!content() || !hasTextContent()) + return {}; + + const auto& rel = static_cast(content())->relatesTo; + return !rel.omitted() && rel->type == RelatesTo::ReplacementTypeId() + ? rel->eventId : QString(); +} + QString rawMsgTypeForMimeType(const QMimeType& mimeType) { auto name = mimeType.name(); @@ -251,41 +275,72 @@ TextContent::TextContent(const QString& text, const QString& contentType, mimeType = QMimeDatabase().mimeTypeForName("text/html"); } +namespace QMatrixClient +{ +// Overload the default fromJson<> logic that defined in converters.h +// as we want +template <> +Omittable fromJson(const QJsonValue& jv) +{ + const auto jo = jv.toObject(); + if (jo.isEmpty()) + return none; + const auto replyJson = jo.value(RelatesTo::ReplyTypeId()).toObject(); + if (!replyJson.isEmpty()) + return replyTo(fromJson(replyJson[EventIdKeyL])); + + return RelatesTo { jo.value("rel_type"_ls).toString(), + jo.value(EventIdKeyL).toString() }; +} +} + TextContent::TextContent(const QJsonObject& json) + : relatesTo(fromJson>(json[RelatesToKey])) { QMimeDatabase db; static const auto PlainTextMimeType = db.mimeTypeForName("text/plain"); static const auto HtmlMimeType = db.mimeTypeForName("text/html"); + const auto actualJson = + relatesTo.omitted() || relatesTo->type != RelatesTo::ReplacementTypeId() + ? json : json.value("m.new_content"_ls).toObject(); // Special-casing the custom matrix.org's (actually, Riot's) way // of sending HTML messages. - if (json["format"_ls].toString() == HtmlContentTypeId) + if (actualJson["format"_ls].toString() == HtmlContentTypeId) { mimeType = HtmlMimeType; - body = json[FormattedBodyKey].toString(); + body = actualJson[FormattedBodyKey].toString(); } else { // Falling back to plain text, as there's no standard way to describe // rich text in messages. mimeType = PlainTextMimeType; - body = json[BodyKey].toString(); + body = actualJson[BodyKey].toString(); } - const auto replyJson = json[RelatesToKey].toObject() - .value(RelatesTo::ReplyTypeId()).toObject(); - if (!replyJson.isEmpty()) - relatesTo = replyTo(fromJson(replyJson[EventIdKeyL])); } void TextContent::fillJson(QJsonObject* json) const { + static const auto FormatKey = QStringLiteral("format"); + static const auto RichBodyKey = QStringLiteral("formatted_body"); + Q_ASSERT(json); if (mimeType.inherits("text/html")) { - json->insert(QStringLiteral("format"), HtmlContentTypeId); - json->insert(QStringLiteral("formatted_body"), body); + json->insert(FormatKey, HtmlContentTypeId); + json->insert(RichBodyKey, body); } - if (!relatesTo.omitted()) + if (!relatesTo.omitted()) { json->insert(QStringLiteral("m.relates_to"), - QJsonObject { { relatesTo->type, relatesTo->eventId } }); + QJsonObject { { relatesTo->type, relatesTo->eventId } }); + if (relatesTo->type == RelatesTo::ReplacementTypeId()) { + QJsonObject newContentJson; + if (mimeType.inherits("text/html")) { + json->insert(FormatKey, HtmlContentTypeId); + json->insert(RichBodyKey, body); + } + json->insert(QStringLiteral("m.new_content"), newContentJson); + } + } } LocationContent::LocationContent(const QString& geoUri, diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index c2e075eb..7320e4ea 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -72,6 +72,7 @@ namespace QMatrixClient bool hasTextContent() const; bool hasFileContent() const; bool hasThumbnail() const; + QString replacedEvent() const; static QString rawMsgTypeForUrl(const QUrl& url); static QString rawMsgTypeForFile(const QFileInfo& fi); @@ -79,6 +80,7 @@ namespace QMatrixClient private: QScopedPointer _content; + // FIXME: should it really be static? static QJsonObject assembleContentJson(const QString& plainBody, const QString& jsonMsgType, EventContent::TypedBase* content); @@ -95,6 +97,7 @@ namespace QMatrixClient struct RelatesTo { static constexpr const char* ReplyTypeId() { return "m.in_reply_to"; } + static constexpr const char* ReplacementTypeId() { return "m.replace"; } QString type; // The only supported relation so far QString eventId; }; diff --git a/lib/room.cpp b/lib/room.cpp index cb368d9e..958991b7 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -307,14 +307,22 @@ class Room::Private return requestSetState(EvT(std::forward(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; @@ -2041,6 +2049,52 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) 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(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); @@ -2052,10 +2106,16 @@ 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(*ep); + if (is(*ep)) + return true; + if (auto* msgEvent = eventCast(ep)) + return msgEvent->replacedEvent().isEmpty(); + + return false; } Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) @@ -2067,18 +2127,19 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) // 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())) + auto it = std::find_if(events.begin(), events.end(), isEditing); + for(const auto& eptr: RoomEventsRange(it, events.end())) + { if (auto* r = eventCast(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, + auto targetIt = std::find_if(events.begin(), it, [id=r->redactedEvent()] (const RoomEventPtr& ep) { return ep->id() == id; }); - if (targetIt != redactionIt) + if (targetIt != it) *targetIt = makeRedacted(**targetIt, *r); else qCDebug(MAIN) << "Redaction" << r->id() @@ -2086,6 +2147,25 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) << "is not found"; // If the target event comes later, it comes already redacted. } + if (auto* msg = eventCast(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 -- cgit v1.2.3 From b89d5c43746ed672d98bf350db2a9e1b0878e2d0 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 18 May 2019 13:52:54 +0900 Subject: Support for receiving m.reaction events * struct EventRelation; class ReactionEvent; Room::relatedEvents() * Reaction events are processed in both history and sync batches * Redacting a reaction removes it from the list of related events * QMCTest::sendReaction() --- lib/events/reactionevent.cpp | 44 ++++++++++++++++ lib/events/reactionevent.h | 78 ++++++++++++++++++++++++++++ lib/room.cpp | 119 +++++++++++++++++++++++++++++-------------- lib/room.h | 7 +++ 4 files changed, 210 insertions(+), 38 deletions(-) create mode 100644 lib/events/reactionevent.cpp create mode 100644 lib/events/reactionevent.h (limited to 'lib') diff --git a/lib/events/reactionevent.cpp b/lib/events/reactionevent.cpp new file mode 100644 index 00000000..0081edc2 --- /dev/null +++ b/lib/events/reactionevent.cpp @@ -0,0 +1,44 @@ +/****************************************************************************** + * Copyright (C) 2019 Kitsune Ral + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "reactionevent.h" + +using namespace QMatrixClient; + +void QMatrixClient::JsonObjectConverter::dumpTo( + QJsonObject& jo, const EventRelation& pod) +{ + if (pod.type.isEmpty()) { + qCWarning(MAIN) << "Empty relation type; won't dump to JSON"; + return; + } + jo.insert(QStringLiteral("rel_type"), pod.type); + jo.insert(EventIdKey, pod.eventId); + if (pod.type == EventRelation::Annotation()) + jo.insert(QStringLiteral("key"), pod.key); +} + +void QMatrixClient::JsonObjectConverter::fillFrom( + const QJsonObject& jo, EventRelation& pod) +{ + // The experimental logic for generic relationships (MSC1849) + fromJson(jo["rel_type"_ls], pod.type); + fromJson(jo[EventIdKeyL], pod.eventId); + if (pod.type == EventRelation::Annotation()) + fromJson(jo["key"_ls], pod.key); +} diff --git a/lib/events/reactionevent.h b/lib/events/reactionevent.h new file mode 100644 index 00000000..9a6adbbd --- /dev/null +++ b/lib/events/reactionevent.h @@ -0,0 +1,78 @@ +/****************************************************************************** + * Copyright (C) 2019 Kitsune Ral + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "roomevent.h" + +namespace QMatrixClient { + +struct EventRelation +{ + using reltypeid_t = const char*; + static constexpr reltypeid_t Reply() { return "m.in_reply_to"; } + static constexpr reltypeid_t Annotation() { return "m.annotation"; } + static constexpr reltypeid_t Replacement() { return "m.replace"; } + + QString type; + QString eventId; + QString key = {}; // Only used for m.annotation for now + + static EventRelation replyTo(QString eventId) + { + return { Reply(), std::move(eventId) }; + } + static EventRelation annotate(QString eventId, QString key) + { + return { Annotation(), std::move(eventId), std::move(key) }; + } + static EventRelation replace(QString eventId) + { + return { Replacement(), std::move(eventId) }; + } +}; +template <> +struct JsonObjectConverter +{ + static void dumpTo(QJsonObject& jo, const EventRelation& pod); + static void fillFrom(const QJsonObject& jo, EventRelation& pod); +}; + +class ReactionEvent : public RoomEvent +{ +public: + DEFINE_EVENT_TYPEID("m.reaction", ReactionEvent) + + explicit ReactionEvent(const EventRelation& value) + : RoomEvent(typeId(), matrixTypeId(), + { { QStringLiteral("m.relates_to"), toJson(value) } }) + {} + explicit ReactionEvent(const QJsonObject& obj) + : RoomEvent(typeId(), obj) + {} + EventRelation relation() const + { + return content(QStringLiteral("m.relates_to")); + } + +private: + EventRelation _relation; +}; +REGISTER_EVENT_TYPE(ReactionEvent) + +} // namespace QMatrixClient diff --git a/lib/room.cpp b/lib/room.cpp index 958991b7..8b05568a 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -38,6 +38,7 @@ #include "events/roommemberevent.h" #include "events/typingevent.h" #include "events/receiptevent.h" +#include "events/reactionevent.h" #include "events/callinviteevent.h" #include "events/callcandidatesevent.h" #include "events/callanswerevent.h" @@ -106,7 +107,10 @@ class Room::Private Timeline timeline; PendingEvents unsyncedEvents; QHash 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, RelatedEvents> relations; QString displayname; Avatar avatar; int highlightCount = 0; @@ -724,10 +728,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) @@ -743,6 +747,18 @@ Room::findPendingEvent(const QString& txnId) const [txnId] (const auto& item) { return item->transactionId() == txnId; }); } +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. @@ -2044,6 +2060,14 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) updateDisplayname(); } } + if (const auto* reaction = eventCast(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; @@ -2124,45 +2148,49 @@ 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 it = std::find_if(events.begin(), events.end(), isEditing); - for(const auto& eptr: RoomEventsRange(it, events.end())) { - if (auto* r = eventCast(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(eptr)) { - if (!msg->replacedEvent().isEmpty()) { - if (processReplacement(*msg)) + // 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(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=msg->replacedEvent()] (const RoomEventPtr& ep) { - return ep->id() == id; - }); + [id = r->redactedEvent()]( + 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. + *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(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. + } } } } @@ -2231,6 +2259,14 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) if (totalInserted > 0) { + for (auto it = from; it != timeline.cend(); ++it) { + if (const auto* reaction = it->viewAs()) { + 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(); @@ -2289,6 +2325,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()) { + const auto& relation = reaction->relation(); + relations[{ relation.eventId, relation.type }] << reaction; + emit q->updatedEvent(relation.eventId); + } + } if (from <= q->readMarker()) updateUnreadCount(from, timeline.crend()); diff --git a/lib/room.h b/lib/room.h index c79ca1e0..65b9070a 100644 --- a/lib/room.h +++ b/lib/room.h @@ -121,6 +121,7 @@ namespace QMatrixClient public: using Timeline = std::deque; using PendingEvents = std::vector; + using RelatedEvents = QVector; using rev_iter_t = Timeline::const_reverse_iterator; using timeline_iter_t = Timeline::const_iterator; @@ -264,6 +265,11 @@ namespace QMatrixClient PendingEvents::iterator findPendingEvent(const QString & txnId); PendingEvents::const_iterator findPendingEvent(const QString & txnId) const; + const RelatedEvents relatedEvents(const QString& evtId, + const char* relType) const; + const RelatedEvents relatedEvents(const RoomEvent& evt, + const char* relType) const; + bool displayed() const; /// Mark the room as currently displayed to the user /** @@ -584,6 +590,7 @@ namespace QMatrixClient void tagsAboutToChange(); void tagsChanged(); + void updatedEvent(QString eventId); void replacedEvent(const RoomEvent* newEvent, const RoomEvent* oldEvent); -- cgit v1.2.3 From 1ea94b918569dd26452f285c408e605f9dc15343 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 30 Jul 2019 07:53:34 +0900 Subject: Room::postReaction() --- lib/room.cpp | 5 +++++ lib/room.h | 2 ++ 2 files changed, 7 insertions(+) (limited to 'lib') diff --git a/lib/room.cpp b/lib/room.cpp index 8b05568a..6519db12 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1603,6 +1603,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(EventRelation::annotate(eventId, key)); +} + QString Room::postFile(const QString& plainText, const QUrl& localPath, bool asGenericFile) { diff --git a/lib/room.h b/lib/room.h index 65b9070a..470bb7f9 100644 --- a/lib/room.h +++ b/lib/room.h @@ -430,6 +430,8 @@ namespace QMatrixClient const QString& html, MessageEventType type = MessageEventType::Text); QString postHtmlText(const QString& plainText, const QString& html); + /// Send a reaction on a given event with a given key + QString postReaction(const QString& eventId, const QString& key); QString postFile(const QString& plainText, const QUrl& localPath, bool asGenericFile = false); /** Post a pre-created room message event -- cgit v1.2.3