From a35c9fa159173388d88ef77e1d31f53488aad094 Mon Sep 17 00:00:00 2001 From: Andrew Branson Date: Fri, 3 Apr 2026 22:55:30 +0200 Subject: Generalize for all fediverse accounts --- .../fediverseshareplugin/FediverseSharePost.qml | 138 ++++++++++ .../fediverseshareplugin/fediverseplugininfo.cpp | 62 +++++ .../fediverseshareplugin/fediverseplugininfo.h | 35 +++ .../fediverseshareplugin/fediverseshareplugin.cpp | 29 ++ .../fediverseshareplugin/fediverseshareplugin.h | 28 ++ .../fediverseshareplugin/fediverseshareplugin.pro | 32 +++ .../fediverseshareservicestatus.cpp | 305 +++++++++++++++++++++ .../fediverseshareservicestatus.h | 82 ++++++ .../fediversetransferplugin/fediverseapi.cpp | 255 +++++++++++++++++ .../fediversetransferplugin/fediverseapi.h | 65 +++++ .../fediversetransferplugin.cpp | 31 +++ .../fediversetransferplugin.h | 33 +++ .../fediversetransferplugin.pro | 30 ++ .../fediversetransferplugin/fediverseuploader.cpp | 252 +++++++++++++++++ .../fediversetransferplugin/fediverseuploader.h | 59 ++++ .../mastodonshareplugin/MastodonSharePost.qml | 138 ---------- .../mastodonshareplugin/mastodonplugininfo.cpp | 60 ---- .../mastodonshareplugin/mastodonplugininfo.h | 35 --- .../mastodonshareplugin/mastodonshareplugin.cpp | 29 -- .../mastodonshareplugin/mastodonshareplugin.h | 28 -- .../mastodonshareplugin/mastodonshareplugin.pro | 30 -- .../mastodonshareservicestatus.cpp | 297 -------------------- .../mastodonshareservicestatus.h | 81 ------ .../mastodontransferplugin/mastodonapi.cpp | 255 ----------------- .../mastodontransferplugin/mastodonapi.h | 65 ----- .../mastodontransferplugin.cpp | 31 --- .../mastodontransferplugin.h | 33 --- .../mastodontransferplugin.pro | 29 -- .../mastodontransferplugin/mastodonuploader.cpp | 252 ----------------- .../mastodontransferplugin/mastodonuploader.h | 59 ---- transferengine-plugins/transferengine-plugins.pro | 2 +- 31 files changed, 1437 insertions(+), 1423 deletions(-) create mode 100644 transferengine-plugins/fediverseshareplugin/FediverseSharePost.qml create mode 100644 transferengine-plugins/fediverseshareplugin/fediverseplugininfo.cpp create mode 100644 transferengine-plugins/fediverseshareplugin/fediverseplugininfo.h create mode 100644 transferengine-plugins/fediverseshareplugin/fediverseshareplugin.cpp create mode 100644 transferengine-plugins/fediverseshareplugin/fediverseshareplugin.h create mode 100644 transferengine-plugins/fediverseshareplugin/fediverseshareplugin.pro create mode 100644 transferengine-plugins/fediverseshareservicestatus.cpp create mode 100644 transferengine-plugins/fediverseshareservicestatus.h create mode 100644 transferengine-plugins/fediversetransferplugin/fediverseapi.cpp create mode 100644 transferengine-plugins/fediversetransferplugin/fediverseapi.h create mode 100644 transferengine-plugins/fediversetransferplugin/fediversetransferplugin.cpp create mode 100644 transferengine-plugins/fediversetransferplugin/fediversetransferplugin.h create mode 100644 transferengine-plugins/fediversetransferplugin/fediversetransferplugin.pro create mode 100644 transferengine-plugins/fediversetransferplugin/fediverseuploader.cpp create mode 100644 transferengine-plugins/fediversetransferplugin/fediverseuploader.h delete mode 100644 transferengine-plugins/mastodonshareplugin/MastodonSharePost.qml delete mode 100644 transferengine-plugins/mastodonshareplugin/mastodonplugininfo.cpp delete mode 100644 transferengine-plugins/mastodonshareplugin/mastodonplugininfo.h delete mode 100644 transferengine-plugins/mastodonshareplugin/mastodonshareplugin.cpp delete mode 100644 transferengine-plugins/mastodonshareplugin/mastodonshareplugin.h delete mode 100644 transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro delete mode 100644 transferengine-plugins/mastodonshareservicestatus.cpp delete mode 100644 transferengine-plugins/mastodonshareservicestatus.h delete mode 100644 transferengine-plugins/mastodontransferplugin/mastodonapi.cpp delete mode 100644 transferengine-plugins/mastodontransferplugin/mastodonapi.h delete mode 100644 transferengine-plugins/mastodontransferplugin/mastodontransferplugin.cpp delete mode 100644 transferengine-plugins/mastodontransferplugin/mastodontransferplugin.h delete mode 100644 transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro delete mode 100644 transferengine-plugins/mastodontransferplugin/mastodonuploader.cpp delete mode 100644 transferengine-plugins/mastodontransferplugin/mastodonuploader.h (limited to 'transferengine-plugins') diff --git a/transferengine-plugins/fediverseshareplugin/FediverseSharePost.qml b/transferengine-plugins/fediverseshareplugin/FediverseSharePost.qml new file mode 100644 index 0000000..d859d96 --- /dev/null +++ b/transferengine-plugins/fediverseshareplugin/FediverseSharePost.qml @@ -0,0 +1,138 @@ +/* + * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Sailfish.Lipstick 1.0 +import Sailfish.TransferEngine 1.0 + +Item { + id: root + + property var shareAction + property string mimeType: { + if (shareAction && shareAction.mimeType) { + return shareAction.mimeType + } + if (shareAction && shareAction.resources + && shareAction.resources.length > 0 + && shareAction.resources[0] + && shareAction.resources[0].type) { + return shareAction.resources[0].type + } + return "" + } + property bool textShare: mimeType === "text/x-url" || mimeType === "text/plain" + + width: parent ? parent.width : 0 + height: previewLoader.item ? previewLoader.item.height : 0 + + Loader { + id: previewLoader + + anchors.fill: parent + sourceComponent: root.textShare ? postPreview : imagePreview + } + + Component { + id: imagePreview + + ShareFilePreview { + shareAction: root.shareAction + metadataStripped: true + descriptionPlaceholderText: qsTr("Write a post") + } + } + + Component { + id: postPreview + + SilicaFlickable { + id: postRoot + + width: parent.width + height: contentHeight + contentHeight: contentColumn.height + + Component.onCompleted: { + sailfishTransfer.loadConfiguration(root.shareAction.toConfiguration()) + statusTextField.forceActiveFocus() + statusTextField.cursorPosition = statusTextField.text.length + } + + SailfishTransfer { + id: sailfishTransfer + } + + Column { + id: contentColumn + + width: parent.width + + TextArea { + id: linkTextField + + width: parent.width + //% "Link" + label: qsTrId("sailfishshare-la-link") + placeholderText: label + visible: sailfishTransfer.content.type === "text/x-url" + text: sailfishTransfer.content.data || sailfishTransfer.content.status || "" + } + + TextArea { + id: statusTextField + + width: parent.width + //% "Status update" + label: qsTrId("sailfishshare-la-status_update") + placeholderText: label + text: { + var title = sailfishTransfer.content.name || sailfishTransfer.content.linkTitle || "" + if (linkTextField.visible) { + return title + } + var body = sailfishTransfer.content.data || sailfishTransfer.content.status || "" + if (title.length > 0 && body.length > 0) { + return title + ": " + body + } + return title + body + } + } + + SystemDialogIconButton { + id: postButton + + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width / 2 + iconSource: "image://theme/icon-m-share" + bottomPadding: Theme.paddingLarge + _showPress: false + + //: Post a social network account status update + //% "Post" + text: qsTrId("sailfishshare-la-post_status") + + onClicked: { + var status = statusTextField.text || "" + var link = linkTextField.visible ? (linkTextField.text || "") : "" + if (link.length > 0 && status.indexOf(link) === -1) { + status = status.length > 0 ? (status + "\n" + link) : link + } + + sailfishTransfer.userData = { + "accountId": sailfishTransfer.transferMethodInfo.accountId, + "status": status + } + sailfishTransfer.mimeType = linkTextField.visible ? "text/x-url" : "text/plain" + sailfishTransfer.start() + root.shareAction.done() + } + } + } + } + } +} diff --git a/transferengine-plugins/fediverseshareplugin/fediverseplugininfo.cpp b/transferengine-plugins/fediverseshareplugin/fediverseplugininfo.cpp new file mode 100644 index 0000000..4e04be1 --- /dev/null +++ b/transferengine-plugins/fediverseshareplugin/fediverseplugininfo.cpp @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "fediverseplugininfo.h" +#include "fediverseshareservicestatus.h" + +FediversePluginInfo::FediversePluginInfo() + : SharingPluginInfo() + , m_fediverseShareServiceStatus(new FediverseShareServiceStatus(this)) +{ + m_capabilities << QLatin1String("image/jpeg") + << QLatin1String("image/png") + << QLatin1String("video/mp4") + << QLatin1String("text/x-url") + << QLatin1String("text/plain"); + + connect(m_fediverseShareServiceStatus, &FediverseShareServiceStatus::serviceReady, + this, &FediversePluginInfo::serviceReady); + connect(m_fediverseShareServiceStatus, &FediverseShareServiceStatus::serviceError, + this, &FediversePluginInfo::infoError); +} + +FediversePluginInfo::~FediversePluginInfo() +{ +} + +QList FediversePluginInfo::info() const +{ + return m_info; +} + +void FediversePluginInfo::query() +{ + m_fediverseShareServiceStatus->queryStatus(FediverseShareServiceStatus::PassiveMode); +} + +void FediversePluginInfo::serviceReady() +{ + m_info.clear(); + + for (int i = 0; i < m_fediverseShareServiceStatus->count(); ++i) { + SharingMethodInfo info; + + const FediverseShareServiceStatus::AccountDetails details = m_fediverseShareServiceStatus->details(i); + info.setDisplayName(details.providerName); + info.setSubtitle(details.displayName); + info.setAccountId(details.accountId); + + info.setMethodId(QLatin1String("Fediverse")); + info.setMethodIcon(details.iconPath.isEmpty() + ? QLatin1String("image://theme/icon-l-fediverse") + : details.iconPath); + info.setShareUIPath(QLatin1String("/usr/share/nemo-transferengine/plugins/sharing/FediverseSharePost.qml")); + info.setCapabilities(m_capabilities); + m_info << info; + } + + emit infoReady(); +} diff --git a/transferengine-plugins/fediverseshareplugin/fediverseplugininfo.h b/transferengine-plugins/fediverseshareplugin/fediverseplugininfo.h new file mode 100644 index 0000000..fdd8fc6 --- /dev/null +++ b/transferengine-plugins/fediverseshareplugin/fediverseplugininfo.h @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef FEDIVERSEPLUGININFO_H +#define FEDIVERSEPLUGININFO_H + +#include +#include + +class FediverseShareServiceStatus; + +class FediversePluginInfo : public SharingPluginInfo +{ + Q_OBJECT + +public: + FediversePluginInfo(); + ~FediversePluginInfo(); + + QList info() const; + void query(); + +private Q_SLOTS: + void serviceReady(); + +private: + FediverseShareServiceStatus *m_fediverseShareServiceStatus; + QList m_info; + QStringList m_capabilities; +}; + +#endif // FEDIVERSEPLUGININFO_H diff --git a/transferengine-plugins/fediverseshareplugin/fediverseshareplugin.cpp b/transferengine-plugins/fediverseshareplugin/fediverseshareplugin.cpp new file mode 100644 index 0000000..18c9c7c --- /dev/null +++ b/transferengine-plugins/fediverseshareplugin/fediverseshareplugin.cpp @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "fediverseshareplugin.h" +#include "fediverseplugininfo.h" + +#include + +FediverseSharePlugin::FediverseSharePlugin() + : QObject(), SharingPluginInterface() +{ +} + +FediverseSharePlugin::~FediverseSharePlugin() +{ +} + +SharingPluginInfo *FediverseSharePlugin::infoObject() +{ + return new FediversePluginInfo; +} + +QString FediverseSharePlugin::pluginId() const +{ + return QLatin1String("Fediverse"); +} diff --git a/transferengine-plugins/fediverseshareplugin/fediverseshareplugin.h b/transferengine-plugins/fediverseshareplugin/fediverseshareplugin.h new file mode 100644 index 0000000..0eb7772 --- /dev/null +++ b/transferengine-plugins/fediverseshareplugin/fediverseshareplugin.h @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef FEDIVERSESHAREPLUGIN_H +#define FEDIVERSESHAREPLUGIN_H + +#include + +#include + +class Q_DECL_EXPORT FediverseSharePlugin : public QObject, public SharingPluginInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.sailfishos.share.plugin.fediverse") + Q_INTERFACES(SharingPluginInterface) + +public: + FediverseSharePlugin(); + ~FediverseSharePlugin(); + + SharingPluginInfo *infoObject(); + QString pluginId() const; +}; + +#endif // FEDIVERSESHAREPLUGIN_H diff --git a/transferengine-plugins/fediverseshareplugin/fediverseshareplugin.pro b/transferengine-plugins/fediverseshareplugin/fediverseshareplugin.pro new file mode 100644 index 0000000..a085a30 --- /dev/null +++ b/transferengine-plugins/fediverseshareplugin/fediverseshareplugin.pro @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. +# +# SPDX-License-Identifier: BSD-3-Clause + +TEMPLATE = lib +TARGET = $$qtLibraryTarget(fediverseshareplugin) +CONFIG += plugin +DEPENDPATH += . +INCLUDEPATH += .. +INCLUDEPATH += ../../common + +QT -= gui + +CONFIG += link_pkgconfig +PKGCONFIG += nemotransferengine-qt5 accounts-qt5 sailfishaccounts libsignon-qt5 + +HEADERS += fediverseshareplugin.h \ + fediverseplugininfo.h \ + ../fediverseshareservicestatus.h + +SOURCES += fediverseshareplugin.cpp \ + fediverseplugininfo.cpp \ + ../fediverseshareservicestatus.cpp + +target.path = $$[QT_INSTALL_LIBS]/nemo-transferengine/plugins/sharing + +OTHER_FILES += *.qml + +shareui.files = FediverseSharePost.qml +shareui.path = /usr/share/nemo-transferengine/plugins/sharing + +INSTALLS += target shareui diff --git a/transferengine-plugins/fediverseshareservicestatus.cpp b/transferengine-plugins/fediverseshareservicestatus.cpp new file mode 100644 index 0000000..0ab1460 --- /dev/null +++ b/transferengine-plugins/fediverseshareservicestatus.cpp @@ -0,0 +1,305 @@ +/* + * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "fediverseshareservicestatus.h" +#include "fediverseauthutils.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +FediverseShareServiceStatus::FediverseShareServiceStatus(QObject *parent) + : QObject(parent) + , m_auth(new AccountAuthenticator(this)) + , m_accountManager(new Accounts::Manager(this)) + , m_serviceName(QStringLiteral("fediverse-sharing")) +{ +} + +QString FediverseShareServiceStatus::authServiceName() const +{ + return QStringLiteral("fediverse-microblog"); +} + +void FediverseShareServiceStatus::signIn(int accountId) +{ + Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this); + if (!account) { + qWarning() << Q_FUNC_INFO << "Failed to retrieve account for id:" << accountId; + setAccountDetailsState(accountId, Error); + return; + } + + const Accounts::Service service(m_accountManager->service(authServiceName())); + if (!service.isValid()) { + qWarning() << Q_FUNC_INFO << "Invalid auth service" << authServiceName(); + account->deleteLater(); + setAccountDetailsState(accountId, Error); + return; + } + + account->selectService(service); + + SignOn::Identity *identity = account->credentialsId() > 0 + ? SignOn::Identity::existingIdentity(account->credentialsId()) + : 0; + if (!identity) { + qWarning() << Q_FUNC_INFO << "account" << accountId << "has no valid credentials"; + account->deleteLater(); + setAccountDetailsState(accountId, Error); + return; + } + + Accounts::AccountService accountService(account, service); + const QString method = accountService.authData().method(); + const QString mechanism = accountService.authData().mechanism(); + + SignOn::AuthSession *session = identity->createSession(method); + if (!session) { + qWarning() << Q_FUNC_INFO << "could not create signon session for account" << accountId; + identity->deleteLater(); + account->deleteLater(); + setAccountDetailsState(accountId, Error); + return; + } + + QVariantMap signonSessionData = accountService.authData().parameters(); + + FediverseAuthUtils::addSignOnSessionParameters(account, &signonSessionData); + + connect(session, SIGNAL(response(SignOn::SessionData)), + this, SLOT(signOnResponse(SignOn::SessionData)), + Qt::UniqueConnection); + connect(session, SIGNAL(error(SignOn::Error)), + this, SLOT(signOnError(SignOn::Error)), + Qt::UniqueConnection); + + session->setProperty("account", QVariant::fromValue(account)); + session->setProperty("identity", QVariant::fromValue(identity)); + session->process(SignOn::SessionData(signonSessionData), mechanism); +} + +void FediverseShareServiceStatus::signOnResponse(const SignOn::SessionData &responseData) +{ + const QVariantMap data = FediverseAuthUtils::responseDataToMap(responseData); + + SignOn::AuthSession *session = qobject_cast(sender()); + Accounts::Account *account = session->property("account").value(); + SignOn::Identity *identity = session->property("identity").value(); + const int accountId = account ? account->id() : 0; + + QString accessToken = FediverseAuthUtils::accessToken(data); + + if (accountId > 0 && m_accountIdToDetailsIdx.contains(accountId)) { + AccountDetails &accountDetails(m_accountDetails[m_accountIdToDetailsIdx[accountId]]); + accountDetails.accessToken = accessToken; + setAccountDetailsState(accountId, accessToken.isEmpty() ? Error : Populated); + } + + session->disconnect(this); + if (identity) { + identity->destroySession(session); + identity->deleteLater(); + } + if (account) { + account->deleteLater(); + } +} + +void FediverseShareServiceStatus::signOnError(const SignOn::Error &error) +{ + SignOn::AuthSession *session = qobject_cast(sender()); + Accounts::Account *account = session->property("account").value(); + SignOn::Identity *identity = session->property("identity").value(); + const int accountId = account ? account->id() : 0; + + qWarning() << Q_FUNC_INFO << "failed to retrieve credentials for account" << accountId + << error.type() << error.message(); + + if (accountId > 0 && error.type() == SignOn::Error::UserInteraction) { + setCredentialsNeedUpdate(accountId, authServiceName()); + } + + session->disconnect(this); + if (identity) { + identity->destroySession(session); + identity->deleteLater(); + } + if (account) { + account->deleteLater(); + } + + if (accountId > 0) { + setAccountDetailsState(accountId, Error); + } +} + +void FediverseShareServiceStatus::setAccountDetailsState(int accountId, AccountDetailsState state) +{ + if (!m_accountIdToDetailsIdx.contains(accountId)) { + return; + } + + m_accountDetailsState[accountId] = state; + + bool anyWaiting = false; + bool anyPopulated = false; + Q_FOREACH (int id, m_accountDetailsState.keys()) { + AccountDetailsState accountState = m_accountDetailsState.value(id, Waiting); + if (accountState == Waiting) { + anyWaiting = true; + } else if (accountState == Populated) { + anyPopulated = true; + } + } + + if (!anyWaiting) { + if (anyPopulated) { + emit serviceReady(); + } else { + emit serviceError(QStringLiteral("Unable to retrieve Fediverse account credentials")); + } + } +} + +int FediverseShareServiceStatus::count() const +{ + return m_accountDetails.count(); +} + +bool FediverseShareServiceStatus::setCredentialsNeedUpdate(int accountId, const QString &serviceName) +{ + return m_auth->setCredentialsNeedUpdate(accountId, serviceName); +} + +void FediverseShareServiceStatus::queryStatus(QueryStatusMode mode) +{ + m_accountDetails.clear(); + m_accountIdToDetailsIdx.clear(); + m_accountDetailsState.clear(); + + bool signInActive = false; + Q_FOREACH (Accounts::AccountId id, m_accountManager->accountList()) { + Accounts::Account *acc = Accounts::Account::fromId(m_accountManager, id, this); + + if (!acc) { + qWarning() << Q_FUNC_INFO << "Failed to get account for id:" << id; + continue; + } + + acc->selectService(Accounts::Service()); + + const Accounts::Service service(m_accountManager->service(m_serviceName)); + const Accounts::ServiceList services = acc->services(); + bool serviceFound = false; + Q_FOREACH (const Accounts::Service &s, services) { + if (s.name() == m_serviceName) { + serviceFound = true; + break; + } + } + + if (!service.isValid() || !serviceFound) { + acc->deleteLater(); + continue; + } + + const bool accountEnabled = acc->enabled(); + acc->selectService(service); + const bool shareServiceEnabled = acc->enabled(); + if (!accountEnabled || !shareServiceEnabled) { + acc->selectService(Accounts::Service()); + acc->deleteLater(); + continue; + } + + if (acc->value(QStringLiteral("CredentialsNeedUpdate")).toBool()) { + qWarning() << Q_FUNC_INFO << "Credentials need update for account id:" << id; + acc->selectService(Accounts::Service()); + acc->deleteLater(); + continue; + } + + if (!m_accountIdToDetailsIdx.contains(id)) { + AccountDetails details; + details.accountId = id; + acc->selectService(Accounts::Service()); + details.apiHost = FediverseAuthUtils::normalizeApiHost(acc->value(QStringLiteral("api/Host")).toString()); + const QString instanceTitle = acc->value(QStringLiteral("instance/Title")).toString().trimmed(); + details.iconPath = acc->value(QStringLiteral("iconPath")).toString().trimmed(); + acc->selectService(service); + + QUrl apiUrl(details.apiHost); + details.providerName = instanceTitle; + if (details.providerName.isEmpty()) { + details.providerName = apiUrl.host(); + } + if (details.providerName.isEmpty()) { + details.providerName = details.apiHost; + if (details.providerName.startsWith(QLatin1String("https://"))) { + details.providerName.remove(0, 8); + } else if (details.providerName.startsWith(QLatin1String("http://"))) { + details.providerName.remove(0, 7); + } + const int separator = details.providerName.indexOf(QLatin1Char('/')); + if (separator > 0) { + details.providerName.truncate(separator); + } + } + + details.displayName = acc->value(QStringLiteral("description")).toString().trimmed(); + if (details.displayName.isEmpty()) { + details.displayName = acc->displayName(); + } + + m_accountIdToDetailsIdx.insert(id, m_accountDetails.size()); + m_accountDetails.append(details); + } + + if (mode == SignInMode) { + signInActive = true; + m_accountDetailsState.insert(id, Waiting); + signIn(id); + } + + acc->selectService(Accounts::Service()); + acc->deleteLater(); + } + + if (!signInActive) { + emit serviceReady(); + } +} + +FediverseShareServiceStatus::AccountDetails FediverseShareServiceStatus::details(int index) const +{ + if (index < 0 || index >= m_accountDetails.size()) { + qWarning() << Q_FUNC_INFO << "Index out of range"; + return AccountDetails(); + } + + return m_accountDetails.at(index); +} + +FediverseShareServiceStatus::AccountDetails FediverseShareServiceStatus::detailsByIdentifier(int accountIdentifier) const +{ + if (!m_accountIdToDetailsIdx.contains(accountIdentifier)) { + qWarning() << Q_FUNC_INFO << "No details known for account with identifier" << accountIdentifier; + return AccountDetails(); + } + + return m_accountDetails[m_accountIdToDetailsIdx[accountIdentifier]]; +} diff --git a/transferengine-plugins/fediverseshareservicestatus.h b/transferengine-plugins/fediverseshareservicestatus.h new file mode 100644 index 0000000..a82be17 --- /dev/null +++ b/transferengine-plugins/fediverseshareservicestatus.h @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef FEDIVERSESHARESERVICESTATUS_H +#define FEDIVERSESHARESERVICESTATUS_H + +#include +#include +#include + +#include + +namespace Accounts { +class Account; +class Manager; +} + +namespace SignOn { +class Error; +class SessionData; +} + +class FediverseShareServiceStatus : public QObject +{ + Q_OBJECT + +public: + explicit FediverseShareServiceStatus(QObject *parent = 0); + + enum QueryStatusMode { + PassiveMode = 0, + SignInMode = 1 + }; + + void queryStatus(QueryStatusMode mode = SignInMode); + + struct AccountDetails { + int accountId = 0; + QString providerName; + QString displayName; + QString accessToken; + QString apiHost; + QString iconPath; + }; + + AccountDetails details(int index = 0) const; + AccountDetails detailsByIdentifier(int accountIdentifier) const; + int count() const; + + bool setCredentialsNeedUpdate(int accountId, const QString &serviceName); + +Q_SIGNALS: + void serviceReady(); + void serviceError(const QString &message); + +private Q_SLOTS: + void signOnResponse(const SignOn::SessionData &responseData); + void signOnError(const SignOn::Error &error); + +private: + enum AccountDetailsState { + Waiting, + Populated, + Error + }; + + QString authServiceName() const; + void setAccountDetailsState(int accountId, AccountDetailsState state); + void signIn(int accountId); + + AccountAuthenticator *m_auth; + Accounts::Manager *m_accountManager; + QString m_serviceName; + QVector m_accountDetails; + QHash m_accountIdToDetailsIdx; + QHash m_accountDetailsState; +}; + +#endif // FEDIVERSESHARESERVICESTATUS_H 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 +#include +#include +#include +#include +#include +#include + +#include + +#include + +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 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(total)); + } +} + +void FediverseApi::finished() +{ + QNetworkReply *reply = qobject_cast(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(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 +#include + +#include +#include + +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 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 +#include + +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 + +#include + +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 +#include + +#include +#include +#include + +#include + +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 + +#include + +#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 diff --git a/transferengine-plugins/mastodonshareplugin/MastodonSharePost.qml b/transferengine-plugins/mastodonshareplugin/MastodonSharePost.qml deleted file mode 100644 index d859d96..0000000 --- a/transferengine-plugins/mastodonshareplugin/MastodonSharePost.qml +++ /dev/null @@ -1,138 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -import QtQuick 2.6 -import Sailfish.Silica 1.0 -import Sailfish.Lipstick 1.0 -import Sailfish.TransferEngine 1.0 - -Item { - id: root - - property var shareAction - property string mimeType: { - if (shareAction && shareAction.mimeType) { - return shareAction.mimeType - } - if (shareAction && shareAction.resources - && shareAction.resources.length > 0 - && shareAction.resources[0] - && shareAction.resources[0].type) { - return shareAction.resources[0].type - } - return "" - } - property bool textShare: mimeType === "text/x-url" || mimeType === "text/plain" - - width: parent ? parent.width : 0 - height: previewLoader.item ? previewLoader.item.height : 0 - - Loader { - id: previewLoader - - anchors.fill: parent - sourceComponent: root.textShare ? postPreview : imagePreview - } - - Component { - id: imagePreview - - ShareFilePreview { - shareAction: root.shareAction - metadataStripped: true - descriptionPlaceholderText: qsTr("Write a post") - } - } - - Component { - id: postPreview - - SilicaFlickable { - id: postRoot - - width: parent.width - height: contentHeight - contentHeight: contentColumn.height - - Component.onCompleted: { - sailfishTransfer.loadConfiguration(root.shareAction.toConfiguration()) - statusTextField.forceActiveFocus() - statusTextField.cursorPosition = statusTextField.text.length - } - - SailfishTransfer { - id: sailfishTransfer - } - - Column { - id: contentColumn - - width: parent.width - - TextArea { - id: linkTextField - - width: parent.width - //% "Link" - label: qsTrId("sailfishshare-la-link") - placeholderText: label - visible: sailfishTransfer.content.type === "text/x-url" - text: sailfishTransfer.content.data || sailfishTransfer.content.status || "" - } - - TextArea { - id: statusTextField - - width: parent.width - //% "Status update" - label: qsTrId("sailfishshare-la-status_update") - placeholderText: label - text: { - var title = sailfishTransfer.content.name || sailfishTransfer.content.linkTitle || "" - if (linkTextField.visible) { - return title - } - var body = sailfishTransfer.content.data || sailfishTransfer.content.status || "" - if (title.length > 0 && body.length > 0) { - return title + ": " + body - } - return title + body - } - } - - SystemDialogIconButton { - id: postButton - - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width / 2 - iconSource: "image://theme/icon-m-share" - bottomPadding: Theme.paddingLarge - _showPress: false - - //: Post a social network account status update - //% "Post" - text: qsTrId("sailfishshare-la-post_status") - - onClicked: { - var status = statusTextField.text || "" - var link = linkTextField.visible ? (linkTextField.text || "") : "" - if (link.length > 0 && status.indexOf(link) === -1) { - status = status.length > 0 ? (status + "\n" + link) : link - } - - sailfishTransfer.userData = { - "accountId": sailfishTransfer.transferMethodInfo.accountId, - "status": status - } - sailfishTransfer.mimeType = linkTextField.visible ? "text/x-url" : "text/plain" - sailfishTransfer.start() - root.shareAction.done() - } - } - } - } - } -} diff --git a/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.cpp b/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.cpp deleted file mode 100644 index 919d544..0000000 --- a/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.cpp +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -#include "mastodonplugininfo.h" -#include "mastodonshareservicestatus.h" - -MastodonPluginInfo::MastodonPluginInfo() - : SharingPluginInfo() - , m_mastodonShareServiceStatus(new MastodonShareServiceStatus(this)) -{ - m_capabilities << QLatin1String("image/jpeg") - << QLatin1String("image/png") - << QLatin1String("video/mp4") - << QLatin1String("text/x-url") - << QLatin1String("text/plain"); - - connect(m_mastodonShareServiceStatus, &MastodonShareServiceStatus::serviceReady, - this, &MastodonPluginInfo::serviceReady); - connect(m_mastodonShareServiceStatus, &MastodonShareServiceStatus::serviceError, - this, &MastodonPluginInfo::infoError); -} - -MastodonPluginInfo::~MastodonPluginInfo() -{ -} - -QList MastodonPluginInfo::info() const -{ - return m_info; -} - -void MastodonPluginInfo::query() -{ - m_mastodonShareServiceStatus->queryStatus(MastodonShareServiceStatus::PassiveMode); -} - -void MastodonPluginInfo::serviceReady() -{ - m_info.clear(); - - for (int i = 0; i < m_mastodonShareServiceStatus->count(); ++i) { - SharingMethodInfo info; - - const MastodonShareServiceStatus::AccountDetails details = m_mastodonShareServiceStatus->details(i); - info.setDisplayName(details.providerName); - info.setSubtitle(details.displayName); - info.setAccountId(details.accountId); - - info.setMethodId(QLatin1String("Mastodon")); - info.setMethodIcon(QLatin1String("image://theme/icon-l-mastodon")); - info.setShareUIPath(QLatin1String("/usr/share/nemo-transferengine/plugins/sharing/MastodonSharePost.qml")); - info.setCapabilities(m_capabilities); - m_info << info; - } - - emit infoReady(); -} diff --git a/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.h b/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.h deleted file mode 100644 index 80fe552..0000000 --- a/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.h +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -#ifndef MASTODONPLUGININFO_H -#define MASTODONPLUGININFO_H - -#include -#include - -class MastodonShareServiceStatus; - -class MastodonPluginInfo : public SharingPluginInfo -{ - Q_OBJECT - -public: - MastodonPluginInfo(); - ~MastodonPluginInfo(); - - QList info() const; - void query(); - -private Q_SLOTS: - void serviceReady(); - -private: - MastodonShareServiceStatus *m_mastodonShareServiceStatus; - QList m_info; - QStringList m_capabilities; -}; - -#endif // MASTODONPLUGININFO_H diff --git a/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.cpp b/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.cpp deleted file mode 100644 index 8c139a2..0000000 --- a/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.cpp +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -#include "mastodonshareplugin.h" -#include "mastodonplugininfo.h" - -#include - -MastodonSharePlugin::MastodonSharePlugin() - : QObject(), SharingPluginInterface() -{ -} - -MastodonSharePlugin::~MastodonSharePlugin() -{ -} - -SharingPluginInfo *MastodonSharePlugin::infoObject() -{ - return new MastodonPluginInfo; -} - -QString MastodonSharePlugin::pluginId() const -{ - return QLatin1String("Mastodon"); -} diff --git a/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.h b/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.h deleted file mode 100644 index 04d8412..0000000 --- a/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.h +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -#ifndef MASTODONSHAREPLUGIN_H -#define MASTODONSHAREPLUGIN_H - -#include - -#include - -class Q_DECL_EXPORT MastodonSharePlugin : public QObject, public SharingPluginInterface -{ - Q_OBJECT - Q_PLUGIN_METADATA(IID "org.sailfishos.share.plugin.mastodon") - Q_INTERFACES(SharingPluginInterface) - -public: - MastodonSharePlugin(); - ~MastodonSharePlugin(); - - SharingPluginInfo *infoObject(); - QString pluginId() const; -}; - -#endif // MASTODONSHAREPLUGIN_H diff --git a/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro b/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro deleted file mode 100644 index 59fb7e1..0000000 --- a/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro +++ /dev/null @@ -1,30 +0,0 @@ -# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. -# -# SPDX-License-Identifier: BSD-3-Clause - -TEMPLATE = lib -TARGET = $$qtLibraryTarget(mastodonshareplugin) -CONFIG += plugin -DEPENDPATH += . -INCLUDEPATH += .. -INCLUDEPATH += ../../common - -CONFIG += link_pkgconfig -PKGCONFIG += nemotransferengine-qt5 accounts-qt5 sailfishaccounts libsignon-qt5 - -HEADERS += mastodonshareplugin.h \ - mastodonplugininfo.h \ - ../mastodonshareservicestatus.h - -SOURCES += mastodonshareplugin.cpp \ - mastodonplugininfo.cpp \ - ../mastodonshareservicestatus.cpp - -target.path = $$[QT_INSTALL_LIBS]/nemo-transferengine/plugins/sharing - -OTHER_FILES += *.qml - -shareui.files = MastodonSharePost.qml -shareui.path = /usr/share/nemo-transferengine/plugins/sharing - -INSTALLS += target shareui diff --git a/transferengine-plugins/mastodonshareservicestatus.cpp b/transferengine-plugins/mastodonshareservicestatus.cpp deleted file mode 100644 index 2591520..0000000 --- a/transferengine-plugins/mastodonshareservicestatus.cpp +++ /dev/null @@ -1,297 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -#include "mastodonshareservicestatus.h" -#include "mastodonauthutils.h" - -#include -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include - -MastodonShareServiceStatus::MastodonShareServiceStatus(QObject *parent) - : QObject(parent) - , m_auth(new AccountAuthenticator(this)) - , m_accountManager(new Accounts::Manager(this)) - , m_serviceName(QStringLiteral("mastodon-sharing")) -{ -} - -QString MastodonShareServiceStatus::authServiceName() const -{ - return QStringLiteral("mastodon-microblog"); -} - -void MastodonShareServiceStatus::signIn(int accountId) -{ - Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this); - if (!account) { - qWarning() << Q_FUNC_INFO << "Failed to retrieve account for id:" << accountId; - setAccountDetailsState(accountId, Error); - return; - } - - const Accounts::Service service(m_accountManager->service(authServiceName())); - if (!service.isValid()) { - qWarning() << Q_FUNC_INFO << "Invalid auth service" << authServiceName(); - account->deleteLater(); - setAccountDetailsState(accountId, Error); - return; - } - - account->selectService(service); - - SignOn::Identity *identity = account->credentialsId() > 0 - ? SignOn::Identity::existingIdentity(account->credentialsId()) - : 0; - if (!identity) { - qWarning() << Q_FUNC_INFO << "account" << accountId << "has no valid credentials"; - account->deleteLater(); - setAccountDetailsState(accountId, Error); - return; - } - - Accounts::AccountService accountService(account, service); - const QString method = accountService.authData().method(); - const QString mechanism = accountService.authData().mechanism(); - - SignOn::AuthSession *session = identity->createSession(method); - if (!session) { - qWarning() << Q_FUNC_INFO << "could not create signon session for account" << accountId; - identity->deleteLater(); - account->deleteLater(); - setAccountDetailsState(accountId, Error); - return; - } - - QVariantMap signonSessionData = accountService.authData().parameters(); - - MastodonAuthUtils::addSignOnSessionParameters(account, &signonSessionData); - - connect(session, SIGNAL(response(SignOn::SessionData)), - this, SLOT(signOnResponse(SignOn::SessionData)), - Qt::UniqueConnection); - connect(session, SIGNAL(error(SignOn::Error)), - this, SLOT(signOnError(SignOn::Error)), - Qt::UniqueConnection); - - session->setProperty("account", QVariant::fromValue(account)); - session->setProperty("identity", QVariant::fromValue(identity)); - session->process(SignOn::SessionData(signonSessionData), mechanism); -} - -void MastodonShareServiceStatus::signOnResponse(const SignOn::SessionData &responseData) -{ - const QVariantMap data = MastodonAuthUtils::responseDataToMap(responseData); - - SignOn::AuthSession *session = qobject_cast(sender()); - Accounts::Account *account = session->property("account").value(); - SignOn::Identity *identity = session->property("identity").value(); - const int accountId = account ? account->id() : 0; - - QString accessToken = MastodonAuthUtils::accessToken(data); - - if (accountId > 0 && m_accountIdToDetailsIdx.contains(accountId)) { - AccountDetails &accountDetails(m_accountDetails[m_accountIdToDetailsIdx[accountId]]); - accountDetails.accessToken = accessToken; - setAccountDetailsState(accountId, accessToken.isEmpty() ? Error : Populated); - } - - session->disconnect(this); - if (identity) { - identity->destroySession(session); - identity->deleteLater(); - } - if (account) { - account->deleteLater(); - } -} - -void MastodonShareServiceStatus::signOnError(const SignOn::Error &error) -{ - SignOn::AuthSession *session = qobject_cast(sender()); - Accounts::Account *account = session->property("account").value(); - SignOn::Identity *identity = session->property("identity").value(); - const int accountId = account ? account->id() : 0; - - qWarning() << Q_FUNC_INFO << "failed to retrieve credentials for account" << accountId - << error.type() << error.message(); - - if (accountId > 0 && error.type() == SignOn::Error::UserInteraction) { - setCredentialsNeedUpdate(accountId, authServiceName()); - } - - session->disconnect(this); - if (identity) { - identity->destroySession(session); - identity->deleteLater(); - } - if (account) { - account->deleteLater(); - } - - if (accountId > 0) { - setAccountDetailsState(accountId, Error); - } -} - -void MastodonShareServiceStatus::setAccountDetailsState(int accountId, AccountDetailsState state) -{ - if (!m_accountIdToDetailsIdx.contains(accountId)) { - return; - } - - m_accountDetailsState[accountId] = state; - - bool anyWaiting = false; - bool anyPopulated = false; - Q_FOREACH (int id, m_accountDetailsState.keys()) { - AccountDetailsState accountState = m_accountDetailsState.value(id, Waiting); - if (accountState == Waiting) { - anyWaiting = true; - } else if (accountState == Populated) { - anyPopulated = true; - } - } - - if (!anyWaiting) { - if (anyPopulated) { - emit serviceReady(); - } else { - emit serviceError(QStringLiteral("Unable to retrieve Mastodon account credentials")); - } - } -} - -int MastodonShareServiceStatus::count() const -{ - return m_accountDetails.count(); -} - -bool MastodonShareServiceStatus::setCredentialsNeedUpdate(int accountId, const QString &serviceName) -{ - return m_auth->setCredentialsNeedUpdate(accountId, serviceName); -} - -void MastodonShareServiceStatus::queryStatus(QueryStatusMode mode) -{ - m_accountDetails.clear(); - m_accountIdToDetailsIdx.clear(); - m_accountDetailsState.clear(); - - bool signInActive = false; - Q_FOREACH (Accounts::AccountId id, m_accountManager->accountList()) { - Accounts::Account *acc = Accounts::Account::fromId(m_accountManager, id, this); - - if (!acc) { - qWarning() << Q_FUNC_INFO << "Failed to get account for id:" << id; - continue; - } - - acc->selectService(Accounts::Service()); - - const Accounts::Service service(m_accountManager->service(m_serviceName)); - const Accounts::ServiceList services = acc->services(); - bool serviceFound = false; - Q_FOREACH (const Accounts::Service &s, services) { - if (s.name() == m_serviceName) { - serviceFound = true; - break; - } - } - - if (!service.isValid() || !serviceFound) { - acc->deleteLater(); - continue; - } - - const bool accountEnabled = acc->enabled(); - acc->selectService(service); - const bool shareServiceEnabled = acc->enabled(); - if (!accountEnabled || !shareServiceEnabled) { - acc->selectService(Accounts::Service()); - acc->deleteLater(); - continue; - } - - if (acc->value(QStringLiteral("CredentialsNeedUpdate")).toBool()) { - qWarning() << Q_FUNC_INFO << "Credentials need update for account id:" << id; - acc->selectService(Accounts::Service()); - acc->deleteLater(); - continue; - } - - if (!m_accountIdToDetailsIdx.contains(id)) { - AccountDetails details; - details.accountId = id; - acc->selectService(Accounts::Service()); - details.apiHost = MastodonAuthUtils::normalizeApiHost(acc->value(QStringLiteral("api/Host")).toString()); - acc->selectService(service); - - QUrl apiUrl(details.apiHost); - details.providerName = apiUrl.host(); - if (details.providerName.isEmpty()) { - details.providerName = details.apiHost; - if (details.providerName.startsWith(QLatin1String("https://"))) { - details.providerName.remove(0, 8); - } else if (details.providerName.startsWith(QLatin1String("http://"))) { - details.providerName.remove(0, 7); - } - const int separator = details.providerName.indexOf(QLatin1Char('/')); - if (separator > 0) { - details.providerName.truncate(separator); - } - } - - details.displayName = acc->displayName(); - - m_accountIdToDetailsIdx.insert(id, m_accountDetails.size()); - m_accountDetails.append(details); - } - - if (mode == SignInMode) { - signInActive = true; - m_accountDetailsState.insert(id, Waiting); - signIn(id); - } - - acc->selectService(Accounts::Service()); - acc->deleteLater(); - } - - if (!signInActive) { - emit serviceReady(); - } -} - -MastodonShareServiceStatus::AccountDetails MastodonShareServiceStatus::details(int index) const -{ - if (index < 0 || index >= m_accountDetails.size()) { - qWarning() << Q_FUNC_INFO << "Index out of range"; - return AccountDetails(); - } - - return m_accountDetails.at(index); -} - -MastodonShareServiceStatus::AccountDetails MastodonShareServiceStatus::detailsByIdentifier(int accountIdentifier) const -{ - if (!m_accountIdToDetailsIdx.contains(accountIdentifier)) { - qWarning() << Q_FUNC_INFO << "No details known for account with identifier" << accountIdentifier; - return AccountDetails(); - } - - return m_accountDetails[m_accountIdToDetailsIdx[accountIdentifier]]; -} diff --git a/transferengine-plugins/mastodonshareservicestatus.h b/transferengine-plugins/mastodonshareservicestatus.h deleted file mode 100644 index be76c37..0000000 --- a/transferengine-plugins/mastodonshareservicestatus.h +++ /dev/null @@ -1,81 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -#ifndef MASTODONSHARESERVICESTATUS_H -#define MASTODONSHARESERVICESTATUS_H - -#include -#include -#include - -#include - -namespace Accounts { -class Account; -class Manager; -} - -namespace SignOn { -class Error; -class SessionData; -} - -class MastodonShareServiceStatus : public QObject -{ - Q_OBJECT - -public: - explicit MastodonShareServiceStatus(QObject *parent = 0); - - enum QueryStatusMode { - PassiveMode = 0, - SignInMode = 1 - }; - - void queryStatus(QueryStatusMode mode = SignInMode); - - struct AccountDetails { - int accountId = 0; - QString providerName; - QString displayName; - QString accessToken; - QString apiHost; - }; - - AccountDetails details(int index = 0) const; - AccountDetails detailsByIdentifier(int accountIdentifier) const; - int count() const; - - bool setCredentialsNeedUpdate(int accountId, const QString &serviceName); - -Q_SIGNALS: - void serviceReady(); - void serviceError(const QString &message); - -private Q_SLOTS: - void signOnResponse(const SignOn::SessionData &responseData); - void signOnError(const SignOn::Error &error); - -private: - enum AccountDetailsState { - Waiting, - Populated, - Error - }; - - QString authServiceName() const; - void setAccountDetailsState(int accountId, AccountDetailsState state); - void signIn(int accountId); - - AccountAuthenticator *m_auth; - Accounts::Manager *m_accountManager; - QString m_serviceName; - QVector m_accountDetails; - QHash m_accountIdToDetailsIdx; - QHash m_accountDetailsState; -}; - -#endif // MASTODONSHARESERVICESTATUS_H diff --git a/transferengine-plugins/mastodontransferplugin/mastodonapi.cpp b/transferengine-plugins/mastodontransferplugin/mastodonapi.cpp deleted file mode 100644 index fa973d0..0000000 --- a/transferengine-plugins/mastodontransferplugin/mastodonapi.cpp +++ /dev/null @@ -1,255 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -#include "mastodonapi.h" -#include "mastodonauthutils.h" - -#include -#include -#include -#include -#include -#include -#include - -#include - -#include - -MastodonApi::MastodonApi(QNetworkAccessManager *qnam, QObject *parent) - : QObject(parent) - , m_cancelRequested(false) - , m_qnam(qnam) -{ -} - -MastodonApi::~MastodonApi() -{ -} - -bool MastodonApi::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 = MastodonAuthUtils::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, &MastodonApi::uploadProgress); - connect(reply, &QNetworkReply::finished, - this, &MastodonApi::finished); - - return true; -} - -bool MastodonApi::postStatus(const QString &statusText, - const QString &apiHost, - const QString &accessToken) -{ - m_cancelRequested = false; - m_apiHost = MastodonAuthUtils::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 MastodonApi::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, &MastodonApi::finished); - - return true; -} - -void MastodonApi::cancelUpload() -{ - if (m_replies.isEmpty()) { - qWarning() << Q_FUNC_INFO << "can't cancel upload"; - return; - } - - m_cancelRequested = true; - const QList replies = m_replies.keys(); - Q_FOREACH (QNetworkReply *reply, replies) { - reply->abort(); - } -} - -void MastodonApi::replyError(QNetworkReply::NetworkError error) -{ - Q_UNUSED(error) -} - -void MastodonApi::uploadProgress(qint64 sent, qint64 total) -{ - if (total > 0) { - emit transferProgressUpdated(sent / static_cast(total)); - } -} - -void MastodonApi::finished() -{ - QNetworkReply *reply = qobject_cast(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(idValue.toDouble())); - } - } - - if (!postStatusInternal(mediaId)) { - qWarning() << Q_FUNC_INFO << "unable to create mastodon status"; - emit transferError(); - } - return; - } - - if (apiCall == POST_STATUS) { - finishTransfer(error, httpCode, data); - return; - } - - emit transferError(); -} - -void MastodonApi::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/mastodontransferplugin/mastodonapi.h b/transferengine-plugins/mastodontransferplugin/mastodonapi.h deleted file mode 100644 index df4c87a..0000000 --- a/transferengine-plugins/mastodontransferplugin/mastodonapi.h +++ /dev/null @@ -1,65 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -#ifndef MASTODONAPI_H -#define MASTODONAPI_H - -#include -#include - -#include -#include - -class MastodonApi : public QObject -{ - Q_OBJECT - -public: - enum API_CALL { - NONE, - UPLOAD_MEDIA, - POST_STATUS - }; - - explicit MastodonApi(QNetworkAccessManager *qnam, QObject *parent = 0); - ~MastodonApi(); - - 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 m_replies; - bool m_cancelRequested; - QNetworkAccessManager *m_qnam; - QString m_accessToken; - QString m_apiHost; - QString m_statusText; -}; - -#endif // MASTODONAPI_H diff --git a/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.cpp b/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.cpp deleted file mode 100644 index a843df2..0000000 --- a/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.cpp +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -#include "mastodontransferplugin.h" -#include "mastodonuploader.h" - -#include -#include - -MastodonTransferPlugin::MastodonTransferPlugin() - : QObject(), TransferPluginInterface() - , m_qnam(new QNetworkAccessManager(this)) -{ -} - -MastodonTransferPlugin::~MastodonTransferPlugin() -{ -} - -MediaTransferInterface *MastodonTransferPlugin::transferObject() -{ - return new MastodonUploader(m_qnam, this); -} - -QString MastodonTransferPlugin::pluginId() const -{ - return QLatin1String("Mastodon"); -} diff --git a/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.h b/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.h deleted file mode 100644 index 4d3baaf..0000000 --- a/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -#ifndef MASTODONTRANSFERPLUGIN_H -#define MASTODONTRANSFERPLUGIN_H - -#include - -#include - -class QNetworkAccessManager; - -class Q_DECL_EXPORT MastodonTransferPlugin : public QObject, public TransferPluginInterface -{ - Q_OBJECT - Q_PLUGIN_METADATA(IID "org.sailfishos.transfer.plugin.mastodon") - Q_INTERFACES(TransferPluginInterface) - -public: - MastodonTransferPlugin(); - ~MastodonTransferPlugin(); - - MediaTransferInterface *transferObject(); - QString pluginId() const; - -private: - QNetworkAccessManager *m_qnam; -}; - -#endif // MASTODONTRANSFERPLUGIN_H diff --git a/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro b/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro deleted file mode 100644 index 422a889..0000000 --- a/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro +++ /dev/null @@ -1,29 +0,0 @@ -# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. -# -# SPDX-License-Identifier: BSD-3-Clause - -TEMPLATE = lib -TARGET = $$qtLibraryTarget(mastodontransferplugin) -CONFIG += plugin -DEPENDPATH += . -INCLUDEPATH += .. -INCLUDEPATH += ../../common - -QT += network - -CONFIG += link_pkgconfig -PKGCONFIG += nemotransferengine-qt5 accounts-qt5 sailfishaccounts libsignon-qt5 - -HEADERS += mastodontransferplugin.h \ - mastodonuploader.h \ - ../mastodonshareservicestatus.h \ - mastodonapi.h - -SOURCES += mastodontransferplugin.cpp \ - mastodonuploader.cpp \ - ../mastodonshareservicestatus.cpp \ - mastodonapi.cpp - -target.path = $$[QT_INSTALL_LIBS]/nemo-transferengine/plugins/transfer - -INSTALLS += target diff --git a/transferengine-plugins/mastodontransferplugin/mastodonuploader.cpp b/transferengine-plugins/mastodontransferplugin/mastodonuploader.cpp deleted file mode 100644 index 7b87823..0000000 --- a/transferengine-plugins/mastodontransferplugin/mastodonuploader.cpp +++ /dev/null @@ -1,252 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -#include "mastodonuploader.h" -#include "mastodonapi.h" - -#include -#include - -#include -#include -#include - -#include - -MastodonUploader::MastodonUploader(QNetworkAccessManager *qnam, QObject *parent) - : MediaTransferInterface(parent) - , m_api(0) - , m_mastodonShareServiceStatus(0) - , m_qnam(qnam) - , m_useTmpFile(false) -{ -} - -MastodonUploader::~MastodonUploader() -{ -} - -QString MastodonUploader::displayName() const -{ - return tr("Mastodon"); -} - -QUrl MastodonUploader::serviceIcon() const -{ - return QUrl(QStringLiteral("image://theme/icon-l-mastodon")); -} - -bool MastodonUploader::cancelEnabled() const -{ - return true; -} - -bool MastodonUploader::restartEnabled() const -{ - return true; -} - -void MastodonUploader::start() -{ - if (!mediaItem()) { - qWarning() << Q_FUNC_INFO << "NULL MediaItem. Can't continue"; - setStatus(MediaTransferInterface::TransferInterrupted); - return; - } - - if (!m_mastodonShareServiceStatus) { - m_mastodonShareServiceStatus = new MastodonShareServiceStatus(this); - connect(m_mastodonShareServiceStatus, &MastodonShareServiceStatus::serviceReady, - this, &MastodonUploader::startUploading); - connect(m_mastodonShareServiceStatus, &MastodonShareServiceStatus::serviceError, - this, [this] (const QString &) { - transferError(); - }); - } - - m_mastodonShareServiceStatus->queryStatus(); -} - -void MastodonUploader::cancel() -{ - if (m_api) { - m_api->cancelUpload(); - } else { - qWarning() << Q_FUNC_INFO << "Can't cancel. NULL MastodonApi object!"; - } -} - -void MastodonUploader::startUploading() -{ - if (!m_mastodonShareServiceStatus) { - qWarning() << Q_FUNC_INFO << "NULL MastodonShareServiceStatus object!"; - return; - } - - const quint32 accountId = mediaItem()->value(MediaItem::AccountId).toInt(); - m_accountDetails = m_mastodonShareServiceStatus->detailsByIdentifier(accountId); - if (m_accountDetails.accountId <= 0 || m_accountDetails.accessToken.isEmpty()) { - qWarning() << Q_FUNC_INFO << "Mastodon 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 MastodonUploader::transferFinished() -{ - setStatus(MediaTransferInterface::TransferFinished); -} - -void MastodonUploader::transferProgress(qreal progress) -{ - setProgress(progress); -} - -void MastodonUploader::transferError() -{ - setStatus(MediaTransferInterface::TransferInterrupted); - qWarning() << Q_FUNC_INFO << "Transfer interrupted"; -} - -void MastodonUploader::transferCanceled() -{ - setStatus(MediaTransferInterface::TransferCanceled); -} - -void MastodonUploader::credentialsExpired() -{ - const quint32 accountId = mediaItem()->value(MediaItem::AccountId).toInt(); - m_mastodonShareServiceStatus->setCredentialsNeedUpdate(accountId, QStringLiteral("mastodon-sharing")); -} - -void MastodonUploader::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 MastodonUploader::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 MastodonUploader::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 MastodonUploader::ensureApi() -{ - if (!m_api) { - m_api = new MastodonApi(m_qnam, this); - connect(m_api, &MastodonApi::transferProgressUpdated, - this, &MastodonUploader::transferProgress); - connect(m_api, &MastodonApi::transferFinished, - this, &MastodonUploader::transferFinished); - connect(m_api, &MastodonApi::transferError, - this, &MastodonUploader::transferError); - connect(m_api, &MastodonApi::transferCanceled, - this, &MastodonUploader::transferCanceled); - connect(m_api, &MastodonApi::credentialsExpired, - this, &MastodonUploader::credentialsExpired); - } -} diff --git a/transferengine-plugins/mastodontransferplugin/mastodonuploader.h b/transferengine-plugins/mastodontransferplugin/mastodonuploader.h deleted file mode 100644 index 72d9689..0000000 --- a/transferengine-plugins/mastodontransferplugin/mastodonuploader.h +++ /dev/null @@ -1,59 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -#ifndef MASTODONUPLOADER_H -#define MASTODONUPLOADER_H - -#include - -#include - -#include "mastodonshareservicestatus.h" - -class MastodonApi; - -class MastodonUploader : public MediaTransferInterface -{ - Q_OBJECT - -public: - MastodonUploader(QNetworkAccessManager *qnam, QObject *parent = 0); - ~MastodonUploader(); - - 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(); - - MastodonApi *m_api; - MastodonShareServiceStatus *m_mastodonShareServiceStatus; - QNetworkAccessManager *m_qnam; - MastodonShareServiceStatus::AccountDetails m_accountDetails; - bool m_useTmpFile; - QString m_filePath; -}; - -#endif // MASTODONUPLOADER_H diff --git a/transferengine-plugins/transferengine-plugins.pro b/transferengine-plugins/transferengine-plugins.pro index e2503a3..ce1a102 100644 --- a/transferengine-plugins/transferengine-plugins.pro +++ b/transferengine-plugins/transferengine-plugins.pro @@ -3,4 +3,4 @@ # SPDX-License-Identifier: BSD-3-Clause TEMPLATE = subdirs -SUBDIRS = mastodonshareplugin mastodontransferplugin +SUBDIRS = fediverseshareplugin fediversetransferplugin -- cgit v1.2.3