aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml1
-rw-r--r--CONTRIBUTING.md17
-rw-r--r--README.md8
-rw-r--r--SECURITY.md32
-rw-r--r--lib/connection.cpp112
-rw-r--r--lib/connection.h14
-rw-r--r--lib/csapi/capabilities.cpp2
-rw-r--r--lib/csapi/content-repo.cpp2
-rw-r--r--lib/csapi/create_room.cpp2
-rw-r--r--lib/csapi/filter.cpp2
-rw-r--r--lib/csapi/joining.cpp4
-rw-r--r--lib/csapi/keys.cpp2
-rw-r--r--lib/csapi/list_joined_rooms.cpp2
-rw-r--r--lib/csapi/notifications.cpp2
-rw-r--r--lib/csapi/openid.cpp8
-rw-r--r--lib/csapi/presence.cpp2
-rw-r--r--lib/csapi/pushrules.cpp6
-rw-r--r--lib/csapi/registration.cpp2
-rw-r--r--lib/csapi/room_upgrades.cpp2
-rw-r--r--lib/csapi/search.cpp2
-rw-r--r--lib/csapi/users.cpp4
-rw-r--r--lib/csapi/versions.cpp2
-rw-r--r--lib/csapi/whoami.cpp2
-rw-r--r--lib/csapi/{{base}}.cpp.mustache2
-rw-r--r--lib/events/event.h6
-rw-r--r--lib/events/eventcontent.h8
-rw-r--r--lib/events/eventloader.h20
-rw-r--r--lib/events/roomevent.cpp2
-rw-r--r--lib/events/roommemberevent.h10
-rw-r--r--lib/events/simplestateevents.h24
-rw-r--r--lib/events/stateevent.cpp8
-rw-r--r--lib/events/stateevent.h21
-rw-r--r--lib/jobs/basejob.cpp95
-rw-r--r--lib/jobs/basejob.h47
-rw-r--r--lib/joinstate.h2
-rw-r--r--lib/room.cpp113
-rw-r--r--lib/room.h26
-rw-r--r--lib/user.cpp22
-rw-r--r--lib/util.cpp21
-rw-r--r--lib/util.h8
40 files changed, 439 insertions, 228 deletions
diff --git a/.travis.yml b/.travis.yml
index 859cabfc..4654f60c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -24,6 +24,7 @@ matrix:
env: [ 'PATH=/usr/local/opt/qt/bin:$PATH' ]
addons:
homebrew:
+ update: true
packages:
- qt5
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 56bc9d91..37dfa77e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -99,22 +99,7 @@ Any components proposed for reuse should have a license that permits releasing
a derivative work under *LGPL v2.1 or later* or LGPL v3. Moreover, the license of
a proposed component should be approved by OSI, no exceptions.
-## Vulnerability reporting (security issues)
-
-If you find a significant vulnerability, or evidence of one,
-use either of the following contacts:
-* send an email to Kitsune Ral [Kitsune-Ral@users.sf.net](mailto:Kitsune-Ral@users.sf.net)
-* reach out in Matrix to #kitsune:matrix.org (if you can, switch encryption **on**)
-
-In any of these two options, _indicate that you have such information_
-(do not share the information yet), and we'll tell you the next steps.
-
-By default, we will give credit to anyone who reports a vulnerability in
-a responsible way so that we can fix it before public disclosure. If you want
-to remain anonymous or pseudonymous instead, please let us know; we will
-gladly respect your wishes. If you provide a fix as a PR, you have no way
-to remain anonymous (and you also disclose the vulnerability thereby) so this
-is not the right way, unless the vulnerability is already made public.
+## Vulnerability reporting (security issues) - see [SECURITY.md](./SECURITY.md)
## Documentation changes
diff --git a/README.md b/README.md
index 857543e1..787fa0df 100644
--- a/README.md
+++ b/README.md
@@ -12,9 +12,13 @@
libQMatrixClient is a Qt5-based library to make IM clients for the [Matrix](https://matrix.org) protocol. It is the backbone of [Quaternion](https://github.com/QMatrixClient/Quaternion), [Spectral](https://matrix.org/docs/projects/client/spectral.html) and some other projects.
## Contacts
-You can find authors of libQMatrixClient in the Matrix room: [#qmatrixclient:matrix.org](https://matrix.to/#/#qmatrixclient:matrix.org).
+You can find authors of libQMatrixClient in the Matrix room:
+[#qmatrixclient:matrix.org](https://matrix.to/#/#qmatrixclient:matrix.org).
-You can also file issues at [the project's issue tracker](https://github.com/QMatrixClient/libqmatrixclient/issues). If you have what looks like a security issue, please see respective instructions in CONTRIBUTING.md.
+You can also file issues at
+[the project's issue tracker](https://github.com/QMatrixClient/libqmatrixclient/issues).
+If you find what looks like a security issue, please use instructions
+in SECURITY.md.
## Building and usage
So far the library is typically used as a git submodule of another project
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..086258bd
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,32 @@
+# Security Policy
+
+## Supported Versions
+
+| Version | Supported |
+| ------- | ------------------ |
+| master | :white_check_mark: |
+| 0.5.x | :white_check_mark: |
+| older | :x: |
+
+## Reporting a Vulnerability
+
+If you find a significant vulnerability, or evidence of one, use either of the following contacts:
+- send an email to [Kitsune Ral](mailto:Kitsune-Ral@users.sf.net); or
+- reach out in Matrix to [@kitsune:matrix.org](https://matrix.to/#/@kitsune:matrix.org) (if you can, switch encryption on).
+
+In any of these two options, indicate that you have such information (do not share it yet), and we'll tell you the next steps.
+
+By default, we will give credit to anyone who reports a vulnerability in a responsible way so that we can fix it before public disclosure.
+If you want to remain anonymous or pseudonymous instead, please let us know; we will gladly respect your wishes.
+If you provide a fix as a PR, you have no way to remain anonymous; you also thereby lay out the vulnerability itself
+so this is NOT the right way for undisclosed vulnerabilities, whether or not you want to stay incognito.
+
+## Timeline and commitments
+
+Initial reaction to the message about a vulnerability (see above) will be no more than 5 days. From the moment of the private report or
+public disclosure (if it hasn't been reported earlier in private) of each vulnerability, we take effort to fix it on priority before
+any other issues. In case of vulnerabilities with [CVSS v2](https://nvd.nist.gov/cvss.cfm) score of 4.0 and higher the commitment is
+to provide a workaround within 30 days and a full fix within 60 days after the specific information on the vulnerability has been
+reported to the project by any means (in private or in public). For vulnerabilities with lower score there is no commitment on the timeline,
+only prioritisation. The full fix doesn't imply that all software functionality remains accessible (in the worst case
+the vulnerable functionality may be disabled or removed to prevent the attack).
diff --git a/lib/connection.cpp b/lib/connection.cpp
index d7c3d78f..20fb367c 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -83,8 +83,9 @@ class Connection::Private
// separately; specifically, we should keep objects for Invite and
// Leave state of the same room if the two happen to co-exist.
QHash<QPair<QString, bool>, Room*> roomMap;
- // Mapping from aliases to room ids, as per the last sync
- QHash<QString, QString> roomAliasMap;
+ /// Mapping from serverparts to alias/room id mappings,
+ /// as of the last sync
+ QHash<QString, QHash<QString, QString>> roomAliasMap;
QVector<QString> roomIdsToForget;
QVector<Room*> firstTimeRooms;
QVector<QString> pendingStateRoomIds;
@@ -113,6 +114,7 @@ class Connection::Private
void connectWithToken(const QString& user, const QString& accessToken,
const QString& deviceId);
+ void removeRoom(const QString& roomId);
template <typename EventT>
EventT* unpackAccountData() const
@@ -160,20 +162,31 @@ Connection::~Connection()
stopSync();
}
-void Connection::resolveServer(const QString& mxidOrDomain)
+static const auto ServerPartRegEx = QStringLiteral(
+ "(\\[[^]]+\\]|[^:@]+)" // Either IPv6 address or hostname/IPv4 address
+ "(?::(\\d{1,5}))?" // Optional port
+);
+
+QString serverPart(const QString& mxId)
{
- // At this point we may have something as complex as
- // @username:[IPv6:address]:port, or as simple as a plain domain name.
+ static QString re = "^[@!#$+].+?:(" // Localpart and colon
+ % ServerPartRegEx % ")$";
+ static QRegularExpression parser(re,
+ QRegularExpression::UseUnicodePropertiesOption); // Because Asian digits
+ return parser.match(mxId).captured(1);
+}
- // Try to parse as an FQID; if there's no @ part, assume it's a domain name.
- QRegularExpression parser(
+void Connection::resolveServer(const QString& mxidOrDomain)
+{
+ // mxIdOrDomain may be something as complex as
+ // @username:[IPv6:address]:port, or as simple as a plain serverpart.
+ static QRegularExpression parser(
"^(@.+?:)?" // Optional username (allow everything for compatibility)
- "(\\[[^]]+\\]|[^:@]+)" // Either IPv6 address or hostname/IPv4 address
- "(:\\d{1,5})?$", // Optional port
- QRegularExpression::UseUnicodePropertiesOption); // Because asian digits
+ % ServerPartRegEx % '$',
+ QRegularExpression::UseUnicodePropertiesOption); // Because Asian digits
auto match = parser.match(mxidOrDomain);
- QUrl maybeBaseUrl = QUrl::fromUserInput(match.captured(2));
+ auto maybeBaseUrl = QUrl::fromUserInput(match.captured(2));
maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http"
if (!match.hasMatch() || !maybeBaseUrl.isValid())
{
@@ -800,29 +813,36 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id)
if (room && room->joinState() != JoinState::Leave)
{
auto leaveJob = room->leaveRoom();
- connect(leaveJob, &BaseJob::success, this, [this, forgetJob, room] {
- forgetJob->start(connectionData());
- // If the matching /sync response hasn't arrived yet, mark the room
- // for explicit deletion
- if (room->joinState() != JoinState::Leave)
- d->roomIdsToForget.push_back(room->id());
- });
+ connect(leaveJob, &BaseJob::result, this,
+ [this, leaveJob, forgetJob, room] {
+ if (leaveJob->error() == BaseJob::Success
+ || leaveJob->error() == BaseJob::NotFoundError)
+ {
+ forgetJob->start(connectionData());
+ // If the matching /sync response hasn't arrived yet,
+ // mark the room for explicit deletion
+ if (room->joinState() != JoinState::Leave)
+ d->roomIdsToForget.push_back(room->id());
+ } else {
+ qCWarning(MAIN).nospace()
+ << "Error leaving room " << room->objectName()
+ << ": " << leaveJob->errorString();
+ forgetJob->abandon();
+ }
+ });
connect(leaveJob, &BaseJob::failure, forgetJob, &BaseJob::abandon);
}
else
forgetJob->start(connectionData());
- connect(forgetJob, &BaseJob::success, this, [this, id]
+ connect(forgetJob, &BaseJob::result, this, [this, id, forgetJob]
{
- // Delete whatever instances of the room are still in the map.
- for (auto f: {false, true})
- if (auto r = d->roomMap.take({ id, f }))
- {
- qCDebug(MAIN) << "Room" << r->objectName()
- << "in state" << toCString(r->joinState())
- << "will be deleted";
- emit r->beforeDestruction(r);
- r->deleteLater();
- }
+ // Leave room in case of success, or room not known by server
+ if(forgetJob->error() == BaseJob::Success
+ || forgetJob->error() == BaseJob::NotFoundError)
+ d->removeRoom(id); // Delete the room from roomMap
+ else
+ qCWarning(MAIN).nospace() << "Error forgetting room " << id << ": "
+ << forgetJob->errorString();
});
return forgetJob;
}
@@ -885,33 +905,36 @@ Room* Connection::room(const QString& roomId, JoinStates states) const
Room* Connection::roomByAlias(const QString& roomAlias, JoinStates states) const
{
- const auto id = d->roomAliasMap.value(roomAlias);
+ const auto id =
+ d->roomAliasMap.value(serverPart(roomAlias)).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 QString& aliasServer,
const QStringList& previousRoomAliases,
const QStringList& roomAliases)
{
+ auto& aliasMap = d->roomAliasMap[aliasServer]; // Allocate if necessary
for (const auto& a: previousRoomAliases)
- if (d->roomAliasMap.remove(a) == 0)
+ if (aliasMap.remove(a) == 0)
qCWarning(MAIN) << "Alias" << a << "is not found (already deleted?)";
for (const auto& a: roomAliases)
{
- auto& mappedId = d->roomAliasMap[a];
+ auto& mappedId = aliasMap[a];
if (!mappedId.isEmpty())
{
if (mappedId == roomId)
- qCDebug(MAIN) << "Alias" << a << "is already mapped to room"
+ qCDebug(MAIN) << "Alias" << a << "is already mapped to"
<< roomId;
else
- qCWarning(MAIN) << "Alias" << a
- << "will be force-remapped from room"
+ qCWarning(MAIN) << "Alias" << a << "will be force-remapped from"
<< mappedId << "to" << roomId;
}
mappedId = roomId;
@@ -960,11 +983,6 @@ QString Connection::deviceId() const
return d->data->deviceId();
}
-QString Connection::token() const
-{
- return accessToken();
-}
-
QByteArray Connection::accessToken() const
{
return d->data->accessToken();
@@ -1066,6 +1084,20 @@ Connection::DirectChatsMap Connection::directChats() const
return d->directChats;
}
+// Removes room with given id from roomMap
+void Connection::Private::removeRoom(const QString& roomId)
+{
+ for (auto f: {false, true})
+ if (auto r = roomMap.take({ roomId, f }))
+ {
+ qCDebug(MAIN) << "Room" << r->objectName()
+ << "in state" << toCString(r->joinState())
+ << "will be deleted";
+ emit r->beforeDestruction(r);
+ r->deleteLater();
+ }
+}
+
void Connection::addToDirectChats(const Room* room, User* user)
{
Q_ASSERT(room != nullptr && user != nullptr);
diff --git a/lib/connection.h b/lib/connection.h
index cc2feed8..4ab8d5ba 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -122,7 +122,7 @@ namespace QMatrixClient
explicit Connection(QObject* parent = nullptr);
explicit Connection(const QUrl& server, QObject* parent = nullptr);
- virtual ~Connection();
+ ~Connection() override;
/** Get all Invited and Joined rooms
* \return a hashmap from a composite key - room name and whether
@@ -250,11 +250,13 @@ namespace QMatrixClient
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
+ /// This is used to maintain the internal index of room aliases.
+ /// It does NOT change aliases on the server,
+ /// \sa Room::setLocalAliases
void updateRoomAliases(const QString& roomId,
- const QStringList& previousRoomAliases,
- const QStringList& roomAliases);
+ const QString& aliasServer,
+ 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;
@@ -265,8 +267,6 @@ namespace QMatrixClient
Q_INVOKABLE SyncJob* syncJob() const;
Q_INVOKABLE int millisToReconnect() const;
- [[deprecated("Use accessToken() instead")]]
- Q_INVOKABLE QString token() const;
Q_INVOKABLE void getTurnServers();
struct SupportedRoomVersion
diff --git a/lib/csapi/capabilities.cpp b/lib/csapi/capabilities.cpp
index 210423f5..fb506784 100644
--- a/lib/csapi/capabilities.cpp
+++ b/lib/csapi/capabilities.cpp
@@ -76,7 +76,7 @@ BaseJob::Status GetCapabilitiesJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("capabilities"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'capabilities' not found in the response" };
fromJson(json.value("capabilities"_ls), d->capabilities);
return Success;
diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp
index 22223985..7e490604 100644
--- a/lib/csapi/content-repo.cpp
+++ b/lib/csapi/content-repo.cpp
@@ -50,7 +50,7 @@ BaseJob::Status UploadContentJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("content_uri"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'content_uri' not found in the response" };
fromJson(json.value("content_uri"_ls), d->contentUri);
return Success;
diff --git a/lib/csapi/create_room.cpp b/lib/csapi/create_room.cpp
index 448547ae..3101152a 100644
--- a/lib/csapi/create_room.cpp
+++ b/lib/csapi/create_room.cpp
@@ -77,7 +77,7 @@ BaseJob::Status CreateRoomJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("room_id"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'room_id' not found in the response" };
fromJson(json.value("room_id"_ls), d->roomId);
return Success;
diff --git a/lib/csapi/filter.cpp b/lib/csapi/filter.cpp
index 982e60b5..9f412d53 100644
--- a/lib/csapi/filter.cpp
+++ b/lib/csapi/filter.cpp
@@ -39,7 +39,7 @@ BaseJob::Status DefineFilterJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("filter_id"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'filter_id' not found in the response" };
fromJson(json.value("filter_id"_ls), d->filterId);
return Success;
diff --git a/lib/csapi/joining.cpp b/lib/csapi/joining.cpp
index 00d930fa..544f442f 100644
--- a/lib/csapi/joining.cpp
+++ b/lib/csapi/joining.cpp
@@ -57,7 +57,7 @@ BaseJob::Status JoinRoomByIdJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("room_id"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'room_id' not found in the response" };
fromJson(json.value("room_id"_ls), d->roomId);
return Success;
@@ -124,7 +124,7 @@ BaseJob::Status JoinRoomJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("room_id"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'room_id' not found in the response" };
fromJson(json.value("room_id"_ls), d->roomId);
return Success;
diff --git a/lib/csapi/keys.cpp b/lib/csapi/keys.cpp
index 6c16a8a3..5bbc1aab 100644
--- a/lib/csapi/keys.cpp
+++ b/lib/csapi/keys.cpp
@@ -42,7 +42,7 @@ BaseJob::Status UploadKeysJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("one_time_key_counts"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'one_time_key_counts' not found in the response" };
fromJson(json.value("one_time_key_counts"_ls), d->oneTimeKeyCounts);
return Success;
diff --git a/lib/csapi/list_joined_rooms.cpp b/lib/csapi/list_joined_rooms.cpp
index 85a9cae4..297a5ae0 100644
--- a/lib/csapi/list_joined_rooms.cpp
+++ b/lib/csapi/list_joined_rooms.cpp
@@ -44,7 +44,7 @@ BaseJob::Status GetJoinedRoomsJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("joined_rooms"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'joined_rooms' not found in the response" };
fromJson(json.value("joined_rooms"_ls), d->joinedRooms);
return Success;
diff --git a/lib/csapi/notifications.cpp b/lib/csapi/notifications.cpp
index c00b7cb0..5d3bdb47 100644
--- a/lib/csapi/notifications.cpp
+++ b/lib/csapi/notifications.cpp
@@ -80,7 +80,7 @@ BaseJob::Status GetNotificationsJob::parseJson(const QJsonDocument& data)
auto json = data.object();
fromJson(json.value("next_token"_ls), d->nextToken);
if (!json.contains("notifications"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'notifications' not found in the response" };
fromJson(json.value("notifications"_ls), d->notifications);
return Success;
diff --git a/lib/csapi/openid.cpp b/lib/csapi/openid.cpp
index b27fe0b8..03d24790 100644
--- a/lib/csapi/openid.cpp
+++ b/lib/csapi/openid.cpp
@@ -57,19 +57,19 @@ BaseJob::Status RequestOpenIdTokenJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("access_token"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'access_token' not found in the response" };
fromJson(json.value("access_token"_ls), d->accessToken);
if (!json.contains("token_type"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'token_type' not found in the response" };
fromJson(json.value("token_type"_ls), d->tokenType);
if (!json.contains("matrix_server_name"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'matrix_server_name' not found in the response" };
fromJson(json.value("matrix_server_name"_ls), d->matrixServerName);
if (!json.contains("expires_in"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'expires_in' not found in the response" };
fromJson(json.value("expires_in"_ls), d->expiresIn);
return Success;
diff --git a/lib/csapi/presence.cpp b/lib/csapi/presence.cpp
index 024d7a34..210ee0ae 100644
--- a/lib/csapi/presence.cpp
+++ b/lib/csapi/presence.cpp
@@ -74,7 +74,7 @@ BaseJob::Status GetPresenceJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("presence"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'presence' not found in the response" };
fromJson(json.value("presence"_ls), d->presence);
fromJson(json.value("last_active_ago"_ls), d->lastActiveAgo);
diff --git a/lib/csapi/pushrules.cpp b/lib/csapi/pushrules.cpp
index b91d18f7..9b5b7cd1 100644
--- a/lib/csapi/pushrules.cpp
+++ b/lib/csapi/pushrules.cpp
@@ -44,7 +44,7 @@ BaseJob::Status GetPushRulesJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("global"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'global' not found in the response" };
fromJson(json.value("global"_ls), d->global);
return Success;
@@ -152,7 +152,7 @@ BaseJob::Status IsPushRuleEnabledJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("enabled"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'enabled' not found in the response" };
fromJson(json.value("enabled"_ls), d->enabled);
return Success;
@@ -201,7 +201,7 @@ BaseJob::Status GetPushRuleActionsJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("actions"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'actions' not found in the response" };
fromJson(json.value("actions"_ls), d->actions);
return Success;
diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp
index 5dc9c1e5..76741a50 100644
--- a/lib/csapi/registration.cpp
+++ b/lib/csapi/registration.cpp
@@ -74,7 +74,7 @@ BaseJob::Status RegisterJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("user_id"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'user_id' not found in the response" };
fromJson(json.value("user_id"_ls), d->userId);
fromJson(json.value("access_token"_ls), d->accessToken);
diff --git a/lib/csapi/room_upgrades.cpp b/lib/csapi/room_upgrades.cpp
index f58fd675..f80c3aba 100644
--- a/lib/csapi/room_upgrades.cpp
+++ b/lib/csapi/room_upgrades.cpp
@@ -41,7 +41,7 @@ BaseJob::Status UpgradeRoomJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("replacement_room"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'replacement_room' not found in the response" };
fromJson(json.value("replacement_room"_ls), d->replacementRoom);
return Success;
diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp
index a5f83c79..ad2c34a3 100644
--- a/lib/csapi/search.cpp
+++ b/lib/csapi/search.cpp
@@ -164,7 +164,7 @@ BaseJob::Status SearchJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("search_categories"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'search_categories' not found in the response" };
fromJson(json.value("search_categories"_ls), d->searchCategories);
return Success;
diff --git a/lib/csapi/users.cpp b/lib/csapi/users.cpp
index 97d8962d..0d867145 100644
--- a/lib/csapi/users.cpp
+++ b/lib/csapi/users.cpp
@@ -63,11 +63,11 @@ BaseJob::Status SearchUserDirectoryJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("results"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'results' not found in the response" };
fromJson(json.value("results"_ls), d->results);
if (!json.contains("limited"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'limited' not found in the response" };
fromJson(json.value("limited"_ls), d->limited);
return Success;
diff --git a/lib/csapi/versions.cpp b/lib/csapi/versions.cpp
index 6ee6725d..4b7c4ced 100644
--- a/lib/csapi/versions.cpp
+++ b/lib/csapi/versions.cpp
@@ -50,7 +50,7 @@ BaseJob::Status GetVersionsJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("versions"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'versions' not found in the response" };
fromJson(json.value("versions"_ls), d->versions);
fromJson(json.value("unstable_features"_ls), d->unstableFeatures);
diff --git a/lib/csapi/whoami.cpp b/lib/csapi/whoami.cpp
index aebdf5d3..ce024c33 100644
--- a/lib/csapi/whoami.cpp
+++ b/lib/csapi/whoami.cpp
@@ -44,7 +44,7 @@ BaseJob::Status GetTokenOwnerJob::parseJson(const QJsonDocument& data)
{
auto json = data.object();
if (!json.contains("user_id"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key 'user_id' not found in the response" };
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 ff888d76..010f9116 100644
--- a/lib/csapi/{{base}}.cpp.mustache
+++ b/lib/csapi/{{base}}.cpp.mustache
@@ -115,7 +115,7 @@ BaseJob::Status {{camelCaseOperationId}}Job::parseJson(const QJsonDocument& data
{{#inlineResponse}} fromJson(data, d->{{paramName}});
{{/inlineResponse}}{{^inlineResponse}} auto json = data.object();
{{#properties}}{{#required?}} if (!json.contains("{{baseName}}"_ls))
- return { JsonParseError,
+ return { IncorrectResponse,
"The key '{{baseName}}' not found in the response" };
{{/required?}} fromJson(json.value("{{baseName}}"_ls), d->{{paramName}});
{{/properties}}{{/inlineResponse}} return Success;
diff --git a/lib/events/event.h b/lib/events/event.h
index b7bbd83e..b3a58806 100644
--- a/lib/events/event.h
+++ b/lib/events/event.h
@@ -60,14 +60,16 @@ namespace QMatrixClient
static const auto ContentKey = QStringLiteral("content");
static const auto EventIdKey = QStringLiteral("event_id");
static const auto UnsignedKey = QStringLiteral("unsigned");
+ static const auto StateKeyKey = QStringLiteral("state_key");
static const auto TypeKeyL = "type"_ls;
static const auto ContentKeyL = "content"_ls;
static const auto EventIdKeyL = "event_id"_ls;
static const auto UnsignedKeyL = "unsigned"_ls;
static const auto RedactedCauseKeyL = "redacted_because"_ls;
static const auto PrevContentKeyL = "prev_content"_ls;
+ static const auto StateKeyKeyL = "state_key"_ls;
- // Minimal correct Matrix event JSON
+ /// Make a minimal correct Matrix event JSON
template <typename StrT>
inline QJsonObject basicEventJson(StrT matrixType,
const QJsonObject& content)
@@ -259,7 +261,7 @@ namespace QMatrixClient
}
template <typename T>
- T content(const QLatin1String& key) const
+ T content(QLatin1String key) const
{
return fromJson<T>(contentJson()[key]);
}
diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h
index ab31a75d..254eb9a9 100644
--- a/lib/events/eventcontent.h
+++ b/lib/events/eventcontent.h
@@ -53,6 +53,9 @@ namespace QMatrixClient
QJsonObject originalJson;
protected:
+ Base(const Base&) = default;
+ Base(Base&&) = default;
+
virtual void fillJson(QJsonObject* o) const = 0;
};
@@ -167,11 +170,14 @@ namespace QMatrixClient
class TypedBase: public Base
{
public:
- explicit TypedBase(const QJsonObject& o = {}) : Base(o) { }
+ explicit TypedBase(QJsonObject o = {}) : Base(std::move(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; }
+
+ protected:
+ using Base::Base;
};
/**
diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h
index da663392..d0fa60a2 100644
--- a/lib/events/eventloader.h
+++ b/lib/events/eventloader.h
@@ -32,7 +32,8 @@ namespace QMatrixClient {
}
}
- /** Create an event with proper type from a JSON object
+ /*! Create an event with proper type from a JSON object
+ *
* Use this factory template to detect the type from the JSON object
* contents (the detected event type should derive from the template
* parameter type) and create an event object of that type.
@@ -44,7 +45,8 @@ namespace QMatrixClient {
fullJson[TypeKeyL].toString());
}
- /** Create an event from a type string and content JSON
+ /*! Create an event from a type string and content JSON
+ *
* Use this factory template to resolve the C++ type from the Matrix
* type string in \p matrixType and create an event of that type that has
* its content part set to \p content.
@@ -57,6 +59,20 @@ namespace QMatrixClient {
matrixType);
}
+ /*! Create a state event from a type string, content JSON and state key
+ *
+ * Use this factory to resolve the C++ type from the Matrix type string
+ * in \p matrixType and create a state event of that type with content part
+ * set to \p content and state key set to \p stateKey (empty by default).
+ */
+ inline StateEventPtr loadStateEvent(const QString& matrixType,
+ const QJsonObject& content,
+ const QString& stateKey = {})
+ {
+ return _impl::loadEvent<StateEventBase>(
+ basicStateEventJson(matrixType, content, stateKey), matrixType);
+ }
+
template <typename EventT> struct JsonConverter<event_ptr_tt<EventT>>
{
static auto load(const QJsonValue& jv)
diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp
index 3d03509f..62c2a76d 100644
--- a/lib/events/roomevent.cpp
+++ b/lib/events/roomevent.cpp
@@ -78,7 +78,7 @@ QString RoomEvent::transactionId() const
QString RoomEvent::stateKey() const
{
- return fullJson()["state_key"_ls].toString();
+ return fullJson()[StateKeyKeyL].toString();
}
void RoomEvent::setTransactionId(const QString& txnId)
diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h
index b8224033..39aa280c 100644
--- a/lib/events/roommemberevent.h
+++ b/lib/events/roommemberevent.h
@@ -56,8 +56,14 @@ namespace QMatrixClient
explicit RoomMemberEvent(const QJsonObject& obj)
: StateEvent(typeId(), obj)
{ }
+ [[deprecated("Use RoomMemberEvent(userId, contentArgs) instead")]]
RoomMemberEvent(MemberEventContent&& c)
- : StateEvent(typeId(), matrixTypeId(), c)
+ : StateEvent(typeId(), matrixTypeId(), QString(), c)
+ { }
+ template <typename... ArgTs>
+ RoomMemberEvent(const QString& userId, ArgTs&&... contentArgs)
+ : StateEvent(typeId(), matrixTypeId(), userId,
+ std::forward<ArgTs>(contentArgs)...)
{ }
/// A special constructor to create unknown RoomMemberEvents
@@ -76,7 +82,7 @@ namespace QMatrixClient
MembershipType membership() const { return content().membership; }
QString userId() const
- { return fullJson()["state_key"_ls].toString(); }
+ { return fullJson()[StateKeyKeyL].toString(); }
bool isDirect() const { return content().isDirect; }
QString displayName() const { return content().displayName; }
QUrl avatarUrl() const { return content().avatarUrl; }
diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h
index 81401532..ef56c7b2 100644
--- a/lib/events/simplestateevents.h
+++ b/lib/events/simplestateevents.h
@@ -20,8 +20,6 @@
#include "stateevent.h"
-#include "converters.h"
-
namespace QMatrixClient
{
namespace EventContent
@@ -63,7 +61,7 @@ namespace QMatrixClient
explicit _Name() : _Name(value_type()) { } \
template <typename T> \
explicit _Name(T&& value) \
- : StateEvent(typeId(), matrixTypeId(), \
+ : StateEvent(typeId(), matrixTypeId(), QString(), \
QStringLiteral(#_ContentKey), \
std::forward<T>(value)) \
{ } \
@@ -78,13 +76,27 @@ namespace QMatrixClient
DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name)
DEFINE_EVENTTYPE_ALIAS(RoomName, RoomNameEvent)
- DEFINE_SIMPLE_STATE_EVENT(RoomAliasesEvent, "m.room.aliases",
- QStringList, aliases)
- DEFINE_EVENTTYPE_ALIAS(RoomAliases, RoomAliasesEvent)
DEFINE_SIMPLE_STATE_EVENT(RoomCanonicalAliasEvent, "m.room.canonical_alias",
QString, alias)
DEFINE_EVENTTYPE_ALIAS(RoomCanonicalAlias, RoomCanonicalAliasEvent)
DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", QString, topic)
DEFINE_EVENTTYPE_ALIAS(RoomTopic, RoomTopicEvent)
DEFINE_EVENTTYPE_ALIAS(RoomEncryption, EncryptionEvent)
+
+ class RoomAliasesEvent
+ : public StateEvent<EventContent::SimpleContent<QStringList>>
+ {
+ public:
+ DEFINE_EVENT_TYPEID("m.room.aliases", RoomAliasesEvent)
+ explicit RoomAliasesEvent(const QJsonObject& obj)
+ : StateEvent(typeId(), obj, QStringLiteral("aliases"))
+ { }
+ RoomAliasesEvent(const QString& server, const QStringList& aliases)
+ : StateEvent(typeId(), matrixTypeId(), server,
+ QStringLiteral("aliases"), aliases)
+ { }
+ QString server() const { return stateKey(); }
+ QStringList aliases() const { return content().value; }
+ };
+ REGISTER_EVENT_TYPE(RoomAliasesEvent)
} // namespace QMatrixClient
diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp
index a84f302b..6a6e7782 100644
--- a/lib/events/stateevent.cpp
+++ b/lib/events/stateevent.cpp
@@ -27,7 +27,7 @@ using namespace QMatrixClient;
RoomEvent::factory_t::addMethod(
[] (const QJsonObject& json, const QString& matrixType) -> StateEventPtr
{
- if (!json.contains("state_key"_ls))
+ if (!json.contains(StateKeyKeyL))
return nullptr;
if (auto e = StateEventBase::factory_t::make(json, matrixType))
@@ -36,6 +36,12 @@ using namespace QMatrixClient;
return makeEvent<StateEventBase>(unknownEventTypeId(), json);
});
+StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType,
+ const QString &stateKey,
+ const QJsonObject &contentJson)
+ : RoomEvent(type, basicStateEventJson(matrixType, contentJson, stateKey))
+{ }
+
bool StateEventBase::repeatsState() const
{
const auto prevContentJson = unsignedJson().value(PrevContentKeyL);
diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h
index 3f54f7bf..3b56a265 100644
--- a/lib/events/stateevent.h
+++ b/lib/events/stateevent.h
@@ -21,12 +21,28 @@
#include "roomevent.h"
namespace QMatrixClient {
+
+ /// Make a minimal correct Matrix state event JSON
+ template <typename StrT>
+ inline QJsonObject basicStateEventJson(StrT matrixType,
+ const QJsonObject& content, const QString& stateKey = {})
+ {
+ return { { TypeKey, std::forward<StrT>(matrixType) },
+ { StateKeyKey, stateKey },
+ { ContentKey, content } };
+ }
+
class StateEventBase: public RoomEvent
{
public:
using factory_t = EventFactory<StateEventBase>;
- using RoomEvent::RoomEvent;
+ StateEventBase(Type type, const QJsonObject& json)
+ : RoomEvent(type, json)
+ { }
+ StateEventBase(Type type, event_mtype_t matrixType,
+ const QString& stateKey = {},
+ const QJsonObject& contentJson = {});
~StateEventBase() override = default;
bool isStateEvent() const override { return true; }
@@ -83,8 +99,9 @@ namespace QMatrixClient {
}
template <typename... ContentParamTs>
explicit StateEvent(Type type, event_mtype_t matrixType,
+ const QString& stateKey,
ContentParamTs&&... contentParams)
- : StateEventBase(type, matrixType)
+ : StateEventBase(type, matrixType, stateKey)
, _content(std::forward<ContentParamTs>(contentParams)...)
{
editJson().insert(ContentKey, _content.toJson());
diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp
index 0d9b9f10..6ad8d971 100644
--- a/lib/jobs/basejob.cpp
+++ b/lib/jobs/basejob.cpp
@@ -289,56 +289,23 @@ void BaseJob::gotReply()
if (status().good())
setStatus(parseReply(d->reply.data()));
else {
- // FIXME: Factor out to smth like BaseJob::handleError()
d->rawResponse = d->reply->readAll();
const auto jsonBody =
- d->reply->rawHeader("Content-Type") == "application/json";
+ d->reply->rawHeader("Content-Type") == "application/json";
qCDebug(d->logCat).noquote()
- << "Error body (truncated if long):" << d->rawResponse.left(500);
+ << "Error body (truncated if long):" << d->rawResponse.left(500);
if (jsonBody)
- {
- auto json = QJsonDocument::fromJson(d->rawResponse).object();
- const auto errCode = json.value("errcode"_ls).toString();
- if (error() == TooManyRequestsError ||
- errCode == "M_LIMIT_EXCEEDED")
- {
- QString msg = tr("Too many requests");
- auto retryInterval = json.value("retry_after_ms"_ls).toInt(-1);
- if (retryInterval != -1)
- msg += tr(", next retry advised after %1 ms")
- .arg(retryInterval);
- else // We still have to figure some reasonable interval
- retryInterval = getNextRetryInterval();
-
- setStatus(TooManyRequestsError, msg);
-
- // Shortcut to retry instead of executing finishJob()
- stop();
- qCWarning(d->logCat)
- << this << "will retry in" << retryInterval << "ms";
- d->retryTimer.start(retryInterval);
- emit retryScheduled(d->retriesTaken, retryInterval);
- return;
- }
- if (errCode == "M_CONSENT_NOT_GIVEN")
- {
- d->status.code = UserConsentRequiredError;
- d->errorUrl = json.value("consent_uri"_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());
- }
+ setStatus(
+ parseError(d->reply.data(),
+ QJsonDocument::fromJson(d->rawResponse).object()));
}
- finishJob();
+ if (error() != TooManyRequestsError)
+ finishJob();
+ else {
+ stop();
+ emit retryScheduled(d->retriesTaken, d->retryTimer.interval());
+ }
}
bool checkContentType(const QByteArray& type, const QByteArrayList& patterns)
@@ -442,8 +409,46 @@ BaseJob::Status BaseJob::parseJson(const QJsonDocument&)
return Success;
}
+BaseJob::Status BaseJob::parseError(QNetworkReply* reply,
+ const QJsonObject& errorJson)
+{
+ const auto errCode = errorJson.value("errcode"_ls).toString();
+ if (error() == TooManyRequestsError || errCode == "M_LIMIT_EXCEEDED") {
+ QString msg = tr("Too many requests");
+ auto retryInterval = errorJson.value("retry_after_ms"_ls).toInt(-1);
+ if (retryInterval != -1)
+ msg += tr(", next retry advised after %1 ms").arg(retryInterval);
+ else // We still have to figure some reasonable interval
+ retryInterval = getNextRetryInterval();
+
+ qCWarning(d->logCat) << this << "will retry in" << retryInterval << "ms";
+ d->retryTimer.start(retryInterval);
+
+ return { TooManyRequestsError, msg };
+ }
+ if (errCode == "M_CONSENT_NOT_GIVEN") {
+ d->errorUrl = errorJson.value("consent_uri"_ls).toString();
+ return { UserConsentRequiredError };
+ }
+ if (errCode == "M_UNSUPPORTED_ROOM_VERSION"
+ || errCode == "M_INCOMPATIBLE_ROOM_VERSION")
+ return { UnsupportedRoomVersionError,
+ errorJson.contains("room_version"_ls)
+ ? tr("Requested room version: %1")
+ .arg(errorJson.value("room_version"_ls).toString())
+ : errorJson.value("error"_ls).toString() };
+
+ // Not localisable on the client side
+ if (errorJson.contains("error"_ls))
+ d->status.message = errorJson.value("error"_ls).toString();
+
+ return d->status;
+}
+
void BaseJob::stop()
{
+ // This method is used to semi-finalise the job before retrying; so
+ // stop the timeout timer but keep the retry timer running.
d->timer.stop();
if (d->reply)
{
@@ -555,8 +560,6 @@ QString BaseJob::statusCaption() const
return tr("Request was abandoned");
case NetworkError:
return tr("Network problems");
- case JsonParseError:
- return tr("Response could not be parsed");
case TimeoutError:
return tr("Request timed out");
case ContentAccessError:
diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h
index 4c1c7706..d435749a 100644
--- a/lib/jobs/basejob.h
+++ b/lib/jobs/basejob.h
@@ -46,28 +46,36 @@ namespace QMatrixClient
Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT)
Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries)
public:
- /* Just in case, the values are compatible with KJob
- * (which BaseJob used to inherit from). */
enum StatusCode { NoError = 0 // To be compatible with Qt conventions
, Success = 0
, Pending = 1
, WarningLevel = 20
- , UnexpectedResponseTypeWarning = 21
+ , UnexpectedResponseType = 21
+ , UnexpectedResponseTypeWarning = UnexpectedResponseType
, Abandoned = 50 //< A very brief period between abandoning and object deletion
, ErrorLevel = 100 //< Errors have codes starting from this
, NetworkError = 100
- , JsonParseError // TODO: Merge into IncorrectResponseError
- , TimeoutError
+ , Timeout
+ , TimeoutError = Timeout
, ContentAccessError
, NotFoundError
- , IncorrectRequestError
- , IncorrectResponseError
- , TooManyRequestsError
- , RequestNotImplementedError
- , UnsupportedRoomVersionError
- , NetworkAuthRequiredError
- , UserConsentRequiredError
- , UserDefinedError = 200
+ , IncorrectRequest
+ , IncorrectRequestError = IncorrectRequest
+ , IncorrectResponse
+ , IncorrectResponseError = IncorrectResponse
+ , JsonParseError //< deprecated; Use IncorrectResponse instead
+ = IncorrectResponse
+ , TooManyRequests
+ , TooManyRequestsError = TooManyRequests
+ , RequestNotImplemented
+ , RequestNotImplementedError = RequestNotImplemented
+ , UnsupportedRoomVersion
+ , UnsupportedRoomVersionError = UnsupportedRoomVersion
+ , NetworkAuthRequired
+ , NetworkAuthRequiredError = NetworkAuthRequired
+ , UserConsentRequired
+ , UserConsentRequiredError = UserConsentRequired
+ , UserDefinedError = 256
};
/**
@@ -302,7 +310,7 @@ namespace QMatrixClient
* Processes the reply. By default, parses the reply into
* a QJsonDocument and calls parseJson() if it's a valid JSON.
*
- * @param reply raw contents of a HTTP reply from the server (without headers)
+ * @param reply raw contents of a HTTP reply from the server
*
* @see gotReply, parseJson
*/
@@ -310,7 +318,7 @@ namespace QMatrixClient
/**
* Processes the JSON document received from the Matrix server.
- * By default returns succesful status without analysing the JSON.
+ * By default returns successful status without analysing the JSON.
*
* @param json valid JSON document received from the server
*
@@ -318,6 +326,15 @@ namespace QMatrixClient
*/
virtual Status parseJson(const QJsonDocument&);
+ /**
+ * Processes the reply in case of unsuccessful HTTP code.
+ * The body is already loaded from the reply object to errorJson.
+ * @param reply the HTTP reply from the server
+ * @param errorJson the JSON payload describing the error
+ */
+ virtual Status parseError(QNetworkReply* reply,
+ const QJsonObject& errorJson);
+
void setStatus(Status s);
void setStatus(int code, QString message);
diff --git a/lib/joinstate.h b/lib/joinstate.h
index 379183f6..4ae67de8 100644
--- a/lib/joinstate.h
+++ b/lib/joinstate.h
@@ -41,7 +41,7 @@ namespace QMatrixClient
inline const char* toCString(JoinState js)
{
size_t state = size_t(js), index = 0;
- while (state >>= 1) ++index;
+ while (state >>= 1u) ++index;
return JoinStateStrings[index];
}
} // namespace QMatrixClient
diff --git a/lib/room.cpp b/lib/room.cpp
index 14e16850..dea21082 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -76,7 +76,8 @@ enum EventsPlacement : int { Older = -1, Newer = 1 };
class Room::Private
{
public:
- /** Map of user names to users. User names potentially duplicate, hence a multi-hashmap. */
+ /// Map of user names to users
+ /** User names potentially duplicate, hence QMultiHash. */
using members_map_t = QMultiHash<QString, User*>;
Private(Connection* c, QString id_, JoinState initialJoinState)
@@ -93,12 +94,19 @@ class Room::Private
/// The state of the room at timeline position before-0
/// \sa timelineBase
std::unordered_map<StateEventKey, StateEventPtr> baseState;
+ /// State event stubs - events without content, just type and state key
+ static decltype(baseState) stubbedState;
/// The state of the room at timeline position after-maxTimelineIndex()
/// \sa Room::syncEdge
QHash<StateEventKey, const StateEventBase*> currentState;
+ /// Servers with aliases for this room except the one of the local user
+ /// \sa Room::remoteAliases
+ QSet<QString> aliasServers;
+
Timeline timeline;
PendingEvents unsyncedEvents;
QHash<QString, TimelineItem::index_t> eventsIndex;
+
QString displayname;
Avatar avatar;
int highlightCount = 0;
@@ -188,9 +196,22 @@ class Room::Private
template <typename EventT>
const EventT* getCurrentState(const QString& stateKey = {}) const
{
- static const EventT empty;
- const auto* evt =
- currentState.value({EventT::matrixTypeId(), stateKey}, &empty);
+ const StateEventKey evtKey { EventT::matrixTypeId(), stateKey };
+ const auto* evt = currentState.value(evtKey, nullptr);
+ if (!evt) {
+ if (stubbedState.find(evtKey) == stubbedState.end()) {
+ // In the absence of a real event, make a stub as-if an event
+ // with empty content has been received. Event classes should be
+ // prepared for empty/invalid/malicious content anyway.
+ stubbedState.emplace(evtKey,
+ loadStateEvent(EventT::matrixTypeId(),
+ {}, stateKey));
+ qCDebug(MAIN) << "A new stub event created for key {"
+ << evtKey.first << evtKey.second << "}";
+ }
+ evt = stubbedState[evtKey].get();
+ Q_ASSERT(evt);
+ }
Q_ASSERT(evt->type() == EventT::typeId() &&
evt->matrixType() == EventT::matrixTypeId());
return static_cast<const EventT*>(evt);
@@ -267,28 +288,26 @@ class Room::Private
QString doSendEvent(const RoomEvent* pEvent);
void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr);
- template <typename EvT>
- SetRoomStateWithKeyJob* requestSetState(const QString& stateKey,
- const EvT& event)
+ SetRoomStateWithKeyJob* requestSetState(const StateEventBase& event)
{
if (q->successorId().isEmpty())
{
// TODO: Queue up state events sending (see #133).
return connection->callApi<SetRoomStateWithKeyJob>(
- id, EvT::matrixTypeId(), stateKey, event.contentJson());
+ id, event.matrixType(),
+ event.stateKey(), event.contentJson());
}
qCWarning(MAIN) << q << "has been upgraded, state won't be set";
return nullptr;
}
- template <typename EvT>
- auto requestSetState(const EvT& event)
+ template <typename EvT, typename... ArgTs>
+ auto requestSetState(ArgTs&&... args)
{
- return connection->callApi<SetRoomStateJob>(
- id, EvT::matrixTypeId(), event.contentJson());
+ return requestSetState(EvT(std::forward<ArgTs>(args)...));
}
- /**
+ /**
* @brief Apply redaction to the timeline
*
* Tries to find an event in the timeline and redact it; deletes the
@@ -312,6 +331,8 @@ class Room::Private
}
};
+decltype(Room::Private::baseState) Room::Private::stubbedState { };
+
Room::Room(Connection* connection, QString id, JoinState initialJoinState)
: QObject(connection), d(new Private(connection, id, initialJoinState))
{
@@ -381,9 +402,18 @@ QString Room::name() const
return d->getCurrentState<RoomNameEvent>()->name();
}
-QStringList Room::aliases() const
+QStringList Room::localAliases() const
{
- return d->getCurrentState<RoomAliasesEvent>()->aliases();
+ return d->getCurrentState<RoomAliasesEvent>(
+ connection()->homeserver().authority())->aliases();
+}
+
+QStringList Room::remoteAliases() const
+{
+ QStringList result;
+ for (const auto& s: d->aliasServers)
+ result += d->getCurrentState<RoomAliasesEvent>(s)->aliases();
+ return result;
}
QString Room::canonicalAlias() const
@@ -516,7 +546,7 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to)
if(newUnreadMessages > 0)
{
- // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count
+ // See https://github.com/quotient-im/libQuotient/wiki/unread_count
if (unreadMessages < 0)
unreadMessages = 0;
@@ -557,7 +587,7 @@ Room::Changes Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker,
if (et.nsecsElapsed() > profilerMinNsecs() / 10)
qCDebug(PROFILER) << "Recounting unread messages took" << et;
- // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count
+ // See https://github.com/quotient-im/libQuotient/wiki/unread_count
if (unreadMessages == 0)
unreadMessages = -1;
@@ -613,7 +643,7 @@ void Room::markAllMessagesAsRead()
bool Room::canSwitchVersions() const
{
if (!successorId().isEmpty())
- return false; // Noone can upgrade a room that's already upgraded
+ return false; // No one can upgrade a room that's already upgraded
// TODO, #276: m.room.power_levels
const auto* plEvt =
@@ -1236,7 +1266,7 @@ 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.
+ // for it because it doesn't need to be disambiguated any more.
if (namesake)
emit q->memberRenamed(namesake);
}
@@ -1355,7 +1385,7 @@ void Room::updateData(SyncRoomData&& data, bool fromCache)
for( auto&& ephemeralEvent: data.ephemeral )
roomChanges |= processEphemeralEvent(move(ephemeralEvent));
- // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count
+ // See https://github.com/quotient-im/libQuotient/wiki/unread_count
if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages)
{
qCDebug(MAIN) << "Setting unread_count to" << data.unreadCount;
@@ -1611,27 +1641,32 @@ QString Room::postEvent(RoomEvent* event)
QString Room::postJson(const QString& matrixType,
const QJsonObject& eventContent)
{
- return d->sendEvent(loadEvent<RoomEvent>(basicEventJson(matrixType, eventContent)));
+ return d->sendEvent(loadEvent<RoomEvent>(matrixType, eventContent));
+}
+
+SetRoomStateWithKeyJob* Room::setState(const StateEventBase& evt) const {
+ return d->requestSetState(evt);
}
void Room::setName(const QString& newName)
{
- d->requestSetState(RoomNameEvent(newName));
+ d->requestSetState<RoomNameEvent>(newName);
}
void Room::setCanonicalAlias(const QString& newAlias)
{
- d->requestSetState(RoomCanonicalAliasEvent(newAlias));
+ d->requestSetState<RoomCanonicalAliasEvent>(newAlias);
}
-void Room::setAliases(const QStringList& aliases)
+void Room::setLocalAliases(const QStringList& aliases)
{
- d->requestSetState(RoomAliasesEvent(aliases));
+ d->requestSetState<RoomAliasesEvent>(
+ connection()->homeserver().authority(), aliases);
}
void Room::setTopic(const QString& newTopic)
{
- d->requestSetState(RoomTopicEvent(newTopic));
+ d->requestSetState<RoomTopicEvent>(newTopic);
}
bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re)
@@ -1741,9 +1776,10 @@ LeaveRoomJob* Room::leaveRoom()
return connection()->leaveRoom(this);
}
-SetRoomStateWithKeyJob*Room::setMemberState(const QString& memberId, const RoomMemberEvent& event) const
+SetRoomStateWithKeyJob* Room::setMemberState(
+ const QString& memberId, const RoomMemberEvent& event) const
{
- return d->requestSetState(memberId, event);
+ return d->requestSetState<RoomMemberEvent>(memberId, event.content());
}
void Room::kickMember(const QString& memberId, const QString& reason)
@@ -1905,7 +1941,7 @@ RoomEventPtr makeRedacted(const RoomEvent& target,
auto originalJson = target.originalJsonObject();
static const QStringList keepKeys {
EventIdKey, TypeKey, QStringLiteral("room_id"),
- QStringLiteral("sender"), QStringLiteral("state_key"),
+ QStringLiteral("sender"), StateKeyKey,
QStringLiteral("prev_content"), ContentKey,
QStringLiteral("hashes"), QStringLiteral("signatures"),
QStringLiteral("depth"), QStringLiteral("prev_events"),
@@ -2192,16 +2228,30 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
if (!is<RoomMemberEvent>(e)) // Room member events are too numerous
qCDebug(EVENTS) << "Room state event:" << e;
+ // clang-format off
return visit(e
, [] (const RoomNameEvent&) {
return NameChange;
}
, [this,oldStateEvent] (const RoomAliasesEvent& ae) {
+ // clang-format on
+ if (ae.aliases().isEmpty()) {
+ qDebug(MAIN).noquote() << ae.stateKey()
+ << "no more has aliases for room" << objectName();
+ d->aliasServers.remove(ae.stateKey());
+ } else {
+ d->aliasServers.insert(ae.stateKey());
+ qDebug(MAIN).nospace().noquote()
+ << "New server with aliases for room " << objectName()
+ << ": " << ae.stateKey();
+ }
const auto previousAliases = oldStateEvent
? static_cast<const RoomAliasesEvent*>(oldStateEvent)->aliases()
: QStringList();
- connection()->updateRoomAliases(id(), previousAliases, ae.aliases());
+ connection()->updateRoomAliases(id(), ae.stateKey(),
+ previousAliases, ae.aliases());
return OtherChange;
+ // clang-format off
}
, [this] (const RoomCanonicalAliasEvent& evt) {
setObjectName(evt.alias().isEmpty() ? d->id : evt.alias());
@@ -2216,6 +2266,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
return AvatarChange;
}
, [this,oldStateEvent] (const RoomMemberEvent& evt) {
+ // clang-format on
auto* u = user(evt.userId());
const auto* oldMemberEvent =
static_cast<const RoomMemberEvent*>(oldStateEvent);
@@ -2288,6 +2339,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
d->membersLeft.append(u);
}
return MembersChange;
+ // clang-format off
}
, [this] (const EncryptionEvent&) {
emit encryption(); // It can only be done once, so emit it here.
@@ -2310,6 +2362,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
return OtherChange;
}
);
+ // clang-format on
}
Room::Changes Room::processEphemeralEvent(EventPtr&& event)
diff --git a/lib/room.h b/lib/room.h
index 055da3da..3abf262d 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -86,7 +86,8 @@ namespace QMatrixClient
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(QStringList localAliases READ localAliases NOTIFY namesChanged)
+ Q_PROPERTY(QStringList remoteAliases READ remoteAliases NOTIFY namesChanged)
Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged)
Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged)
Q_PROPERTY(QString topic READ topic NOTIFY topicChanged)
@@ -156,7 +157,12 @@ namespace QMatrixClient
QString predecessorId() const;
QString successorId() const;
QString name() const;
- QStringList aliases() const;
+ /// Room aliases defined on the current user's server
+ /// \sa remoteAliases, setLocalAliases
+ QStringList localAliases() const;
+ /// Room aliases defined on other servers
+ /// \sa localAliases
+ QStringList remoteAliases() const;
QString canonicalAlias() const;
QString displayName() const;
QString topic() const;
@@ -322,7 +328,7 @@ namespace QMatrixClient
bool hasAccountData(const QString& type) const;
/** Get a generic account data event of the given type
- * This returns a generic hashmap for any room account data event
+ * This returns a generic hash map for any room account data event
* stored on the server. Tags and read markers cannot be retrieved
* using this method _yet_.
*/
@@ -434,9 +440,13 @@ namespace QMatrixClient
const QJsonObject& eventContent);
QString retryMessage(const QString& txnId);
void discardMessage(const QString& txnId);
+
+ /// Send a request to update the room state with the given event
+ SetRoomStateWithKeyJob* setState(const StateEventBase& evt) const;
void setName(const QString& newName);
void setCanonicalAlias(const QString& newAlias);
- void setAliases(const QStringList& aliases);
+ /// Set room aliases on the user's current server
+ void setLocalAliases(const QStringList& aliases);
void setTopic(const QString& newTopic);
/// You shouldn't normally call this method; it's here for debugging
@@ -446,6 +456,7 @@ namespace QMatrixClient
void inviteToRoom(const QString& memberId);
LeaveRoomJob* leaveRoom();
+ /// \deprecated - use setState() instead")
SetRoomStateWithKeyJob* setMemberState(
const QString& memberId, const RoomMemberEvent& event) const;
void kickMember(const QString& memberId, const QString& reason = {});
@@ -516,7 +527,7 @@ namespace QMatrixClient
/** 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
+ * @param changes a set of flags describing what changes occurred
* upon the last sync
* \sa StateChange
*/
@@ -524,7 +535,7 @@ namespace QMatrixClient
/**
* \brief The room name, the canonical alias or other aliases changed
*
- * Not triggered when displayname changes.
+ * Not triggered when display name changes.
*/
void namesChanged(Room* room);
void displaynameAboutToChange(Room* room);
@@ -581,7 +592,7 @@ namespace QMatrixClient
/// 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
+ /// This room has been upgraded and won't receive updates any more
void upgraded(QString serverMessage, Room* successor);
/// An attempted room upgrade has failed
void upgradeFailed(QString errorMessage);
@@ -590,7 +601,6 @@ namespace QMatrixClient
void beforeDestruction(Room*);
protected:
- /// Returns true if any of room names/aliases has changed
virtual Changes processStateEvent(const RoomEvent& e);
virtual Changes processEphemeralEvent(EventPtr&& event);
virtual Changes processAccountDataEvent(EventPtr&& event);
diff --git a/lib/user.cpp b/lib/user.cpp
index 7f3f11f6..8bdcbe97 100644
--- a/lib/user.cpp
+++ b/lib/user.cpp
@@ -33,9 +33,6 @@
#include <QtCore/QStringBuilder>
#include <QtCore/QElapsedTimer>
-#include <QtCore/QCryptographicHash>
-#include <QtCore/QtEndian>
-
#include <functional>
using namespace QMatrixClient;
@@ -50,23 +47,8 @@ class User::Private
return Avatar(move(url));
}
- qreal makeHueF()
- {
- Q_ASSERT(!userId.isEmpty());
- QByteArray hash = QCryptographicHash::hash(userId.toUtf8(),
- QCryptographicHash::Sha1);
- QDataStream dataStream(qToLittleEndian(hash).left(2));
- dataStream.setByteOrder(QDataStream::LittleEndian);
- quint16 hashValue;
- dataStream >> hashValue;
- const auto hueF =
- qreal(hashValue)/std::numeric_limits<quint16>::max();
- Q_ASSERT((0 <= hueF) && (hueF <= 1));
- return hueF;
- }
-
Private(QString userId, Connection* connection)
- : userId(move(userId)), connection(connection), hueF(makeHueF())
+ : userId(move(userId)), connection(connection), hueF(stringToHueF(this->userId))
{ }
QString userId;
@@ -310,7 +292,7 @@ void User::rename(const QString& newName, const Room* r)
const auto actualNewName = sanitized(newName);
MemberEventContent evtC;
evtC.displayName = actualNewName;
- connect(r->setMemberState(id(), RoomMemberEvent(move(evtC))),
+ connect(r->setState(RoomMemberEvent(id(), move(evtC))),
&BaseJob::success, this, [=] { updateName(actualNewName, r); });
}
diff --git a/lib/util.cpp b/lib/util.cpp
index 4e17d2f9..88cba959 100644
--- a/lib/util.cpp
+++ b/lib/util.cpp
@@ -23,6 +23,10 @@
#include <QtCore/QDir>
#include <QtCore/QStringBuilder>
+#include <QtCore/QCryptographicHash>
+#include <QtCore/QtEndian>
+#include <QtCore/QDataStream>
+
static const auto RegExpOptions =
QRegularExpression::CaseInsensitiveOption
| QRegularExpression::OptimizeOnFirstUsageOption
@@ -41,7 +45,7 @@ void QMatrixClient::linkifyUrls(QString& htmlEscapedText)
// <, >, ' or ", and ends before whitespaces, <, >, ', ", ], !, ), :,
// comma or dot
static const QRegularExpression FullUrlRegExp(QStringLiteral(
- R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp|magnet)://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"
+ R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp|magnet|matrix):(//)?)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"
), RegExpOptions);
// email address:
// [word chars, dots or dashes]@[word chars, dots or dashes].[word chars]
@@ -93,6 +97,21 @@ QString QMatrixClient::cacheLocation(const QString& dirName)
return cachePath;
}
+qreal QMatrixClient::stringToHueF(const QString &string)
+{
+ Q_ASSERT(!string.isEmpty());
+ QByteArray hash = QCryptographicHash::hash(string.toUtf8(),
+ QCryptographicHash::Sha1);
+ QDataStream dataStream(qToLittleEndian(hash).left(2));
+ dataStream.setByteOrder(QDataStream::LittleEndian);
+ quint16 hashValue;
+ dataStream >> hashValue;
+ const auto hueF =
+ qreal(hashValue)/std::numeric_limits<quint16>::max();
+ Q_ASSERT((0 <= hueF) && (hueF <= 1));
+ return hueF;
+}
+
// Tests for function_traits<>
#ifdef Q_CC_CLANG
diff --git a/lib/util.h b/lib/util.h
index f08c1c95..eda817a1 100644
--- a/lib/util.h
+++ b/lib/util.h
@@ -314,5 +314,13 @@ namespace QMatrixClient
* \param dir path to cache directory relative to the standard cache path
*/
QString cacheLocation(const QString& dirName);
+
+ /** Hue color component of based of the hash of the string.
+ * The implementation is based on XEP-0392:
+ * https://xmpp.org/extensions/xep-0392.html
+ * Naming and range are the same as QColor's hueF method:
+ * https://doc.qt.io/qt-5/qcolor.html#integer-vs-floating-point-precision
+ */
+ qreal stringToHueF(const QString& string);
} // namespace QMatrixClient