summaryrefslogtreecommitdiff
path: root/transferengine-plugins/fediversetransferplugin
diff options
context:
space:
mode:
Diffstat (limited to 'transferengine-plugins/fediversetransferplugin')
-rw-r--r--transferengine-plugins/fediversetransferplugin/fediverseapi.cpp255
-rw-r--r--transferengine-plugins/fediversetransferplugin/fediverseapi.h65
-rw-r--r--transferengine-plugins/fediversetransferplugin/fediversetransferplugin.cpp31
-rw-r--r--transferengine-plugins/fediversetransferplugin/fediversetransferplugin.h33
-rw-r--r--transferengine-plugins/fediversetransferplugin/fediversetransferplugin.pro30
-rw-r--r--transferengine-plugins/fediversetransferplugin/fediverseuploader.cpp252
-rw-r--r--transferengine-plugins/fediversetransferplugin/fediverseuploader.h59
7 files changed, 725 insertions, 0 deletions
diff --git a/transferengine-plugins/fediversetransferplugin/fediverseapi.cpp b/transferengine-plugins/fediversetransferplugin/fediverseapi.cpp
new file mode 100644
index 0000000..d9de9eb
--- /dev/null
+++ b/transferengine-plugins/fediversetransferplugin/fediverseapi.cpp
@@ -0,0 +1,255 @@
+/*
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include "fediverseapi.h"
+#include "fediverseauthutils.h"
+
+#include <QtCore/QFile>
+#include <QtCore/QFileInfo>
+#include <QtNetwork/QHttpMultiPart>
+#include <QtCore/QJsonDocument>
+#include <QtCore/QJsonObject>
+#include <QtCore/QUrl>
+#include <QtCore/QUrlQuery>
+
+#include <QtNetwork/QNetworkRequest>
+
+#include <QtDebug>
+
+FediverseApi::FediverseApi(QNetworkAccessManager *qnam, QObject *parent)
+ : QObject(parent)
+ , m_cancelRequested(false)
+ , m_qnam(qnam)
+{
+}
+
+FediverseApi::~FediverseApi()
+{
+}
+
+bool FediverseApi::uploadImage(const QString &filePath,
+ const QString &statusText,
+ const QString &mimeType,
+ const QString &apiHost,
+ const QString &accessToken)
+{
+ QFile file(filePath);
+ if (filePath.isEmpty() || !file.open(QIODevice::ReadOnly)) {
+ qWarning() << Q_FUNC_INFO << "error opening file:" << filePath;
+ return false;
+ }
+
+ m_cancelRequested = false;
+ m_apiHost = FediverseAuthUtils::normalizeApiHost(apiHost);
+ m_accessToken = accessToken;
+ m_statusText = statusText;
+
+ if (m_accessToken.isEmpty()) {
+ qWarning() << Q_FUNC_INFO << "missing access token";
+ return false;
+ }
+
+ const QByteArray imageData = file.readAll();
+ const QFileInfo fileInfo(filePath);
+
+ QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
+
+ QHttpPart filePart;
+ filePart.setHeader(QNetworkRequest::ContentDispositionHeader,
+ QVariant(QStringLiteral("form-data; name=\"file\"; filename=\"%1\"")
+ .arg(fileInfo.fileName())));
+ if (!mimeType.isEmpty()) {
+ filePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant(mimeType));
+ }
+ filePart.setBody(imageData);
+ multiPart->append(filePart);
+
+ QNetworkRequest request(QUrl(m_apiHost + QStringLiteral("/api/v1/media")));
+ request.setRawHeader(QByteArrayLiteral("Authorization"),
+ QByteArrayLiteral("Bearer ") + m_accessToken.toUtf8());
+
+ QNetworkReply *reply = m_qnam->post(request, multiPart);
+ if (!reply) {
+ delete multiPart;
+ return false;
+ }
+
+ multiPart->setParent(reply);
+ m_replies.insert(reply, UPLOAD_MEDIA);
+
+ connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
+ this, SLOT(replyError(QNetworkReply::NetworkError)));
+ connect(reply, &QNetworkReply::uploadProgress,
+ this, &FediverseApi::uploadProgress);
+ connect(reply, &QNetworkReply::finished,
+ this, &FediverseApi::finished);
+
+ return true;
+}
+
+bool FediverseApi::postStatus(const QString &statusText,
+ const QString &apiHost,
+ const QString &accessToken)
+{
+ m_cancelRequested = false;
+ m_apiHost = FediverseAuthUtils::normalizeApiHost(apiHost);
+ m_accessToken = accessToken;
+ m_statusText = statusText;
+
+ if (m_accessToken.isEmpty()) {
+ qWarning() << Q_FUNC_INFO << "missing access token";
+ return false;
+ }
+
+ return postStatusInternal(QString());
+}
+
+bool FediverseApi::postStatusInternal(const QString &mediaId)
+{
+ if (m_statusText.trimmed().isEmpty() && mediaId.isEmpty()) {
+ qWarning() << Q_FUNC_INFO << "status and media id are empty";
+ return false;
+ }
+
+ QUrlQuery query;
+ if (!m_statusText.isEmpty()) {
+ query.addQueryItem(QStringLiteral("status"), m_statusText);
+ }
+ if (!mediaId.isEmpty()) {
+ query.addQueryItem(QStringLiteral("media_ids[]"), mediaId);
+ }
+
+ const QByteArray postData = query.query(QUrl::FullyEncoded).toUtf8();
+
+ QNetworkRequest request(QUrl(m_apiHost + QStringLiteral("/api/v1/statuses")));
+ request.setRawHeader(QByteArrayLiteral("Authorization"),
+ QByteArrayLiteral("Bearer ") + m_accessToken.toUtf8());
+ request.setHeader(QNetworkRequest::ContentTypeHeader,
+ QVariant(QStringLiteral("application/x-www-form-urlencoded")));
+
+ QNetworkReply *reply = m_qnam->post(request, postData);
+ if (!reply) {
+ return false;
+ }
+
+ m_replies.insert(reply, POST_STATUS);
+ connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
+ this, SLOT(replyError(QNetworkReply::NetworkError)));
+ connect(reply, &QNetworkReply::finished,
+ this, &FediverseApi::finished);
+
+ return true;
+}
+
+void FediverseApi::cancelUpload()
+{
+ if (m_replies.isEmpty()) {
+ qWarning() << Q_FUNC_INFO << "can't cancel upload";
+ return;
+ }
+
+ m_cancelRequested = true;
+ const QList<QNetworkReply*> replies = m_replies.keys();
+ Q_FOREACH (QNetworkReply *reply, replies) {
+ reply->abort();
+ }
+}
+
+void FediverseApi::replyError(QNetworkReply::NetworkError error)
+{
+ Q_UNUSED(error)
+}
+
+void FediverseApi::uploadProgress(qint64 sent, qint64 total)
+{
+ if (total > 0) {
+ emit transferProgressUpdated(sent / static_cast<qreal>(total));
+ }
+}
+
+void FediverseApi::finished()
+{
+ QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
+ if (!reply || !m_replies.contains(reply)) {
+ return;
+ }
+
+ const API_CALL apiCall = m_replies.take(reply);
+ const QByteArray data = reply->readAll();
+ const int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ const QNetworkReply::NetworkError error = reply->error();
+
+ reply->deleteLater();
+
+ if (m_cancelRequested && error == QNetworkReply::OperationCanceledError) {
+ if (m_replies.isEmpty()) {
+ m_cancelRequested = false;
+ emit transferCanceled();
+ }
+ return;
+ }
+
+ if (apiCall == UPLOAD_MEDIA) {
+ if (error != QNetworkReply::NoError || httpCode < 200 || httpCode >= 300) {
+ finishTransfer(error == QNetworkReply::NoError ? QNetworkReply::UnknownNetworkError : error,
+ httpCode,
+ data);
+ return;
+ }
+
+ QString mediaId;
+ const QJsonDocument doc = QJsonDocument::fromJson(data);
+ if (doc.isObject()) {
+ const QJsonValue idValue = doc.object().value(QStringLiteral("id"));
+ if (idValue.isString()) {
+ mediaId = idValue.toString();
+ } else if (idValue.isDouble()) {
+ mediaId = QString::number(static_cast<qint64>(idValue.toDouble()));
+ }
+ }
+
+ if (!postStatusInternal(mediaId)) {
+ qWarning() << Q_FUNC_INFO << "unable to create fediverse status";
+ emit transferError();
+ }
+ return;
+ }
+
+ if (apiCall == POST_STATUS) {
+ finishTransfer(error, httpCode, data);
+ return;
+ }
+
+ emit transferError();
+}
+
+void FediverseApi::finishTransfer(QNetworkReply::NetworkError error, int httpCode, const QByteArray &data)
+{
+ m_cancelRequested = false;
+
+ if (httpCode == 401) {
+ emit credentialsExpired();
+ }
+
+ if (error != QNetworkReply::NoError) {
+ if (error == QNetworkReply::OperationCanceledError) {
+ emit transferCanceled();
+ return;
+ }
+
+ qWarning() << Q_FUNC_INFO << "network error:" << error << "httpCode:" << httpCode << "data:" << data;
+ emit transferError();
+ return;
+ }
+
+ if (httpCode < 200 || httpCode >= 300) {
+ qWarning() << Q_FUNC_INFO << "http error:" << httpCode << "data:" << data;
+ emit transferError();
+ return;
+ }
+
+ emit transferFinished();
+}
diff --git a/transferengine-plugins/fediversetransferplugin/fediverseapi.h b/transferengine-plugins/fediversetransferplugin/fediverseapi.h
new file mode 100644
index 0000000..a85442c
--- /dev/null
+++ b/transferengine-plugins/fediversetransferplugin/fediverseapi.h
@@ -0,0 +1,65 @@
+/*
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#ifndef FEDIVERSEAPI_H
+#define FEDIVERSEAPI_H
+
+#include <QtCore/QMap>
+#include <QtCore/QObject>
+
+#include <QtNetwork/QNetworkAccessManager>
+#include <QtNetwork/QNetworkReply>
+
+class FediverseApi : public QObject
+{
+ Q_OBJECT
+
+public:
+ enum API_CALL {
+ NONE,
+ UPLOAD_MEDIA,
+ POST_STATUS
+ };
+
+ explicit FediverseApi(QNetworkAccessManager *qnam, QObject *parent = 0);
+ ~FediverseApi();
+
+ bool uploadImage(const QString &filePath,
+ const QString &statusText,
+ const QString &mimeType,
+ const QString &apiHost,
+ const QString &accessToken);
+ bool postStatus(const QString &statusText,
+ const QString &apiHost,
+ const QString &accessToken);
+
+ void cancelUpload();
+
+Q_SIGNALS:
+ void transferProgressUpdated(qreal progress);
+ void transferFinished();
+ void transferError();
+ void transferCanceled();
+ void credentialsExpired();
+
+private Q_SLOTS:
+ void replyError(QNetworkReply::NetworkError error);
+ void finished();
+ void uploadProgress(qint64 received, qint64 total);
+
+private:
+ bool postStatusInternal(const QString &mediaId);
+ void finishTransfer(QNetworkReply::NetworkError error, int httpCode, const QByteArray &data);
+
+ QMap<QNetworkReply*, API_CALL> m_replies;
+ bool m_cancelRequested;
+ QNetworkAccessManager *m_qnam;
+ QString m_accessToken;
+ QString m_apiHost;
+ QString m_statusText;
+};
+
+#endif // FEDIVERSEAPI_H
diff --git a/transferengine-plugins/fediversetransferplugin/fediversetransferplugin.cpp b/transferengine-plugins/fediversetransferplugin/fediversetransferplugin.cpp
new file mode 100644
index 0000000..bd213f8
--- /dev/null
+++ b/transferengine-plugins/fediversetransferplugin/fediversetransferplugin.cpp
@@ -0,0 +1,31 @@
+/*
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include "fediversetransferplugin.h"
+#include "fediverseuploader.h"
+
+#include <QtPlugin>
+#include <QNetworkAccessManager>
+
+FediverseTransferPlugin::FediverseTransferPlugin()
+ : QObject(), TransferPluginInterface()
+ , m_qnam(new QNetworkAccessManager(this))
+{
+}
+
+FediverseTransferPlugin::~FediverseTransferPlugin()
+{
+}
+
+MediaTransferInterface *FediverseTransferPlugin::transferObject()
+{
+ return new FediverseUploader(m_qnam, this);
+}
+
+QString FediverseTransferPlugin::pluginId() const
+{
+ return QLatin1String("Fediverse");
+}
diff --git a/transferengine-plugins/fediversetransferplugin/fediversetransferplugin.h b/transferengine-plugins/fediversetransferplugin/fediversetransferplugin.h
new file mode 100644
index 0000000..163d23f
--- /dev/null
+++ b/transferengine-plugins/fediversetransferplugin/fediversetransferplugin.h
@@ -0,0 +1,33 @@
+/*
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#ifndef FEDIVERSETRANSFERPLUGIN_H
+#define FEDIVERSETRANSFERPLUGIN_H
+
+#include <QtCore/QObject>
+
+#include <transferplugininterface.h>
+
+class QNetworkAccessManager;
+
+class Q_DECL_EXPORT FediverseTransferPlugin : public QObject, public TransferPluginInterface
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID "org.sailfishos.transfer.plugin.fediverse")
+ Q_INTERFACES(TransferPluginInterface)
+
+public:
+ FediverseTransferPlugin();
+ ~FediverseTransferPlugin();
+
+ MediaTransferInterface *transferObject();
+ QString pluginId() const;
+
+private:
+ QNetworkAccessManager *m_qnam;
+};
+
+#endif // FEDIVERSETRANSFERPLUGIN_H
diff --git a/transferengine-plugins/fediversetransferplugin/fediversetransferplugin.pro b/transferengine-plugins/fediversetransferplugin/fediversetransferplugin.pro
new file mode 100644
index 0000000..8451dc5
--- /dev/null
+++ b/transferengine-plugins/fediversetransferplugin/fediversetransferplugin.pro
@@ -0,0 +1,30 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+TEMPLATE = lib
+TARGET = $$qtLibraryTarget(fediversetransferplugin)
+CONFIG += plugin
+DEPENDPATH += .
+INCLUDEPATH += ..
+INCLUDEPATH += ../../common
+
+QT -= gui
+QT += network
+
+CONFIG += link_pkgconfig
+PKGCONFIG += nemotransferengine-qt5 accounts-qt5 sailfishaccounts libsignon-qt5
+
+HEADERS += fediversetransferplugin.h \
+ fediverseuploader.h \
+ ../fediverseshareservicestatus.h \
+ fediverseapi.h
+
+SOURCES += fediversetransferplugin.cpp \
+ fediverseuploader.cpp \
+ ../fediverseshareservicestatus.cpp \
+ fediverseapi.cpp
+
+target.path = $$[QT_INSTALL_LIBS]/nemo-transferengine/plugins/transfer
+
+INSTALLS += target
diff --git a/transferengine-plugins/fediversetransferplugin/fediverseuploader.cpp b/transferengine-plugins/fediversetransferplugin/fediverseuploader.cpp
new file mode 100644
index 0000000..7c8766b
--- /dev/null
+++ b/transferengine-plugins/fediversetransferplugin/fediverseuploader.cpp
@@ -0,0 +1,252 @@
+/*
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include "fediverseuploader.h"
+#include "fediverseapi.h"
+
+#include <imageoperation.h>
+#include <mediaitem.h>
+
+#include <QtCore/QFile>
+#include <QtCore/QMimeDatabase>
+#include <QtCore/QMimeType>
+
+#include <QtDebug>
+
+FediverseUploader::FediverseUploader(QNetworkAccessManager *qnam, QObject *parent)
+ : MediaTransferInterface(parent)
+ , m_api(0)
+ , m_fediverseShareServiceStatus(0)
+ , m_qnam(qnam)
+ , m_useTmpFile(false)
+{
+}
+
+FediverseUploader::~FediverseUploader()
+{
+}
+
+QString FediverseUploader::displayName() const
+{
+ return tr("Fediverse");
+}
+
+QUrl FediverseUploader::serviceIcon() const
+{
+ return QUrl(QStringLiteral("image://theme/icon-l-fediverse"));
+}
+
+bool FediverseUploader::cancelEnabled() const
+{
+ return true;
+}
+
+bool FediverseUploader::restartEnabled() const
+{
+ return true;
+}
+
+void FediverseUploader::start()
+{
+ if (!mediaItem()) {
+ qWarning() << Q_FUNC_INFO << "NULL MediaItem. Can't continue";
+ setStatus(MediaTransferInterface::TransferInterrupted);
+ return;
+ }
+
+ if (!m_fediverseShareServiceStatus) {
+ m_fediverseShareServiceStatus = new FediverseShareServiceStatus(this);
+ connect(m_fediverseShareServiceStatus, &FediverseShareServiceStatus::serviceReady,
+ this, &FediverseUploader::startUploading);
+ connect(m_fediverseShareServiceStatus, &FediverseShareServiceStatus::serviceError,
+ this, [this] (const QString &) {
+ transferError();
+ });
+ }
+
+ m_fediverseShareServiceStatus->queryStatus();
+}
+
+void FediverseUploader::cancel()
+{
+ if (m_api) {
+ m_api->cancelUpload();
+ } else {
+ qWarning() << Q_FUNC_INFO << "Can't cancel. NULL FediverseApi object!";
+ }
+}
+
+void FediverseUploader::startUploading()
+{
+ if (!m_fediverseShareServiceStatus) {
+ qWarning() << Q_FUNC_INFO << "NULL FediverseShareServiceStatus object!";
+ return;
+ }
+
+ const quint32 accountId = mediaItem()->value(MediaItem::AccountId).toInt();
+ m_accountDetails = m_fediverseShareServiceStatus->detailsByIdentifier(accountId);
+ if (m_accountDetails.accountId <= 0 || m_accountDetails.accessToken.isEmpty()) {
+ qWarning() << Q_FUNC_INFO << "Fediverse account details missing for id" << accountId;
+ transferError();
+ return;
+ }
+
+ const QString mimeType = mediaItem()->value(MediaItem::MimeType).toString();
+ if (mimeType.startsWith(QLatin1String("image/"))
+ || mimeType.startsWith(QLatin1String("video/"))) {
+ postImage();
+ } else if (mimeType.contains(QLatin1String("text/plain"))
+ || mimeType.contains(QLatin1String("text/x-url"))) {
+ postStatus();
+ } else {
+ qWarning() << Q_FUNC_INFO << "Unsupported mime type:" << mimeType;
+ setStatus(MediaTransferInterface::TransferInterrupted);
+ }
+}
+
+void FediverseUploader::transferFinished()
+{
+ setStatus(MediaTransferInterface::TransferFinished);
+}
+
+void FediverseUploader::transferProgress(qreal progress)
+{
+ setProgress(progress);
+}
+
+void FediverseUploader::transferError()
+{
+ setStatus(MediaTransferInterface::TransferInterrupted);
+ qWarning() << Q_FUNC_INFO << "Transfer interrupted";
+}
+
+void FediverseUploader::transferCanceled()
+{
+ setStatus(MediaTransferInterface::TransferCanceled);
+}
+
+void FediverseUploader::credentialsExpired()
+{
+ const quint32 accountId = mediaItem()->value(MediaItem::AccountId).toInt();
+ m_fediverseShareServiceStatus->setCredentialsNeedUpdate(accountId, QStringLiteral("fediverse-sharing"));
+}
+
+void FediverseUploader::setStatus(MediaTransferInterface::TransferStatus status)
+{
+ const bool finished = (status == TransferCanceled
+ || status == TransferInterrupted
+ || status == TransferFinished);
+ if (m_useTmpFile && finished) {
+ QFile::remove(m_filePath);
+ m_useTmpFile = false;
+ m_filePath.clear();
+ }
+
+ MediaTransferInterface::setStatus(status);
+}
+
+void FediverseUploader::postImage()
+{
+ m_useTmpFile = false;
+ m_filePath.clear();
+ const QString sourceFile = mediaItem()->value(MediaItem::Url).toUrl().toLocalFile();
+ if (sourceFile.isEmpty()) {
+ qWarning() << Q_FUNC_INFO << "Empty source file";
+ setStatus(MediaTransferInterface::TransferInterrupted);
+ return;
+ }
+
+ QMimeDatabase db;
+ const QMimeType mime = db.mimeTypeForFile(sourceFile);
+ const bool isImage = mediaItem()->value(MediaItem::MimeType).toString().startsWith(QLatin1String("image/"));
+ const bool isJpeg = isImage && mime.name() == QLatin1String("image/jpeg");
+
+ if (isJpeg && mediaItem()->value(MediaItem::MetadataStripped).toBool()) {
+ m_useTmpFile = true;
+ m_filePath = ImageOperation::removeImageMetadata(sourceFile);
+ if (m_filePath.isEmpty()) {
+ qWarning() << Q_FUNC_INFO << "Failed to remove metadata";
+ MediaTransferInterface::setStatus(MediaTransferInterface::TransferInterrupted);
+ return;
+ }
+ }
+
+ const qreal scale = mediaItem()->value(MediaItem::ScalePercent).toReal();
+ if (isImage && 0 < scale && scale < 1) {
+ m_useTmpFile = true;
+ m_filePath = ImageOperation::scaleImage(sourceFile, scale, m_filePath);
+ if (m_filePath.isEmpty()) {
+ qWarning() << Q_FUNC_INFO << "Failed to scale image";
+ MediaTransferInterface::setStatus(MediaTransferInterface::TransferInterrupted);
+ return;
+ }
+ }
+
+ if (!m_useTmpFile) {
+ m_filePath = sourceFile;
+ }
+
+ ensureApi();
+
+ const bool ok = m_api->uploadImage(m_filePath,
+ mediaItem()->value(MediaItem::Description).toString(),
+ mediaItem()->value(MediaItem::MimeType).toString(),
+ m_accountDetails.apiHost,
+ m_accountDetails.accessToken);
+ if (ok) {
+ setStatus(MediaTransferInterface::TransferStarted);
+ } else {
+ setStatus(MediaTransferInterface::TransferInterrupted);
+ qWarning() << Q_FUNC_INFO << "Failed to upload media";
+ }
+}
+
+void FediverseUploader::postStatus()
+{
+ ensureApi();
+
+ const QVariantMap userData = mediaItem()->value(MediaItem::UserData).toMap();
+ QString statusText = userData.value(QStringLiteral("status")).toString().trimmed();
+ if (statusText.isEmpty()) {
+ statusText = mediaItem()->value(MediaItem::Description).toString().trimmed();
+ }
+ if (statusText.isEmpty()) {
+ statusText = mediaItem()->value(MediaItem::ContentData).toString().trimmed();
+ }
+
+ if (statusText.isEmpty()) {
+ qWarning() << Q_FUNC_INFO << "Failed to resolve status text";
+ setStatus(MediaTransferInterface::TransferInterrupted);
+ return;
+ }
+
+ const bool ok = m_api->postStatus(statusText,
+ m_accountDetails.apiHost,
+ m_accountDetails.accessToken);
+ if (ok) {
+ setStatus(MediaTransferInterface::TransferStarted);
+ } else {
+ setStatus(MediaTransferInterface::TransferInterrupted);
+ qWarning() << Q_FUNC_INFO << "Failed to post status";
+ }
+}
+
+void FediverseUploader::ensureApi()
+{
+ if (!m_api) {
+ m_api = new FediverseApi(m_qnam, this);
+ connect(m_api, &FediverseApi::transferProgressUpdated,
+ this, &FediverseUploader::transferProgress);
+ connect(m_api, &FediverseApi::transferFinished,
+ this, &FediverseUploader::transferFinished);
+ connect(m_api, &FediverseApi::transferError,
+ this, &FediverseUploader::transferError);
+ connect(m_api, &FediverseApi::transferCanceled,
+ this, &FediverseUploader::transferCanceled);
+ connect(m_api, &FediverseApi::credentialsExpired,
+ this, &FediverseUploader::credentialsExpired);
+ }
+}
diff --git a/transferengine-plugins/fediversetransferplugin/fediverseuploader.h b/transferengine-plugins/fediversetransferplugin/fediverseuploader.h
new file mode 100644
index 0000000..2343145
--- /dev/null
+++ b/transferengine-plugins/fediversetransferplugin/fediverseuploader.h
@@ -0,0 +1,59 @@
+/*
+ * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#ifndef FEDIVERSEUPLOADER_H
+#define FEDIVERSEUPLOADER_H
+
+#include <QtNetwork/QNetworkAccessManager>
+
+#include <mediatransferinterface.h>
+
+#include "fediverseshareservicestatus.h"
+
+class FediverseApi;
+
+class FediverseUploader : public MediaTransferInterface
+{
+ Q_OBJECT
+
+public:
+ FediverseUploader(QNetworkAccessManager *qnam, QObject *parent = 0);
+ ~FediverseUploader();
+
+ QString displayName() const;
+ QUrl serviceIcon() const;
+ bool cancelEnabled() const;
+ bool restartEnabled() const;
+
+public Q_SLOTS:
+ void start();
+ void cancel();
+
+private Q_SLOTS:
+ void startUploading();
+ void transferFinished();
+ void transferProgress(qreal progress);
+ void transferError();
+ void transferCanceled();
+ void credentialsExpired();
+
+protected:
+ void setStatus(MediaTransferInterface::TransferStatus status);
+
+private:
+ void ensureApi();
+ void postImage();
+ void postStatus();
+
+ FediverseApi *m_api;
+ FediverseShareServiceStatus *m_fediverseShareServiceStatus;
+ QNetworkAccessManager *m_qnam;
+ FediverseShareServiceStatus::AccountDetails m_accountDetails;
+ bool m_useTmpFile;
+ QString m_filePath;
+};
+
+#endif // FEDIVERSEUPLOADER_H