aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt1
-rw-r--r--lib/connection.cpp12
-rw-r--r--lib/connection.h2
-rw-r--r--lib/logging.cpp1
-rw-r--r--lib/logging.h1
-rw-r--r--lib/mxcreply.cpp70
-rw-r--r--lib/mxcreply.h29
-rw-r--r--lib/networkaccessmanager.cpp78
-rw-r--r--lib/networkaccessmanager.h5
-rw-r--r--lib/room.cpp11
-rw-r--r--lib/room.h3
-rw-r--r--quotest/CMakeLists.txt3
-rw-r--r--quotest/quotest.cpp49
13 files changed, 254 insertions, 11 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7043a653..bae833c3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -143,6 +143,7 @@ list(APPEND lib_SRCS
lib/encryptionmanager.cpp
lib/eventitem.cpp
lib/accountregistry.cpp
+ lib/mxcreply.cpp
lib/events/event.cpp
lib/events/roomevent.cpp
lib/events/stateevent.cpp
diff --git a/lib/connection.cpp b/lib/connection.cpp
index 222c3b71..4abf5097 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -13,6 +13,7 @@
#include "room.h"
#include "settings.h"
#include "user.h"
+#include "accountregistry.h"
// NB: since Qt 6, moc_connection.cpp needs Room and User fully defined
#include "moc_connection.cpp"
@@ -258,6 +259,7 @@ Connection::~Connection()
{
qCDebug(MAIN) << "deconstructing connection object for" << userId();
stopSync();
+ AccountRegistry::instance().drop(this);
}
void Connection::resolveServer(const QString& mxid)
@@ -441,6 +443,7 @@ void Connection::Private::completeSetup(const QString& mxId)
qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString()
<< "by user" << data->userId()
<< "from device" << data->deviceId();
+ AccountRegistry::instance().add(q);
#ifndef Quotient_E2EE_ENABLED
qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
#else // Quotient_E2EE_ENABLED
@@ -836,6 +839,15 @@ inline auto splitMediaId(const QString& mediaId)
return idParts;
}
+QUrl Connection::makeMediaUrl(QUrl mxcUrl) const
+{
+ Q_ASSERT(mxcUrl.scheme() == "mxc");
+ QUrlQuery q(mxcUrl.query());
+ q.addQueryItem(QStringLiteral("user_id"), userId());
+ mxcUrl.setQuery(q);
+ return mxcUrl;
+}
+
MediaThumbnailJob* Connection::getThumbnail(const QString& mediaId,
QSize requestedSize,
RunningPolicy policy)
diff --git a/lib/connection.h b/lib/connection.h
index ecbb1a19..1a6ca9b0 100644
--- a/lib/connection.h
+++ b/lib/connection.h
@@ -529,6 +529,8 @@ public Q_SLOTS:
void stopSync();
QString nextBatchToken() const;
+ Q_INVOKABLE QUrl makeMediaUrl(QUrl mxcUrl) const;
+
virtual MediaThumbnailJob*
getThumbnail(const QString& mediaId, QSize requestedSize,
RunningPolicy policy = BackgroundRequest);
diff --git a/lib/logging.cpp b/lib/logging.cpp
index ffcc851c..15eac69d 100644
--- a/lib/logging.cpp
+++ b/lib/logging.cpp
@@ -17,4 +17,5 @@ LOGGING_CATEGORY(E2EE, "quotient.e2ee")
LOGGING_CATEGORY(JOBS, "quotient.jobs")
LOGGING_CATEGORY(SYNCJOB, "quotient.jobs.sync")
LOGGING_CATEGORY(THUMBNAILJOB, "quotient.jobs.thumbnail")
+LOGGING_CATEGORY(NETWORK, "quotient.network")
LOGGING_CATEGORY(PROFILER, "quotient.profiler")
diff --git a/lib/logging.h b/lib/logging.h
index 1d1394e8..7e0da975 100644
--- a/lib/logging.h
+++ b/lib/logging.h
@@ -17,6 +17,7 @@ Q_DECLARE_LOGGING_CATEGORY(E2EE)
Q_DECLARE_LOGGING_CATEGORY(JOBS)
Q_DECLARE_LOGGING_CATEGORY(SYNCJOB)
Q_DECLARE_LOGGING_CATEGORY(THUMBNAILJOB)
+Q_DECLARE_LOGGING_CATEGORY(NETWORK)
Q_DECLARE_LOGGING_CATEGORY(PROFILER)
namespace Quotient {
diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp
new file mode 100644
index 00000000..0b6643fc
--- /dev/null
+++ b/lib/mxcreply.cpp
@@ -0,0 +1,70 @@
+// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include "mxcreply.h"
+
+#include "room.h"
+
+using namespace Quotient;
+
+class MxcReply::Private
+{
+public:
+ explicit Private(QNetworkReply* r = nullptr)
+ : m_reply(r)
+ {}
+ QNetworkReply* m_reply;
+};
+
+MxcReply::MxcReply(QNetworkReply* reply)
+ : d(std::make_unique<Private>(reply))
+{
+ reply->setParent(this);
+ connect(d->m_reply, &QNetworkReply::finished, this, [this]() {
+ setError(d->m_reply->error(), d->m_reply->errorString());
+ setOpenMode(ReadOnly);
+ Q_EMIT finished();
+ });
+}
+
+MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId)
+ : d(std::make_unique<Private>(reply))
+{
+ reply->setParent(this);
+ connect(d->m_reply, &QNetworkReply::finished, this, [this, room, eventId]() {
+ setError(d->m_reply->error(), d->m_reply->errorString());
+ setOpenMode(ReadOnly);
+ emit finished();
+ });
+}
+
+#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
+#define ERROR_SIGNAL errorOccurred
+#else
+#define ERROR_SIGNAL error
+#endif
+
+MxcReply::MxcReply()
+{
+ static const auto BadRequestPhrase = tr("Bad Request");
+ QMetaObject::invokeMethod(this, [this]() {
+ setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 400);
+ setAttribute(QNetworkRequest::HttpReasonPhraseAttribute,
+ BadRequestPhrase);
+ setError(QNetworkReply::ProtocolInvalidOperationError,
+ BadRequestPhrase);
+ setFinished(true);
+ emit ERROR_SIGNAL(QNetworkReply::ProtocolInvalidOperationError);
+ emit finished();
+ }, Qt::QueuedConnection);
+}
+
+qint64 MxcReply::readData(char *data, qint64 maxSize)
+{
+ return d->m_reply->read(data, maxSize);
+}
+
+void MxcReply::abort()
+{
+ d->m_reply->abort();
+}
diff --git a/lib/mxcreply.h b/lib/mxcreply.h
new file mode 100644
index 00000000..efaf01c6
--- /dev/null
+++ b/lib/mxcreply.h
@@ -0,0 +1,29 @@
+// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#pragma once
+
+#include <QtNetwork/QNetworkReply>
+#include <memory>
+
+namespace Quotient {
+class Room;
+
+class MxcReply : public QNetworkReply
+{
+public:
+ explicit MxcReply();
+ explicit MxcReply(QNetworkReply *reply);
+ MxcReply(QNetworkReply* reply, Room* room, const QString& eventId);
+
+public Q_SLOTS:
+ void abort() override;
+
+protected:
+ qint64 readData(char *data, qint64 maxSize) override;
+
+private:
+ class Private;
+ std::unique_ptr<Private> d;
+};
+}
diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp
index a94ead34..57618329 100644
--- a/lib/networkaccessmanager.cpp
+++ b/lib/networkaccessmanager.cpp
@@ -3,18 +3,43 @@
#include "networkaccessmanager.h"
+#include "connection.h"
+#include "room.h"
+#include "accountregistry.h"
+#include "mxcreply.h"
+
#include <QtCore/QCoreApplication>
+#include <QtCore/QThreadStorage>
+#include <QtCore/QSettings>
#include <QtNetwork/QNetworkReply>
using namespace Quotient;
class NetworkAccessManager::Private {
public:
+ explicit Private(NetworkAccessManager* q)
+ : q(q)
+ {}
+
+ QNetworkReply* createImplRequest(Operation op,
+ const QNetworkRequest& outerRequest,
+ Connection* connection)
+ {
+ Q_ASSERT(outerRequest.url().scheme() == "mxc");
+ QNetworkRequest r(outerRequest);
+ r.setUrl(QUrl(QStringLiteral("%1/_matrix/media/r0/download/%2")
+ .arg(connection->homeserver().toString(),
+ outerRequest.url().authority()
+ + outerRequest.url().path())));
+ return q->createRequest(op, r);
+ }
+
+ NetworkAccessManager* q;
QList<QSslError> ignoredSslErrors;
};
NetworkAccessManager::NetworkAccessManager(QObject* parent)
- : QNetworkAccessManager(parent), d(std::make_unique<Private>())
+ : QNetworkAccessManager(parent), d(std::make_unique<Private>(this))
{}
QList<QSslError> NetworkAccessManager::ignoredSslErrors() const
@@ -34,7 +59,7 @@ void NetworkAccessManager::clearIgnoredSslErrors()
static NetworkAccessManager* createNam()
{
- auto nam = new NetworkAccessManager(QCoreApplication::instance());
+ auto nam = new NetworkAccessManager();
#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
// See #109; in newer Qt, bearer management is deprecated altogether
NetworkAccessManager::connect(nam,
@@ -47,8 +72,11 @@ static NetworkAccessManager* createNam()
NetworkAccessManager* NetworkAccessManager::instance()
{
- static auto* nam = createNam();
- return nam;
+ static QThreadStorage<NetworkAccessManager*> storage;
+ if(!storage.hasLocalData()) {
+ storage.setLocalData(createNam());
+ }
+ return storage.localData();
}
NetworkAccessManager::~NetworkAccessManager() = default;
@@ -56,7 +84,49 @@ NetworkAccessManager::~NetworkAccessManager() = default;
QNetworkReply* NetworkAccessManager::createRequest(
Operation op, const QNetworkRequest& request, QIODevice* outgoingData)
{
+ const auto& mxcUrl = request.url();
+ if (mxcUrl.scheme() == "mxc") {
+ const QUrlQuery query(mxcUrl.query());
+ const auto accountId = query.queryItemValue(QStringLiteral("user_id"));
+ if (accountId.isEmpty()) {
+ // Using QSettings here because Quotient::NetworkSettings
+ // doesn't provide multithreading guarantees
+ static thread_local QSettings s;
+ if (!s.value("Network/allow_direct_media_requests").toBool()) {
+ qCWarning(NETWORK) << "No connection specified";
+ return new MxcReply();
+ }
+ // TODO: Make the best effort with a direct unauthenticated request
+ // to the media server
+ } else {
+ auto* const connection = AccountRegistry::instance().get(accountId);
+ if (!connection) {
+ qCWarning(NETWORK) << "Connection" << accountId << "not found";
+ return new MxcReply();
+ }
+ const auto roomId = query.queryItemValue(QStringLiteral("room_id"));
+ if (!roomId.isEmpty()) {
+ auto room = connection->room(roomId);
+ if (!room) {
+ qCWarning(NETWORK) << "Room" << roomId << "not found";
+ return new MxcReply();
+ }
+ return new MxcReply(
+ d->createImplRequest(op, request, connection), room,
+ query.queryItemValue(QStringLiteral("event_id")));
+ }
+ return new MxcReply(
+ d->createImplRequest(op, request, connection));
+ }
+ }
auto reply = QNetworkAccessManager::createRequest(op, request, outgoingData);
reply->ignoreSslErrors(d->ignoredSslErrors);
return reply;
}
+
+QStringList NetworkAccessManager::supportedSchemesImplementation() const
+{
+ auto schemes = QNetworkAccessManager::supportedSchemesImplementation();
+ schemes += QStringLiteral("mxc");
+ return schemes;
+}
diff --git a/lib/networkaccessmanager.h b/lib/networkaccessmanager.h
index 47729a1b..87bc12a1 100644
--- a/lib/networkaccessmanager.h
+++ b/lib/networkaccessmanager.h
@@ -8,6 +8,8 @@
#include <memory>
namespace Quotient {
+class Room;
+class Connection;
class NetworkAccessManager : public QNetworkAccessManager {
Q_OBJECT
public:
@@ -21,6 +23,9 @@ public:
/** Get a pointer to the singleton */
static NetworkAccessManager* instance();
+public Q_SLOTS:
+ QStringList supportedSchemesImplementation() const;
+
private:
QNetworkReply* createRequest(Operation op, const QNetworkRequest& request,
QIODevice* outgoingData = Q_NULLPTR) override;
diff --git a/lib/room.cpp b/lib/room.cpp
index c6cca2ea..bcbb121b 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -1116,6 +1116,17 @@ QList<User*> Room::directChatUsers() const
return connection()->directChatUsers(this);
}
+QUrl Room::makeMediaUrl(const QString& eventId, const QUrl& mxcUrl) const
+{
+ auto url = connection()->makeMediaUrl(mxcUrl);
+ QUrlQuery q(url.query());
+ Q_ASSERT(q.hasQueryItem("user_id"));
+ q.addQueryItem("room_id", id());
+ q.addQueryItem("event_id", eventId);
+ url.setQuery(q);
+ return url;
+}
+
QString safeFileName(QString rawName)
{
return rawName.replace(QRegularExpression("[/\\<>|\"*?:]"), "_");
diff --git a/lib/room.h b/lib/room.h
index 4e55dbaf..7290f9a9 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -458,6 +458,9 @@ public:
/// Get the list of users this room is a direct chat with
QList<User*> directChatUsers() const;
+ Q_INVOKABLE QUrl makeMediaUrl(const QString& eventId,
+ const QUrl &mxcUrl) const;
+
Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId) const;
Q_INVOKABLE QUrl urlToDownload(const QString& eventId) const;
diff --git a/quotest/CMakeLists.txt b/quotest/CMakeLists.txt
index bf9af796..59334e30 100644
--- a/quotest/CMakeLists.txt
+++ b/quotest/CMakeLists.txt
@@ -4,8 +4,9 @@
set(quotest_SRCS quotest.cpp)
+find_package(${Qt} COMPONENTS Concurrent)
add_executable(quotest ${quotest_SRCS})
-target_link_libraries(quotest PRIVATE ${Qt}::Core ${Qt}::Test ${PROJECT_NAME})
+target_link_libraries(quotest PRIVATE ${Qt}::Core ${Qt}::Test ${Qt}::Concurrent ${PROJECT_NAME})
option(${PROJECT_NAME}_INSTALL_TESTS "install quotest (former qmc-example) application" ON)
add_feature_info(InstallQuotest ${PROJECT_NAME}_INSTALL_TESTS
diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp
index ec7d4dcb..4142c718 100644
--- a/quotest/quotest.cpp
+++ b/quotest/quotest.cpp
@@ -5,6 +5,7 @@
#include "room.h"
#include "user.h"
#include "uriresolver.h"
+#include "networkaccessmanager.h"
#include "csapi/joining.h"
#include "csapi/leaving.h"
@@ -20,6 +21,8 @@
#include <QtCore/QStringBuilder>
#include <QtCore/QTemporaryFile>
#include <QtCore/QTimer>
+#include <QtConcurrent/QtConcurrent>
+#include <QtNetwork/QNetworkReply>
#include <functional>
#include <iostream>
@@ -435,6 +438,39 @@ TEST_IMPL(sendFile)
return false;
}
+// Can be replaced with a lambda once QtConcurrent is able to resolve return
+// types from lambda invocations (Qt 6 can, not sure about earlier)
+struct DownloadRunner {
+ QUrl url;
+
+ using result_type = QNetworkReply::NetworkError;
+
+ QNetworkReply::NetworkError operator()(int) const
+ {
+ QEventLoop el;
+ QScopedPointer<QNetworkReply, QScopedPointerDeleteLater> reply {
+ NetworkAccessManager::instance()->get(QNetworkRequest(url))
+ };
+ QObject::connect(
+ reply.data(), &QNetworkReply::finished, &el, [&el] { el.exit(); },
+ Qt::QueuedConnection);
+ el.exec();
+ return reply->error();
+ }
+};
+
+bool testDownload(const QUrl& url)
+{
+ // Move out actual test from the multithreaded code
+ // to help debugging
+ auto results = QtConcurrent::blockingMapped(QVector<int> { 1, 2, 3 },
+ DownloadRunner { url });
+ return std::all_of(results.cbegin(), results.cend(),
+ [](QNetworkReply::NetworkError ne) {
+ return ne == QNetworkReply::NoError;
+ });
+}
+
bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest,
const QString& txnId,
const QString& fileName)
@@ -465,14 +501,15 @@ bool TestSuite::checkFileSendingOutcome(const TestToken& thisTest,
return visit(
*evt,
[&](const RoomMessageEvent& e) {
- // TODO: actually try to download it to check, e.g., #366
- // (and #368 would help to test against bad file names).
+ // TODO: check #366 once #368 is implemented
FINISH_TEST(
!e.id().isEmpty()
- && pendingEvents[size_t(pendingIdx)]->transactionId()
- == txnId
- && e.hasFileContent()
- && e.content()->fileInfo()->originalName == fileName);
+ && pendingEvents[size_t(pendingIdx)]->transactionId()
+ == txnId
+ && e.hasFileContent()
+ && e.content()->fileInfo()->originalName == fileName
+ && testDownload(targetRoom->connection()->makeMediaUrl(
+ e.content()->fileInfo()->url)));
},
[this, thisTest](const RoomEvent&) { FAIL_TEST(); });
});