aboutsummaryrefslogtreecommitdiff
path: root/lib/connection.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'lib/connection.cpp')
-rw-r--r--lib/connection.cpp256
1 files changed, 185 insertions, 71 deletions
diff --git a/lib/connection.cpp b/lib/connection.cpp
index ac69228b..0c98c383 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -32,12 +32,13 @@
#include "csapi/joining.h"
#include "csapi/to_device.h"
#include "csapi/room_send.h"
+#include "csapi/wellknown.h"
+#include "csapi/versions.h"
#include "jobs/syncjob.h"
#include "jobs/mediathumbnailjob.h"
#include "jobs/downloadfilejob.h"
#include "csapi/voip.h"
-#include <QtNetwork/QDnsLookup>
#include <QtCore/QFile>
#include <QtCore/QDir>
#include <QtCore/QStandardPaths>
@@ -79,10 +80,10 @@ class Connection::Private
std::unique_ptr<ConnectionData> data;
// A complex key below is a pair of room name and whether its
// state is Invited. The spec mandates to keep Invited room state
- // separately so we should, e.g., keep objects for Invite and
- // Leave state of the same room.
+ // 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
+ /// Mapping from aliases to room ids, as of the last sync
QHash<QString, QString> roomAliasMap;
QVector<QString> roomIdsToForget;
QVector<Room*> firstTimeRooms;
@@ -101,6 +102,8 @@ class Connection::Private
GetCapabilitiesJob* capabilitiesJob = nullptr;
GetCapabilitiesJob::Capabilities capabilities;
+ QVector<GetLoginFlowsJob::LoginFlow> loginFlows;
+
SyncJob* syncJob = nullptr;
bool cacheState = true;
@@ -108,8 +111,10 @@ class Connection::Private
.value("cache_type").toString() != "json";
bool lazyLoading = false;
- void connectWithToken(const QString& user, const QString& accessToken,
- const QString& deviceId);
+ template <typename... LoginArgTs>
+ void loginToServer(LoginArgTs&&... loginArgs);
+ void assumeIdentity(const QString& newUserId, const QString& accessToken,
+ const QString& deviceId);
template <typename EventT>
EventT* unpackAccountData() const
@@ -170,44 +175,73 @@ void Connection::resolveServer(const QString& mxidOrDomain)
maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http"
if (!match.hasMatch() || !maybeBaseUrl.isValid())
{
- emit resolveError(
- tr("%1 is not a valid homeserver address")
- .arg(maybeBaseUrl.toString()));
+ emit resolveError(tr("%1 is not a valid homeserver address")
+ .arg(maybeBaseUrl.toString()));
return;
}
- setHomeserver(maybeBaseUrl);
- emit resolved();
- return;
-
- // FIXME, #178: The below code is incorrect and is no more executed. The
- // correct server resolution should be done from .well-known/matrix/client
auto domain = maybeBaseUrl.host();
qCDebug(MAIN) << "Finding the server" << domain;
- // Check if the Matrix server has a dedicated service record.
- auto* dns = new QDnsLookup();
- dns->setType(QDnsLookup::SRV);
- dns->setName("_matrix._tcp." + domain);
-
- connect(dns, &QDnsLookup::finished, [this,dns,maybeBaseUrl]() {
- QUrl baseUrl { maybeBaseUrl };
- if (dns->error() == QDnsLookup::NoError &&
- dns->serviceRecords().isEmpty())
- {
- auto record = dns->serviceRecords().front();
- baseUrl.setHost(record.target());
- baseUrl.setPort(record.port());
- qCDebug(MAIN) << "SRV record for" << maybeBaseUrl.host()
- << "is" << baseUrl.authority();
- } else {
- qCDebug(MAIN) << baseUrl.host() << "doesn't have SRV record"
- << dns->name() << "- using the hostname as is";
- }
- setHomeserver(baseUrl);
- emit resolved();
- dns->deleteLater();
- });
- dns->lookup();
+
+ d->data->setBaseUrl(maybeBaseUrl); // Just enough to check .well-known file
+ auto getWellKnownJob = callApi<GetWellknownJob>();
+ // This is a workaround for 0.5.x; due to the way Quaternion's login dialog
+ // operates, Connection can disappear any moment during server resolution.
+ // Quotient 0.6 will reparent all jobs to enforce lifetimes. See also #398.
+ getWellKnownJob->setParent(this);
+ connect(getWellKnownJob, &BaseJob::finished, this,
+ [this, getWellKnownJob, maybeBaseUrl] {
+ if (getWellKnownJob->status() != BaseJob::NotFoundError) {
+ if (getWellKnownJob->status() != BaseJob::Success) {
+ qCWarning(MAIN)
+ << "Fetching .well-known file failed, FAIL_PROMPT";
+ emit resolveError(tr("Failed resolving the homeserver"));
+ return;
+ }
+ QUrl baseUrl { getWellKnownJob->data().homeserver.baseUrl };
+ if (baseUrl.isEmpty()) {
+ qCWarning(MAIN) << "base_url not provided, FAIL_PROMPT";
+ emit resolveError(
+ tr("The homeserver base URL is not provided"));
+ return;
+ }
+ if (!baseUrl.isValid()) {
+ qCWarning(MAIN) << "base_url invalid, FAIL_ERROR";
+ emit resolveError(tr("The homeserver base URL is invalid"));
+ return;
+ }
+ qCInfo(MAIN) << ".well-known URL for" << maybeBaseUrl.host()
+ << "is" << baseUrl.authority();
+ setHomeserver(baseUrl);
+ } else {
+ qCInfo(MAIN) << "No .well-known file, using" << maybeBaseUrl
+ << "for base URL";
+ setHomeserver(maybeBaseUrl);
+ }
+
+ auto getVersionsJob = callApi<GetVersionsJob>();
+ getVersionsJob->setParent(this); // Same workaround as above
+ connect(getVersionsJob, &BaseJob::success, this,
+ &Connection::resolved);
+ connect(getVersionsJob, &BaseJob::failure, this, [this] {
+ qCWarning(MAIN) << "Homeserver base URL invalid";
+ emit resolveError(tr("The homeserver base URL "
+ "doesn't seem to work"));
+ });
+ });
+}
+
+inline UserIdentifier makeUserIdentifier(const QString& id)
+{
+ return { QStringLiteral("m.id.user"), { { QStringLiteral("user"), id } } };
+}
+
+inline UserIdentifier make3rdPartyIdentifier(const QString& medium,
+ const QString& address)
+{
+ return { QStringLiteral("m.id.thirdparty"),
+ { { QStringLiteral("medium"), medium },
+ { QStringLiteral("address"), address } } };
}
void Connection::connectToServer(const QString& user, const QString& password,
@@ -219,23 +253,28 @@ void Connection::connectToServer(const QString& user, const QString& password,
doConnectToServer(user, password, initialDeviceName, deviceId);
});
}
+
void Connection::doConnectToServer(const QString& user, const QString& password,
const QString& initialDeviceName,
const QString& deviceId)
{
- auto loginJob = callApi<LoginJob>(QStringLiteral("m.login.password"),
- UserIdentifier { QStringLiteral("m.id.user"),
- {{ QStringLiteral("user"), user }} },
- password, /*token*/ "", deviceId, initialDeviceName);
- connect(loginJob, &BaseJob::success, this,
- [this, loginJob] {
- d->connectWithToken(loginJob->userId(), loginJob->accessToken(),
- loginJob->deviceId());
- });
- connect(loginJob, &BaseJob::failure, this,
- [this, loginJob] {
- emit loginError(loginJob->errorString(), loginJob->rawDataSample());
- });
+ d->loginToServer(LoginFlows::Password.type, makeUserIdentifier(user),
+ password, /*token*/ "", deviceId, initialDeviceName);
+}
+
+SsoSession* Connection::prepareForSso(const QString& initialDeviceName,
+ const QString& deviceId)
+{
+ return new SsoSession(this, initialDeviceName, deviceId);
+}
+
+void Connection::loginWithToken(const QByteArray& loginToken,
+ const QString& initialDeviceName,
+ const QString& deviceId)
+{
+ d->loginToServer(LoginFlows::Token.type,
+ makeUserIdentifier(/*user is encoded in loginToken*/ {}),
+ /*password*/ "", loginToken, deviceId, initialDeviceName);
}
void Connection::syncLoopIteration()
@@ -247,8 +286,15 @@ void Connection::connectWithToken(const QString& userId,
const QString& accessToken,
const QString& deviceId)
{
+ assumeIdentity(userId, accessToken, deviceId);
+}
+
+void Connection::assumeIdentity(const QString& userId,
+ const QString& accessToken,
+ const QString& deviceId)
+{
checkAndConnect(userId,
- [=] { d->connectWithToken(userId, accessToken, deviceId); });
+ [=] { d->assumeIdentity(userId, accessToken, deviceId); });
}
void Connection::reloadCapabilities()
@@ -283,11 +329,25 @@ bool Connection::loadingCapabilities() const
return d->capabilities.roomVersions.omitted();
}
-void Connection::Private::connectWithToken(const QString& user,
- const QString& accessToken,
- const QString& deviceId)
+template <typename... LoginArgTs>
+void Connection::Private::loginToServer(LoginArgTs&&... loginArgs)
+{
+ auto loginJob =
+ q->callApi<LoginJob>(std::forward<LoginArgTs>(loginArgs)...);
+ connect(loginJob, &BaseJob::success, q, [this, loginJob] {
+ assumeIdentity(loginJob->userId(), loginJob->accessToken(),
+ loginJob->deviceId());
+ });
+ connect(loginJob, &BaseJob::failure, q, [this, loginJob] {
+ emit q->loginError(loginJob->errorString(), loginJob->rawDataSample());
+ });
+}
+
+void Connection::Private::assumeIdentity(const QString& newUserId,
+ const QString& accessToken,
+ const QString& deviceId)
{
- userId = user;
+ userId = newUserId;
q->user(); // Creates a User object for the local user
data->setToken(accessToken.toLatin1());
data->setDeviceId(deviceId);
@@ -850,6 +910,21 @@ QString Connection::domain() const
return d->userId.section(':', 1);
}
+QVector<GetLoginFlowsJob::LoginFlow> Connection::loginFlows() const
+{
+ return d->loginFlows;
+}
+
+bool Connection::supportsPasswordAuth() const
+{
+ return d->loginFlows.contains(LoginFlows::Password);
+}
+
+bool Connection::supportsSso() const
+{
+ return d->loginFlows.contains(LoginFlows::SSO);
+}
+
Room* Connection::room(const QString& roomId, JoinStates states) const
{
Room* room = d->roomMap.value({roomId, false}, nullptr);
@@ -979,6 +1054,33 @@ QHash< QPair<QString, bool>, Room* > Connection::roomMap() const
return roomMap;
}
+QVector<Room*> Connection::allRooms() const
+{
+ QVector<Room*> result;
+ result.resize(d->roomMap.size());
+ std::copy(d->roomMap.cbegin(), d->roomMap.cend(), result.begin());
+ return result;
+}
+
+QVector<Room*> Connection::rooms(JoinStates joinStates) const
+{
+ QVector<Room*> result;
+ for (auto* r: qAsConst(d->roomMap))
+ if (joinStates.testFlag(r->joinState()))
+ result.push_back(r);
+ return result;
+}
+
+int Connection::roomsCount(JoinStates joinStates) const
+{
+ // Using int to maintain compatibility with QML
+ // (consider also that QHash<>::size() returns int anyway).
+ return int(std::count_if(d->roomMap.begin(), d->roomMap.end(),
+ [joinStates](Room* r) {
+ return joinStates.testFlag(r->joinState());
+ }));
+}
+
bool Connection::hasAccountData(const QString& type) const
{
return d->accountData.find(type) != d->accountData.cend();
@@ -1244,11 +1346,21 @@ QByteArray Connection::generateTxnId() const
void Connection::setHomeserver(const QUrl& url)
{
- if (homeserver() == url)
- return;
+ if (homeserver() != url) {
+ d->data->setBaseUrl(url);
+ d->loginFlows.clear();
+ emit homeserverChanged(homeserver());
+ }
- d->data->setBaseUrl(url);
- emit homeserverChanged(homeserver());
+ // Whenever a homeserver is updated, retrieve available login flows from it
+ auto* j = callApi<GetLoginFlowsJob>(BackgroundRequest);
+ connect(j, &BaseJob::finished, this, [this, j] {
+ if (j->status().good())
+ d->loginFlows = j->flows();
+ else
+ d->loginFlows.clear();
+ emit loginFlowsChanged();
+ });
}
void Connection::saveRoomState(Room* r) const
@@ -1294,18 +1406,20 @@ void Connection::saveState() const
{ QStringLiteral("minor"), SyncData::cacheVersion().second }
}}};
{
- QJsonObject rooms;
- QJsonObject inviteRooms;
- 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 roomsJson;
+ QJsonObject inviteRoomsJson;
+ for (const auto* r: qAsConst(d->roomMap)) {
+ if (r->joinState() == JoinState::Leave)
+ continue;
+ (r->joinState() == JoinState::Invite ? inviteRoomsJson : roomsJson)
+ .insert(r->id(), QJsonValue::Null);
+ }
QJsonObject roomObj;
- if (!rooms.isEmpty())
- roomObj.insert(QStringLiteral("join"), rooms);
- if (!inviteRooms.isEmpty())
- roomObj.insert(QStringLiteral("invite"), inviteRooms);
+ if (!roomsJson.isEmpty())
+ roomObj.insert(QStringLiteral("join"), roomsJson);
+ if (!inviteRoomsJson.isEmpty())
+ roomObj.insert(QStringLiteral("invite"), inviteRoomsJson);
rootObj.insert(QStringLiteral("next_batch"), d->data->lastEvent());
rootObj.insert(QStringLiteral("rooms"), roomObj);