aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/application-service/definitions/location.cpp20
-rw-r--r--lib/application-service/definitions/location.h8
-rw-r--r--lib/application-service/definitions/protocol.cpp66
-rw-r--r--lib/application-service/definitions/protocol.h24
-rw-r--r--lib/application-service/definitions/user.cpp20
-rw-r--r--lib/application-service/definitions/user.h8
-rw-r--r--lib/avatar.cpp12
-rw-r--r--lib/connection.cpp635
-rw-r--r--lib/connection.h150
-rw-r--r--lib/connectiondata.cpp6
-rw-r--r--lib/converters.cpp38
-rw-r--r--lib/converters.h293
-rw-r--r--lib/csapi/account-data.cpp28
-rw-r--r--lib/csapi/account-data.h66
-rw-r--r--lib/csapi/admin.cpp40
-rw-r--r--lib/csapi/administrative_contact.cpp42
-rw-r--r--lib/csapi/administrative_contact.h2
-rw-r--r--lib/csapi/capabilities.cpp84
-rw-r--r--lib/csapi/capabilities.h82
-rw-r--r--lib/csapi/content-repo.cpp8
-rw-r--r--lib/csapi/create_room.cpp34
-rw-r--r--lib/csapi/create_room.h2
-rw-r--r--lib/csapi/definitions/auth_data.cpp19
-rw-r--r--lib/csapi/definitions/auth_data.h8
-rw-r--r--lib/csapi/definitions/client_device.cpp23
-rw-r--r--lib/csapi/definitions/client_device.h8
-rw-r--r--lib/csapi/definitions/device_keys.cpp26
-rw-r--r--lib/csapi/definitions/device_keys.h8
-rw-r--r--lib/csapi/definitions/event_filter.cpp26
-rw-r--r--lib/csapi/definitions/event_filter.h8
-rw-r--r--lib/csapi/definitions/public_rooms_response.cpp61
-rw-r--r--lib/csapi/definitions/public_rooms_response.h16
-rw-r--r--lib/csapi/definitions/push_condition.cpp23
-rw-r--r--lib/csapi/definitions/push_condition.h8
-rw-r--r--lib/csapi/definitions/push_rule.cpp29
-rw-r--r--lib/csapi/definitions/push_rule.h12
-rw-r--r--lib/csapi/definitions/push_ruleset.cpp26
-rw-r--r--lib/csapi/definitions/push_ruleset.h8
-rw-r--r--lib/csapi/definitions/room_event_filter.cpp22
-rw-r--r--lib/csapi/definitions/room_event_filter.h12
-rw-r--r--lib/csapi/definitions/sync_filter.cpp74
-rw-r--r--lib/csapi/definitions/sync_filter.h51
-rw-r--r--lib/csapi/definitions/user_identifier.cpp16
-rw-r--r--lib/csapi/definitions/user_identifier.h8
-rw-r--r--lib/csapi/definitions/wellknown/full.cpp25
-rw-r--r--lib/csapi/definitions/wellknown/full.h38
-rw-r--r--lib/csapi/definitions/wellknown/homeserver.cpp14
-rw-r--r--lib/csapi/definitions/wellknown/homeserver.h8
-rw-r--r--lib/csapi/definitions/wellknown/identity_server.cpp14
-rw-r--r--lib/csapi/definitions/wellknown/identity_server.h8
-rw-r--r--lib/csapi/device_management.cpp4
-rw-r--r--lib/csapi/directory.cpp4
-rw-r--r--lib/csapi/event_context.cpp12
-rw-r--r--lib/csapi/filter.cpp4
-rw-r--r--lib/csapi/gtad.yaml16
-rw-r--r--lib/csapi/joining.cpp51
-rw-r--r--lib/csapi/joining.h4
-rw-r--r--lib/csapi/keys.cpp35
-rw-r--r--lib/csapi/kicking.h2
-rw-r--r--lib/csapi/list_joined_rooms.cpp2
-rw-r--r--lib/csapi/list_public_rooms.cpp19
-rw-r--r--lib/csapi/list_public_rooms.h2
-rw-r--r--lib/csapi/login.cpp27
-rw-r--r--lib/csapi/login.h6
-rw-r--r--lib/csapi/message_pagination.cpp6
-rw-r--r--lib/csapi/notifications.cpp29
-rw-r--r--lib/csapi/openid.cpp8
-rw-r--r--lib/csapi/peeking_events.cpp6
-rw-r--r--lib/csapi/presence.cpp58
-rw-r--r--lib/csapi/presence.h55
-rw-r--r--lib/csapi/profile.cpp8
-rw-r--r--lib/csapi/pusher.cpp61
-rw-r--r--lib/csapi/pusher.h2
-rw-r--r--lib/csapi/pushrules.cpp8
-rw-r--r--lib/csapi/read_markers.h2
-rw-r--r--lib/csapi/redaction.cpp2
-rw-r--r--lib/csapi/registration.cpp24
-rw-r--r--lib/csapi/registration.h4
-rw-r--r--lib/csapi/room_send.cpp2
-rw-r--r--lib/csapi/room_state.cpp4
-rw-r--r--lib/csapi/room_upgrades.cpp49
-rw-r--r--lib/csapi/room_upgrades.h41
-rw-r--r--lib/csapi/rooms.cpp80
-rw-r--r--lib/csapi/rooms.h39
-rw-r--r--lib/csapi/search.cpp179
-rw-r--r--lib/csapi/search.h4
-rw-r--r--lib/csapi/sso_login_redirect.cpp38
-rw-r--r--lib/csapi/sso_login_redirect.h39
-rw-r--r--lib/csapi/tags.cpp14
-rw-r--r--lib/csapi/third_party_lookup.cpp12
-rw-r--r--lib/csapi/users.cpp20
-rw-r--r--lib/csapi/versions.cpp12
-rw-r--r--lib/csapi/versions.h19
-rw-r--r--lib/csapi/voip.cpp2
-rw-r--r--lib/csapi/wellknown.cpp19
-rw-r--r--lib/csapi/wellknown.h9
-rw-r--r--lib/csapi/whoami.cpp2
-rw-r--r--lib/csapi/{{base}}.cpp.mustache69
-rw-r--r--lib/csapi/{{base}}.h.mustache13
-rw-r--r--lib/eventitem.cpp26
-rw-r--r--lib/eventitem.h15
-rw-r--r--lib/events/accountdataevents.h35
-rw-r--r--lib/events/event.cpp8
-rw-r--r--lib/events/event.h28
-rw-r--r--lib/events/eventcontent.cpp38
-rw-r--r--lib/events/eventcontent.h28
-rw-r--r--lib/events/eventloader.h11
-rw-r--r--lib/events/roomavatarevent.h3
-rw-r--r--lib/events/roomcreateevent.cpp45
-rw-r--r--lib/events/roomcreateevent.h49
-rw-r--r--lib/events/roomevent.cpp5
-rw-r--r--lib/events/roommemberevent.cpp14
-rw-r--r--lib/events/roommemberevent.h31
-rw-r--r--lib/events/roommessageevent.cpp129
-rw-r--r--lib/events/roommessageevent.h36
-rw-r--r--lib/events/roomtombstoneevent.cpp31
-rw-r--r--lib/events/roomtombstoneevent.h41
-rw-r--r--lib/events/simplestateevents.h31
-rw-r--r--lib/events/stateevent.cpp22
-rw-r--r--lib/events/stateevent.h31
-rw-r--r--lib/identity/definitions/request_email_validation.cpp23
-rw-r--r--lib/identity/definitions/request_email_validation.h8
-rw-r--r--lib/identity/definitions/request_msisdn_validation.cpp26
-rw-r--r--lib/identity/definitions/request_msisdn_validation.h8
-rw-r--r--lib/identity/definitions/sid.cpp14
-rw-r--r--lib/identity/definitions/sid.h8
-rw-r--r--lib/jobs/basejob.cpp58
-rw-r--r--lib/jobs/basejob.h21
-rw-r--r--lib/jobs/downloadfilejob.cpp5
-rw-r--r--lib/jobs/mediathumbnailjob.cpp2
-rw-r--r--lib/jobs/syncjob.cpp118
-rw-r--r--lib/jobs/syncjob.h51
-rw-r--r--lib/joinstate.h2
-rw-r--r--lib/networkaccessmanager.cpp3
-rw-r--r--lib/networksettings.cpp2
-rw-r--r--lib/qt_connection_util.h107
-rw-r--r--lib/room.cpp1133
-rw-r--r--lib/room.h208
-rw-r--r--lib/settings.cpp21
-rw-r--r--lib/settings.h2
-rw-r--r--lib/syncdata.cpp228
-rw-r--r--lib/syncdata.h116
-rw-r--r--lib/user.cpp47
-rw-r--r--lib/user.h8
-rw-r--r--lib/util.cpp110
-rw-r--r--lib/util.h195
146 files changed, 4280 insertions, 2295 deletions
diff --git a/lib/application-service/definitions/location.cpp b/lib/application-service/definitions/location.cpp
index 958a55bf..a53db8d7 100644
--- a/lib/application-service/definitions/location.cpp
+++ b/lib/application-service/definitions/location.cpp
@@ -6,25 +6,19 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const ThirdPartyLocation& pod)
+void JsonObjectConverter<ThirdPartyLocation>::dumpTo(
+ QJsonObject& jo, const ThirdPartyLocation& pod)
{
- QJsonObject jo;
addParam<>(jo, QStringLiteral("alias"), pod.alias);
addParam<>(jo, QStringLiteral("protocol"), pod.protocol);
addParam<>(jo, QStringLiteral("fields"), pod.fields);
- return jo;
}
-ThirdPartyLocation FromJsonObject<ThirdPartyLocation>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<ThirdPartyLocation>::fillFrom(
+ const QJsonObject& jo, ThirdPartyLocation& result)
{
- ThirdPartyLocation result;
- result.alias =
- fromJson<QString>(jo.value("alias"_ls));
- result.protocol =
- fromJson<QString>(jo.value("protocol"_ls));
- result.fields =
- fromJson<QJsonObject>(jo.value("fields"_ls));
-
- return result;
+ fromJson(jo.value("alias"_ls), result.alias);
+ fromJson(jo.value("protocol"_ls), result.protocol);
+ fromJson(jo.value("fields"_ls), result.fields);
}
diff --git a/lib/application-service/definitions/location.h b/lib/application-service/definitions/location.h
index 89b48a43..5586cfc6 100644
--- a/lib/application-service/definitions/location.h
+++ b/lib/application-service/definitions/location.h
@@ -21,12 +21,10 @@ namespace QMatrixClient
/// Information used to identify this third party location.
QJsonObject fields;
};
-
- QJsonObject toJson(const ThirdPartyLocation& pod);
-
- template <> struct FromJsonObject<ThirdPartyLocation>
+ template <> struct JsonObjectConverter<ThirdPartyLocation>
{
- ThirdPartyLocation operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const ThirdPartyLocation& pod);
+ static void fillFrom(const QJsonObject& jo, ThirdPartyLocation& pod);
};
} // namespace QMatrixClient
diff --git a/lib/application-service/definitions/protocol.cpp b/lib/application-service/definitions/protocol.cpp
index 04bb7dfc..2a62b15d 100644
--- a/lib/application-service/definitions/protocol.cpp
+++ b/lib/application-service/definitions/protocol.cpp
@@ -6,75 +6,55 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const FieldType& pod)
+void JsonObjectConverter<FieldType>::dumpTo(
+ QJsonObject& jo, const FieldType& pod)
{
- QJsonObject jo;
addParam<>(jo, QStringLiteral("regexp"), pod.regexp);
addParam<>(jo, QStringLiteral("placeholder"), pod.placeholder);
- return jo;
}
-FieldType FromJsonObject<FieldType>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<FieldType>::fillFrom(
+ const QJsonObject& jo, FieldType& result)
{
- FieldType result;
- result.regexp =
- fromJson<QString>(jo.value("regexp"_ls));
- result.placeholder =
- fromJson<QString>(jo.value("placeholder"_ls));
-
- return result;
+ fromJson(jo.value("regexp"_ls), result.regexp);
+ fromJson(jo.value("placeholder"_ls), result.placeholder);
}
-QJsonObject QMatrixClient::toJson(const ProtocolInstance& pod)
+void JsonObjectConverter<ProtocolInstance>::dumpTo(
+ QJsonObject& jo, const ProtocolInstance& pod)
{
- QJsonObject jo;
addParam<>(jo, QStringLiteral("desc"), pod.desc);
addParam<IfNotEmpty>(jo, QStringLiteral("icon"), pod.icon);
addParam<>(jo, QStringLiteral("fields"), pod.fields);
addParam<>(jo, QStringLiteral("network_id"), pod.networkId);
- return jo;
}
-ProtocolInstance FromJsonObject<ProtocolInstance>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<ProtocolInstance>::fillFrom(
+ const QJsonObject& jo, ProtocolInstance& result)
{
- ProtocolInstance result;
- result.desc =
- fromJson<QString>(jo.value("desc"_ls));
- result.icon =
- fromJson<QString>(jo.value("icon"_ls));
- result.fields =
- fromJson<QJsonObject>(jo.value("fields"_ls));
- result.networkId =
- fromJson<QString>(jo.value("network_id"_ls));
-
- return result;
+ fromJson(jo.value("desc"_ls), result.desc);
+ fromJson(jo.value("icon"_ls), result.icon);
+ fromJson(jo.value("fields"_ls), result.fields);
+ fromJson(jo.value("network_id"_ls), result.networkId);
}
-QJsonObject QMatrixClient::toJson(const ThirdPartyProtocol& pod)
+void JsonObjectConverter<ThirdPartyProtocol>::dumpTo(
+ QJsonObject& jo, const ThirdPartyProtocol& pod)
{
- QJsonObject jo;
addParam<>(jo, QStringLiteral("user_fields"), pod.userFields);
addParam<>(jo, QStringLiteral("location_fields"), pod.locationFields);
addParam<>(jo, QStringLiteral("icon"), pod.icon);
addParam<>(jo, QStringLiteral("field_types"), pod.fieldTypes);
addParam<>(jo, QStringLiteral("instances"), pod.instances);
- return jo;
}
-ThirdPartyProtocol FromJsonObject<ThirdPartyProtocol>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<ThirdPartyProtocol>::fillFrom(
+ const QJsonObject& jo, ThirdPartyProtocol& result)
{
- ThirdPartyProtocol result;
- result.userFields =
- fromJson<QStringList>(jo.value("user_fields"_ls));
- result.locationFields =
- fromJson<QStringList>(jo.value("location_fields"_ls));
- result.icon =
- fromJson<QString>(jo.value("icon"_ls));
- result.fieldTypes =
- fromJson<QHash<QString, FieldType>>(jo.value("field_types"_ls));
- result.instances =
- fromJson<QVector<ProtocolInstance>>(jo.value("instances"_ls));
-
- return result;
+ fromJson(jo.value("user_fields"_ls), result.userFields);
+ fromJson(jo.value("location_fields"_ls), result.locationFields);
+ fromJson(jo.value("icon"_ls), result.icon);
+ fromJson(jo.value("field_types"_ls), result.fieldTypes);
+ fromJson(jo.value("instances"_ls), result.instances);
}
diff --git a/lib/application-service/definitions/protocol.h b/lib/application-service/definitions/protocol.h
index 2aca7d66..0a1f9a21 100644
--- a/lib/application-service/definitions/protocol.h
+++ b/lib/application-service/definitions/protocol.h
@@ -25,12 +25,10 @@ namespace QMatrixClient
/// An placeholder serving as a valid example of the field value.
QString placeholder;
};
-
- QJsonObject toJson(const FieldType& pod);
-
- template <> struct FromJsonObject<FieldType>
+ template <> struct JsonObjectConverter<FieldType>
{
- FieldType operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const FieldType& pod);
+ static void fillFrom(const QJsonObject& jo, FieldType& pod);
};
struct ProtocolInstance
@@ -45,12 +43,10 @@ namespace QMatrixClient
/// A unique identifier across all instances.
QString networkId;
};
-
- QJsonObject toJson(const ProtocolInstance& pod);
-
- template <> struct FromJsonObject<ProtocolInstance>
+ template <> struct JsonObjectConverter<ProtocolInstance>
{
- ProtocolInstance operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const ProtocolInstance& pod);
+ static void fillFrom(const QJsonObject& jo, ProtocolInstance& pod);
};
struct ThirdPartyProtocol
@@ -78,12 +74,10 @@ namespace QMatrixClient
/// same application service.
QVector<ProtocolInstance> instances;
};
-
- QJsonObject toJson(const ThirdPartyProtocol& pod);
-
- template <> struct FromJsonObject<ThirdPartyProtocol>
+ template <> struct JsonObjectConverter<ThirdPartyProtocol>
{
- ThirdPartyProtocol operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const ThirdPartyProtocol& pod);
+ static void fillFrom(const QJsonObject& jo, ThirdPartyProtocol& pod);
};
} // namespace QMatrixClient
diff --git a/lib/application-service/definitions/user.cpp b/lib/application-service/definitions/user.cpp
index ca334236..8ba92321 100644
--- a/lib/application-service/definitions/user.cpp
+++ b/lib/application-service/definitions/user.cpp
@@ -6,25 +6,19 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const ThirdPartyUser& pod)
+void JsonObjectConverter<ThirdPartyUser>::dumpTo(
+ QJsonObject& jo, const ThirdPartyUser& pod)
{
- QJsonObject jo;
addParam<>(jo, QStringLiteral("userid"), pod.userid);
addParam<>(jo, QStringLiteral("protocol"), pod.protocol);
addParam<>(jo, QStringLiteral("fields"), pod.fields);
- return jo;
}
-ThirdPartyUser FromJsonObject<ThirdPartyUser>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<ThirdPartyUser>::fillFrom(
+ const QJsonObject& jo, ThirdPartyUser& result)
{
- ThirdPartyUser result;
- result.userid =
- fromJson<QString>(jo.value("userid"_ls));
- result.protocol =
- fromJson<QString>(jo.value("protocol"_ls));
- result.fields =
- fromJson<QJsonObject>(jo.value("fields"_ls));
-
- return result;
+ fromJson(jo.value("userid"_ls), result.userid);
+ fromJson(jo.value("protocol"_ls), result.protocol);
+ fromJson(jo.value("fields"_ls), result.fields);
}
diff --git a/lib/application-service/definitions/user.h b/lib/application-service/definitions/user.h
index 79ca7789..062d2cac 100644
--- a/lib/application-service/definitions/user.h
+++ b/lib/application-service/definitions/user.h
@@ -21,12 +21,10 @@ namespace QMatrixClient
/// Information used to identify this third party location.
QJsonObject fields;
};
-
- QJsonObject toJson(const ThirdPartyUser& pod);
-
- template <> struct FromJsonObject<ThirdPartyUser>
+ template <> struct JsonObjectConverter<ThirdPartyUser>
{
- ThirdPartyUser operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const ThirdPartyUser& pod);
+ static void fillFrom(const QJsonObject& jo, ThirdPartyUser& pod);
};
} // namespace QMatrixClient
diff --git a/lib/avatar.cpp b/lib/avatar.cpp
index b8e1096d..9279ef9d 100644
--- a/lib/avatar.cpp
+++ b/lib/avatar.cpp
@@ -190,18 +190,8 @@ bool Avatar::Private::checkUrl(const QUrl& url) const
return _imageSource != Banned;
}
-QString cacheLocation() {
- const auto cachePath =
- QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
- + "/avatar/";
- QDir dir;
- if (!dir.exists(cachePath))
- dir.mkpath(cachePath);
- return cachePath;
-}
-
QString Avatar::Private::localFile() const {
- static const auto cachePath = cacheLocation();
+ static const auto cachePath = cacheLocation(QStringLiteral("avatars"));
return cachePath % _url.authority() % '_' % _url.fileName() % ".png";
}
diff --git a/lib/connection.cpp b/lib/connection.cpp
index 6bdedf26..ac69228b 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -24,6 +24,7 @@
#include "room.h"
#include "settings.h"
#include "csapi/login.h"
+#include "csapi/capabilities.h"
#include "csapi/logout.h"
#include "csapi/receipts.h"
#include "csapi/leaving.h"
@@ -39,11 +40,11 @@
#include <QtNetwork/QDnsLookup>
#include <QtCore/QFile>
#include <QtCore/QDir>
-#include <QtCore/QFileInfo>
#include <QtCore/QStandardPaths>
#include <QtCore/QStringBuilder>
#include <QtCore/QElapsedTimer>
#include <QtCore/QRegularExpression>
+#include <QtCore/QMimeDatabase>
#include <QtCore/QCoreApplication>
using namespace QMatrixClient;
@@ -65,10 +66,6 @@ HashT erase_if(HashT& hashMap, Pred pred)
return removals;
}
-#ifndef TRIM_RAW_DATA
-#define TRIM_RAW_DATA 65535
-#endif
-
class Connection::Private
{
public:
@@ -76,8 +73,7 @@ class Connection::Private
: data(move(connection))
{ }
Q_DISABLE_COPY(Private)
- Private(Private&&) = delete;
- Private operator=(Private&&) = delete;
+ DISABLE_MOVE(Private)
Connection* q = nullptr;
std::unique_ptr<ConnectionData> data;
@@ -86,24 +82,34 @@ class Connection::Private
// separately so we should, e.g., keep objects for Invite and
// Leave state of the same room.
QHash<QPair<QString, bool>, Room*> roomMap;
+ // Mapping from aliases to room ids, as per the last sync
+ QHash<QString, QString> roomAliasMap;
QVector<QString> roomIdsToForget;
QVector<Room*> firstTimeRooms;
+ QVector<QString> pendingStateRoomIds;
QMap<QString, User*> userMap;
DirectChatsMap directChats;
DirectChatUsersMap directChatUsers;
+ // The below two variables track local changes between sync completions.
+ // See also: https://github.com/QMatrixClient/libqmatrixclient/wiki/Handling-direct-chat-events
+ DirectChatsMap dcLocalAdditions;
+ DirectChatsMap dcLocalRemovals;
std::unordered_map<QString, EventPtr> accountData;
QString userId;
+ int syncLoopTimeout = -1;
+
+ GetCapabilitiesJob* capabilitiesJob = nullptr;
+ GetCapabilitiesJob::Capabilities capabilities;
SyncJob* syncJob = nullptr;
bool cacheState = true;
bool cacheToBinary = SettingsGroup("libqmatrixclient")
.value("cache_type").toString() != "json";
+ bool lazyLoading = false;
void connectWithToken(const QString& user, const QString& accessToken,
const QString& deviceId);
- void broadcastDirectChatUpdates(const DirectChatsMap& additions,
- const DirectChatsMap& removals);
template <typename EventT>
EventT* unpackAccountData() const
@@ -228,11 +234,15 @@ void Connection::doConnectToServer(const QString& user, const QString& password,
});
connect(loginJob, &BaseJob::failure, this,
[this, loginJob] {
- emit loginError(loginJob->errorString(),
- loginJob->rawData(TRIM_RAW_DATA));
+ emit loginError(loginJob->errorString(), loginJob->rawDataSample());
});
}
+void Connection::syncLoopIteration()
+{
+ sync(d->syncLoopTimeout);
+}
+
void Connection::connectWithToken(const QString& userId,
const QString& accessToken,
const QString& deviceId)
@@ -241,6 +251,38 @@ void Connection::connectWithToken(const QString& userId,
[=] { d->connectWithToken(userId, accessToken, deviceId); });
}
+void Connection::reloadCapabilities()
+{
+ d->capabilitiesJob = callApi<GetCapabilitiesJob>(BackgroundRequest);
+ connect(d->capabilitiesJob, &BaseJob::finished, this, [this] {
+ if (d->capabilitiesJob->error() == BaseJob::Success)
+ d->capabilities = d->capabilitiesJob->capabilities();
+ else if (d->capabilitiesJob->error() == BaseJob::IncorrectRequestError)
+ qCDebug(MAIN) << "Server doesn't support /capabilities";
+
+ if (d->capabilities.roomVersions.omitted())
+ {
+ qCWarning(MAIN) << "Pinning supported room version to 1";
+ d->capabilities.roomVersions = { "1", {{ "1", "stable" }} };
+ } else {
+ qCDebug(MAIN) << "Room versions:"
+ << defaultRoomVersion() << "is default, full list:"
+ << availableRoomVersions();
+ }
+ Q_ASSERT(!d->capabilities.roomVersions.omitted());
+ emit capabilitiesLoaded();
+ for (auto* r: d->roomMap)
+ r->checkVersion();
+ });
+}
+
+bool Connection::loadingCapabilities() const
+{
+ // (Ab)use the fact that room versions cannot be omitted after
+ // the capabilities have been loaded (see reloadCapabilities() above).
+ return d->capabilities.roomVersions.omitted();
+}
+
void Connection::Private::connectWithToken(const QString& user,
const QString& accessToken,
const QString& deviceId)
@@ -253,7 +295,7 @@ void Connection::Private::connectWithToken(const QString& user,
<< "by user" << userId << "from device" << deviceId;
emit q->stateChanged();
emit q->connected();
-
+ q->reloadCapabilities();
}
void Connection::checkAndConnect(const QString& userId,
@@ -280,11 +322,14 @@ void Connection::checkAndConnect(const QString& userId,
void Connection::logout()
{
auto job = callApi<LogoutJob>();
- connect( job, &LogoutJob::success, this, [this] {
- stopSync();
- d->data->setToken({});
- emit stateChanged();
- emit loggedOut();
+ connect( job, &LogoutJob::finished, this, [job,this] {
+ if (job->status().good() || job->error() == BaseJob::ContentAccessError)
+ {
+ stopSync();
+ d->data->setToken({});
+ emit stateChanged();
+ emit loggedOut();
+ }
});
}
@@ -293,11 +338,11 @@ void Connection::sync(int timeout)
if (d->syncJob)
return;
- // Raw string: http://en.cppreference.com/w/cpp/language/string_literal
- const auto filter =
- QStringLiteral(R"({"room": { "timeline": { "limit": 100 } } })");
+ Filter filter;
+ filter.room->timeline->limit = 100;
+ filter.room->state->lazyLoadMembers = d->lazyLoading;
auto job = d->syncJob = callApi<SyncJob>(BackgroundRequest,
- d->data->lastEvent(), filter, timeout);
+ d->data->lastEvent(), filter, timeout);
connect( job, &SyncJob::success, this, [this, job] {
onSyncSuccess(job->takeData());
d->syncJob = nullptr;
@@ -306,7 +351,7 @@ void Connection::sync(int timeout)
connect( job, &SyncJob::retryScheduled, this,
[this,job] (int retriesTaken, int nextInMilliseconds)
{
- emit networkError(job->errorString(), job->rawData(TRIM_RAW_DATA),
+ emit networkError(job->errorString(), job->rawDataSample(),
retriesTaken, nextInMilliseconds);
});
connect( job, &SyncJob::failure, this, [this, job] {
@@ -315,14 +360,35 @@ void Connection::sync(int timeout)
{
qCWarning(SYNCJOB)
<< "Sync job failed with ContentAccessError - login expired?";
- emit loginError(job->errorString(), job->rawData(TRIM_RAW_DATA));
+ emit loginError(job->errorString(), job->rawDataSample());
}
else
- emit syncError(job->errorString(), job->rawData(TRIM_RAW_DATA));
+ emit syncError(job->errorString(), job->rawDataSample());
});
}
-void Connection::onSyncSuccess(SyncData &&data) {
+void Connection::syncLoop(int timeout)
+{
+ d->syncLoopTimeout = timeout;
+ connect(this, &Connection::syncDone, this, &Connection::syncLoopIteration);
+ syncLoopIteration(); // initial sync to start the loop
+}
+
+QJsonObject toJson(const Connection::DirectChatsMap& directChats)
+{
+ QJsonObject json;
+ for (auto it = directChats.begin(); it != directChats.end();)
+ {
+ QJsonArray roomIds;
+ const auto* user = it.key();
+ for (; it != directChats.end() && it.key() == user; ++it)
+ roomIds.append(*it);
+ json.insert(user->id(), roomIds);
+ }
+ return json;
+}
+
+void Connection::onSyncSuccess(SyncData &&data, bool fromCache) {
d->data->setLastEvent(data.nextBatch());
for (auto&& roomData: data.takeRoomData())
{
@@ -343,80 +409,123 @@ void Connection::onSyncSuccess(SyncData &&data) {
}
if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) )
{
- r->updateData(std::move(roomData));
+ d->pendingStateRoomIds.removeOne(roomData.roomId);
+ r->updateData(std::move(roomData), fromCache);
if (d->firstTimeRooms.removeOne(r))
+ {
emit loadedRoomState(r);
+ if (!d->capabilities.roomVersions.omitted())
+ r->checkVersion();
+ // Otherwise, the version will be checked in reloadCapabilities()
+ }
}
+ // Let UI update itself after updating each room
QCoreApplication::processEvents();
}
- for (auto&& accountEvent: data.takeAccountData())
+ // After running this loop, the account data events not saved in
+ // d->accountData (see the end of the loop body) are auto-cleaned away
+ for (auto& eventPtr : data.takeAccountData())
{
- if (is<DirectChatEvent>(*accountEvent))
- {
- const auto usersToDCs = ptrCast<DirectChatEvent>(move(accountEvent))
- ->usersToDirectChats();
- DirectChatsMap removals =
- erase_if(d->directChats, [&usersToDCs] (auto it) {
- return !usersToDCs.contains(it.key()->id(), it.value());
- });
- erase_if(d->directChatUsers, [&usersToDCs] (auto it) {
- return !usersToDCs.contains(it.value()->id(), it.key());
- });
- if (MAIN().isDebugEnabled())
- for (auto it = removals.begin(); it != removals.end(); ++it)
- qCDebug(MAIN) << it.value()
- << "is no more a direct chat with" << it.key()->id();
-
- DirectChatsMap additions;
- for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it)
- {
- if (auto* u = user(it.key()))
- {
- if (!d->directChats.contains(u, it.value()))
- {
- Q_ASSERT(!d->directChatUsers.contains(it.value(), u));
- additions.insert(u, it.value());
- d->directChats.insert(u, it.value());
- d->directChatUsers.insert(it.value(), u);
- qCDebug(MAIN) << "Marked room" << it.value()
+ visit(*eventPtr,
+ [this](const DirectChatEvent& dce) {
+ // See https://github.com/QMatrixClient/libqmatrixclient/wiki/Handling-direct-chat-events
+ const auto& usersToDCs = dce.usersToDirectChats();
+ DirectChatsMap remoteRemovals =
+ erase_if(d->directChats, [&usersToDCs, this](auto it) {
+ return !(usersToDCs.contains(it.key()->id(), it.value())
+ || d->dcLocalAdditions.contains(it.key(),
+ it.value()));
+ });
+ erase_if(d->directChatUsers, [&remoteRemovals](auto it) {
+ return remoteRemovals.contains(it.value(), it.key());
+ });
+ // Remove from dcLocalRemovals what the server already has.
+ erase_if(d->dcLocalRemovals, [&remoteRemovals](auto it) {
+ return remoteRemovals.contains(it.key(), it.value());
+ });
+ if (MAIN().isDebugEnabled())
+ for (auto it = remoteRemovals.begin();
+ it != remoteRemovals.end(); ++it) {
+ qCDebug(MAIN)
+ << it.value() << "is no more a direct chat with"
+ << it.key()->id();
+ }
+
+ DirectChatsMap remoteAdditions;
+ for (auto it = usersToDCs.begin(); it != usersToDCs.end();
+ ++it) {
+ if (auto* u = user(it.key())) {
+ if (!d->directChats.contains(u, it.value())
+ && !d->dcLocalRemovals.contains(u, it.value()))
+ {
+ Q_ASSERT(
+ !d->directChatUsers.contains(it.value(), u));
+ remoteAdditions.insert(u, it.value());
+ d->directChats.insert(u, it.value());
+ d->directChatUsers.insert(it.value(), u);
+ qCDebug(MAIN)
+ << "Marked room" << it.value()
<< "as a direct chat with" << u->id();
- }
- } else
- qCWarning(MAIN)
- << "Couldn't get a user object for" << it.key();
- }
- if (!additions.isEmpty() || !removals.isEmpty())
- emit directChatsListChanged(additions, removals);
-
- continue;
- }
- if (is<IgnoredUsersEvent>(*accountEvent))
- qCDebug(MAIN) << "Users ignored by" << d->userId << "updated:"
- << QStringList::fromSet(ignoredUsers()).join(',');
-
- auto& currentData = d->accountData[accountEvent->matrixType()];
- // A polymorphic event-specific comparison might be a bit more
- // efficient; maaybe do it another day
- if (!currentData ||
- currentData->contentJson() != accountEvent->contentJson())
- {
- currentData = std::move(accountEvent);
- qCDebug(MAIN) << "Updated account data of type"
- << currentData->matrixType();
- emit accountDataChanged(currentData->matrixType());
- }
+ }
+ } else
+ qCWarning(MAIN) << "Couldn't get a user object for"
+ << it.key();
+ }
+ // Remove from dcLocalAdditions what the server already has.
+ erase_if(d->dcLocalAdditions, [&remoteAdditions](auto it) {
+ return remoteAdditions.contains(it.key(), it.value());
+ });
+ if (!remoteAdditions.isEmpty() || !remoteRemovals.isEmpty())
+ emit directChatsListChanged(remoteAdditions, remoteRemovals);
+ },
+ // catch-all, passing eventPtr for a possible take-over
+ [this, &eventPtr](const Event& accountEvent) {
+ if (is<IgnoredUsersEvent>(accountEvent))
+ qCDebug(MAIN)
+ << "Users ignored by" << d->userId << "updated:"
+ << QStringList::fromSet(ignoredUsers()).join(',');
+
+ auto& currentData = d->accountData[accountEvent.matrixType()];
+ // A polymorphic event-specific comparison might be a bit more
+ // efficient; maaybe do it another day
+ if (!currentData
+ || currentData->contentJson()
+ != accountEvent.contentJson()) {
+ currentData = std::move(eventPtr);
+ qCDebug(MAIN) << "Updated account data of type"
+ << currentData->matrixType();
+ emit accountDataChanged(currentData->matrixType());
+ }
+ });
+ }
+ if (!d->dcLocalAdditions.isEmpty() || !d->dcLocalRemovals.isEmpty()) {
+ qDebug(MAIN) << "Sending updated direct chats to the server:"
+ << d->dcLocalRemovals.size() << "removal(s),"
+ << d->dcLocalAdditions.size() << "addition(s)";
+ callApi<SetAccountDataJob>(d->userId, QStringLiteral("m.direct"),
+ toJson(d->directChats));
+ d->dcLocalAdditions.clear();
+ d->dcLocalRemovals.clear();
}
}
void Connection::stopSync()
{
- if (d->syncJob)
+ // If there's a sync loop, break it
+ disconnect(this, &Connection::syncDone,
+ this, &Connection::syncLoopIteration);
+ if (d->syncJob) // If there's an ongoing sync job, stop it too
{
d->syncJob->abandon();
d->syncJob = nullptr;
}
}
+QString Connection::nextBatchToken() const
+{
+ return d->data->lastEvent();
+}
+
PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const
{
return callApi<PostReceiptJob>(room->id(), "m.read", event->id());
@@ -426,14 +535,32 @@ JoinRoomJob* Connection::joinRoom(const QString& roomAlias,
const QStringList& serverNames)
{
auto job = callApi<JoinRoomJob>(roomAlias, serverNames);
+ // Upon completion, ensure a room object in Join state is created but only
+ // if it's not already there due to a sync completing earlier.
connect(job, &JoinRoomJob::success,
- this, [this, job] { provideRoom(job->roomId(), JoinState::Join); });
+ this, [this, job] { provideRoom(job->roomId()); });
return job;
}
-void Connection::leaveRoom(Room* room)
+LeaveRoomJob* Connection::leaveRoom(Room* room)
{
- callApi<LeaveRoomJob>(room->id());
+ const auto& roomId = room->id();
+ const auto job = callApi<LeaveRoomJob>(roomId);
+ if (room->joinState() == JoinState::Invite)
+ {
+ // Workaround matrix-org/synapse#2181 - if the room is in invite state
+ // the invite may have been cancelled but Synapse didn't send it in
+ // `/sync`. See also #273 for the discussion in the library context.
+ d->pendingStateRoomIds.push_back(roomId);
+ connect(job, &LeaveRoomJob::success, this, [this,roomId] {
+ if (d->pendingStateRoomIds.removeOne(roomId))
+ {
+ qCDebug(MAIN) << "Forcing the room to Leave status";
+ provideRoom(roomId, JoinState::Leave);
+ }
+ });
+ }
+ return job;
}
inline auto splitMediaId(const QString& mediaId)
@@ -466,13 +593,21 @@ MediaThumbnailJob* Connection::getThumbnail(const QUrl& url,
}
UploadContentJob* Connection::uploadContent(QIODevice* contentSource,
- const QString& filename, const QString& contentType) const
+ const QString& filename, const QString& overrideContentType) const
{
+ auto contentType = overrideContentType;
+ if (contentType.isEmpty())
+ {
+ contentType =
+ QMimeDatabase().mimeTypeForFileNameAndData(filename, contentSource)
+ .name();
+ contentSource->open(QIODevice::ReadOnly);
+ }
return callApi<UploadContentJob>(contentSource, filename, contentType);
}
UploadContentJob* Connection::uploadFile(const QString& fileName,
- const QString& contentType)
+ const QString& overrideContentType)
{
auto sourceFile = new QFile(fileName);
if (!sourceFile->open(QIODevice::ReadOnly))
@@ -482,7 +617,7 @@ UploadContentJob* Connection::uploadFile(const QString& fileName,
return nullptr;
}
return uploadContent(sourceFile, QFileInfo(*sourceFile).fileName(),
- contentType);
+ overrideContentType);
}
GetContentJob* Connection::getContent(const QString& mediaId) const
@@ -508,7 +643,8 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url,
CreateRoomJob* Connection::createRoom(RoomVisibility visibility,
const QString& alias, const QString& name, const QString& topic,
- QStringList invites, const QString& presetName, bool isDirect,
+ QStringList invites, const QString& presetName,
+ const QString& roomVersion, bool isDirect,
const QVector<CreateRoomJob::StateEvent>& initialState,
const QVector<CreateRoomJob::Invite3pid>& invite3pids,
const QJsonObject& creationContent)
@@ -517,10 +653,19 @@ CreateRoomJob* Connection::createRoom(RoomVisibility visibility,
auto job = callApi<CreateRoomJob>(
visibility == PublishRoom ? QStringLiteral("public")
: QStringLiteral("private"),
- alias, name, topic, invites, invite3pids, QString(/*TODO: #233*/),
+ alias, name, topic, invites, invite3pids, roomVersion,
creationContent, initialState, presetName, isDirect);
- connect(job, &BaseJob::success, this, [this,job] {
- emit createdRoom(provideRoom(job->roomId(), JoinState::Join));
+ connect(job, &BaseJob::success, this, [this,job,invites,isDirect] {
+ auto* room = provideRoom(job->roomId(), JoinState::Join);
+ if (!room)
+ {
+ Q_ASSERT_X(room, "Connection::createRoom", "Failed to create a room");
+ return;
+ }
+ emit createdRoom(room);
+ if (isDirect)
+ for (const auto& i: invites)
+ addToDirectChats(room, user(i));
});
return job;
}
@@ -556,8 +701,8 @@ void Connection::doInDirectChat(User* u,
{
Q_ASSERT(u);
const auto& userId = u->id();
- // There can be more than one DC; find the first valid, and delete invalid
- // (left/forgotten) ones along the way.
+ // There can be more than one DC; find the first valid (existing and
+ // not left), and delete inexistent (forgotten?) ones along the way.
DirectChatsMap removals;
for (auto it = d->directChats.find(u);
it != d->directChats.end() && it.key() == u; ++it)
@@ -567,7 +712,7 @@ void Connection::doInDirectChat(User* u,
{
Q_ASSERT(r->id() == roomId);
// A direct chat with yourself should only involve yourself :)
- if (userId == d->userId && r->memberCount() > 1)
+ if (userId == d->userId && r->totalMemberCount() > 1)
continue;
qCDebug(MAIN) << "Requested direct chat with" << userId
<< "is already available as" << r->id();
@@ -594,6 +739,8 @@ void Connection::doInDirectChat(User* u,
<< roomId << "is not valid and will be discarded";
// Postpone actual deletion until we finish iterating d->directChats.
removals.insert(it.key(), it.value());
+ // Add to the list of updates to send to the server upon the next sync.
+ d->dcLocalRemovals.insert(it.key(), it.value());
}
if (!removals.isEmpty())
{
@@ -603,7 +750,7 @@ void Connection::doInDirectChat(User* u,
d->directChatUsers.remove(it.value(),
const_cast<User*>(it.key())); // FIXME
}
- d->broadcastDirectChatUpdates({}, removals);
+ emit directChatsListChanged({}, removals);
}
auto j = createDirectChat(userId);
@@ -618,8 +765,8 @@ void Connection::doInDirectChat(User* u,
CreateRoomJob* Connection::createDirectChat(const QString& userId,
const QString& topic, const QString& name)
{
- return createRoom(UnpublishRoom, "", name, topic, {userId},
- "trusted_private_chat", true);
+ return createRoom(UnpublishRoom, {}, name, topic, {userId},
+ QStringLiteral("trusted_private_chat"), {}, true);
}
ForgetRoomJob* Connection::forgetRoom(const QString& id)
@@ -698,6 +845,11 @@ QUrl Connection::homeserver() const
return d->data->baseUrl();
}
+QString Connection::domain() const
+{
+ return d->userId.section(':', 1);
+}
+
Room* Connection::room(const QString& roomId, JoinStates states) const
{
Room* room = d->roomMap.value({roomId, false}, nullptr);
@@ -716,6 +868,41 @@ Room* Connection::room(const QString& roomId, JoinStates states) const
return nullptr;
}
+Room* Connection::roomByAlias(const QString& roomAlias, JoinStates states) const
+{
+ const auto id = d->roomAliasMap.value(roomAlias);
+ if (!id.isEmpty())
+ return room(id, states);
+ qCWarning(MAIN) << "Room for alias" << roomAlias
+ << "is not found under account" << userId();
+ return nullptr;
+}
+
+void Connection::updateRoomAliases(const QString& roomId,
+ const QStringList& previousRoomAliases,
+ const QStringList& roomAliases)
+{
+ for (const auto& a: previousRoomAliases)
+ if (d->roomAliasMap.remove(a) == 0)
+ qCWarning(MAIN) << "Alias" << a << "is not found (already deleted?)";
+
+ for (const auto& a: roomAliases)
+ {
+ auto& mappedId = d->roomAliasMap[a];
+ if (!mappedId.isEmpty())
+ {
+ if (mappedId == roomId)
+ qCDebug(MAIN) << "Alias" << a << "is already mapped to room"
+ << roomId;
+ else
+ qCWarning(MAIN) << "Alias" << a
+ << "will be force-remapped from room"
+ << mappedId << "to" << roomId;
+ }
+ mappedId = roomId;
+ }
+}
+
Room* Connection::invitation(const QString& roomId) const
{
return d->roomMap.value({roomId, true}, nullptr);
@@ -825,7 +1012,8 @@ QHash<QString, QVector<Room*>> Connection::tagsToRooms() const
QHash<QString, QVector<Room*>> result;
for (auto* r: qAsConst(d->roomMap))
{
- for (const auto& tagName: r->tagNames())
+ const auto& tagNames = r->tagNames();
+ for (const auto& tagName: tagNames)
result[tagName].push_back(r);
}
for (auto it = result.begin(); it != result.end(); ++it)
@@ -840,9 +1028,12 @@ QStringList Connection::tagNames() const
{
QStringList tags ({FavouriteTag});
for (auto* r: qAsConst(d->roomMap))
- for (const auto& tag: r->tagNames())
+ {
+ const auto& tagNames = r->tagNames();
+ for (const auto& tag: tagNames)
if (tag != LowPriorityTag && !tags.contains(tag))
tags.push_back(tag);
+ }
tags.push_back(LowPriorityTag);
return tags;
}
@@ -860,28 +1051,6 @@ Connection::DirectChatsMap Connection::directChats() const
return d->directChats;
}
-QJsonObject toJson(const Connection::DirectChatsMap& directChats)
-{
- QJsonObject json;
- for (auto it = directChats.begin(); it != directChats.end();)
- {
- QJsonArray roomIds;
- const auto* user = it.key();
- for (; it != directChats.end() && it.key() == user; ++it)
- roomIds.append(*it);
- json.insert(user->id(), roomIds);
- }
- return json;
-}
-
-void Connection::Private::broadcastDirectChatUpdates(const DirectChatsMap& additions,
- const DirectChatsMap& removals)
-{
- q->callApi<SetAccountDataJob>(userId, QStringLiteral("m.direct"),
- toJson(directChats));
- emit q->directChatsListChanged(additions, removals);
-}
-
void Connection::addToDirectChats(const Room* room, User* user)
{
Q_ASSERT(room != nullptr && user != nullptr);
@@ -890,8 +1059,8 @@ void Connection::addToDirectChats(const Room* room, User* user)
Q_ASSERT(!d->directChatUsers.contains(room->id(), user));
d->directChats.insert(user, room->id());
d->directChatUsers.insert(room->id(), user);
- DirectChatsMap additions { { user, room->id() } };
- d->broadcastDirectChatUpdates(additions, {});
+ d->dcLocalAdditions.insert(user, room->id());
+ emit directChatsListChanged({ { user, room->id() } }, {});
}
void Connection::removeFromDirectChats(const QString& roomId, User* user)
@@ -904,15 +1073,17 @@ void Connection::removeFromDirectChats(const QString& roomId, User* user)
DirectChatsMap removals;
if (user != nullptr)
{
- removals.insert(user, roomId);
d->directChats.remove(user, roomId);
d->directChatUsers.remove(roomId, user);
+ removals.insert(user, roomId);
+ d->dcLocalRemovals.insert(user, roomId);
} else {
removals = erase_if(d->directChats,
[&roomId] (auto it) { return it.value() == roomId; });
d->directChatUsers.remove(roomId);
+ d->dcLocalRemovals += removals;
}
- d->broadcastDirectChatUpdates({}, removals);
+ emit directChatsListChanged({}, removals);
}
bool Connection::isDirectChat(const QString& roomId) const
@@ -972,11 +1143,12 @@ const ConnectionData* Connection::connectionData() const
return d->data.get();
}
-Room* Connection::provideRoom(const QString& id, JoinState joinState)
+Room* Connection::provideRoom(const QString& id, Omittable<JoinState> joinState)
{
// TODO: This whole function is a strong case for a RoomManager class.
Q_ASSERT_X(!id.isEmpty(), __FUNCTION__, "Empty room id");
+ // If joinState.omitted(), all joinState == comparisons below are false.
const auto roomKey = qMakePair(id, joinState == JoinState::Invite);
auto* room = d->roomMap.value(roomKey, nullptr);
if (room)
@@ -986,10 +1158,19 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState)
// and emit a signal. For Invite and Join, there's no such problem.
if (room->joinState() == joinState && joinState != JoinState::Leave)
return room;
+ } else if (joinState.omitted())
+ {
+ // No Join and Leave, maybe Invite?
+ room = d->roomMap.value({id, true}, nullptr);
+ if (room)
+ return room;
+ // No Invite either, setup a new room object below
}
- else
+
+ if (!room)
{
- room = roomFactory()(this, id, joinState);
+ room = roomFactory()(this, id,
+ joinState.omitted() ? JoinState::Join : joinState.value());
if (!room)
{
qCCritical(MAIN) << "Failed to create a room" << id;
@@ -1001,6 +1182,9 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState)
this, &Connection::aboutToDeleteRoom);
emit newRoom(room);
}
+ if (joinState.omitted())
+ return room;
+
if (joinState == JoinState::Invite)
{
// prev is either Leave or nullptr
@@ -1009,7 +1193,7 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState)
}
else
{
- room->setJoinState(joinState);
+ room->setJoinState(joinState.value());
// Preempt the Invite room (if any) with a room in Join/Leave state.
auto* prevInvite = d->roomMap.take({id, true});
if (joinState == JoinState::Join)
@@ -1018,6 +1202,9 @@ Room* Connection::provideRoom(const QString& id, JoinState joinState)
emit leftRoom(room, prevInvite);
if (prevInvite)
{
+ const auto dcUsers = prevInvite->directChatUsers();
+ for (auto* u: dcUsers)
+ addToDirectChats(room, u);
qCDebug(MAIN) << "Deleting Invite state for room" << prevInvite->id();
emit prevInvite->beforeDestruction(prevInvite);
prevInvite->deleteLater();
@@ -1064,56 +1251,64 @@ void Connection::setHomeserver(const QUrl& url)
emit homeserverChanged(homeserver());
}
-static constexpr int CACHE_VERSION_MAJOR = 8;
-static constexpr int CACHE_VERSION_MINOR = 0;
+void Connection::saveRoomState(Room* r) const
+{
+ Q_ASSERT(r);
+ if (!d->cacheState)
+ return;
-void Connection::saveState(const QUrl &toFile) const
+ QFile outRoomFile { stateCachePath() % SyncData::fileNameForRoom(r->id()) };
+ if (outRoomFile.open(QFile::WriteOnly))
+ {
+ QJsonDocument json { r->toJson() };
+ auto data = d->cacheToBinary ? json.toBinaryData()
+ : json.toJson(QJsonDocument::Compact);
+ outRoomFile.write(data.data(), data.size());
+ qCDebug(MAIN) << "Room state cache saved to" << outRoomFile.fileName();
+ } else {
+ qCWarning(MAIN) << "Error opening" << outRoomFile.fileName()
+ << ":" << outRoomFile.errorString();
+ }
+}
+
+void Connection::saveState() const
{
if (!d->cacheState)
return;
QElapsedTimer et; et.start();
- QFileInfo stateFile {
- toFile.isEmpty() ? stateCachePath() : toFile.toLocalFile()
- };
- if (!stateFile.dir().exists())
- stateFile.dir().mkpath(".");
-
- QFile outfile { stateFile.absoluteFilePath() };
- if (!outfile.open(QFile::WriteOnly))
+ QFile outFile { stateCachePath() % "state.json" };
+ if (!outFile.open(QFile::WriteOnly))
{
- qCWarning(MAIN) << "Error opening" << stateFile.absoluteFilePath()
- << ":" << outfile.errorString();
+ qCWarning(MAIN) << "Error opening" << outFile.fileName()
+ << ":" << outFile.errorString();
qCWarning(MAIN) << "Caching the rooms state disabled";
d->cacheState = false;
return;
}
- QJsonObject rootObj;
+ QJsonObject rootObj {
+ { QStringLiteral("cache_version"), QJsonObject {
+ { QStringLiteral("major"), SyncData::cacheVersion().first },
+ { QStringLiteral("minor"), SyncData::cacheVersion().second }
+ }}};
{
QJsonObject rooms;
QJsonObject inviteRooms;
- for (const auto* i : roomMap()) // Pass on rooms in Leave state
- {
- if (i->joinState() == JoinState::Invite)
- inviteRooms.insert(i->id(), i->toJson());
- else
- rooms.insert(i->id(), i->toJson());
- QElapsedTimer et1; et1.start();
- QCoreApplication::processEvents();
- if (et1.elapsed() > 1)
- qCDebug(PROFILER) << "processEvents() borrowed" << et1;
- }
+ const auto& rs = roomMap(); // Pass on rooms in Leave state
+ for (const auto* i : rs)
+ (i->joinState() == JoinState::Invite ? inviteRooms : rooms)
+ .insert(i->id(), QJsonValue::Null);
QJsonObject roomObj;
if (!rooms.isEmpty())
- roomObj.insert("join", rooms);
+ roomObj.insert(QStringLiteral("join"), rooms);
if (!inviteRooms.isEmpty())
- roomObj.insert("invite", inviteRooms);
+ roomObj.insert(QStringLiteral("invite"), inviteRooms);
- rootObj.insert("next_batch", d->data->lastEvent());
- rootObj.insert("rooms", roomObj);
+ rootObj.insert(QStringLiteral("next_batch"), d->data->lastEvent());
+ rootObj.insert(QStringLiteral("rooms"), roomObj);
}
{
QJsonArray accountDataEvents {
@@ -1123,67 +1318,39 @@ void Connection::saveState(const QUrl &toFile) const
accountDataEvents.append(
basicEventJson(e.first, e.second->contentJson()));
- rootObj.insert("account_data",
+ rootObj.insert(QStringLiteral("account_data"),
QJsonObject {{ QStringLiteral("events"), accountDataEvents }});
}
- QJsonObject versionObj;
- versionObj.insert("major", CACHE_VERSION_MAJOR);
- versionObj.insert("minor", CACHE_VERSION_MINOR);
- rootObj.insert("cache_version", versionObj);
-
QJsonDocument json { rootObj };
auto data = d->cacheToBinary ? json.toBinaryData() :
json.toJson(QJsonDocument::Compact);
qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et;
- outfile.write(data.data(), data.size());
- qCDebug(MAIN) << "State cache saved to" << outfile.fileName();
+ outFile.write(data.data(), data.size());
+ qCDebug(MAIN) << "State cache saved to" << outFile.fileName();
}
-void Connection::loadState(const QUrl &fromFile)
+void Connection::loadState()
{
if (!d->cacheState)
return;
QElapsedTimer et; et.start();
- QFile file {
- fromFile.isEmpty() ? stateCachePath() : fromFile.toLocalFile()
- };
- if (!file.exists())
- {
- qCDebug(MAIN) << "No state cache file found";
- return;
- }
- if(!file.open(QFile::ReadOnly))
- {
- qCWarning(MAIN) << "file " << file.fileName() << "failed to open for read";
- return;
- }
- QByteArray data = file.readAll();
- auto jsonDoc = d->cacheToBinary ? QJsonDocument::fromBinaryData(data) :
- QJsonDocument::fromJson(data);
- if (jsonDoc.isNull())
- {
- qCWarning(MAIN) << "Cache file broken, discarding";
+ SyncData sync { stateCachePath() % "state.json" };
+ if (sync.nextBatch().isEmpty()) // No token means no cache by definition
return;
- }
- auto actualCacheVersionMajor =
- jsonDoc.object()
- .value("cache_version").toObject()
- .value("major").toInt();
- if (actualCacheVersionMajor < CACHE_VERSION_MAJOR)
+
+ if (!sync.unresolvedRooms().isEmpty())
{
- qCWarning(MAIN)
- << "Major version of the cache file is" << actualCacheVersionMajor
- << "but" << CACHE_VERSION_MAJOR << "required; discarding the cache";
+ qCWarning(MAIN) << "State cache incomplete, discarding";
return;
}
-
- SyncData sync;
- sync.parseJson(jsonDoc);
- onSyncSuccess(std::move(sync));
+ // TODO: to handle load failures, instead of the above block:
+ // 1. Do initial sync on failed rooms without saving the nextBatch token
+ // 2. Do the sync across all rooms as normal
+ onSyncSuccess(std::move(sync), true);
qCDebug(PROFILER) << "*** Cached state for" << userId() << "loaded in" << et;
}
@@ -1191,8 +1358,7 @@ QString Connection::stateCachePath() const
{
auto safeUserId = userId();
safeUserId.replace(':', '_');
- return QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
- % '/' % safeUserId % "_state.json";
+ return cacheLocation(safeUserId);
}
bool Connection::cacheState() const
@@ -1209,11 +1375,70 @@ void Connection::setCacheState(bool newValue)
}
}
+bool QMatrixClient::Connection::lazyLoading() const
+{
+ return d->lazyLoading;
+}
+
+void QMatrixClient::Connection::setLazyLoading(bool newValue)
+{
+ if (d->lazyLoading != newValue)
+ {
+ d->lazyLoading = newValue;
+ emit lazyLoadingChanged();
+ }
+}
+
void Connection::getTurnServers()
{
- auto job = callApi<GetTurnServerJob>();
- connect( job, &GetTurnServerJob::success, [=] {
- emit turnServersChanged(job->data());
- });
+ auto job = callApi<GetTurnServerJob>();
+ connect(job, &GetTurnServerJob::success,
+ this, [=] { emit turnServersChanged(job->data()); });
+}
+
+const QString Connection::SupportedRoomVersion::StableTag =
+ QStringLiteral("stable");
+
+QString Connection::defaultRoomVersion() const
+{
+ Q_ASSERT(!d->capabilities.roomVersions.omitted());
+ return d->capabilities.roomVersions->defaultVersion;
+}
+QStringList Connection::stableRoomVersions() const
+{
+ Q_ASSERT(!d->capabilities.roomVersions.omitted());
+ QStringList l;
+ const auto& allVersions = d->capabilities.roomVersions->available;
+ for (auto it = allVersions.begin(); it != allVersions.end(); ++it)
+ if (it.value() == SupportedRoomVersion::StableTag)
+ l.push_back(it.key());
+ return l;
+}
+
+inline bool roomVersionLess(const Connection::SupportedRoomVersion& v1,
+ const Connection::SupportedRoomVersion& v2)
+{
+ bool ok1 = false, ok2 = false;
+ const auto vNum1 = v1.id.toFloat(&ok1);
+ const auto vNum2 = v2.id.toFloat(&ok2);
+ return ok1 && ok2 ? vNum1 < vNum2 : v1.id < v2.id;
+}
+
+QVector<Connection::SupportedRoomVersion> Connection::availableRoomVersions() const
+{
+ Q_ASSERT(!d->capabilities.roomVersions.omitted());
+ QVector<SupportedRoomVersion> result;
+ result.reserve(d->capabilities.roomVersions->available.size());
+ for (auto it = d->capabilities.roomVersions->available.begin();
+ it != d->capabilities.roomVersions->available.end(); ++it)
+ result.push_back({ it.key(), it.value() });
+ // Put stable versions over unstable; within each group,
+ // sort numeric versions as numbers, the rest as strings.
+ const auto mid = std::partition(result.begin(), result.end(),
+ std::mem_fn(&SupportedRoomVersion::isStable));
+ std::sort(result.begin(), mid, roomVersionLess);
+ std::sort(mid, result.end(), roomVersionLess);
+
+ return result;
}
diff --git a/lib/connection.h b/lib/connection.h
index b06fb143..ea5be51a 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -21,6 +21,7 @@
#include "csapi/create_room.h"
#include "joinstate.h"
#include "events/accountdataevents.h"
+#include "qt_connection_util.h"
#include <QtCore/QObject>
#include <QtCore/QUrl>
@@ -48,26 +49,7 @@ namespace QMatrixClient
class DownloadFileJob;
class SendToDeviceJob;
class SendMessageJob;
-
- /** Create a single-shot connection that triggers on the signal and
- * then self-disconnects
- *
- * Only supports DirectConnection type.
- */
- template <typename SenderT1, typename SignalT,
- typename ReceiverT2, typename SlotT>
- inline auto connectSingleShot(SenderT1* sender, SignalT signal,
- ReceiverT2* receiver, SlotT slot)
- {
- QMetaObject::Connection connection;
- connection = QObject::connect(sender, signal, receiver, slot,
- Qt::DirectConnection);
- Q_ASSERT(connection);
- QObject::connect(sender, signal, receiver,
- [connection] { QObject::disconnect(connection); },
- Qt::DirectConnection);
- return connection;
- }
+ class LeaveRoomJob;
class Connection;
@@ -120,8 +102,12 @@ namespace QMatrixClient
Q_PROPERTY(QString localUserId READ userId NOTIFY stateChanged)
Q_PROPERTY(QString deviceId READ deviceId NOTIFY stateChanged)
Q_PROPERTY(QByteArray accessToken READ accessToken NOTIFY stateChanged)
+ Q_PROPERTY(QString defaultRoomVersion READ defaultRoomVersion NOTIFY capabilitiesLoaded)
Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged)
+ Q_PROPERTY(QString domain READ domain NOTIFY homeserverChanged)
Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged)
+ Q_PROPERTY(bool lazyLoading READ lazyLoading WRITE setLazyLoading NOTIFY lazyLoadingChanged)
+
public:
// Room ids, rather than room pointers, are used in the direct chat
// map types because the library keeps Invite rooms separate from
@@ -246,8 +232,8 @@ namespace QMatrixClient
*/
void addToIgnoredUsers(const User* user);
- /** Remove the user from the ignore list
- * Similar to adding, the change signal is emitted synchronously.
+ /** Remove the user from the ignore list */
+ /** Similar to adding, the change signal is emitted synchronously.
*
* \sa ignoredUsersListChanged
*/
@@ -256,9 +242,22 @@ namespace QMatrixClient
/** Get the full list of users known to this account */
QMap<QString, User*> users() const;
+ /** Get the base URL of the homeserver to connect to */
QUrl homeserver() const;
+ /** Get the domain name used for ids/aliases on the server */
+ QString domain() const;
+ /** Find a room by its id and a mask of applicable states */
Q_INVOKABLE Room* room(const QString& roomId,
- JoinStates states = JoinState::Invite|JoinState::Join) const;
+ JoinStates states = JoinState::Invite|JoinState::Join) const;
+ /** Find a room by its alias and a mask of applicable states */
+ Q_INVOKABLE Room* roomByAlias(const QString& roomAlias,
+ JoinStates states = JoinState::Invite|JoinState::Join) const;
+ /** Update the internal map of room aliases to IDs */
+ /// This is used for internal bookkeeping of rooms. Do NOT use
+ /// it to try change aliases, use Room::setAliases instead
+ void updateRoomAliases(const QString& roomId,
+ const QStringList& previousRoomAliases,
+ const QStringList& roomAliases);
Q_INVOKABLE Room* invitation(const QString& roomId) const;
Q_INVOKABLE User* user(const QString& userId);
const User* user() const;
@@ -273,6 +272,35 @@ namespace QMatrixClient
Q_INVOKABLE QString token() const;
Q_INVOKABLE void getTurnServers();
+ struct SupportedRoomVersion
+ {
+ QString id;
+ QString status;
+
+ static const QString StableTag; // "stable", as of CS API 0.5
+ bool isStable() const { return status == StableTag; }
+
+ friend QDebug operator<<(QDebug dbg,
+ const SupportedRoomVersion& v)
+ {
+ QDebugStateSaver _(dbg);
+ return dbg.nospace() << v.id << '/' << v.status;
+ }
+ };
+
+ /// Get the room version recommended by the server
+ /** Only works after server capabilities have been loaded.
+ * \sa loadingCapabilities */
+ QString defaultRoomVersion() const;
+ /// Get the room version considered stable by the server
+ /** Only works after server capabilities have been loaded.
+ * \sa loadingCapabilities */
+ QStringList stableRoomVersions() const;
+ /// Get all room versions supported by the server
+ /** Only works after server capabilities have been loaded.
+ * \sa loadingCapabilities */
+ QVector<SupportedRoomVersion> availableRoomVersions() const;
+
/**
* Call this before first sync to load from previously saved file.
*
@@ -280,7 +308,7 @@ namespace QMatrixClient
* to be QML-friendly. Empty parameter means using a path
* defined by stateCachePath().
*/
- Q_INVOKABLE void loadState(const QUrl &fromFile = {});
+ Q_INVOKABLE void loadState();
/**
* This method saves the current state of rooms (but not messages
* in them) to a local cache file, so that it could be loaded by
@@ -290,7 +318,10 @@ namespace QMatrixClient
* QML-friendly. Empty parameter means using a path defined by
* stateCachePath().
*/
- Q_INVOKABLE void saveState(const QUrl &toFile = {}) const;
+ Q_INVOKABLE void saveState() const;
+
+ /// This method saves the current state of a single room.
+ void saveRoomState(Room* r) const;
/**
* The default path to store the cached room state, defined as
@@ -305,6 +336,9 @@ namespace QMatrixClient
bool cacheState() const;
void setCacheState(bool newValue);
+ bool lazyLoading() const;
+ void setLazyLoading(bool newValue);
+
/** Start a job of a specified type with specified arguments and policy
*
* This is a universal method to start a job of a type passed
@@ -375,13 +409,21 @@ namespace QMatrixClient
const QString& deviceId = {});
void connectWithToken(const QString& userId, const QString& accessToken,
const QString& deviceId);
+ /// Explicitly request capabilities from the server
+ void reloadCapabilities();
+
+ /// Find out if capabilites are still loading from the server
+ bool loadingCapabilities() const;
/** @deprecated Use stopSync() instead */
void disconnectFromServer() { stopSync(); }
void logout();
void sync(int timeout = -1);
+ void syncLoop(int timeout = -1);
+
void stopSync();
+ QString nextBatchToken() const;
virtual MediaThumbnailJob* getThumbnail(const QString& mediaId,
QSize requestedSize, RunningPolicy policy = BackgroundRequest) const;
@@ -393,10 +435,10 @@ namespace QMatrixClient
// QIODevice* should already be open
UploadContentJob* uploadContent(QIODevice* contentSource,
- const QString& filename = {},
- const QString& contentType = {}) const;
+ const QString& filename = {},
+ const QString& overrideContentType = {}) const;
UploadContentJob* uploadFile(const QString& fileName,
- const QString& contentType = {});
+ const QString& overrideContentType = {});
GetContentJob* getContent(const QString& mediaId) const;
GetContentJob* getContent(const QUrl& url) const;
// If localFilename is empty, a temporary file will be created
@@ -411,7 +453,7 @@ namespace QMatrixClient
CreateRoomJob* createRoom(RoomVisibility visibility,
const QString& alias, const QString& name, const QString& topic,
QStringList invites, const QString& presetName = {},
- bool isDirect = false,
+ const QString& roomVersion = {}, bool isDirect = false,
const QVector<CreateRoomJob::StateEvent>& initialState = {},
const QVector<CreateRoomJob::Invite3pid>& invite3pids = {},
const QJsonObject& creationContent = {});
@@ -485,14 +527,14 @@ namespace QMatrixClient
SendMessageJob* sendMessage(const QString& roomId,
const RoomEvent& event) const;
+ /** \deprecated Do not use this directly, use Room::leaveRoom() instead */
+ virtual LeaveRoomJob* leaveRoom( Room* room );
+
// Old API that will be abolished any time soon. DO NOT USE.
/** @deprecated Use callApi<PostReceiptJob>() or Room::postReceipt() instead */
virtual PostReceiptJob* postReceipt(Room* room,
RoomEvent* event) const;
- /** @deprecated Use callApi<LeaveRoomJob>() or Room::leaveRoom() instead */
- virtual void leaveRoom( Room* room );
-
signals:
/**
* @deprecated
@@ -508,6 +550,7 @@ namespace QMatrixClient
void resolveError(QString error);
void homeserverChanged(QUrl baseUrl);
+ void capabilitiesLoaded();
void connected();
void reconnected(); //< \deprecated Use connected() instead
@@ -519,7 +562,7 @@ namespace QMatrixClient
* a successful login and logout and are constant at other times.
*/
void stateChanged();
- void loginError(QString message, QByteArray details);
+ void loginError(QString message, QString details);
/** A network request (job) failed
*
@@ -537,11 +580,11 @@ namespace QMatrixClient
* @param retriesTaken - how many retries have already been taken
* @param nextRetryInMilliseconds - when the job will retry again
*/
- void networkError(QString message, QByteArray details,
+ void networkError(QString message, QString details,
int retriesTaken, int nextRetryInMilliseconds);
void syncDone();
- void syncError(QString message, QByteArray details);
+ void syncError(QString message, QString details);
void newUser(User* user);
@@ -652,6 +695,7 @@ namespace QMatrixClient
IgnoredUsersList removals);
void cacheStateChanged();
+ void lazyLoadingChanged();
void turnServersChanged(const QJsonObject& servers);
protected:
@@ -660,21 +704,35 @@ namespace QMatrixClient
*/
const ConnectionData* connectionData() const;
- /**
- * @brief Find a (possibly new) Room object for the specified id
- * Use this method whenever you need to find a Room object in
- * the local list of rooms. Note that this does not interact with
- * the server; in particular, does not automatically create rooms
- * on the server.
- * @return a pointer to a Room object with the specified id; nullptr
- * if roomId is empty or roomFactory() failed to create a Room object.
- */
- Room* provideRoom(const QString& roomId, JoinState joinState);
+ /** Get a Room object for the given id in the given state
+ *
+ * Use this method when you need a Room object in the local list
+ * of rooms, with the given state. Note that this does not interact
+ * with the server; in particular, does not automatically create
+ * rooms on the server. This call performs necessary join state
+ * transitions; e.g., if it finds a room in Invite but
+ * `joinState == JoinState::Join` then the Invite room object
+ * will be deleted and a new room object with Join state created.
+ * In contrast, switching between Join and Leave happens within
+ * the same object.
+ * \param roomId room id (not alias!)
+ * \param joinState desired (target) join state of the room; if
+ * omitted, any state will be found and return unchanged, or a
+ * new Join room created.
+ * @return a pointer to a Room object with the specified id and the
+ * specified state; nullptr if roomId is empty or if roomFactory()
+ * failed to create a Room object.
+ */
+ Room* provideRoom(const QString& roomId,
+ Omittable<JoinState> joinState = none);
/**
* Completes loading sync data.
*/
- void onSyncSuccess(SyncData &&data);
+ void onSyncSuccess(SyncData &&data, bool fromCache = false);
+
+ protected slots:
+ void syncLoopIteration();
private:
class Private;
diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp
index eb516ef7..91cda09f 100644
--- a/lib/connectiondata.cpp
+++ b/lib/connectiondata.cpp
@@ -25,7 +25,7 @@ using namespace QMatrixClient;
struct ConnectionData::Private
{
- explicit Private(const QUrl& url) : baseUrl(url) { }
+ explicit Private(QUrl url) : baseUrl(std::move(url)) { }
QUrl baseUrl;
QByteArray accessToken;
@@ -37,7 +37,7 @@ struct ConnectionData::Private
};
ConnectionData::ConnectionData(QUrl baseUrl)
- : d(std::make_unique<Private>(baseUrl))
+ : d(std::make_unique<Private>(std::move(baseUrl)))
{ }
ConnectionData::~ConnectionData() = default;
@@ -98,7 +98,7 @@ QString ConnectionData::lastEvent() const
void ConnectionData::setLastEvent(QString identifier)
{
- d->lastEvent = identifier;
+ d->lastEvent = std::move(identifier);
}
QByteArray ConnectionData::generateTxnId() const
diff --git a/lib/converters.cpp b/lib/converters.cpp
index 41a9a65e..88f5267e 100644
--- a/lib/converters.cpp
+++ b/lib/converters.cpp
@@ -22,38 +22,34 @@
using namespace QMatrixClient;
-QJsonValue QMatrixClient::variantToJson(const QVariant& v)
+QJsonValue JsonConverter<QVariant>::dump(const QVariant& v)
{
return QJsonValue::fromVariant(v);
}
-QJsonObject QMatrixClient::toJson(const QVariantMap& map)
+QVariant JsonConverter<QVariant>::load(const QJsonValue& jv)
{
- return QJsonObject::fromVariantMap(map);
+ return jv.toVariant();
}
-#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0))
-QJsonObject QMatrixClient::toJson(const QVariantHash& hMap)
+QJsonObject JsonConverter<variant_map_t>::dump(const variant_map_t& map)
{
- return QJsonObject::fromVariantHash(hMap);
-}
+ return
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0))
+ QJsonObject::fromVariantHash
+#else
+ QJsonObject::fromVariantMap
#endif
-
-QVariant FromJson<QVariant>::operator()(const QJsonValue& jv) const
-{
- return jv.toVariant();
+ (map);
}
-QMap<QString, QVariant>
-FromJson<QMap<QString, QVariant>>::operator()(const QJsonValue& jv) const
+variant_map_t JsonConverter<QVariantHash>::load(const QJsonValue& jv)
{
- return jv.toObject().toVariantMap();
-}
-
+ return jv.toObject().
#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0))
-QHash<QString, QVariant>
-FromJson<QHash<QString, QVariant>>::operator()(const QJsonValue& jv) const
-{
- return jv.toObject().toVariantHash();
-}
+ toVariantHash
+#else
+ toVariantMap
#endif
+ ();
+}
diff --git a/lib/converters.h b/lib/converters.h
index 53855a1f..af2be645 100644
--- a/lib/converters.h
+++ b/lib/converters.h
@@ -57,238 +57,226 @@ class QVariant;
namespace QMatrixClient
{
- // This catches anything implicitly convertible to QJsonValue/Object/Array
- inline auto toJson(const QJsonValue& val) { return val; }
- inline auto toJson(const QJsonObject& o) { return o; }
- inline auto toJson(const QJsonArray& arr) { return arr; }
- // Special-case QString to avoid ambiguity between QJsonValue
- // and QVariant (also, QString.isEmpty() is used in _impl::AddNode<> below)
- inline auto toJson(const QString& s) { return s; }
-
- inline QJsonArray toJson(const QStringList& strings)
- {
- return QJsonArray::fromStringList(strings);
- }
-
- inline QString toJson(const QByteArray& bytes)
+ template <typename T>
+ struct JsonObjectConverter
{
- return bytes.constData();
- }
+ static void dumpTo(QJsonObject& jo, const T& pod) { jo = pod; }
+ static void fillFrom(const QJsonObject& jo, T& pod) { pod = jo; }
+ };
- // QVariant is outrageously omnivorous - it consumes whatever is not
- // exactly matching the signature of other toJson overloads. The trick
- // below disables implicit conversion to QVariant through its numerous
- // non-explicit constructors.
- QJsonValue variantToJson(const QVariant& v);
template <typename T>
- inline auto toJson(T&& /* const QVariant& or QVariant&& */ var)
- -> std::enable_if_t<std::is_same<std::decay_t<T>, QVariant>::value,
- QJsonValue>
+ struct JsonConverter
{
- return variantToJson(var);
- }
- QJsonObject toJson(const QMap<QString, QVariant>& map);
-#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0))
- QJsonObject toJson(const QHash<QString, QVariant>& hMap);
-#endif
+ static QJsonObject dump(const T& pod)
+ {
+ QJsonObject jo;
+ JsonObjectConverter<T>::dumpTo(jo, pod);
+ return jo;
+ }
+ static T doLoad(const QJsonObject& jo)
+ {
+ T pod;
+ JsonObjectConverter<T>::fillFrom(jo, pod);
+ return pod;
+ }
+ static T load(const QJsonValue& jv) { return doLoad(jv.toObject()); }
+ static T load(const QJsonDocument& jd) { return doLoad(jd.object()); }
+ };
template <typename T>
- inline QJsonArray toJson(const std::vector<T>& vals)
+ inline auto toJson(const T& pod)
{
- QJsonArray ar;
- for (const auto& v: vals)
- ar.push_back(toJson(v));
- return ar;
+ return JsonConverter<T>::dump(pod);
}
template <typename T>
- inline QJsonArray toJson(const QVector<T>& vals)
+ inline auto fillJson(QJsonObject& json, const T& data)
{
- QJsonArray ar;
- for (const auto& v: vals)
- ar.push_back(toJson(v));
- return ar;
+ JsonObjectConverter<T>::dumpTo(json, data);
}
template <typename T>
- inline QJsonObject toJson(const QSet<T>& set)
+ inline auto fromJson(const QJsonValue& jv)
{
- QJsonObject json;
- for (auto e: set)
- json.insert(toJson(e), QJsonObject{});
- return json;
+ return JsonConverter<T>::load(jv);
}
template <typename T>
- inline QJsonObject toJson(const QHash<QString, T>& hashMap)
+ inline T fromJson(const QJsonDocument& jd)
{
- QJsonObject json;
- for (auto it = hashMap.begin(); it != hashMap.end(); ++it)
- json.insert(it.key(), toJson(it.value()));
- return json;
+ return JsonConverter<T>::load(jd);
}
template <typename T>
- inline QJsonObject toJson(const std::unordered_map<QString, T>& hashMap)
+ inline void fromJson(const QJsonValue& jv, T& pod)
{
- QJsonObject json;
- for (auto it = hashMap.begin(); it != hashMap.end(); ++it)
- json.insert(it.key(), toJson(it.value()));
- return json;
+ if (!jv.isUndefined())
+ pod = fromJson<T>(jv);
}
template <typename T>
- struct FromJsonObject
+ inline void fromJson(const QJsonDocument& jd, T& pod)
{
- T operator()(const QJsonObject& jo) const { return T(jo); }
- };
+ pod = fromJson<T>(jd);
+ }
+ // Unfolds Omittable<>
template <typename T>
- struct FromJson
+ inline void fromJson(const QJsonValue& jv, Omittable<T>& pod)
{
- T operator()(const QJsonValue& jv) const
- {
- return FromJsonObject<T>()(jv.toObject());
- }
- T operator()(const QJsonDocument& jd) const
- {
- return FromJsonObject<T>()(jd.object());
- }
- };
+ if (jv.isUndefined())
+ pod = none;
+ else
+ pod = fromJson<T>(jv);
+ }
template <typename T>
- inline auto fromJson(const QJsonValue& jv)
+ inline void fillFromJson(const QJsonValue& jv, T& pod)
{
- return FromJson<T>()(jv);
+ if (jv.isObject())
+ JsonObjectConverter<T>::fillFrom(jv.toObject(), pod);
}
+ // JsonConverter<> specialisations
+
template <typename T>
- inline auto fromJson(const QJsonDocument& jd)
+ struct TrivialJsonDumper
{
- return FromJson<T>()(jd);
- }
+ // Works for: QJsonValue (and all things it can consume),
+ // QJsonObject, QJsonArray
+ static auto dump(const T& val) { return val; }
+ };
- template <> struct FromJson<bool>
+ template <> struct JsonConverter<bool> : public TrivialJsonDumper<bool>
{
- auto operator()(const QJsonValue& jv) const { return jv.toBool(); }
+ static auto load(const QJsonValue& jv) { return jv.toBool(); }
};
- template <> struct FromJson<int>
+ template <> struct JsonConverter<int> : public TrivialJsonDumper<int>
{
- auto operator()(const QJsonValue& jv) const { return jv.toInt(); }
+ static auto load(const QJsonValue& jv) { return jv.toInt(); }
};
- template <> struct FromJson<double>
+ template <> struct JsonConverter<double>
+ : public TrivialJsonDumper<double>
{
- auto operator()(const QJsonValue& jv) const { return jv.toDouble(); }
+ static auto load(const QJsonValue& jv) { return jv.toDouble(); }
};
- template <> struct FromJson<float>
+ template <> struct JsonConverter<float> : public TrivialJsonDumper<float>
{
- auto operator()(const QJsonValue& jv) const { return float(jv.toDouble()); }
+ static auto load(const QJsonValue& jv) { return float(jv.toDouble()); }
};
- template <> struct FromJson<qint64>
+ template <> struct JsonConverter<qint64>
+ : public TrivialJsonDumper<qint64>
{
- auto operator()(const QJsonValue& jv) const { return qint64(jv.toDouble()); }
+ static auto load(const QJsonValue& jv) { return qint64(jv.toDouble()); }
};
- template <> struct FromJson<QString>
+ template <> struct JsonConverter<QString>
+ : public TrivialJsonDumper<QString>
{
- auto operator()(const QJsonValue& jv) const { return jv.toString(); }
+ static auto load(const QJsonValue& jv) { return jv.toString(); }
};
- template <> struct FromJson<QDateTime>
+ template <> struct JsonConverter<QDateTime>
{
- auto operator()(const QJsonValue& jv) const
+ static auto dump(const QDateTime& val) = delete; // not provided yet
+ static auto load(const QJsonValue& jv)
{
- return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC);
+ return QDateTime::fromMSecsSinceEpoch(
+ fromJson<qint64>(jv), Qt::UTC);
}
};
- template <> struct FromJson<QDate>
+ template <> struct JsonConverter<QDate>
{
- auto operator()(const QJsonValue& jv) const
+ static auto dump(const QDate& val) = delete; // not provided yet
+ static auto load(const QJsonValue& jv)
{
return fromJson<QDateTime>(jv).date();
}
};
- template <> struct FromJson<QJsonArray>
+ template <> struct JsonConverter<QJsonArray>
+ : public TrivialJsonDumper<QJsonArray>
{
- auto operator()(const QJsonValue& jv) const
- {
- return jv.toArray();
- }
+ static auto load(const QJsonValue& jv) { return jv.toArray(); }
};
- template <> struct FromJson<QByteArray>
+ template <> struct JsonConverter<QByteArray>
{
- auto operator()(const QJsonValue& jv) const
+ static QString dump(const QByteArray& ba) { return ba.constData(); }
+ static auto load(const QJsonValue& jv)
{
return fromJson<QString>(jv).toLatin1();
}
};
- template <> struct FromJson<QVariant>
+ template <> struct JsonConverter<QVariant>
{
- QVariant operator()(const QJsonValue& jv) const;
+ static QJsonValue dump(const QVariant& v);
+ static QVariant load(const QJsonValue& jv);
};
- template <typename VectorT>
- struct ArrayFromJson
+ template <typename VectorT,
+ typename T = typename VectorT::value_type>
+ struct JsonArrayConverter
{
- auto operator()(const QJsonArray& ja) const
+ static void dumpTo(QJsonArray& ar, const VectorT& vals)
{
- using size_type = typename VectorT::size_type;
- VectorT vect; vect.resize(size_type(ja.size()));
- std::transform(ja.begin(), ja.end(),
- vect.begin(), FromJson<typename VectorT::value_type>());
- return vect;
+ for (const auto& v: vals)
+ ar.push_back(toJson(v));
}
- auto operator()(const QJsonValue& jv) const
+ static auto dump(const VectorT& vals)
{
- return operator()(jv.toArray());
+ QJsonArray ja;
+ dumpTo(ja, vals);
+ return ja;
}
- auto operator()(const QJsonDocument& jd) const
+ static auto load(const QJsonArray& ja)
{
- return operator()(jd.array());
+ VectorT vect; vect.reserve(typename VectorT::size_type(ja.size()));
+ for (const auto& i: ja)
+ vect.push_back(fromJson<T>(i));
+ return vect;
}
+ static auto load(const QJsonValue& jv) { return load(jv.toArray()); }
+ static auto load(const QJsonDocument& jd) { return load(jd.array()); }
};
- template <typename T>
- struct FromJson<std::vector<T>> : ArrayFromJson<std::vector<T>>
+ template <typename T> struct JsonConverter<std::vector<T>>
+ : public JsonArrayConverter<std::vector<T>>
{ };
- template <typename T>
- struct FromJson<QVector<T>> : ArrayFromJson<QVector<T>>
+ template <typename T> struct JsonConverter<QVector<T>>
+ : public JsonArrayConverter<QVector<T>>
+ { };
+
+ template <typename T> struct JsonConverter<QList<T>>
+ : public JsonArrayConverter<QList<T>>
{ };
- template <typename T> struct FromJson<QList<T>>
+ template <> struct JsonConverter<QStringList>
+ : public JsonConverter<QList<QString>>
{
- auto operator()(const QJsonValue& jv) const
+ static auto dump(const QStringList& sl)
{
- const auto jsonArray = jv.toArray();
- QList<T> sl; sl.reserve(jsonArray.size());
- std::transform(jsonArray.begin(), jsonArray.end(),
- std::back_inserter(sl), FromJson<T>());
- return sl;
+ return QJsonArray::fromStringList(sl);
}
};
- template <> struct FromJson<QStringList> : FromJson<QList<QString>> { };
-
- template <> struct FromJson<QMap<QString, QVariant>>
+ template <> struct JsonObjectConverter<QSet<QString>>
{
- QMap<QString, QVariant> operator()(const QJsonValue& jv) const;
- };
-
- template <typename T> struct FromJson<QSet<T>>
- {
- auto operator()(const QJsonValue& jv) const
+ static void dumpTo(QJsonObject& json, const QSet<QString>& s)
+ {
+ for (const auto& e: s)
+ json.insert(toJson(e), QJsonObject{});
+ }
+ static auto fillFrom(const QJsonObject& json, QSet<QString>& s)
{
- const auto json = jv.toObject();
- QSet<T> s; s.reserve(json.size());
+ s.reserve(s.size() + json.size());
for (auto it = json.begin(); it != json.end(); ++it)
s.insert(it.key());
return s;
@@ -298,39 +286,44 @@ namespace QMatrixClient
template <typename HashMapT>
struct HashMapFromJson
{
- auto operator()(const QJsonObject& jo) const
+ static void dumpTo(QJsonObject& json, const HashMapT& hashMap)
+ {
+ for (auto it = hashMap.begin(); it != hashMap.end(); ++it)
+ json.insert(it.key(), toJson(it.value()));
+ }
+ static void fillFrom(const QJsonObject& jo, HashMapT& h)
{
- HashMapT h; h.reserve(jo.size());
+ h.reserve(jo.size());
for (auto it = jo.begin(); it != jo.end(); ++it)
h[it.key()] =
fromJson<typename HashMapT::mapped_type>(it.value());
- return h;
- }
- auto operator()(const QJsonValue& jv) const
- {
- return operator()(jv.toObject());
- }
- auto operator()(const QJsonDocument& jd) const
- {
- return operator()(jd.object());
}
};
template <typename T>
- struct FromJson<std::unordered_map<QString, T>>
- : HashMapFromJson<std::unordered_map<QString, T>>
+ struct JsonObjectConverter<std::unordered_map<QString, T>>
+ : public HashMapFromJson<std::unordered_map<QString, T>>
{ };
template <typename T>
- struct FromJson<QHash<QString, T>> : HashMapFromJson<QHash<QString, T>>
+ struct JsonObjectConverter<QHash<QString, T>>
+ : public HashMapFromJson<QHash<QString, T>>
{ };
+ // We could use std::conditional<> below but QT_VERSION* macros in C++ code
+ // cause (kinda valid but useless and noisy) compiler warnings about
+ // bitwise operations on signed integers; so use the preprocessor for now.
+ using variant_map_t =
#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0))
- template <> struct FromJson<QHash<QString, QVariant>>
+ QVariantHash;
+#else
+ QVariantMap;
+#endif
+ template <> struct JsonConverter<variant_map_t>
{
- QHash<QString, QVariant> operator()(const QJsonValue& jv) const;
+ static QJsonObject dump(const variant_map_t& vh);
+ static QVariantHash load(const QJsonValue& jv);
};
-#endif
// Conditional insertion into a QJsonObject
diff --git a/lib/csapi/account-data.cpp b/lib/csapi/account-data.cpp
index 5021c73a..96b32a92 100644
--- a/lib/csapi/account-data.cpp
+++ b/lib/csapi/account-data.cpp
@@ -21,6 +21,20 @@ SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type,
setRequestData(Data(toJson(content)));
}
+QUrl GetAccountDataJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& type)
+{
+ return BaseJob::makeRequestUrl(std::move(baseUrl),
+ basePath % "/user/" % userId % "/account_data/" % type);
+}
+
+static const auto GetAccountDataJobName = QStringLiteral("GetAccountDataJob");
+
+GetAccountDataJob::GetAccountDataJob(const QString& userId, const QString& type)
+ : BaseJob(HttpVerb::Get, GetAccountDataJobName,
+ basePath % "/user/" % userId % "/account_data/" % type)
+{
+}
+
static const auto SetAccountDataPerRoomJobName = QStringLiteral("SetAccountDataPerRoomJob");
SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content)
@@ -30,3 +44,17 @@ SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const
setRequestData(Data(toJson(content)));
}
+QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& type)
+{
+ return BaseJob::makeRequestUrl(std::move(baseUrl),
+ basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type);
+}
+
+static const auto GetAccountDataPerRoomJobName = QStringLiteral("GetAccountDataPerRoomJob");
+
+GetAccountDataPerRoomJob::GetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type)
+ : BaseJob(HttpVerb::Get, GetAccountDataPerRoomJobName,
+ basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type)
+{
+}
+
diff --git a/lib/csapi/account-data.h b/lib/csapi/account-data.h
index f3656a14..b067618f 100644
--- a/lib/csapi/account-data.h
+++ b/lib/csapi/account-data.h
@@ -22,8 +22,8 @@ namespace QMatrixClient
public:
/*! Set some account_data for the user.
* \param userId
- * The id of the user to set account_data for. The access token must be
- * authorized to make requests for this user id.
+ * The ID of the user to set account_data for. The access token must be
+ * authorized to make requests for this user ID.
* \param type
* The event type of the account_data to set. Custom types should be
* namespaced to avoid clashes.
@@ -33,6 +33,33 @@ namespace QMatrixClient
explicit SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content = {});
};
+ /// Get some account_data for the user.
+ ///
+ /// Get some account_data for the client. This config is only visible to the user
+ /// that set the account_data.
+ class GetAccountDataJob : public BaseJob
+ {
+ public:
+ /*! Get some account_data for the user.
+ * \param userId
+ * The ID of the user to get account_data for. The access token must be
+ * authorized to make requests for this user ID.
+ * \param type
+ * The event type of the account_data to get. Custom types should be
+ * namespaced to avoid clashes.
+ */
+ explicit GetAccountDataJob(const QString& userId, const QString& type);
+
+ /*! Construct a URL without creating a full-fledged job object
+ *
+ * This function can be used when a URL for
+ * GetAccountDataJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& type);
+
+ };
+
/// Set some account_data for the user.
///
/// Set some account_data for the client on a given room. This config is only
@@ -43,10 +70,10 @@ namespace QMatrixClient
public:
/*! Set some account_data for the user.
* \param userId
- * The id of the user to set account_data for. The access token must be
- * authorized to make requests for this user id.
+ * The ID of the user to set account_data for. The access token must be
+ * authorized to make requests for this user ID.
* \param roomId
- * The id of the room to set account_data on.
+ * The ID of the room to set account_data on.
* \param type
* The event type of the account_data to set. Custom types should be
* namespaced to avoid clashes.
@@ -55,4 +82,33 @@ namespace QMatrixClient
*/
explicit SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content = {});
};
+
+ /// Get some account_data for the user.
+ ///
+ /// Get some account_data for the client on a given room. This config is only
+ /// visible to the user that set the account_data.
+ class GetAccountDataPerRoomJob : public BaseJob
+ {
+ public:
+ /*! Get some account_data for the user.
+ * \param userId
+ * The ID of the user to set account_data for. The access token must be
+ * authorized to make requests for this user ID.
+ * \param roomId
+ * The ID of the room to get account_data for.
+ * \param type
+ * The event type of the account_data to get. Custom types should be
+ * namespaced to avoid clashes.
+ */
+ explicit GetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type);
+
+ /*! Construct a URL without creating a full-fledged job object
+ *
+ * This function can be used when a URL for
+ * GetAccountDataPerRoomJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& type);
+
+ };
} // namespace QMatrixClient
diff --git a/lib/csapi/admin.cpp b/lib/csapi/admin.cpp
index 6066d4d9..ce06a56d 100644
--- a/lib/csapi/admin.cpp
+++ b/lib/csapi/admin.cpp
@@ -16,43 +16,29 @@ namespace QMatrixClient
{
// Converters
- template <> struct FromJsonObject<GetWhoIsJob::ConnectionInfo>
+ template <> struct JsonObjectConverter<GetWhoIsJob::ConnectionInfo>
{
- GetWhoIsJob::ConnectionInfo operator()(const QJsonObject& jo) const
+ static void fillFrom(const QJsonObject& jo, GetWhoIsJob::ConnectionInfo& result)
{
- GetWhoIsJob::ConnectionInfo result;
- result.ip =
- fromJson<QString>(jo.value("ip"_ls));
- result.lastSeen =
- fromJson<qint64>(jo.value("last_seen"_ls));
- result.userAgent =
- fromJson<QString>(jo.value("user_agent"_ls));
-
- return result;
+ fromJson(jo.value("ip"_ls), result.ip);
+ fromJson(jo.value("last_seen"_ls), result.lastSeen);
+ fromJson(jo.value("user_agent"_ls), result.userAgent);
}
};
- template <> struct FromJsonObject<GetWhoIsJob::SessionInfo>
+ template <> struct JsonObjectConverter<GetWhoIsJob::SessionInfo>
{
- GetWhoIsJob::SessionInfo operator()(const QJsonObject& jo) const
+ static void fillFrom(const QJsonObject& jo, GetWhoIsJob::SessionInfo& result)
{
- GetWhoIsJob::SessionInfo result;
- result.connections =
- fromJson<QVector<GetWhoIsJob::ConnectionInfo>>(jo.value("connections"_ls));
-
- return result;
+ fromJson(jo.value("connections"_ls), result.connections);
}
};
- template <> struct FromJsonObject<GetWhoIsJob::DeviceInfo>
+ template <> struct JsonObjectConverter<GetWhoIsJob::DeviceInfo>
{
- GetWhoIsJob::DeviceInfo operator()(const QJsonObject& jo) const
+ static void fillFrom(const QJsonObject& jo, GetWhoIsJob::DeviceInfo& result)
{
- GetWhoIsJob::DeviceInfo result;
- result.sessions =
- fromJson<QVector<GetWhoIsJob::SessionInfo>>(jo.value("sessions"_ls));
-
- return result;
+ fromJson(jo.value("sessions"_ls), result.sessions);
}
};
} // namespace QMatrixClient
@@ -94,8 +80,8 @@ const QHash<QString, GetWhoIsJob::DeviceInfo>& GetWhoIsJob::devices() const
BaseJob::Status GetWhoIsJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->userId = fromJson<QString>(json.value("user_id"_ls));
- d->devices = fromJson<QHash<QString, DeviceInfo>>(json.value("devices"_ls));
+ fromJson(json.value("user_id"_ls), d->userId);
+ fromJson(json.value("devices"_ls), d->devices);
return Success;
}
diff --git a/lib/csapi/administrative_contact.cpp b/lib/csapi/administrative_contact.cpp
index f62002a6..11385dff 100644
--- a/lib/csapi/administrative_contact.cpp
+++ b/lib/csapi/administrative_contact.cpp
@@ -16,21 +16,14 @@ namespace QMatrixClient
{
// Converters
- template <> struct FromJsonObject<GetAccount3PIDsJob::ThirdPartyIdentifier>
+ template <> struct JsonObjectConverter<GetAccount3PIDsJob::ThirdPartyIdentifier>
{
- GetAccount3PIDsJob::ThirdPartyIdentifier operator()(const QJsonObject& jo) const
+ static void fillFrom(const QJsonObject& jo, GetAccount3PIDsJob::ThirdPartyIdentifier& result)
{
- GetAccount3PIDsJob::ThirdPartyIdentifier result;
- result.medium =
- fromJson<QString>(jo.value("medium"_ls));
- result.address =
- fromJson<QString>(jo.value("address"_ls));
- result.validatedAt =
- fromJson<qint64>(jo.value("validated_at"_ls));
- result.addedAt =
- fromJson<qint64>(jo.value("added_at"_ls));
-
- return result;
+ fromJson(jo.value("medium"_ls), result.medium);
+ fromJson(jo.value("address"_ls), result.address);
+ fromJson(jo.value("validated_at"_ls), result.validatedAt);
+ fromJson(jo.value("added_at"_ls), result.addedAt);
}
};
} // namespace QMatrixClient
@@ -66,7 +59,7 @@ const QVector<GetAccount3PIDsJob::ThirdPartyIdentifier>& GetAccount3PIDsJob::thr
BaseJob::Status GetAccount3PIDsJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->threepids = fromJson<QVector<ThirdPartyIdentifier>>(json.value("threepids"_ls));
+ fromJson(json.value("threepids"_ls), d->threepids);
return Success;
}
@@ -74,19 +67,20 @@ namespace QMatrixClient
{
// Converters
- QJsonObject toJson(const Post3PIDsJob::ThreePidCredentials& pod)
+ template <> struct JsonObjectConverter<Post3PIDsJob::ThreePidCredentials>
{
- QJsonObject jo;
- addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret);
- addParam<>(jo, QStringLiteral("id_server"), pod.idServer);
- addParam<>(jo, QStringLiteral("sid"), pod.sid);
- return jo;
- }
+ static void dumpTo(QJsonObject& jo, const Post3PIDsJob::ThreePidCredentials& pod)
+ {
+ addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret);
+ addParam<>(jo, QStringLiteral("id_server"), pod.idServer);
+ addParam<>(jo, QStringLiteral("sid"), pod.sid);
+ }
+ };
} // namespace QMatrixClient
static const auto Post3PIDsJobName = QStringLiteral("Post3PIDsJob");
-Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds, bool bind)
+Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds, Omittable<bool> bind)
: BaseJob(HttpVerb::Post, Post3PIDsJobName,
basePath % "/account/3pid")
{
@@ -139,7 +133,7 @@ const Sid& RequestTokenTo3PIDEmailJob::data() const
BaseJob::Status RequestTokenTo3PIDEmailJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<Sid>(data);
+ fromJson(data, d->data);
return Success;
}
@@ -175,7 +169,7 @@ const Sid& RequestTokenTo3PIDMSISDNJob::data() const
BaseJob::Status RequestTokenTo3PIDMSISDNJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<Sid>(data);
+ fromJson(data, d->data);
return Success;
}
diff --git a/lib/csapi/administrative_contact.h b/lib/csapi/administrative_contact.h
index 3fb3d44c..02aeee4d 100644
--- a/lib/csapi/administrative_contact.h
+++ b/lib/csapi/administrative_contact.h
@@ -113,7 +113,7 @@ namespace QMatrixClient
* identifier to the account's Matrix ID with the passed identity
* server. Default: ``false``.
*/
- explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds, bool bind = false);
+ explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds, Omittable<bool> bind = none);
};
/// Deletes a third party identifier from the user's account
diff --git a/lib/csapi/capabilities.cpp b/lib/csapi/capabilities.cpp
new file mode 100644
index 00000000..210423f5
--- /dev/null
+++ b/lib/csapi/capabilities.cpp
@@ -0,0 +1,84 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "capabilities.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+namespace QMatrixClient
+{
+ // Converters
+
+ template <> struct JsonObjectConverter<GetCapabilitiesJob::ChangePasswordCapability>
+ {
+ static void fillFrom(const QJsonObject& jo, GetCapabilitiesJob::ChangePasswordCapability& result)
+ {
+ fromJson(jo.value("enabled"_ls), result.enabled);
+ }
+ };
+
+ template <> struct JsonObjectConverter<GetCapabilitiesJob::RoomVersionsCapability>
+ {
+ static void fillFrom(const QJsonObject& jo, GetCapabilitiesJob::RoomVersionsCapability& result)
+ {
+ fromJson(jo.value("default"_ls), result.defaultVersion);
+ fromJson(jo.value("available"_ls), result.available);
+ }
+ };
+
+ template <> struct JsonObjectConverter<GetCapabilitiesJob::Capabilities>
+ {
+ static void fillFrom(QJsonObject jo, GetCapabilitiesJob::Capabilities& result)
+ {
+ fromJson(jo.take("m.change_password"_ls), result.changePassword);
+ fromJson(jo.take("m.room_versions"_ls), result.roomVersions);
+ fromJson(jo, result.additionalProperties);
+ }
+ };
+} // namespace QMatrixClient
+
+class GetCapabilitiesJob::Private
+{
+ public:
+ Capabilities capabilities;
+};
+
+QUrl GetCapabilitiesJob::makeRequestUrl(QUrl baseUrl)
+{
+ return BaseJob::makeRequestUrl(std::move(baseUrl),
+ basePath % "/capabilities");
+}
+
+static const auto GetCapabilitiesJobName = QStringLiteral("GetCapabilitiesJob");
+
+GetCapabilitiesJob::GetCapabilitiesJob()
+ : BaseJob(HttpVerb::Get, GetCapabilitiesJobName,
+ basePath % "/capabilities")
+ , d(new Private)
+{
+}
+
+GetCapabilitiesJob::~GetCapabilitiesJob() = default;
+
+const GetCapabilitiesJob::Capabilities& GetCapabilitiesJob::capabilities() const
+{
+ return d->capabilities;
+}
+
+BaseJob::Status GetCapabilitiesJob::parseJson(const QJsonDocument& data)
+{
+ auto json = data.object();
+ if (!json.contains("capabilities"_ls))
+ return { JsonParseError,
+ "The key 'capabilities' not found in the response" };
+ fromJson(json.value("capabilities"_ls), d->capabilities);
+ return Success;
+}
+
diff --git a/lib/csapi/capabilities.h b/lib/csapi/capabilities.h
new file mode 100644
index 00000000..06a8bf0d
--- /dev/null
+++ b/lib/csapi/capabilities.h
@@ -0,0 +1,82 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "jobs/basejob.h"
+
+#include <QtCore/QJsonObject>
+#include "converters.h"
+#include <QtCore/QHash>
+
+namespace QMatrixClient
+{
+ // Operations
+
+ /// Gets information about the server's capabilities.
+ ///
+ /// Gets information about the server's supported feature set
+ /// and other relevant capabilities.
+ class GetCapabilitiesJob : public BaseJob
+ {
+ public:
+ // Inner data structures
+
+ /// Capability to indicate if the user can change their password.
+ struct ChangePasswordCapability
+ {
+ /// True if the user can change their password, false otherwise.
+ bool enabled;
+ };
+
+ /// The room versions the server supports.
+ struct RoomVersionsCapability
+ {
+ /// The default room version the server is using for new rooms.
+ QString defaultVersion;
+ /// A detailed description of the room versions the server supports.
+ QHash<QString, QString> available;
+ };
+
+ /// The custom capabilities the server supports, using the
+ /// Java package naming convention.
+ struct Capabilities
+ {
+ /// Capability to indicate if the user can change their password.
+ Omittable<ChangePasswordCapability> changePassword;
+ /// The room versions the server supports.
+ Omittable<RoomVersionsCapability> roomVersions;
+ /// The custom capabilities the server supports, using the
+ /// Java package naming convention.
+ QHash<QString, QJsonObject> additionalProperties;
+ };
+
+ // Construction/destruction
+
+ explicit GetCapabilitiesJob();
+
+ /*! Construct a URL without creating a full-fledged job object
+ *
+ * This function can be used when a URL for
+ * GetCapabilitiesJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl);
+
+ ~GetCapabilitiesJob() override;
+
+ // Result properties
+
+ /// The custom capabilities the server supports, using the
+ /// Java package naming convention.
+ const Capabilities& capabilities() const;
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+} // namespace QMatrixClient
diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp
index 9b590e42..22223985 100644
--- a/lib/csapi/content-repo.cpp
+++ b/lib/csapi/content-repo.cpp
@@ -52,7 +52,7 @@ BaseJob::Status UploadContentJob::parseJson(const QJsonDocument& data)
if (!json.contains("content_uri"_ls))
return { JsonParseError,
"The key 'content_uri' not found in the response" };
- d->contentUri = fromJson<QString>(json.value("content_uri"_ls));
+ fromJson(json.value("content_uri"_ls), d->contentUri);
return Success;
}
@@ -276,8 +276,8 @@ const QString& GetUrlPreviewJob::ogImage() const
BaseJob::Status GetUrlPreviewJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->matrixImageSize = fromJson<qint64>(json.value("matrix:image:size"_ls));
- d->ogImage = fromJson<QString>(json.value("og:image"_ls));
+ fromJson(json.value("matrix:image:size"_ls), d->matrixImageSize);
+ fromJson(json.value("og:image"_ls), d->ogImage);
return Success;
}
@@ -312,7 +312,7 @@ Omittable<qint64> GetConfigJob::uploadSize() const
BaseJob::Status GetConfigJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->uploadSize = fromJson<qint64>(json.value("m.upload.size"_ls));
+ fromJson(json.value("m.upload.size"_ls), d->uploadSize);
return Success;
}
diff --git a/lib/csapi/create_room.cpp b/lib/csapi/create_room.cpp
index 36f83727..448547ae 100644
--- a/lib/csapi/create_room.cpp
+++ b/lib/csapi/create_room.cpp
@@ -16,23 +16,25 @@ namespace QMatrixClient
{
// Converters
- QJsonObject toJson(const CreateRoomJob::Invite3pid& pod)
+ template <> struct JsonObjectConverter<CreateRoomJob::Invite3pid>
{
- QJsonObject jo;
- addParam<>(jo, QStringLiteral("id_server"), pod.idServer);
- addParam<>(jo, QStringLiteral("medium"), pod.medium);
- addParam<>(jo, QStringLiteral("address"), pod.address);
- return jo;
- }
+ static void dumpTo(QJsonObject& jo, const CreateRoomJob::Invite3pid& pod)
+ {
+ addParam<>(jo, QStringLiteral("id_server"), pod.idServer);
+ addParam<>(jo, QStringLiteral("medium"), pod.medium);
+ addParam<>(jo, QStringLiteral("address"), pod.address);
+ }
+ };
- QJsonObject toJson(const CreateRoomJob::StateEvent& pod)
+ template <> struct JsonObjectConverter<CreateRoomJob::StateEvent>
{
- QJsonObject jo;
- addParam<>(jo, QStringLiteral("type"), pod.type);
- addParam<IfNotEmpty>(jo, QStringLiteral("state_key"), pod.stateKey);
- addParam<>(jo, QStringLiteral("content"), pod.content);
- return jo;
- }
+ static void dumpTo(QJsonObject& jo, const CreateRoomJob::StateEvent& pod)
+ {
+ addParam<>(jo, QStringLiteral("type"), pod.type);
+ addParam<IfNotEmpty>(jo, QStringLiteral("state_key"), pod.stateKey);
+ addParam<>(jo, QStringLiteral("content"), pod.content);
+ }
+ };
} // namespace QMatrixClient
class CreateRoomJob::Private
@@ -43,7 +45,7 @@ class CreateRoomJob::Private
static const auto CreateRoomJobName = QStringLiteral("CreateRoomJob");
-CreateRoomJob::CreateRoomJob(const QString& visibility, const QString& roomAliasName, const QString& name, const QString& topic, const QStringList& invite, const QVector<Invite3pid>& invite3pid, const QString& roomVersion, const QJsonObject& creationContent, const QVector<StateEvent>& initialState, const QString& preset, bool isDirect, const QJsonObject& powerLevelContentOverride)
+CreateRoomJob::CreateRoomJob(const QString& visibility, const QString& roomAliasName, const QString& name, const QString& topic, const QStringList& invite, const QVector<Invite3pid>& invite3pid, const QString& roomVersion, const QJsonObject& creationContent, const QVector<StateEvent>& initialState, const QString& preset, Omittable<bool> isDirect, const QJsonObject& powerLevelContentOverride)
: BaseJob(HttpVerb::Post, CreateRoomJobName,
basePath % "/createRoom")
, d(new Private)
@@ -77,7 +79,7 @@ BaseJob::Status CreateRoomJob::parseJson(const QJsonDocument& data)
if (!json.contains("room_id"_ls))
return { JsonParseError,
"The key 'room_id' not found in the response" };
- d->roomId = fromJson<QString>(json.value("room_id"_ls));
+ fromJson(json.value("room_id"_ls), d->roomId);
return Success;
}
diff --git a/lib/csapi/create_room.h b/lib/csapi/create_room.h
index a0a64df0..d7c01d00 100644
--- a/lib/csapi/create_room.h
+++ b/lib/csapi/create_room.h
@@ -216,7 +216,7 @@ namespace QMatrixClient
* event content prior to it being sent to the room. Defaults to
* overriding nothing.
*/
- explicit CreateRoomJob(const QString& visibility = {}, const QString& roomAliasName = {}, const QString& name = {}, const QString& topic = {}, const QStringList& invite = {}, const QVector<Invite3pid>& invite3pid = {}, const QString& roomVersion = {}, const QJsonObject& creationContent = {}, const QVector<StateEvent>& initialState = {}, const QString& preset = {}, bool isDirect = false, const QJsonObject& powerLevelContentOverride = {});
+ explicit CreateRoomJob(const QString& visibility = {}, const QString& roomAliasName = {}, const QString& name = {}, const QString& topic = {}, const QStringList& invite = {}, const QVector<Invite3pid>& invite3pid = {}, const QString& roomVersion = {}, const QJsonObject& creationContent = {}, const QVector<StateEvent>& initialState = {}, const QString& preset = {}, Omittable<bool> isDirect = none, const QJsonObject& powerLevelContentOverride = {});
~CreateRoomJob() override;
// Result properties
diff --git a/lib/csapi/definitions/auth_data.cpp b/lib/csapi/definitions/auth_data.cpp
index f8639432..006b8c7e 100644
--- a/lib/csapi/definitions/auth_data.cpp
+++ b/lib/csapi/definitions/auth_data.cpp
@@ -6,23 +6,20 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const AuthenticationData& pod)
+void JsonObjectConverter<AuthenticationData>::dumpTo(
+ QJsonObject& jo, const AuthenticationData& pod)
{
- QJsonObject jo = toJson(pod.authInfo);
+ fillJson(jo, pod.authInfo);
addParam<>(jo, QStringLiteral("type"), pod.type);
addParam<IfNotEmpty>(jo, QStringLiteral("session"), pod.session);
- return jo;
}
-AuthenticationData FromJsonObject<AuthenticationData>::operator()(QJsonObject jo) const
+void JsonObjectConverter<AuthenticationData>::fillFrom(
+ QJsonObject jo, AuthenticationData& result)
{
- AuthenticationData result;
- result.type =
- fromJson<QString>(jo.take("type"_ls));
- result.session =
- fromJson<QString>(jo.take("session"_ls));
+ fromJson(jo.take("type"_ls), result.type);
+ fromJson(jo.take("session"_ls), result.session);
- result.authInfo = fromJson<QHash<QString, QJsonObject>>(jo);
- return result;
+ fromJson(jo, result.authInfo);
}
diff --git a/lib/csapi/definitions/auth_data.h b/lib/csapi/definitions/auth_data.h
index 661d3e5f..26eb205c 100644
--- a/lib/csapi/definitions/auth_data.h
+++ b/lib/csapi/definitions/auth_data.h
@@ -23,12 +23,10 @@ namespace QMatrixClient
/// Keys dependent on the login type
QHash<QString, QJsonObject> authInfo;
};
-
- QJsonObject toJson(const AuthenticationData& pod);
-
- template <> struct FromJsonObject<AuthenticationData>
+ template <> struct JsonObjectConverter<AuthenticationData>
{
- AuthenticationData operator()(QJsonObject jo) const;
+ static void dumpTo(QJsonObject& jo, const AuthenticationData& pod);
+ static void fillFrom(QJsonObject jo, AuthenticationData& pod);
};
} // namespace QMatrixClient
diff --git a/lib/csapi/definitions/client_device.cpp b/lib/csapi/definitions/client_device.cpp
index 4a192f85..752b806a 100644
--- a/lib/csapi/definitions/client_device.cpp
+++ b/lib/csapi/definitions/client_device.cpp
@@ -6,28 +6,21 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const Device& pod)
+void JsonObjectConverter<Device>::dumpTo(
+ QJsonObject& jo, const Device& pod)
{
- QJsonObject jo;
addParam<>(jo, QStringLiteral("device_id"), pod.deviceId);
addParam<IfNotEmpty>(jo, QStringLiteral("display_name"), pod.displayName);
addParam<IfNotEmpty>(jo, QStringLiteral("last_seen_ip"), pod.lastSeenIp);
addParam<IfNotEmpty>(jo, QStringLiteral("last_seen_ts"), pod.lastSeenTs);
- return jo;
}
-Device FromJsonObject<Device>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<Device>::fillFrom(
+ const QJsonObject& jo, Device& result)
{
- Device result;
- result.deviceId =
- fromJson<QString>(jo.value("device_id"_ls));
- result.displayName =
- fromJson<QString>(jo.value("display_name"_ls));
- result.lastSeenIp =
- fromJson<QString>(jo.value("last_seen_ip"_ls));
- result.lastSeenTs =
- fromJson<qint64>(jo.value("last_seen_ts"_ls));
-
- return result;
+ fromJson(jo.value("device_id"_ls), result.deviceId);
+ fromJson(jo.value("display_name"_ls), result.displayName);
+ fromJson(jo.value("last_seen_ip"_ls), result.lastSeenIp);
+ fromJson(jo.value("last_seen_ts"_ls), result.lastSeenTs);
}
diff --git a/lib/csapi/definitions/client_device.h b/lib/csapi/definitions/client_device.h
index 9f10888a..a6224f71 100644
--- a/lib/csapi/definitions/client_device.h
+++ b/lib/csapi/definitions/client_device.h
@@ -28,12 +28,10 @@ namespace QMatrixClient
/// reasons).
Omittable<qint64> lastSeenTs;
};
-
- QJsonObject toJson(const Device& pod);
-
- template <> struct FromJsonObject<Device>
+ template <> struct JsonObjectConverter<Device>
{
- Device operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const Device& pod);
+ static void fillFrom(const QJsonObject& jo, Device& pod);
};
} // namespace QMatrixClient
diff --git a/lib/csapi/definitions/device_keys.cpp b/lib/csapi/definitions/device_keys.cpp
index a0e0ca42..1e79499f 100644
--- a/lib/csapi/definitions/device_keys.cpp
+++ b/lib/csapi/definitions/device_keys.cpp
@@ -6,31 +6,23 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const DeviceKeys& pod)
+void JsonObjectConverter<DeviceKeys>::dumpTo(
+ QJsonObject& jo, const DeviceKeys& pod)
{
- QJsonObject jo;
addParam<>(jo, QStringLiteral("user_id"), pod.userId);
addParam<>(jo, QStringLiteral("device_id"), pod.deviceId);
addParam<>(jo, QStringLiteral("algorithms"), pod.algorithms);
addParam<>(jo, QStringLiteral("keys"), pod.keys);
addParam<>(jo, QStringLiteral("signatures"), pod.signatures);
- return jo;
}
-DeviceKeys FromJsonObject<DeviceKeys>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<DeviceKeys>::fillFrom(
+ const QJsonObject& jo, DeviceKeys& result)
{
- DeviceKeys result;
- result.userId =
- fromJson<QString>(jo.value("user_id"_ls));
- result.deviceId =
- fromJson<QString>(jo.value("device_id"_ls));
- result.algorithms =
- fromJson<QStringList>(jo.value("algorithms"_ls));
- result.keys =
- fromJson<QHash<QString, QString>>(jo.value("keys"_ls));
- result.signatures =
- fromJson<QHash<QString, QHash<QString, QString>>>(jo.value("signatures"_ls));
-
- return result;
+ fromJson(jo.value("user_id"_ls), result.userId);
+ fromJson(jo.value("device_id"_ls), result.deviceId);
+ fromJson(jo.value("algorithms"_ls), result.algorithms);
+ fromJson(jo.value("keys"_ls), result.keys);
+ fromJson(jo.value("signatures"_ls), result.signatures);
}
diff --git a/lib/csapi/definitions/device_keys.h b/lib/csapi/definitions/device_keys.h
index 6023e7e8..8ebe1125 100644
--- a/lib/csapi/definitions/device_keys.h
+++ b/lib/csapi/definitions/device_keys.h
@@ -34,12 +34,10 @@ namespace QMatrixClient
/// JSON`_.
QHash<QString, QHash<QString, QString>> signatures;
};
-
- QJsonObject toJson(const DeviceKeys& pod);
-
- template <> struct FromJsonObject<DeviceKeys>
+ template <> struct JsonObjectConverter<DeviceKeys>
{
- DeviceKeys operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const DeviceKeys& pod);
+ static void fillFrom(const QJsonObject& jo, DeviceKeys& pod);
};
} // namespace QMatrixClient
diff --git a/lib/csapi/definitions/event_filter.cpp b/lib/csapi/definitions/event_filter.cpp
index cc444db0..b20d7807 100644
--- a/lib/csapi/definitions/event_filter.cpp
+++ b/lib/csapi/definitions/event_filter.cpp
@@ -6,31 +6,23 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const EventFilter& pod)
+void JsonObjectConverter<EventFilter>::dumpTo(
+ QJsonObject& jo, const EventFilter& pod)
{
- QJsonObject jo;
addParam<IfNotEmpty>(jo, QStringLiteral("limit"), pod.limit);
addParam<IfNotEmpty>(jo, QStringLiteral("not_senders"), pod.notSenders);
addParam<IfNotEmpty>(jo, QStringLiteral("not_types"), pod.notTypes);
addParam<IfNotEmpty>(jo, QStringLiteral("senders"), pod.senders);
addParam<IfNotEmpty>(jo, QStringLiteral("types"), pod.types);
- return jo;
}
-EventFilter FromJsonObject<EventFilter>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<EventFilter>::fillFrom(
+ const QJsonObject& jo, EventFilter& result)
{
- EventFilter result;
- result.limit =
- fromJson<int>(jo.value("limit"_ls));
- result.notSenders =
- fromJson<QStringList>(jo.value("not_senders"_ls));
- result.notTypes =
- fromJson<QStringList>(jo.value("not_types"_ls));
- result.senders =
- fromJson<QStringList>(jo.value("senders"_ls));
- result.types =
- fromJson<QStringList>(jo.value("types"_ls));
-
- return result;
+ fromJson(jo.value("limit"_ls), result.limit);
+ fromJson(jo.value("not_senders"_ls), result.notSenders);
+ fromJson(jo.value("not_types"_ls), result.notTypes);
+ fromJson(jo.value("senders"_ls), result.senders);
+ fromJson(jo.value("types"_ls), result.types);
}
diff --git a/lib/csapi/definitions/event_filter.h b/lib/csapi/definitions/event_filter.h
index 5c6a5b27..6de1fe79 100644
--- a/lib/csapi/definitions/event_filter.h
+++ b/lib/csapi/definitions/event_filter.h
@@ -25,12 +25,10 @@ namespace QMatrixClient
/// A list of event types to include. If this list is absent then all event types are included. A ``'*'`` can be used as a wildcard to match any sequence of characters.
QStringList types;
};
-
- QJsonObject toJson(const EventFilter& pod);
-
- template <> struct FromJsonObject<EventFilter>
+ template <> struct JsonObjectConverter<EventFilter>
{
- EventFilter operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const EventFilter& pod);
+ static void fillFrom(const QJsonObject& jo, EventFilter& pod);
};
} // namespace QMatrixClient
diff --git a/lib/csapi/definitions/public_rooms_response.cpp b/lib/csapi/definitions/public_rooms_response.cpp
index 2f52501d..0d26662c 100644
--- a/lib/csapi/definitions/public_rooms_response.cpp
+++ b/lib/csapi/definitions/public_rooms_response.cpp
@@ -6,9 +6,9 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const PublicRoomsChunk& pod)
+void JsonObjectConverter<PublicRoomsChunk>::dumpTo(
+ QJsonObject& jo, const PublicRoomsChunk& pod)
{
- QJsonObject jo;
addParam<IfNotEmpty>(jo, QStringLiteral("aliases"), pod.aliases);
addParam<IfNotEmpty>(jo, QStringLiteral("canonical_alias"), pod.canonicalAlias);
addParam<IfNotEmpty>(jo, QStringLiteral("name"), pod.name);
@@ -18,56 +18,37 @@ QJsonObject QMatrixClient::toJson(const PublicRoomsChunk& pod)
addParam<>(jo, QStringLiteral("world_readable"), pod.worldReadable);
addParam<>(jo, QStringLiteral("guest_can_join"), pod.guestCanJoin);
addParam<IfNotEmpty>(jo, QStringLiteral("avatar_url"), pod.avatarUrl);
- return jo;
}
-PublicRoomsChunk FromJsonObject<PublicRoomsChunk>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<PublicRoomsChunk>::fillFrom(
+ const QJsonObject& jo, PublicRoomsChunk& result)
{
- PublicRoomsChunk result;
- result.aliases =
- fromJson<QStringList>(jo.value("aliases"_ls));
- result.canonicalAlias =
- fromJson<QString>(jo.value("canonical_alias"_ls));
- result.name =
- fromJson<QString>(jo.value("name"_ls));
- result.numJoinedMembers =
- fromJson<int>(jo.value("num_joined_members"_ls));
- result.roomId =
- fromJson<QString>(jo.value("room_id"_ls));
- result.topic =
- fromJson<QString>(jo.value("topic"_ls));
- result.worldReadable =
- fromJson<bool>(jo.value("world_readable"_ls));
- result.guestCanJoin =
- fromJson<bool>(jo.value("guest_can_join"_ls));
- result.avatarUrl =
- fromJson<QString>(jo.value("avatar_url"_ls));
-
- return result;
+ fromJson(jo.value("aliases"_ls), result.aliases);
+ fromJson(jo.value("canonical_alias"_ls), result.canonicalAlias);
+ fromJson(jo.value("name"_ls), result.name);
+ fromJson(jo.value("num_joined_members"_ls), result.numJoinedMembers);
+ fromJson(jo.value("room_id"_ls), result.roomId);
+ fromJson(jo.value("topic"_ls), result.topic);
+ fromJson(jo.value("world_readable"_ls), result.worldReadable);
+ fromJson(jo.value("guest_can_join"_ls), result.guestCanJoin);
+ fromJson(jo.value("avatar_url"_ls), result.avatarUrl);
}
-QJsonObject QMatrixClient::toJson(const PublicRoomsResponse& pod)
+void JsonObjectConverter<PublicRoomsResponse>::dumpTo(
+ QJsonObject& jo, const PublicRoomsResponse& pod)
{
- QJsonObject jo;
addParam<>(jo, QStringLiteral("chunk"), pod.chunk);
addParam<IfNotEmpty>(jo, QStringLiteral("next_batch"), pod.nextBatch);
addParam<IfNotEmpty>(jo, QStringLiteral("prev_batch"), pod.prevBatch);
addParam<IfNotEmpty>(jo, QStringLiteral("total_room_count_estimate"), pod.totalRoomCountEstimate);
- return jo;
}
-PublicRoomsResponse FromJsonObject<PublicRoomsResponse>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<PublicRoomsResponse>::fillFrom(
+ const QJsonObject& jo, PublicRoomsResponse& result)
{
- PublicRoomsResponse result;
- result.chunk =
- fromJson<QVector<PublicRoomsChunk>>(jo.value("chunk"_ls));
- result.nextBatch =
- fromJson<QString>(jo.value("next_batch"_ls));
- result.prevBatch =
- fromJson<QString>(jo.value("prev_batch"_ls));
- result.totalRoomCountEstimate =
- fromJson<int>(jo.value("total_room_count_estimate"_ls));
-
- return result;
+ fromJson(jo.value("chunk"_ls), result.chunk);
+ fromJson(jo.value("next_batch"_ls), result.nextBatch);
+ fromJson(jo.value("prev_batch"_ls), result.prevBatch);
+ fromJson(jo.value("total_room_count_estimate"_ls), result.totalRoomCountEstimate);
}
diff --git a/lib/csapi/definitions/public_rooms_response.h b/lib/csapi/definitions/public_rooms_response.h
index 88c805ba..4c54ac25 100644
--- a/lib/csapi/definitions/public_rooms_response.h
+++ b/lib/csapi/definitions/public_rooms_response.h
@@ -36,12 +36,10 @@ namespace QMatrixClient
/// The URL for the room's avatar, if one is set.
QString avatarUrl;
};
-
- QJsonObject toJson(const PublicRoomsChunk& pod);
-
- template <> struct FromJsonObject<PublicRoomsChunk>
+ template <> struct JsonObjectConverter<PublicRoomsChunk>
{
- PublicRoomsChunk operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const PublicRoomsChunk& pod);
+ static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod);
};
/// A list of the rooms on the server.
@@ -61,12 +59,10 @@ namespace QMatrixClient
/// server has an estimate.
Omittable<int> totalRoomCountEstimate;
};
-
- QJsonObject toJson(const PublicRoomsResponse& pod);
-
- template <> struct FromJsonObject<PublicRoomsResponse>
+ template <> struct JsonObjectConverter<PublicRoomsResponse>
{
- PublicRoomsResponse operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const PublicRoomsResponse& pod);
+ static void fillFrom(const QJsonObject& jo, PublicRoomsResponse& pod);
};
} // namespace QMatrixClient
diff --git a/lib/csapi/definitions/push_condition.cpp b/lib/csapi/definitions/push_condition.cpp
index 045094bc..ace02755 100644
--- a/lib/csapi/definitions/push_condition.cpp
+++ b/lib/csapi/definitions/push_condition.cpp
@@ -6,28 +6,21 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const PushCondition& pod)
+void JsonObjectConverter<PushCondition>::dumpTo(
+ QJsonObject& jo, const PushCondition& pod)
{
- QJsonObject jo;
addParam<>(jo, QStringLiteral("kind"), pod.kind);
addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key);
addParam<IfNotEmpty>(jo, QStringLiteral("pattern"), pod.pattern);
addParam<IfNotEmpty>(jo, QStringLiteral("is"), pod.is);
- return jo;
}
-PushCondition FromJsonObject<PushCondition>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<PushCondition>::fillFrom(
+ const QJsonObject& jo, PushCondition& result)
{
- PushCondition result;
- result.kind =
- fromJson<QString>(jo.value("kind"_ls));
- result.key =
- fromJson<QString>(jo.value("key"_ls));
- result.pattern =
- fromJson<QString>(jo.value("pattern"_ls));
- result.is =
- fromJson<QString>(jo.value("is"_ls));
-
- return result;
+ fromJson(jo.value("kind"_ls), result.kind);
+ fromJson(jo.value("key"_ls), result.key);
+ fromJson(jo.value("pattern"_ls), result.pattern);
+ fromJson(jo.value("is"_ls), result.is);
}
diff --git a/lib/csapi/definitions/push_condition.h b/lib/csapi/definitions/push_condition.h
index defcebb3..e45526d2 100644
--- a/lib/csapi/definitions/push_condition.h
+++ b/lib/csapi/definitions/push_condition.h
@@ -28,12 +28,10 @@ namespace QMatrixClient
/// so forth. If no prefix is present, this parameter defaults to ==.
QString is;
};
-
- QJsonObject toJson(const PushCondition& pod);
-
- template <> struct FromJsonObject<PushCondition>
+ template <> struct JsonObjectConverter<PushCondition>
{
- PushCondition operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const PushCondition& pod);
+ static void fillFrom(const QJsonObject& jo, PushCondition& pod);
};
} // namespace QMatrixClient
diff --git a/lib/csapi/definitions/push_rule.cpp b/lib/csapi/definitions/push_rule.cpp
index baddd187..abbb04b5 100644
--- a/lib/csapi/definitions/push_rule.cpp
+++ b/lib/csapi/definitions/push_rule.cpp
@@ -6,34 +6,25 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const PushRule& pod)
+void JsonObjectConverter<PushRule>::dumpTo(
+ QJsonObject& jo, const PushRule& pod)
{
- QJsonObject jo;
addParam<>(jo, QStringLiteral("actions"), pod.actions);
addParam<>(jo, QStringLiteral("default"), pod.isDefault);
addParam<>(jo, QStringLiteral("enabled"), pod.enabled);
addParam<>(jo, QStringLiteral("rule_id"), pod.ruleId);
addParam<IfNotEmpty>(jo, QStringLiteral("conditions"), pod.conditions);
addParam<IfNotEmpty>(jo, QStringLiteral("pattern"), pod.pattern);
- return jo;
}
-PushRule FromJsonObject<PushRule>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<PushRule>::fillFrom(
+ const QJsonObject& jo, PushRule& result)
{
- PushRule result;
- result.actions =
- fromJson<QVector<QVariant>>(jo.value("actions"_ls));
- result.isDefault =
- fromJson<bool>(jo.value("default"_ls));
- result.enabled =
- fromJson<bool>(jo.value("enabled"_ls));
- result.ruleId =
- fromJson<QString>(jo.value("rule_id"_ls));
- result.conditions =
- fromJson<QVector<PushCondition>>(jo.value("conditions"_ls));
- result.pattern =
- fromJson<QString>(jo.value("pattern"_ls));
-
- return result;
+ fromJson(jo.value("actions"_ls), result.actions);
+ fromJson(jo.value("default"_ls), result.isDefault);
+ fromJson(jo.value("enabled"_ls), result.enabled);
+ fromJson(jo.value("rule_id"_ls), result.ruleId);
+ fromJson(jo.value("conditions"_ls), result.conditions);
+ fromJson(jo.value("pattern"_ls), result.pattern);
}
diff --git a/lib/csapi/definitions/push_rule.h b/lib/csapi/definitions/push_rule.h
index 5f52876d..bea13e96 100644
--- a/lib/csapi/definitions/push_rule.h
+++ b/lib/csapi/definitions/push_rule.h
@@ -7,10 +7,10 @@
#include "converters.h"
#include "csapi/definitions/push_condition.h"
-#include "converters.h"
+#include <QtCore/QJsonObject>
#include <QtCore/QVector>
#include <QtCore/QVariant>
-#include <QtCore/QJsonObject>
+#include "converters.h"
namespace QMatrixClient
{
@@ -34,12 +34,10 @@ namespace QMatrixClient
/// rules.
QString pattern;
};
-
- QJsonObject toJson(const PushRule& pod);
-
- template <> struct FromJsonObject<PushRule>
+ template <> struct JsonObjectConverter<PushRule>
{
- PushRule operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const PushRule& pod);
+ static void fillFrom(const QJsonObject& jo, PushRule& pod);
};
} // namespace QMatrixClient
diff --git a/lib/csapi/definitions/push_ruleset.cpp b/lib/csapi/definitions/push_ruleset.cpp
index 14b7a4b6..f1bad882 100644
--- a/lib/csapi/definitions/push_ruleset.cpp
+++ b/lib/csapi/definitions/push_ruleset.cpp
@@ -6,31 +6,23 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const PushRuleset& pod)
+void JsonObjectConverter<PushRuleset>::dumpTo(
+ QJsonObject& jo, const PushRuleset& pod)
{
- QJsonObject jo;
addParam<IfNotEmpty>(jo, QStringLiteral("content"), pod.content);
addParam<IfNotEmpty>(jo, QStringLiteral("override"), pod.override);
addParam<IfNotEmpty>(jo, QStringLiteral("room"), pod.room);
addParam<IfNotEmpty>(jo, QStringLiteral("sender"), pod.sender);
addParam<IfNotEmpty>(jo, QStringLiteral("underride"), pod.underride);
- return jo;
}
-PushRuleset FromJsonObject<PushRuleset>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<PushRuleset>::fillFrom(
+ const QJsonObject& jo, PushRuleset& result)
{
- PushRuleset result;
- result.content =
- fromJson<QVector<PushRule>>(jo.value("content"_ls));
- result.override =
- fromJson<QVector<PushRule>>(jo.value("override"_ls));
- result.room =
- fromJson<QVector<PushRule>>(jo.value("room"_ls));
- result.sender =
- fromJson<QVector<PushRule>>(jo.value("sender"_ls));
- result.underride =
- fromJson<QVector<PushRule>>(jo.value("underride"_ls));
-
- return result;
+ fromJson(jo.value("content"_ls), result.content);
+ fromJson(jo.value("override"_ls), result.override);
+ fromJson(jo.value("room"_ls), result.room);
+ fromJson(jo.value("sender"_ls), result.sender);
+ fromJson(jo.value("underride"_ls), result.underride);
}
diff --git a/lib/csapi/definitions/push_ruleset.h b/lib/csapi/definitions/push_ruleset.h
index a274b72a..f2d937c0 100644
--- a/lib/csapi/definitions/push_ruleset.h
+++ b/lib/csapi/definitions/push_ruleset.h
@@ -22,12 +22,10 @@ namespace QMatrixClient
QVector<PushRule> sender;
QVector<PushRule> underride;
};
-
- QJsonObject toJson(const PushRuleset& pod);
-
- template <> struct FromJsonObject<PushRuleset>
+ template <> struct JsonObjectConverter<PushRuleset>
{
- PushRuleset operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const PushRuleset& pod);
+ static void fillFrom(const QJsonObject& jo, PushRuleset& pod);
};
} // namespace QMatrixClient
diff --git a/lib/csapi/definitions/room_event_filter.cpp b/lib/csapi/definitions/room_event_filter.cpp
index f6f1e5cb..df92e684 100644
--- a/lib/csapi/definitions/room_event_filter.cpp
+++ b/lib/csapi/definitions/room_event_filter.cpp
@@ -6,25 +6,21 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const RoomEventFilter& pod)
+void JsonObjectConverter<RoomEventFilter>::dumpTo(
+ QJsonObject& jo, const RoomEventFilter& pod)
{
- QJsonObject jo;
+ fillJson<EventFilter>(jo, pod);
addParam<IfNotEmpty>(jo, QStringLiteral("not_rooms"), pod.notRooms);
addParam<IfNotEmpty>(jo, QStringLiteral("rooms"), pod.rooms);
addParam<IfNotEmpty>(jo, QStringLiteral("contains_url"), pod.containsUrl);
- return jo;
}
-RoomEventFilter FromJsonObject<RoomEventFilter>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<RoomEventFilter>::fillFrom(
+ const QJsonObject& jo, RoomEventFilter& result)
{
- RoomEventFilter result;
- result.notRooms =
- fromJson<QStringList>(jo.value("not_rooms"_ls));
- result.rooms =
- fromJson<QStringList>(jo.value("rooms"_ls));
- result.containsUrl =
- fromJson<bool>(jo.value("contains_url"_ls));
-
- return result;
+ fillFromJson<EventFilter>(jo, result);
+ fromJson(jo.value("not_rooms"_ls), result.notRooms);
+ fromJson(jo.value("rooms"_ls), result.rooms);
+ fromJson(jo.value("contains_url"_ls), result.containsUrl);
}
diff --git a/lib/csapi/definitions/room_event_filter.h b/lib/csapi/definitions/room_event_filter.h
index 697fe661..6eb9a390 100644
--- a/lib/csapi/definitions/room_event_filter.h
+++ b/lib/csapi/definitions/room_event_filter.h
@@ -19,15 +19,13 @@ namespace QMatrixClient
QStringList notRooms;
/// A list of room IDs to include. If this list is absent then all rooms are included.
QStringList rooms;
- /// If ``true``, includes only events with a ``url`` key in their content. If ``false``, excludes those events. Defaults to ``false``.
- bool containsUrl;
+ /// If ``true``, includes only events with a ``url`` key in their content. If ``false``, excludes those events. If omitted, ``url`` key is not considered for filtering.
+ Omittable<bool> containsUrl;
};
-
- QJsonObject toJson(const RoomEventFilter& pod);
-
- template <> struct FromJsonObject<RoomEventFilter>
+ template <> struct JsonObjectConverter<RoomEventFilter>
{
- RoomEventFilter operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const RoomEventFilter& pod);
+ static void fillFrom(const QJsonObject& jo, RoomEventFilter& pod);
};
} // namespace QMatrixClient
diff --git a/lib/csapi/definitions/sync_filter.cpp b/lib/csapi/definitions/sync_filter.cpp
index bd87804c..32752d1f 100644
--- a/lib/csapi/definitions/sync_filter.cpp
+++ b/lib/csapi/definitions/sync_filter.cpp
@@ -6,9 +6,25 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const RoomFilter& pod)
+void JsonObjectConverter<StateFilter>::dumpTo(
+ QJsonObject& jo, const StateFilter& pod)
+{
+ fillJson<RoomEventFilter>(jo, pod);
+ addParam<IfNotEmpty>(jo, QStringLiteral("lazy_load_members"), pod.lazyLoadMembers);
+ addParam<IfNotEmpty>(jo, QStringLiteral("include_redundant_members"), pod.includeRedundantMembers);
+}
+
+void JsonObjectConverter<StateFilter>::fillFrom(
+ const QJsonObject& jo, StateFilter& result)
+{
+ fillFromJson<RoomEventFilter>(jo, result);
+ fromJson(jo.value("lazy_load_members"_ls), result.lazyLoadMembers);
+ fromJson(jo.value("include_redundant_members"_ls), result.includeRedundantMembers);
+}
+
+void JsonObjectConverter<RoomFilter>::dumpTo(
+ QJsonObject& jo, const RoomFilter& pod)
{
- QJsonObject jo;
addParam<IfNotEmpty>(jo, QStringLiteral("not_rooms"), pod.notRooms);
addParam<IfNotEmpty>(jo, QStringLiteral("rooms"), pod.rooms);
addParam<IfNotEmpty>(jo, QStringLiteral("ephemeral"), pod.ephemeral);
@@ -16,55 +32,37 @@ QJsonObject QMatrixClient::toJson(const RoomFilter& pod)
addParam<IfNotEmpty>(jo, QStringLiteral("state"), pod.state);
addParam<IfNotEmpty>(jo, QStringLiteral("timeline"), pod.timeline);
addParam<IfNotEmpty>(jo, QStringLiteral("account_data"), pod.accountData);
- return jo;
}
-RoomFilter FromJsonObject<RoomFilter>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<RoomFilter>::fillFrom(
+ const QJsonObject& jo, RoomFilter& result)
{
- RoomFilter result;
- result.notRooms =
- fromJson<QStringList>(jo.value("not_rooms"_ls));
- result.rooms =
- fromJson<QStringList>(jo.value("rooms"_ls));
- result.ephemeral =
- fromJson<RoomEventFilter>(jo.value("ephemeral"_ls));
- result.includeLeave =
- fromJson<bool>(jo.value("include_leave"_ls));
- result.state =
- fromJson<RoomEventFilter>(jo.value("state"_ls));
- result.timeline =
- fromJson<RoomEventFilter>(jo.value("timeline"_ls));
- result.accountData =
- fromJson<RoomEventFilter>(jo.value("account_data"_ls));
-
- return result;
+ fromJson(jo.value("not_rooms"_ls), result.notRooms);
+ fromJson(jo.value("rooms"_ls), result.rooms);
+ fromJson(jo.value("ephemeral"_ls), result.ephemeral);
+ fromJson(jo.value("include_leave"_ls), result.includeLeave);
+ fromJson(jo.value("state"_ls), result.state);
+ fromJson(jo.value("timeline"_ls), result.timeline);
+ fromJson(jo.value("account_data"_ls), result.accountData);
}
-QJsonObject QMatrixClient::toJson(const Filter& pod)
+void JsonObjectConverter<Filter>::dumpTo(
+ QJsonObject& jo, const Filter& pod)
{
- QJsonObject jo;
addParam<IfNotEmpty>(jo, QStringLiteral("event_fields"), pod.eventFields);
addParam<IfNotEmpty>(jo, QStringLiteral("event_format"), pod.eventFormat);
addParam<IfNotEmpty>(jo, QStringLiteral("presence"), pod.presence);
addParam<IfNotEmpty>(jo, QStringLiteral("account_data"), pod.accountData);
addParam<IfNotEmpty>(jo, QStringLiteral("room"), pod.room);
- return jo;
}
-Filter FromJsonObject<Filter>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<Filter>::fillFrom(
+ const QJsonObject& jo, Filter& result)
{
- Filter result;
- result.eventFields =
- fromJson<QStringList>(jo.value("event_fields"_ls));
- result.eventFormat =
- fromJson<QString>(jo.value("event_format"_ls));
- result.presence =
- fromJson<EventFilter>(jo.value("presence"_ls));
- result.accountData =
- fromJson<EventFilter>(jo.value("account_data"_ls));
- result.room =
- fromJson<RoomFilter>(jo.value("room"_ls));
-
- return result;
+ fromJson(jo.value("event_fields"_ls), result.eventFields);
+ fromJson(jo.value("event_format"_ls), result.eventFormat);
+ fromJson(jo.value("presence"_ls), result.presence);
+ fromJson(jo.value("account_data"_ls), result.accountData);
+ fromJson(jo.value("room"_ls), result.room);
}
diff --git a/lib/csapi/definitions/sync_filter.h b/lib/csapi/definitions/sync_filter.h
index ca275a9a..d94c74d7 100644
--- a/lib/csapi/definitions/sync_filter.h
+++ b/lib/csapi/definitions/sync_filter.h
@@ -14,6 +14,37 @@ namespace QMatrixClient
{
// Data structures
+ /// The state events to include for rooms.
+ struct StateFilter : RoomEventFilter
+ {
+ /// If ``true``, the only ``m.room.member`` events returned in
+ /// the ``state`` section of the ``/sync`` response are those
+ /// which are definitely necessary for a client to display
+ /// the ``sender`` of the timeline events in that response.
+ /// If ``false``, ``m.room.member`` events are not filtered.
+ /// By default, servers should suppress duplicate redundant
+ /// lazy-loaded ``m.room.member`` events from being sent to a given
+ /// client across multiple calls to ``/sync``, given that most clients
+ /// cache membership events (see ``include_redundant_members``
+ /// to change this behaviour).
+ Omittable<bool> lazyLoadMembers;
+ /// If ``true``, the ``state`` section of the ``/sync`` response will
+ /// always contain the ``m.room.member`` events required to display
+ /// the ``sender`` of the timeline events in that response, assuming
+ /// ``lazy_load_members`` is enabled. This means that redundant
+ /// duplicate member events may be returned across multiple calls to
+ /// ``/sync``. This is useful for naive clients who never track
+ /// membership data. If ``false``, duplicate ``m.room.member`` events
+ /// may be suppressed by the server across multiple calls to ``/sync``.
+ /// If ``lazy_load_members`` is ``false`` this field is ignored.
+ Omittable<bool> includeRedundantMembers;
+ };
+ template <> struct JsonObjectConverter<StateFilter>
+ {
+ static void dumpTo(QJsonObject& jo, const StateFilter& pod);
+ static void fillFrom(const QJsonObject& jo, StateFilter& pod);
+ };
+
/// Filters to be applied to room data.
struct RoomFilter
{
@@ -24,20 +55,18 @@ namespace QMatrixClient
/// The events that aren't recorded in the room history, e.g. typing and receipts, to include for rooms.
Omittable<RoomEventFilter> ephemeral;
/// Include rooms that the user has left in the sync, default false
- bool includeLeave;
+ Omittable<bool> includeLeave;
/// The state events to include for rooms.
- Omittable<RoomEventFilter> state;
+ Omittable<StateFilter> state;
/// The message and state update events to include for rooms.
Omittable<RoomEventFilter> timeline;
/// The per user account data to include for rooms.
Omittable<RoomEventFilter> accountData;
};
-
- QJsonObject toJson(const RoomFilter& pod);
-
- template <> struct FromJsonObject<RoomFilter>
+ template <> struct JsonObjectConverter<RoomFilter>
{
- RoomFilter operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const RoomFilter& pod);
+ static void fillFrom(const QJsonObject& jo, RoomFilter& pod);
};
struct Filter
@@ -53,12 +82,10 @@ namespace QMatrixClient
/// Filters to be applied to room data.
Omittable<RoomFilter> room;
};
-
- QJsonObject toJson(const Filter& pod);
-
- template <> struct FromJsonObject<Filter>
+ template <> struct JsonObjectConverter<Filter>
{
- Filter operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const Filter& pod);
+ static void fillFrom(const QJsonObject& jo, Filter& pod);
};
} // namespace QMatrixClient
diff --git a/lib/csapi/definitions/user_identifier.cpp b/lib/csapi/definitions/user_identifier.cpp
index 80a6d450..05a27c1c 100644
--- a/lib/csapi/definitions/user_identifier.cpp
+++ b/lib/csapi/definitions/user_identifier.cpp
@@ -6,20 +6,18 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const UserIdentifier& pod)
+void JsonObjectConverter<UserIdentifier>::dumpTo(
+ QJsonObject& jo, const UserIdentifier& pod)
{
- QJsonObject jo = toJson(pod.additionalProperties);
+ fillJson(jo, pod.additionalProperties);
addParam<>(jo, QStringLiteral("type"), pod.type);
- return jo;
}
-UserIdentifier FromJsonObject<UserIdentifier>::operator()(QJsonObject jo) const
+void JsonObjectConverter<UserIdentifier>::fillFrom(
+ QJsonObject jo, UserIdentifier& result)
{
- UserIdentifier result;
- result.type =
- fromJson<QString>(jo.take("type"_ls));
+ fromJson(jo.take("type"_ls), result.type);
- result.additionalProperties = fromJson<QVariantHash>(jo);
- return result;
+ fromJson(jo, result.additionalProperties);
}
diff --git a/lib/csapi/definitions/user_identifier.h b/lib/csapi/definitions/user_identifier.h
index 42614436..cbb1550f 100644
--- a/lib/csapi/definitions/user_identifier.h
+++ b/lib/csapi/definitions/user_identifier.h
@@ -20,12 +20,10 @@ namespace QMatrixClient
/// Identification information for a user
QVariantHash additionalProperties;
};
-
- QJsonObject toJson(const UserIdentifier& pod);
-
- template <> struct FromJsonObject<UserIdentifier>
+ template <> struct JsonObjectConverter<UserIdentifier>
{
- UserIdentifier operator()(QJsonObject jo) const;
+ static void dumpTo(QJsonObject& jo, const UserIdentifier& pod);
+ static void fillFrom(QJsonObject jo, UserIdentifier& pod);
};
} // namespace QMatrixClient
diff --git a/lib/csapi/definitions/wellknown/full.cpp b/lib/csapi/definitions/wellknown/full.cpp
new file mode 100644
index 00000000..5ecef34f
--- /dev/null
+++ b/lib/csapi/definitions/wellknown/full.cpp
@@ -0,0 +1,25 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "full.h"
+
+using namespace QMatrixClient;
+
+void JsonObjectConverter<DiscoveryInformation>::dumpTo(
+ QJsonObject& jo, const DiscoveryInformation& pod)
+{
+ fillJson(jo, pod.additionalProperties);
+ addParam<>(jo, QStringLiteral("m.homeserver"), pod.homeserver);
+ addParam<IfNotEmpty>(jo, QStringLiteral("m.identity_server"), pod.identityServer);
+}
+
+void JsonObjectConverter<DiscoveryInformation>::fillFrom(
+ QJsonObject jo, DiscoveryInformation& result)
+{
+ fromJson(jo.take("m.homeserver"_ls), result.homeserver);
+ fromJson(jo.take("m.identity_server"_ls), result.identityServer);
+
+ fromJson(jo, result.additionalProperties);
+}
+
diff --git a/lib/csapi/definitions/wellknown/full.h b/lib/csapi/definitions/wellknown/full.h
new file mode 100644
index 00000000..d9346acb
--- /dev/null
+++ b/lib/csapi/definitions/wellknown/full.h
@@ -0,0 +1,38 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "converters.h"
+
+#include <QtCore/QJsonObject>
+#include "converters.h"
+#include "csapi/definitions/wellknown/homeserver.h"
+#include "csapi/definitions/wellknown/identity_server.h"
+#include <QtCore/QHash>
+
+namespace QMatrixClient
+{
+ // Data structures
+
+ /// Used by clients to determine the homeserver, identity server, and other
+ /// optional components they should be interacting with.
+ struct DiscoveryInformation
+ {
+ /// Used by clients to determine the homeserver, identity server, and other
+ /// optional components they should be interacting with.
+ HomeserverInformation homeserver;
+ /// Used by clients to determine the homeserver, identity server, and other
+ /// optional components they should be interacting with.
+ Omittable<IdentityServerInformation> identityServer;
+ /// Application-dependent keys using Java package naming convention.
+ QHash<QString, QJsonObject> additionalProperties;
+ };
+ template <> struct JsonObjectConverter<DiscoveryInformation>
+ {
+ static void dumpTo(QJsonObject& jo, const DiscoveryInformation& pod);
+ static void fillFrom(QJsonObject jo, DiscoveryInformation& pod);
+ };
+
+} // namespace QMatrixClient
diff --git a/lib/csapi/definitions/wellknown/homeserver.cpp b/lib/csapi/definitions/wellknown/homeserver.cpp
index f1482ee4..0783f11b 100644
--- a/lib/csapi/definitions/wellknown/homeserver.cpp
+++ b/lib/csapi/definitions/wellknown/homeserver.cpp
@@ -6,19 +6,15 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const HomeserverInformation& pod)
+void JsonObjectConverter<HomeserverInformation>::dumpTo(
+ QJsonObject& jo, const HomeserverInformation& pod)
{
- QJsonObject jo;
addParam<>(jo, QStringLiteral("base_url"), pod.baseUrl);
- return jo;
}
-HomeserverInformation FromJsonObject<HomeserverInformation>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<HomeserverInformation>::fillFrom(
+ const QJsonObject& jo, HomeserverInformation& result)
{
- HomeserverInformation result;
- result.baseUrl =
- fromJson<QString>(jo.value("base_url"_ls));
-
- return result;
+ fromJson(jo.value("base_url"_ls), result.baseUrl);
}
diff --git a/lib/csapi/definitions/wellknown/homeserver.h b/lib/csapi/definitions/wellknown/homeserver.h
index 09d6ba63..f6761c30 100644
--- a/lib/csapi/definitions/wellknown/homeserver.h
+++ b/lib/csapi/definitions/wellknown/homeserver.h
@@ -17,12 +17,10 @@ namespace QMatrixClient
/// The base URL for the homeserver for client-server connections.
QString baseUrl;
};
-
- QJsonObject toJson(const HomeserverInformation& pod);
-
- template <> struct FromJsonObject<HomeserverInformation>
+ template <> struct JsonObjectConverter<HomeserverInformation>
{
- HomeserverInformation operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const HomeserverInformation& pod);
+ static void fillFrom(const QJsonObject& jo, HomeserverInformation& pod);
};
} // namespace QMatrixClient
diff --git a/lib/csapi/definitions/wellknown/identity_server.cpp b/lib/csapi/definitions/wellknown/identity_server.cpp
index f9d7bc37..99f36641 100644
--- a/lib/csapi/definitions/wellknown/identity_server.cpp
+++ b/lib/csapi/definitions/wellknown/identity_server.cpp
@@ -6,19 +6,15 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const IdentityServerInformation& pod)
+void JsonObjectConverter<IdentityServerInformation>::dumpTo(
+ QJsonObject& jo, const IdentityServerInformation& pod)
{
- QJsonObject jo;
addParam<>(jo, QStringLiteral("base_url"), pod.baseUrl);
- return jo;
}
-IdentityServerInformation FromJsonObject<IdentityServerInformation>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<IdentityServerInformation>::fillFrom(
+ const QJsonObject& jo, IdentityServerInformation& result)
{
- IdentityServerInformation result;
- result.baseUrl =
- fromJson<QString>(jo.value("base_url"_ls));
-
- return result;
+ fromJson(jo.value("base_url"_ls), result.baseUrl);
}
diff --git a/lib/csapi/definitions/wellknown/identity_server.h b/lib/csapi/definitions/wellknown/identity_server.h
index cb8ffcee..67d8b08d 100644
--- a/lib/csapi/definitions/wellknown/identity_server.h
+++ b/lib/csapi/definitions/wellknown/identity_server.h
@@ -17,12 +17,10 @@ namespace QMatrixClient
/// The base URL for the identity server for client-server connections.
QString baseUrl;
};
-
- QJsonObject toJson(const IdentityServerInformation& pod);
-
- template <> struct FromJsonObject<IdentityServerInformation>
+ template <> struct JsonObjectConverter<IdentityServerInformation>
{
- IdentityServerInformation operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const IdentityServerInformation& pod);
+ static void fillFrom(const QJsonObject& jo, IdentityServerInformation& pod);
};
} // namespace QMatrixClient
diff --git a/lib/csapi/device_management.cpp b/lib/csapi/device_management.cpp
index 861e1994..9c31db5d 100644
--- a/lib/csapi/device_management.cpp
+++ b/lib/csapi/device_management.cpp
@@ -43,7 +43,7 @@ const QVector<Device>& GetDevicesJob::devices() const
BaseJob::Status GetDevicesJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->devices = fromJson<QVector<Device>>(json.value("devices"_ls));
+ fromJson(json.value("devices"_ls), d->devices);
return Success;
}
@@ -77,7 +77,7 @@ const Device& GetDeviceJob::data() const
BaseJob::Status GetDeviceJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<Device>(data);
+ fromJson(data, d->data);
return Success;
}
diff --git a/lib/csapi/directory.cpp b/lib/csapi/directory.cpp
index 5353f3bc..4af86f7b 100644
--- a/lib/csapi/directory.cpp
+++ b/lib/csapi/directory.cpp
@@ -60,8 +60,8 @@ const QStringList& GetRoomIdByAliasJob::servers() const
BaseJob::Status GetRoomIdByAliasJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->roomId = fromJson<QString>(json.value("room_id"_ls));
- d->servers = fromJson<QStringList>(json.value("servers"_ls));
+ fromJson(json.value("room_id"_ls), d->roomId);
+ fromJson(json.value("servers"_ls), d->servers);
return Success;
}
diff --git a/lib/csapi/event_context.cpp b/lib/csapi/event_context.cpp
index 806c1613..bb1f5301 100644
--- a/lib/csapi/event_context.cpp
+++ b/lib/csapi/event_context.cpp
@@ -82,12 +82,12 @@ StateEvents&& GetEventContextJob::state()
BaseJob::Status GetEventContextJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->begin = fromJson<QString>(json.value("start"_ls));
- d->end = fromJson<QString>(json.value("end"_ls));
- d->eventsBefore = fromJson<RoomEvents>(json.value("events_before"_ls));
- d->event = fromJson<RoomEventPtr>(json.value("event"_ls));
- d->eventsAfter = fromJson<RoomEvents>(json.value("events_after"_ls));
- d->state = fromJson<StateEvents>(json.value("state"_ls));
+ fromJson(json.value("start"_ls), d->begin);
+ fromJson(json.value("end"_ls), d->end);
+ fromJson(json.value("events_before"_ls), d->eventsBefore);
+ fromJson(json.value("event"_ls), d->event);
+ fromJson(json.value("events_after"_ls), d->eventsAfter);
+ fromJson(json.value("state"_ls), d->state);
return Success;
}
diff --git a/lib/csapi/filter.cpp b/lib/csapi/filter.cpp
index 77dc9b92..982e60b5 100644
--- a/lib/csapi/filter.cpp
+++ b/lib/csapi/filter.cpp
@@ -41,7 +41,7 @@ BaseJob::Status DefineFilterJob::parseJson(const QJsonDocument& data)
if (!json.contains("filter_id"_ls))
return { JsonParseError,
"The key 'filter_id' not found in the response" };
- d->filterId = fromJson<QString>(json.value("filter_id"_ls));
+ fromJson(json.value("filter_id"_ls), d->filterId);
return Success;
}
@@ -75,7 +75,7 @@ const Filter& GetFilterJob::data() const
BaseJob::Status GetFilterJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<Filter>(data);
+ fromJson(data, d->data);
return Success;
}
diff --git a/lib/csapi/gtad.yaml b/lib/csapi/gtad.yaml
index cb5e553c..a44f803a 100644
--- a/lib/csapi/gtad.yaml
+++ b/lib/csapi/gtad.yaml
@@ -5,12 +5,15 @@ analyzer:
identifiers:
signed: signedData
unsigned: unsignedData
- default: isDefault
+ PushRule/default: isDefault
+ default: defaultVersion # getCapabilities/RoomVersionsCapability
origin_server_ts: originServerTimestamp # Instead of originServerTs
start: begin # Because start() is a method in BaseJob
m.upload.size: uploadSize
m.homeserver: homeserver
m.identity_server: identityServer
+ m.change_password: changePassword
+ m.room_versions: roomVersions
AuthenticationData/additionalProperties: authInfo
# Structure inside `types`:
@@ -38,7 +41,7 @@ analyzer:
- number:
- float: float
- //: double
- - boolean: { type: bool, omittedValue: 'false' }
+ - boolean: bool
- string:
- byte: &ByteStream
type: QIODevice*
@@ -86,12 +89,9 @@ analyzer:
- /m\.room\.member$/:
type: "EventsArray<RoomMemberEvent>"
imports: '"events/roommemberevent.h"'
- - /state_event.yaml$/:
- type: StateEvents
- - /room_event.yaml$/:
- type: RoomEvents
- - /event.yaml$/:
- type: Events
+ - /state_event.yaml$/: StateEvents
+ - /room_event.yaml$/: RoomEvents
+ - /event.yaml$/: Events
- //: { type: "QVector<{{1}}>", imports: <QtCore/QVector> }
- map: # `additionalProperties` in OpenAPI
- RoomState:
diff --git a/lib/csapi/joining.cpp b/lib/csapi/joining.cpp
index 71781154..00d930fa 100644
--- a/lib/csapi/joining.cpp
+++ b/lib/csapi/joining.cpp
@@ -16,15 +16,16 @@ namespace QMatrixClient
{
// Converters
- QJsonObject toJson(const JoinRoomByIdJob::ThirdPartySigned& pod)
+ template <> struct JsonObjectConverter<JoinRoomByIdJob::ThirdPartySigned>
{
- QJsonObject jo;
- addParam<>(jo, QStringLiteral("sender"), pod.sender);
- addParam<>(jo, QStringLiteral("mxid"), pod.mxid);
- addParam<>(jo, QStringLiteral("token"), pod.token);
- addParam<>(jo, QStringLiteral("signatures"), pod.signatures);
- return jo;
- }
+ static void dumpTo(QJsonObject& jo, const JoinRoomByIdJob::ThirdPartySigned& pod)
+ {
+ addParam<>(jo, QStringLiteral("sender"), pod.sender);
+ addParam<>(jo, QStringLiteral("mxid"), pod.mxid);
+ addParam<>(jo, QStringLiteral("token"), pod.token);
+ addParam<>(jo, QStringLiteral("signatures"), pod.signatures);
+ }
+ };
} // namespace QMatrixClient
class JoinRoomByIdJob::Private
@@ -58,7 +59,7 @@ BaseJob::Status JoinRoomByIdJob::parseJson(const QJsonDocument& data)
if (!json.contains("room_id"_ls))
return { JsonParseError,
"The key 'room_id' not found in the response" };
- d->roomId = fromJson<QString>(json.value("room_id"_ls));
+ fromJson(json.value("room_id"_ls), d->roomId);
return Success;
}
@@ -66,22 +67,24 @@ namespace QMatrixClient
{
// Converters
- QJsonObject toJson(const JoinRoomJob::Signed& pod)
+ template <> struct JsonObjectConverter<JoinRoomJob::Signed>
{
- QJsonObject jo;
- addParam<>(jo, QStringLiteral("sender"), pod.sender);
- addParam<>(jo, QStringLiteral("mxid"), pod.mxid);
- addParam<>(jo, QStringLiteral("token"), pod.token);
- addParam<>(jo, QStringLiteral("signatures"), pod.signatures);
- return jo;
- }
-
- QJsonObject toJson(const JoinRoomJob::ThirdPartySigned& pod)
+ static void dumpTo(QJsonObject& jo, const JoinRoomJob::Signed& pod)
+ {
+ addParam<>(jo, QStringLiteral("sender"), pod.sender);
+ addParam<>(jo, QStringLiteral("mxid"), pod.mxid);
+ addParam<>(jo, QStringLiteral("token"), pod.token);
+ addParam<>(jo, QStringLiteral("signatures"), pod.signatures);
+ }
+ };
+
+ template <> struct JsonObjectConverter<JoinRoomJob::ThirdPartySigned>
{
- QJsonObject jo;
- addParam<>(jo, QStringLiteral("signed"), pod.signedData);
- return jo;
- }
+ static void dumpTo(QJsonObject& jo, const JoinRoomJob::ThirdPartySigned& pod)
+ {
+ addParam<>(jo, QStringLiteral("signed"), pod.signedData);
+ }
+ };
} // namespace QMatrixClient
class JoinRoomJob::Private
@@ -123,7 +126,7 @@ BaseJob::Status JoinRoomJob::parseJson(const QJsonDocument& data)
if (!json.contains("room_id"_ls))
return { JsonParseError,
"The key 'room_id' not found in the response" };
- d->roomId = fromJson<QString>(json.value("room_id"_ls));
+ fromJson(json.value("room_id"_ls), d->roomId);
return Success;
}
diff --git a/lib/csapi/joining.h b/lib/csapi/joining.h
index 137afbfc..52c8ea42 100644
--- a/lib/csapi/joining.h
+++ b/lib/csapi/joining.h
@@ -59,7 +59,7 @@ namespace QMatrixClient
// Result properties
- /// The joined room id
+ /// The joined room ID.
const QString& roomId() const;
protected:
@@ -138,7 +138,7 @@ namespace QMatrixClient
// Result properties
- /// The joined room id
+ /// The joined room ID.
const QString& roomId() const;
protected:
diff --git a/lib/csapi/keys.cpp b/lib/csapi/keys.cpp
index c7492411..6c16a8a3 100644
--- a/lib/csapi/keys.cpp
+++ b/lib/csapi/keys.cpp
@@ -44,7 +44,7 @@ BaseJob::Status UploadKeysJob::parseJson(const QJsonDocument& data)
if (!json.contains("one_time_key_counts"_ls))
return { JsonParseError,
"The key 'one_time_key_counts' not found in the response" };
- d->oneTimeKeyCounts = fromJson<QHash<QString, int>>(json.value("one_time_key_counts"_ls));
+ fromJson(json.value("one_time_key_counts"_ls), d->oneTimeKeyCounts);
return Success;
}
@@ -52,27 +52,20 @@ namespace QMatrixClient
{
// Converters
- template <> struct FromJsonObject<QueryKeysJob::UnsignedDeviceInfo>
+ template <> struct JsonObjectConverter<QueryKeysJob::UnsignedDeviceInfo>
{
- QueryKeysJob::UnsignedDeviceInfo operator()(const QJsonObject& jo) const
+ static void fillFrom(const QJsonObject& jo, QueryKeysJob::UnsignedDeviceInfo& result)
{
- QueryKeysJob::UnsignedDeviceInfo result;
- result.deviceDisplayName =
- fromJson<QString>(jo.value("device_display_name"_ls));
-
- return result;
+ fromJson(jo.value("device_display_name"_ls), result.deviceDisplayName);
}
};
- template <> struct FromJsonObject<QueryKeysJob::DeviceInformation>
+ template <> struct JsonObjectConverter<QueryKeysJob::DeviceInformation>
{
- QueryKeysJob::DeviceInformation operator()(const QJsonObject& jo) const
+ static void fillFrom(const QJsonObject& jo, QueryKeysJob::DeviceInformation& result)
{
- QueryKeysJob::DeviceInformation result;
- result.unsignedData =
- fromJson<QueryKeysJob::UnsignedDeviceInfo>(jo.value("unsigned"_ls));
-
- return result;
+ fillFromJson<DeviceKeys>(jo, result);
+ fromJson(jo.value("unsigned"_ls), result.unsignedData);
}
};
} // namespace QMatrixClient
@@ -113,8 +106,8 @@ const QHash<QString, QHash<QString, QueryKeysJob::DeviceInformation>>& QueryKeys
BaseJob::Status QueryKeysJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->failures = fromJson<QHash<QString, QJsonObject>>(json.value("failures"_ls));
- d->deviceKeys = fromJson<QHash<QString, QHash<QString, DeviceInformation>>>(json.value("device_keys"_ls));
+ fromJson(json.value("failures"_ls), d->failures);
+ fromJson(json.value("device_keys"_ls), d->deviceKeys);
return Success;
}
@@ -153,8 +146,8 @@ const QHash<QString, QHash<QString, QVariant>>& ClaimKeysJob::oneTimeKeys() cons
BaseJob::Status ClaimKeysJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->failures = fromJson<QHash<QString, QJsonObject>>(json.value("failures"_ls));
- d->oneTimeKeys = fromJson<QHash<QString, QHash<QString, QVariant>>>(json.value("one_time_keys"_ls));
+ fromJson(json.value("failures"_ls), d->failures);
+ fromJson(json.value("one_time_keys"_ls), d->oneTimeKeys);
return Success;
}
@@ -205,8 +198,8 @@ const QStringList& GetKeysChangesJob::left() const
BaseJob::Status GetKeysChangesJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->changed = fromJson<QStringList>(json.value("changed"_ls));
- d->left = fromJson<QStringList>(json.value("left"_ls));
+ fromJson(json.value("changed"_ls), d->changed);
+ fromJson(json.value("left"_ls), d->left);
return Success;
}
diff --git a/lib/csapi/kicking.h b/lib/csapi/kicking.h
index 5968187e..714079cf 100644
--- a/lib/csapi/kicking.h
+++ b/lib/csapi/kicking.h
@@ -29,7 +29,7 @@ namespace QMatrixClient
* \param userId
* The fully qualified user ID of the user being kicked.
* \param reason
- * The reason the user has been kicked. This will be supplied as the
+ * The reason the user has been kicked. This will be supplied as the
* ``reason`` on the target's updated `m.room.member`_ event.
*/
explicit KickJob(const QString& roomId, const QString& userId, const QString& reason = {});
diff --git a/lib/csapi/list_joined_rooms.cpp b/lib/csapi/list_joined_rooms.cpp
index a745dba1..85a9cae4 100644
--- a/lib/csapi/list_joined_rooms.cpp
+++ b/lib/csapi/list_joined_rooms.cpp
@@ -46,7 +46,7 @@ BaseJob::Status GetJoinedRoomsJob::parseJson(const QJsonDocument& data)
if (!json.contains("joined_rooms"_ls))
return { JsonParseError,
"The key 'joined_rooms' not found in the response" };
- d->joinedRooms = fromJson<QStringList>(json.value("joined_rooms"_ls));
+ fromJson(json.value("joined_rooms"_ls), d->joinedRooms);
return Success;
}
diff --git a/lib/csapi/list_public_rooms.cpp b/lib/csapi/list_public_rooms.cpp
index 2fdb2005..71b3c541 100644
--- a/lib/csapi/list_public_rooms.cpp
+++ b/lib/csapi/list_public_rooms.cpp
@@ -43,7 +43,7 @@ const QString& GetRoomVisibilityOnDirectoryJob::visibility() const
BaseJob::Status GetRoomVisibilityOnDirectoryJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->visibility = fromJson<QString>(json.value("visibility"_ls));
+ fromJson(json.value("visibility"_ls), d->visibility);
return Success;
}
@@ -100,7 +100,7 @@ const PublicRoomsResponse& GetPublicRoomsJob::data() const
BaseJob::Status GetPublicRoomsJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<PublicRoomsResponse>(data);
+ fromJson(data, d->data);
return Success;
}
@@ -108,12 +108,13 @@ namespace QMatrixClient
{
// Converters
- QJsonObject toJson(const QueryPublicRoomsJob::Filter& pod)
+ template <> struct JsonObjectConverter<QueryPublicRoomsJob::Filter>
{
- QJsonObject jo;
- addParam<IfNotEmpty>(jo, QStringLiteral("generic_search_term"), pod.genericSearchTerm);
- return jo;
- }
+ static void dumpTo(QJsonObject& jo, const QueryPublicRoomsJob::Filter& pod)
+ {
+ addParam<IfNotEmpty>(jo, QStringLiteral("generic_search_term"), pod.genericSearchTerm);
+ }
+ };
} // namespace QMatrixClient
class QueryPublicRoomsJob::Private
@@ -131,7 +132,7 @@ BaseJob::Query queryToQueryPublicRooms(const QString& server)
static const auto QueryPublicRoomsJobName = QStringLiteral("QueryPublicRoomsJob");
-QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, Omittable<int> limit, const QString& since, const Omittable<Filter>& filter, bool includeAllNetworks, const QString& thirdPartyInstanceId)
+QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, Omittable<int> limit, const QString& since, const Omittable<Filter>& filter, Omittable<bool> includeAllNetworks, const QString& thirdPartyInstanceId)
: BaseJob(HttpVerb::Post, QueryPublicRoomsJobName,
basePath % "/publicRooms",
queryToQueryPublicRooms(server))
@@ -155,7 +156,7 @@ const PublicRoomsResponse& QueryPublicRoomsJob::data() const
BaseJob::Status QueryPublicRoomsJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<PublicRoomsResponse>(data);
+ fromJson(data, d->data);
return Success;
}
diff --git a/lib/csapi/list_public_rooms.h b/lib/csapi/list_public_rooms.h
index 8401c134..a6498745 100644
--- a/lib/csapi/list_public_rooms.h
+++ b/lib/csapi/list_public_rooms.h
@@ -156,7 +156,7 @@ namespace QMatrixClient
* The specific third party network/protocol to request from the
* homeserver. Can only be used if ``include_all_networks`` is false.
*/
- explicit QueryPublicRoomsJob(const QString& server = {}, Omittable<int> limit = none, const QString& since = {}, const Omittable<Filter>& filter = none, bool includeAllNetworks = false, const QString& thirdPartyInstanceId = {});
+ explicit QueryPublicRoomsJob(const QString& server = {}, Omittable<int> limit = none, const QString& since = {}, const Omittable<Filter>& filter = none, Omittable<bool> includeAllNetworks = none, const QString& thirdPartyInstanceId = {});
~QueryPublicRoomsJob() override;
// Result properties
diff --git a/lib/csapi/login.cpp b/lib/csapi/login.cpp
index 4d15a30b..5e369b9a 100644
--- a/lib/csapi/login.cpp
+++ b/lib/csapi/login.cpp
@@ -16,15 +16,11 @@ namespace QMatrixClient
{
// Converters
- template <> struct FromJsonObject<GetLoginFlowsJob::LoginFlow>
+ template <> struct JsonObjectConverter<GetLoginFlowsJob::LoginFlow>
{
- GetLoginFlowsJob::LoginFlow operator()(const QJsonObject& jo) const
+ static void fillFrom(const QJsonObject& jo, GetLoginFlowsJob::LoginFlow& result)
{
- GetLoginFlowsJob::LoginFlow result;
- result.type =
- fromJson<QString>(jo.value("type"_ls));
-
- return result;
+ fromJson(jo.value("type"_ls), result.type);
}
};
} // namespace QMatrixClient
@@ -60,7 +56,7 @@ const QVector<GetLoginFlowsJob::LoginFlow>& GetLoginFlowsJob::flows() const
BaseJob::Status GetLoginFlowsJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->flows = fromJson<QVector<LoginFlow>>(json.value("flows"_ls));
+ fromJson(json.value("flows"_ls), d->flows);
return Success;
}
@@ -71,6 +67,7 @@ class LoginJob::Private
QString accessToken;
QString homeServer;
QString deviceId;
+ Omittable<DiscoveryInformation> wellKnown;
};
static const auto LoginJobName = QStringLiteral("LoginJob");
@@ -115,13 +112,19 @@ const QString& LoginJob::deviceId() const
return d->deviceId;
}
+const Omittable<DiscoveryInformation>& LoginJob::wellKnown() const
+{
+ return d->wellKnown;
+}
+
BaseJob::Status LoginJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->userId = fromJson<QString>(json.value("user_id"_ls));
- d->accessToken = fromJson<QString>(json.value("access_token"_ls));
- d->homeServer = fromJson<QString>(json.value("home_server"_ls));
- d->deviceId = fromJson<QString>(json.value("device_id"_ls));
+ fromJson(json.value("user_id"_ls), d->userId);
+ fromJson(json.value("access_token"_ls), d->accessToken);
+ fromJson(json.value("home_server"_ls), d->homeServer);
+ fromJson(json.value("device_id"_ls), d->deviceId);
+ fromJson(json.value("well_known"_ls), d->wellKnown);
return Success;
}
diff --git a/lib/csapi/login.h b/lib/csapi/login.h
index 957d8881..648316df 100644
--- a/lib/csapi/login.h
+++ b/lib/csapi/login.h
@@ -7,6 +7,7 @@
#include "jobs/basejob.h"
#include <QtCore/QVector>
+#include "csapi/definitions/wellknown/full.h"
#include "csapi/definitions/user_identifier.h"
#include "converters.h"
@@ -118,6 +119,11 @@ namespace QMatrixClient
/// ID of the logged-in device. Will be the same as the
/// corresponding parameter in the request, if one was specified.
const QString& deviceId() const;
+ /// Optional client configuration provided by the server. If present,
+ /// clients SHOULD use the provided object to reconfigure themselves,
+ /// optionally validating the URLs within. This object takes the same
+ /// form as the one returned from .well-known autodiscovery.
+ const Omittable<DiscoveryInformation>& wellKnown() const;
protected:
Status parseJson(const QJsonDocument& data) override;
diff --git a/lib/csapi/message_pagination.cpp b/lib/csapi/message_pagination.cpp
index c59a51ab..9aca7ec9 100644
--- a/lib/csapi/message_pagination.cpp
+++ b/lib/csapi/message_pagination.cpp
@@ -68,9 +68,9 @@ RoomEvents&& GetRoomEventsJob::chunk()
BaseJob::Status GetRoomEventsJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->begin = fromJson<QString>(json.value("start"_ls));
- d->end = fromJson<QString>(json.value("end"_ls));
- d->chunk = fromJson<RoomEvents>(json.value("chunk"_ls));
+ fromJson(json.value("start"_ls), d->begin);
+ fromJson(json.value("end"_ls), d->end);
+ fromJson(json.value("chunk"_ls), d->chunk);
return Success;
}
diff --git a/lib/csapi/notifications.cpp b/lib/csapi/notifications.cpp
index 785a0a8a..c00b7cb0 100644
--- a/lib/csapi/notifications.cpp
+++ b/lib/csapi/notifications.cpp
@@ -16,25 +16,16 @@ namespace QMatrixClient
{
// Converters
- template <> struct FromJsonObject<GetNotificationsJob::Notification>
+ template <> struct JsonObjectConverter<GetNotificationsJob::Notification>
{
- GetNotificationsJob::Notification operator()(const QJsonObject& jo) const
+ static void fillFrom(const QJsonObject& jo, GetNotificationsJob::Notification& result)
{
- GetNotificationsJob::Notification result;
- result.actions =
- fromJson<QVector<QVariant>>(jo.value("actions"_ls));
- result.event =
- fromJson<EventPtr>(jo.value("event"_ls));
- result.profileTag =
- fromJson<QString>(jo.value("profile_tag"_ls));
- result.read =
- fromJson<bool>(jo.value("read"_ls));
- result.roomId =
- fromJson<QString>(jo.value("room_id"_ls));
- result.ts =
- fromJson<int>(jo.value("ts"_ls));
-
- return result;
+ fromJson(jo.value("actions"_ls), result.actions);
+ fromJson(jo.value("event"_ls), result.event);
+ fromJson(jo.value("profile_tag"_ls), result.profileTag);
+ fromJson(jo.value("read"_ls), result.read);
+ fromJson(jo.value("room_id"_ls), result.roomId);
+ fromJson(jo.value("ts"_ls), result.ts);
}
};
} // namespace QMatrixClient
@@ -87,11 +78,11 @@ std::vector<GetNotificationsJob::Notification>&& GetNotificationsJob::notificati
BaseJob::Status GetNotificationsJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->nextToken = fromJson<QString>(json.value("next_token"_ls));
+ fromJson(json.value("next_token"_ls), d->nextToken);
if (!json.contains("notifications"_ls))
return { JsonParseError,
"The key 'notifications' not found in the response" };
- d->notifications = fromJson<std::vector<Notification>>(json.value("notifications"_ls));
+ fromJson(json.value("notifications"_ls), d->notifications);
return Success;
}
diff --git a/lib/csapi/openid.cpp b/lib/csapi/openid.cpp
index 2547f0c8..b27fe0b8 100644
--- a/lib/csapi/openid.cpp
+++ b/lib/csapi/openid.cpp
@@ -59,19 +59,19 @@ BaseJob::Status RequestOpenIdTokenJob::parseJson(const QJsonDocument& data)
if (!json.contains("access_token"_ls))
return { JsonParseError,
"The key 'access_token' not found in the response" };
- d->accessToken = fromJson<QString>(json.value("access_token"_ls));
+ fromJson(json.value("access_token"_ls), d->accessToken);
if (!json.contains("token_type"_ls))
return { JsonParseError,
"The key 'token_type' not found in the response" };
- d->tokenType = fromJson<QString>(json.value("token_type"_ls));
+ fromJson(json.value("token_type"_ls), d->tokenType);
if (!json.contains("matrix_server_name"_ls))
return { JsonParseError,
"The key 'matrix_server_name' not found in the response" };
- d->matrixServerName = fromJson<QString>(json.value("matrix_server_name"_ls));
+ fromJson(json.value("matrix_server_name"_ls), d->matrixServerName);
if (!json.contains("expires_in"_ls))
return { JsonParseError,
"The key 'expires_in' not found in the response" };
- d->expiresIn = fromJson<int>(json.value("expires_in"_ls));
+ fromJson(json.value("expires_in"_ls), d->expiresIn);
return Success;
}
diff --git a/lib/csapi/peeking_events.cpp b/lib/csapi/peeking_events.cpp
index e046a62e..3208d48d 100644
--- a/lib/csapi/peeking_events.cpp
+++ b/lib/csapi/peeking_events.cpp
@@ -66,9 +66,9 @@ RoomEvents&& PeekEventsJob::chunk()
BaseJob::Status PeekEventsJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->begin = fromJson<QString>(json.value("start"_ls));
- d->end = fromJson<QString>(json.value("end"_ls));
- d->chunk = fromJson<RoomEvents>(json.value("chunk"_ls));
+ fromJson(json.value("start"_ls), d->begin);
+ fromJson(json.value("end"_ls), d->end);
+ fromJson(json.value("chunk"_ls), d->chunk);
return Success;
}
diff --git a/lib/csapi/presence.cpp b/lib/csapi/presence.cpp
index 7aba8b61..024d7a34 100644
--- a/lib/csapi/presence.cpp
+++ b/lib/csapi/presence.cpp
@@ -30,7 +30,7 @@ class GetPresenceJob::Private
QString presence;
Omittable<int> lastActiveAgo;
QString statusMsg;
- bool currentlyActive;
+ Omittable<bool> currentlyActive;
};
QUrl GetPresenceJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
@@ -65,7 +65,7 @@ const QString& GetPresenceJob::statusMsg() const
return d->statusMsg;
}
-bool GetPresenceJob::currentlyActive() const
+Omittable<bool> GetPresenceJob::currentlyActive() const
{
return d->currentlyActive;
}
@@ -76,56 +76,10 @@ BaseJob::Status GetPresenceJob::parseJson(const QJsonDocument& data)
if (!json.contains("presence"_ls))
return { JsonParseError,
"The key 'presence' not found in the response" };
- d->presence = fromJson<QString>(json.value("presence"_ls));
- d->lastActiveAgo = fromJson<int>(json.value("last_active_ago"_ls));
- d->statusMsg = fromJson<QString>(json.value("status_msg"_ls));
- d->currentlyActive = fromJson<bool>(json.value("currently_active"_ls));
- return Success;
-}
-
-static const auto ModifyPresenceListJobName = QStringLiteral("ModifyPresenceListJob");
-
-ModifyPresenceListJob::ModifyPresenceListJob(const QString& userId, const QStringList& invite, const QStringList& drop)
- : BaseJob(HttpVerb::Post, ModifyPresenceListJobName,
- basePath % "/presence/list/" % userId)
-{
- QJsonObject _data;
- addParam<IfNotEmpty>(_data, QStringLiteral("invite"), invite);
- addParam<IfNotEmpty>(_data, QStringLiteral("drop"), drop);
- setRequestData(_data);
-}
-
-class GetPresenceForListJob::Private
-{
- public:
- Events data;
-};
-
-QUrl GetPresenceForListJob::makeRequestUrl(QUrl baseUrl, const QString& userId)
-{
- return BaseJob::makeRequestUrl(std::move(baseUrl),
- basePath % "/presence/list/" % userId);
-}
-
-static const auto GetPresenceForListJobName = QStringLiteral("GetPresenceForListJob");
-
-GetPresenceForListJob::GetPresenceForListJob(const QString& userId)
- : BaseJob(HttpVerb::Get, GetPresenceForListJobName,
- basePath % "/presence/list/" % userId, false)
- , d(new Private)
-{
-}
-
-GetPresenceForListJob::~GetPresenceForListJob() = default;
-
-Events&& GetPresenceForListJob::data()
-{
- return std::move(d->data);
-}
-
-BaseJob::Status GetPresenceForListJob::parseJson(const QJsonDocument& data)
-{
- d->data = fromJson<Events>(data);
+ fromJson(json.value("presence"_ls), d->presence);
+ fromJson(json.value("last_active_ago"_ls), d->lastActiveAgo);
+ fromJson(json.value("status_msg"_ls), d->statusMsg);
+ fromJson(json.value("currently_active"_ls), d->currentlyActive);
return Success;
}
diff --git a/lib/csapi/presence.h b/lib/csapi/presence.h
index 86b9d395..5e132d24 100644
--- a/lib/csapi/presence.h
+++ b/lib/csapi/presence.h
@@ -6,7 +6,6 @@
#include "jobs/basejob.h"
-#include "events/eventloader.h"
#include "converters.h"
namespace QMatrixClient
@@ -65,59 +64,7 @@ namespace QMatrixClient
/// The state message for this user if one was set.
const QString& statusMsg() const;
/// Whether the user is currently active
- bool currentlyActive() const;
-
- protected:
- Status parseJson(const QJsonDocument& data) override;
-
- private:
- class Private;
- QScopedPointer<Private> d;
- };
-
- /// Add or remove users from this presence list.
- ///
- /// Adds or removes users from this presence list.
- class ModifyPresenceListJob : public BaseJob
- {
- public:
- /*! Add or remove users from this presence list.
- * \param userId
- * The user whose presence list is being modified.
- * \param invite
- * A list of user IDs to add to the list.
- * \param drop
- * A list of user IDs to remove from the list.
- */
- explicit ModifyPresenceListJob(const QString& userId, const QStringList& invite = {}, const QStringList& drop = {});
- };
-
- /// Get presence events for this presence list.
- ///
- /// Retrieve a list of presence events for every user on this list.
- class GetPresenceForListJob : public BaseJob
- {
- public:
- /*! Get presence events for this presence list.
- * \param userId
- * The user whose presence list should be retrieved.
- */
- explicit GetPresenceForListJob(const QString& userId);
-
- /*! Construct a URL without creating a full-fledged job object
- *
- * This function can be used when a URL for
- * GetPresenceForListJob is necessary but the job
- * itself isn't.
- */
- static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId);
-
- ~GetPresenceForListJob() override;
-
- // Result properties
-
- /// A list of presence events for this list.
- Events&& data();
+ Omittable<bool> currentlyActive() const;
protected:
Status parseJson(const QJsonDocument& data) override;
diff --git a/lib/csapi/profile.cpp b/lib/csapi/profile.cpp
index bb053062..4ed3ad9b 100644
--- a/lib/csapi/profile.cpp
+++ b/lib/csapi/profile.cpp
@@ -54,7 +54,7 @@ const QString& GetDisplayNameJob::displayname() const
BaseJob::Status GetDisplayNameJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->displayname = fromJson<QString>(json.value("displayname"_ls));
+ fromJson(json.value("displayname"_ls), d->displayname);
return Success;
}
@@ -100,7 +100,7 @@ const QString& GetAvatarUrlJob::avatarUrl() const
BaseJob::Status GetAvatarUrlJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->avatarUrl = fromJson<QString>(json.value("avatar_url"_ls));
+ fromJson(json.value("avatar_url"_ls), d->avatarUrl);
return Success;
}
@@ -141,8 +141,8 @@ const QString& GetUserProfileJob::displayname() const
BaseJob::Status GetUserProfileJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->avatarUrl = fromJson<QString>(json.value("avatar_url"_ls));
- d->displayname = fromJson<QString>(json.value("displayname"_ls));
+ fromJson(json.value("avatar_url"_ls), d->avatarUrl);
+ fromJson(json.value("displayname"_ls), d->displayname);
return Success;
}
diff --git a/lib/csapi/pusher.cpp b/lib/csapi/pusher.cpp
index d20db88a..664959f4 100644
--- a/lib/csapi/pusher.cpp
+++ b/lib/csapi/pusher.cpp
@@ -16,43 +16,27 @@ namespace QMatrixClient
{
// Converters
- template <> struct FromJsonObject<GetPushersJob::PusherData>
+ template <> struct JsonObjectConverter<GetPushersJob::PusherData>
{
- GetPushersJob::PusherData operator()(const QJsonObject& jo) const
+ static void fillFrom(const QJsonObject& jo, GetPushersJob::PusherData& result)
{
- GetPushersJob::PusherData result;
- result.url =
- fromJson<QString>(jo.value("url"_ls));
- result.format =
- fromJson<QString>(jo.value("format"_ls));
-
- return result;
+ fromJson(jo.value("url"_ls), result.url);
+ fromJson(jo.value("format"_ls), result.format);
}
};
- template <> struct FromJsonObject<GetPushersJob::Pusher>
+ template <> struct JsonObjectConverter<GetPushersJob::Pusher>
{
- GetPushersJob::Pusher operator()(const QJsonObject& jo) const
+ static void fillFrom(const QJsonObject& jo, GetPushersJob::Pusher& result)
{
- GetPushersJob::Pusher result;
- result.pushkey =
- fromJson<QString>(jo.value("pushkey"_ls));
- result.kind =
- fromJson<QString>(jo.value("kind"_ls));
- result.appId =
- fromJson<QString>(jo.value("app_id"_ls));
- result.appDisplayName =
- fromJson<QString>(jo.value("app_display_name"_ls));
- result.deviceDisplayName =
- fromJson<QString>(jo.value("device_display_name"_ls));
- result.profileTag =
- fromJson<QString>(jo.value("profile_tag"_ls));
- result.lang =
- fromJson<QString>(jo.value("lang"_ls));
- result.data =
- fromJson<GetPushersJob::PusherData>(jo.value("data"_ls));
-
- return result;
+ fromJson(jo.value("pushkey"_ls), result.pushkey);
+ fromJson(jo.value("kind"_ls), result.kind);
+ fromJson(jo.value("app_id"_ls), result.appId);
+ fromJson(jo.value("app_display_name"_ls), result.appDisplayName);
+ fromJson(jo.value("device_display_name"_ls), result.deviceDisplayName);
+ fromJson(jo.value("profile_tag"_ls), result.profileTag);
+ fromJson(jo.value("lang"_ls), result.lang);
+ fromJson(jo.value("data"_ls), result.data);
}
};
} // namespace QMatrixClient
@@ -88,7 +72,7 @@ const QVector<GetPushersJob::Pusher>& GetPushersJob::pushers() const
BaseJob::Status GetPushersJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->pushers = fromJson<QVector<Pusher>>(json.value("pushers"_ls));
+ fromJson(json.value("pushers"_ls), d->pushers);
return Success;
}
@@ -96,18 +80,19 @@ namespace QMatrixClient
{
// Converters
- QJsonObject toJson(const PostPusherJob::PusherData& pod)
+ template <> struct JsonObjectConverter<PostPusherJob::PusherData>
{
- QJsonObject jo;
- addParam<IfNotEmpty>(jo, QStringLiteral("url"), pod.url);
- addParam<IfNotEmpty>(jo, QStringLiteral("format"), pod.format);
- return jo;
- }
+ static void dumpTo(QJsonObject& jo, const PostPusherJob::PusherData& pod)
+ {
+ addParam<IfNotEmpty>(jo, QStringLiteral("url"), pod.url);
+ addParam<IfNotEmpty>(jo, QStringLiteral("format"), pod.format);
+ }
+ };
} // namespace QMatrixClient
static const auto PostPusherJobName = QStringLiteral("PostPusherJob");
-PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag, bool append)
+PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag, Omittable<bool> append)
: BaseJob(HttpVerb::Post, PostPusherJobName,
basePath % "/pushers/set")
{
diff --git a/lib/csapi/pusher.h b/lib/csapi/pusher.h
index 2b506183..da3303fe 100644
--- a/lib/csapi/pusher.h
+++ b/lib/csapi/pusher.h
@@ -164,6 +164,6 @@ namespace QMatrixClient
* other pushers with the same App ID and pushkey for different
* users. The default is ``false``.
*/
- explicit PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag = {}, bool append = false);
+ explicit PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag = {}, Omittable<bool> append = none);
};
} // namespace QMatrixClient
diff --git a/lib/csapi/pushrules.cpp b/lib/csapi/pushrules.cpp
index ea8ad02a..b91d18f7 100644
--- a/lib/csapi/pushrules.cpp
+++ b/lib/csapi/pushrules.cpp
@@ -46,7 +46,7 @@ BaseJob::Status GetPushRulesJob::parseJson(const QJsonDocument& data)
if (!json.contains("global"_ls))
return { JsonParseError,
"The key 'global' not found in the response" };
- d->global = fromJson<PushRuleset>(json.value("global"_ls));
+ fromJson(json.value("global"_ls), d->global);
return Success;
}
@@ -80,7 +80,7 @@ const PushRule& GetPushRuleJob::data() const
BaseJob::Status GetPushRuleJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<PushRule>(data);
+ fromJson(data, d->data);
return Success;
}
@@ -154,7 +154,7 @@ BaseJob::Status IsPushRuleEnabledJob::parseJson(const QJsonDocument& data)
if (!json.contains("enabled"_ls))
return { JsonParseError,
"The key 'enabled' not found in the response" };
- d->enabled = fromJson<bool>(json.value("enabled"_ls));
+ fromJson(json.value("enabled"_ls), d->enabled);
return Success;
}
@@ -203,7 +203,7 @@ BaseJob::Status GetPushRuleActionsJob::parseJson(const QJsonDocument& data)
if (!json.contains("actions"_ls))
return { JsonParseError,
"The key 'actions' not found in the response" };
- d->actions = fromJson<QStringList>(json.value("actions"_ls));
+ fromJson(json.value("actions"_ls), d->actions);
return Success;
}
diff --git a/lib/csapi/read_markers.h b/lib/csapi/read_markers.h
index f19f46b0..d982b477 100644
--- a/lib/csapi/read_markers.h
+++ b/lib/csapi/read_markers.h
@@ -26,7 +26,7 @@ namespace QMatrixClient
* event MUST belong to the room.
* \param mRead
* The event ID to set the read receipt location at. This is
- * equivalent to calling ``/receipt/m.read/$elsewhere:domain.com``
+ * equivalent to calling ``/receipt/m.read/$elsewhere:example.org``
* and is provided here to save that extra call.
*/
explicit SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, const QString& mRead = {});
diff --git a/lib/csapi/redaction.cpp b/lib/csapi/redaction.cpp
index 64098670..1d54e36d 100644
--- a/lib/csapi/redaction.cpp
+++ b/lib/csapi/redaction.cpp
@@ -40,7 +40,7 @@ const QString& RedactEventJob::eventId() const
BaseJob::Status RedactEventJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->eventId = fromJson<QString>(json.value("event_id"_ls));
+ fromJson(json.value("event_id"_ls), d->eventId);
return Success;
}
diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp
index 320ec796..5dc9c1e5 100644
--- a/lib/csapi/registration.cpp
+++ b/lib/csapi/registration.cpp
@@ -30,7 +30,7 @@ BaseJob::Query queryToRegister(const QString& kind)
static const auto RegisterJobName = QStringLiteral("RegisterJob");
-RegisterJob::RegisterJob(const QString& kind, const Omittable<AuthenticationData>& auth, bool bindEmail, const QString& username, const QString& password, const QString& deviceId, const QString& initialDeviceDisplayName, bool inhibitLogin)
+RegisterJob::RegisterJob(const QString& kind, const Omittable<AuthenticationData>& auth, Omittable<bool> bindEmail, const QString& username, const QString& password, const QString& deviceId, const QString& initialDeviceDisplayName, Omittable<bool> inhibitLogin)
: BaseJob(HttpVerb::Post, RegisterJobName,
basePath % "/register",
queryToRegister(kind),
@@ -76,10 +76,10 @@ BaseJob::Status RegisterJob::parseJson(const QJsonDocument& data)
if (!json.contains("user_id"_ls))
return { JsonParseError,
"The key 'user_id' not found in the response" };
- d->userId = fromJson<QString>(json.value("user_id"_ls));
- d->accessToken = fromJson<QString>(json.value("access_token"_ls));
- d->homeServer = fromJson<QString>(json.value("home_server"_ls));
- d->deviceId = fromJson<QString>(json.value("device_id"_ls));
+ fromJson(json.value("user_id"_ls), d->userId);
+ fromJson(json.value("access_token"_ls), d->accessToken);
+ fromJson(json.value("home_server"_ls), d->homeServer);
+ fromJson(json.value("device_id"_ls), d->deviceId);
return Success;
}
@@ -114,7 +114,7 @@ const Sid& RequestTokenToRegisterEmailJob::data() const
BaseJob::Status RequestTokenToRegisterEmailJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<Sid>(data);
+ fromJson(data, d->data);
return Success;
}
@@ -150,7 +150,7 @@ const Sid& RequestTokenToRegisterMSISDNJob::data() const
BaseJob::Status RequestTokenToRegisterMSISDNJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<Sid>(data);
+ fromJson(data, d->data);
return Success;
}
@@ -197,7 +197,7 @@ const Sid& RequestTokenToResetPasswordEmailJob::data() const
BaseJob::Status RequestTokenToResetPasswordEmailJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<Sid>(data);
+ fromJson(data, d->data);
return Success;
}
@@ -233,7 +233,7 @@ const Sid& RequestTokenToResetPasswordMSISDNJob::data() const
BaseJob::Status RequestTokenToResetPasswordMSISDNJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<Sid>(data);
+ fromJson(data, d->data);
return Success;
}
@@ -251,7 +251,7 @@ DeactivateAccountJob::DeactivateAccountJob(const Omittable<AuthenticationData>&
class CheckUsernameAvailabilityJob::Private
{
public:
- bool available;
+ Omittable<bool> available;
};
BaseJob::Query queryToCheckUsernameAvailability(const QString& username)
@@ -281,7 +281,7 @@ CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob(const QString& userna
CheckUsernameAvailabilityJob::~CheckUsernameAvailabilityJob() = default;
-bool CheckUsernameAvailabilityJob::available() const
+Omittable<bool> CheckUsernameAvailabilityJob::available() const
{
return d->available;
}
@@ -289,7 +289,7 @@ bool CheckUsernameAvailabilityJob::available() const
BaseJob::Status CheckUsernameAvailabilityJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->available = fromJson<bool>(json.value("available"_ls));
+ fromJson(json.value("available"_ls), d->available);
return Success;
}
diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h
index 9002b5c8..ca1a1c21 100644
--- a/lib/csapi/registration.h
+++ b/lib/csapi/registration.h
@@ -80,7 +80,7 @@ namespace QMatrixClient
* returned from this call, therefore preventing an automatic
* login. Defaults to false.
*/
- explicit RegisterJob(const QString& kind = QStringLiteral("user"), const Omittable<AuthenticationData>& auth = none, bool bindEmail = false, const QString& username = {}, const QString& password = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {}, bool inhibitLogin = false);
+ explicit RegisterJob(const QString& kind = QStringLiteral("user"), const Omittable<AuthenticationData>& auth = none, Omittable<bool> bindEmail = none, const QString& username = {}, const QString& password = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {}, Omittable<bool> inhibitLogin = none);
~RegisterJob() override;
// Result properties
@@ -418,7 +418,7 @@ namespace QMatrixClient
/// A flag to indicate that the username is available. This should always
/// be ``true`` when the server replies with 200 OK.
- bool available() const;
+ Omittable<bool> available() const;
protected:
Status parseJson(const QJsonDocument& data) override;
diff --git a/lib/csapi/room_send.cpp b/lib/csapi/room_send.cpp
index 2b39ede2..0d25eb69 100644
--- a/lib/csapi/room_send.cpp
+++ b/lib/csapi/room_send.cpp
@@ -38,7 +38,7 @@ const QString& SendMessageJob::eventId() const
BaseJob::Status SendMessageJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->eventId = fromJson<QString>(json.value("event_id"_ls));
+ fromJson(json.value("event_id"_ls), d->eventId);
return Success;
}
diff --git a/lib/csapi/room_state.cpp b/lib/csapi/room_state.cpp
index 8f87979d..3aa7d736 100644
--- a/lib/csapi/room_state.cpp
+++ b/lib/csapi/room_state.cpp
@@ -38,7 +38,7 @@ const QString& SetRoomStateWithKeyJob::eventId() const
BaseJob::Status SetRoomStateWithKeyJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->eventId = fromJson<QString>(json.value("event_id"_ls));
+ fromJson(json.value("event_id"_ls), d->eventId);
return Success;
}
@@ -68,7 +68,7 @@ const QString& SetRoomStateJob::eventId() const
BaseJob::Status SetRoomStateJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->eventId = fromJson<QString>(json.value("event_id"_ls));
+ fromJson(json.value("event_id"_ls), d->eventId);
return Success;
}
diff --git a/lib/csapi/room_upgrades.cpp b/lib/csapi/room_upgrades.cpp
new file mode 100644
index 00000000..f58fd675
--- /dev/null
+++ b/lib/csapi/room_upgrades.cpp
@@ -0,0 +1,49 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "room_upgrades.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+class UpgradeRoomJob::Private
+{
+ public:
+ QString replacementRoom;
+};
+
+static const auto UpgradeRoomJobName = QStringLiteral("UpgradeRoomJob");
+
+UpgradeRoomJob::UpgradeRoomJob(const QString& roomId, const QString& newVersion)
+ : BaseJob(HttpVerb::Post, UpgradeRoomJobName,
+ basePath % "/rooms/" % roomId % "/upgrade")
+ , d(new Private)
+{
+ QJsonObject _data;
+ addParam<>(_data, QStringLiteral("new_version"), newVersion);
+ setRequestData(_data);
+}
+
+UpgradeRoomJob::~UpgradeRoomJob() = default;
+
+const QString& UpgradeRoomJob::replacementRoom() const
+{
+ return d->replacementRoom;
+}
+
+BaseJob::Status UpgradeRoomJob::parseJson(const QJsonDocument& data)
+{
+ auto json = data.object();
+ if (!json.contains("replacement_room"_ls))
+ return { JsonParseError,
+ "The key 'replacement_room' not found in the response" };
+ fromJson(json.value("replacement_room"_ls), d->replacementRoom);
+ return Success;
+}
+
diff --git a/lib/csapi/room_upgrades.h b/lib/csapi/room_upgrades.h
new file mode 100644
index 00000000..4da5941a
--- /dev/null
+++ b/lib/csapi/room_upgrades.h
@@ -0,0 +1,41 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "jobs/basejob.h"
+
+
+namespace QMatrixClient
+{
+ // Operations
+
+ /// Upgrades a room to a new room version.
+ ///
+ /// Upgrades the given room to a particular room version.
+ class UpgradeRoomJob : public BaseJob
+ {
+ public:
+ /*! Upgrades a room to a new room version.
+ * \param roomId
+ * The ID of the room to upgrade.
+ * \param newVersion
+ * The new version for the room.
+ */
+ explicit UpgradeRoomJob(const QString& roomId, const QString& newVersion);
+ ~UpgradeRoomJob() override;
+
+ // Result properties
+
+ /// The ID of the new room.
+ const QString& replacementRoom() const;
+
+ protected:
+ Status parseJson(const QJsonDocument& data) override;
+
+ private:
+ class Private;
+ QScopedPointer<Private> d;
+ };
+} // namespace QMatrixClient
diff --git a/lib/csapi/rooms.cpp b/lib/csapi/rooms.cpp
index 3befeee5..0b08ccec 100644
--- a/lib/csapi/rooms.cpp
+++ b/lib/csapi/rooms.cpp
@@ -42,16 +42,10 @@ EventPtr&& GetOneRoomEventJob::data()
BaseJob::Status GetOneRoomEventJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<EventPtr>(data);
+ fromJson(data, d->data);
return Success;
}
-class GetRoomStateWithKeyJob::Private
-{
- public:
- StateEventPtr data;
-};
-
QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType, const QString& stateKey)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
@@ -63,29 +57,9 @@ static const auto GetRoomStateWithKeyJobName = QStringLiteral("GetRoomStateWithK
GetRoomStateWithKeyJob::GetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey)
: BaseJob(HttpVerb::Get, GetRoomStateWithKeyJobName,
basePath % "/rooms/" % roomId % "/state/" % eventType % "/" % stateKey)
- , d(new Private)
-{
-}
-
-GetRoomStateWithKeyJob::~GetRoomStateWithKeyJob() = default;
-
-StateEventPtr&& GetRoomStateWithKeyJob::data()
-{
- return std::move(d->data);
-}
-
-BaseJob::Status GetRoomStateWithKeyJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<StateEventPtr>(data);
- return Success;
}
-class GetRoomStateByTypeJob::Private
-{
- public:
- StateEventPtr data;
-};
-
QUrl GetRoomStateByTypeJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
@@ -97,21 +71,7 @@ static const auto GetRoomStateByTypeJobName = QStringLiteral("GetRoomStateByType
GetRoomStateByTypeJob::GetRoomStateByTypeJob(const QString& roomId, const QString& eventType)
: BaseJob(HttpVerb::Get, GetRoomStateByTypeJobName,
basePath % "/rooms/" % roomId % "/state/" % eventType)
- , d(new Private)
-{
-}
-
-GetRoomStateByTypeJob::~GetRoomStateByTypeJob() = default;
-
-StateEventPtr&& GetRoomStateByTypeJob::data()
-{
- return std::move(d->data);
-}
-
-BaseJob::Status GetRoomStateByTypeJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<StateEventPtr>(data);
- return Success;
}
class GetRoomStateJob::Private
@@ -144,7 +104,7 @@ StateEvents&& GetRoomStateJob::data()
BaseJob::Status GetRoomStateJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<StateEvents>(data);
+ fromJson(data, d->data);
return Success;
}
@@ -154,17 +114,28 @@ class GetMembersByRoomJob::Private
EventsArray<RoomMemberEvent> chunk;
};
-QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId)
+BaseJob::Query queryToGetMembersByRoom(const QString& at, const QString& membership, const QString& notMembership)
+{
+ BaseJob::Query _q;
+ addParam<IfNotEmpty>(_q, QStringLiteral("at"), at);
+ addParam<IfNotEmpty>(_q, QStringLiteral("membership"), membership);
+ addParam<IfNotEmpty>(_q, QStringLiteral("not_membership"), notMembership);
+ return _q;
+}
+
+QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& at, const QString& membership, const QString& notMembership)
{
return BaseJob::makeRequestUrl(std::move(baseUrl),
- basePath % "/rooms/" % roomId % "/members");
+ basePath % "/rooms/" % roomId % "/members",
+ queryToGetMembersByRoom(at, membership, notMembership));
}
static const auto GetMembersByRoomJobName = QStringLiteral("GetMembersByRoomJob");
-GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId)
+GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId, const QString& at, const QString& membership, const QString& notMembership)
: BaseJob(HttpVerb::Get, GetMembersByRoomJobName,
- basePath % "/rooms/" % roomId % "/members")
+ basePath % "/rooms/" % roomId % "/members",
+ queryToGetMembersByRoom(at, membership, notMembership))
, d(new Private)
{
}
@@ -179,7 +150,7 @@ EventsArray<RoomMemberEvent>&& GetMembersByRoomJob::chunk()
BaseJob::Status GetMembersByRoomJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->chunk = fromJson<EventsArray<RoomMemberEvent>>(json.value("chunk"_ls));
+ fromJson(json.value("chunk"_ls), d->chunk);
return Success;
}
@@ -187,17 +158,12 @@ namespace QMatrixClient
{
// Converters
- template <> struct FromJsonObject<GetJoinedMembersByRoomJob::RoomMember>
+ template <> struct JsonObjectConverter<GetJoinedMembersByRoomJob::RoomMember>
{
- GetJoinedMembersByRoomJob::RoomMember operator()(const QJsonObject& jo) const
+ static void fillFrom(const QJsonObject& jo, GetJoinedMembersByRoomJob::RoomMember& result)
{
- GetJoinedMembersByRoomJob::RoomMember result;
- result.displayName =
- fromJson<QString>(jo.value("display_name"_ls));
- result.avatarUrl =
- fromJson<QString>(jo.value("avatar_url"_ls));
-
- return result;
+ fromJson(jo.value("display_name"_ls), result.displayName);
+ fromJson(jo.value("avatar_url"_ls), result.avatarUrl);
}
};
} // namespace QMatrixClient
@@ -233,7 +199,7 @@ const QHash<QString, GetJoinedMembersByRoomJob::RoomMember>& GetJoinedMembersByR
BaseJob::Status GetJoinedMembersByRoomJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->joined = fromJson<QHash<QString, RoomMember>>(json.value("joined"_ls));
+ fromJson(json.value("joined"_ls), d->joined);
return Success;
}
diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h
index 2366918b..b4d3d9b6 100644
--- a/lib/csapi/rooms.h
+++ b/lib/csapi/rooms.h
@@ -80,19 +80,6 @@ namespace QMatrixClient
*/
static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType, const QString& stateKey);
- ~GetRoomStateWithKeyJob() override;
-
- // Result properties
-
- /// The content of the state event.
- StateEventPtr&& data();
-
- protected:
- Status parseJson(const QJsonDocument& data) override;
-
- private:
- class Private;
- QScopedPointer<Private> d;
};
/// Get the state identified by the type, with the empty state key.
@@ -122,19 +109,6 @@ namespace QMatrixClient
*/
static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType);
- ~GetRoomStateByTypeJob() override;
-
- // Result properties
-
- /// The content of the state event.
- StateEventPtr&& data();
-
- protected:
- Status parseJson(const QJsonDocument& data) override;
-
- private:
- class Private;
- QScopedPointer<Private> d;
};
/// Get all state events in the current state of a room.
@@ -184,8 +158,17 @@ namespace QMatrixClient
/*! Get the m.room.member events for the room.
* \param roomId
* The room to get the member events for.
+ * \param at
+ * The token defining the timeline position as-of which to return
+ * the list of members. This token can be obtained from a batch token
+ * returned for each room by the sync API, or from
+ * a ``start``/``end`` token returned by a ``/messages`` request.
+ * \param membership
+ * Only return users with the specified membership
+ * \param notMembership
+ * Only return users with membership state other than specified
*/
- explicit GetMembersByRoomJob(const QString& roomId);
+ explicit GetMembersByRoomJob(const QString& roomId, const QString& at = {}, const QString& membership = {}, const QString& notMembership = {});
/*! Construct a URL without creating a full-fledged job object
*
@@ -193,7 +176,7 @@ namespace QMatrixClient
* GetMembersByRoomJob is necessary but the job
* itself isn't.
*/
- static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId);
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& at = {}, const QString& membership = {}, const QString& notMembership = {});
~GetMembersByRoomJob() override;
diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp
index 9436eb47..a5f83c79 100644
--- a/lib/csapi/search.cpp
+++ b/lib/csapi/search.cpp
@@ -16,146 +16,113 @@ namespace QMatrixClient
{
// Converters
- QJsonObject toJson(const SearchJob::IncludeEventContext& pod)
+ template <> struct JsonObjectConverter<SearchJob::IncludeEventContext>
{
- QJsonObject jo;
- addParam<IfNotEmpty>(jo, QStringLiteral("before_limit"), pod.beforeLimit);
- addParam<IfNotEmpty>(jo, QStringLiteral("after_limit"), pod.afterLimit);
- addParam<IfNotEmpty>(jo, QStringLiteral("include_profile"), pod.includeProfile);
- return jo;
- }
-
- QJsonObject toJson(const SearchJob::Group& pod)
- {
- QJsonObject jo;
- addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key);
- return jo;
- }
+ static void dumpTo(QJsonObject& jo, const SearchJob::IncludeEventContext& pod)
+ {
+ addParam<IfNotEmpty>(jo, QStringLiteral("before_limit"), pod.beforeLimit);
+ addParam<IfNotEmpty>(jo, QStringLiteral("after_limit"), pod.afterLimit);
+ addParam<IfNotEmpty>(jo, QStringLiteral("include_profile"), pod.includeProfile);
+ }
+ };
- QJsonObject toJson(const SearchJob::Groupings& pod)
+ template <> struct JsonObjectConverter<SearchJob::Group>
{
- QJsonObject jo;
- addParam<IfNotEmpty>(jo, QStringLiteral("group_by"), pod.groupBy);
- return jo;
- }
+ static void dumpTo(QJsonObject& jo, const SearchJob::Group& pod)
+ {
+ addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key);
+ }
+ };
- QJsonObject toJson(const SearchJob::RoomEventsCriteria& pod)
- {
- QJsonObject jo;
- addParam<>(jo, QStringLiteral("search_term"), pod.searchTerm);
- addParam<IfNotEmpty>(jo, QStringLiteral("keys"), pod.keys);
- addParam<IfNotEmpty>(jo, QStringLiteral("filter"), pod.filter);
- addParam<IfNotEmpty>(jo, QStringLiteral("order_by"), pod.orderBy);
- addParam<IfNotEmpty>(jo, QStringLiteral("event_context"), pod.eventContext);
- addParam<IfNotEmpty>(jo, QStringLiteral("include_state"), pod.includeState);
- addParam<IfNotEmpty>(jo, QStringLiteral("groupings"), pod.groupings);
- return jo;
- }
-
- QJsonObject toJson(const SearchJob::Categories& pod)
+ template <> struct JsonObjectConverter<SearchJob::Groupings>
{
- QJsonObject jo;
- addParam<IfNotEmpty>(jo, QStringLiteral("room_events"), pod.roomEvents);
- return jo;
- }
+ static void dumpTo(QJsonObject& jo, const SearchJob::Groupings& pod)
+ {
+ addParam<IfNotEmpty>(jo, QStringLiteral("group_by"), pod.groupBy);
+ }
+ };
- template <> struct FromJsonObject<SearchJob::UserProfile>
+ template <> struct JsonObjectConverter<SearchJob::RoomEventsCriteria>
{
- SearchJob::UserProfile operator()(const QJsonObject& jo) const
+ static void dumpTo(QJsonObject& jo, const SearchJob::RoomEventsCriteria& pod)
{
- SearchJob::UserProfile result;
- result.displayname =
- fromJson<QString>(jo.value("displayname"_ls));
- result.avatarUrl =
- fromJson<QString>(jo.value("avatar_url"_ls));
+ addParam<>(jo, QStringLiteral("search_term"), pod.searchTerm);
+ addParam<IfNotEmpty>(jo, QStringLiteral("keys"), pod.keys);
+ addParam<IfNotEmpty>(jo, QStringLiteral("filter"), pod.filter);
+ addParam<IfNotEmpty>(jo, QStringLiteral("order_by"), pod.orderBy);
+ addParam<IfNotEmpty>(jo, QStringLiteral("event_context"), pod.eventContext);
+ addParam<IfNotEmpty>(jo, QStringLiteral("include_state"), pod.includeState);
+ addParam<IfNotEmpty>(jo, QStringLiteral("groupings"), pod.groupings);
+ }
+ };
- return result;
+ template <> struct JsonObjectConverter<SearchJob::Categories>
+ {
+ static void dumpTo(QJsonObject& jo, const SearchJob::Categories& pod)
+ {
+ addParam<IfNotEmpty>(jo, QStringLiteral("room_events"), pod.roomEvents);
}
};
- template <> struct FromJsonObject<SearchJob::EventContext>
+ template <> struct JsonObjectConverter<SearchJob::UserProfile>
{
- SearchJob::EventContext operator()(const QJsonObject& jo) const
+ static void fillFrom(const QJsonObject& jo, SearchJob::UserProfile& result)
{
- SearchJob::EventContext result;
- result.begin =
- fromJson<QString>(jo.value("start"_ls));
- result.end =
- fromJson<QString>(jo.value("end"_ls));
- result.profileInfo =
- fromJson<QHash<QString, SearchJob::UserProfile>>(jo.value("profile_info"_ls));
- result.eventsBefore =
- fromJson<RoomEvents>(jo.value("events_before"_ls));
- result.eventsAfter =
- fromJson<RoomEvents>(jo.value("events_after"_ls));
-
- return result;
+ fromJson(jo.value("displayname"_ls), result.displayname);
+ fromJson(jo.value("avatar_url"_ls), result.avatarUrl);
}
};
- template <> struct FromJsonObject<SearchJob::Result>
+ template <> struct JsonObjectConverter<SearchJob::EventContext>
{
- SearchJob::Result operator()(const QJsonObject& jo) const
+ static void fillFrom(const QJsonObject& jo, SearchJob::EventContext& result)
{
- SearchJob::Result result;
- result.rank =
- fromJson<double>(jo.value("rank"_ls));
- result.result =
- fromJson<RoomEventPtr>(jo.value("result"_ls));
- result.context =
- fromJson<SearchJob::EventContext>(jo.value("context"_ls));
-
- return result;
+ fromJson(jo.value("start"_ls), result.begin);
+ fromJson(jo.value("end"_ls), result.end);
+ fromJson(jo.value("profile_info"_ls), result.profileInfo);
+ fromJson(jo.value("events_before"_ls), result.eventsBefore);
+ fromJson(jo.value("events_after"_ls), result.eventsAfter);
}
};
- template <> struct FromJsonObject<SearchJob::GroupValue>
+ template <> struct JsonObjectConverter<SearchJob::Result>
{
- SearchJob::GroupValue operator()(const QJsonObject& jo) const
+ static void fillFrom(const QJsonObject& jo, SearchJob::Result& result)
{
- SearchJob::GroupValue result;
- result.nextBatch =
- fromJson<QString>(jo.value("next_batch"_ls));
- result.order =
- fromJson<int>(jo.value("order"_ls));
- result.results =
- fromJson<QStringList>(jo.value("results"_ls));
-
- return result;
+ fromJson(jo.value("rank"_ls), result.rank);
+ fromJson(jo.value("result"_ls), result.result);
+ fromJson(jo.value("context"_ls), result.context);
}
};
- template <> struct FromJsonObject<SearchJob::ResultRoomEvents>
+ template <> struct JsonObjectConverter<SearchJob::GroupValue>
{
- SearchJob::ResultRoomEvents operator()(const QJsonObject& jo) const
+ static void fillFrom(const QJsonObject& jo, SearchJob::GroupValue& result)
{
- SearchJob::ResultRoomEvents result;
- result.count =
- fromJson<int>(jo.value("count"_ls));
- result.highlights =
- fromJson<QStringList>(jo.value("highlights"_ls));
- result.results =
- fromJson<std::vector<SearchJob::Result>>(jo.value("results"_ls));
- result.state =
- fromJson<std::unordered_map<QString, StateEvents>>(jo.value("state"_ls));
- result.groups =
- fromJson<QHash<QString, QHash<QString, SearchJob::GroupValue>>>(jo.value("groups"_ls));
- result.nextBatch =
- fromJson<QString>(jo.value("next_batch"_ls));
-
- return result;
+ fromJson(jo.value("next_batch"_ls), result.nextBatch);
+ fromJson(jo.value("order"_ls), result.order);
+ fromJson(jo.value("results"_ls), result.results);
}
};
- template <> struct FromJsonObject<SearchJob::ResultCategories>
+ template <> struct JsonObjectConverter<SearchJob::ResultRoomEvents>
{
- SearchJob::ResultCategories operator()(const QJsonObject& jo) const
+ static void fillFrom(const QJsonObject& jo, SearchJob::ResultRoomEvents& result)
{
- SearchJob::ResultCategories result;
- result.roomEvents =
- fromJson<SearchJob::ResultRoomEvents>(jo.value("room_events"_ls));
+ fromJson(jo.value("count"_ls), result.count);
+ fromJson(jo.value("highlights"_ls), result.highlights);
+ fromJson(jo.value("results"_ls), result.results);
+ fromJson(jo.value("state"_ls), result.state);
+ fromJson(jo.value("groups"_ls), result.groups);
+ fromJson(jo.value("next_batch"_ls), result.nextBatch);
+ }
+ };
- return result;
+ template <> struct JsonObjectConverter<SearchJob::ResultCategories>
+ {
+ static void fillFrom(const QJsonObject& jo, SearchJob::ResultCategories& result)
+ {
+ fromJson(jo.value("room_events"_ls), result.roomEvents);
}
};
} // namespace QMatrixClient
@@ -199,7 +166,7 @@ BaseJob::Status SearchJob::parseJson(const QJsonDocument& data)
if (!json.contains("search_categories"_ls))
return { JsonParseError,
"The key 'search_categories' not found in the response" };
- d->searchCategories = fromJson<ResultCategories>(json.value("search_categories"_ls));
+ fromJson(json.value("search_categories"_ls), d->searchCategories);
return Success;
}
diff --git a/lib/csapi/search.h b/lib/csapi/search.h
index 85b0886b..86a0ee92 100644
--- a/lib/csapi/search.h
+++ b/lib/csapi/search.h
@@ -39,7 +39,7 @@ namespace QMatrixClient
/// historic profile information for the users
/// that sent the events that were returned.
/// By default, this is ``false``.
- bool includeProfile;
+ Omittable<bool> includeProfile;
};
/// Configuration for group.
@@ -74,7 +74,7 @@ namespace QMatrixClient
Omittable<IncludeEventContext> eventContext;
/// Requests the server return the current state for
/// each room returned.
- bool includeState;
+ Omittable<bool> includeState;
/// Requests that the server partitions the result set
/// based on the provided list of keys.
Omittable<Groupings> groupings;
diff --git a/lib/csapi/sso_login_redirect.cpp b/lib/csapi/sso_login_redirect.cpp
new file mode 100644
index 00000000..7323951c
--- /dev/null
+++ b/lib/csapi/sso_login_redirect.cpp
@@ -0,0 +1,38 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "sso_login_redirect.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+BaseJob::Query queryToRedirectToSSO(const QString& redirectUrl)
+{
+ BaseJob::Query _q;
+ addParam<>(_q, QStringLiteral("redirectUrl"), redirectUrl);
+ return _q;
+}
+
+QUrl RedirectToSSOJob::makeRequestUrl(QUrl baseUrl, const QString& redirectUrl)
+{
+ return BaseJob::makeRequestUrl(std::move(baseUrl),
+ basePath % "/login/sso/redirect",
+ queryToRedirectToSSO(redirectUrl));
+}
+
+static const auto RedirectToSSOJobName = QStringLiteral("RedirectToSSOJob");
+
+RedirectToSSOJob::RedirectToSSOJob(const QString& redirectUrl)
+ : BaseJob(HttpVerb::Get, RedirectToSSOJobName,
+ basePath % "/login/sso/redirect",
+ queryToRedirectToSSO(redirectUrl),
+ {}, false)
+{
+}
+
diff --git a/lib/csapi/sso_login_redirect.h b/lib/csapi/sso_login_redirect.h
new file mode 100644
index 00000000..c09365b0
--- /dev/null
+++ b/lib/csapi/sso_login_redirect.h
@@ -0,0 +1,39 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "jobs/basejob.h"
+
+
+namespace QMatrixClient
+{
+ // Operations
+
+ /// Redirect the user's browser to the SSO interface.
+ ///
+ /// A web-based Matrix client should instruct the user's browser to
+ /// navigate to this endpoint in order to log in via SSO.
+ ///
+ /// The server MUST respond with an HTTP redirect to the SSO interface.
+ class RedirectToSSOJob : public BaseJob
+ {
+ public:
+ /*! Redirect the user's browser to the SSO interface.
+ * \param redirectUrl
+ * URI to which the user will be redirected after the homeserver has
+ * authenticated the user with SSO.
+ */
+ explicit RedirectToSSOJob(const QString& redirectUrl);
+
+ /*! Construct a URL without creating a full-fledged job object
+ *
+ * This function can be used when a URL for
+ * RedirectToSSOJob is necessary but the job
+ * itself isn't.
+ */
+ static QUrl makeRequestUrl(QUrl baseUrl, const QString& redirectUrl);
+
+ };
+} // namespace QMatrixClient
diff --git a/lib/csapi/tags.cpp b/lib/csapi/tags.cpp
index 808915ac..94026bb9 100644
--- a/lib/csapi/tags.cpp
+++ b/lib/csapi/tags.cpp
@@ -16,16 +16,12 @@ namespace QMatrixClient
{
// Converters
- template <> struct FromJsonObject<GetRoomTagsJob::Tag>
+ template <> struct JsonObjectConverter<GetRoomTagsJob::Tag>
{
- GetRoomTagsJob::Tag operator()(QJsonObject jo) const
+ static void fillFrom(QJsonObject jo, GetRoomTagsJob::Tag& result)
{
- GetRoomTagsJob::Tag result;
- result.order =
- fromJson<float>(jo.take("order"_ls));
-
- result.additionalProperties = fromJson<QVariantHash>(jo);
- return result;
+ fromJson(jo.take("order"_ls), result.order);
+ fromJson(jo, result.additionalProperties);
}
};
} // namespace QMatrixClient
@@ -61,7 +57,7 @@ const QHash<QString, GetRoomTagsJob::Tag>& GetRoomTagsJob::tags() const
BaseJob::Status GetRoomTagsJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->tags = fromJson<QHash<QString, Tag>>(json.value("tags"_ls));
+ fromJson(json.value("tags"_ls), d->tags);
return Success;
}
diff --git a/lib/csapi/third_party_lookup.cpp b/lib/csapi/third_party_lookup.cpp
index 3ba1a5ad..12cb7c59 100644
--- a/lib/csapi/third_party_lookup.cpp
+++ b/lib/csapi/third_party_lookup.cpp
@@ -42,7 +42,7 @@ const QHash<QString, ThirdPartyProtocol>& GetProtocolsJob::data() const
BaseJob::Status GetProtocolsJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<QHash<QString, ThirdPartyProtocol>>(data);
+ fromJson(data, d->data);
return Success;
}
@@ -76,7 +76,7 @@ const ThirdPartyProtocol& GetProtocolMetadataJob::data() const
BaseJob::Status GetProtocolMetadataJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<ThirdPartyProtocol>(data);
+ fromJson(data, d->data);
return Success;
}
@@ -119,7 +119,7 @@ const QVector<ThirdPartyLocation>& QueryLocationByProtocolJob::data() const
BaseJob::Status QueryLocationByProtocolJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<QVector<ThirdPartyLocation>>(data);
+ fromJson(data, d->data);
return Success;
}
@@ -162,7 +162,7 @@ const QVector<ThirdPartyUser>& QueryUserByProtocolJob::data() const
BaseJob::Status QueryUserByProtocolJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<QVector<ThirdPartyUser>>(data);
+ fromJson(data, d->data);
return Success;
}
@@ -205,7 +205,7 @@ const QVector<ThirdPartyLocation>& QueryLocationByAliasJob::data() const
BaseJob::Status QueryLocationByAliasJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<QVector<ThirdPartyLocation>>(data);
+ fromJson(data, d->data);
return Success;
}
@@ -248,7 +248,7 @@ const QVector<ThirdPartyUser>& QueryUserByIDJob::data() const
BaseJob::Status QueryUserByIDJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<QVector<ThirdPartyUser>>(data);
+ fromJson(data, d->data);
return Success;
}
diff --git a/lib/csapi/users.cpp b/lib/csapi/users.cpp
index deb9cb8a..97d8962d 100644
--- a/lib/csapi/users.cpp
+++ b/lib/csapi/users.cpp
@@ -16,19 +16,13 @@ namespace QMatrixClient
{
// Converters
- template <> struct FromJsonObject<SearchUserDirectoryJob::User>
+ template <> struct JsonObjectConverter<SearchUserDirectoryJob::User>
{
- SearchUserDirectoryJob::User operator()(const QJsonObject& jo) const
+ static void fillFrom(const QJsonObject& jo, SearchUserDirectoryJob::User& result)
{
- SearchUserDirectoryJob::User result;
- result.userId =
- fromJson<QString>(jo.value("user_id"_ls));
- result.displayName =
- fromJson<QString>(jo.value("display_name"_ls));
- result.avatarUrl =
- fromJson<QString>(jo.value("avatar_url"_ls));
-
- return result;
+ fromJson(jo.value("user_id"_ls), result.userId);
+ fromJson(jo.value("display_name"_ls), result.displayName);
+ fromJson(jo.value("avatar_url"_ls), result.avatarUrl);
}
};
} // namespace QMatrixClient
@@ -71,11 +65,11 @@ BaseJob::Status SearchUserDirectoryJob::parseJson(const QJsonDocument& data)
if (!json.contains("results"_ls))
return { JsonParseError,
"The key 'results' not found in the response" };
- d->results = fromJson<QVector<User>>(json.value("results"_ls));
+ fromJson(json.value("results"_ls), d->results);
if (!json.contains("limited"_ls))
return { JsonParseError,
"The key 'limited' not found in the response" };
- d->limited = fromJson<bool>(json.value("limited"_ls));
+ fromJson(json.value("limited"_ls), d->limited);
return Success;
}
diff --git a/lib/csapi/versions.cpp b/lib/csapi/versions.cpp
index 128902e2..6ee6725d 100644
--- a/lib/csapi/versions.cpp
+++ b/lib/csapi/versions.cpp
@@ -16,6 +16,7 @@ class GetVersionsJob::Private
{
public:
QStringList versions;
+ QHash<QString, bool> unstableFeatures;
};
QUrl GetVersionsJob::makeRequestUrl(QUrl baseUrl)
@@ -40,10 +41,19 @@ const QStringList& GetVersionsJob::versions() const
return d->versions;
}
+const QHash<QString, bool>& GetVersionsJob::unstableFeatures() const
+{
+ return d->unstableFeatures;
+}
+
BaseJob::Status GetVersionsJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
- d->versions = fromJson<QStringList>(json.value("versions"_ls));
+ if (!json.contains("versions"_ls))
+ return { JsonParseError,
+ "The key 'versions' not found in the response" };
+ fromJson(json.value("versions"_ls), d->versions);
+ fromJson(json.value("unstable_features"_ls), d->unstableFeatures);
return Success;
}
diff --git a/lib/csapi/versions.h b/lib/csapi/versions.h
index 309de184..b56f293f 100644
--- a/lib/csapi/versions.h
+++ b/lib/csapi/versions.h
@@ -6,6 +6,8 @@
#include "jobs/basejob.h"
+#include <QtCore/QHash>
+#include "converters.h"
namespace QMatrixClient
{
@@ -19,6 +21,19 @@ namespace QMatrixClient
///
/// Only the latest ``Z`` value will be reported for each supported ``X.Y`` value.
/// i.e. if the server implements ``r0.0.0``, ``r0.0.1``, and ``r1.2.0``, it will report ``r0.0.1`` and ``r1.2.0``.
+ ///
+ /// The server may additionally advertise experimental features it supports
+ /// through ``unstable_features``. These features should be namespaced and
+ /// may optionally include version information within their name if desired.
+ /// Features listed here are not for optionally toggling parts of the Matrix
+ /// specification and should only be used to advertise support for a feature
+ /// which has not yet landed in the spec. For example, a feature currently
+ /// undergoing the proposal process may appear here and eventually be taken
+ /// off this list once the feature lands in the spec and the server deems it
+ /// reasonable to do so. Servers may wish to keep advertising features here
+ /// after they've been released into the spec to give clients a chance to
+ /// upgrade appropriately. Additionally, clients should avoid using unstable
+ /// features in their stable releases.
class GetVersionsJob : public BaseJob
{
public:
@@ -38,6 +53,10 @@ namespace QMatrixClient
/// The supported versions.
const QStringList& versions() const;
+ /// Experimental features the server supports. Features not listed here,
+ /// or the lack of this property all together, indicate that a feature is
+ /// not supported.
+ const QHash<QString, bool>& unstableFeatures() const;
protected:
Status parseJson(const QJsonDocument& data) override;
diff --git a/lib/csapi/voip.cpp b/lib/csapi/voip.cpp
index 0479b645..e8158723 100644
--- a/lib/csapi/voip.cpp
+++ b/lib/csapi/voip.cpp
@@ -42,7 +42,7 @@ const QJsonObject& GetTurnServerJob::data() const
BaseJob::Status GetTurnServerJob::parseJson(const QJsonDocument& data)
{
- d->data = fromJson<QJsonObject>(data);
+ fromJson(data, d->data);
return Success;
}
diff --git a/lib/csapi/wellknown.cpp b/lib/csapi/wellknown.cpp
index d42534a0..a6107f86 100644
--- a/lib/csapi/wellknown.cpp
+++ b/lib/csapi/wellknown.cpp
@@ -15,8 +15,7 @@ static const auto basePath = QStringLiteral("/.well-known");
class GetWellknownJob::Private
{
public:
- HomeserverInformation homeserver;
- Omittable<IdentityServerInformation> identityServer;
+ DiscoveryInformation data;
};
QUrl GetWellknownJob::makeRequestUrl(QUrl baseUrl)
@@ -36,24 +35,14 @@ GetWellknownJob::GetWellknownJob()
GetWellknownJob::~GetWellknownJob() = default;
-const HomeserverInformation& GetWellknownJob::homeserver() const
+const DiscoveryInformation& GetWellknownJob::data() const
{
- return d->homeserver;
-}
-
-const Omittable<IdentityServerInformation>& GetWellknownJob::identityServer() const
-{
- return d->identityServer;
+ return d->data;
}
BaseJob::Status GetWellknownJob::parseJson(const QJsonDocument& data)
{
- auto json = data.object();
- if (!json.contains("m.homeserver"_ls))
- return { JsonParseError,
- "The key 'm.homeserver' not found in the response" };
- d->homeserver = fromJson<HomeserverInformation>(json.value("m.homeserver"_ls));
- d->identityServer = fromJson<IdentityServerInformation>(json.value("m.identity_server"_ls));
+ fromJson(data, d->data);
return Success;
}
diff --git a/lib/csapi/wellknown.h b/lib/csapi/wellknown.h
index df4c8c6e..8da9ce9f 100644
--- a/lib/csapi/wellknown.h
+++ b/lib/csapi/wellknown.h
@@ -6,9 +6,8 @@
#include "jobs/basejob.h"
+#include "csapi/definitions/wellknown/full.h"
#include "converters.h"
-#include "csapi/definitions/wellknown/identity_server.h"
-#include "csapi/definitions/wellknown/homeserver.h"
namespace QMatrixClient
{
@@ -41,10 +40,8 @@ namespace QMatrixClient
// Result properties
- /// Information about the homeserver to connect to.
- const HomeserverInformation& homeserver() const;
- /// Optional. Information about the identity server to connect to.
- const Omittable<IdentityServerInformation>& identityServer() const;
+ /// Server discovery information.
+ const DiscoveryInformation& data() const;
protected:
Status parseJson(const QJsonDocument& data) override;
diff --git a/lib/csapi/whoami.cpp b/lib/csapi/whoami.cpp
index cb6439ef..aebdf5d3 100644
--- a/lib/csapi/whoami.cpp
+++ b/lib/csapi/whoami.cpp
@@ -46,7 +46,7 @@ BaseJob::Status GetTokenOwnerJob::parseJson(const QJsonDocument& data)
if (!json.contains("user_id"_ls))
return { JsonParseError,
"The key 'user_id' not found in the response" };
- d->userId = fromJson<QString>(json.value("user_id"_ls));
+ fromJson(json.value("user_id"_ls), d->userId);
return Success;
}
diff --git a/lib/csapi/{{base}}.cpp.mustache b/lib/csapi/{{base}}.cpp.mustache
index 64fd8bf3..ff888d76 100644
--- a/lib/csapi/{{base}}.cpp.mustache
+++ b/lib/csapi/{{base}}.cpp.mustache
@@ -8,49 +8,52 @@
{{/operations}}
using namespace QMatrixClient;
{{#models.model}}{{#in?}}
-QJsonObject QMatrixClient::toJson(const {{qualifiedName}}& pod)
+void JsonObjectConverter<{{qualifiedName}}>::dumpTo(
+ QJsonObject& jo, const {{qualifiedName}}& pod)
{
- QJsonObject jo{{#propertyMap}} = toJson(pod.{{nameCamelCase}}){{/propertyMap}};{{#vars}}
- addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}});{{/vars}}
- return jo;
-}
+{{#propertyMap}} fillJson(jo, pod.{{nameCamelCase}});
+{{/propertyMap}}{{#parents}} fillJson<{{name}}>(jo, pod);
+{{/parents}}{{#vars}} addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}});
+{{/vars}}}{{!<- dumpTo() ends here}}
{{/in?}}{{#out?}}
-{{qualifiedName}} FromJsonObject<{{qualifiedName}}>::operator()({{^propertyMap}}const QJsonObject&{{/propertyMap}}{{#propertyMap}}QJsonObject{{/propertyMap}} jo) const
+void JsonObjectConverter<{{qualifiedName}}>::fillFrom(
+ {{^propertyMap}}const QJsonObject&{{/propertyMap
+ }}{{#propertyMap}}QJsonObject{{/propertyMap}} jo, {{qualifiedName}}& result)
{
- {{qualifiedName}} result;
-{{#vars}} result.{{nameCamelCase}} =
- fromJson<{{dataType.qualifiedName}}>(jo.{{#propertyMap}}take{{/propertyMap}}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls));
+{{#parents}} fillFromJson<{{qualifiedName}}>(jo, result);
+{{/parents}}{{#vars}} fromJson(jo.{{#propertyMap}}take{{/propertyMap
+ }}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls), result.{{nameCamelCase}});
{{/vars}}{{#propertyMap}}
- result.{{nameCamelCase}} = fromJson<{{dataType.qualifiedName}}>(jo);{{/propertyMap}}
- return result;
-}
+ fromJson(jo, result.{{nameCamelCase}});
+{{/propertyMap}}}
{{/out?}}{{/models.model}}{{#operations}}
static const auto basePath = QStringLiteral("{{basePathWithoutHost}}");
{{# operation}}{{#models}}
namespace QMatrixClient
{
// Converters
-{{#model}}{{#in?}}
- QJsonObject toJson(const {{qualifiedName}}& pod)
- {
- QJsonObject jo{{#propertyMap}} = toJson(pod.{{nameCamelCase}}){{/propertyMap}};{{#vars}}
- addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}});{{/vars}}
- return jo;
- }
-{{/in?}}{{#out?}}
- template <> struct FromJsonObject<{{qualifiedName}}>
+{{#model}}
+ template <> struct JsonObjectConverter<{{qualifiedName}}>
{
- {{qualifiedName}} operator()({{^propertyMap}}const QJsonObject&{{/propertyMap}}{{#propertyMap}}QJsonObject{{/propertyMap}} jo) const
+{{#in?}} static void dumpTo(QJsonObject& jo, const {{qualifiedName}}& pod)
{
- {{qualifiedName}} result;
-{{#vars}} result.{{nameCamelCase}} =
- fromJson<{{dataType.qualifiedName}}>(jo.{{#propertyMap}}take{{/propertyMap}}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls));
-{{/vars}}{{#propertyMap}}
- result.{{nameCamelCase}} = fromJson<{{dataType.qualifiedName}}>(jo);{{/propertyMap}}
- return result;
- }
- };
-{{/out?}}{{/model}}} // namespace QMatrixClient
+{{#propertyMap}} fillJson(jo, pod.{{nameCamelCase}});
+ {{/propertyMap}}{{#parents}}fillJson<{{name}}>(jo, pod);
+ {{/parents}}{{#vars
+}} addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}});
+{{/vars}} }
+{{/in?}}{{#out?
+}} static void fillFrom({{^propertyMap}}const QJsonObject&{{/propertyMap
+ }}{{#propertyMap}}QJsonObject{{/propertyMap}} jo, {{qualifiedName}}& result)
+ {
+{{#parents}} fillFromJson<{{qualifiedName}}{{!of the parent!}}>(jo, result);
+ {{/parents}}{{#vars
+}} fromJson(jo.{{#propertyMap}}take{{/propertyMap
+ }}{{^propertyMap}}value{{/propertyMap}}("{{baseName}}"_ls), result.{{nameCamelCase}});
+{{/vars}}{{#propertyMap}} fromJson(jo, result.{{nameCamelCase}});
+{{/propertyMap}} }
+{{/out?}} };
+{{/model}}} // namespace QMatrixClient
{{/ models}}{{#responses}}{{#normalResponse?}}{{#allProperties?}}
class {{camelCaseOperationId}}Job::Private
{
@@ -109,12 +112,12 @@ BaseJob::Status {{camelCaseOperationId}}Job::parseReply(QNetworkReply* reply)
}{{/ producesNonJson?}}{{^producesNonJson?}}
BaseJob::Status {{camelCaseOperationId}}Job::parseJson(const QJsonDocument& data)
{
-{{#inlineResponse}} d->{{paramName}} = fromJson<{{dataType.name}}>(data);
+{{#inlineResponse}} fromJson(data, d->{{paramName}});
{{/inlineResponse}}{{^inlineResponse}} auto json = data.object();
{{#properties}}{{#required?}} if (!json.contains("{{baseName}}"_ls))
return { JsonParseError,
"The key '{{baseName}}' not found in the response" };
-{{/required?}} d->{{paramName}} = fromJson<{{dataType.name}}>(json.value("{{baseName}}"_ls));
+{{/required?}} fromJson(json.value("{{baseName}}"_ls), d->{{paramName}});
{{/properties}}{{/inlineResponse}} return Success;
}{{/ producesNonJson?}}
{{/allProperties?}}{{/normalResponse?}}{{/responses}}{{/operation}}{{/operations}}
diff --git a/lib/csapi/{{base}}.h.mustache b/lib/csapi/{{base}}.h.mustache
index 147c8607..a9c3a63a 100644
--- a/lib/csapi/{{base}}.h.mustache
+++ b/lib/csapi/{{base}}.h.mustache
@@ -18,14 +18,13 @@ namespace QMatrixClient
{{/vars}}{{#propertyMap}}{{#description}} /// {{_}}
{{/description}} {{>maybeOmittableType}} {{nameCamelCase}};
{{/propertyMap}} };
-{{#in?}}
- QJsonObject toJson(const {{name}}& pod);
-{{/in?}}{{#out?}}
- template <> struct FromJsonObject<{{name}}>
+ template <> struct JsonObjectConverter<{{name}}>
{
- {{name}} operator()({{^propertyMap}}const QJsonObject&{{/propertyMap}}{{#propertyMap}}QJsonObject{{/propertyMap}} jo) const;
- };
-{{/ out?}}{{/model}}
+ {{#in?}}static void dumpTo(QJsonObject& jo, const {{name}}& pod);
+ {{/in?}}{{#out?}}static void fillFrom({{^propertyMap}}const QJsonObject&{{/propertyMap
+ }}{{#propertyMap}}QJsonObject{{/propertyMap}} jo, {{name}}& pod);
+{{/out?}} };
+{{/model}}
{{/models}}{{#operations}} // Operations
{{# operation}}{{#summary}}
/// {{summary}}{{#description?}}{{!add a linebreak between summary and description if both exist}}
diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp
index 79ef769c..8ec3fe48 100644
--- a/lib/eventitem.cpp
+++ b/lib/eventitem.cpp
@@ -17,3 +17,29 @@
*/
#include "eventitem.h"
+
+#include "events/roommessageevent.h"
+#include "events/roomavatarevent.h"
+
+using namespace QMatrixClient;
+
+void PendingEventItem::setFileUploaded(const QUrl& remoteUrl)
+{
+ // TODO: eventually we might introduce hasFileContent to RoomEvent,
+ // and unify the code below.
+ if (auto* rme = getAs<RoomMessageEvent>())
+ {
+ Q_ASSERT(rme->hasFileContent());
+ rme->editContent([remoteUrl] (EventContent::TypedBase& ec) {
+ ec.fileInfo()->url = remoteUrl;
+ });
+ }
+ if (auto* rae = getAs<RoomAvatarEvent>())
+ {
+ Q_ASSERT(rae->content().fileInfo());
+ rae->editContent([remoteUrl] (EventContent::FileInfo& fi) {
+ fi.url = remoteUrl;
+ });
+ }
+ setStatus(EventStatus::FileUploaded);
+}
diff --git a/lib/eventitem.h b/lib/eventitem.h
index 5f1d10c9..36ed2132 100644
--- a/lib/eventitem.h
+++ b/lib/eventitem.h
@@ -33,16 +33,17 @@ namespace QMatrixClient
/** Special marks an event can assume
*
* This is used to hint at a special status of some events in UI.
- * Most status values are mutually exclusive.
+ * All values except Redacted and Hidden are mutually exclusive.
*/
enum Code {
Normal = 0x0, //< No special designation
Submitted = 0x01, //< The event has just been submitted for sending
- Departed = 0x02, //< The event has left the client
- ReachedServer = 0x03, //< The server has received the event
- SendingFailed = 0x04, //< The server could not receive the event
+ FileUploaded = 0x02, //< The file attached to the event has been uploaded to the server
+ Departed = 0x03, //< The event has left the client
+ ReachedServer = 0x04, //< The server has received the event
+ SendingFailed = 0x05, //< The server could not receive the event
Redacted = 0x08, //< The event has been redacted
- Hidden = 0x10, //< The event should be hidden
+ Hidden = 0x10, //< The event should not be shown in the timeline
};
Q_DECLARE_FLAGS(Status, Code)
Q_FLAG(Status)
@@ -70,6 +71,9 @@ namespace QMatrixClient
return std::exchange(evt, move(other));
}
+ protected:
+ template <typename EventT>
+ EventT* getAs() { return eventCast<EventT>(evt); }
private:
RoomEventPtr evt;
};
@@ -116,6 +120,7 @@ namespace QMatrixClient
QString annotation() const { return _annotation; }
void setDeparted() { setStatus(EventStatus::Departed); }
+ void setFileUploaded(const QUrl& remoteUrl);
void setReachedServer(const QString& eventId)
{
setStatus(EventStatus::ReachedServer);
diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h
index d1c1abc8..a99d85ac 100644
--- a/lib/events/accountdataevents.h
+++ b/lib/events/accountdataevents.h
@@ -36,37 +36,38 @@ namespace QMatrixClient
order_type order;
TagRecord (order_type order = none) : order(order) { }
- explicit TagRecord(const QJsonObject& jo)
+
+ bool operator<(const TagRecord& other) const
+ {
+ // Per The Spec, rooms with no order should be after those with order
+ return !order.omitted() &&
+ (other.order.omitted() || order.value() < other.order.value());
+ }
+ };
+
+ template <> struct JsonObjectConverter<TagRecord>
+ {
+ static void fillFrom(const QJsonObject& jo, TagRecord& rec)
{
// Parse a float both from JSON double and JSON string because
// libqmatrixclient previously used to use strings to store order.
const auto orderJv = jo.value("order"_ls);
if (orderJv.isDouble())
- order = fromJson<float>(orderJv);
- else if (orderJv.isString())
+ rec.order = fromJson<float>(orderJv);
+ if (orderJv.isString())
{
bool ok;
- order = orderJv.toString().toFloat(&ok);
+ rec.order = orderJv.toString().toFloat(&ok);
if (!ok)
- order = none;
+ rec.order = none;
}
}
-
- bool operator<(const TagRecord& other) const
+ static void dumpTo(QJsonObject& jo, const TagRecord& rec)
{
- // Per The Spec, rooms with no order should be after those with order
- return !order.omitted() &&
- (other.order.omitted() || order.value() < other.order.value());
+ addParam<IfNotEmpty>(jo, QStringLiteral("order"), rec.order);
}
};
- inline QJsonValue toJson(const TagRecord& rec)
- {
- QJsonObject o;
- addParam<IfNotEmpty>(o, QStringLiteral("order"), rec.order);
- return o;
- }
-
using TagsMap = QHash<QString, TagRecord>;
#define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \
diff --git a/lib/events/event.cpp b/lib/events/event.cpp
index fd6e3939..6505d89a 100644
--- a/lib/events/event.cpp
+++ b/lib/events/event.cpp
@@ -38,7 +38,8 @@ event_type_t EventTypeRegistry::initializeTypeId(event_mtype_t matrixTypeId)
QString EventTypeRegistry::getMatrixType(event_type_t typeId)
{
- return typeId < get().eventTypes.size() ? get().eventTypes[typeId] : "";
+ return typeId < get().eventTypes.size()
+ ? get().eventTypes[typeId] : QString();
}
Event::Event(Type type, const QJsonObject& json)
@@ -77,3 +78,8 @@ const QJsonObject Event::unsignedJson() const
{
return fullJson()[UnsignedKeyL].toObject();
}
+
+void Event::dumpTo(QDebug dbg) const
+{
+ dbg << QJsonDocument(contentJson()).toJson(QJsonDocument::Compact);
+}
diff --git a/lib/events/event.h b/lib/events/event.h
index e0d83976..b7bbd83e 100644
--- a/lib/events/event.h
+++ b/lib/events/event.h
@@ -32,19 +32,23 @@ namespace QMatrixClient
template <typename EventT>
using event_ptr_tt = std::unique_ptr<EventT>;
+ /// Unwrap a plain pointer from a smart pointer
template <typename EventT>
- inline EventT* rawPtr(const event_ptr_tt<EventT>& ptr) // unwrap
+ inline EventT* rawPtr(const event_ptr_tt<EventT>& ptr)
{
return ptr.get();
}
+ /// Unwrap a plain pointer and downcast it to the specified type
template <typename TargetEventT, typename EventT>
inline TargetEventT* weakPtrCast(const event_ptr_tt<EventT>& ptr)
{
return static_cast<TargetEventT*>(rawPtr(ptr));
}
+ /// Re-wrap a smart pointer to base into a smart pointer to derived
template <typename TargetT, typename SourceT>
+ [[deprecated("Consider using eventCast() or visit() instead")]]
inline event_ptr_tt<TargetT> ptrCast(event_ptr_tt<SourceT>&& ptr)
{
return unique_ptr_cast<TargetT>(ptr);
@@ -208,6 +212,9 @@ namespace QMatrixClient
template <typename EventT>
inline auto registerEventType()
{
+ // Initialise exactly once, even if this function is called twice for
+ // the same type (for whatever reason - you never know the ways of
+ // static initialisation is done).
static const auto _ = setupFactory<EventT>();
return _; // Only to facilitate usage in static initialisation
}
@@ -257,8 +264,18 @@ namespace QMatrixClient
return fromJson<T>(contentJson()[key]);
}
+ friend QDebug operator<<(QDebug dbg, const Event& e)
+ {
+ QDebugStateSaver _dss { dbg };
+ dbg.noquote().nospace()
+ << e.matrixType() << '(' << e.type() << "): ";
+ e.dumpTo(dbg);
+ return dbg;
+ }
+
virtual bool isStateEvent() const { return false; }
virtual bool isCallEvent() const { return false; }
+ virtual void dumpTo(QDebug dbg) const;
protected:
QJsonObject& editJson() { return _json; }
@@ -327,7 +344,8 @@ namespace QMatrixClient
-> decltype(static_cast<EventT*>(&*eptr))
{
Q_ASSERT(eptr);
- return is<EventT>(*eptr) ? static_cast<EventT*>(&*eptr) : nullptr;
+ return is<std::decay_t<EventT>>(*eptr)
+ ? static_cast<EventT*>(&*eptr) : nullptr;
}
// A single generic catch-all visitor
@@ -359,7 +377,7 @@ namespace QMatrixClient
visit(const BaseEventT& event, FnT&& visitor)
{
using event_type = fn_arg_t<FnT>;
- if (is<event_type>(event))
+ if (is<std::decay_t<event_type>>(event))
visitor(static_cast<event_type>(event));
}
@@ -373,7 +391,7 @@ namespace QMatrixClient
fn_return_t<FnT>&& defaultValue = {})
{
using event_type = fn_arg_t<FnT>;
- if (is<event_type>(event))
+ if (is<std::decay_t<event_type>>(event))
return visitor(static_cast<event_type>(event));
return std::forward<fn_return_t<FnT>>(defaultValue);
}
@@ -386,7 +404,7 @@ namespace QMatrixClient
FnTs&&... visitors)
{
using event_type1 = fn_arg_t<FnT1>;
- if (is<event_type1>(event))
+ if (is<std::decay_t<event_type1>>(event))
return visitor1(static_cast<event_type1&>(event));
return visit(event, std::forward<FnT2>(visitor2),
std::forward<FnTs>(visitors)...);
diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp
index a6b1c763..77f756cd 100644
--- a/lib/events/eventcontent.cpp
+++ b/lib/events/eventcontent.cpp
@@ -17,6 +17,8 @@
*/
#include "eventcontent.h"
+
+#include "converters.h"
#include "util.h"
#include <QtCore/QMimeDatabase>
@@ -30,7 +32,7 @@ QJsonObject Base::toJson() const
return o;
}
-FileInfo::FileInfo(const QUrl& u, int payloadSize, const QMimeType& mimeType,
+FileInfo::FileInfo(const QUrl& u, qint64 payloadSize, const QMimeType& mimeType,
const QString& originalFilename)
: mimeType(mimeType), url(u), payloadSize(payloadSize)
, originalName(originalFilename)
@@ -41,23 +43,31 @@ FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson,
: originalInfoJson(infoJson)
, mimeType(QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString()))
, url(u)
- , payloadSize(infoJson["size"_ls].toInt())
+ , payloadSize(fromJson<qint64>(infoJson["size"_ls]))
, originalName(originalFilename)
{
if (!mimeType.isValid())
mimeType = QMimeDatabase().mimeTypeForData(QByteArray());
}
+bool FileInfo::isValid() const
+{
+ return url.scheme() == "mxc"
+ && (url.authority() + url.path()).count('/') == 1;
+}
+
void FileInfo::fillInfoJson(QJsonObject* infoJson) const
{
Q_ASSERT(infoJson);
- infoJson->insert(QStringLiteral("size"), payloadSize);
- infoJson->insert(QStringLiteral("mimetype"), mimeType.name());
+ if (payloadSize != -1)
+ infoJson->insert(QStringLiteral("size"), payloadSize);
+ if (mimeType.isValid())
+ infoJson->insert(QStringLiteral("mimetype"), mimeType.name());
}
-ImageInfo::ImageInfo(const QUrl& u, int fileSize, QMimeType mimeType,
- const QSize& imageSize)
- : FileInfo(u, fileSize, mimeType), imageSize(imageSize)
+ImageInfo::ImageInfo(const QUrl& u, qint64 fileSize, QMimeType mimeType,
+ const QSize& imageSize, const QString& originalFilename)
+ : FileInfo(u, fileSize, mimeType, originalFilename), imageSize(imageSize)
{ }
ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson,
@@ -69,8 +79,10 @@ ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson,
void ImageInfo::fillInfoJson(QJsonObject* infoJson) const
{
FileInfo::fillInfoJson(infoJson);
- infoJson->insert(QStringLiteral("w"), imageSize.width());
- infoJson->insert(QStringLiteral("h"), imageSize.height());
+ if (imageSize.width() != -1)
+ infoJson->insert(QStringLiteral("w"), imageSize.width());
+ if (imageSize.height() != -1)
+ infoJson->insert(QStringLiteral("h"), imageSize.height());
}
Thumbnail::Thumbnail(const QJsonObject& infoJson)
@@ -80,7 +92,9 @@ Thumbnail::Thumbnail(const QJsonObject& infoJson)
void Thumbnail::fillInfoJson(QJsonObject* infoJson) const
{
- infoJson->insert(QStringLiteral("thumbnail_url"), url.toString());
- infoJson->insert(QStringLiteral("thumbnail_info"),
- toInfoJson<ImageInfo>(*this));
+ if (url.isValid())
+ infoJson->insert(QStringLiteral("thumbnail_url"), url.toString());
+ if (!imageSize.isEmpty())
+ infoJson->insert(QStringLiteral("thumbnail_info"),
+ toInfoJson<ImageInfo>(*this));
}
diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h
index 91d7a8c8..ab31a75d 100644
--- a/lib/events/eventcontent.h
+++ b/lib/events/eventcontent.h
@@ -43,9 +43,10 @@ namespace QMatrixClient
class Base
{
public:
- explicit Base (const QJsonObject& o = {}) : originalJson(o) { }
+ explicit Base (QJsonObject o = {}) : originalJson(std::move(o)) { }
virtual ~Base() = default;
+ // FIXME: make toJson() from converters.* work on base classes
QJsonObject toJson() const;
public:
@@ -87,12 +88,14 @@ namespace QMatrixClient
class FileInfo
{
public:
- explicit FileInfo(const QUrl& u, int payloadSize = -1,
+ explicit FileInfo(const QUrl& u, qint64 payloadSize = -1,
const QMimeType& mimeType = {},
const QString& originalFilename = {});
FileInfo(const QUrl& u, const QJsonObject& infoJson,
const QString& originalFilename = {});
+ bool isValid() const;
+
void fillInfoJson(QJsonObject* infoJson) const;
/**
@@ -108,7 +111,7 @@ namespace QMatrixClient
QJsonObject originalInfoJson;
QMimeType mimeType;
QUrl url;
- int payloadSize;
+ qint64 payloadSize;
QString originalName;
};
@@ -126,9 +129,10 @@ namespace QMatrixClient
class ImageInfo : public FileInfo
{
public:
- explicit ImageInfo(const QUrl& u, int fileSize = -1,
+ explicit ImageInfo(const QUrl& u, qint64 fileSize = -1,
QMimeType mimeType = {},
- const QSize& imageSize = {});
+ const QSize& imageSize = {},
+ const QString& originalFilename = {});
ImageInfo(const QUrl& u, const QJsonObject& infoJson,
const QString& originalFilename = {});
@@ -148,10 +152,10 @@ namespace QMatrixClient
class Thumbnail : public ImageInfo
{
public:
+ Thumbnail() : ImageInfo(QUrl()) { } // To allow empty thumbnails
Thumbnail(const QJsonObject& infoJson);
- Thumbnail(const ImageInfo& info)
- : ImageInfo(info)
- { }
+ Thumbnail(const ImageInfo& info) : ImageInfo(info) { }
+ using ImageInfo::ImageInfo;
/**
* Writes thumbnail information to "thumbnail_info" subobject
@@ -166,6 +170,7 @@ namespace QMatrixClient
explicit TypedBase(const QJsonObject& o = {}) : Base(o) { }
virtual QMimeType type() const = 0;
virtual const FileInfo* fileInfo() const { return nullptr; }
+ virtual FileInfo* fileInfo() { return nullptr; }
virtual const Thumbnail* thumbnailInfo() const { return nullptr; }
};
@@ -183,9 +188,7 @@ namespace QMatrixClient
class UrlBasedContent : public TypedBase, public InfoT
{
public:
- UrlBasedContent(QUrl url, InfoT&& info, QString filename = {})
- : InfoT(url, std::forward<InfoT>(info), filename)
- { }
+ using InfoT::InfoT;
explicit UrlBasedContent(const QJsonObject& json)
: TypedBase(json)
, InfoT(json["url"].toString(), json["info"].toObject(),
@@ -197,6 +200,7 @@ namespace QMatrixClient
QMimeType type() const override { return InfoT::mimeType; }
const FileInfo* fileInfo() const override { return this; }
+ FileInfo* fileInfo() override { return this; }
protected:
void fillJson(QJsonObject* json) const override
@@ -213,7 +217,7 @@ namespace QMatrixClient
class UrlWithThumbnailContent : public UrlBasedContent<InfoT>
{
public:
- // TODO: POD constructor
+ using UrlBasedContent<InfoT>::UrlBasedContent;
explicit UrlWithThumbnailContent(const QJsonObject& json)
: UrlBasedContent<InfoT>(json)
, thumbnail(InfoT::originalInfoJson)
diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h
index 3ee9a181..da663392 100644
--- a/lib/events/eventloader.h
+++ b/lib/events/eventloader.h
@@ -19,7 +19,6 @@
#pragma once
#include "stateevent.h"
-#include "converters.h"
namespace QMatrixClient {
namespace _impl {
@@ -58,11 +57,15 @@ namespace QMatrixClient {
matrixType);
}
- template <typename EventT> struct FromJsonObject<event_ptr_tt<EventT>>
+ template <typename EventT> struct JsonConverter<event_ptr_tt<EventT>>
{
- auto operator()(const QJsonObject& jo) const
+ static auto load(const QJsonValue& jv)
{
- return loadEvent<EventT>(jo);
+ return loadEvent<EventT>(jv.toObject());
+ }
+ static auto load(const QJsonDocument& jd)
+ {
+ return loadEvent<EventT>(jd.object());
}
};
} // namespace QMatrixClient
diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h
index 491861b1..a43d3a85 100644
--- a/lib/events/roomavatarevent.h
+++ b/lib/events/roomavatarevent.h
@@ -18,8 +18,7 @@
#pragma once
-#include "event.h"
-
+#include "stateevent.h"
#include "eventcontent.h"
namespace QMatrixClient
diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp
new file mode 100644
index 00000000..8fd0f1de
--- /dev/null
+++ b/lib/events/roomcreateevent.cpp
@@ -0,0 +1,45 @@
+/******************************************************************************
+* Copyright (C) 2019 QMatrixClient project
+*
+* 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 "roomcreateevent.h"
+
+using namespace QMatrixClient;
+
+bool RoomCreateEvent::isFederated() const
+{
+ return fromJson<bool>(contentJson()["m.federate"_ls]);
+}
+
+QString RoomCreateEvent::version() const
+{
+ return fromJson<QString>(contentJson()["room_version"_ls]);
+}
+
+RoomCreateEvent::Predecessor RoomCreateEvent::predecessor() const
+{
+ const auto predJson = contentJson()["predecessor"_ls].toObject();
+ return {
+ fromJson<QString>(predJson["room_id"_ls]),
+ fromJson<QString>(predJson["event_id"_ls])
+ };
+}
+
+bool RoomCreateEvent::isUpgrade() const
+{
+ return contentJson().contains("predecessor"_ls);
+}
diff --git a/lib/events/roomcreateevent.h b/lib/events/roomcreateevent.h
new file mode 100644
index 00000000..0a8f27cc
--- /dev/null
+++ b/lib/events/roomcreateevent.h
@@ -0,0 +1,49 @@
+/******************************************************************************
+* Copyright (C) 2019 QMatrixClient project
+*
+* 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 "stateevent.h"
+
+namespace QMatrixClient
+{
+ class RoomCreateEvent : public StateEventBase
+ {
+ public:
+ DEFINE_EVENT_TYPEID("m.room.create", RoomCreateEvent)
+
+ explicit RoomCreateEvent()
+ : StateEventBase(typeId(), matrixTypeId())
+ { }
+ explicit RoomCreateEvent(const QJsonObject& obj)
+ : StateEventBase(typeId(), obj)
+ { }
+
+ struct Predecessor
+ {
+ QString roomId;
+ QString eventId;
+ };
+
+ bool isFederated() const;
+ QString version() const;
+ Predecessor predecessor() const;
+ bool isUpgrade() const;
+ };
+ REGISTER_EVENT_TYPE(RoomCreateEvent)
+}
diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp
index 80d121de..3d03509f 100644
--- a/lib/events/roomevent.cpp
+++ b/lib/events/roomevent.cpp
@@ -42,10 +42,6 @@ RoomEvent::RoomEvent(Type type, const QJsonObject& json)
_redactedBecause = makeEvent<RedactionEvent>(redaction.toObject());
return;
}
-
- const auto& txnId = transactionId();
- if (!txnId.isEmpty())
- qCDebug(EVENTS) << "Event transactionId:" << txnId;
}
RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job
@@ -90,7 +86,6 @@ void RoomEvent::setTransactionId(const QString& txnId)
auto unsignedData = fullJson()[UnsignedKeyL].toObject();
unsignedData.insert(QStringLiteral("transaction_id"), txnId);
editJson().insert(UnsignedKey, unsignedData);
- qCDebug(EVENTS) << "New event transactionId:" << txnId;
Q_ASSERT(transactionId() == txnId);
}
diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp
index eaa3302c..6da76526 100644
--- a/lib/events/roommemberevent.cpp
+++ b/lib/events/roommemberevent.cpp
@@ -23,20 +23,17 @@
#include <array>
-using namespace QMatrixClient;
-
static const std::array<QString, 5> membershipStrings = { {
QStringLiteral("invite"), QStringLiteral("join"),
QStringLiteral("knock"), QStringLiteral("leave"),
QStringLiteral("ban")
} };
-namespace QMatrixClient
-{
+namespace QMatrixClient {
template <>
- struct FromJson<MembershipType>
+ struct JsonConverter<MembershipType>
{
- MembershipType operator()(const QJsonValue& jv) const
+ static MembershipType load(const QJsonValue& jv)
{
const auto& membershipString = jv.toString();
for (auto it = membershipStrings.begin();
@@ -48,13 +45,14 @@ namespace QMatrixClient
return MembershipType::Undefined;
}
};
-
}
+using namespace QMatrixClient;
+
MemberEventContent::MemberEventContent(const QJsonObject& json)
: membership(fromJson<MembershipType>(json["membership"_ls]))
, isDirect(json["is_direct"_ls].toBool())
- , displayName(json["displayname"_ls].toString())
+ , displayName(sanitized(json["displayname"_ls].toString()))
, avatarUrl(json["avatar_url"_ls].toString())
{ }
diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h
index db25d026..b8224033 100644
--- a/lib/events/roommemberevent.h
+++ b/lib/events/roommemberevent.h
@@ -29,13 +29,10 @@ namespace QMatrixClient
enum MembershipType : size_t { Invite = 0, Join, Knock, Leave, Ban,
Undefined };
- explicit MemberEventContent(MembershipType mt = MembershipType::Join)
+ explicit MemberEventContent(MembershipType mt = Join)
: membership(mt)
{ }
explicit MemberEventContent(const QJsonObject& json);
- explicit MemberEventContent(const QJsonValue& jv)
- : MemberEventContent(jv.toObject())
- { }
MembershipType membership;
bool isDirect = false;
@@ -60,11 +57,19 @@ namespace QMatrixClient
: StateEvent(typeId(), obj)
{ }
RoomMemberEvent(MemberEventContent&& c)
- : StateEvent(typeId(), matrixTypeId(), c.toJson())
+ : StateEvent(typeId(), matrixTypeId(), c)
{ }
- // This is a special constructor enabling RoomMemberEvent to be
- // a base class for more specific member events.
+ /// A special constructor to create unknown RoomMemberEvents
+ /**
+ * This is needed in order to use RoomMemberEvent as a "base event
+ * class" in cases like GetMembersByRoomJob when RoomMemberEvents
+ * (rather than RoomEvents or StateEvents) are resolved from JSON.
+ * For such cases loadEvent<> requires an underlying class to be
+ * constructible with unknownTypeId() instead of its genuine id.
+ * Don't use it directly.
+ * \sa GetMembersByRoomJob, loadEvent, unknownTypeId
+ */
RoomMemberEvent(Type type, const QJsonObject& fullJson)
: StateEvent(type, fullJson)
{ }
@@ -84,6 +89,18 @@ namespace QMatrixClient
private:
REGISTER_ENUM(MembershipType)
};
+
+ template <>
+ class EventFactory<RoomMemberEvent>
+ {
+ public:
+ static event_ptr_tt<RoomMemberEvent> make(const QJsonObject& json,
+ const QString&)
+ {
+ return makeEvent<RoomMemberEvent>(json);
+ }
+ };
+
REGISTER_EVENT_TYPE(RoomMemberEvent)
DEFINE_EVENTTYPE_ALIAS(RoomMember, RoomMemberEvent)
} // namespace QMatrixClient
diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp
index 1c5cf058..8f4e0ebc 100644
--- a/lib/events/roommessageevent.cpp
+++ b/lib/events/roommessageevent.cpp
@@ -21,18 +21,38 @@
#include "logging.h"
#include <QtCore/QMimeDatabase>
+#include <QtCore/QFileInfo>
+#include <QtGui/QImageReader>
+#include <QtMultimedia/QMediaResource>
using namespace QMatrixClient;
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 TextTypeKey = "m.text";
+static const auto NoticeTypeKey = "m.notice";
+
+static const auto HtmlContentTypeId = QStringLiteral("org.matrix.custom.html");
+
template <typename ContentT>
TypedBase* make(const QJsonObject& json)
{
return new ContentT(json);
}
+template <>
+TypedBase* make<TextContent>(const QJsonObject& json)
+{
+ return json.contains(FormattedBodyKey) || json.contains(RelatesToKey)
+ ? new TextContent(json) : nullptr;
+}
+
struct MsgTypeDesc
{
QString matrixType;
@@ -41,9 +61,9 @@ struct MsgTypeDesc
};
const std::vector<MsgTypeDesc> msgTypes =
- { { QStringLiteral("m.text"), MsgType::Text, make<TextContent> }
+ { { TextTypeKey, MsgType::Text, make<TextContent> }
, { QStringLiteral("m.emote"), MsgType::Emote, make<TextContent> }
- , { QStringLiteral("m.notice"), MsgType::Notice, make<TextContent> }
+ , { NoticeTypeKey, MsgType::Notice, make<TextContent> }
, { QStringLiteral("m.image"), MsgType::Image, make<ImageContent> }
, { QStringLiteral("m.file"), MsgType::File, make<FileContent> }
, { QStringLiteral("m.location"), MsgType::Location, make<LocationContent> }
@@ -71,22 +91,26 @@ MsgType jsonToMsgType(const QString& matrixType)
return MsgType::Unknown;
}
-inline QJsonObject toMsgJson(const QString& plainBody, const QString& jsonMsgType,
- TypedBase* content)
+QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody,
+ const QString& jsonMsgType, TypedBase* content)
{
auto json = content ? content->toJson() : QJsonObject();
+ if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey &&
+ json.contains(RelatesToKey))
+ {
+ json.remove(RelatesToKey);
+ qCWarning(EVENTS) << RelatesToKey << "cannot be used in" << jsonMsgType
+ << "messages; the relation has been stripped off";
+ }
json.insert(QStringLiteral("msgtype"), jsonMsgType);
json.insert(QStringLiteral("body"), plainBody);
return json;
}
-static const auto MsgTypeKey = "msgtype"_ls;
-static const auto BodyKey = "body"_ls;
-
RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
const QString& jsonMsgType, TypedBase* content)
: RoomEvent(typeId(), matrixTypeId(),
- toMsgJson(plainBody, jsonMsgType, content))
+ assembleContentJson(plainBody, jsonMsgType, content))
, _content(content)
{ }
@@ -95,6 +119,40 @@ RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
: RoomMessageEvent(plainBody, msgTypeToJson(msgType), content)
{ }
+TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile)
+{
+ auto filePath = file.absoluteFilePath();
+ auto localUrl = QUrl::fromLocalFile(filePath);
+ auto mimeType = QMimeDatabase().mimeTypeForFile(file);
+ if (!asGenericFile)
+ {
+ auto mimeTypeName = mimeType.name();
+ if (mimeTypeName.startsWith("image/"))
+ return new ImageContent(localUrl, file.size(), mimeType,
+ QImageReader(filePath).size(),
+ file.fileName());
+
+ // duration can only be obtained asynchronously and can only be reliably
+ // done by starting to play the file. Left for a future implementation.
+ if (mimeTypeName.startsWith("video/"))
+ return new VideoContent(localUrl, file.size(), mimeType,
+ QMediaResource(localUrl).resolution(),
+ file.fileName());
+
+ if (mimeTypeName.startsWith("audio/"))
+ return new AudioContent(localUrl, file.size(), mimeType,
+ file.fileName());
+ }
+ return new FileContent(localUrl, file.size(), mimeType, file.fileName());
+}
+
+RoomMessageEvent::RoomMessageEvent(const QString& plainBody,
+ const QFileInfo& file, bool asGenericFile)
+ : RoomMessageEvent(plainBody,
+ asGenericFile ? QStringLiteral("m.file") : rawMsgTypeForFile(file),
+ contentFromFile(file, asGenericFile))
+{ }
+
RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj)
: RoomEvent(typeId(), obj), _content(nullptr)
{
@@ -104,13 +162,17 @@ RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj)
if ( content.contains(MsgTypeKey) && content.contains(BodyKey) )
{
auto msgtype = content[MsgTypeKey].toString();
+ bool msgTypeFound = false;
for (const auto& mt: msgTypes)
if (mt.matrixType == msgtype)
+ {
_content.reset(mt.maker(content));
+ msgTypeFound = true;
+ }
- if (!_content)
+ if (!msgTypeFound)
{
- qCWarning(EVENTS) << "RoomMessageEvent: couldn't load content,"
+ qCWarning(EVENTS) << "RoomMessageEvent: unknown msg_type,"
<< " full content dump follows";
qCWarning(EVENTS) << formatJson << content;
}
@@ -142,14 +204,13 @@ QMimeType RoomMessageEvent::mimeType() const
static const auto PlainTextMimeType =
QMimeDatabase().mimeTypeForName("text/plain");
return _content ? _content->type() : PlainTextMimeType;
- ;
}
bool RoomMessageEvent::hasTextContent() const
{
- return content() &&
+ return !content() ||
(msgtype() == MsgType::Text || msgtype() == MsgType::Emote ||
- msgtype() == MsgType::Notice); // FIXME: Unbind from specific msgtypes
+ msgtype() == MsgType::Notice);
}
bool RoomMessageEvent::hasFileContent() const
@@ -162,10 +223,31 @@ bool RoomMessageEvent::hasThumbnail() const
return content() && content()->thumbnailInfo();
}
-TextContent::TextContent(const QString& text, const QString& contentType)
+QString rawMsgTypeForMimeType(const QMimeType& mimeType)
+{
+ auto name = mimeType.name();
+ return name.startsWith("image/") ? QStringLiteral("m.image") :
+ name.startsWith("video/") ? QStringLiteral("m.video") :
+ name.startsWith("audio/") ? QStringLiteral("m.audio") :
+ QStringLiteral("m.file");
+}
+
+QString RoomMessageEvent::rawMsgTypeForUrl(const QUrl& url)
+{
+ return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForUrl(url));
+}
+
+QString RoomMessageEvent::rawMsgTypeForFile(const QFileInfo& fi)
+{
+ return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForFile(fi));
+}
+
+TextContent::TextContent(const QString& text, const QString& contentType,
+ Omittable<RelatesTo> relatesTo)
: mimeType(QMimeDatabase().mimeTypeForName(contentType)), body(text)
+ , relatesTo(std::move(relatesTo))
{
- if (contentType == "org.matrix.custom.html")
+ if (contentType == HtmlContentTypeId)
mimeType = QMimeDatabase().mimeTypeForName("text/html");
}
@@ -177,16 +259,20 @@ TextContent::TextContent(const QJsonObject& json)
// Special-casing the custom matrix.org's (actually, Riot's) way
// of sending HTML messages.
- if (json["format"_ls].toString() == "org.matrix.custom.html")
+ if (json["format"_ls].toString() == HtmlContentTypeId)
{
mimeType = HtmlMimeType;
- body = json["formatted_body"_ls].toString();
+ body = json[FormattedBodyKey].toString();
} else {
// Falling back to plain text, as there's no standard way to describe
// rich text in messages.
mimeType = PlainTextMimeType;
body = json[BodyKey].toString();
}
+ 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
@@ -194,13 +280,16 @@ void TextContent::fillJson(QJsonObject* json) const
Q_ASSERT(json);
if (mimeType.inherits("text/html"))
{
- json->insert(QStringLiteral("format"),
- QStringLiteral("org.matrix.custom.html"));
+ json->insert(QStringLiteral("format"), HtmlContentTypeId);
json->insert(QStringLiteral("formatted_body"), body);
}
+ if (!relatesTo.omitted())
+ json->insert(QStringLiteral("m.relates_to"),
+ QJsonObject { { relatesTo->type, relatesTo->eventId } });
}
-LocationContent::LocationContent(const QString& geoUri, const ImageInfo& thumbnail)
+LocationContent::LocationContent(const QString& geoUri,
+ const Thumbnail& thumbnail)
: geoUri(geoUri), thumbnail(thumbnail)
{ }
diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h
index 4c29a93e..c2e075eb 100644
--- a/lib/events/roommessageevent.h
+++ b/lib/events/roommessageevent.h
@@ -21,6 +21,8 @@
#include "roomevent.h"
#include "eventcontent.h"
+class QFileInfo;
+
namespace QMatrixClient
{
namespace MessageEventContent = EventContent; // Back-compatibility
@@ -49,6 +51,9 @@ namespace QMatrixClient
explicit RoomMessageEvent(const QString& plainBody,
MsgType msgType = MsgType::Text,
EventContent::TypedBase* content = nullptr);
+ explicit RoomMessageEvent(const QString& plainBody,
+ const QFileInfo& file,
+ bool asGenericFile = false);
explicit RoomMessageEvent(const QJsonObject& obj);
MsgType msgtype() const;
@@ -56,14 +61,27 @@ namespace QMatrixClient
QString plainBody() const;
EventContent::TypedBase* content() const
{ return _content.data(); }
+ template <typename VisitorT>
+ void editContent(VisitorT visitor)
+ {
+ visitor(*_content);
+ editJson()[ContentKeyL] =
+ assembleContentJson(plainBody(), rawMsgtype(), content());
+ }
QMimeType mimeType() const;
bool hasTextContent() const;
bool hasFileContent() const;
bool hasThumbnail() const;
+ static QString rawMsgTypeForUrl(const QUrl& url);
+ static QString rawMsgTypeForFile(const QFileInfo& fi);
+
private:
QScopedPointer<EventContent::TypedBase> _content;
+ static QJsonObject assembleContentJson(const QString& plainBody,
+ const QString& jsonMsgType, EventContent::TypedBase* content);
+
REGISTER_ENUM(MsgType)
};
REGISTER_EVENT_TYPE(RoomMessageEvent)
@@ -74,6 +92,17 @@ namespace QMatrixClient
{
// Additional event content types
+ struct RelatesTo
+ {
+ static constexpr const char* ReplyTypeId() { return "m.in_reply_to"; }
+ QString type; // The only supported relation so far
+ QString eventId;
+ };
+ inline RelatesTo replyTo(QString eventId)
+ {
+ return { RelatesTo::ReplyTypeId(), std::move(eventId) };
+ }
+
/**
* Rich text content for m.text, m.emote, m.notice
*
@@ -83,13 +112,15 @@ namespace QMatrixClient
class TextContent: public TypedBase
{
public:
- TextContent(const QString& text, const QString& contentType);
+ TextContent(const QString& text, const QString& contentType,
+ Omittable<RelatesTo> relatesTo = none);
explicit TextContent(const QJsonObject& json);
QMimeType type() const override { return mimeType; }
QMimeType mimeType;
QString body;
+ Omittable<RelatesTo> relatesTo;
protected:
void fillJson(QJsonObject* json) const override;
@@ -112,7 +143,7 @@ namespace QMatrixClient
{
public:
LocationContent(const QString& geoUri,
- const ImageInfo& thumbnail);
+ const Thumbnail& thumbnail = {});
explicit LocationContent(const QJsonObject& json);
QMimeType type() const override;
@@ -132,6 +163,7 @@ namespace QMatrixClient
class PlayableContent : public ContentT
{
public:
+ using ContentT::ContentT;
PlayableContent(const QJsonObject& json)
: ContentT(json)
, duration(ContentT::originalInfoJson["duration"_ls].toInt())
diff --git a/lib/events/roomtombstoneevent.cpp b/lib/events/roomtombstoneevent.cpp
new file mode 100644
index 00000000..9c3bafd4
--- /dev/null
+++ b/lib/events/roomtombstoneevent.cpp
@@ -0,0 +1,31 @@
+/******************************************************************************
+* Copyright (C) 2019 QMatrixClient project
+*
+* 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 "roomtombstoneevent.h"
+
+using namespace QMatrixClient;
+
+QString RoomTombstoneEvent::serverMessage() const
+{
+ return fromJson<QString>(contentJson()["body"_ls]);
+}
+
+QString RoomTombstoneEvent::successorRoomId() const
+{
+ return fromJson<QString>(contentJson()["replacement_room"_ls]);
+}
diff --git a/lib/events/roomtombstoneevent.h b/lib/events/roomtombstoneevent.h
new file mode 100644
index 00000000..c7008ec4
--- /dev/null
+++ b/lib/events/roomtombstoneevent.h
@@ -0,0 +1,41 @@
+/******************************************************************************
+* Copyright (C) 2019 QMatrixClient project
+*
+* 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 "stateevent.h"
+
+namespace QMatrixClient
+{
+ class RoomTombstoneEvent : public StateEventBase
+ {
+ public:
+ DEFINE_EVENT_TYPEID("m.room.tombstone", RoomTombstoneEvent)
+
+ explicit RoomTombstoneEvent()
+ : StateEventBase(typeId(), matrixTypeId())
+ { }
+ explicit RoomTombstoneEvent(const QJsonObject& obj)
+ : StateEventBase(typeId(), obj)
+ { }
+
+ QString serverMessage() const;
+ QString successorRoomId() const;
+ };
+ REGISTER_EVENT_TYPE(RoomTombstoneEvent)
+}
diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h
index 56be947c..2c23d9ca 100644
--- a/lib/events/simplestateevents.h
+++ b/lib/events/simplestateevents.h
@@ -19,7 +19,6 @@
#pragma once
#include "stateevent.h"
-#include "eventcontent.h"
#include "converters.h"
@@ -28,7 +27,7 @@ namespace QMatrixClient
namespace EventContent
{
template <typename T>
- class SimpleContent: public Base
+ class SimpleContent
{
public:
using value_type = T;
@@ -39,41 +38,39 @@ namespace QMatrixClient
: value(std::forward<TT>(value)), key(std::move(keyName))
{ }
SimpleContent(const QJsonObject& json, QString keyName)
- : Base(json)
- , value(QMatrixClient::fromJson<T>(json[keyName]))
+ : value(fromJson<T>(json[keyName]))
, key(std::move(keyName))
{ }
+ QJsonObject toJson() const
+ {
+ return { { key, QMatrixClient::toJson(value) } };
+ }
public:
T value;
protected:
QString key;
-
- private:
- void fillJson(QJsonObject* json) const override
- {
- Q_ASSERT(json);
- json->insert(key, QMatrixClient::toJson(value));
- }
};
} // namespace EventContent
-#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \
- class _Name : public StateEvent<EventContent::SimpleContent<_ContentType>> \
+#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \
+ class _Name : public StateEvent<EventContent::SimpleContent<_ValueType>> \
{ \
public: \
- using content_type = _ContentType; \
+ using value_type = content_type::value_type; \
DEFINE_EVENT_TYPEID(_TypeId, _Name) \
- explicit _Name(const QJsonObject& obj) \
- : StateEvent(typeId(), obj, QStringLiteral(#_ContentKey)) \
- { } \
+ explicit _Name() : _Name(value_type()) { } \
template <typename T> \
explicit _Name(T&& value) \
: StateEvent(typeId(), matrixTypeId(), \
QStringLiteral(#_ContentKey), \
std::forward<T>(value)) \
{ } \
+ explicit _Name(QJsonObject obj) \
+ : StateEvent(typeId(), std::move(obj), \
+ QStringLiteral(#_ContentKey)) \
+ { } \
auto _ContentKey() const { return content().value; } \
}; \
REGISTER_EVENT_TYPE(_Name) \
diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp
index 877d0fae..a84f302b 100644
--- a/lib/events/stateevent.cpp
+++ b/lib/events/stateevent.cpp
@@ -20,17 +20,20 @@
using namespace QMatrixClient;
+// Aside from the normal factory to instantiate StateEventBase inheritors
+// StateEventBase itself can be instantiated if there's a state_key JSON key
+// but the event type is unknown.
[[gnu::unused]] static auto stateEventTypeInitialised =
RoomEvent::factory_t::addMethod(
[] (const QJsonObject& json, const QString& matrixType) -> StateEventPtr
{
- if (!json.contains("state_key"))
+ if (!json.contains("state_key"_ls))
return nullptr;
if (auto e = StateEventBase::factory_t::make(json, matrixType))
return e;
- return nullptr;
+ return makeEvent<StateEventBase>(unknownEventTypeId(), json);
});
bool StateEventBase::repeatsState() const
@@ -38,3 +41,18 @@ bool StateEventBase::repeatsState() const
const auto prevContentJson = unsignedJson().value(PrevContentKeyL);
return fullJson().value(ContentKeyL) == prevContentJson;
}
+
+QString StateEventBase::replacedState() const
+{
+ return unsignedJson().value("replaces_state"_ls).toString();
+}
+
+void StateEventBase::dumpTo(QDebug dbg) const
+{
+ if (!stateKey().isEmpty())
+ dbg << '<' << stateKey() << "> ";
+ if (unsignedJson().contains(PrevContentKeyL))
+ dbg << QJsonDocument(unsignedJson()[PrevContentKeyL].toObject())
+ .toJson(QJsonDocument::Compact) << " -> ";
+ RoomEvent::dumpTo(dbg);
+}
diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h
index 6032132e..3f54f7bf 100644
--- a/lib/events/stateevent.h
+++ b/lib/events/stateevent.h
@@ -30,11 +30,24 @@ namespace QMatrixClient {
~StateEventBase() override = default;
bool isStateEvent() const override { return true; }
+ QString replacedState() const;
+ void dumpTo(QDebug dbg) const override;
+
virtual bool repeatsState() const;
};
using StateEventPtr = event_ptr_tt<StateEventBase>;
using StateEvents = EventsArray<StateEventBase>;
+ template <>
+ inline bool is<StateEventBase>(const Event& e) { return e.isStateEvent(); }
+
+ /**
+ * A combination of event type and state key uniquely identifies a piece
+ * of state in Matrix.
+ * \sa https://matrix.org/docs/spec/client_server/unstable.html#types-of-room-events
+ */
+ using StateEventKey = QPair<QString, QString>;
+
template <typename ContentT>
struct Prev
{
@@ -78,6 +91,12 @@ namespace QMatrixClient {
}
const ContentT& content() const { return _content; }
+ template <typename VisitorT>
+ void editContent(VisitorT&& visitor)
+ {
+ visitor(_content);
+ editJson()[ContentKeyL] = _content.toJson();
+ }
[[deprecated("Use prevContent instead")]]
const ContentT* prev_content() const { return prevContent(); }
const ContentT* prevContent() const
@@ -85,8 +104,18 @@ namespace QMatrixClient {
QString prevSenderId() const
{ return _prev ? _prev->senderId : QString(); }
- protected:
+ private:
ContentT _content;
std::unique_ptr<Prev<ContentT>> _prev;
};
} // namespace QMatrixClient
+
+namespace std {
+ template <> struct hash<QMatrixClient::StateEventKey>
+ {
+ size_t operator()(const QMatrixClient::StateEventKey& k) const Q_DECL_NOEXCEPT
+ {
+ return qHash(k);
+ }
+ };
+}
diff --git a/lib/identity/definitions/request_email_validation.cpp b/lib/identity/definitions/request_email_validation.cpp
index 95088bcb..47463a8b 100644
--- a/lib/identity/definitions/request_email_validation.cpp
+++ b/lib/identity/definitions/request_email_validation.cpp
@@ -6,28 +6,21 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const RequestEmailValidation& pod)
+void JsonObjectConverter<RequestEmailValidation>::dumpTo(
+ QJsonObject& jo, const RequestEmailValidation& pod)
{
- QJsonObject jo;
addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret);
addParam<>(jo, QStringLiteral("email"), pod.email);
addParam<>(jo, QStringLiteral("send_attempt"), pod.sendAttempt);
addParam<IfNotEmpty>(jo, QStringLiteral("next_link"), pod.nextLink);
- return jo;
}
-RequestEmailValidation FromJsonObject<RequestEmailValidation>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<RequestEmailValidation>::fillFrom(
+ const QJsonObject& jo, RequestEmailValidation& result)
{
- RequestEmailValidation result;
- result.clientSecret =
- fromJson<QString>(jo.value("client_secret"_ls));
- result.email =
- fromJson<QString>(jo.value("email"_ls));
- result.sendAttempt =
- fromJson<int>(jo.value("send_attempt"_ls));
- result.nextLink =
- fromJson<QString>(jo.value("next_link"_ls));
-
- return result;
+ fromJson(jo.value("client_secret"_ls), result.clientSecret);
+ fromJson(jo.value("email"_ls), result.email);
+ fromJson(jo.value("send_attempt"_ls), result.sendAttempt);
+ fromJson(jo.value("next_link"_ls), result.nextLink);
}
diff --git a/lib/identity/definitions/request_email_validation.h b/lib/identity/definitions/request_email_validation.h
index 3e72275f..eb7d8ed6 100644
--- a/lib/identity/definitions/request_email_validation.h
+++ b/lib/identity/definitions/request_email_validation.h
@@ -33,12 +33,10 @@ namespace QMatrixClient
/// server will redirect the user to this URL.
QString nextLink;
};
-
- QJsonObject toJson(const RequestEmailValidation& pod);
-
- template <> struct FromJsonObject<RequestEmailValidation>
+ template <> struct JsonObjectConverter<RequestEmailValidation>
{
- RequestEmailValidation operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const RequestEmailValidation& pod);
+ static void fillFrom(const QJsonObject& jo, RequestEmailValidation& pod);
};
} // namespace QMatrixClient
diff --git a/lib/identity/definitions/request_msisdn_validation.cpp b/lib/identity/definitions/request_msisdn_validation.cpp
index 125baa9c..a123d326 100644
--- a/lib/identity/definitions/request_msisdn_validation.cpp
+++ b/lib/identity/definitions/request_msisdn_validation.cpp
@@ -6,31 +6,23 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const RequestMsisdnValidation& pod)
+void JsonObjectConverter<RequestMsisdnValidation>::dumpTo(
+ QJsonObject& jo, const RequestMsisdnValidation& pod)
{
- QJsonObject jo;
addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret);
addParam<>(jo, QStringLiteral("country"), pod.country);
addParam<>(jo, QStringLiteral("phone_number"), pod.phoneNumber);
addParam<>(jo, QStringLiteral("send_attempt"), pod.sendAttempt);
addParam<IfNotEmpty>(jo, QStringLiteral("next_link"), pod.nextLink);
- return jo;
}
-RequestMsisdnValidation FromJsonObject<RequestMsisdnValidation>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<RequestMsisdnValidation>::fillFrom(
+ const QJsonObject& jo, RequestMsisdnValidation& result)
{
- RequestMsisdnValidation result;
- result.clientSecret =
- fromJson<QString>(jo.value("client_secret"_ls));
- result.country =
- fromJson<QString>(jo.value("country"_ls));
- result.phoneNumber =
- fromJson<QString>(jo.value("phone_number"_ls));
- result.sendAttempt =
- fromJson<int>(jo.value("send_attempt"_ls));
- result.nextLink =
- fromJson<QString>(jo.value("next_link"_ls));
-
- return result;
+ fromJson(jo.value("client_secret"_ls), result.clientSecret);
+ fromJson(jo.value("country"_ls), result.country);
+ fromJson(jo.value("phone_number"_ls), result.phoneNumber);
+ fromJson(jo.value("send_attempt"_ls), result.sendAttempt);
+ fromJson(jo.value("next_link"_ls), result.nextLink);
}
diff --git a/lib/identity/definitions/request_msisdn_validation.h b/lib/identity/definitions/request_msisdn_validation.h
index 77bea2bc..b48ed6d5 100644
--- a/lib/identity/definitions/request_msisdn_validation.h
+++ b/lib/identity/definitions/request_msisdn_validation.h
@@ -36,12 +36,10 @@ namespace QMatrixClient
/// server will redirect the user to this URL.
QString nextLink;
};
-
- QJsonObject toJson(const RequestMsisdnValidation& pod);
-
- template <> struct FromJsonObject<RequestMsisdnValidation>
+ template <> struct JsonObjectConverter<RequestMsisdnValidation>
{
- RequestMsisdnValidation operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const RequestMsisdnValidation& pod);
+ static void fillFrom(const QJsonObject& jo, RequestMsisdnValidation& pod);
};
} // namespace QMatrixClient
diff --git a/lib/identity/definitions/sid.cpp b/lib/identity/definitions/sid.cpp
index 443dbedf..1ba4b3b5 100644
--- a/lib/identity/definitions/sid.cpp
+++ b/lib/identity/definitions/sid.cpp
@@ -6,19 +6,15 @@
using namespace QMatrixClient;
-QJsonObject QMatrixClient::toJson(const Sid& pod)
+void JsonObjectConverter<Sid>::dumpTo(
+ QJsonObject& jo, const Sid& pod)
{
- QJsonObject jo;
addParam<>(jo, QStringLiteral("sid"), pod.sid);
- return jo;
}
-Sid FromJsonObject<Sid>::operator()(const QJsonObject& jo) const
+void JsonObjectConverter<Sid>::fillFrom(
+ const QJsonObject& jo, Sid& result)
{
- Sid result;
- result.sid =
- fromJson<QString>(jo.value("sid"_ls));
-
- return result;
+ fromJson(jo.value("sid"_ls), result.sid);
}
diff --git a/lib/identity/definitions/sid.h b/lib/identity/definitions/sid.h
index eae60c47..ac8c4130 100644
--- a/lib/identity/definitions/sid.h
+++ b/lib/identity/definitions/sid.h
@@ -19,12 +19,10 @@ namespace QMatrixClient
/// must not be empty.
QString sid;
};
-
- QJsonObject toJson(const Sid& pod);
-
- template <> struct FromJsonObject<Sid>
+ template <> struct JsonObjectConverter<Sid>
{
- Sid operator()(const QJsonObject& jo) const;
+ static void dumpTo(QJsonObject& jo, const Sid& pod);
+ static void fillFrom(const QJsonObject& jo, Sid& pod);
};
} // namespace QMatrixClient
diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp
index b21173ae..0d9b9f10 100644
--- a/lib/jobs/basejob.cpp
+++ b/lib/jobs/basejob.cpp
@@ -186,7 +186,7 @@ QUrl BaseJob::makeRequestUrl(QUrl baseUrl,
if (!pathBase.endsWith('/') && !path.startsWith('/'))
pathBase.push_back('/');
- baseUrl.setPath( pathBase + path );
+ baseUrl.setPath(pathBase + path, QUrl::TolerantMode);
baseUrl.setQuery(query);
return baseUrl;
}
@@ -325,9 +325,16 @@ void BaseJob::gotReply()
d->status.code = UserConsentRequiredError;
d->errorUrl = json.value("consent_uri"_ls).toString();
}
- else if (!json.isEmpty()) // Not localisable on the client side
- setStatus(IncorrectRequestError,
- json.value("error"_ls).toString());
+ else if (errCode == "M_UNSUPPORTED_ROOM_VERSION" ||
+ errCode == "M_INCOMPATIBLE_ROOM_VERSION")
+ {
+ d->status.code = UnsupportedRoomVersionError;
+ if (json.contains("room_version"))
+ d->status.message =
+ tr("Requested room version: %1")
+ .arg(json.value("room_version").toString());
+ } else if (!json.isEmpty()) // Not localisable on the client side
+ setStatus(d->status.code, json.value("error"_ls).toString());
}
}
@@ -422,12 +429,12 @@ BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const
BaseJob::Status BaseJob::parseReply(QNetworkReply* reply)
{
d->rawResponse = reply->readAll();
- QJsonParseError error;
+ QJsonParseError error { 0, QJsonParseError::MissingObject };
const auto& json = QJsonDocument::fromJson(d->rawResponse, &error);
if( error.error == QJsonParseError::NoError )
return parseJson(json);
- else
- return { IncorrectResponseError, error.errorString() };
+
+ return { IncorrectResponseError, error.errorString() };
}
BaseJob::Status BaseJob::parseJson(const QJsonDocument&)
@@ -519,8 +526,19 @@ BaseJob::Status BaseJob::status() const
QByteArray BaseJob::rawData(int bytesAtMost) const
{
- return bytesAtMost > 0 && d->rawResponse.size() > bytesAtMost ?
- d->rawResponse.left(bytesAtMost) + "...(truncated)" : d->rawResponse;
+ return bytesAtMost > 0 && d->rawResponse.size() > bytesAtMost
+ ? d->rawResponse.left(bytesAtMost) : d->rawResponse;
+}
+
+QString BaseJob::rawDataSample(int bytesAtMost) const
+{
+ auto data = rawData(bytesAtMost);
+ Q_ASSERT(data.size() <= d->rawResponse.size());
+ return data.size() == d->rawResponse.size()
+ ? data : data + tr("...(truncated, %Ln bytes in total)",
+ "Comes after trimmed raw network response",
+ d->rawResponse.size());
+
}
QString BaseJob::statusCaption() const
@@ -557,6 +575,8 @@ QString BaseJob::statusCaption() const
return tr("Network authentication required");
case UserConsentRequiredError:
return tr("User consent required");
+ case UnsupportedRoomVersionError:
+ return tr("The server does not support the needed room version");
default:
return tr("Request failed");
}
@@ -579,10 +599,25 @@ QUrl BaseJob::errorUrl() const
void BaseJob::setStatus(Status s)
{
+ // The crash that led to this code has been reported in
+ // https://github.com/QMatrixClient/Quaternion/issues/566 - basically,
+ // when cleaning up childrent of a deleted Connection, there's a chance
+ // of pending jobs being abandoned, calling setStatus(Abandoned).
+ // There's nothing wrong with this; however, the safety check for
+ // cleartext access tokens below uses d->connection - which is a dangling
+ // pointer.
+ // To alleviate that, a stricter condition is applied, that for Abandoned
+ // and to-be-Abandoned jobs the status message will be disregarded entirely.
+ // For 0.6 we might rectify the situation by making d->connection
+ // a QPointer<> (and derive ConnectionData from QObject, respectively).
+ if (d->status.code == Abandoned || s.code == Abandoned)
+ s.message.clear();
+
if (d->status == s)
return;
- if (!d->connection->accessToken().isEmpty())
+ if (!s.message.isEmpty()
+ && d->connection && !d->connection->accessToken().isEmpty())
s.message.replace(d->connection->accessToken(), "(REDACTED)");
if (!s.good())
qCWarning(d->logCat) << this << "status" << s;
@@ -597,9 +632,8 @@ void BaseJob::setStatus(int code, QString message)
void BaseJob::abandon()
{
- beforeAbandon(d->reply.data());
+ beforeAbandon(d->reply ? d->reply.data() : nullptr);
setStatus(Abandoned);
- this->disconnect();
if (d->reply)
d->reply->disconnect(this);
emit finished(this);
diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h
index 4ef25ab8..4c1c7706 100644
--- a/lib/jobs/basejob.h
+++ b/lib/jobs/basejob.h
@@ -64,6 +64,7 @@ namespace QMatrixClient
, IncorrectResponseError
, TooManyRequestsError
, RequestNotImplementedError
+ , UnsupportedRoomVersionError
, NetworkAuthRequiredError
, UserConsentRequiredError
, UserDefinedError = 200
@@ -138,8 +139,20 @@ namespace QMatrixClient
Status status() const;
/** Short human-friendly message on the job status */
QString statusCaption() const;
- /** Raw response body as received from the server */
+ /** Get raw response body as received from the server
+ * \param bytesAtMost return this number of leftmost bytes, or -1
+ * to return the entire response
+ */
QByteArray rawData(int bytesAtMost = -1) const;
+ /** Get UI-friendly sample of raw data
+ *
+ * This is almost the same as rawData but appends the "truncated"
+ * suffix if not all data fit in bytesAtMost. This call is
+ * recommended to present a sample of raw data as "details" next to
+ * error messages. Note that the default \p bytesAtMost value is
+ * also tailored to UI cases.
+ */
+ QString rawDataSample(int bytesAtMost = 65535) const;
/** Error (more generally, status) code
* Equivalent to status().code
@@ -203,9 +216,9 @@ namespace QMatrixClient
*
* In general, to be notified of a job's completion, client code
* should connect to result(), success(), or failure()
- * rather than finished(). However if you store a list of jobs
- * and need to track their lifecycle, then you should connect to this
- * instead of result(), to avoid dangling pointers in your list.
+ * rather than finished(). However if you need to track the job's
+ * lifecycle you should connect to this instead of result();
+ * in particular, only this signal will be emitted on abandoning.
*
* @param job the job that emitted this signal
*
diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp
index 2bf9dd8f..672a7b2d 100644
--- a/lib/jobs/downloadfilejob.cpp
+++ b/lib/jobs/downloadfilejob.cpp
@@ -22,7 +22,8 @@ class DownloadFileJob::Private
QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri)
{
- return makeRequestUrl(baseUrl, mxcUri.authority(), mxcUri.path().mid(1));
+ return makeRequestUrl(
+ std::move(baseUrl), mxcUri.authority(), mxcUri.path().mid(1));
}
DownloadFileJob::DownloadFileJob(const QString& serverName,
@@ -31,7 +32,7 @@ DownloadFileJob::DownloadFileJob(const QString& serverName,
: GetContentJob(serverName, mediaId)
, d(localFilename.isEmpty() ? new Private : new Private(localFilename))
{
- setObjectName("DownloadFileJob");
+ setObjectName(QStringLiteral("DownloadFileJob"));
}
QString DownloadFileJob::targetFileName() const
diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp
index aeb49839..edb9b156 100644
--- a/lib/jobs/mediathumbnailjob.cpp
+++ b/lib/jobs/mediathumbnailjob.cpp
@@ -59,5 +59,5 @@ BaseJob::Status MediaThumbnailJob::parseReply(QNetworkReply* reply)
if( _thumbnail.loadFromData(data()->readAll()) )
return Success;
- return { IncorrectResponseError, "Could not read image data" };
+ return { IncorrectResponseError, QStringLiteral("Could not read image data") };
}
diff --git a/lib/jobs/syncjob.cpp b/lib/jobs/syncjob.cpp
index 6baf388e..84385b55 100644
--- a/lib/jobs/syncjob.cpp
+++ b/lib/jobs/syncjob.cpp
@@ -18,10 +18,6 @@
#include "syncjob.h"
-#include "events/eventloader.h"
-
-#include <QtCore/QElapsedTimer>
-
using namespace QMatrixClient;
static size_t jobId = 0;
@@ -46,111 +42,21 @@ SyncJob::SyncJob(const QString& since, const QString& filter, int timeout,
setMaxRetries(std::numeric_limits<int>::max());
}
-QString SyncData::nextBatch() const
-{
- return nextBatch_;
-}
-
-SyncDataList&& SyncData::takeRoomData()
-{
- return std::move(roomData);
-}
-
-Events&& SyncData::takePresenceData()
-{
- return std::move(presenceData);
-}
-
-Events&& SyncData::takeAccountData()
-{
- return std::move(accountData);
-}
-
-Events&&SyncData::takeToDeviceEvents()
-{
- return std::move(toDeviceEvents);
-}
-
-template <typename EventsArrayT, typename StrT>
-inline EventsArrayT load(const QJsonObject& batches, StrT keyName)
-{
- return fromJson<EventsArrayT>(batches[keyName].toObject().value("events"_ls));
-}
+SyncJob::SyncJob(const QString& since, const Filter& filter,
+ int timeout, const QString& presence)
+ : SyncJob(since,
+ QJsonDocument(toJson(filter)).toJson(QJsonDocument::Compact),
+ timeout, presence)
+{ }
BaseJob::Status SyncJob::parseJson(const QJsonDocument& data)
{
- return d.parseJson(data);
-}
-
-BaseJob::Status SyncData::parseJson(const QJsonDocument &data)
-{
- QElapsedTimer et; et.start();
-
- auto json = data.object();
- nextBatch_ = json.value("next_batch"_ls).toString();
- presenceData = load<Events>(json, "presence"_ls);
- accountData = load<Events>(json, "account_data"_ls);
- toDeviceEvents = load<Events>(json, "to_device"_ls);
+ d.parseJson(data.object());
+ if (d.unresolvedRooms().isEmpty())
+ return BaseJob::Success;
- auto rooms = json.value("rooms"_ls).toObject();
- JoinStates::Int ii = 1; // ii is used to make a JoinState value
- auto totalRooms = 0;
- auto totalEvents = 0;
- for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1)
- {
- const auto rs = rooms.value(JoinStateStrings[i]).toObject();
- // We have a Qt container on the right and an STL one on the left
- roomData.reserve(static_cast<size_t>(rs.size()));
- for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt)
- {
- roomData.emplace_back(roomIt.key(), JoinState(ii),
- roomIt.value().toObject());
- const auto& r = roomData.back();
- totalEvents += r.state.size() + r.ephemeral.size() +
- r.accountData.size() + r.timeline.size();
- }
- totalRooms += rs.size();
- }
- if (totalRooms > 9 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with"
- << totalRooms << "room(s),"
- << totalEvents << "event(s) in" << et;
- return BaseJob::Success;
+ qCCritical(MAIN).noquote() << "Incomplete sync response, missing rooms:"
+ << d.unresolvedRooms().join(',');
+ return BaseJob::IncorrectResponseError;
}
-const QString SyncRoomData::UnreadCountKey =
- QStringLiteral("x-qmatrixclient.unread_count");
-
-SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_,
- const QJsonObject& room_)
- : roomId(roomId_)
- , joinState(joinState_)
- , state(load<StateEvents>(room_,
- joinState == JoinState::Invite ? "invite_state"_ls : "state"_ls))
-{
- switch (joinState) {
- case JoinState::Join:
- ephemeral = load<Events>(room_, "ephemeral"_ls);
- FALLTHROUGH;
- case JoinState::Leave:
- {
- accountData = load<Events>(room_, "account_data"_ls);
- timeline = load<RoomEvents>(room_, "timeline"_ls);
- const auto timelineJson = room_.value("timeline"_ls).toObject();
- timelineLimited = timelineJson.value("limited"_ls).toBool();
- timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString();
-
- break;
- }
- default: /* nothing on top of state */;
- }
-
- const auto unreadJson = room_.value("unread_notifications"_ls).toObject();
- unreadCount = unreadJson.value(UnreadCountKey).toInt(-2);
- highlightCount = unreadJson.value("highlight_count"_ls).toInt();
- notificationCount = unreadJson.value("notification_count"_ls).toInt();
- if (highlightCount > 0 || notificationCount > 0)
- qCDebug(SYNCJOB) << "Room" << roomId_
- << "has highlights:" << highlightCount
- << "and notifications:" << notificationCount;
-}
diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h
index 6b9bedfa..036b25d0 100644
--- a/lib/jobs/syncjob.h
+++ b/lib/jobs/syncjob.h
@@ -20,62 +20,19 @@
#include "basejob.h"
-#include "joinstate.h"
-#include "events/stateevent.h"
-#include "util.h"
+#include "../syncdata.h"
+#include "../csapi/definitions/sync_filter.h"
namespace QMatrixClient
{
- class SyncRoomData
- {
- public:
- QString roomId;
- JoinState joinState;
- StateEvents state;
- RoomEvents timeline;
- Events ephemeral;
- Events accountData;
-
- bool timelineLimited;
- QString timelinePrevBatch;
- int unreadCount;
- int highlightCount;
- int notificationCount;
-
- SyncRoomData(const QString& roomId, JoinState joinState_,
- const QJsonObject& room_);
- SyncRoomData(SyncRoomData&&) = default;
- SyncRoomData& operator=(SyncRoomData&&) = default;
-
- static const QString UnreadCountKey;
- };
- // QVector cannot work with non-copiable objects, std::vector can.
- using SyncDataList = std::vector<SyncRoomData>;
-
- class SyncData
- {
- public:
- BaseJob::Status parseJson(const QJsonDocument &data);
- Events&& takePresenceData();
- Events&& takeAccountData();
- Events&& takeToDeviceEvents();
- SyncDataList&& takeRoomData();
- QString nextBatch() const;
-
- private:
- QString nextBatch_;
- Events presenceData;
- Events accountData;
- Events toDeviceEvents;
- SyncDataList roomData;
- };
-
class SyncJob: public BaseJob
{
public:
explicit SyncJob(const QString& since = {},
const QString& filter = {},
int timeout = -1, const QString& presence = {});
+ explicit SyncJob(const QString& since, const Filter& filter,
+ int timeout = -1, const QString& presence = {});
SyncData &&takeData() { return std::move(d); }
diff --git a/lib/joinstate.h b/lib/joinstate.h
index c172f576..379183f6 100644
--- a/lib/joinstate.h
+++ b/lib/joinstate.h
@@ -24,7 +24,7 @@
namespace QMatrixClient
{
- enum class JoinState
+ enum class JoinState : unsigned int
{
Join = 0x1,
Invite = 0x2,
diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp
index 89967a8a..7d9cb360 100644
--- a/lib/networkaccessmanager.cpp
+++ b/lib/networkaccessmanager.cpp
@@ -29,7 +29,8 @@ class NetworkAccessManager::Private
QList<QSslError> ignoredSslErrors;
};
-NetworkAccessManager::NetworkAccessManager(QObject* parent) : d(std::make_unique<Private>())
+NetworkAccessManager::NetworkAccessManager(QObject* parent)
+ : QNetworkAccessManager(parent), d(std::make_unique<Private>())
{ }
QList<QSslError> NetworkAccessManager::ignoredSslErrors() const
diff --git a/lib/networksettings.cpp b/lib/networksettings.cpp
index 48bd09f3..6ff2bc1f 100644
--- a/lib/networksettings.cpp
+++ b/lib/networksettings.cpp
@@ -27,5 +27,5 @@ void NetworkSettings::setupApplicationProxy() const
}
QMC_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, "proxy_type", QNetworkProxy::DefaultProxy, setProxyType)
-QMC_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", "", setProxyHostName)
+QMC_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", {}, setProxyHostName)
QMC_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, setProxyPort)
diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h
new file mode 100644
index 00000000..c2bde8df
--- /dev/null
+++ b/lib/qt_connection_util.h
@@ -0,0 +1,107 @@
+/******************************************************************************
+ * 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 "util.h"
+
+#include <QtCore/QPointer>
+
+namespace QMatrixClient {
+ namespace _impl {
+ template <typename SenderT, typename SignalT,
+ typename ContextT, typename... ArgTs>
+ inline QMetaObject::Connection connectUntil(
+ SenderT* sender, SignalT signal, ContextT* context,
+ std::function<bool(ArgTs...)> slot, Qt::ConnectionType connType)
+ {
+ // See https://bugreports.qt.io/browse/QTBUG-60339
+#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
+ auto pc = std::make_shared<QMetaObject::Connection>();
+#else
+ auto pc = std::make_unique<QMetaObject::Connection>();
+#endif
+ auto& c = *pc; // Resolve a reference before pc is moved to lambda
+ c = QObject::connect(sender, signal, context,
+ [pc=std::move(pc),slot] (ArgTs... args) {
+ Q_ASSERT(*pc); // If it's been triggered, it should exist
+ if (slot(std::forward<ArgTs>(args)...))
+ QObject::disconnect(*pc);
+ }, connType);
+ return c;
+ }
+ }
+
+ template <typename SenderT, typename SignalT,
+ typename ContextT, typename FunctorT>
+ inline auto connectUntil(SenderT* sender, SignalT signal, ContextT* context,
+ const FunctorT& slot,
+ Qt::ConnectionType connType = Qt::AutoConnection)
+ {
+ return _impl::connectUntil(sender, signal, context,
+ typename function_traits<FunctorT>::function_type(slot),
+ connType);
+ }
+
+ /** Create a single-shot connection that triggers on the signal and
+ * then self-disconnects
+ *
+ * Only supports DirectConnection type.
+ */
+ template <typename SenderT, typename SignalT,
+ typename ReceiverT, typename SlotT>
+ inline auto connectSingleShot(SenderT* sender, SignalT signal,
+ ReceiverT* receiver, SlotT slot)
+ {
+ QMetaObject::Connection connection;
+ connection = QObject::connect(sender, signal, receiver, slot,
+ Qt::DirectConnection);
+ Q_ASSERT(connection);
+ QObject::connect(sender, signal, receiver,
+ [connection] { QObject::disconnect(connection); },
+ Qt::DirectConnection);
+ return connection;
+ }
+
+ /** A guard pointer that disconnects an interested object upon destruction
+ * It's almost QPointer<> except that you have to initialise it with one
+ * more additional parameter - a pointer to a QObject that will be
+ * disconnected from signals of the underlying pointer upon the guard's
+ * destruction.
+ */
+ template <typename T>
+ class ConnectionsGuard : public QPointer<T>
+ {
+ public:
+ ConnectionsGuard(T* publisher, QObject* subscriber)
+ : QPointer<T>(publisher), subscriber(subscriber)
+ { }
+ ~ConnectionsGuard()
+ {
+ if (*this)
+ (*this)->disconnect(subscriber);
+ }
+ ConnectionsGuard(ConnectionsGuard&&) = default;
+ ConnectionsGuard& operator=(ConnectionsGuard&&) = default;
+ Q_DISABLE_COPY(ConnectionsGuard)
+ using QPointer<T>::operator=;
+
+ private:
+ QObject* subscriber;
+ };
+}
diff --git a/lib/room.cpp b/lib/room.cpp
index ea771f17..9e7ff8d2 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -25,11 +25,14 @@
#include "csapi/receipts.h"
#include "csapi/redaction.h"
#include "csapi/account-data.h"
-#include "csapi/message_pagination.h"
#include "csapi/room_state.h"
#include "csapi/room_send.h"
+#include "csapi/rooms.h"
#include "csapi/tags.h"
+#include "csapi/room_upgrades.h"
#include "events/simplestateevents.h"
+#include "events/roomcreateevent.h"
+#include "events/roomtombstoneevent.h"
#include "events/roomavatarevent.h"
#include "events/roommemberevent.h"
#include "events/typingevent.h"
@@ -46,13 +49,15 @@
#include "connection.h"
#include "user.h"
#include "converters.h"
+#include "syncdata.h"
#include <QtCore/QHash>
#include <QtCore/QStringBuilder> // for efficient string concats (operator%)
-#include <QtCore/QElapsedTimer>
#include <QtCore/QPointer>
#include <QtCore/QDir>
#include <QtCore/QTemporaryFile>
+#include <QtCore/QRegularExpression>
+#include <QtCore/QMimeDatabase>
#include <array>
#include <functional>
@@ -67,12 +72,6 @@ using std::llround;
enum EventsPlacement : int { Older = -1, Newer = 1 };
-// A workaround for MSVC 2015 that fails with "error C2440: 'return':
-// cannot convert from 'initializer list' to 'QMatrixClient::FileTransferInfo'"
-#if (defined(_MSC_VER) && _MSC_VER < 1910) || (defined(__GNUC__) && __GNUC__ <= 4)
-# define WORKAROUND_EXTENDED_INITIALIZER_LIST
-#endif
-
class Room::Private
{
public:
@@ -86,29 +85,27 @@ class Room::Private
Room* q;
- // This updates the room displayname field (which is the way a room
- // should be shown in the room list) It should be called whenever the
- // list of members or the room name (m.room.name) or canonical alias change.
- void updateDisplayname();
-
Connection* connection;
+ QString id;
+ JoinState joinState;
+ RoomSummary summary = { none, 0, none };
+ /// The state of the room at timeline position before-0
+ /// \sa timelineBase
+ std::unordered_map<StateEventKey, StateEventPtr> baseState;
+ /// The state of the room at timeline position after-maxTimelineIndex()
+ /// \sa Room::syncEdge
+ QHash<StateEventKey, const StateEventBase*> currentState;
Timeline timeline;
PendingEvents unsyncedEvents;
QHash<QString, TimelineItem::index_t> eventsIndex;
- QString id;
- QStringList aliases;
- QString canonicalAlias;
- QString name;
QString displayname;
- QString topic;
- QString encryptionAlgorithm;
Avatar avatar;
- JoinState joinState;
int highlightCount = 0;
int notificationCount = 0;
members_map_t membersMap;
QList<User*> usersTyping;
QMultiHash<QString, User*> eventIdReadUsers;
+ QList<User*> usersInvited;
QList<User*> membersLeft;
int unreadMessages = 0;
bool displayed = false;
@@ -120,18 +117,21 @@ class Room::Private
std::unordered_map<QString, EventPtr> accountData;
QString prevBatch;
QPointer<GetRoomEventsJob> eventsHistoryJob;
+ QPointer<GetMembersByRoomJob> allMembersJob;
struct FileTransferPrivateInfo
{
-#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST
FileTransferPrivateInfo() = default;
- FileTransferPrivateInfo(BaseJob* j, QString fileName)
- : job(j), localFileInfo(fileName)
+ FileTransferPrivateInfo(BaseJob* j, const QString& fileName,
+ bool isUploading = false)
+ : status(FileTransferInfo::Started), job(j)
+ , localFileInfo(fileName), isUpload(isUploading)
{ }
-#endif
+
+ FileTransferInfo::Status status = FileTransferInfo::None;
QPointer<BaseJob> job = nullptr;
QFileInfo localFileInfo { };
- FileTransferInfo::Status status = FileTransferInfo::Started;
+ bool isUpload = false;
qint64 progress = 0;
qint64 total = -1;
@@ -164,13 +164,37 @@ class Room::Private
const RoomMessageEvent* getEventWithFile(const QString& eventId) const;
QString fileNameToDownload(const RoomMessageEvent* event) const;
+ Changes setSummary(RoomSummary&& newSummary);
+
//void inviteUser(User* u); // We might get it at some point in time.
void insertMemberIntoMap(User* u);
- void renameMember(User* u, QString oldName);
+ void renameMember(User* u, const QString& oldName);
void removeMemberFromMap(const QString& username, User* u);
+ // This updates the room displayname field (which is the way a room
+ // should be shown in the room list); called whenever the list of
+ // members, the room name (m.room.name) or canonical alias change.
+ void updateDisplayname();
+ // This is used by updateDisplayname() but only calculates the new name
+ // without any updates.
+ QString calculateDisplayname() const;
+
+ /// A point in the timeline corresponding to baseState
+ rev_iter_t timelineBase() const { return q->findInTimeline(-1); }
+
void getPreviousContent(int limit = 10);
+ template <typename EventT>
+ const EventT* getCurrentState(const QString& stateKey = {}) const
+ {
+ static const EventT empty;
+ const auto* evt =
+ currentState.value({EventT::matrixTypeId(), stateKey}, &empty);
+ Q_ASSERT(evt->type() == EventT::typeId() &&
+ evt->matrixType() == EventT::matrixTypeId());
+ return static_cast<const EventT*>(evt);
+ }
+
bool isEventNotable(const TimelineItem& ti) const
{
return !ti->isRedacted() &&
@@ -178,7 +202,29 @@ class Room::Private
is<RoomMessageEvent>(*ti);
}
- void addNewMessageEvents(RoomEvents&& events);
+ template <typename EventArrayT>
+ Changes updateStateFrom(EventArrayT&& events)
+ {
+ Changes changes = NoChange;
+ if (!events.empty())
+ {
+ QElapsedTimer et; et.start();
+ 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);
+ }
+ if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
+ qCDebug(PROFILER) << "*** Room::Private::updateStateFrom():"
+ << events.size() << "event(s)," << et;
+ }
+ return changes;
+ }
+ Changes addNewMessageEvents(RoomEvents&& events);
void addHistoricalMessageEvents(RoomEvents&& events);
/** Move events into the timeline
@@ -190,20 +236,22 @@ class Room::Private
* @param placement - position and direction of insertion: Older for
* historical messages, Newer for new ones
*/
- Timeline::difference_type moveEventsToTimeline(RoomEventsRange events,
- EventsPlacement placement);
+ Timeline::size_type moveEventsToTimeline(RoomEventsRange events,
+ EventsPlacement placement);
/**
* Remove events from the passed container that are already in the timeline
*/
void dropDuplicateEvents(RoomEvents& events) const;
- void setLastReadEvent(User* u, QString eventId);
+ Changes setLastReadEvent(User* u, QString eventId);
void updateUnreadCount(rev_iter_t from, rev_iter_t to);
- void promoteReadMarker(User* u, rev_iter_t newMarker,
- bool force = false);
+ Changes promoteReadMarker(User* u, rev_iter_t newMarker,
+ bool force = false);
- void markMessagesAsRead(rev_iter_t upToMarker);
+ Changes markMessagesAsRead(rev_iter_t upToMarker);
+
+ void getAllMembers();
QString sendEvent(RoomEventPtr&& event);
@@ -213,17 +261,23 @@ class Room::Private
return sendEvent(makeEvent<EventT>(std::forward<ArgTs>(eventArgs)...));
}
+ RoomEvent* addAsPending(RoomEventPtr&& event);
+
QString doSendEvent(const RoomEvent* pEvent);
- PendingEvents::iterator findAsPending(const RoomEvent* rawEvtPtr);
- void onEventSendingFailure(const RoomEvent* pEvent,
- const QString& txnId, BaseJob* call = nullptr);
+ void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr);
template <typename EvT>
- auto requestSetState(const QString& stateKey, const EvT& event)
+ SetRoomStateWithKeyJob* requestSetState(const QString& stateKey,
+ const EvT& event)
{
- // TODO: Queue up state events sending (see #133).
- return connection->callApi<SetRoomStateWithKeyJob>(
+ if (q->successorId().isEmpty())
+ {
+ // TODO: Queue up state events sending (see #133).
+ return connection->callApi<SetRoomStateWithKeyJob>(
id, EvT::matrixTypeId(), stateKey, event.contentJson());
+ }
+ qCWarning(MAIN) << q << "has been upgraded, state won't be set";
+ return nullptr;
}
template <typename EvT>
@@ -246,8 +300,10 @@ class Room::Private
QJsonObject toJson() const;
private:
- QString calculateDisplayname() const;
- QString roomNameFromMemberNames(const QList<User*>& userlist) const;
+ using users_shortlist_t = std::array<User*, 3>;
+ template<typename ContT>
+ users_shortlist_t buildShortlist(const ContT& users) const;
+ users_shortlist_t buildShortlist(const QStringList& userIds) const;
bool isLocalUser(const User* u) const
{
@@ -262,9 +318,13 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState)
// See "Accessing the Public Class" section in
// https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/
d->q = this;
- connect(this, &Room::userAdded, this, &Room::memberListChanged);
- connect(this, &Room::userRemoved, this, &Room::memberListChanged);
- connect(this, &Room::memberRenamed, this, &Room::memberListChanged);
+ d->displayname = d->calculateDisplayname(); // Set initial "Empty room" name
+ connectUntil(connection, &Connection::loadedRoomState, this,
+ [this] (Room* r) {
+ if (this == r)
+ emit baseStateLoaded();
+ return this == r; // loadedRoomState fires only once per room
+ });
qCDebug(MAIN) << "New" << toCString(initialJoinState) << "Room:" << id;
}
@@ -278,6 +338,28 @@ const QString& Room::id() const
return d->id;
}
+QString Room::version() const
+{
+ const auto v = d->getCurrentState<RoomCreateEvent>()->version();
+ return v.isEmpty() ? QStringLiteral("1") : v;
+}
+
+bool Room::isUnstable() const
+{
+ return !connection()->loadingCapabilities() &&
+ !connection()->stableRoomVersions().contains(version());
+}
+
+QString Room::predecessorId() const
+{
+ return d->getCurrentState<RoomCreateEvent>()->predecessor().roomId;
+}
+
+QString Room::successorId() const
+{
+ return d->getCurrentState<RoomTombstoneEvent>()->successorRoomId();
+}
+
const Room::Timeline& Room::messageEvents() const
{
return d->timeline;
@@ -290,17 +372,17 @@ const Room::PendingEvents& Room::pendingEvents() const
QString Room::name() const
{
- return d->name;
+ return d->getCurrentState<RoomNameEvent>()->name();
}
QStringList Room::aliases() const
{
- return d->aliases;
+ return d->getCurrentState<RoomAliasesEvent>()->aliases();
}
QString Room::canonicalAlias() const
{
- return d->canonicalAlias;
+ return d->getCurrentState<RoomCanonicalAliasEvent>()->alias();
}
QString Room::displayName() const
@@ -308,9 +390,14 @@ QString Room::displayName() const
return d->displayname;
}
+void Room::refreshDisplayName()
+{
+ d->updateDisplayname();
+}
+
QString Room::topic() const
{
- return d->topic;
+ return d->getCurrentState<RoomTopicEvent>()->topic();
}
QString Room::avatarMediaId() const
@@ -323,6 +410,11 @@ QUrl Room::avatarUrl() const
return d->avatar.url();
}
+const Avatar& Room::avatarObject() const
+{
+ return d->avatar;
+}
+
QImage Room::avatar(int dimension)
{
return avatar(dimension, dimension);
@@ -368,14 +460,15 @@ void Room::setJoinState(JoinState state)
d->joinState = state;
qCDebug(MAIN) << "Room" << id() << "changed state: "
<< int(oldState) << "->" << int(state);
+ emit changed(Change::JoinStateChange);
emit joinStateChanged(oldState, state);
}
-void Room::Private::setLastReadEvent(User* u, QString eventId)
+Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId)
{
auto& storedId = lastReadEventIds[u];
if (storedId == eventId)
- return;
+ return Change::NoChange;
eventIdReadUsers.remove(storedId, u);
eventIdReadUsers.insert(eventId, u);
swap(storedId, eventId);
@@ -386,7 +479,9 @@ void Room::Private::setLastReadEvent(User* u, QString eventId)
if (storedId != serverReadMarker)
connection->callApi<PostReadMarkersJob>(id, storedId);
emit q->readMarkerMoved(eventId, storedId);
+ return Change::ReadMarkerChange;
}
+ return Change::NoChange;
}
void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to)
@@ -429,14 +524,15 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to)
}
}
-void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force)
+Room::Changes Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker,
+ bool force)
{
Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr");
Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend());
const auto prevMarker = q->readMarker(u);
if (!force && prevMarker <= newMarker) // Remember, we deal with reverse iterators
- return;
+ return Change::NoChange;
Q_ASSERT(newMarker < timeline.crend());
@@ -445,13 +541,13 @@ void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force)
auto eagerMarker = find_if(newMarker.base(), timeline.cend(),
[=](const TimelineItem& ti) { return ti->senderId() != u->id(); });
- setLastReadEvent(u, (*(eagerMarker - 1))->id());
+ auto changes = setLastReadEvent(u, (*(eagerMarker - 1))->id());
if (isLocalUser(u))
{
const auto oldUnreadCount = unreadMessages;
QElapsedTimer et; et.start();
- unreadMessages = count_if(eagerMarker, timeline.cend(),
- std::bind(&Room::Private::isEventNotable, this, _1));
+ unreadMessages = int(count_if(eagerMarker, timeline.cend(),
+ std::bind(&Room::Private::isEventNotable, this, _1)));
if (et.nsecsElapsed() > profilerMinNsecs() / 10)
qCDebug(PROFILER) << "Recounting unread messages took" << et;
@@ -469,14 +565,16 @@ void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force)
qCDebug(MAIN) << "Room" << displayname << "still has"
<< unreadMessages << "unread message(s)";
emit q->unreadMessagesChanged(q);
+ changes |= Change::UnreadNotifsChange;
}
}
+ return changes;
}
-void Room::Private::markMessagesAsRead(rev_iter_t upToMarker)
+Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker)
{
const auto prevMarker = q->readMarker();
- promoteReadMarker(q->localUser(), upToMarker);
+ auto changes = promoteReadMarker(q->localUser(), upToMarker);
if (prevMarker != upToMarker)
qCDebug(MAIN) << "Marked messages as read until" << *q->readMarker();
@@ -487,11 +585,12 @@ void Room::Private::markMessagesAsRead(rev_iter_t upToMarker)
{
if ((*upToMarker)->senderId() != q->localUser()->id())
{
- connection->callApi<PostReceiptJob>(id, "m.read",
- (*upToMarker)->id());
+ connection->callApi<PostReceiptJob>(id, QStringLiteral("m.read"),
+ QUrl::toPercentEncoding((*upToMarker)->id()));
break;
}
}
+ return changes;
}
void Room::markMessagesAsRead(QString uptoEventId)
@@ -505,6 +604,29 @@ void Room::markAllMessagesAsRead()
d->markMessagesAsRead(d->timeline.crbegin());
}
+bool Room::canSwitchVersions() const
+{
+ if (!successorId().isEmpty())
+ return false; // Noone can upgrade a room that's already upgraded
+
+ // TODO, #276: m.room.power_levels
+ const auto* plEvt =
+ d->currentState.value({QStringLiteral("m.room.power_levels"), {}});
+ if (!plEvt)
+ return true;
+
+ const auto plJson = plEvt->contentJson();
+ const auto currentUserLevel =
+ plJson.value("users"_ls).toObject()
+ .value(localUser()->id()).toInt(
+ plJson.value("users_default"_ls).toInt());
+ const auto tombstonePowerLevel =
+ plJson.value("events"_ls).toObject()
+ .value("m.room.tombstone"_ls).toInt(
+ plJson.value("state_default"_ls).toInt());
+ return currentUserLevel >= tombstonePowerLevel;
+}
+
bool Room::hasUnreadMessages() const
{
return unreadCount() >= 0;
@@ -515,11 +637,21 @@ int Room::unreadCount() const
return d->unreadMessages;
}
-Room::rev_iter_t Room::timelineEdge() const
+Room::rev_iter_t Room::historyEdge() const
{
return d->timeline.crend();
}
+Room::Timeline::const_iterator Room::syncEdge() const
+{
+ return d->timeline.cend();
+}
+
+Room::rev_iter_t Room::timelineEdge() const
+{
+ return historyEdge();
+}
+
TimelineItem::index_t Room::minTimelineIndex() const
{
return d->timeline.empty() ? 0 : d->timeline.front().index();
@@ -554,6 +686,44 @@ Room::rev_iter_t Room::findInTimeline(const QString& evtId) const
return timelineEdge();
}
+Room::PendingEvents::iterator Room::findPendingEvent(const QString& txnId)
+{
+ return std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(),
+ [txnId] (const auto& item) { return item->transactionId() == txnId; });
+}
+
+Room::PendingEvents::const_iterator
+Room::findPendingEvent(const QString& txnId) const
+{
+ return std::find_if(d->unsyncedEvents.cbegin(), d->unsyncedEvents.cend(),
+ [txnId] (const auto& item) { return item->transactionId() == txnId; });
+}
+
+void Room::Private::getAllMembers()
+{
+ // If already loaded or already loading, there's nothing to do here.
+ if (q->joinedCount() <= membersMap.size() || isJobRunning(allMembersJob))
+ return;
+
+ allMembersJob = connection->callApi<GetMembersByRoomJob>(
+ id, connection->nextBatchToken(), "join");
+ auto nextIndex = timeline.empty() ? 0 : timeline.back().index() + 1;
+ connect( allMembersJob, &BaseJob::success, q, [=] {
+ Q_ASSERT(timeline.empty() || nextIndex <= q->maxTimelineIndex() + 1);
+ auto roomChanges = updateStateFrom(allMembersJob->chunk());
+ // Replay member events that arrived after the point for which
+ // the full members list was requested.
+ if (!timeline.empty() )
+ for (auto it = q->findInTimeline(nextIndex).base();
+ it != timeline.cend(); ++it)
+ if (is<RoomMemberEvent>(**it))
+ roomChanges |= q->processStateEvent(**it);
+ if (roomChanges&MembersChange)
+ emit q->memberListChanged();
+ emit q->allMembersLoaded();
+ });
+}
+
bool Room::displayed() const
{
return d->displayed;
@@ -570,6 +740,7 @@ void Room::setDisplayed(bool displayed)
{
resetHighlightCount();
resetNotificationCount();
+ d->getAllMembers();
}
}
@@ -669,6 +840,14 @@ void Room::resetHighlightCount()
emit highlightCountChanged(this);
}
+void Room::switchVersion(QString newVersion)
+{
+ auto* job = connection()->callApi<UpgradeRoomJob>(id(), newVersion);
+ connect(job, &BaseJob::failure, this, [this,job] {
+ emit upgradeFailed(job->errorString());
+ });
+}
+
bool Room::hasAccountData(const QString& type) const
{
return d->accountData.find(type) != d->accountData.end();
@@ -768,7 +947,7 @@ void Room::Private::setTags(TagsMap newTags)
}
tags = move(newTags);
qCDebug(MAIN) << "Room" << q->objectName() << "is tagged with"
- << q->tagNames().join(", ");
+ << q->tagNames().join(QStringLiteral(", "));
emit q->tagsChanged();
}
@@ -839,7 +1018,7 @@ QString Room::Private::fileNameToDownload(const RoomMessageEvent* event) const
return fileName;
}
-QUrl Room::urlToThumbnail(const QString& eventId)
+QUrl Room::urlToThumbnail(const QString& eventId) const
{
if (auto* event = d->getEventWithFile(eventId))
if (event->hasThumbnail())
@@ -853,7 +1032,7 @@ QUrl Room::urlToThumbnail(const QString& eventId)
return {};
}
-QUrl Room::urlToDownload(const QString& eventId)
+QUrl Room::urlToDownload(const QString& eventId) const
{
if (auto* event = d->getEventWithFile(eventId))
{
@@ -865,7 +1044,7 @@ QUrl Room::urlToDownload(const QString& eventId)
return {};
}
-QString Room::fileNameToDownload(const QString& eventId)
+QString Room::fileNameToDownload(const QString& eventId) const
{
if (auto* event = d->getEventWithFile(eventId))
return d->fileNameToDownload(event);
@@ -890,7 +1069,7 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const
total = INT_MAX;
}
-#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST
+#ifdef BROKEN_INITIALIZER_LISTS
FileTransferInfo fti;
fti.status = infoIt->status;
fti.progress = int(progress);
@@ -899,13 +1078,28 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const
fti.localPath = QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath());
return fti;
#else
- return { infoIt->status, int(progress), int(total),
+ return { infoIt->status, infoIt->isUpload, int(progress), int(total),
QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()),
QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath())
};
#endif
}
+QUrl Room::fileSource(const QString& id) const
+{
+ auto url = urlToDownload(id);
+ if (url.isValid())
+ return url;
+
+ // No urlToDownload means it's a pending or completed upload.
+ auto infoIt = d->fileTransfers.find(id);
+ if (infoIt != d->fileTransfers.end())
+ return QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath());
+
+ qCWarning(MAIN) << "File source for identifier" << id << "not found";
+ return {};
+}
+
QString Room::prettyPrint(const QString& plainText) const
{
return QMatrixClient::prettyPrint(plainText);
@@ -947,7 +1141,41 @@ int Room::timelineSize() const
bool Room::usesEncryption() const
{
- return !d->encryptionAlgorithm.isEmpty();
+ return !d->getCurrentState<EncryptionEvent>()->algorithm().isEmpty();
+}
+
+int Room::joinedCount() const
+{
+ return d->summary.joinedMemberCount.omitted()
+ ? d->membersMap.size()
+ : d->summary.joinedMemberCount.value();
+}
+
+int Room::invitedCount() const
+{
+ // TODO: Store invited users in Room too
+ Q_ASSERT(!d->summary.invitedMemberCount.omitted());
+ return d->summary.invitedMemberCount.value();
+}
+
+int Room::totalMemberCount() const
+{
+ return joinedCount() + invitedCount();
+}
+
+GetRoomEventsJob* Room::eventsHistoryJob() const
+{
+ return d->eventsHistoryJob;
+}
+
+Room::Changes Room::Private::setSummary(RoomSummary&& newSummary)
+{
+ if (!summary.merge(newSummary))
+ return Change::NoChange;
+ qCDebug(MAIN).nospace().noquote()
+ << "Updated room summary for " << q->objectName() << ": " << summary;
+ emit q->memberListChanged();
+ return Change::SummaryChange;
}
void Room::Private::insertMemberIntoMap(User *u)
@@ -955,7 +1183,11 @@ void Room::Private::insertMemberIntoMap(User *u)
const auto userName = u->name(q);
// If there is exactly one namesake of the added user, signal member renaming
// for that other one because the two should be disambiguated now.
- auto namesakes = membersMap.values(userName);
+ const auto namesakes = membersMap.values(userName);
+
+ // Callers should check they are not adding an existing user once more.
+ Q_ASSERT(!namesakes.contains(u));
+
if (namesakes.size() == 1)
emit q->memberAboutToRename(namesakes.front(),
namesakes.front()->fullName(q));
@@ -964,7 +1196,7 @@ void Room::Private::insertMemberIntoMap(User *u)
emit q->memberRenamed(namesakes.front());
}
-void Room::Private::renameMember(User* u, QString oldName)
+void Room::Private::renameMember(User* u, const QString& oldName)
{
if (u->name(q) == oldName)
{
@@ -977,7 +1209,6 @@ void Room::Private::renameMember(User* u, QString oldName)
removeMemberFromMap(oldName, u);
insertMemberIntoMap(u);
}
- emit q->memberRenamed(u);
}
void Room::Private::removeMemberFromMap(const QString& username, User* u)
@@ -993,7 +1224,6 @@ void Room::Private::removeMemberFromMap(const QString& username, User* u)
membersMap.remove(username, u);
// If there was one namesake besides the removed user, signal member renaming
// for it because it doesn't need to be disambiguated anymore.
- // TODO: Think about left users.
if (namesake)
emit q->memberRenamed(namesake);
}
@@ -1003,13 +1233,14 @@ inline auto makeErrorStr(const Event& e, QByteArray msg)
return msg.append("; event dump follows:\n").append(e.originalJson());
}
-Room::Timeline::difference_type Room::Private::moveEventsToTimeline(
+Room::Timeline::size_type Room::Private::moveEventsToTimeline(
RoomEventsRange events, EventsPlacement placement)
{
Q_ASSERT(!events.empty());
// Historical messages arrive in newest-to-oldest order, so the process for
- // them is symmetric to the one for new messages.
- auto index = timeline.empty() ? -int(placement) :
+ // them is almost symmetric to the one for new messages. New messages get
+ // appended from index 0; old messages go backwards from index -1.
+ auto index = timeline.empty() ? -((placement+1)/2) /* 1 -> -1; -1 -> 0 */ :
placement == Older ? timeline.front().index() :
timeline.back().index();
auto baseIndex = index;
@@ -1030,7 +1261,7 @@ Room::Timeline::difference_type Room::Private::moveEventsToTimeline(
eventsIndex.insert(eId, index);
Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId);
}
- const auto insertedSize = (index - baseIndex) * int(placement);
+ const auto insertedSize = (index - baseIndex) * placement;
Q_ASSERT(insertedSize == int(events.size()));
return insertedSize;
}
@@ -1076,50 +1307,40 @@ QString Room::roomMembername(const QString& userId) const
return roomMembername(user(userId));
}
-void Room::updateData(SyncRoomData&& data)
+void Room::updateData(SyncRoomData&& data, bool fromCache)
{
if( d->prevBatch.isEmpty() )
d->prevBatch = data.timelinePrevBatch;
setJoinState(data.joinState);
+ Changes roomChanges = Change::NoChange;
QElapsedTimer et; et.start();
for (auto&& event: data.accountData)
- processAccountDataEvent(move(event));
+ roomChanges |= processAccountDataEvent(move(event));
- bool emitNamesChanged = false;
- if (!data.state.empty())
- {
- et.restart();
- for (const auto& e: data.state)
- emitNamesChanged |= processStateEvent(*e);
+ roomChanges |= d->updateStateFrom(data.state);
- if (data.state.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER) << "*** Room::processStateEvents():"
- << data.state.size() << "event(s)," << et;
- }
if (!data.timeline.empty())
{
et.restart();
- // State changes can arrive in a timeline event; so check those.
- for (const auto& e: data.timeline)
- emitNamesChanged |= processStateEvent(*e);
+ roomChanges |= d->addNewMessageEvents(move(data.timeline));
if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER) << "*** Room::processStateEvents(timeline):"
+ qCDebug(PROFILER) << "*** Room::addNewMessageEvents():"
<< data.timeline.size() << "event(s)," << et;
}
- if (emitNamesChanged)
+ if (roomChanges&TopicChange)
+ emit topicChanged();
+
+ if (roomChanges&NameChange)
emit namesChanged(this);
- d->updateDisplayname();
- if (!data.timeline.empty())
- {
- et.restart();
- d->addNewMessageEvents(move(data.timeline));
- if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << et;
- }
+ if (roomChanges&MembersChange)
+ emit memberListChanged();
+
+ roomChanges |= d->setSummary(move(data.summary));
+
for( auto&& ephemeralEvent: data.ephemeral )
- processEphemeralEvent(move(ephemeralEvent));
+ roomChanges |= processEphemeralEvent(move(ephemeralEvent));
// See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count
if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages)
@@ -1139,29 +1360,45 @@ void Room::updateData(SyncRoomData&& data)
d->notificationCount = data.notificationCount;
emit notificationCountChanged(this);
}
+ if (roomChanges != Change::NoChange)
+ {
+ d->updateDisplayname();
+ emit changed(roomChanges);
+ if (!fromCache)
+ connection()->saveRoomState(this);
+ }
}
-QString Room::Private::sendEvent(RoomEventPtr&& event)
+RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event)
{
if (event->transactionId().isEmpty())
event->setTransactionId(connection->generateTxnId());
auto* pEvent = rawPtr(event);
- emit q->pendingEventAboutToAdd();
+ emit q->pendingEventAboutToAdd(pEvent);
unsyncedEvents.emplace_back(move(event));
emit q->pendingEventAdded();
- return doSendEvent(pEvent);
+ return pEvent;
+}
+
+QString Room::Private::sendEvent(RoomEventPtr&& event)
+{
+ if (q->successorId().isEmpty())
+ return doSendEvent(addAsPending(std::move(event)));
+
+ qCWarning(MAIN) << q << "has been upgraded, event won't be sent";
+ return {};
}
QString Room::Private::doSendEvent(const RoomEvent* pEvent)
{
- auto txnId = pEvent->transactionId();
+ const auto txnId = pEvent->transactionId();
// TODO, #133: Enqueue the job rather than immediately trigger it.
if (auto call = connection->callApi<SendMessageJob>(BackgroundRequest,
id, pEvent->matrixType(), txnId, pEvent->contentJson()))
{
Room::connect(call, &BaseJob::started, q,
- [this,pEvent,txnId] {
- auto it = findAsPending(pEvent);
+ [this,txnId] {
+ auto it = q->findPendingEvent(txnId);
if (it == unsyncedEvents.end())
{
qWarning(EVENTS) << "Pending event for transaction" << txnId
@@ -1169,16 +1406,14 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent)
return;
}
it->setDeparted();
- emit q->pendingEventChanged(it - unsyncedEvents.begin());
+ emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
});
Room::connect(call, &BaseJob::failure, q,
- std::bind(&Room::Private::onEventSendingFailure,
- this, pEvent, txnId, call));
+ std::bind(&Room::Private::onEventSendingFailure, this, txnId, call));
Room::connect(call, &BaseJob::success, q,
- [this,call,pEvent,txnId] {
- // Find an event by the pointer saved in the lambda (the pointer
- // may be dangling by now but we can still search by it).
- auto it = findAsPending(pEvent);
+ [this,call,txnId] {
+ emit q->messageSent(txnId, call->eventId());
+ auto it = q->findPendingEvent(txnId);
if (it == unsyncedEvents.end())
{
qDebug(EVENTS) << "Pending event for transaction" << txnId
@@ -1187,26 +1422,16 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent)
}
it->setReachedServer(call->eventId());
- emit q->pendingEventChanged(it - unsyncedEvents.begin());
+ emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
});
} else
- onEventSendingFailure(pEvent, txnId);
+ onEventSendingFailure(txnId);
return txnId;
}
-Room::PendingEvents::iterator Room::Private::findAsPending(
- const RoomEvent* rawEvtPtr)
-{
- const auto comp =
- [rawEvtPtr] (const auto& pe) { return pe.event() == rawEvtPtr; };
-
- return std::find_if(unsyncedEvents.begin(), unsyncedEvents.end(), comp);
-}
-
-void Room::Private::onEventSendingFailure(const RoomEvent* pEvent,
- const QString& txnId, BaseJob* call)
+void Room::Private::onEventSendingFailure(const QString& txnId, BaseJob* call)
{
- auto it = findAsPending(pEvent);
+ auto it = q->findPendingEvent(txnId);
if (it == unsyncedEvents.end())
{
qCritical(EVENTS) << "Pending event for transaction" << txnId
@@ -1216,15 +1441,40 @@ void Room::Private::onEventSendingFailure(const RoomEvent* pEvent,
it->setSendingFailed(call
? call->statusCaption() % ": " % call->errorString()
: tr("The call could not be started"));
- emit q->pendingEventChanged(it - unsyncedEvents.begin());
+ emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
}
QString Room::retryMessage(const QString& txnId)
{
- auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(),
- [txnId] (const auto& evt) { return evt->transactionId() == txnId; });
+ const auto it = findPendingEvent(txnId);
Q_ASSERT(it != d->unsyncedEvents.end());
qDebug(EVENTS) << "Retrying transaction" << txnId;
+ const auto& transferIt = d->fileTransfers.find(txnId);
+ if (transferIt != d->fileTransfers.end())
+ {
+ Q_ASSERT(transferIt->isUpload);
+ if (transferIt->status == FileTransferInfo::Completed)
+ {
+ qCDebug(MAIN) << "File for transaction" << txnId
+ << "has already been uploaded, bypassing re-upload";
+ } else {
+ if (isJobRunning(transferIt->job))
+ {
+ qCDebug(MAIN) << "Abandoning the upload job for transaction"
+ << txnId << "and starting again";
+ transferIt->job->abandon();
+ emit fileTransferFailed(txnId, tr("File upload will be retried"));
+ }
+ uploadFile(txnId,
+ QUrl::fromLocalFile(transferIt->localFileInfo.absoluteFilePath()));
+ // FIXME: Content type is no more passed here but it should
+ }
+ }
+ if (it->deliveryStatus() == EventStatus::ReachedServer)
+ {
+ qCWarning(MAIN) << "The previous attempt has reached the server; two"
+ " events are likely to be in the timeline after retry";
+ }
it->resetStatus();
return d->doSendEvent(it->event());
}
@@ -1235,7 +1485,22 @@ void Room::discardMessage(const QString& txnId)
[txnId] (const auto& evt) { return evt->transactionId() == txnId; });
Q_ASSERT(it != d->unsyncedEvents.end());
qDebug(EVENTS) << "Discarding transaction" << txnId;
- emit pendingEventAboutToDiscard(it - d->unsyncedEvents.begin());
+ const auto& transferIt = d->fileTransfers.find(txnId);
+ if (transferIt != d->fileTransfers.end())
+ {
+ Q_ASSERT(transferIt->isUpload);
+ if (isJobRunning(transferIt->job))
+ {
+ transferIt->status = FileTransferInfo::Cancelled;
+ transferIt->job->abandon();
+ emit fileTransferFailed(txnId, tr("File upload cancelled"));
+ } else if (transferIt->status == FileTransferInfo::Completed)
+ {
+ qCWarning(MAIN) << "File for transaction" << txnId
+ << "has been uploaded but the message was discarded";
+ }
+ }
+ emit pendingEventAboutToDiscard(int(it - d->unsyncedEvents.begin()));
d->unsyncedEvents.erase(it);
emit pendingEventDiscarded();
}
@@ -1251,7 +1516,7 @@ QString Room::postPlainText(const QString& plainText)
}
QString Room::postHtmlMessage(const QString& plainText, const QString& html,
- MessageEventType type)
+ MessageEventType type)
{
return d->sendEvent<RoomMessageEvent>(plainText, type,
new EventContent::TextContent(html, QStringLiteral("text/html")));
@@ -1259,7 +1524,65 @@ QString Room::postHtmlMessage(const QString& plainText, const QString& html,
QString Room::postHtmlText(const QString& plainText, const QString& html)
{
- return postHtmlMessage(plainText, html, MessageEventType::Text);
+ return postHtmlMessage(plainText, html);
+}
+
+QString Room::postFile(const QString& plainText, const QUrl& localPath,
+ bool asGenericFile)
+{
+ QFileInfo localFile { localPath.toLocalFile() };
+ Q_ASSERT(localFile.isFile());
+
+ const auto txnId = connection()->generateTxnId();
+ // Remote URL will only be known after upload; fill in the local path
+ // to enable the preview while the event is pending.
+ uploadFile(txnId, localPath);
+ {
+ auto&& event =
+ makeEvent<RoomMessageEvent>(plainText, localFile, asGenericFile);
+ event->setTransactionId(txnId);
+ d->addAsPending(std::move(event));
+ }
+ auto* context = new QObject(this);
+ connect(this, &Room::fileTransferCompleted, context,
+ [context,this,txnId] (const QString& id, QUrl, const QUrl& mxcUri) {
+ if (id == txnId)
+ {
+ auto it = findPendingEvent(txnId);
+ if (it != d->unsyncedEvents.end())
+ {
+ it->setFileUploaded(mxcUri);
+ emit pendingEventChanged(
+ int(it - d->unsyncedEvents.begin()));
+ d->doSendEvent(it->get());
+ } else {
+ // Normally in this situation we should instruct
+ // the media server to delete the file; alas, there's no
+ // API specced for that.
+ qCWarning(MAIN) << "File uploaded to" << mxcUri
+ << "but the event referring to it was cancelled";
+ }
+ context->deleteLater();
+ }
+ });
+ connect(this, &Room::fileTransferCancelled, this,
+ [context,this,txnId] (const QString& id) {
+ if (id == txnId)
+ {
+ auto it = findPendingEvent(txnId);
+ if (it != d->unsyncedEvents.end())
+ {
+ const auto idx = int(it - d->unsyncedEvents.begin());
+ emit pendingEventAboutToDiscard(idx);
+ // See #286 on why iterator may not be valid here.
+ d->unsyncedEvents.erase(d->unsyncedEvents.begin() + idx);
+ emit pendingEventDiscarded();
+ }
+ context->deleteLater();
+ }
+ });
+
+ return txnId;
}
QString Room::postEvent(RoomEvent* event)
@@ -1288,6 +1611,11 @@ void Room::setCanonicalAlias(const QString& newAlias)
d->requestSetState(RoomCanonicalAliasEvent(newAlias));
}
+void Room::setAliases(const QStringList& aliases)
+{
+ d->requestSetState(RoomAliasesEvent(aliases));
+}
+
void Room::setTopic(const QString& newTopic)
{
d->requestSetState(RoomTopicEvent(newTopic));
@@ -1315,7 +1643,24 @@ bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re)
bool Room::supportsCalls() const
{
- return d->membersMap.size() == 2;
+ return joinedCount() == 2;
+}
+
+void Room::checkVersion()
+{
+ const auto defaultVersion = connection()->defaultRoomVersion();
+ const auto stableVersions = connection()->stableRoomVersions();
+ Q_ASSERT(!defaultVersion.isEmpty());
+ // This method is only called after the base state has been loaded
+ // or the server capabilities have been loaded.
+ emit stabilityUpdated(defaultVersion, stableVersions);
+ if (!stableVersions.contains(version()))
+ {
+ qCDebug(MAIN) << this << "version is" << version()
+ << "which the server doesn't count as stable";
+ if (canSwitchVersions())
+ qCDebug(MAIN) << "The current user has enough privileges to fix it";
+ }
}
void Room::inviteCall(const QString& callId, const int lifetime,
@@ -1358,15 +1703,18 @@ void Room::getPreviousContent(int limit)
void Room::Private::getPreviousContent(int limit)
{
- if( !isJobRunning(eventsHistoryJob) )
- {
- eventsHistoryJob =
- connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit);
- connect( eventsHistoryJob, &BaseJob::success, q, [=] {
- prevBatch = eventsHistoryJob->end();
- addHistoricalMessageEvents(eventsHistoryJob->chunk());
- });
- }
+ if (isJobRunning(eventsHistoryJob))
+ return;
+
+ eventsHistoryJob =
+ connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit);
+ emit q->eventsHistoryJobChanged();
+ connect( eventsHistoryJob, &BaseJob::success, q, [=] {
+ prevBatch = eventsHistoryJob->end();
+ addHistoricalMessageEvents(eventsHistoryJob->chunk());
+ });
+ connect( eventsHistoryJob, &QObject::destroyed,
+ q, &Room::eventsHistoryJobChanged);
}
void Room::inviteToRoom(const QString& memberId)
@@ -1376,7 +1724,8 @@ void Room::inviteToRoom(const QString& memberId)
LeaveRoomJob* Room::leaveRoom()
{
- return connection()->callApi<LeaveRoomJob>(id());
+ // FIXME, #63: It should be RoomManager, not Connection
+ return connection()->leaveRoom(this);
}
SetRoomStateWithKeyJob*Room::setMemberState(const QString& memberId, const RoomMemberEvent& event) const
@@ -1401,8 +1750,8 @@ void Room::unban(const QString& userId)
void Room::redactEvent(const QString& eventId, const QString& reason)
{
- connection()->callApi<RedactEventJob>(
- id(), eventId, connection()->generateTxnId(), reason);
+ connection()->callApi<RedactEventJob>(id(),
+ QUrl::toPercentEncoding(eventId), connection()->generateTxnId(), reason);
}
void Room::uploadFile(const QString& id, const QUrl& localFilename,
@@ -1414,7 +1763,7 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename,
auto job = connection()->uploadFile(fileName, overrideContentType);
if (isJobRunning(job))
{
- d->fileTransfers.insert(id, { job, fileName });
+ d->fileTransfers.insert(id, { job, fileName, true });
connect(job, &BaseJob::uploadProgress, this,
[this,id] (qint64 sent, qint64 total) {
d->fileTransfers[id].update(sent, total);
@@ -1437,8 +1786,8 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
if (ongoingTransfer != d->fileTransfers.end() &&
ongoingTransfer->status == FileTransferInfo::Started)
{
- qCWarning(MAIN) << "Download for" << eventId
- << "already started; to restart, cancel it first";
+ qCWarning(MAIN) << "Transfer for" << eventId
+ << "is ongoing; download won't start";
return;
}
@@ -1452,13 +1801,21 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
Q_ASSERT(false);
return;
}
- const auto fileUrl = event->content()->fileInfo()->url;
+ const auto* const fileInfo = event->content()->fileInfo();
+ if (!fileInfo->isValid())
+ {
+ qCWarning(MAIN) << "Event" << eventId
+ << "has an empty or malformed mxc URL; won't download";
+ return;
+ }
+ const auto fileUrl = fileInfo->url;
auto filePath = localFilename.toLocalFile();
if (filePath.isEmpty())
{
// Build our own file path, starting with temp directory and eventId.
filePath = eventId;
- filePath = QDir::tempPath() % '/' % filePath.replace(':', '_') %
+ filePath = QDir::tempPath() % '/' %
+ filePath.replace(QRegularExpression("[/\\<>|\"*?:]"), "_") %
'#' % d->fileNameToDownload(event);
}
auto job = connection()->downloadFile(fileUrl, filePath);
@@ -1533,22 +1890,29 @@ RoomEventPtr makeRedacted(const RoomEvent& target,
const RedactionEvent& redaction)
{
auto originalJson = target.originalJsonObject();
- static const QStringList keepKeys =
- { EventIdKey, TypeKey, QStringLiteral("room_id"),
- QStringLiteral("sender"), QStringLiteral("state_key"),
- QStringLiteral("prev_content"), ContentKey,
- QStringLiteral("origin_server_ts") };
+ static const QStringList keepKeys {
+ EventIdKey, TypeKey, QStringLiteral("room_id"),
+ QStringLiteral("sender"), QStringLiteral("state_key"),
+ QStringLiteral("prev_content"), ContentKey,
+ QStringLiteral("hashes"), QStringLiteral("signatures"),
+ QStringLiteral("depth"), QStringLiteral("prev_events"),
+ QStringLiteral("prev_state"), QStringLiteral("auth_events"),
+ QStringLiteral("origin"), QStringLiteral("origin_server_ts"),
+ QStringLiteral("membership")
+ };
std::vector<std::pair<Event::Type, QStringList>> keepContentKeysMap
{ { RoomMemberEvent::typeId(), { QStringLiteral("membership") } }
-// , { RoomCreateEvent::typeId(), { QStringLiteral("creator") } }
+ , { RoomCreateEvent::typeId(), { QStringLiteral("creator") } }
// , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } }
// , { RoomPowerLevels::typeId(),
// { QStringLiteral("ban"), QStringLiteral("events"),
// QStringLiteral("events_default"), QStringLiteral("kick"),
// QStringLiteral("redact"), QStringLiteral("state_default"),
// QStringLiteral("users"), QStringLiteral("users_default") } }
- , { RoomAliasesEvent::typeId(), { QStringLiteral("alias") } }
+ , { RoomAliasesEvent::typeId(), { QStringLiteral("aliases") } }
+// , { RoomHistoryVisibility::typeId(),
+// { QStringLiteral("history_visibility") } }
};
for (auto it = originalJson.begin(); it != originalJson.end();)
{
@@ -1600,11 +1964,26 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction)
return true;
}
- // Make a new event from the redacted JSON, exchange events,
- // notify everyone and delete the old event
+ // 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(makeRedacted(*ti, redaction));
- q->onRedaction(*oldEvent, *ti.event());
qCDebug(MAIN) << "Redacted" << oldEvent->id() << "with" << redaction.id();
+ if (oldEvent->isStateEvent())
+ {
+ const StateEventKey evtKey { oldEvent->matrixType(), oldEvent->stateKey() };
+ Q_ASSERT(currentState.contains(evtKey));
+ if (currentState.value(evtKey) == oldEvent.get())
+ {
+ Q_ASSERT(ti.index() >= 0); // Historical states can't be in currentState
+ qCDebug(MAIN).nospace() << "Redacting state "
+ << oldEvent->matrixType() << "/" << oldEvent->stateKey();
+ // Retarget the current state to the newly made event.
+ if (q->processStateEvent(*ti))
+ emit q->namesChanged(q);
+ updateDisplayname();
+ }
+ }
+ q->onRedaction(*oldEvent, *ti);
emit q->replacedEvent(ti.event(), rawPtr(oldEvent));
return true;
}
@@ -1626,11 +2005,11 @@ inline bool isRedaction(const RoomEventPtr& ep)
return is<RedactionEvent>(*ep);
}
-void Room::Private::addNewMessageEvents(RoomEvents&& events)
+Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
{
dropDuplicateEvents(events);
if (events.empty())
- return;
+ return Change::NoChange;
// Pre-process redactions so that events that get redacted in the same
// batch landed in the timeline already redacted.
@@ -1655,8 +2034,17 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events)
// If the target event comes later, it comes already redacted.
}
+ // State changes arrive as a part of timeline; the current room state gets
+ // updated before merging events to the timeline because that's what
+ // clients historically expect. This may eventually change though if we
+ // postulate that the current state is only current between syncs but not
+ // within a sync.
+ Changes roomChanges = Change::NoChange;
+ for (const auto& eptr: events)
+ roomChanges |= q->processStateEvent(*eptr);
+
auto timelineSize = timeline.size();
- auto totalInserted = 0;
+ size_t totalInserted = 0;
for (auto it = events.begin(); it != events.end();)
{
auto nextPendingPair = findFirstOf(it, events.end(),
@@ -1677,12 +2065,22 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events)
break;
it = nextPending + 1;
- emit q->pendingEventAboutToMerge(nextPending->get(),
- nextPendingPair.second - unsyncedEvents.begin());
+ auto* nextPendingEvt = nextPending->get();
+ const auto pendingEvtIdx =
+ int(nextPendingPair.second - unsyncedEvents.begin());
+ emit q->pendingEventAboutToMerge(nextPendingEvt, pendingEvtIdx);
qDebug(EVENTS) << "Merging pending event from transaction"
- << (*nextPending)->transactionId() << "into"
- << (*nextPending)->id();
- unsyncedEvents.erase(nextPendingPair.second);
+ << nextPendingEvt->transactionId() << "into"
+ << nextPendingEvt->id();
+ auto transfer = fileTransfers.take(nextPendingEvt->transactionId());
+ if (transfer.status != FileTransferInfo::None)
+ fileTransfers.insert(nextPendingEvt->id(), transfer);
+ // After emitting pendingEventAboutToMerge() above we cannot rely
+ // on the previously obtained nextPendingPair.second staying valid
+ // because a signal handler may send another message, thereby altering
+ // unsyncedEvents (see #286). Fortunately, unsyncedEvents only grows at
+ // its back so we can rely on the index staying valid at least.
+ unsyncedEvents.erase(unsyncedEvents.begin() + pendingEvtIdx);
if (auto insertedSize = moveEventsToTimeline({nextPending, it}, Newer))
{
totalInserted += insertedSize;
@@ -1713,15 +2111,17 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events)
auto firstWriter = q->user((*from)->senderId());
if (q->readMarker(firstWriter) != timeline.crend())
{
- promoteReadMarker(firstWriter, rev_iter_t(from) - 1);
+ roomChanges |= promoteReadMarker(firstWriter, rev_iter_t(from) - 1);
qCDebug(MAIN) << "Auto-promoted read marker for" << firstWriter->id()
<< "to" << *q->readMarker(firstWriter);
}
updateUnreadCount(timeline.crbegin(), rev_iter_t(from));
+ roomChanges |= Change::UnreadNotifsChange;
}
Q_ASSERT(timeline.size() == timelineSize + totalInserted);
+ return roomChanges;
}
void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
@@ -1730,14 +2130,25 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
const auto timelineSize = timeline.size();
dropDuplicateEvents(events);
- RoomEventsRange normalEvents {
- events.begin(), events.end() //remove_if(events.begin(), events.end(), isRedaction)
- };
- if (normalEvents.empty())
+ if (events.empty())
return;
- emit q->aboutToAddHistoricalMessages(normalEvents);
- const auto insertedSize = moveEventsToTimeline(normalEvents, Older);
+ // In case of lazy-loading new members may be loaded with historical
+ // messages. Also, the cache doesn't store events with empty content;
+ // so when such events show up in the timeline they should be properly
+ // incorporated.
+ for (const auto& eptr: events)
+ {
+ const auto& e = *eptr;
+ if (e.isStateEvent() &&
+ !currentState.contains({e.matrixType(), e.stateKey()}))
+ {
+ q->processStateEvent(e);
+ }
+ }
+
+ emit q->aboutToAddHistoricalMessages(events);
+ const auto insertedSize = moveEventsToTimeline(events, Older);
const auto from = timeline.crend() - insertedSize;
qCDebug(MAIN) << "Room" << displayname << "received" << insertedSize
@@ -1754,52 +2165,89 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
<< insertedSize << "event(s)," << et;
}
-bool Room::processStateEvent(const RoomEvent& e)
+Room::Changes Room::processStateEvent(const RoomEvent& e)
{
+ if (!e.isStateEvent())
+ return Change::NoChange;
+
+ const auto* oldStateEvent = std::exchange(
+ d->currentState[{e.matrixType(),e.stateKey()}],
+ static_cast<const StateEventBase*>(&e));
+ Q_ASSERT(!oldStateEvent ||
+ (oldStateEvent->matrixType() == e.matrixType() &&
+ oldStateEvent->stateKey() == e.stateKey()));
+ if (!is<RoomMemberEvent>(e)) // Room member events are too numerous
+ qCDebug(EVENTS) << "Room state event:" << e;
+
return visit(e
- , [this] (const RoomNameEvent& evt) {
- d->name = evt.name();
- qCDebug(MAIN) << "Room name updated:" << d->name;
- return true;
+ , [] (const RoomNameEvent&) {
+ return NameChange;
}
- , [this] (const RoomAliasesEvent& evt) {
- d->aliases = evt.aliases();
- qCDebug(MAIN) << "Room aliases updated:" << d->aliases;
- return true;
+ , [this,oldStateEvent] (const RoomAliasesEvent& ae) {
+ const auto previousAliases = oldStateEvent
+ ? static_cast<const RoomAliasesEvent*>(oldStateEvent)->aliases()
+ : QStringList();
+ connection()->updateRoomAliases(id(), previousAliases, ae.aliases());
+ return OtherChange;
}
, [this] (const RoomCanonicalAliasEvent& evt) {
- d->canonicalAlias = evt.alias();
- if (!d->canonicalAlias.isEmpty())
- setObjectName(d->canonicalAlias);
- qCDebug(MAIN) << "Room canonical alias updated:"
- << d->canonicalAlias;
- return true;
+ setObjectName(evt.alias().isEmpty() ? d->id : evt.alias());
+ return CanonicalAliasChange;
}
- , [this] (const RoomTopicEvent& evt) {
- d->topic = evt.topic();
- qCDebug(MAIN) << "Room topic updated:" << d->topic;
- emit topicChanged();
- return false;
+ , [] (const RoomTopicEvent&) {
+ return TopicChange;
}
, [this] (const RoomAvatarEvent& evt) {
if (d->avatar.updateUrl(evt.url()))
- {
- qCDebug(MAIN) << "Room avatar URL updated:"
- << evt.url().toString();
emit avatarChanged();
- }
- return false;
+ return AvatarChange;
}
- , [this] (const RoomMemberEvent& evt) {
+ , [this,oldStateEvent] (const RoomMemberEvent& evt) {
auto* u = user(evt.userId());
- u->processEvent(evt, this);
- if (u == localUser() && memberJoinState(u) == JoinState::Invite
+ const auto* oldMemberEvent =
+ static_cast<const RoomMemberEvent*>(oldStateEvent);
+ u->processEvent(evt, this, oldMemberEvent == nullptr);
+ const auto prevMembership = oldMemberEvent
+ ? oldMemberEvent->membership() : MembershipType::Leave;
+ if (u == localUser() && evt.membership() == MembershipType::Invite
&& evt.isDirect())
connection()->addToDirectChats(this, user(evt.senderId()));
- if( evt.membership() == MembershipType::Join )
+ switch (prevMembership)
{
- if (memberJoinState(u) != JoinState::Join)
+ case MembershipType::Invite:
+ if (evt.membership() != prevMembership)
+ {
+ d->usersInvited.removeOne(u);
+ Q_ASSERT(!d->usersInvited.contains(u));
+ }
+ break;
+ case MembershipType::Join:
+ if (evt.membership() == MembershipType::Invite)
+ qCWarning(MAIN)
+ << "Invalid membership change from Join to Invite:"
+ << evt;
+ if (evt.membership() != prevMembership)
+ {
+ disconnect(u, &User::nameAboutToChange, this, nullptr);
+ disconnect(u, &User::nameChanged, this, nullptr);
+ d->removeMemberFromMap(u->name(this), u);
+ emit userRemoved(u);
+ }
+ break;
+ default:
+ if (evt.membership() == MembershipType::Invite
+ || evt.membership() == MembershipType::Join)
+ {
+ d->membersLeft.removeOne(u);
+ Q_ASSERT(!d->membersLeft.contains(u));
+ }
+ }
+
+ switch(evt.membership())
+ {
+ case MembershipType::Join:
+ if (prevMembership != MembershipType::Join)
{
d->insertMemberIntoMap(u);
connect(u, &User::nameAboutToChange, this,
@@ -1810,35 +2258,50 @@ bool Room::processStateEvent(const RoomEvent& e)
connect(u, &User::nameChanged, this,
[=] (QString, QString oldName, const Room* context) {
if (context == this)
+ {
d->renameMember(u, oldName);
+ emit memberRenamed(u);
+ }
});
emit userAdded(u);
}
+ break;
+ case MembershipType::Invite:
+ if (!d->usersInvited.contains(u))
+ d->usersInvited.push_back(u);
+ break;
+ default:
+ if (!d->membersLeft.contains(u))
+ d->membersLeft.append(u);
}
- else if( evt.membership() == MembershipType::Leave )
- {
- if (memberJoinState(u) == JoinState::Join)
- {
- if (!d->membersLeft.contains(u))
- d->membersLeft.append(u);
- d->removeMemberFromMap(u->name(this), u);
- emit userRemoved(u);
- }
- }
- return false;
+ return MembersChange;
}
- , [this] (const EncryptionEvent& evt) {
- d->encryptionAlgorithm = evt.algorithm();
- qCDebug(MAIN) << "Encryption switched on in room" << id()
- << "with algorithm" << d->encryptionAlgorithm;
- emit encryption();
- return false;
+ , [this] (const EncryptionEvent&) {
+ emit encryption(); // It can only be done once, so emit it here.
+ return OtherChange;
+ }
+ , [this] (const RoomTombstoneEvent& evt) {
+ const auto successorId = evt.successorRoomId();
+ if (auto* successor = connection()->room(successorId))
+ emit upgraded(evt.serverMessage(), successor);
+ else
+ connectUntil(connection(), &Connection::loadedRoomState, this,
+ [this,successorId,serverMsg=evt.serverMessage()]
+ (Room* newRoom) {
+ if (newRoom->id() != successorId)
+ return false;
+ emit upgraded(serverMsg, newRoom);
+ return true;
+ });
+
+ return OtherChange;
}
);
}
-void Room::processEphemeralEvent(EventPtr&& event)
+Room::Changes Room::processEphemeralEvent(EventPtr&& event)
{
+ Changes changes = NoChange;
QElapsedTimer et; et.start();
if (auto* evt = eventCast<TypingEvent>(event))
{
@@ -1877,7 +2340,7 @@ void Room::processEphemeralEvent(EventPtr&& event)
continue; // FIXME, #185
auto u = user(r.userId);
if (memberJoinState(u) == JoinState::Join)
- d->promoteReadMarker(u, newMarker);
+ changes |= d->promoteReadMarker(u, newMarker);
}
} else
{
@@ -1894,7 +2357,7 @@ void Room::processEphemeralEvent(EventPtr&& event)
auto u = user(r.userId);
if (memberJoinState(u) == JoinState::Join &&
readMarker(u) == timelineEdge())
- d->setLastReadEvent(u, p.evtId);
+ changes |= d->setLastReadEvent(u, p.evtId);
}
}
}
@@ -1904,12 +2367,17 @@ void Room::processEphemeralEvent(EventPtr&& event)
<< evt->eventsWithReceipts().size()
<< "event(s) with" << totalReceipts << "receipt(s)," << et;
}
+ return changes;
}
-void Room::processAccountDataEvent(EventPtr&& event)
+Room::Changes Room::processAccountDataEvent(EventPtr&& event)
{
+ Changes changes = NoChange;
if (auto* evt = eventCast<TagEvent>(event))
+ {
d->setTags(evt->tags());
+ changes |= Change::TagsChange;
+ }
if (auto* evt = eventCast<ReadMarkerEvent>(event))
{
@@ -1917,10 +2385,9 @@ void Room::processAccountDataEvent(EventPtr&& event)
qCDebug(MAIN) << "Server-side read marker at" << readEventId;
d->serverReadMarker = readEventId;
const auto newMarker = findInTimeline(readEventId);
- if (newMarker != timelineEdge())
- d->markMessagesAsRead(newMarker);
- else
- d->setLastReadEvent(localUser(), readEventId);
+ changes |= newMarker != timelineEdge()
+ ? d->markMessagesAsRead(newMarker)
+ : d->setLastReadEvent(localUser(), readEventId);
}
// For all account data events
auto& currentData = d->accountData[event->matrixType()];
@@ -1933,52 +2400,40 @@ void Room::processAccountDataEvent(EventPtr&& event)
qCDebug(MAIN) << "Updated account data of type"
<< currentData->matrixType();
emit accountDataChanged(currentData->matrixType());
+ return Change::AccountDataChange;
}
+ return Change::NoChange;
}
-QString Room::Private::roomNameFromMemberNames(const QList<User *> &userlist) const
+template <typename ContT>
+Room::Private::users_shortlist_t
+Room::Private::buildShortlist(const ContT& users) const
{
- // This is part 3(i,ii,iii) in the room displayname algorithm described
- // in the CS spec (see also Room::Private::updateDisplayname() ).
- // The spec requires to sort users lexicographically by state_key (user id)
- // and use disambiguated display names of two topmost users excluding
- // the current one to render the name of the room.
-
- // std::array is the leanest C++ container
- std::array<User*, 2> first_two = { {nullptr, nullptr} };
+ // To calculate room display name the spec requires to sort users
+ // lexicographically by state_key (user id) and use disambiguated
+ // display names of two topmost users excluding the current one to render
+ // the name of the room. The below code selects 3 topmost users,
+ // slightly extending the spec.
+ users_shortlist_t shortlist { }; // Prefill with nullptrs
std::partial_sort_copy(
- userlist.begin(), userlist.end(),
- first_two.begin(), first_two.end(),
- [this](const User* u1, const User* u2) {
- // Filter out the "me" user so that it never hits the room name
+ users.begin(), users.end(),
+ shortlist.begin(), shortlist.end(),
+ [this] (const User* u1, const User* u2) {
+ // localUser(), if it's in the list, is sorted below all others
return isLocalUser(u2) || (!isLocalUser(u1) && u1->id() < u2->id());
}
);
+ return shortlist;
+}
- // Spec extension. A single person in the chat but not the local user
- // (the local user is invited).
- if (userlist.size() == 1 && !isLocalUser(first_two.front()) &&
- joinState == JoinState::Invite)
- return tr("Invitation from %1")
- .arg(q->roomMembername(first_two.front()));
-
- // i. One-on-one chat. first_two[1] == localUser() in this case.
- if (userlist.size() == 2)
- return q->roomMembername(first_two[0]);
-
- // ii. Two users besides the current one.
- if (userlist.size() == 3)
- return tr("%1 and %2")
- .arg(q->roomMembername(first_two[0]),
- q->roomMembername(first_two[1]));
-
- // iii. More users.
- if (userlist.size() > 3)
- return tr("%1 and %Ln other(s)", "", userlist.size() - 3)
- .arg(q->roomMembername(first_two[0]));
-
- // userlist.size() < 2 - apparently, there's only current user in the room
- return QString();
+Room::Private::users_shortlist_t
+Room::Private::buildShortlist(const QStringList& userIds) const
+{
+ QList<User*> users;
+ users.reserve(userIds.size());
+ for (const auto& h: userIds)
+ users.push_back(q->user(h));
+ return buildShortlist(users);
}
QString Room::Private::calculateDisplayname() const
@@ -1987,27 +2442,73 @@ QString Room::Private::calculateDisplayname() const
// Numbers below refer to respective parts in the spec.
// 1. Name (from m.room.name)
- if (!name.isEmpty()) {
- return name;
+ auto dispName = q->name();
+ if (!dispName.isEmpty()) {
+ return dispName;
}
// 2. Canonical alias
- if (!canonicalAlias.isEmpty())
- return canonicalAlias;
+ dispName = q->canonicalAlias();
+ if (!dispName.isEmpty())
+ return dispName;
// Using m.room.aliases in naming is explicitly discouraged by the spec
- //if (!aliases.empty() && !aliases.at(0).isEmpty())
- // return aliases.at(0);
+
+ // Supplementary code for 3 and 4: build the shortlist of users whose names
+ // will be used to construct the room name. Takes into account MSC688's
+ // "heroes" if available.
+
+ const bool localUserIsIn = joinState == JoinState::Join;
+ const bool emptyRoom = membersMap.isEmpty() ||
+ (membersMap.size() == 1 && isLocalUser(*membersMap.begin()));
+ const bool nonEmptySummary =
+ !summary.heroes.omitted() && !summary.heroes->empty();
+ auto shortlist = nonEmptySummary ? buildShortlist(summary.heroes.value()) :
+ !emptyRoom ? buildShortlist(membersMap) :
+ users_shortlist_t { };
+
+ // When lazy-loading is on, we can rely on the heroes list.
+ // If it's off, the below code gathers invited and left members.
+ // NB: including invitations, if any, into naming is a spec extension.
+ // This kicks in when there's no lazy loading and it's a room with
+ // the local user as the only member, with more users invited.
+ if (!shortlist.front() && localUserIsIn)
+ shortlist = buildShortlist(usersInvited);
+
+ if (!shortlist.front()) // Still empty shortlist; use left members
+ shortlist = buildShortlist(membersLeft);
+
+ QStringList names;
+ for (auto u: shortlist)
+ {
+ if (u == nullptr || isLocalUser(u))
+ break;
+ // Only disambiguate if the room is not empty
+ names.push_back(u->displayname(emptyRoom ? nullptr : q));
+ }
+
+ const auto usersCountExceptLocal =
+ !emptyRoom ? q->joinedCount() - int(joinState == JoinState::Join) :
+ !usersInvited.empty() ? usersInvited.count() :
+ membersLeft.size() - int(joinState == JoinState::Leave);
+ if (usersCountExceptLocal > int(shortlist.size()))
+ names <<
+ tr("%Ln other(s)",
+ "Used to make a room name from user names: A, B and _N others_",
+ usersCountExceptLocal - int(shortlist.size()));
+ const auto namesList = QLocale().createSeparatedList(names);
// 3. Room members
- QString topMemberNames = roomNameFromMemberNames(membersMap.values());
- if (!topMemberNames.isEmpty())
- return topMemberNames;
+ if (!emptyRoom)
+ return namesList;
+
+ // (Spec extension) Invited users
+ if (!usersInvited.empty())
+ return tr("Empty room (invited: %1)").arg(namesList);
// 4. Users that previously left the room
- topMemberNames = roomNameFromMemberNames(membersLeft);
- if (!topMemberNames.isEmpty())
- return tr("Empty room (was: %1)").arg(topMemberNames);
+ if (membersLeft.size() > 0)
+ return tr("Empty room (was: %1)").arg(namesList);
// 5. Fail miserably
return tr("Empty room (%1)").arg(id);
@@ -2026,58 +2527,27 @@ void Room::Private::updateDisplayname()
}
}
-void appendStateEvent(QJsonArray& events, const QString& type,
- const QJsonObject& content, const QString& stateKey = {})
-{
- if (!content.isEmpty() || !stateKey.isEmpty())
- {
- auto json = basicEventJson(type, content);
- json.insert(QStringLiteral("state_key"), stateKey);
- events.append(json);
- }
-}
-
-#define ADD_STATE_EVENT(events, type, name, content) \
- appendStateEvent((events), QStringLiteral(type), \
- {{ QStringLiteral(name), content }});
-
-void appendEvent(QJsonArray& events, const QString& type,
- const QJsonObject& content)
-{
- if (!content.isEmpty())
- events.append(basicEventJson(type, content));
-}
-
-template <typename EvtT>
-void appendEvent(QJsonArray& events, const EvtT& event)
-{
- appendEvent(events, EvtT::matrixTypeId(), event.toJson());
-}
-
QJsonObject Room::Private::toJson() const
{
QElapsedTimer et; et.start();
QJsonObject result;
+ addParam<IfNotEmpty>(result, QStringLiteral("summary"), summary);
{
QJsonArray stateEvents;
- ADD_STATE_EVENT(stateEvents, "m.room.name", "name", name);
- ADD_STATE_EVENT(stateEvents, "m.room.topic", "topic", topic);
- ADD_STATE_EVENT(stateEvents, "m.room.avatar", "url",
- avatar.url().toString());
- ADD_STATE_EVENT(stateEvents, "m.room.aliases", "aliases",
- QJsonArray::fromStringList(aliases));
- ADD_STATE_EVENT(stateEvents, "m.room.canonical_alias", "alias",
- canonicalAlias);
- ADD_STATE_EVENT(stateEvents, "m.room.encryption", "algorithm",
- encryptionAlgorithm);
-
- for (const auto *m : membersMap)
- appendStateEvent(stateEvents, QStringLiteral("m.room.member"),
- { { QStringLiteral("membership"), QStringLiteral("join") }
- , { QStringLiteral("displayname"), m->rawName(q) }
- , { QStringLiteral("avatar_url"), m->avatarUrl(q).toString() }
- }, m->id());
+ for (const auto* evt: currentState)
+ {
+ Q_ASSERT(evt->isStateEvent());
+ if ((evt->isRedacted() && !is<RoomMemberEvent>(*evt)) ||
+ evt->contentJson().isEmpty())
+ continue;
+
+ auto json = evt->fullJson();
+ auto unsignedJson = evt->unsignedJson();
+ unsignedJson.remove(QStringLiteral("prev_content"));
+ json[UnsignedKeyL] = unsignedJson;
+ stateEvents.append(json);
+ }
const auto stateObjName = joinState == JoinState::Invite ?
QStringLiteral("invite_state") : QStringLiteral("state");
@@ -2085,14 +2555,17 @@ QJsonObject Room::Private::toJson() const
QJsonObject {{ QStringLiteral("events"), stateEvents }});
}
- QJsonArray accountDataEvents;
if (!accountData.empty())
{
+ QJsonArray accountDataEvents;
for (const auto& e: accountData)
- appendEvent(accountDataEvents, e.first, e.second->contentJson());
+ {
+ if (!e.second->contentJson().isEmpty())
+ accountDataEvents.append(e.second->fullJson());
+ }
+ result.insert(QStringLiteral("account_data"),
+ QJsonObject {{ QStringLiteral("events"), accountDataEvents }});
}
- result.insert(QStringLiteral("account_data"),
- QJsonObject {{ QStringLiteral("events"), accountDataEvents }});
QJsonObject unreadNotifObj
{ { SyncRoomData::UnreadCountKey, unreadMessages } };
diff --git a/lib/room.h b/lib/room.h
index a9ed9647..33d1f4ea 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -18,7 +18,7 @@
#pragma once
-#include "jobs/syncjob.h"
+#include "csapi/message_pagination.h"
#include "events/roommessageevent.h"
#include "events/accountdataevents.h"
#include "eventitem.h"
@@ -33,6 +33,8 @@
namespace QMatrixClient
{
class Event;
+ class Avatar;
+ class SyncRoomData;
class RoomMemberEvent;
class Connection;
class User;
@@ -41,10 +43,17 @@ namespace QMatrixClient
class SetRoomStateWithKeyJob;
class RedactEventJob;
+ /** The data structure used to expose file transfer information to views
+ *
+ * This is specifically tuned to work with QML exposing all traits as
+ * Q_PROPERTY values.
+ */
class FileTransferInfo
{
Q_GADGET
+ Q_PROPERTY(bool isUpload MEMBER isUpload CONSTANT)
Q_PROPERTY(bool active READ active CONSTANT)
+ Q_PROPERTY(bool started READ started CONSTANT)
Q_PROPERTY(bool completed READ completed CONSTANT)
Q_PROPERTY(bool failed READ failed CONSTANT)
Q_PROPERTY(int progress MEMBER progress CONSTANT)
@@ -52,16 +61,17 @@ namespace QMatrixClient
Q_PROPERTY(QUrl localDir MEMBER localDir CONSTANT)
Q_PROPERTY(QUrl localPath MEMBER localPath CONSTANT)
public:
- enum Status { None, Started, Completed, Failed };
+ enum Status { None, Started, Completed, Failed, Cancelled };
Status status = None;
+ bool isUpload = false;
int progress = 0;
int total = -1;
QUrl localDir { };
QUrl localPath { };
- bool active() const
- { return status == Started || status == Completed; }
+ bool started() const { return status == Started; }
bool completed() const { return status == Completed; }
+ bool active() const { return started() || completed(); }
bool failed() const { return status == Failed; }
};
@@ -71,10 +81,14 @@ namespace QMatrixClient
Q_PROPERTY(Connection* connection READ connection CONSTANT)
Q_PROPERTY(User* localUser READ localUser CONSTANT)
Q_PROPERTY(QString id READ id CONSTANT)
+ Q_PROPERTY(QString version READ version NOTIFY baseStateLoaded)
+ Q_PROPERTY(bool isUnstable READ isUnstable NOTIFY stabilityUpdated)
+ Q_PROPERTY(QString predecessorId READ predecessorId NOTIFY baseStateLoaded)
+ Q_PROPERTY(QString successorId READ successorId NOTIFY upgraded)
Q_PROPERTY(QString name READ name NOTIFY namesChanged)
Q_PROPERTY(QStringList aliases READ aliases NOTIFY namesChanged)
Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged)
- Q_PROPERTY(QString displayName READ displayName NOTIFY namesChanged)
+ Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged)
Q_PROPERTY(QString topic READ topic NOTIFY topicChanged)
Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false)
Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged)
@@ -83,6 +97,9 @@ namespace QMatrixClient
Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages)
Q_PROPERTY(QStringList memberNames READ memberNames NOTIFY memberListChanged)
Q_PROPERTY(int memberCount READ memberCount NOTIFY memberListChanged)
+ Q_PROPERTY(int joinedCount READ joinedCount NOTIFY memberListChanged)
+ Q_PROPERTY(int invitedCount READ invitedCount NOTIFY memberListChanged)
+ Q_PROPERTY(int totalMemberCount READ totalMemberCount NOTIFY memberListChanged)
Q_PROPERTY(bool displayed READ displayed WRITE setDisplayed NOTIFY displayedChanged)
Q_PROPERTY(QString firstDisplayedEventId READ firstDisplayedEventId WRITE setFirstDisplayedEventId NOTIFY firstDisplayedEventChanged)
@@ -95,12 +112,34 @@ namespace QMatrixClient
Q_PROPERTY(bool isFavourite READ isFavourite NOTIFY tagsChanged)
Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged)
+ Q_PROPERTY(GetRoomEventsJob* eventsHistoryJob READ eventsHistoryJob NOTIFY eventsHistoryJobChanged)
+
public:
using Timeline = std::deque<TimelineItem>;
using PendingEvents = std::vector<PendingEventItem>;
using rev_iter_t = Timeline::const_reverse_iterator;
using timeline_iter_t = Timeline::const_iterator;
+ enum Change : uint {
+ NoChange = 0x0,
+ NameChange = 0x1,
+ CanonicalAliasChange = 0x2,
+ TopicChange = 0x4,
+ UnreadNotifsChange = 0x8,
+ AvatarChange = 0x10,
+ JoinStateChange = 0x20,
+ TagsChange = 0x40,
+ MembersChange = 0x80,
+ /* = 0x100, */
+ AccountDataChange = 0x200,
+ SummaryChange = 0x400,
+ ReadMarkerChange = 0x800,
+ OtherChange = 0x8000,
+ AnyChange = 0xFFFF
+ };
+ Q_DECLARE_FLAGS(Changes, Change)
+ Q_FLAG(Changes)
+
Room(Connection* connection, QString id, JoinState initialJoinState);
~Room() override;
@@ -109,6 +148,10 @@ namespace QMatrixClient
Connection* connection() const;
User* localUser() const;
const QString& id() const;
+ QString version() const;
+ bool isUnstable() const;
+ QString predecessorId() const;
+ QString successorId() const;
QString name() const;
QStringList aliases() const;
QString canonicalAlias() const;
@@ -116,15 +159,22 @@ namespace QMatrixClient
QString topic() const;
QString avatarMediaId() const;
QUrl avatarUrl() const;
+ const Avatar& avatarObject() const;
Q_INVOKABLE JoinState joinState() const;
Q_INVOKABLE QList<User*> usersTyping() const;
QList<User*> membersLeft() const;
Q_INVOKABLE QList<User*> users() const;
QStringList memberNames() const;
+ [[deprecated("Use joinedCount(), invitedCount(), totalMemberCount()")]]
int memberCount() const;
int timelineSize() const;
bool usesEncryption() const;
+ int joinedCount() const;
+ int invitedCount() const;
+ int totalMemberCount() const;
+
+ GetRoomEventsJob* eventsHistoryJob() const;
/**
* Returns a square room avatar with the given size and requests it
@@ -177,9 +227,16 @@ namespace QMatrixClient
const Timeline& messageEvents() const;
const PendingEvents& pendingEvents() const;
/**
- * A convenience method returning the read marker to
- * the before-oldest message
+ * A convenience method returning the read marker to the position
+ * before the "oldest" event; same as messageEvents().crend()
*/
+ rev_iter_t historyEdge() const;
+ /**
+ * A convenience method returning the iterator beyond the latest
+ * arrived event; same as messageEvents().cend()
+ */
+ Timeline::const_iterator syncEdge() const;
+ /// \deprecated Use historyEdge instead
rev_iter_t timelineEdge() const;
Q_INVOKABLE TimelineItem::index_t minTimelineIndex() const;
Q_INVOKABLE TimelineItem::index_t maxTimelineIndex() const;
@@ -187,8 +244,17 @@ namespace QMatrixClient
rev_iter_t findInTimeline(TimelineItem::index_t index) const;
rev_iter_t findInTimeline(const QString& evtId) const;
+ PendingEvents::iterator findPendingEvent(const QString & txnId);
+ PendingEvents::const_iterator findPendingEvent(const QString & txnId) const;
bool displayed() const;
+ /// Mark the room as currently displayed to the user
+ /**
+ * Marking the room displayed causes the room to obtain the full
+ * list of members if it's been lazy-loaded before; in the future
+ * it may do more things bound to "screen time" of the room, e.g.
+ * measure that "screen time".
+ */
void setDisplayed(bool displayed = true);
QString firstDisplayedEventId() const;
rev_iter_t firstDisplayedMarker() const;
@@ -288,11 +354,32 @@ namespace QMatrixClient
/// Get the list of users this room is a direct chat with
QList<User*> directChatUsers() const;
- Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId);
- Q_INVOKABLE QUrl urlToDownload(const QString& eventId);
- Q_INVOKABLE QString fileNameToDownload(const QString& eventId);
+ Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId) const;
+ Q_INVOKABLE QUrl urlToDownload(const QString& eventId) const;
+
+ /// Get a file name for downloading for a given event id
+ /*!
+ * The event MUST be RoomMessageEvent and have content
+ * for downloading. \sa RoomMessageEvent::hasContent
+ */
+ Q_INVOKABLE QString fileNameToDownload(const QString& eventId) const;
+
+ /// Get information on file upload/download
+ /*!
+ * \param id uploads are identified by the corresponding event's
+ * transactionId (because uploads are done before
+ * the event is even sent), while downloads are using
+ * the normal event id for identifier.
+ */
Q_INVOKABLE FileTransferInfo fileTransferInfo(const QString& id) const;
+ /// Get the URL to the actual file source in a unified way
+ /*!
+ * For uploads it will return a URL to a local file; for downloads
+ * the URL will be taken from the corresponding room event.
+ */
+ Q_INVOKABLE QUrl fileSource(const QString& id) const;
+
/** Pretty-prints plain text into HTML
* As of now, it's exactly the same as QMatrixClient::prettyPrint();
* in the future, it will also linkify room aliases, mxids etc.
@@ -314,11 +401,17 @@ namespace QMatrixClient
Q_INVOKABLE bool supportsCalls() const;
public slots:
+ /** Check whether the room should be upgraded */
+ void checkVersion();
+
QString postMessage(const QString& plainText, MessageEventType type);
QString postPlainText(const QString& plainText);
QString postHtmlMessage(const QString& plainText,
- const QString& html, MessageEventType type);
+ const QString& html,
+ MessageEventType type = MessageEventType::Text);
QString postHtmlText(const QString& plainText, const QString& html);
+ QString postFile(const QString& plainText, const QUrl& localPath,
+ bool asGenericFile = false);
/** Post a pre-created room message event
*
* Takes ownership of the event, deleting it once the matching one
@@ -332,8 +425,12 @@ namespace QMatrixClient
void discardMessage(const QString& txnId);
void setName(const QString& newName);
void setCanonicalAlias(const QString& newAlias);
+ void setAliases(const QStringList& aliases);
void setTopic(const QString& newTopic);
+ /// You shouldn't normally call this method; it's here for debugging
+ void refreshDisplayName();
+
void getPreviousContent(int limit = 10);
void inviteToRoom(const QString& memberId);
@@ -356,19 +453,63 @@ namespace QMatrixClient
/// Mark all messages in the room as read
void markAllMessagesAsRead();
+ /// Whether the current user is allowed to upgrade the room
+ bool canSwitchVersions() const;
+
+ /// Switch the room's version (aka upgrade)
+ void switchVersion(QString newVersion);
+
signals:
+ /// Initial set of state events has been loaded
+ /**
+ * The initial set is what comes from the initial sync for the room.
+ * This includes all basic things like RoomCreateEvent,
+ * RoomNameEvent, a (lazy-loaded, not full) set of RoomMemberEvents
+ * etc. This is a per-room reflection of Connection::loadedRoomState
+ * \sa Connection::loadedRoomState
+ */
+ void baseStateLoaded();
+ void eventsHistoryJobChanged();
void aboutToAddHistoricalMessages(RoomEventsRange events);
void aboutToAddNewMessages(RoomEventsRange events);
void addedMessages(int fromIndex, int toIndex);
- void pendingEventAboutToAdd();
+ /// The event is about to be appended to the list of pending events
+ void pendingEventAboutToAdd(RoomEvent* event);
+ /// An event has been appended to the list of pending events
void pendingEventAdded();
+ /// The remote echo has arrived with the sync and will be merged
+ /// with its local counterpart
+ /** NB: Requires a sync loop to be emitted */
void pendingEventAboutToMerge(RoomEvent* serverEvent,
int pendingEventIndex);
+ /// The remote and local copies of the event have been merged
+ /** NB: Requires a sync loop to be emitted */
void pendingEventMerged();
+ /// An event will be removed from the list of pending events
void pendingEventAboutToDiscard(int pendingEventIndex);
+ /// An event has just been removed from the list of pending events
void pendingEventDiscarded();
+ /// The status of a pending event has changed
+ /** \sa PendingEventItem::deliveryStatus */
void pendingEventChanged(int pendingEventIndex);
+ /// The server accepted the message
+ /** This is emitted when an event sending request has successfully
+ * completed. This does not mean that the event is already in the
+ * local timeline, only that the server has accepted it.
+ * \param txnId transaction id assigned by the client during sending
+ * \param eventId event id assigned by the server upon acceptance
+ * \sa postEvent, postPlainText, postMessage, postHtmlMessage
+ * \sa pendingEventMerged, aboutToAddNewMessages
+ */
+ void messageSent(QString txnId, QString eventId);
+ /** A common signal for various kinds of changes in the room
+ * Aside from all changes in the room state
+ * @param changes a set of flags describing what changes occured
+ * upon the last sync
+ * \sa StateChange
+ */
+ void changed(Changes changes);
/**
* \brief The room name, the canonical alias or other aliases changed
*
@@ -383,7 +524,17 @@ namespace QMatrixClient
void userRemoved(User* user);
void memberAboutToRename(User* user, QString newName);
void memberRenamed(User* user);
+ /// The list of members has changed
+ /** Emitted no more than once per sync, this is a good signal to
+ * for cases when some action should be done upon any change in
+ * the member list. If you need per-item granularity you should use
+ * userAdded, userRemoved and memberAboutToRename / memberRenamed
+ * instead.
+ */
void memberListChanged();
+ /// The previously lazy-loaded members list is now loaded entirely
+ /// \sa setDisplayed
+ void allMembersLoaded();
void encryption();
void joinStateChanged(JoinState oldState, JoinState newState);
@@ -415,30 +566,40 @@ namespace QMatrixClient
void fileTransferCancelled(QString id);
void callEvent(Room* room, const RoomEvent* event);
- /// The room is about to be deleted
- void beforeDestruction(Room*);
- public: // Used by Connection - not a part of the client API
- QJsonObject toJson() const;
- void updateData(SyncRoomData&& data );
+ /// The room's version stability may have changed
+ void stabilityUpdated(QString recommendedDefault,
+ QStringList stableVersions);
+ /// This room has been upgraded and won't receive updates anymore
+ void upgraded(QString serverMessage, Room* successor);
+ /// An attempted room upgrade has failed
+ void upgradeFailed(QString errorMessage);
- // Clients should use Connection::joinRoom() and Room::leaveRoom()
- // to change the room state
- void setJoinState( JoinState state );
+ /// The room is about to be deleted
+ void beforeDestruction(Room*);
protected:
/// Returns true if any of room names/aliases has changed
- virtual bool processStateEvent(const RoomEvent& e);
- virtual void processEphemeralEvent(EventPtr&& event);
- virtual void processAccountDataEvent(EventPtr&& event);
+ virtual Changes processStateEvent(const RoomEvent& e);
+ virtual Changes processEphemeralEvent(EventPtr&& event);
+ virtual Changes processAccountDataEvent(EventPtr&& event);
virtual void onAddNewTimelineEvents(timeline_iter_t /*from*/) { }
virtual void onAddHistoricalTimelineEvents(rev_iter_t /*from*/) { }
virtual void onRedaction(const RoomEvent& /*prevEvent*/,
const RoomEvent& /*after*/) { }
+ virtual QJsonObject toJson() const;
+ virtual void updateData(SyncRoomData&& data, bool fromCache = false);
private:
+ friend class Connection;
+
class Private;
Private* d;
+
+ // This is called from Connection, reflecting a state change that
+ // arrived from the server. Clients should use
+ // Connection::joinRoom() and Room::leaveRoom() to change the state.
+ void setJoinState(JoinState state);
};
class MemberSorter
@@ -461,3 +622,4 @@ namespace QMatrixClient
};
} // namespace QMatrixClient
Q_DECLARE_METATYPE(QMatrixClient::FileTransferInfo)
+Q_DECLARE_OPERATORS_FOR_FLAGS(QMatrixClient::Room::Changes)
diff --git a/lib/settings.cpp b/lib/settings.cpp
index 852e19cb..124d7042 100644
--- a/lib/settings.cpp
+++ b/lib/settings.cpp
@@ -84,18 +84,21 @@ void SettingsGroup::remove(const QString& key)
Settings::remove(fullKey);
}
-QMC_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", "", setDeviceId)
-QMC_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", "", setDeviceName)
+QMC_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", {}, setDeviceId)
+QMC_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {}, setDeviceName)
QMC_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, setKeepLoggedIn)
+static const auto HomeserverKey = QStringLiteral("homeserver");
+static const auto AccessTokenKey = QStringLiteral("access_token");
+
QUrl AccountSettings::homeserver() const
{
- return QUrl::fromUserInput(value("homeserver").toString());
+ return QUrl::fromUserInput(value(HomeserverKey).toString());
}
void AccountSettings::setHomeserver(const QUrl& url)
{
- setValue("homeserver", url.toString());
+ setValue(HomeserverKey, url.toString());
}
QString AccountSettings::userId() const
@@ -105,19 +108,19 @@ QString AccountSettings::userId() const
QString AccountSettings::accessToken() const
{
- return value("access_token").toString();
+ return value(AccessTokenKey).toString();
}
void AccountSettings::setAccessToken(const QString& accessToken)
{
qCWarning(MAIN) << "Saving access_token to QSettings is insecure."
" Developers, please save access_token separately.";
- setValue("access_token", accessToken);
+ setValue(AccessTokenKey, accessToken);
}
void AccountSettings::clearAccessToken()
{
- legacySettings.remove("access_token");
- legacySettings.remove("device_id"); // Force the server to re-issue it
- remove("access_token");
+ legacySettings.remove(AccessTokenKey);
+ legacySettings.remove(QStringLiteral("device_id")); // Force the server to re-issue it
+ remove(AccessTokenKey);
}
diff --git a/lib/settings.h b/lib/settings.h
index 0b3ecaff..759bda35 100644
--- a/lib/settings.h
+++ b/lib/settings.h
@@ -119,7 +119,7 @@ type classname::propname() const \
\
void classname::setter(type newValue) \
{ \
- setValue(QStringLiteral(qsettingname), newValue); \
+ setValue(QStringLiteral(qsettingname), std::move(newValue)); \
} \
class AccountSettings: public SettingsGroup
diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp
new file mode 100644
index 00000000..21517884
--- /dev/null
+++ b/lib/syncdata.cpp
@@ -0,0 +1,228 @@
+/******************************************************************************
+ * Copyright (C) 2018 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 "syncdata.h"
+
+#include "events/eventloader.h"
+
+#include <QtCore/QFile>
+#include <QtCore/QFileInfo>
+
+using namespace QMatrixClient;
+
+const QString SyncRoomData::UnreadCountKey =
+ QStringLiteral("x-qmatrixclient.unread_count");
+
+bool RoomSummary::isEmpty() const
+{
+ return joinedMemberCount.omitted() && invitedMemberCount.omitted() &&
+ heroes.omitted();
+}
+
+bool RoomSummary::merge(const RoomSummary& other)
+{
+ // Using bitwise OR to prevent computation shortcut.
+ return
+ joinedMemberCount.merge(other.joinedMemberCount) |
+ invitedMemberCount.merge(other.invitedMemberCount) |
+ heroes.merge(other.heroes);
+}
+
+QDebug QMatrixClient::operator<<(QDebug dbg, const RoomSummary& rs)
+{
+ QDebugStateSaver _(dbg);
+ QStringList sl;
+ if (!rs.joinedMemberCount.omitted())
+ sl << QStringLiteral("joined: %1").arg(rs.joinedMemberCount.value());
+ if (!rs.invitedMemberCount.omitted())
+ sl << QStringLiteral("invited: %1").arg(rs.invitedMemberCount.value());
+ if (!rs.heroes.omitted())
+ sl << QStringLiteral("heroes: [%1]").arg(rs.heroes.value().join(','));
+ dbg.nospace().noquote() << sl.join(QStringLiteral("; "));
+ return dbg;
+}
+
+void JsonObjectConverter<RoomSummary>::dumpTo(QJsonObject& jo,
+ const RoomSummary& rs)
+{
+ addParam<IfNotEmpty>(jo, QStringLiteral("m.joined_member_count"),
+ rs.joinedMemberCount);
+ addParam<IfNotEmpty>(jo, QStringLiteral("m.invited_member_count"),
+ rs.invitedMemberCount);
+ addParam<IfNotEmpty>(jo, QStringLiteral("m.heroes"), rs.heroes);
+}
+
+void JsonObjectConverter<RoomSummary>::fillFrom(const QJsonObject& jo,
+ RoomSummary& rs)
+{
+ fromJson(jo["m.joined_member_count"_ls], rs.joinedMemberCount);
+ fromJson(jo["m.invited_member_count"_ls], rs.invitedMemberCount);
+ fromJson(jo["m.heroes"_ls], rs.heroes);
+}
+
+template <typename EventsArrayT, typename StrT>
+inline EventsArrayT load(const QJsonObject& batches, StrT keyName)
+{
+ return fromJson<EventsArrayT>(batches[keyName].toObject().value("events"_ls));
+}
+
+SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_,
+ const QJsonObject& room_)
+ : roomId(roomId_)
+ , joinState(joinState_)
+ , summary(fromJson<RoomSummary>(room_["summary"_ls]))
+ , state(load<StateEvents>(room_, joinState == JoinState::Invite
+ ? "invite_state"_ls : "state"_ls))
+{
+ switch (joinState) {
+ case JoinState::Join:
+ ephemeral = load<Events>(room_, "ephemeral"_ls);
+ FALLTHROUGH;
+ case JoinState::Leave:
+ {
+ accountData = load<Events>(room_, "account_data"_ls);
+ timeline = load<RoomEvents>(room_, "timeline"_ls);
+ const auto timelineJson = room_.value("timeline"_ls).toObject();
+ timelineLimited = timelineJson.value("limited"_ls).toBool();
+ timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString();
+
+ break;
+ }
+ default: /* nothing on top of state */;
+ }
+
+ const auto unreadJson = room_.value("unread_notifications"_ls).toObject();
+ unreadCount = unreadJson.value(UnreadCountKey).toInt(-2);
+ highlightCount = unreadJson.value("highlight_count"_ls).toInt();
+ notificationCount = unreadJson.value("notification_count"_ls).toInt();
+ if (highlightCount > 0 || notificationCount > 0)
+ qCDebug(SYNCJOB) << "Room" << roomId_
+ << "has highlights:" << highlightCount
+ << "and notifications:" << notificationCount;
+}
+
+SyncData::SyncData(const QString& cacheFileName)
+{
+ QFileInfo cacheFileInfo { cacheFileName };
+ auto json = loadJson(cacheFileName);
+ auto requiredVersion = std::get<0>(cacheVersion());
+ auto actualVersion = json.value("cache_version"_ls).toObject()
+ .value("major"_ls).toInt();
+ if (actualVersion == requiredVersion)
+ parseJson(json, cacheFileInfo.absolutePath() + '/');
+ else
+ qCWarning(MAIN)
+ << "Major version of the cache file is" << actualVersion << "but"
+ << requiredVersion << "is required; discarding the cache";
+}
+
+SyncDataList&& SyncData::takeRoomData()
+{
+ return move(roomData);
+}
+
+QString SyncData::fileNameForRoom(QString roomId)
+{
+ roomId.replace(':', '_');
+ return roomId + ".json";
+}
+
+Events&& SyncData::takePresenceData()
+{
+ return std::move(presenceData);
+}
+
+Events&& SyncData::takeAccountData()
+{
+ return std::move(accountData);
+}
+
+Events&& SyncData::takeToDeviceEvents()
+{
+ return std::move(toDeviceEvents);
+}
+
+QJsonObject SyncData::loadJson(const QString& fileName)
+{
+ QFile roomFile { fileName };
+ if (!roomFile.exists())
+ {
+ qCWarning(MAIN) << "No state cache file" << fileName;
+ return {};
+ }
+ if(!roomFile.open(QIODevice::ReadOnly))
+ {
+ qCWarning(MAIN) << "Failed to open state cache file"
+ << roomFile.fileName();
+ return {};
+ }
+ auto data = roomFile.readAll();
+
+ const auto json =
+ (data.startsWith('{') ? QJsonDocument::fromJson(data)
+ : QJsonDocument::fromBinaryData(data)).object();
+ if (json.isEmpty())
+ {
+ qCWarning(MAIN) << "State cache in" << fileName
+ << "is broken or empty, discarding";
+ }
+ return json;
+}
+
+void SyncData::parseJson(const QJsonObject& json, const QString& baseDir)
+{
+ QElapsedTimer et; et.start();
+
+ nextBatch_ = json.value("next_batch"_ls).toString();
+ presenceData = load<Events>(json, "presence"_ls);
+ accountData = load<Events>(json, "account_data"_ls);
+ toDeviceEvents = load<Events>(json, "to_device"_ls);
+
+ auto rooms = json.value("rooms"_ls).toObject();
+ JoinStates::Int ii = 1; // ii is used to make a JoinState value
+ auto totalRooms = 0;
+ auto totalEvents = 0;
+ for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1)
+ {
+ const auto rs = rooms.value(JoinStateStrings[i]).toObject();
+ // We have a Qt container on the right and an STL one on the left
+ roomData.reserve(static_cast<size_t>(rs.size()));
+ for(auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt)
+ {
+ auto roomJson = roomIt->isObject()
+ ? roomIt->toObject()
+ : loadJson(baseDir + fileNameForRoom(roomIt.key()));
+ if (roomJson.isEmpty())
+ {
+ unresolvedRoomIds.push_back(roomIt.key());
+ continue;
+ }
+ roomData.emplace_back(roomIt.key(), JoinState(ii), roomJson);
+ const auto& r = roomData.back();
+ totalEvents += r.state.size() + r.ephemeral.size() +
+ r.accountData.size() + r.timeline.size();
+ }
+ totalRooms += rs.size();
+ }
+ if (!unresolvedRoomIds.empty())
+ qCWarning(MAIN) << "Unresolved rooms:" << unresolvedRoomIds.join(',');
+ if (totalRooms > 9 || et.nsecsElapsed() >= profilerMinNsecs())
+ qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with"
+ << totalRooms << "room(s),"
+ << totalEvents << "event(s) in" << et;
+}
diff --git a/lib/syncdata.h b/lib/syncdata.h
new file mode 100644
index 00000000..8694626e
--- /dev/null
+++ b/lib/syncdata.h
@@ -0,0 +1,116 @@
+/******************************************************************************
+ * Copyright (C) 2018 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 "joinstate.h"
+#include "events/stateevent.h"
+
+namespace QMatrixClient {
+ /// Room summary, as defined in MSC688
+ /**
+ * Every member of this structure is an Omittable; as per the MSC, only
+ * changed values are sent from the server so if nothing is in the payload
+ * the respective member will be omitted. In particular, `heroes.omitted()`
+ * means that nothing has come from the server; heroes.value().isEmpty()
+ * means a peculiar case of a room with the only member - the current user.
+ */
+ struct RoomSummary
+ {
+ Omittable<int> joinedMemberCount;
+ Omittable<int> invitedMemberCount;
+ Omittable<QStringList> heroes; //< mxids of users to take part in the room name
+
+ bool isEmpty() const;
+ /// Merge the contents of another RoomSummary object into this one
+ /// \return true, if the current object has changed; false otherwise
+ bool merge(const RoomSummary& other);
+
+ friend QDebug operator<<(QDebug dbg, const RoomSummary& rs);
+ };
+
+ template <>
+ struct JsonObjectConverter<RoomSummary>
+ {
+ static void dumpTo(QJsonObject& jo, const RoomSummary& rs);
+ static void fillFrom(const QJsonObject& jo, RoomSummary& rs);
+ };
+
+ class SyncRoomData
+ {
+ public:
+ QString roomId;
+ JoinState joinState;
+ RoomSummary summary;
+ StateEvents state;
+ RoomEvents timeline;
+ Events ephemeral;
+ Events accountData;
+
+ bool timelineLimited;
+ QString timelinePrevBatch;
+ int unreadCount;
+ int highlightCount;
+ int notificationCount;
+
+ SyncRoomData(const QString& roomId, JoinState joinState_,
+ const QJsonObject& room_);
+ SyncRoomData(SyncRoomData&&) = default;
+ SyncRoomData& operator=(SyncRoomData&&) = default;
+
+ static const QString UnreadCountKey;
+ };
+
+ // QVector cannot work with non-copiable objects, std::vector can.
+ using SyncDataList = std::vector<SyncRoomData>;
+
+ class SyncData
+ {
+ public:
+ SyncData() = default;
+ explicit SyncData(const QString& cacheFileName);
+ /** Parse sync response into room events
+ * \param json response from /sync or a room state cache
+ * \return the list of rooms with missing cache files; always
+ * empty when parsing response from /sync
+ */
+ void parseJson(const QJsonObject& json, const QString& baseDir = {});
+
+ Events&& takePresenceData();
+ Events&& takeAccountData();
+ Events&& takeToDeviceEvents();
+ SyncDataList&& takeRoomData();
+
+ QString nextBatch() const { return nextBatch_; }
+
+ QStringList unresolvedRooms() const { return unresolvedRoomIds; }
+
+ static std::pair<int, int> cacheVersion() { return { 10, 0 }; }
+ static QString fileNameForRoom(QString roomId);
+
+ private:
+ QString nextBatch_;
+ Events presenceData;
+ Events accountData;
+ Events toDeviceEvents;
+ SyncDataList roomData;
+ QStringList unresolvedRoomIds;
+
+ static QJsonObject loadJson(const QString& fileName);
+ };
+} // namespace QMatrixClient
diff --git a/lib/user.cpp b/lib/user.cpp
index bfd23ae2..17db5760 100644
--- a/lib/user.cpp
+++ b/lib/user.cpp
@@ -59,7 +59,7 @@ class User::Private
QMultiHash<QString, const Room*> otherNames;
Avatar mostUsedAvatar { makeAvatar({}) };
std::vector<Avatar> otherAvatars;
- auto otherAvatar(QUrl url)
+ auto otherAvatar(const QUrl& url)
{
return std::find_if(otherAvatars.begin(), otherAvatars.end(),
[&url] (const auto& av) { return av.url() == url; });
@@ -69,7 +69,7 @@ class User::Private
mutable int totalRooms = 0;
QString nameForRoom(const Room* r, const QString& hint = {}) const;
- void setNameForRoom(const Room* r, QString newName, QString oldName);
+ void setNameForRoom(const Room* r, QString newName, const QString& oldName);
QUrl avatarUrlForRoom(const Room* r, const QUrl& hint = {}) const;
void setAvatarForRoom(const Room* r, const QUrl& newUrl,
const QUrl& oldUrl);
@@ -82,7 +82,8 @@ class User::Private
QString User::Private::nameForRoom(const Room* r, const QString& hint) const
{
// If the hint is accurate, this function is O(1) instead of O(n)
- if (hint == mostUsedName || otherNames.contains(hint, r))
+ if (!hint.isNull()
+ && (hint == mostUsedName || otherNames.contains(hint, r)))
return hint;
return otherNames.key(r, mostUsedName);
}
@@ -90,7 +91,7 @@ QString User::Private::nameForRoom(const Room* r, const QString& hint) const
static constexpr int MIN_JOINED_ROOMS_TO_LOG = 20;
void User::Private::setNameForRoom(const Room* r, QString newName,
- QString oldName)
+ const QString& oldName)
{
Q_ASSERT(oldName != newName);
Q_ASSERT(oldName == mostUsedName || otherNames.contains(oldName, r));
@@ -117,7 +118,8 @@ void User::Private::setNameForRoom(const Room* r, QString newName,
et.start();
}
- for (auto* r1: connection->roomMap())
+ const auto& roomMap = connection->roomMap();
+ for (auto* r1: roomMap)
if (nameForRoom(r1) == mostUsedName)
otherNames.insert(mostUsedName, r1);
@@ -177,7 +179,8 @@ void User::Private::setAvatarForRoom(const Room* r, const QUrl& newUrl,
auto nextMostUsedIt = otherAvatar(newUrl);
Q_ASSERT(nextMostUsedIt != otherAvatars.end());
std::swap(mostUsedAvatar, *nextMostUsedIt);
- for (const auto* r1: connection->roomMap())
+ const auto& roomMap = connection->roomMap();
+ for (const auto* r1: roomMap)
if (avatarUrlForRoom(r1) == nextMostUsedIt->url())
avatarsToRooms.insert(nextMostUsedIt->url(), r1);
@@ -264,8 +267,9 @@ void User::updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl,
void User::rename(const QString& newName)
{
- auto job = connection()->callApi<SetDisplayNameJob>(id(), newName);
- connect(job, &BaseJob::success, this, [=] { updateName(newName); });
+ const auto actualNewName = sanitized(newName);
+ connect(connection()->callApi<SetDisplayNameJob>(id(), actualNewName),
+ &BaseJob::success, this, [=] { updateName(actualNewName); });
}
void User::rename(const QString& newName, const Room* r)
@@ -279,10 +283,11 @@ void User::rename(const QString& newName, const Room* r)
}
Q_ASSERT_X(r->memberJoinState(this) == JoinState::Join, __FUNCTION__,
"Attempt to rename a user that's not a room member");
+ const auto actualNewName = sanitized(newName);
MemberEventContent evtC;
- evtC.displayName = newName;
- auto job = r->setMemberState(id(), RoomMemberEvent(move(evtC)));
- connect(job, &BaseJob::success, this, [=] { updateName(newName, r); });
+ evtC.displayName = actualNewName;
+ connect(r->setMemberState(id(), RoomMemberEvent(move(evtC))),
+ &BaseJob::success, this, [=] { updateName(actualNewName, r); });
}
bool User::setAvatar(const QString& fileName)
@@ -312,6 +317,11 @@ void User::unmarkIgnore()
connection()->removeFromIgnoredUsers(this);
}
+bool User::isIgnored() const
+{
+ return connection()->isIgnored(this);
+}
+
void User::Private::setAvatarOnServer(QString contentUri, User* q)
{
auto* j = connection->callApi<SetAvatarUrlJob>(userId, contentUri);
@@ -372,19 +382,18 @@ QUrl User::avatarUrl(const Room* room) const
return avatarObject(room).url();
}
-void User::processEvent(const RoomMemberEvent& event, const Room* room)
+void User::processEvent(const RoomMemberEvent& event, const Room* room,
+ bool firstMention)
{
Q_ASSERT(room);
+
+ if (firstMention)
+ ++d->totalRooms;
+
if (event.membership() != MembershipType::Invite &&
event.membership() != MembershipType::Join)
return;
- auto aboutToEnter = room->memberJoinState(this) == JoinState::Leave &&
- (event.membership() == MembershipType::Join ||
- event.membership() == MembershipType::Invite);
- if (aboutToEnter)
- ++d->totalRooms;
-
auto newName = event.displayName();
// `bridged` value uses the same notification signal as the name;
// it is assumed that first setting of the bridge occurs together with
@@ -392,7 +401,7 @@ void User::processEvent(const RoomMemberEvent& event, const Room* room)
// exceptionally rare (the only reasonable case being that the bridge
// changes the naming convention). For the same reason room-specific
// bridge tags are not supported at all.
- QRegularExpression reSuffix(" \\((IRC|Gitter|Telegram)\\)$");
+ QRegularExpression reSuffix(QStringLiteral(" \\((IRC|Gitter|Telegram)\\)$"));
auto match = reSuffix.match(newName);
if (match.hasMatch())
{
diff --git a/lib/user.h b/lib/user.h
index 17f5625f..7c9ed55f 100644
--- a/lib/user.h
+++ b/lib/user.h
@@ -105,7 +105,11 @@ namespace QMatrixClient
QString avatarMediaId(const Room* room = nullptr) const;
QUrl avatarUrl(const Room* room = nullptr) const;
- void processEvent(const RoomMemberEvent& event, const Room* r);
+ /// This method is for internal use and should not be called
+ /// from client code
+ // FIXME: Move it away to private in lib 0.6
+ void processEvent(const RoomMemberEvent& event, const Room* r,
+ bool firstMention);
public slots:
/** Set a new name in the global user profile */
@@ -125,6 +129,8 @@ namespace QMatrixClient
void ignore();
/** Remove the user from the ignore list */
void unmarkIgnore();
+ /** Check whether the user is in ignore list */
+ bool isIgnored() const;
signals:
void nameAboutToChange(QString newName, QString oldName,
diff --git a/lib/util.cpp b/lib/util.cpp
index 1773fcfe..17674b84 100644
--- a/lib/util.cpp
+++ b/lib/util.cpp
@@ -19,6 +19,9 @@
#include "util.h"
#include <QtCore/QRegularExpression>
+#include <QtCore/QStandardPaths>
+#include <QtCore/QDir>
+#include <QtCore/QStringBuilder>
static const auto RegExpOptions =
QRegularExpression::CaseInsensitiveOption
@@ -35,29 +38,128 @@ static void linkifyUrls(QString& htmlEscapedText)
// comma or dot
// Note: outer parentheses are a part of C++ raw string delimiters, not of
// the regex (see http://en.cppreference.com/w/cpp/language/string_literal).
+ // Note2: the next-outer parentheses are \N in the replacement.
static const QRegularExpression FullUrlRegExp(QStringLiteral(
- R"(((www\.(?!\.)|[a-z][a-z0-9+.-]*://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"
+ R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp|magnet)://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"
), RegExpOptions);
// email address:
// [word chars, dots or dashes]@[word chars, dots or dashes].[word chars]
static const QRegularExpression EmailAddressRegExp(QStringLiteral(
- R"((mailto:)?(\b(\w|\.|-)+@(\w|\.|-)+\.\w+\b))"
+ R"(\b(mailto:)?((\w|\.|-)+@(\w|\.|-)+\.\w+\b))"
+ ), RegExpOptions);
+ // An interim liberal implementation of
+ // https://matrix.org/docs/spec/appendices.html#identifier-grammar
+ static const QRegularExpression MxIdRegExp(QStringLiteral(
+ R"((^|[^<>/])([!#@][-a-z0-9_=/.]{1,252}:[-.a-z0-9]+))"
), RegExpOptions);
- // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,&
+ // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,&,"
htmlEscapedText.replace(EmailAddressRegExp,
QStringLiteral(R"(<a href="mailto:\2">\1\2</a>)"));
htmlEscapedText.replace(FullUrlRegExp,
QStringLiteral(R"(<a href="\1">\1</a>)"));
+ htmlEscapedText.replace(MxIdRegExp,
+ QStringLiteral(R"(\1<a href="https://matrix.to/#/\2">\2</a>)"));
+}
+
+QString QMatrixClient::sanitized(const QString& plainText)
+{
+ auto text = plainText;
+ text.remove(QChar(0x202e));
+ text.remove(QChar(0x202d));
+ return text;
}
QString QMatrixClient::prettyPrint(const QString& plainText)
{
auto pt = QStringLiteral("<span style='white-space:pre-wrap'>") +
- plainText.toHtmlEscaped() + QStringLiteral("</span>");
+ plainText.toHtmlEscaped() + QStringLiteral("</span>");
pt.replace('\n', QStringLiteral("<br/>"));
linkifyUrls(pt);
return pt;
}
+
+QString QMatrixClient::cacheLocation(const QString& dirName)
+{
+ const QString cachePath =
+ QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
+ % '/' % dirName % '/';
+ QDir dir;
+ if (!dir.exists(cachePath))
+ dir.mkpath(cachePath);
+ return cachePath;
+}
+
+// Tests for function_traits<>
+
+#ifdef Q_CC_CLANG
+#pragma clang diagnostic push
+#pragma ide diagnostic ignored "OCSimplifyInspection"
+#endif
+using namespace QMatrixClient;
+
+int f();
+static_assert(std::is_same<fn_return_t<decltype(f)>, int>::value,
+ "Test fn_return_t<>");
+
+void f1(int);
+static_assert(function_traits<decltype(f1)>::arg_number == 1,
+ "Test fn_arg_number");
+
+void f2(int, QString);
+static_assert(std::is_same<fn_arg_t<decltype(f2), 1>, QString>::value,
+ "Test fn_arg_t<>");
+
+struct S { int mf(); };
+static_assert(is_callable_v<decltype(&S::mf)>, "Test member function");
+static_assert(returns<int, decltype(&S::mf)>(), "Test returns<> with member function");
+
+struct Fo { int operator()(); };
+static_assert(is_callable_v<Fo>, "Test is_callable<> with function object");
+static_assert(function_traits<Fo>::arg_number == 0, "Test function object");
+static_assert(std::is_same<fn_return_t<Fo>, int>::value,
+ "Test return type of function object");
+
+struct Fo1 { void operator()(int); };
+static_assert(function_traits<Fo1>::arg_number == 1, "Test function object 1");
+static_assert(is_callable_v<Fo1>, "Test is_callable<> with function object 1");
+static_assert(std::is_same<fn_arg_t<Fo1>, int>(),
+ "Test fn_arg_t defaulting to first argument");
+
+#if (!defined(_MSC_VER) || _MSC_VER >= 1910)
+static auto l = [] { return 1; };
+static_assert(is_callable_v<decltype(l)>, "Test is_callable_v<> with lambda");
+static_assert(std::is_same<fn_return_t<decltype(l)>, int>::value,
+ "Test fn_return_t<> with lambda");
+#endif
+
+template <typename T>
+struct fn_object
+{
+ static int smf(double) { return 0; }
+};
+template <>
+struct fn_object<QString>
+{
+ void operator()(QString);
+};
+static_assert(is_callable_v<fn_object<QString>>, "Test function object");
+static_assert(returns<void, fn_object<QString>>(),
+ "Test returns<> with function object");
+static_assert(!is_callable_v<fn_object<int>>, "Test non-function object");
+// FIXME: These two don't work
+//static_assert(is_callable_v<decltype(&fn_object<int>::smf)>,
+// "Test static member function");
+//static_assert(returns<int, decltype(&fn_object<int>::smf)>(),
+// "Test returns<> with static member function");
+
+template <typename T>
+QString ft(T&&) { return {}; }
+static_assert(std::is_same<fn_arg_t<decltype(ft<QString>)>, QString&&>(),
+ "Test function templates");
+
+#ifdef Q_CC_CLANG
+#pragma clang diagnostic pop
+#endif
diff --git a/lib/util.h b/lib/util.h
index 13eec143..beb3c697 100644
--- a/lib/util.h
+++ b/lib/util.h
@@ -18,8 +18,9 @@
#pragma once
-#include <QtCore/QPointer>
-#if (QT_VERSION < QT_VERSION_CHECK(5, 5, 0))
+#include <QtCore/QLatin1String>
+
+#if QT_VERSION < QT_VERSION_CHECK(5, 5, 0)
#include <QtCore/QMetaEnum>
#include <QtCore/QDebug>
#endif
@@ -27,10 +28,12 @@
#include <functional>
#include <memory>
-#if __cplusplus >= 201703L
+#if __has_cpp_attribute(fallthrough)
#define FALLTHROUGH [[fallthrough]]
#elif __has_cpp_attribute(clang::fallthrough)
#define FALLTHROUGH [[clang::fallthrough]]
+#elif __has_cpp_attribute(gnu::fallthrough)
+#define FALLTHROUGH [[gnu::fallthrough]]
#else
#define FALLTHROUGH // -fallthrough
#endif
@@ -49,6 +52,14 @@ template <typename T>
static void qAsConst(const T &&) Q_DECL_EQ_DELETE;
#endif
+// MSVC 2015 and older GCC's don't handle initialisation from initializer lists
+// right in the absense of a constructor; MSVC 2015, notably, fails with
+// "error C2440: 'return': cannot convert from 'initializer list' to '<type>'"
+#if (defined(_MSC_VER) && _MSC_VER < 1910) || \
+ (defined(__GNUC__) && !defined(__clang__) && __GNUC__ <= 4)
+# define BROKEN_INITIALIZER_LISTS
+#endif
+
namespace QMatrixClient
{
// The below enables pretty-printing of enums in logs
@@ -86,74 +97,150 @@ namespace QMatrixClient
static_assert(!std::is_reference<T>::value,
"You cannot make an Omittable<> with a reference type");
public:
+ using value_type = std::decay_t<T>;
+
explicit Omittable() : Omittable(none) { }
- Omittable(NoneTag) : _value(std::decay_t<T>()), _omitted(true) { }
- Omittable(const std::decay_t<T>& val) : _value(val) { }
- Omittable(std::decay_t<T>&& val) : _value(std::move(val)) { }
- Omittable<T>& operator=(const std::decay_t<T>& val)
+ Omittable(NoneTag) : _value(value_type()), _omitted(true) { }
+ Omittable(const value_type& val) : _value(val) { }
+ Omittable(value_type&& val) : _value(std::move(val)) { }
+ Omittable<T>& operator=(const value_type& val)
{
_value = val;
_omitted = false;
return *this;
}
- Omittable<T>& operator=(std::decay_t<T>&& val)
+ Omittable<T>& operator=(value_type&& val)
{
+ // For some reason GCC complains about -Wmaybe-uninitialized
+ // in the context of using Omittable<bool> with converters.h;
+ // though the logic looks very much benign (GCC bug???)
_value = std::move(val);
_omitted = false;
return *this;
}
+ bool operator==(const value_type& rhs) const
+ {
+ return !omitted() && value() == rhs;
+ }
+ friend bool operator==(const value_type& lhs,
+ const Omittable<value_type>& rhs)
+ {
+ return rhs == lhs;
+ }
+ bool operator!=(const value_type& rhs) const
+ {
+ return !operator==(rhs);
+ }
+ friend bool operator!=(const value_type& lhs,
+ const Omittable<value_type>& rhs)
+ {
+ return !(rhs == lhs);
+ }
+
bool omitted() const { return _omitted; }
- const std::decay_t<T>& value() const { Q_ASSERT(!_omitted); return _value; }
- std::decay_t<T>& value() { Q_ASSERT(!_omitted); return _value; }
- std::decay_t<T>&& release() { _omitted = true; return std::move(_value); }
+ const value_type& value() const
+ {
+ Q_ASSERT(!_omitted);
+ return _value;
+ }
+ value_type& editValue()
+ {
+ _omitted = false;
+ return _value;
+ }
+ /// Merge the value from another Omittable
+ /// \return true if \p other is not omitted and the value of
+ /// the current Omittable was different (or omitted);
+ /// in other words, if the current Omittable has changed;
+ /// false otherwise
+ template <typename T1>
+ auto merge(const Omittable<T1>& other)
+ -> std::enable_if_t<std::is_convertible<T1, T>::value, bool>
+ {
+ if (other.omitted() ||
+ (!_omitted && _value == other.value()))
+ return false;
+ _omitted = false;
+ _value = other.value();
+ return true;
+ }
+ value_type&& release() { _omitted = true; return std::move(_value); }
- operator bool() const { return !omitted(); }
- const std::decay<T>* operator->() const { return &value(); }
- std::decay_t<T>* operator->() { return &value(); }
- const std::decay_t<T>& operator*() const { return value(); }
- std::decay_t<T>& operator*() { return value(); }
+ const value_type* operator->() const & { return &value(); }
+ value_type* operator->() & { return &editValue(); }
+ const value_type& operator*() const & { return value(); }
+ value_type& operator*() & { return editValue(); }
private:
T _value;
bool _omitted = false;
};
+ namespace _impl {
+ template <typename AlwaysVoid, typename> struct fn_traits;
+ }
+
/** Determine traits of an arbitrary function/lambda/functor
- * This only works with arity of 1 (1-argument) for now but is extendable
- * to other cases. Also, doesn't work with generic lambdas and function
- * objects that have operator() overloaded
+ * Doesn't work with generic lambdas and function objects that have
+ * operator() overloaded.
* \sa https://stackoverflow.com/questions/7943525/is-it-possible-to-figure-out-the-parameter-type-and-return-type-of-a-lambda#7943765
*/
template <typename T>
- struct function_traits : public function_traits<decltype(&T::operator())>
- { }; // A generic function object that has (non-overloaded) operator()
+ struct function_traits : public _impl::fn_traits<void, T> {};
// Specialisation for a function
- template <typename ReturnT, typename ArgT>
- struct function_traits<ReturnT(ArgT)>
+ template <typename ReturnT, typename... ArgTs>
+ struct function_traits<ReturnT(ArgTs...)>
{
+ static constexpr auto is_callable = true;
using return_type = ReturnT;
- using arg_type = ArgT;
+ using arg_types = std::tuple<ArgTs...>;
+ using function_type = std::function<ReturnT(ArgTs...)>;
+ static constexpr auto arg_number = std::tuple_size<arg_types>::value;
};
- // Specialisation for a member function
- template <typename ReturnT, typename ClassT, typename ArgT>
- struct function_traits<ReturnT(ClassT::*)(ArgT)>
- : function_traits<ReturnT(ArgT)>
- { };
+ namespace _impl {
+ template <typename AlwaysVoid, typename T>
+ struct fn_traits
+ {
+ static constexpr auto is_callable = false;
+ };
+
+ template <typename T>
+ struct fn_traits<decltype(void(&T::operator())), T>
+ : public fn_traits<void, decltype(&T::operator())>
+ { }; // A generic function object that has (non-overloaded) operator()
- // Specialisation for a const member function
- template <typename ReturnT, typename ClassT, typename ArgT>
- struct function_traits<ReturnT(ClassT::*)(ArgT) const>
- : function_traits<ReturnT(ArgT)>
- { };
+ // Specialisation for a member function
+ template <typename ReturnT, typename ClassT, typename... ArgTs>
+ struct fn_traits<void, ReturnT(ClassT::*)(ArgTs...)>
+ : function_traits<ReturnT(ArgTs...)>
+ { };
+
+ // Specialisation for a const member function
+ template <typename ReturnT, typename ClassT, typename... ArgTs>
+ struct fn_traits<void, ReturnT(ClassT::*)(ArgTs...) const>
+ : function_traits<ReturnT(ArgTs...)>
+ { };
+ } // namespace _impl
template <typename FnT>
using fn_return_t = typename function_traits<FnT>::return_type;
- template <typename FnT>
- using fn_arg_t = typename function_traits<FnT>::arg_type;
+ template <typename FnT, int ArgN = 0>
+ using fn_arg_t =
+ std::tuple_element_t<ArgN, typename function_traits<FnT>::arg_types>;
+
+ template <typename R, typename FnT>
+ constexpr bool returns()
+ {
+ return std::is_same<fn_return_t<FnT>, R>::value;
+ }
+
+ // Poor-man's is_invokable
+ template <typename T>
+ constexpr auto is_callable_v = function_traits<T>::is_callable;
inline auto operator"" _ls(const char* s, std::size_t size)
{
@@ -209,36 +296,20 @@ namespace QMatrixClient
return std::make_pair(last, sLast);
}
- /** A guard pointer that disconnects an interested object upon destruction
- * It's almost QPointer<> except that you have to initialise it with one
- * more additional parameter - a pointer to a QObject that will be
- * disconnected from signals of the underlying pointer upon the guard's
- * destruction.
+ /** Sanitize the text before showing in HTML
+ * This does toHtmlEscaped() and removes Unicode BiDi marks.
*/
- template <typename T>
- class ConnectionsGuard : public QPointer<T>
- {
- public:
- ConnectionsGuard(T* publisher, QObject* subscriber)
- : QPointer<T>(publisher), subscriber(subscriber)
- { }
- ~ConnectionsGuard()
- {
- if (*this)
- (*this)->disconnect(subscriber);
- }
- ConnectionsGuard(ConnectionsGuard&&) = default;
- ConnectionsGuard& operator=(ConnectionsGuard&&) = default;
- Q_DISABLE_COPY(ConnectionsGuard)
- using QPointer<T>::operator=;
+ QString sanitized(const QString& plainText);
- private:
- QObject* subscriber;
- };
-
- /** Pretty-prints plain text into HTML
+ /** Pretty-print plain text into HTML
* This includes HTML escaping of <,>,",& and URLs linkification.
*/
QString prettyPrint(const QString& plainText);
+
+ /** Return a path to cache directory after making sure that it exists
+ * The returned path has a trailing slash, clients don't need to append it.
+ * \param dir path to cache directory relative to the standard cache path
+ */
+ QString cacheLocation(const QString& dirName);
} // namespace QMatrixClient