diff options
Diffstat (limited to 'transferengine-plugins/fediversetransferplugin')
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 |
