aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKitsune Ral <Kitsune-Ral@users.sf.net>2019-08-02 09:32:49 +0900
committerKitsune Ral <Kitsune-Ral@users.sf.net>2019-08-02 09:32:49 +0900
commit35cfef7a4253d49a37e5ce21c337fbb3d2633c42 (patch)
treedfcc77c4e404b6b8f07a4c9ee6283e70e4723883
parent5b37e15d6a57d3b689c88f5cfce7afea9787a034 (diff)
parent5b236dfe895c7766002559570aa29c9033009228 (diff)
downloadlibquotient-35cfef7a4253d49a37e5ce21c337fbb3d2633c42.tar.gz
libquotient-35cfef7a4253d49a37e5ce21c337fbb3d2633c42.zip
Merge branch 'master' into use-clang-format
-rw-r--r--.appveyor.yml9
-rw-r--r--CMakeLists.txt2
-rw-r--r--examples/qmc-example.cpp255
-rw-r--r--lib/connection.cpp75
-rw-r--r--lib/connection.h6
-rw-r--r--lib/converters.h85
-rw-r--r--lib/e2ee.h30
-rw-r--r--lib/encryptionmanager.cpp34
-rw-r--r--lib/encryptionmanager.h7
-rw-r--r--lib/events/encryptedevent.cpp32
-rw-r--r--lib/events/encryptedevent.h68
-rw-r--r--lib/events/encryptionevent.cpp18
-rw-r--r--lib/events/event.h8
-rw-r--r--lib/events/reactionevent.cpp44
-rw-r--r--lib/events/reactionevent.h79
-rw-r--r--lib/events/roomevent.cpp14
-rw-r--r--lib/events/roomevent.h2
-rw-r--r--lib/events/roommessageevent.cpp107
-rw-r--r--lib/events/roommessageevent.h3
-rw-r--r--lib/jobs/basejob.cpp2
-rw-r--r--lib/jobs/basejob.h3
-rw-r--r--lib/room.cpp273
-rw-r--r--lib/room.h19
-rw-r--r--libqmatrixclient.pri4
24 files changed, 925 insertions, 254 deletions
diff --git a/.appveyor.yml b/.appveyor.yml
index 4e2d4b5d..f596b856 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -1,14 +1,13 @@
-image: Visual Studio 2015
+image: Visual Studio 2017
environment:
#DEPLOY_DIR: libqmatrixclient-%APPVEYOR_BUILD_VERSION%
matrix:
- - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
- QTDIR: C:\Qt\5.9\msvc2017_64
+ - QTDIR: C:\Qt\5.12\msvc2017_64
VCVARS: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat"
PLATFORM:
- - QTDIR: C:\Qt\5.9\msvc2015
- VCVARS: "C:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\VC\\vcvarsall.bat"
+ - QTDIR: C:\Qt\5.12\msvc2017
+ VCVARS: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Auxiliary\\Build\\vcvars32.bat"
PLATFORM: x86
init:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index a40d4385..996e76d4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -143,12 +143,14 @@ set(libqmatrixclient_SRCS
lib/events/roommemberevent.cpp
lib/events/typingevent.cpp
lib/events/receiptevent.cpp
+ lib/events/reactionevent.cpp
lib/events/callanswerevent.cpp
lib/events/callcandidatesevent.cpp
lib/events/callhangupevent.cpp
lib/events/callinviteevent.cpp
lib/events/directchatevent.cpp
lib/events/encryptionevent.cpp
+ lib/events/encryptedevent.cpp
lib/jobs/requestdata.cpp
lib/jobs/basejob.cpp
lib/jobs/syncjob.cpp
diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp
index 36fc61c4..d6cba76a 100644
--- a/examples/qmc-example.cpp
+++ b/examples/qmc-example.cpp
@@ -1,17 +1,21 @@
#include "connection.h"
+#include "room.h"
+#include "user.h"
+
#include "csapi/joining.h"
#include "csapi/leaving.h"
#include "csapi/room_send.h"
+
+#include "events/reactionevent.h"
#include "events/simplestateevents.h"
-#include "room.h"
-#include "user.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QFileInfo>
#include <QtCore/QStringBuilder>
#include <QtCore/QTemporaryFile>
#include <QtCore/QTimer>
+
#include <functional>
#include <iostream>
@@ -22,28 +26,33 @@ using namespace std::placeholders;
class QMCTest : public QObject
{
- public:
+public:
QMCTest(Connection* conn, QString testRoomName, QString source);
- private slots:
+private slots:
+ // clang-format off
void setupAndRun();
void onNewRoom(Room* r);
void run();
void doTests();
- void loadMembers();
- void sendMessage();
- void sendFile();
- void checkFileSendingOutcome(const QString& txnId, const QString& fileName);
- void setTopic();
- void addAndRemoveTag();
- void sendAndRedact();
- bool checkRedactionOutcome(const QString& evtIdToRedact);
- void markDirectChat();
- void checkDirectChatOutcome(const Connection::DirectChatsMap& added);
+ void loadMembers();
+ void sendMessage();
+ void sendReaction(const QString& targetEvtId);
+ void sendFile();
+ void checkFileSendingOutcome(const QString& txnId,
+ const QString& fileName);
+ void setTopic();
+ void addAndRemoveTag();
+ void sendAndRedact();
+ bool checkRedactionOutcome(const QString& evtIdToRedact);
+ void markDirectChat();
+ void checkDirectChatOutcome(
+ const Connection::DirectChatsMap& added);
void conclude();
void finalize();
+ // clang-format on
- private:
+private:
QScopedPointer<Connection, QScopedPointerDeleteLater> c;
QStringList running;
QStringList succeeded;
@@ -55,37 +64,37 @@ class QMCTest : public QObject
bool validatePendingEvent(const QString& txnId);
};
-#define QMC_CHECK(description, condition) \
- { \
- Q_ASSERT(running.removeOne(description)); \
- if (!!(condition)) { \
- succeeded.push_back(description); \
- cout << (description) << " successful" << endl; \
- if (targetRoom) \
- targetRoom->postMessage(origin % ": " % (description) \
- % " successful", \
- MessageEventType::Notice); \
- } else { \
- failed.push_back(description); \
- cout << (description) << " FAILED" << endl; \
- if (targetRoom) \
- targetRoom->postPlainText(origin % ": " % (description) \
- % " FAILED"); \
- } \
+#define QMC_CHECK(description, condition) \
+ { \
+ Q_ASSERT(running.removeOne(description)); \
+ if (!!(condition)) { \
+ succeeded.push_back(description); \
+ cout << (description) << " successful" << endl; \
+ if (targetRoom) \
+ targetRoom->postMessage(origin % ": " % (description) \
+ % " successful", \
+ MessageEventType::Notice); \
+ } else { \
+ failed.push_back(description); \
+ cout << (description) << " FAILED" << endl; \
+ if (targetRoom) \
+ targetRoom->postPlainText(origin % ": " % (description) \
+ % " FAILED"); \
+ } \
}
bool QMCTest::validatePendingEvent(const QString& txnId)
{
auto it = targetRoom->findPendingEvent(txnId);
return it != targetRoom->pendingEvents().end()
- && it->deliveryStatus() == EventStatus::Submitted
- && (*it)->transactionId() == txnId;
+ && it->deliveryStatus() == EventStatus::Submitted
+ && (*it)->transactionId() == txnId;
}
QMCTest::QMCTest(Connection* conn, QString testRoomName, QString source)
- : c(conn),
- origin(std::move(source)),
- targetRoomName(std::move(testRoomName))
+ : c(conn)
+ , origin(std::move(source))
+ , targetRoomName(std::move(testRoomName))
{
if (!origin.isEmpty())
cout << "Origin for the test message: " << origin.toStdString() << endl;
@@ -114,9 +123,8 @@ void QMCTest::setupAndRun()
QMC_CHECK("Join room", false);
conclude();
});
- // Connection::joinRoom() creates a Room object upon
- // JoinRoomJob::success but this object is empty until the first sync is
- // done.
+ // Connection::joinRoom() creates a Room object upon JoinRoomJob::success
+ // but this object is empty until the first sync is done.
connect(joinJob, &BaseJob::success, this, [this, joinJob] {
targetRoom = c->room(joinJob->roomId(), JoinState::Join);
QMC_CHECK("Join room", targetRoom != nullptr);
@@ -183,13 +191,10 @@ void QMCTest::doTests()
void QMCTest::loadMembers()
{
running.push_back("Loading members");
- // The dedicated qmc-test room is too small to test
- // lazy-loading-then-full-loading; use #qmatrixclient:matrix.org instead.
- // TODO: #264
- auto* r = c->room(QStringLiteral("!PCzUtxtOjUySxSelof:matrix.org"));
+ auto* r = c->roomByAlias(QStringLiteral("#quotient:matrix.org"),
+ JoinState::Join);
if (!r) {
- cout << "#test:matrix.org is not found in the test user's rooms"
- << endl;
+ cout << "#test:matrix.org is not found in the test user's rooms" << endl;
QMC_CHECK("Loading members", false);
return;
}
@@ -219,23 +224,60 @@ void QMCTest::sendMessage()
return;
}
- connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this,
- [this, txnId](const RoomEvent* evt, int pendingIdx) {
- const auto& pendingEvents = targetRoom->pendingEvents();
- Q_ASSERT(pendingIdx >= 0
- && pendingIdx < int(pendingEvents.size()));
-
- if (evt->transactionId() != txnId)
- return false;
-
- QMC_CHECK("Message sending",
- is<RoomMessageEvent>(*evt)
- && !evt->id().isEmpty()
- && pendingEvents[size_t(pendingIdx)]
- ->transactionId()
- == evt->transactionId());
- return true;
- });
+ connectUntil(
+ targetRoom, &Room::pendingEventAboutToMerge, this,
+ [this, txnId](const RoomEvent* evt, int pendingIdx) {
+ const auto& pendingEvents = targetRoom->pendingEvents();
+ Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size()));
+
+ if (evt->transactionId() != txnId)
+ return false;
+
+ QMC_CHECK("Message sending",
+ is<RoomMessageEvent>(*evt) && !evt->id().isEmpty()
+ && pendingEvents[size_t(pendingIdx)]->transactionId()
+ == evt->transactionId());
+ sendReaction(evt->id());
+ return true;
+ });
+}
+
+void QMCTest::sendReaction(const QString& targetEvtId)
+{
+ running.push_back("Reaction sending");
+ cout << "Reacting to the newest message in the room" << endl;
+ Q_ASSERT(targetRoom->timelineSize() > 0);
+ const auto key = QStringLiteral("+1");
+ auto txnId = targetRoom->postReaction(targetEvtId, key);
+ if (!validatePendingEvent(txnId)) {
+ cout << "Invalid pending event right after submitting" << endl;
+ QMC_CHECK("Reaction sending", false);
+ return;
+ }
+
+ // TODO: Check that it came back as a reaction event and that it attached to
+ // the right event
+ connectUntil(
+ targetRoom, &Room::updatedEvent, this,
+ [this, txnId, key, targetEvtId](const QString& actualTargetEvtId) {
+ if (actualTargetEvtId != targetEvtId)
+ return false;
+ const auto reactions = targetRoom->relatedEvents(
+ targetEvtId, EventRelation::Annotation());
+ // It's a test room, assuming no interference there should
+ // be exactly one reaction
+ if (reactions.size() != 1) {
+ QMC_CHECK("Reaction sending", false);
+ } else {
+ const auto* evt =
+ eventCast<const ReactionEvent>(reactions.back());
+ QMC_CHECK("Reaction sending",
+ is<ReactionEvent>(*evt) && !evt->id().isEmpty()
+ && evt->relation().key == key
+ && evt->transactionId() == txnId);
+ }
+ return true;
+ });
}
void QMCTest::sendFile()
@@ -254,8 +296,8 @@ void QMCTest::sendFile()
// the full path
const auto tfName = QFileInfo(*tf).fileName();
cout << "Sending file" << tfName.toStdString() << endl;
- const auto txnId = targetRoom->postFile(
- "Test file", QUrl::fromLocalFile(tf->fileName()));
+ const auto txnId =
+ targetRoom->postFile("Test file", QUrl::fromLocalFile(tf->fileName()));
if (!validatePendingEvent(txnId)) {
cout << "Invalid pending event right after submitting" << endl;
QMC_CHECK("File sending", false);
@@ -307,41 +349,37 @@ void QMCTest::checkFileSendingOutcome(const QString& txnId,
}
connectUntil(
- targetRoom, &Room::pendingEventAboutToMerge, this,
- [this, txnId, fileName](const RoomEvent* evt, int pendingIdx) {
- const auto& pendingEvents = targetRoom->pendingEvents();
- Q_ASSERT(pendingIdx >= 0
- && pendingIdx < int(pendingEvents.size()));
-
- if (evt->transactionId() != txnId)
- return false;
-
- cout << "File event " << txnId.toStdString()
- << " arrived in the timeline" << endl;
- visit(*evt,
- [&](const RoomMessageEvent& e) {
- QMC_CHECK("File sending",
- !e.id().isEmpty()
- && pendingEvents[size_t(pendingIdx)]
- ->transactionId()
- == txnId
- && e.hasFileContent()
- && e.content()->fileInfo()
- ->originalName
- == fileName);
- },
- [this](const RoomEvent&) {
- QMC_CHECK("File sending", false);
- });
- return true;
- });
+ targetRoom, &Room::pendingEventAboutToMerge, this,
+ [this, txnId, fileName](const RoomEvent* evt, int pendingIdx) {
+ const auto& pendingEvents = targetRoom->pendingEvents();
+ Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size()));
+
+ if (evt->transactionId() != txnId)
+ return false;
+
+ cout << "File event " << txnId.toStdString()
+ << " arrived in the timeline" << endl;
+ visit(
+ *evt,
+ [&](const RoomMessageEvent& e) {
+ QMC_CHECK(
+ "File sending",
+ !e.id().isEmpty()
+ && pendingEvents[size_t(pendingIdx)]->transactionId()
+ == txnId
+ && e.hasFileContent()
+ && e.content()->fileInfo()->originalName == fileName);
+ },
+ [this](const RoomEvent&) { QMC_CHECK("File sending", false); });
+ return true;
+ });
}
void QMCTest::setTopic()
{
static const char* const stateTestName = "State setting test";
static const char* const fakeStateTestName =
- "Fake state event immunity test";
+ "Fake state event immunity test";
running.push_back(stateTestName);
running.push_back(fakeStateTestName);
auto initialTopic = targetRoom->topic();
@@ -368,8 +406,7 @@ void QMCTest::setTopic()
connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this,
[this, fakeTopic, initialTopic](const RoomEvent* e, int) {
- if (e->contentJson().value("topic").toString()
- != fakeTopic)
+ if (e->contentJson().value("topic").toString() != fakeTopic)
return false; // Wait on for the right event
QMC_CHECK(fakeStateTestName, !e->isStateEvent());
@@ -420,9 +457,10 @@ void QMCTest::sendAndRedact()
cout << "Redacting the message" << endl;
targetRoom->redactEvent(evtId, origin);
- connectUntil(
- targetRoom, &Room::addedMessages, this,
- [this, evtId] { return checkRedactionOutcome(evtId); });
+ connectUntil(targetRoom, &Room::addedMessages, this,
+ [this, evtId] {
+ return checkRedactionOutcome(evtId);
+ });
});
}
@@ -449,8 +487,7 @@ bool QMCTest::checkRedactionOutcome(const QString& evtIdToRedact)
QMC_CHECK("Redaction",
newEvent->isRedacted()
- && newEvent->redactionReason()
- == origin);
+ && newEvent->redactionReason() == origin);
return true;
});
}
@@ -498,23 +535,23 @@ void QMCTest::conclude()
c->stopSync();
auto succeededRec = QString::number(succeeded.size()) + " tests succeeded";
if (!failed.isEmpty() || !running.isEmpty())
- succeededRec += " of "
- % QString::number(succeeded.size() + failed.size()
- + running.size())
- % " total";
+ succeededRec +=
+ " of "
+ % QString::number(succeeded.size() + failed.size() + running.size())
+ % " total";
QString plainReport = origin % ": Testing complete, " % succeededRec;
QString color = failed.isEmpty() && running.isEmpty() ? "00AA00" : "AA0000";
QString htmlReport = origin % ": <strong><font data-mx-color='#" % color
- % "' color='#" % color % "'>Testing complete</font></strong>, "
- % succeededRec;
+ % "' color='#" % color
+ % "'>Testing complete</font></strong>, " % succeededRec;
if (!failed.isEmpty()) {
plainReport += "\nFAILED: " % failed.join(", ");
htmlReport += "<br><strong>Failed:</strong> " % failed.join(", ");
}
if (!running.isEmpty()) {
plainReport += "\nDID NOT FINISH: " % running.join(", ");
- htmlReport +=
- "<br><strong>Did not finish:</strong> " % running.join(", ");
+ htmlReport += "<br><strong>Did not finish:</strong> "
+ % running.join(", ");
}
cout << plainReport.toStdString() << endl;
diff --git a/lib/connection.cpp b/lib/connection.cpp
index 52a693f7..6ebe05dc 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -33,7 +33,9 @@
#include "csapi/receipts.h"
#include "csapi/room_send.h"
#include "csapi/to_device.h"
+#include "csapi/versions.h"
#include "csapi/voip.h"
+#include "csapi/wellknown.h"
#include "events/directchatevent.h"
#include "events/eventloader.h"
@@ -176,36 +178,50 @@ void Connection::resolveServer(const QString& mxid)
}
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();
- } else {
- qCDebug(MAIN) << baseUrl.host() << "doesn't have SRV record"
- << dns->name() << "- using the hostname as is";
- }
- setHomeserver(baseUrl);
- emit resolved();
- dns->deleteLater();
- });
- dns->lookup();
+
+ auto getWellKnownJob = callApi<GetWellknownJob>();
+ 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.toString();
+ setHomeserver(baseUrl);
+ }
+
+ auto getVersionsJob = callApi<GetVersionsJob>();
+
+ 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"));
+ }
+ });
+ });
}
void Connection::connectToServer(const QString& user, const QString& password,
@@ -921,6 +937,11 @@ QString Connection::deviceId() const { return d->data->deviceId(); }
QByteArray Connection::accessToken() const { return d->data->accessToken(); }
+QtOlm::Account* Connection::olmAccount() const
+{
+ return d->encryptionManager->account();
+}
+
SyncJob* Connection::syncJob() const { return d->syncJob; }
int Connection::millisToReconnect() const
diff --git a/lib/connection.h b/lib/connection.h
index ef6cc156..8d65f0e7 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -32,6 +32,11 @@
#include <functional>
+namespace QtOlm
+{
+class Account;
+}
+
namespace QMatrixClient
{
class Room;
@@ -274,6 +279,7 @@ public:
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/converters.h b/lib/converters.h
index 3ba65c22..aa07261d 100644
--- a/lib/converters.h
+++ b/lib/converters.h
@@ -62,8 +62,8 @@ namespace QMatrixClient
template <typename T>
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 <typename T>
@@ -91,14 +91,16 @@ inline auto toJson(const T& pod)
return JsonConverter<T>::dump(pod);
}
+inline auto toJson(const QJsonObject& jo) { return jo; }
+
template <typename T>
-inline auto fillJson(QJsonObject& json, const T& data)
+inline void fillJson(QJsonObject& json, const T& data)
{
JsonObjectConverter<T>::dumpTo(json, data);
}
template <typename T>
-inline auto fromJson(const QJsonValue& jv)
+inline T fromJson(const QJsonValue& jv)
{
return JsonConverter<T>::load(jv);
}
@@ -109,11 +111,14 @@ inline T fromJson(const QJsonDocument& jd)
return JsonConverter<T>::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 <typename T>
inline void fromJson(const QJsonValue& jv, T& pod)
{
- if (!jv.isUndefined())
- pod = fromJson<T>(jv);
+ pod = jv.isUndefined() ? T() : fromJson<T>(jv);
}
template <typename T>
@@ -122,21 +127,13 @@ inline void fromJson(const QJsonDocument& jd, T& pod)
pod = fromJson<T>(jd);
}
-// Unfolds Omittable<>
-template <typename T>
-inline void fromJson(const QJsonValue& jv, Omittable<T>& pod)
-{
- if (jv.isUndefined())
- pod = none;
- else
- pod = fromJson<T>(jv);
-}
-
template <typename T>
inline void fillFromJson(const QJsonValue& jv, T& pod)
{
if (jv.isObject())
JsonObjectConverter<T>::fillFrom(jv.toObject(), pod);
+ else if (!jv.isUndefined())
+ pod = fromJson<T>(jv);
}
// JsonConverter<> specialisations
@@ -228,6 +225,21 @@ struct JsonConverter<QVariant>
static QVariant load(const QJsonValue& jv);
};
+template <typename T>
+struct JsonConverter<Omittable<T>>
+{
+ static QJsonValue dump(const Omittable<T>& from)
+ {
+ return from.omitted() ? QJsonValue() : toJson(from.value());
+ }
+ static Omittable<T> load(const QJsonValue& jv)
+ {
+ if (jv.isUndefined())
+ return none;
+ return fromJson<T>(jv);
+ }
+};
+
template <typename VectorT, typename T = typename VectorT::value_type>
struct JsonArrayConverter
{
@@ -369,7 +381,8 @@ namespace _impl
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 <typename ValT, bool Force = true, typename = bool>
struct AddNode
{
@@ -381,7 +394,7 @@ namespace _impl
}
};
- // This one is for types that have isEmpty()
+ // This one is for types that have isEmpty() when Force is false
template <typename ValT>
struct AddNode<ValT, false, decltype(std::declval<ValT>().isEmpty())>
{
@@ -390,23 +403,20 @@ namespace _impl
ForwardedT&& value)
{
if (!value.isEmpty())
- AddNode<ValT>::impl(container, key,
- std::forward<ForwardedT>(value));
+ addTo(container, key, std::forward<ForwardedT>(value));
}
};
- // This is a special one that unfolds Omittable<>
- template <typename ValT, bool Force>
- struct AddNode<Omittable<ValT>, Force>
+ // This one unfolds Omittable<> (also only when Force is false)
+ template <typename ValT>
+ struct AddNode<Omittable<ValT>, false>
{
template <typename ContT, typename OmittableT>
static void impl(ContT& container, const QString& key,
const OmittableT& value)
{
if (!value.omitted())
- AddNode<ValT>::impl(container, key, value.value());
- else if (Force) // Edge case, no value but must put something
- AddNode<ValT>::impl(container, key, QString {});
+ addTo(container, key, value.value());
}
};
@@ -431,6 +441,29 @@ namespace _impl
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 <bool Force = true, typename ContT, typename ValT>
inline void addParam(ContT& container, const QString& key, ValT&& value)
{
diff --git a/lib/e2ee.h b/lib/e2ee.h
new file mode 100644
index 00000000..11f411f1
--- /dev/null
+++ b/lib/e2ee.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <QtCore/QStringList>
+
+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;
+
+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 61fcf9b4..46d937b8 100644
--- a/lib/encryptionmanager.cpp
+++ b/lib/encryptionmanager.cpp
@@ -1,6 +1,7 @@
#include "encryptionmanager.h"
#include "connection.h"
+#include "e2ee.h"
#include "csapi/keys.h"
@@ -15,16 +16,6 @@ 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:
@@ -56,9 +47,9 @@ public:
*/
targetKeysNumber = olmAccount->maxOneTimeKeys(); // 2 // see note below
targetOneTimeKeyCounts = {
- { SignedCurve25519Name,
+ { SignedCurve25519Key,
qRound(signedKeysProportion * targetKeysNumber) },
- { Curve25519Name,
+ { Curve25519Key,
qRound((1 - signedKeysProportion) * targetKeysNumber) }
};
}
@@ -115,9 +106,9 @@ void EncryptionManager::uploadIdentityKeys(Connection* connection)
* format <algorithm>:<device_id>. The keys themselves should be encoded
* as specified by the key algorithm.
*/
- { { Curve25519Name + QStringLiteral(":") + connection->deviceId(),
+ { { Curve25519Key + QStringLiteral(":") + connection->deviceId(),
d->olmAccount->curve25519IdentityKey() },
- { ed25519Name + QStringLiteral(":") + connection->deviceId(),
+ { Ed25519Key + QStringLiteral(":") + connection->deviceId(),
d->olmAccount->ed25519IdentityKey() } },
/* signatures should be provided after the unsigned deviceKeys
generation */
@@ -138,7 +129,7 @@ void EncryptionManager::uploadIdentityKeys(Connection* connection)
*/
deviceKeys.signatures = {
{ connection->userId(),
- { { ed25519Name + QStringLiteral(":") + connection->deviceId(),
+ { { Ed25519Key + QStringLiteral(":") + connection->deviceId(),
d->olmAccount->sign(deviceKeysJsonObject) } } }
};
@@ -160,9 +151,9 @@ void EncryptionManager::uploadOneTimeKeys(Connection* connection,
}
int signedKeysToUploadCount =
- d->oneTimeKeysToUploadCounts.value(SignedCurve25519Name, 0);
+ d->oneTimeKeysToUploadCounts.value(SignedCurve25519Key, 0);
int unsignedKeysToUploadCount =
- d->oneTimeKeysToUploadCounts.value(Curve25519Name, 0);
+ d->oneTimeKeysToUploadCounts.value(Curve25519Key, 0);
d->olmAccount->generateOneTimeKeys(signedKeysToUploadCount
+ unsignedKeysToUploadCount);
@@ -181,11 +172,11 @@ void EncryptionManager::uploadOneTimeKeys(Connection* connection,
QJsonObject message { { 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);
@@ -204,6 +195,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();
diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h
index a41d88e1..02bb882f 100644
--- a/lib/encryptionmanager.h
+++ b/lib/encryptionmanager.h
@@ -5,6 +5,11 @@
#include <functional>
#include <memory>
+namespace QtOlm
+{
+class Account;
+}
+
namespace QMatrixClient
{
class Connection;
@@ -26,6 +31,8 @@ public:
void uploadOneTimeKeys(Connection* connection, bool forceUpdate = false);
QByteArray olmAccountPickle();
+ QtOlm::Account* account() const;
+
private:
class Private;
std::unique_ptr<Private> d;
diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp
new file mode 100644
index 00000000..dac245fa
--- /dev/null
+++ b/lib/events/encryptedevent.cpp
@@ -0,0 +1,32 @@
+#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..0dbce25c
--- /dev/null
+++ b/lib/events/encryptedevent.h
@@ -0,0 +1,68 @@
+#pragma once
+
+#include "e2ee.h"
+#include "roomevent.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<QString>(AlgorithmKeyL);
+ if (!SupportedAlgorithms.contains(algo)) {
+ qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo
+ << "is not supported";
+ }
+ return algo;
+ }
+ QByteArray ciphertext() const
+ {
+ return content<QString>(CiphertextKeyL).toLatin1();
+ }
+ QJsonObject ciphertext(const QString& identityKey) const
+ {
+ return content<QJsonObject>(CiphertextKeyL).value(identityKey).toObject();
+ }
+ QString senderKey() const { return content<QString>(SenderKeyKeyL); }
+
+ /* device_id and session_id are required with Megolm */
+ QString deviceId() const { return content<QString>(DeviceIdKeyL); }
+ QString sessionId() const { return content<QString>(SessionIdKeyL); }
+};
+REGISTER_EVENT_TYPE(EncryptedEvent)
+
+} // namespace QMatrixClient
diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp
index 6aa7063b..995c8dad 100644
--- a/lib/events/encryptionevent.cpp
+++ b/lib/events/encryptionevent.cpp
@@ -6,12 +6,14 @@
#include "encryptionevent.h"
#include "converters.h"
+#include "e2ee.h"
#include "logging.h"
#include <array>
-static const std::array<QString, 1> encryptionStrings = { { QStringLiteral(
- "m.megolm.v1.aes-sha2") } };
+static const std::array<QString, 1> encryptionStrings = {
+ { QMatrixClient::MegolmV1AesSha2AlgoKey }
+};
namespace QMatrixClient
{
@@ -36,9 +38,9 @@ using namespace QMatrixClient;
EncryptionEventContent::EncryptionEventContent(const QJsonObject& json)
: encryption(fromJson<EncryptionType>(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
@@ -48,7 +50,7 @@ void EncryptionEventContent::fillJson(QJsonObject* o) const
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);
}
diff --git a/lib/events/event.h b/lib/events/event.h
index 8056ccbe..d6525281 100644
--- a/lib/events/event.h
+++ b/lib/events/event.h
@@ -58,11 +58,13 @@ ptrCast(event_ptr_tt<SourceT>&& ptr)
// === Standard Matrix key names and basicEventJson() ===
static const auto TypeKey = QStringLiteral("type");
+static const auto BodyKey = QStringLiteral("body");
static const auto ContentKey = QStringLiteral("content");
static const auto EventIdKey = QStringLiteral("event_id");
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;
@@ -391,10 +393,8 @@ template <typename BaseEventT, typename FnT>
inline std::enable_if_t<is_event<BaseEventT>() && needs_cast<BaseEventT, FnT>(),
fn_return_t<FnT>> // non-voidness is guarded by
// defaultValue type
- visit(const BaseEventT& event,
- FnT&& visitor,
- fn_return_t<FnT>&&
- defaultValue = {})
+visit(const BaseEventT& event, FnT&& visitor,
+ fn_return_t<FnT>&& defaultValue = {})
{
using event_type = fn_arg_t<FnT>;
if (is<std::decay_t<event_type>>(event))
diff --git a/lib/events/reactionevent.cpp b/lib/events/reactionevent.cpp
new file mode 100644
index 00000000..0a080607
--- /dev/null
+++ b/lib/events/reactionevent.cpp
@@ -0,0 +1,44 @@
+/******************************************************************************
+ * Copyright (C) 2019 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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<EventRelation>::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<EventRelation>::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..d524b549
--- /dev/null
+++ b/lib/events/reactionevent.h
@@ -0,0 +1,79 @@
+/******************************************************************************
+ * Copyright (C) 2019 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * 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<EventRelation>
+{
+ 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<EventRelation>(QStringLiteral("m.relates_to"));
+ }
+
+private:
+ EventRelation _relation;
+};
+REGISTER_EVENT_TYPE(ReactionEvent)
+
+} // namespace QMatrixClient
diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp
index 513a99d0..fb715473 100644
--- a/lib/events/roomevent.cpp
+++ b/lib/events/roomevent.cpp
@@ -62,6 +62,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 dd0d25eb..8edb397c 100644
--- a/lib/events/roomevent.h
+++ b/lib/events/roomevent.h
@@ -51,6 +51,8 @@ public:
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<RedactionEvent>& redactedBecause() const
{
diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp
index c7f17303..da8d59ca 100644
--- a/lib/events/roommessageevent.cpp
+++ b/lib/events/roommessageevent.cpp
@@ -30,12 +30,12 @@ using namespace EventContent;
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 RelatesToKeyL = "m.relates_to"_ls;
+static const auto MsgTypeKeyL = "msgtype"_ls;
+static const auto FormattedBodyKeyL = "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");
@@ -49,7 +49,7 @@ TypedBase* make(const QJsonObject& json)
template <>
TypedBase* make<TextContent>(const QJsonObject& json)
{
- return json.contains(FormattedBodyKey) || json.contains(RelatesToKey)
+ return json.contains(FormattedBodyKeyL) || json.contains(RelatesToKeyL)
? new TextContent(json)
: nullptr;
}
@@ -63,7 +63,7 @@ struct MsgTypeDesc
const std::vector<MsgTypeDesc> msgTypes = {
{ TextTypeKey, MsgType::Text, make<TextContent> },
- { QStringLiteral("m.emote"), MsgType::Emote, make<TextContent> },
+ { EmoteTypeKey, MsgType::Emote, make<TextContent> },
{ NoticeTypeKey, MsgType::Notice, make<TextContent> },
{ QStringLiteral("m.image"), MsgType::Image, make<ImageContent> },
{ QStringLiteral("m.file"), MsgType::File, make<FileContent> },
@@ -101,11 +101,25 @@ QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody,
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(RelatesToKeyL)) {
+ if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey
+ && jsonMsgType != EmoteTypeKey) {
+ json.remove(RelatesToKeyL);
+ qCWarning(EVENTS)
+ << RelatesToKeyL << "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<const TextContent*>(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[BodyKeyL] = "* " + plainBody;
+ }
+ }
}
json.insert(QStringLiteral("msgtype"), jsonMsgType);
json.insert(QStringLiteral("body"), plainBody);
@@ -166,8 +180,8 @@ RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj)
if (isRedacted())
return;
const QJsonObject content = contentJson();
- if (content.contains(MsgTypeKey) && content.contains(BodyKey)) {
- auto msgtype = content[MsgTypeKey].toString();
+ if (content.contains(MsgTypeKeyL) && content.contains(BodyKeyL)) {
+ auto msgtype = content[MsgTypeKeyL].toString();
bool msgTypeFound = false;
for (const auto& mt : msgTypes)
if (mt.matrixType == msgtype) {
@@ -193,12 +207,12 @@ RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const
QString RoomMessageEvent::rawMsgtype() const
{
- return contentJson()[MsgTypeKey].toString();
+ return contentJson()[MsgTypeKeyL].toString();
}
QString RoomMessageEvent::plainBody() const
{
- return contentJson()[BodyKey].toString();
+ return contentJson()[BodyKeyL].toString();
}
QMimeType RoomMessageEvent::mimeType() const
@@ -225,6 +239,17 @@ bool RoomMessageEvent::hasThumbnail() const
return content() && content()->thumbnailInfo();
}
+QString RoomMessageEvent::replacedEvent() const
+{
+ if (!content() || !hasTextContent())
+ return {};
+
+ const auto& rel = static_cast<const TextContent*>(content())->relatesTo;
+ return !rel.omitted() && rel->type == RelatesTo::ReplacementTypeId()
+ ? rel->eventId
+ : QString();
+}
+
QString rawMsgTypeForMimeType(const QMimeType& mimeType)
{
auto name = mimeType.name();
@@ -256,39 +281,71 @@ 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<RelatesTo> 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<QString>(replyJson[EventIdKeyL]));
+
+ return RelatesTo { jo.value("rel_type"_ls).toString(),
+ jo.value(EventIdKeyL).toString() };
+}
+} // namespace QMatrixClient
+
TextContent::TextContent(const QJsonObject& json)
+ : relatesTo(fromJson<Omittable<RelatesTo>>(json[RelatesToKeyL]))
{
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[FormattedBodyKeyL].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[BodyKeyL].toString();
}
- const auto replyJson =
- json[RelatesToKey].toObject().value(RelatesTo::ReplyTypeId()).toObject();
- if (!replyJson.isEmpty())
- relatesTo = replyTo(fromJson<QString>(replyJson[EventIdKeyL]));
}
void TextContent::fillJson(QJsonObject* json) const
{
+ static const auto FormatKey = QStringLiteral("format");
+ static const auto FormattedBodyKey = 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(FormattedBodyKey, body);
}
- if (!relatesTo.omitted())
+ if (!relatesTo.omitted()) {
json->insert(QStringLiteral("m.relates_to"),
QJsonObject { { relatesTo->type, relatesTo->eventId } });
+ if (relatesTo->type == RelatesTo::ReplacementTypeId()) {
+ QJsonObject newContentJson;
+ if (mimeType.inherits("text/html")) {
+ json->insert(FormatKey, HtmlContentTypeId);
+ json->insert(FormattedBodyKey, 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 eabb21e3..1f1fde41 100644
--- a/lib/events/roommessageevent.h
+++ b/lib/events/roommessageevent.h
@@ -77,6 +77,7 @@ public:
bool hasTextContent() const;
bool hasFileContent() const;
bool hasThumbnail() const;
+ QString replacedEvent() const;
static QString rawMsgTypeForUrl(const QUrl& url);
static QString rawMsgTypeForFile(const QFileInfo& fi);
@@ -84,6 +85,7 @@ public:
private:
QScopedPointer<EventContent::TypedBase> _content;
+ // FIXME: should it really be static?
static QJsonObject assembleContentJson(const QString& plainBody,
const QString& jsonMsgType,
EventContent::TypedBase* content);
@@ -101,6 +103,7 @@ namespace EventContent
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/jobs/basejob.cpp b/lib/jobs/basejob.cpp
index f46d2d61..e4a74954 100644
--- a/lib/jobs/basejob.cpp
+++ b/lib/jobs/basejob.cpp
@@ -286,7 +286,7 @@ void BaseJob::gotReply()
QJsonDocument::fromJson(d->rawResponse).object()));
}
- if (error() != TooManyRequestsError)
+ if (status().code != TooManyRequestsError)
finishJob();
else {
stop();
diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h
index d94ab31d..f445d087 100644
--- a/lib/jobs/basejob.h
+++ b/lib/jobs/basejob.h
@@ -61,8 +61,7 @@ public:
WarningLevel = 20,
UnexpectedResponseType = 21,
UnexpectedResponseTypeWarning = UnexpectedResponseType,
- Abandoned = 50 //< A very brief period between abandoning and object
- //deletion
+ Abandoned = 50 //< A tiny period between abandoning and object deletion
,
ErrorLevel = 100 //< Errors have codes starting from this
,
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());
diff --git a/lib/room.h b/lib/room.h
index 7a0e25dc..f5433fb6 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -25,8 +25,10 @@
#include "csapi/message_pagination.h"
#include "events/accountdataevents.h"
+#include "events/encryptedevent.h"
#include "events/roommessageevent.h"
+#include <QtCore/QJsonObject>
#include <QtGui/QImage>
#include <deque>
@@ -140,6 +142,7 @@ class Room : public QObject
public:
using Timeline = std::deque<TimelineItem>;
using PendingEvents = std::vector<PendingEventItem>;
+ using RelatedEvents = QVector<const RoomEvent*>;
using rev_iter_t = Timeline::const_reverse_iterator;
using timeline_iter_t = Timeline::const_iterator;
@@ -199,6 +202,14 @@ public:
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;
@@ -284,6 +295,11 @@ public:
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
/**
@@ -443,6 +459,8 @@ public slots:
QString postHtmlMessage(const QString& plainText, 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
@@ -599,6 +617,7 @@ signals:
void tagsAboutToChange();
void tagsChanged();
+ void updatedEvent(QString eventId);
void replacedEvent(const RoomEvent* newEvent, const RoomEvent* oldEvent);
void newFileTransfer(QString id, QUrl localFile);
diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri
index c561a415..2ff14ce2 100644
--- a/libqmatrixclient.pri
+++ b/libqmatrixclient.pri
@@ -35,6 +35,7 @@ HEADERS += \
$$SRCPATH/events/roomavatarevent.h \
$$SRCPATH/events/typingevent.h \
$$SRCPATH/events/receiptevent.h \
+ $$SRCPATH/events/reactionevent.h \
$$SRCPATH/events/callanswerevent.h \
$$SRCPATH/events/callcandidatesevent.h \
$$SRCPATH/events/callhangupevent.h \
@@ -42,6 +43,7 @@ HEADERS += \
$$SRCPATH/events/accountdataevents.h \
$$SRCPATH/events/directchatevent.h \
$$SRCPATH/events/encryptionevent.h \
+ $$SRCPATH/events/encryptedevent.h \
$$SRCPATH/events/redactionevent.h \
$$SRCPATH/events/eventloader.h \
$$SRCPATH/jobs/requestdata.h \
@@ -80,6 +82,7 @@ SOURCES += \
$$SRCPATH/events/roommessageevent.cpp \
$$SRCPATH/events/roommemberevent.cpp \
$$SRCPATH/events/typingevent.cpp \
+ $$SRCPATH/events/reactionevent.cpp \
$$SRCPATH/events/callanswerevent.cpp \
$$SRCPATH/events/callcandidatesevent.cpp \
$$SRCPATH/events/callhangupevent.cpp \
@@ -87,6 +90,7 @@ SOURCES += \
$$SRCPATH/events/receiptevent.cpp \
$$SRCPATH/events/directchatevent.cpp \
$$SRCPATH/events/encryptionevent.cpp \
+ $$SRCPATH/events/encryptedevent.cpp \
$$SRCPATH/jobs/requestdata.cpp \
$$SRCPATH/jobs/basejob.cpp \
$$SRCPATH/jobs/syncjob.cpp \