aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/avatar.cpp14
-rw-r--r--lib/connection.cpp130
-rw-r--r--lib/connection.h66
-rw-r--r--lib/encryptionmanager.cpp2
-rw-r--r--lib/eventitem.h8
-rw-r--r--lib/events/event.cpp3
-rw-r--r--lib/events/event.h41
-rw-r--r--lib/events/stickerevent.cpp26
-rw-r--r--lib/events/stickerevent.h38
-rw-r--r--lib/jobs/basejob.cpp46
-rw-r--r--lib/jobs/basejob.h4
-rw-r--r--lib/logging.cpp1
-rw-r--r--lib/logging.h1
-rw-r--r--lib/room.cpp270
-rw-r--r--lib/util.h2
15 files changed, 420 insertions, 232 deletions
diff --git a/lib/avatar.cpp b/lib/avatar.cpp
index 4548be02..0573df5d 100644
--- a/lib/avatar.cpp
+++ b/lib/avatar.cpp
@@ -25,9 +25,9 @@ public:
explicit Private(QUrl url = {}) : _url(move(url)) {}
~Private()
{
- if (isJobRunning(_thumbnailRequest))
+ if (isJobPending(_thumbnailRequest))
_thumbnailRequest->abandon();
- if (isJobRunning(_uploadRequest))
+ if (isJobPending(_uploadRequest))
_uploadRequest->abandon();
}
@@ -75,7 +75,7 @@ QImage Avatar::get(Connection* connection, int width, int height,
bool Avatar::upload(Connection* connection, const QString& fileName,
upload_callback_t callback) const
{
- if (isJobRunning(d->_uploadRequest))
+ if (isJobPending(d->_uploadRequest))
return false;
return d->upload(connection->uploadFile(fileName), move(callback));
}
@@ -83,7 +83,7 @@ bool Avatar::upload(Connection* connection, const QString& fileName,
bool Avatar::upload(Connection* connection, QIODevice* source,
upload_callback_t callback) const
{
- if (isJobRunning(d->_uploadRequest) || !source->isReadable())
+ if (isJobPending(d->_uploadRequest) || !source->isReadable())
return false;
return d->upload(connection->uploadContent(source), move(callback));
}
@@ -113,7 +113,7 @@ QImage Avatar::Private::get(Connection* connection, QSize size,
&& checkUrl(_url)) {
qCDebug(MAIN) << "Getting avatar from" << _url.toString();
_requestedSize = size;
- if (isJobRunning(_thumbnailRequest))
+ if (isJobPending(_thumbnailRequest))
_thumbnailRequest->abandon();
if (callback)
callbacks.emplace_back(move(callback));
@@ -145,7 +145,7 @@ QImage Avatar::Private::get(Connection* connection, QSize size,
bool Avatar::Private::upload(UploadContentJob* job, upload_callback_t &&callback)
{
_uploadRequest = job;
- if (!isJobRunning(_uploadRequest))
+ if (!isJobPending(_uploadRequest))
return false;
_uploadRequest->connect(_uploadRequest, &BaseJob::success, _uploadRequest,
[job, callback] { callback(job->contentUri()); });
@@ -182,7 +182,7 @@ bool Avatar::updateUrl(const QUrl& newUrl)
d->_url = newUrl;
d->_imageSource = Private::Unknown;
- if (isJobRunning(d->_thumbnailRequest))
+ if (isJobPending(d->_thumbnailRequest))
d->_thumbnailRequest->abandon();
return true;
}
diff --git a/lib/connection.cpp b/lib/connection.cpp
index 8f95f3a6..015e73c9 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -25,6 +25,7 @@
#include "csapi/versions.h"
#include "csapi/voip.h"
#include "csapi/wellknown.h"
+#include "csapi/whoami.h"
#include "events/directchatevent.h"
#include "events/eventloader.h"
@@ -121,10 +122,27 @@ public:
!= "json";
bool lazyLoading = false;
+ /** \brief Check the homeserver and resolve it if needed, before connecting
+ *
+ * A single entry for functions that need to check whether the homeserver
+ * is valid before running. May execute connectFn either synchronously
+ * or asynchronously. In case of errors, emits resolveError() if
+ * the homeserver URL is not valid and cannot be resolved from userId, or
+ * the homeserver doesn't support the requested login flow.
+ *
+ * \param userId fully-qualified MXID to resolve HS from
+ * \param connectFn a function to execute once the HS URL is good
+ * \param flow optionally, a login flow that should be supported for
+ * connectFn to work; `none`, if there's no login flow
+ * requirements
+ * \sa resolveServer, resolveError
+ */
+ void checkAndConnect(const QString &userId,
+ const std::function<void ()> &connectFn,
+ const std::optional<LoginFlow> &flow = none);
template <typename... LoginArgTs>
void loginToServer(LoginArgTs&&... loginArgs);
- void assumeIdentity(const QString& userId, const QString& accessToken,
- const QString& deviceId);
+ void completeSetup(const QString &mxId);
void removeRoom(const QString& roomId);
void consumeRoomData(SyncDataList&& roomDataList, bool fromCache);
@@ -241,7 +259,7 @@ Connection::~Connection()
void Connection::resolveServer(const QString& mxid)
{
- if (isJobRunning(d->resolverJob))
+ if (isJobPending(d->resolverJob))
d->resolverJob->abandon();
auto maybeBaseUrl = QUrl::fromUserInput(serverPart(mxid));
@@ -252,12 +270,19 @@ void Connection::resolveServer(const QString& mxid)
return;
}
- auto domain = maybeBaseUrl.host();
- qCDebug(MAIN) << "Finding the server" << domain;
+ qCDebug(MAIN) << "Finding the server" << maybeBaseUrl.host();
- d->data->setBaseUrl(maybeBaseUrl); // Just enough to check .well-known file
+ const auto& oldBaseUrl = d->data->baseUrl();
+ d->data->setBaseUrl(maybeBaseUrl); // Temporarily set it for this one call
d->resolverJob = callApi<GetWellknownJob>();
- connect(d->resolverJob, &BaseJob::finished, this, [this, maybeBaseUrl] {
+ // Connect to finished() to make sure baseUrl is restored in any case
+ connect(d->resolverJob, &BaseJob::finished, this, [this, maybeBaseUrl, oldBaseUrl] {
+ // Revert baseUrl so that setHomeserver() below triggers signals
+ // in case the base URL actually changed
+ d->data->setBaseUrl(oldBaseUrl);
+ if (d->resolverJob->error() == BaseJob::Abandoned)
+ return;
+
if (d->resolverJob->error() != BaseJob::NotFoundError) {
if (!d->resolverJob->status().good()) {
qCWarning(MAIN)
@@ -285,6 +310,7 @@ void Connection::resolveServer(const QString& mxid)
<< "for base URL";
setHomeserver(maybeBaseUrl);
}
+ Q_ASSERT(d->loginFlowsJob != nullptr); // Ensured by setHomeserver()
connect(d->loginFlowsJob, &BaseJob::success, this,
&Connection::resolved);
connect(d->loginFlowsJob, &BaseJob::failure, this, [this] {
@@ -312,10 +338,10 @@ void Connection::loginWithPassword(const QString& userId,
const QString& initialDeviceName,
const QString& deviceId)
{
- checkAndConnect(userId, [=] {
+ d->checkAndConnect(userId, [=] {
d->loginToServer(LoginFlows::Password.type, makeUserIdentifier(userId),
password, /*token*/ "", deviceId, initialDeviceName);
- });
+ }, LoginFlows::Password);
}
SsoSession* Connection::prepareForSso(const QString& initialDeviceName,
@@ -328,17 +354,30 @@ void Connection::loginWithToken(const QByteArray& loginToken,
const QString& initialDeviceName,
const QString& deviceId)
{
+ Q_ASSERT(d->data->baseUrl().isValid() && d->loginFlows.contains(LoginFlows::Token));
d->loginToServer(LoginFlows::Token.type,
none /*user is encoded in loginToken*/, "" /*password*/,
loginToken, deviceId, initialDeviceName);
}
-void Connection::assumeIdentity(const QString& userId,
- const QString& accessToken,
+void Connection::assumeIdentity(const QString& mxId, const QString& accessToken,
const QString& deviceId)
{
- checkAndConnect(userId,
- [=] { d->assumeIdentity(userId, accessToken, deviceId); });
+ d->checkAndConnect(mxId, [this, mxId, accessToken, deviceId] {
+ d->data->setToken(accessToken.toLatin1());
+ d->data->setDeviceId(deviceId); // Can't we deduce this from access_token?
+ auto* job = callApi<GetTokenOwnerJob>();
+ connect(job, &BaseJob::success, this, [this, job, mxId] {
+ if (mxId != job->userId())
+ qCWarning(MAIN).nospace()
+ << "The access_token owner (" << job->userId()
+ << ") is different from passed MXID (" << mxId << ")!";
+ d->completeSetup(job->userId());
+ });
+ connect(job, &BaseJob::failure, this, [this, job] {
+ emit loginError(job->errorString(), job->rawDataSample());
+ });
+ });
}
void Connection::reloadCapabilities()
@@ -378,8 +417,9 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs)
auto loginJob =
q->callApi<LoginJob>(std::forward<LoginArgTs>(loginArgs)...);
connect(loginJob, &BaseJob::success, q, [this, loginJob] {
- assumeIdentity(loginJob->userId(), loginJob->accessToken(),
- loginJob->deviceId());
+ data->setToken(loginJob->accessToken().toLatin1());
+ data->setDeviceId(loginJob->deviceId());
+ completeSetup(loginJob->userId());
#ifndef Quotient_E2EE_ENABLED
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
#else // Quotient_E2EE_ENABLED
@@ -392,21 +432,18 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs)
});
}
-void Connection::Private::assumeIdentity(const QString& userId,
- const QString& accessToken,
- const QString& deviceId)
+void Connection::Private::completeSetup(const QString& mxId)
{
- data->setUserId(userId);
+ data->setUserId(mxId);
q->user(); // Creates a User object for the local user
- data->setToken(accessToken.toLatin1());
- data->setDeviceId(deviceId);
- q->setObjectName(userId % '/' % deviceId);
+ q->setObjectName(data->userId() % '/' % data->deviceId());
qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString()
- << "by user" << userId << "from device" << deviceId;
+ << "by user" << data->userId()
+ << "from device" << data->deviceId();
#ifndef Quotient_E2EE_ENABLED
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
#else // Quotient_E2EE_ENABLED
- AccountSettings accountSettings(userId);
+ AccountSettings accountSettings(data->userId());
encryptionManager.reset(
new EncryptionManager(accountSettings.encryptionAccountPickle()));
if (accountSettings.encryptionAccountPickle().isEmpty()) {
@@ -419,22 +456,37 @@ void Connection::Private::assumeIdentity(const QString& userId,
q->reloadCapabilities();
}
-void Connection::checkAndConnect(const QString& userId,
- std::function<void()> connectFn)
+void Connection::Private::checkAndConnect(const QString& userId,
+ const std::function<void()>& connectFn,
+ const std::optional<LoginFlow>& flow)
{
- if (d->data->baseUrl().isValid()) {
+ if (data->baseUrl().isValid() && (!flow || loginFlows.contains(*flow))) {
connectFn();
return;
}
- // Not good to go, try to fix the homeserver URL.
+ // Not good to go, try to ascertain the homeserver URL and flows
if (userId.startsWith('@') && userId.indexOf(':') != -1) {
- connectSingleShot(this, &Connection::homeserverChanged, this, connectFn);
- // NB: doResolveServer can emit resolveError, so this is a part of
- // checkAndConnect function contract.
- resolveServer(userId);
+ q->resolveServer(userId);
+ if (flow)
+ connectSingleShot(q, &Connection::loginFlowsChanged, q,
+ [this, flow, connectFn] {
+ if (loginFlows.contains(*flow))
+ connectFn();
+ else
+ emit q->loginError(
+ tr("The homeserver at %1 does not support"
+ " the login flow '%2'")
+ .arg(data->baseUrl().toDisplayString()),
+ flow->type);
+ });
+ else
+ connectSingleShot(q, &Connection::homeserverChanged, q, connectFn);
} else
- emit resolveError(tr("%1 is an invalid homeserver URL")
- .arg(d->data->baseUrl().toString()));
+ emit q->resolveError(tr("Please provide the fully-qualified user id"
+ " (such as @user:example.org) so that the"
+ " homeserver could be resolved; the current"
+ " homeserver URL(%1) is not good")
+ .arg(data->baseUrl().toDisplayString()));
}
void Connection::logout()
@@ -1165,7 +1217,7 @@ QByteArray Connection::accessToken() const
{
// The logout job needs access token to do its job; so the token is
// kept inside d->data but no more exposed to the outside world.
- return isJobRunning(d->logoutJob) ? QByteArray() : d->data->accessToken();
+ return isJobPending(d->logoutJob) ? QByteArray() : d->data->accessToken();
}
bool Connection::isLoggedIn() const { return !accessToken().isEmpty(); }
@@ -1480,12 +1532,10 @@ QByteArray Connection::generateTxnId() const
void Connection::setHomeserver(const QUrl& url)
{
- if (isJobRunning(d->resolverJob))
+ if (isJobPending(d->resolverJob))
d->resolverJob->abandon();
- d->resolverJob = nullptr;
- if (isJobRunning(d->loginFlowsJob))
+ if (isJobPending(d->loginFlowsJob))
d->loginFlowsJob->abandon();
- d->loginFlowsJob = nullptr;
d->loginFlows.clear();
if (homeserver() != url) {
@@ -1495,7 +1545,7 @@ void Connection::setHomeserver(const QUrl& url)
// Whenever a homeserver is updated, retrieve available login flows from it
d->loginFlowsJob = callApi<GetLoginFlowsJob>(BackgroundRequest);
- connect(d->loginFlowsJob, &BaseJob::finished, this, [this] {
+ connect(d->loginFlowsJob, &BaseJob::result, this, [this] {
if (d->loginFlowsJob->status().good())
d->loginFlows = d->loginFlowsJob->flows();
else
diff --git a/lib/connection.h b/lib/connection.h
index a32d0801..f3d7d725 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -50,28 +50,27 @@ class SendToDeviceJob;
class SendMessageJob;
class LeaveRoomJob;
+using LoginFlow = GetLoginFlowsJob::LoginFlow;
+
+/// Predefined login flows
+struct LoginFlows {
+ static inline const LoginFlow Password { "m.login.password" };
+ static inline const LoginFlow SSO { "m.login.sso" };
+ static inline const LoginFlow Token { "m.login.token" };
+};
+
// To simplify comparisons of LoginFlows
-inline bool operator==(const GetLoginFlowsJob::LoginFlow& lhs,
- const GetLoginFlowsJob::LoginFlow& rhs)
+inline bool operator==(const LoginFlow& lhs, const LoginFlow& rhs)
{
return lhs.type == rhs.type;
}
-inline bool operator!=(const GetLoginFlowsJob::LoginFlow& lhs,
- const GetLoginFlowsJob::LoginFlow& rhs)
+inline bool operator!=(const LoginFlow& lhs, const LoginFlow& rhs)
{
return !(lhs == rhs);
}
-/// Predefined login flows
-struct LoginFlows {
- using LoginFlow = GetLoginFlowsJob::LoginFlow;
- static inline const LoginFlow Password { "m.login.password" };
- static inline const LoginFlow SSO { "m.login.sso" };
- static inline const LoginFlow Token { "m.login.token" };
-};
-
class Connection;
using room_factory_t =
@@ -492,13 +491,35 @@ public Q_SLOTS:
/** Determine and set the homeserver from MXID */
void resolveServer(const QString& mxid);
+ /** \brief Log in using a username and password pair
+ *
+ * Before logging in, this method checks if the homeserver is valid and
+ * supports the password login flow. If the homeserver is invalid but
+ * a full user MXID is provided, this method calls resolveServer() using
+ * this MXID.
+ *
+ * \sa resolveServer, resolveError, loginError
+ */
void loginWithPassword(const QString& userId, const QString& password,
const QString& initialDeviceName,
const QString& deviceId = {});
+ /** \brief Log in using a login token
+ *
+ * One usual case for this method is the final stage of logging in via SSO.
+ * Unlike loginWithPassword() and assumeIdentity(), this method cannot
+ * resolve the server from the user name because the full user MXID is
+ * encoded in the login token. Callers should ensure the homeserver
+ * sanity in advance.
+ */
void loginWithToken(const QByteArray& loginToken,
const QString& initialDeviceName,
const QString& deviceId = {});
- void assumeIdentity(const QString& userId, const QString& accessToken,
+ /** \brief Use an existing access token to connect to the homeserver
+ *
+ * Similar to loginWithPassword(), this method checks that the homeserver
+ * URL is valid and tries to resolve it from the MXID in case it is not.
+ */
+ void assumeIdentity(const QString& mxId, const QString& accessToken,
const QString& deviceId);
/*! \deprecated Use loginWithPassword instead */
void connectToServer(const QString& userId, const QString& password,
@@ -650,9 +671,9 @@ Q_SIGNALS:
* This was a signal resulting from a successful resolveServer().
* Since Connection now provides setHomeserver(), the HS URL
* may change even without resolveServer() invocation. Use
- * homeserverChanged() instead of resolved(). You can also use
- * connectToServer and connectWithToken without the HS URL set in
- * advance (i.e. without calling resolveServer), as they now trigger
+ * loginFLowsChanged() instead of resolved(). You can also use
+ * loginWith*() and assumeIdentity() without the HS URL set in
+ * advance (i.e. without calling resolveServer), as they trigger
* server name resolution from MXID if the server URL is not valid.
*/
void resolved();
@@ -848,19 +869,6 @@ private:
class Private;
QScopedPointer<Private> d;
- /**
- * A single entry for functions that need to check whether the
- * homeserver is valid before running. May either execute connectFn
- * synchronously or asynchronously (if tryResolve is true and
- * a DNS lookup is initiated); in case of errors, emits resolveError
- * if the homeserver URL is not valid and cannot be resolved from
- * userId.
- *
- * @param userId - fully-qualified MXID to resolve HS from
- * @param connectFn - a function to execute once the HS URL is good
- */
- void checkAndConnect(const QString& userId, std::function<void()> connectFn);
-
static room_factory_t _roomFactory;
static user_factory_t _userFactory;
};
diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp
index 826656d3..8d241eb2 100644
--- a/lib/encryptionmanager.cpp
+++ b/lib/encryptionmanager.cpp
@@ -250,7 +250,7 @@ void EncryptionManager::uploadOneTimeKeys(Connection* connection,
if (forceUpdate || d->oneTimeKeyCounts.isEmpty()) {
d->uploadOneTimeKeysInitJob = connection->callApi<UploadKeysJob>();
connect(d->uploadOneTimeKeysInitJob, &BaseJob::success, this, [this] {
- d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts());
+ d->setOneTimeKeyCounts(d->uploadOneTimeKeysInitJob->oneTimeKeyCounts());
});
}
diff --git a/lib/eventitem.h b/lib/eventitem.h
index ae3d5762..2d3d9ef6 100644
--- a/lib/eventitem.h
+++ b/lib/eventitem.h
@@ -8,6 +8,7 @@
#include "events/stateevent.h"
+#include <any>
#include <utility>
namespace Quotient {
@@ -60,6 +61,12 @@ public:
return std::exchange(evt, move(other));
}
+ /// Store arbitrary data with the event item
+ void setUserData(std::any userData) { data = userData; }
+ /// Obtain custom data previously stored with the event item
+ const std::any& userdata() const { return data; }
+ std::any& userData() { return data; }
+
protected:
template <typename EventT>
EventT* getAs()
@@ -69,6 +76,7 @@ protected:
private:
RoomEventPtr evt;
+ std::any data;
};
class TimelineItem : public EventItemBase {
diff --git a/lib/events/event.cpp b/lib/events/event.cpp
index 6014183e..97edb4e0 100644
--- a/lib/events/event.cpp
+++ b/lib/events/event.cpp
@@ -49,11 +49,14 @@ QString Event::matrixType() const { return fullJson()[TypeKeyL].toString(); }
QByteArray Event::originalJson() const { return QJsonDocument(_json).toJson(); }
+// On const below: this is to catch accidental attempts to change event JSON
+// NOLINTNEXTLINE(readability-const-return-type)
const QJsonObject Event::contentJson() const
{
return fullJson()[ContentKeyL].toObject();
}
+// NOLINTNEXTLINE(readability-const-return-type)
const QJsonObject Event::unsignedJson() const
{
return fullJson()[UnsignedKeyL].toObject();
diff --git a/lib/events/event.h b/lib/events/event.h
index e9d42333..c5752a7a 100644
--- a/lib/events/event.h
+++ b/lib/events/event.h
@@ -286,7 +286,7 @@ using Events = EventsArray<Event>;
// === is<>(), eventCast<>() and visit<>() ===
-template <typename EventT>
+template <class EventT>
inline bool is(const Event& e)
{
return e.type() == typeId<EventT>();
@@ -297,7 +297,7 @@ inline bool isUnknown(const Event& e)
return e.type() == unknownEventTypeId();
}
-template <typename EventT, typename BasePtrT>
+template <class EventT, typename BasePtrT>
inline auto eventCast(const BasePtrT& eptr)
-> decltype(static_cast<EventT*>(&*eptr))
{
@@ -307,7 +307,7 @@ inline auto eventCast(const BasePtrT& eptr)
}
// A single generic catch-all visitor
-template <typename BaseEventT, typename FnT>
+template <class BaseEventT, typename FnT>
inline auto visit(const BaseEventT& event, FnT&& visitor)
-> decltype(visitor(event))
{
@@ -315,18 +315,18 @@ inline auto visit(const BaseEventT& event, FnT&& visitor)
}
namespace _impl {
- template <typename T, typename FnT>
- constexpr auto needs_downcast()
- {
- return !std::is_convertible_v<T, fn_arg_t<FnT>>;
- }
+ // Using bool instead of auto below because auto apparently upsets MSVC
+ template <class BaseT, typename FnT>
+ inline constexpr bool needs_downcast =
+ std::is_base_of_v<BaseT, std::decay_t<fn_arg_t<FnT>>>
+ && !std::is_same_v<BaseT, std::decay_t<fn_arg_t<FnT>>>;
}
// A single type-specific void visitor
-template <typename BaseEventT, typename FnT>
-inline std::enable_if_t<_impl::needs_downcast<BaseEventT, FnT>()
+template <class BaseT, typename FnT>
+inline auto visit(const BaseT& event, FnT&& visitor)
+ -> std::enable_if_t<_impl::needs_downcast<BaseT, FnT>
&& std::is_void_v<fn_return_t<FnT>>>
-visit(const BaseEventT& event, FnT&& visitor)
{
using event_type = fn_arg_t<FnT>;
if (is<std::decay_t<event_type>>(event))
@@ -335,10 +335,10 @@ visit(const BaseEventT& event, FnT&& visitor)
// A single type-specific non-void visitor with an optional default value
// non-voidness is guarded by defaultValue type
-template <typename BaseEventT, typename FnT>
-inline std::enable_if_t<_impl::needs_downcast<BaseEventT, FnT>(), fn_return_t<FnT>>
-visit(const BaseEventT& event, FnT&& visitor,
- fn_return_t<FnT>&& defaultValue = {})
+template <class BaseT, typename FnT>
+inline auto visit(const BaseT& event, FnT&& visitor,
+ fn_return_t<FnT>&& defaultValue = {})
+ -> std::enable_if_t<_impl::needs_downcast<BaseT, FnT>, fn_return_t<FnT>>
{
using event_type = fn_arg_t<FnT>;
if (is<std::decay_t<event_type>>(event))
@@ -347,9 +347,10 @@ visit(const BaseEventT& event, FnT&& visitor,
}
// A chain of 2 or more visitors
-template <typename BaseEventT, typename FnT1, typename FnT2, typename... FnTs>
-inline fn_return_t<FnT1> visit(const BaseEventT& event, FnT1&& visitor1,
- FnT2&& visitor2, FnTs&&... visitors)
+template <class BaseT, typename FnT1, typename FnT2, typename... FnTs>
+inline std::common_type_t<fn_return_t<FnT1>, fn_return_t<FnT2>> visit(
+ const BaseT& event, FnT1&& visitor1, FnT2&& visitor2,
+ FnTs&&... visitors)
{
using event_type1 = fn_arg_t<FnT1>;
if (is<std::decay_t<event_type1>>(event))
@@ -362,8 +363,8 @@ inline fn_return_t<FnT1> visit(const BaseEventT& event, FnT1&& visitor1,
// over a range of event pointers
template <typename RangeT, typename... FnTs>
inline auto visitEach(RangeT&& events, FnTs&&... visitors)
- -> std::enable_if_t<std::is_convertible_v<
- std::decay_t<decltype(**events.begin())>, Event>>
+ -> std::enable_if_t<std::is_void_v<
+ decltype(visit(**begin(events), std::forward<FnTs>(visitors)...))>>
{
for (auto&& evtPtr: events)
visit(*evtPtr, std::forward<FnTs>(visitors)...);
diff --git a/lib/events/stickerevent.cpp b/lib/events/stickerevent.cpp
new file mode 100644
index 00000000..ea4dff3f
--- /dev/null
+++ b/lib/events/stickerevent.cpp
@@ -0,0 +1,26 @@
+// SDPX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "stickerevent.h"
+
+using namespace Quotient;
+
+StickerEvent::StickerEvent(const QJsonObject &obj)
+ : RoomEvent(typeId(), obj)
+ , m_imageContent(EventContent::ImageContent(obj["content"_ls].toObject()))
+{}
+
+QString StickerEvent::body() const
+{
+ return content<QString>("body"_ls);
+}
+
+const EventContent::ImageContent &StickerEvent::image() const
+{
+ return m_imageContent;
+}
+
+QUrl StickerEvent::url() const
+{
+ return m_imageContent.url;
+}
diff --git a/lib/events/stickerevent.h b/lib/events/stickerevent.h
new file mode 100644
index 00000000..93671086
--- /dev/null
+++ b/lib/events/stickerevent.h
@@ -0,0 +1,38 @@
+// SDPX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include "roomevent.h"
+#include "eventcontent.h"
+
+namespace Quotient {
+
+/// Sticker messages are specialised image messages that are displayed without
+/// controls (e.g. no "download" link, or light-box view on click, as would be
+/// displayed for for m.image events).
+class StickerEvent : public RoomEvent
+{
+public:
+ DEFINE_EVENT_TYPEID("m.sticker", StickerEvent)
+
+ explicit StickerEvent(const QJsonObject &obj);
+
+ /// \brief A textual representation or associated description of the
+ /// sticker image.
+ ///
+ /// This could be the alt text of the original image, or a message to
+ /// accompany and further describe the sticker.
+ QString body() const;
+
+ /// \brief Metadata about the image referred to in url including a
+ /// thumbnail representation.
+ const EventContent::ImageContent &image() const;
+
+ /// \brief The URL to the sticker image. This must be a valid mxc:// URI.
+ QUrl url() const;
+private:
+ EventContent::ImageContent m_imageContent;
+};
+REGISTER_EVENT_TYPE(StickerEvent)
+} // namespace Quotient
diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp
index e6dc9f82..ada7337b 100644
--- a/lib/jobs/basejob.cpp
+++ b/lib/jobs/basejob.cpp
@@ -12,6 +12,7 @@
#include <QtCore/QTimer>
#include <QtCore/QStringBuilder>
#include <QtCore/QMetaEnum>
+#include <QtCore/QPointer>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QNetworkRequest>
@@ -64,15 +65,6 @@ QDebug BaseJob::Status::dumpToLog(QDebug dbg) const
return dbg << ": " << message;
}
-struct NetworkReplyDeleter : public QScopedPointerDeleteLater {
- static inline void cleanup(QNetworkReply* reply)
- {
- if (reply && reply->isRunning())
- reply->abort();
- QScopedPointerDeleteLater::cleanup(reply);
- }
-};
-
template <typename... Ts>
constexpr auto make_array(Ts&&... items)
{
@@ -100,6 +92,16 @@ public:
retryTimer.setSingleShot(true);
}
+ ~Private()
+ {
+ if (reply) {
+ if (reply->isRunning()) {
+ reply->abort();
+ }
+ delete reply;
+ }
+ }
+
void sendRequest();
/*! \brief Parse the response byte array into JSON
*
@@ -128,7 +130,10 @@ public:
QByteArrayList expectedKeys;
- QScopedPointer<QNetworkReply, NetworkReplyDeleter> reply;
+ // When the QNetworkAccessManager is destroyed it destroys all pending replies.
+ // Using QPointer allows us to know when that happend.
+ QPointer<QNetworkReply> reply;
+
Status status = Unprepared;
QByteArray rawResponse;
/// Contains a null document in case of non-JSON body (for a successful
@@ -182,6 +187,7 @@ BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint,
setObjectName(name);
connect(&d->timer, &QTimer::timeout, this, &BaseJob::timeout);
connect(&d->retryTimer, &QTimer::timeout, this, [this] {
+ qCDebug(d->logCat) << "Retrying" << this;
d->connection->submit(this);
});
}
@@ -246,7 +252,7 @@ void BaseJob::setExpectedContentTypes(const QByteArrayList& contentTypes)
d->expectedContentTypes = contentTypes;
}
-const QByteArrayList BaseJob::expectedKeys() const { return d->expectedKeys; }
+QByteArrayList BaseJob::expectedKeys() const { return d->expectedKeys; }
void BaseJob::addExpectedKey(const QByteArray& key) { d->expectedKeys << key; }
@@ -303,16 +309,16 @@ void BaseJob::Private::sendRequest()
switch (verb) {
case HttpVerb::Get:
- reply.reset(connection->nam()->get(req));
+ reply = connection->nam()->get(req);
break;
case HttpVerb::Post:
- reply.reset(connection->nam()->post(req, requestData.source()));
+ reply = connection->nam()->post(req, requestData.source());
break;
case HttpVerb::Put:
- reply.reset(connection->nam()->put(req, requestData.source()));
+ reply = connection->nam()->put(req, requestData.source());
break;
case HttpVerb::Delete:
- reply.reset(connection->nam()->sendCustomRequest(req, "DELETE", requestData.source()));
+ reply = connection->nam()->sendCustomRequest(req, "DELETE", requestData.source());
break;
}
}
@@ -325,7 +331,7 @@ void BaseJob::beforeAbandon() { }
void BaseJob::initiate(ConnectionData* connData, bool inBackground)
{
- if (connData && connData->baseUrl().isValid()) {
+ if (Q_LIKELY(connData && connData->baseUrl().isValid())) {
d->inBackground = inBackground;
d->connection = connData;
doPrepare();
@@ -338,7 +344,7 @@ void BaseJob::initiate(ConnectionData* connData, bool inBackground)
setStatus(FileError, "Request data not ready");
}
Q_ASSERT(status().code != Pending); // doPrepare() must NOT set this
- if (status().code == Unprepared) {
+ if (Q_LIKELY(status().code == Unprepared)) {
d->connection->submit(this);
return;
}
@@ -357,8 +363,11 @@ void BaseJob::initiate(ConnectionData* connData, bool inBackground)
void BaseJob::sendRequest()
{
- if (status().code == Abandoned)
+ if (status().code == Abandoned) {
+ qCDebug(d->logCat) << "Won't proceed with the abandoned request:"
+ << d->dumpRequest();
return;
+ }
Q_ASSERT(d->connection && status().code == Pending);
qCDebug(d->logCat).noquote() << "Making" << d->dumpRequest();
d->needsToken |= d->connection->needsToken(objectName());
@@ -604,6 +613,7 @@ void BaseJob::finishJob()
qCWarning(d->logCat).nospace()
<< this << ": retry #" << d->retriesTaken << " in "
<< retryIn.count() << " s";
+ setStatus(Pending, "Pending retry");
d->retryTimer.start(retryIn);
emit retryScheduled(d->retriesTaken, milliseconds(retryIn).count());
return;
diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h
index c2d42f49..3165edd3 100644
--- a/lib/jobs/basejob.h
+++ b/lib/jobs/basejob.h
@@ -351,7 +351,7 @@ protected:
const QByteArrayList& expectedContentTypes() const;
void addExpectedContentType(const QByteArray& contentType);
void setExpectedContentTypes(const QByteArrayList& contentTypes);
- const QByteArrayList expectedKeys() const;
+ QByteArrayList expectedKeys() const;
void addExpectedKey(const QByteArray &key);
void setExpectedKeys(const QByteArrayList &keys);
@@ -466,7 +466,7 @@ private:
QScopedPointer<Private> d;
};
-inline bool isJobRunning(BaseJob* job)
+inline bool isJobPending(BaseJob* job)
{
return job && job->error() == BaseJob::Pending;
}
diff --git a/lib/logging.cpp b/lib/logging.cpp
index c285821c..3f757393 100644
--- a/lib/logging.cpp
+++ b/lib/logging.cpp
@@ -12,6 +12,7 @@
LOGGING_CATEGORY(MAIN, "quotient.main")
LOGGING_CATEGORY(EVENTS, "quotient.events")
LOGGING_CATEGORY(STATE, "quotient.events.state")
+LOGGING_CATEGORY(MEMBERS, "quotient.events.members")
LOGGING_CATEGORY(MESSAGES, "quotient.events.messages")
LOGGING_CATEGORY(EPHEMERAL, "quotient.events.ephemeral")
LOGGING_CATEGORY(E2EE, "quotient.e2ee")
diff --git a/lib/logging.h b/lib/logging.h
index 77e0dad3..21d05d8b 100644
--- a/lib/logging.h
+++ b/lib/logging.h
@@ -11,6 +11,7 @@
Q_DECLARE_LOGGING_CATEGORY(MAIN)
Q_DECLARE_LOGGING_CATEGORY(STATE)
+Q_DECLARE_LOGGING_CATEGORY(MEMBERS)
Q_DECLARE_LOGGING_CATEGORY(MESSAGES)
Q_DECLARE_LOGGING_CATEGORY(EVENTS)
Q_DECLARE_LOGGING_CATEGORY(EPHEMERAL)
diff --git a/lib/room.cpp b/lib/room.cpp
index 155f5cd9..89ac17de 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -24,6 +24,7 @@
#include "csapi/room_state.h"
#include "csapi/room_upgrades.h"
#include "csapi/rooms.h"
+#include "csapi/read_markers.h"
#include "csapi/tags.h"
#include "events/callanswerevent.h"
@@ -43,7 +44,6 @@
#include "events/roompowerlevelsevent.h"
#include "jobs/downloadfilejob.h"
#include "jobs/mediathumbnailjob.h"
-#include "jobs/postreadmarkersjob.h"
#include "events/roomcanonicalaliasevent.h"
#include <QtCore/QDir>
@@ -251,10 +251,11 @@ public:
for (auto&& eptr : events) {
const auto& evt = *eptr;
Q_ASSERT(evt.isStateEvent());
- // Update baseState afterwards to make sure that the old state
- // is valid and usable inside processStateEvent
- changes |= q->processStateEvent(evt);
- baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr);
+ auto change = q->processStateEvent(evt);
+ if (change != NoChange) {
+ changes |= change;
+ baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr);
+ }
}
if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
qCDebug(PROFILER)
@@ -620,8 +621,8 @@ Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId)
emit q->readMarkerForUserMoved(u, eventId, storedId);
if (isLocalUser(u)) {
if (storedId != serverReadMarker)
- connection->callApi<PostReadMarkersJob>(BackgroundRequest, id,
- storedId);
+ connection->callApi<SetReadMarkerJob>(BackgroundRequest, id,
+ storedId);
emit q->readMarkerMoved(eventId, storedId);
return Change::ReadMarkerChange;
}
@@ -845,7 +846,7 @@ const Room::RelatedEvents Room::relatedEvents(const RoomEvent& evt,
void Room::Private::getAllMembers()
{
// If already loaded or already loading, there's nothing to do here.
- if (q->joinedCount() <= membersMap.size() || isJobRunning(allMembersJob))
+ if (q->joinedCount() <= membersMap.size() || isJobPending(allMembersJob))
return;
allMembersJob = connection->callApi<GetMembersByRoomJob>(
@@ -1341,23 +1342,27 @@ Room::Changes Room::Private::setSummary(RoomSummary&& newSummary)
void Room::Private::insertMemberIntoMap(User* u)
{
- const auto userName =
- getCurrentState<RoomMemberEvent>(u->id())->displayName();
- qDebug(STATE) << "insertMemberIntoMap(), user" << u->id() << "with name"
- << userName;
- // If there is exactly one namesake of the added user, signal member
- // renaming for that other one because the two should be disambiguated now.
+ const auto maybeUserName =
+ getCurrentState<RoomMemberEvent>(u->id())->newDisplayName();
+ if (!maybeUserName)
+ qCWarning(MEMBERS) << "insertMemberIntoMap():" << u->id()
+ << "has no name (even empty)";
+ const auto userName = maybeUserName.value_or(QString());
const auto namesakes = membersMap.values(userName);
- qDebug(STATE) << namesakes.size() << "namesake(s) found";
+ qCDebug(MEMBERS) << "insertMemberIntoMap(), user" << u->id()
+ << "with name" << userName << '-'
+ << namesakes.size() << "namesake(s) found";
- // Callers should check they are not adding an existing user once more.
+ // Callers should make sure they are not adding an existing user once more
Q_ASSERT(!namesakes.contains(u));
if (namesakes.contains(u)) { // Release version whines but continues
- qCCritical(STATE) << "Trying to add a user" << u->id() << "to room"
- << q->objectName() << "but that's already in it";
+ qCCritical(MEMBERS) << "Trying to add a user" << u->id() << "to room"
+ << q->objectName() << "but that's already in it";
return;
}
+ // If there is exactly one namesake of the added user, signal member
+ // renaming for that other one because the two should be disambiguated now
if (namesakes.size() == 1)
emit q->memberAboutToRename(namesakes.front(),
namesakes.front()->fullName(q));
@@ -1369,21 +1374,40 @@ void Room::Private::insertMemberIntoMap(User* u)
void Room::Private::removeMemberFromMap(User* u)
{
const auto userName =
- getCurrentState<RoomMemberEvent>(u->id())->displayName();
+ getCurrentState<RoomMemberEvent>(
+ u->id())->newDisplayName().value_or(QString());
- qDebug(STATE) << "removeMemberFromMap(), username" << userName << "for user"
- << u->id();
+ qCDebug(MEMBERS) << "removeMemberFromMap(), username" << userName
+ << "for user" << u->id();
User* namesake = nullptr;
auto namesakes = membersMap.values(userName);
+ // If there was one namesake besides the removed user, signal member
+ // renaming for it because it doesn't need to be disambiguated any more.
if (namesakes.size() == 2) {
- namesake = namesakes.front() == u ? namesakes.back() : namesakes.front();
+ namesake =
+ namesakes.front() == u ? namesakes.back() : namesakes.front();
Q_ASSERT_X(namesake != u, __FUNCTION__, "Room members list is broken");
emit q->memberAboutToRename(namesake, userName);
}
- const auto removed = membersMap.remove(userName, u);
- qDebug(STATE) << "Removed" << removed << "entries";
- // If there was one namesake besides the removed user, signal member
- // renaming for it because it doesn't need to be disambiguated any more.
+ if (membersMap.remove(userName, u) == 0) {
+ qCDebug(MEMBERS) << "No entries removed; checking the whole list";
+ // Unless at the stage of initial filling, this no removed entries
+ // is suspicious; double-check that this user is not found in
+ // the whole map, and stop (for debug builds) or shout in the logs
+ // (for release builds) if there's one. That search is O(n), which
+ // may come rather expensive for larger rooms.
+ QElapsedTimer et;
+ auto it = std::find(membersMap.cbegin(), membersMap.cend(), u);
+ if (et.nsecsElapsed() > profilerMinNsecs() / 10)
+ qCDebug(MEMBERS) << "...done in" << et;
+ if (it != membersMap.cend()) {
+ Q_ASSERT_X(false, __FUNCTION__,
+ "Mismatched name in the room members list");
+ qCCritical(MEMBERS) << "Mismatched name in the room members list;"
+ " avoiding the list corruption";
+ membersMap.remove(it.key(), u);
+ }
+ }
if (namesake)
emit q->memberRenamed(namesake);
}
@@ -1649,7 +1673,7 @@ QString Room::retryMessage(const QString& txnId)
<< "File for transaction" << txnId
<< "has already been uploaded, bypassing re-upload";
} else {
- if (isJobRunning(transferIt->job)) {
+ if (isJobPending(transferIt->job)) {
qCDebug(MESSAGES) << "Abandoning the upload job for transaction"
<< txnId << "and starting again";
transferIt->job->abandon();
@@ -1682,7 +1706,7 @@ void Room::discardMessage(const QString& txnId)
const auto& transferIt = d->fileTransfers.find(txnId);
if (transferIt != d->fileTransfers.end()) {
Q_ASSERT(transferIt->isUpload);
- if (isJobRunning(transferIt->job)) {
+ if (isJobPending(transferIt->job)) {
transferIt->status = FileTransferInfo::Cancelled;
transferIt->job->abandon();
emit fileTransferFailed(txnId, tr("File upload cancelled"));
@@ -1888,7 +1912,7 @@ void Room::getPreviousContent(int limit, const QString &filter) { d->getPrevious
void Room::Private::getPreviousContent(int limit, const QString &filter)
{
- if (isJobRunning(eventsHistoryJob))
+ if (isJobPending(eventsHistoryJob))
return;
eventsHistoryJob =
@@ -1941,7 +1965,7 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename,
"localFilename should point at a local file");
auto fileName = localFilename.toLocalFile();
auto job = connection()->uploadFile(fileName, overrideContentType);
- if (isJobRunning(job)) {
+ if (isJobPending(job)) {
d->fileTransfers[id] = { job, fileName, true };
connect(job, &BaseJob::uploadProgress, this,
[this, id](qint64 sent, qint64 total) {
@@ -1997,7 +2021,7 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
qDebug(MAIN) << "File path:" << filePath;
}
auto job = connection()->downloadFile(fileUrl, filePath);
- if (isJobRunning(job)) {
+ if (isJobPending(job)) {
// If there was a previous transfer (completed or failed), overwrite it.
d->fileTransfers[eventId] = { job, job->targetFileName() };
connect(job, &BaseJob::downloadProgress, this,
@@ -2025,7 +2049,7 @@ void Room::cancelFileTransfer(const QString& id)
<< d->id;
return;
}
- if (isJobRunning(it->job))
+ if (isJobPending(it->job))
it->job->abandon();
d->fileTransfers.remove(id);
emit fileTransferCancelled(id);
@@ -2437,7 +2461,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
Room::Changes Room::processStateEvent(const RoomEvent& e)
{
if (!e.isStateEvent())
- return Change::NoChange;
+ return NoChange;
// Find a value (create an empty one if necessary) and get a reference
// to it. Can't use getCurrentState<>() because it (creates and) returns
@@ -2445,48 +2469,87 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
// or nullptr.
auto& curStateEvent = d->currentState[{ e.matrixType(), e.stateKey() }];
// Prepare for the state change
- visit(e, [this, oldRme = static_cast<const RoomMemberEvent*>(curStateEvent)](
- const RoomMemberEvent& rme) {
- auto* u = user(rme.userId());
- if (!u) { // ???
- qCCritical(MAIN)
- << "Could not get a user object for" << rme.userId();
- return;
- }
- const auto prevMembership = oldRme ? oldRme->membership()
- : MembershipType::Leave;
- switch (prevMembership) {
- case MembershipType::Invite:
- if (rme.membership() != prevMembership) {
- d->usersInvited.removeOne(u);
- Q_ASSERT(!d->usersInvited.contains(u));
+ // clang-format off
+ const bool proceed = visit(e
+ , [this, curStateEvent](const RoomMemberEvent& rme) {
+ // clang-format on
+ auto* oldRme = static_cast<const RoomMemberEvent*>(curStateEvent);
+ auto* u = user(rme.userId());
+ if (!u) { // Some terribly malformed user id?
+ qCCritical(MAIN) << "Could not get a user object for"
+ << rme.userId();
+ return false; // Stay low and hope for the best...
}
- break;
- case MembershipType::Join:
- switch (rme.membership()) {
- case MembershipType::Join: // rename/avatar change or no-op
- if (rme.newDisplayName()) {
- emit memberAboutToRename(u, *rme.newDisplayName());
+ const auto prevMembership = oldRme ? oldRme->membership()
+ : MembershipType::Leave;
+ switch (prevMembership) {
+ case MembershipType::Invite:
+ if (rme.membership() != prevMembership) {
+ d->usersInvited.removeOne(u);
+ Q_ASSERT(!d->usersInvited.contains(u));
+ }
+ break;
+ case MembershipType::Join:
+ if (rme.membership() == MembershipType::Join) {
+ // rename/avatar change or no-op
+ if (rme.newDisplayName()) {
+ emit memberAboutToRename(u, *rme.newDisplayName());
+ d->removeMemberFromMap(u);
+ }
+ if (!rme.newDisplayName() && !rme.newAvatarUrl()) {
+ qCWarning(MEMBERS)
+ << "No-op membership event for" << rme.userId()
+ << "- retaining the state";
+ qCWarning(MEMBERS) << "The event dump:" << rme;
+ return false;
+ }
+ } else {
+ if (rme.membership() == MembershipType::Invite)
+ qCWarning(MAIN)
+ << "Membership change from Join to Invite:" << rme;
+ // whatever the new membership, it's no more Join
d->removeMemberFromMap(u);
+ emit userRemoved(u);
}
break;
- case MembershipType::Invite:
- qCWarning(MAIN) << "Membership change from Join to Invite:"
- << rme;
- [[fallthrough]];
- default: // whatever the new membership, it's no more Join
- d->removeMemberFromMap(u);
- emit userRemoved(u);
+ case MembershipType::Ban:
+ case MembershipType::Knock:
+ case MembershipType::Leave:
+ if (rme.membership() == MembershipType::Invite
+ || rme.membership() == MembershipType::Join) {
+ d->membersLeft.removeOne(u);
+ Q_ASSERT(!d->membersLeft.contains(u));
+ }
+ break;
+ case MembershipType::Undefined:
+ ; // A warning will be dropped in the post-processing block below
}
- break;
- default:
- if (rme.membership() == MembershipType::Invite
- || rme.membership() == MembershipType::Join) {
- d->membersLeft.removeOne(u);
- Q_ASSERT(!d->membersLeft.contains(u));
+ return true;
+ // clang-format off
+ }
+ , [this, curStateEvent]( const EncryptionEvent& ee) {
+ // clang-format on
+ auto* oldEncEvt =
+ static_cast<const EncryptionEvent*>(curStateEvent);
+ if (ee.algorithm().isEmpty()) {
+ qWarning(STATE)
+ << "The encryption event for room" << objectName()
+ << "doesn't have 'algorithm' specified - ignoring";
+ return false;
+ }
+ if (oldEncEvt
+ && oldEncEvt->encryption() != EncryptionEventContent::Undefined) {
+ qCWarning(STATE) << "The room is already encrypted but a new"
+ " room encryption event arrived - ignoring";
+ return false;
}
+ return true;
+ // clang-format off
}
- });
+ , true); // By default, go forward with the state change
+ // clang-format on
+ if (!proceed)
+ return NoChange;
// Change the state
const auto* const oldStateEvent =
@@ -2494,47 +2557,35 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
Q_ASSERT(!oldStateEvent
|| (oldStateEvent->matrixType() == e.matrixType()
&& oldStateEvent->stateKey() == e.stateKey()));
- if (!is<RoomMemberEvent>(e)) // Room member events are too numerous
+ if (is<RoomMemberEvent>(e))
+ qCDebug(MEMBERS) << "Updated room member state:" << e;
+ else
qCDebug(STATE) << "Updated room state:" << e;
// Update internal structures as per the change and work out the return value
// clang-format off
- return visit(e
+ const auto result = visit(e
, [] (const RoomNameEvent&) {
return NameChange;
}
- , [] (const RoomAliasesEvent&) {
- return NoChange; // This event has been removed by MSC2432
- }
, [this, oldStateEvent] (const RoomCanonicalAliasEvent& cae) {
// clang-format on
setObjectName(cae.alias().isEmpty() ? d->id : cae.alias());
- QString previousCanonicalAlias =
- oldStateEvent
- ? static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent)
- ->alias()
- : QString();
-
- auto previousAltAliases =
- oldStateEvent
- ? static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent)
- ->altAliases()
- : QStringList();
-
- if (!previousCanonicalAlias.isEmpty()) {
- previousAltAliases.push_back(previousCanonicalAlias);
+ const auto* oldCae =
+ static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent);
+ QStringList previousAltAliases {};
+ if (oldCae) {
+ previousAltAliases = oldCae->altAliases();
+ if (!oldCae->alias().isEmpty())
+ previousAltAliases.push_back(oldCae->alias());
}
- const auto previousAliases = std::move(previousAltAliases);
-
auto newAliases = cae.altAliases();
-
- if (!cae.alias().isEmpty()) {
+ if (!cae.alias().isEmpty())
newAliases.push_front(cae.alias());
- }
- connection()->updateRoomAliases(id(), previousAliases, newAliases);
+ connection()->updateRoomAliases(id(), previousAltAliases, newAliases);
return AliasesChange;
// clang-format off
}
@@ -2557,13 +2608,11 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
switch (evt.membership()) {
case MembershipType::Join:
if (prevMembership != MembershipType::Join) {
- qDebug(STATE) << "!Join -> Join";
d->insertMemberIntoMap(u);
emit userAdded(u);
} else {
if (evt.newDisplayName()) {
- qDebug(STATE) << "After renaming";
- d->insertMemberIntoMap(u);
+ d->insertMemberIntoMap(u);
emit memberRenamed(u);
}
if (evt.newAvatarUrl())
@@ -2576,33 +2625,23 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
if (u == localUser() && evt.isDirect())
connection()->addToDirectChats(this, user(evt.senderId()));
break;
- default:
+ case MembershipType::Knock:
+ case MembershipType::Ban:
+ case MembershipType::Leave:
if (!d->membersLeft.contains(u))
d->membersLeft.append(u);
+ break;
+ case MembershipType::Undefined:
+ qCWarning(MEMBERS) << "Ignored undefined membership type";
}
return MembersChange;
// clang-format off
}
- , [this, oldEncEvt = static_cast<const EncryptionEvent*>(oldStateEvent)](
- const EncryptionEvent& ee) {
- // clang-format on
- if (ee.algorithm().isEmpty()) {
- qWarning(STATE)
- << "The encryption event for room" << objectName()
- << "doesn't have 'algorithm' specified - ignoring";
- return NoChange;
- }
- if (oldEncEvt
- && oldEncEvt->encryption() != EncryptionEventContent::Undefined) {
- qCWarning(STATE) << "The room is already encrypted but a new"
- " room encryption event arrived - ignoring";
- return NoChange;
- }
+ , [this] (const EncryptionEvent&) {
// As encryption can only be switched on once, emit the signal here
// instead of aggregating and emitting in updateData()
emit encryption();
return OtherChange;
- // clang-format off
}
, [this] (const RoomTombstoneEvent& evt) {
const auto successorId = evt.successorRoomId();
@@ -2619,9 +2658,12 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
});
return OtherChange;
+ // clang-format off
}
- );
+ , OtherChange);
// clang-format on
+ Q_ASSERT(result != NoChange);
+ return result;
}
Room::Changes Room::processEphemeralEvent(EventPtr&& event)
diff --git a/lib/util.h b/lib/util.h
index 12e903ac..7547a75a 100644
--- a/lib/util.h
+++ b/lib/util.h
@@ -136,7 +136,7 @@ public:
namespace _impl {
template <typename AlwaysVoid, typename>
- struct fn_traits;
+ struct fn_traits {};
}
/// Determine traits of an arbitrary function/lambda/functor