summaryrefslogtreecommitdiff
path: root/transferengine-plugins
diff options
context:
space:
mode:
Diffstat (limited to 'transferengine-plugins')
-rw-r--r--transferengine-plugins/mastodonshareplugin/MastodonShareImage.qml10
-rw-r--r--transferengine-plugins/mastodonshareplugin/mastodonplugininfo.cpp52
-rw-r--r--transferengine-plugins/mastodonshareplugin/mastodonplugininfo.h29
-rw-r--r--transferengine-plugins/mastodonshareplugin/mastodonshareplugin.cpp23
-rw-r--r--transferengine-plugins/mastodonshareplugin/mastodonshareplugin.h22
-rw-r--r--transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro25
-rw-r--r--transferengine-plugins/mastodonshareservicestatus.cpp367
-rw-r--r--transferengine-plugins/mastodonshareservicestatus.h75
-rw-r--r--transferengine-plugins/mastodontransferplugin/mastodonapi.cpp244
-rw-r--r--transferengine-plugins/mastodontransferplugin/mastodonapi.h56
-rw-r--r--transferengine-plugins/mastodontransferplugin/mastodontransferplugin.cpp25
-rw-r--r--transferengine-plugins/mastodontransferplugin/mastodontransferplugin.h27
-rw-r--r--transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro24
-rw-r--r--transferengine-plugins/mastodontransferplugin/mastodonuploader.cpp200
-rw-r--r--transferengine-plugins/mastodontransferplugin/mastodonuploader.h51
-rw-r--r--transferengine-plugins/transferengine-plugins.pro2
16 files changed, 1232 insertions, 0 deletions
diff --git a/transferengine-plugins/mastodonshareplugin/MastodonShareImage.qml b/transferengine-plugins/mastodonshareplugin/MastodonShareImage.qml
new file mode 100644
index 0000000..56b4b4b
--- /dev/null
+++ b/transferengine-plugins/mastodonshareplugin/MastodonShareImage.qml
@@ -0,0 +1,10 @@
+import QtQuick 2.6
+import Sailfish.Silica 1.0
+import Sailfish.TransferEngine 1.0
+
+ShareFilePreview {
+ id: root
+
+ metadataStripped: true
+ descriptionPlaceholderText: qsTr("Write a post")
+}
diff --git a/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.cpp b/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.cpp
new file mode 100644
index 0000000..405b86e
--- /dev/null
+++ b/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.cpp
@@ -0,0 +1,52 @@
+#include "mastodonplugininfo.h"
+#include "mastodonshareservicestatus.h"
+
+MastodonPluginInfo::MastodonPluginInfo()
+ : SharingPluginInfo()
+ , m_mastodonShareServiceStatus(new MastodonShareServiceStatus(this))
+{
+ m_capabilities << QLatin1String("image/jpeg")
+ << QLatin1String("image/png");
+
+ connect(m_mastodonShareServiceStatus, &MastodonShareServiceStatus::serviceReady,
+ this, &MastodonPluginInfo::serviceReady);
+ connect(m_mastodonShareServiceStatus, &MastodonShareServiceStatus::serviceError,
+ this, &MastodonPluginInfo::infoError);
+}
+
+MastodonPluginInfo::~MastodonPluginInfo()
+{
+}
+
+QList<SharingMethodInfo> 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/graphic-m-service-mastodon"));
+ info.setShareUIPath(QLatin1String("/usr/share/nemo-transferengine/plugins/sharing/MastodonShareImage.qml"));
+ info.setCapabilities(m_capabilities);
+
+ m_info << info;
+ }
+
+ emit infoReady();
+}
diff --git a/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.h b/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.h
new file mode 100644
index 0000000..28eb479
--- /dev/null
+++ b/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.h
@@ -0,0 +1,29 @@
+#ifndef MASTODONPLUGININFO_H
+#define MASTODONPLUGININFO_H
+
+#include <sharingplugininfo.h>
+#include <QStringList>
+
+class MastodonShareServiceStatus;
+
+class MastodonPluginInfo : public SharingPluginInfo
+{
+ Q_OBJECT
+
+public:
+ MastodonPluginInfo();
+ ~MastodonPluginInfo();
+
+ QList<SharingMethodInfo> info() const;
+ void query();
+
+private Q_SLOTS:
+ void serviceReady();
+
+private:
+ MastodonShareServiceStatus *m_mastodonShareServiceStatus;
+ QList<SharingMethodInfo> m_info;
+ QStringList m_capabilities;
+};
+
+#endif // MASTODONPLUGININFO_H
diff --git a/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.cpp b/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.cpp
new file mode 100644
index 0000000..ec7a732
--- /dev/null
+++ b/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.cpp
@@ -0,0 +1,23 @@
+#include "mastodonshareplugin.h"
+#include "mastodonplugininfo.h"
+
+#include <QtPlugin>
+
+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
new file mode 100644
index 0000000..634d051
--- /dev/null
+++ b/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.h
@@ -0,0 +1,22 @@
+#ifndef MASTODONSHAREPLUGIN_H
+#define MASTODONSHAREPLUGIN_H
+
+#include <QtCore/QObject>
+
+#include <sharingplugininterface.h>
+
+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
new file mode 100644
index 0000000..6de949e
--- /dev/null
+++ b/transferengine-plugins/mastodonshareplugin/mastodonshareplugin.pro
@@ -0,0 +1,25 @@
+TEMPLATE = lib
+TARGET = $$qtLibraryTarget(mastodonshareplugin)
+CONFIG += plugin
+DEPENDPATH += .
+INCLUDEPATH += ..
+
+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 = MastodonShareImage.qml
+shareui.path = /usr/share/nemo-transferengine/plugins/sharing
+
+INSTALLS += target shareui
diff --git a/transferengine-plugins/mastodonshareservicestatus.cpp b/transferengine-plugins/mastodonshareservicestatus.cpp
new file mode 100644
index 0000000..8c70e12
--- /dev/null
+++ b/transferengine-plugins/mastodonshareservicestatus.cpp
@@ -0,0 +1,367 @@
+#include "mastodonshareservicestatus.h"
+
+#include <Accounts/Account>
+#include <Accounts/AccountService>
+#include <Accounts/Manager>
+#include <Accounts/Service>
+
+#include <SignOn/AuthSession>
+#include <SignOn/Error>
+#include <SignOn/Identity>
+#include <SignOn/SessionData>
+
+#include <QtCore/QUrl>
+#include <QtCore/QVariantMap>
+#include <QtDebug>
+
+MastodonShareServiceStatus::MastodonShareServiceStatus(QObject *parent)
+ : QObject(parent)
+ , m_auth(new AccountAuthenticator(this))
+ , m_accountManager(new Accounts::Manager(this))
+ , m_serviceName(QStringLiteral("mastodon-sharing"))
+{
+}
+
+QString MastodonShareServiceStatus::normalizeApiHost(const QString &rawHost)
+{
+ QString host = rawHost.trimmed();
+ if (host.isEmpty()) {
+ host = QStringLiteral("https://mastodon.social");
+ }
+
+ if (!host.startsWith(QLatin1String("https://"))
+ && !host.startsWith(QLatin1String("http://"))) {
+ host.prepend(QStringLiteral("https://"));
+ }
+
+ QUrl url(host);
+ if (!url.isValid() || url.host().isEmpty()) {
+ return QStringLiteral("https://mastodon.social");
+ }
+
+ QString normalized = QString::fromLatin1(url.toEncoded(QUrl::RemovePath
+ | QUrl::RemoveQuery
+ | QUrl::RemoveFragment));
+ if (normalized.endsWith(QLatin1Char('/'))) {
+ normalized.chop(1);
+ }
+
+ return normalized;
+}
+
+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(m_serviceName));
+ if (!service.isValid()) {
+ qWarning() << Q_FUNC_INFO << "Invalid auth service" << m_serviceName;
+ 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();
+
+ QString configuredHost = account->value(QStringLiteral("auth/oauth2/web_server/Host")).toString().trimmed();
+ if (configuredHost.isEmpty()) {
+ configuredHost = normalizeApiHost(account->value(QStringLiteral("api/Host")).toString());
+ }
+
+ if (configuredHost.startsWith(QLatin1String("https://"))) {
+ configuredHost.remove(0, 8);
+ } else if (configuredHost.startsWith(QLatin1String("http://"))) {
+ configuredHost.remove(0, 7);
+ }
+
+ const int separator = configuredHost.indexOf(QLatin1Char('/'));
+ if (separator > -1) {
+ configuredHost.truncate(separator);
+ }
+ while (configuredHost.endsWith(QLatin1Char('/'))) {
+ configuredHost.chop(1);
+ }
+
+ if (configuredHost.isEmpty()) {
+ configuredHost = QStringLiteral("mastodon.social");
+ }
+ signonSessionData.insert(QStringLiteral("Host"), configuredHost);
+
+ const QString authPath = account->value(QStringLiteral("auth/oauth2/web_server/AuthPath")).toString().trimmed();
+ if (!authPath.isEmpty()) {
+ signonSessionData.insert(QStringLiteral("AuthPath"), authPath);
+ }
+
+ const QString tokenPath = account->value(QStringLiteral("auth/oauth2/web_server/TokenPath")).toString().trimmed();
+ if (!tokenPath.isEmpty()) {
+ signonSessionData.insert(QStringLiteral("TokenPath"), tokenPath);
+ }
+
+ const QString responseType = account->value(QStringLiteral("auth/oauth2/web_server/ResponseType")).toString().trimmed();
+ if (!responseType.isEmpty()) {
+ signonSessionData.insert(QStringLiteral("ResponseType"), responseType);
+ }
+
+ const QString redirectUri = account->value(QStringLiteral("auth/oauth2/web_server/RedirectUri")).toString().trimmed();
+ if (!redirectUri.isEmpty()) {
+ signonSessionData.insert(QStringLiteral("RedirectUri"), redirectUri);
+ }
+
+ const QVariant scopeValue = account->value(QStringLiteral("auth/oauth2/web_server/Scope"));
+ if (scopeValue.isValid()) {
+ signonSessionData.insert(QStringLiteral("Scope"), scopeValue);
+ }
+
+ const QString clientId = account->value(QStringLiteral("auth/oauth2/web_server/ClientId")).toString().trimmed();
+ if (!clientId.isEmpty()) {
+ signonSessionData.insert(QStringLiteral("ClientId"), clientId);
+ }
+
+ const QString clientSecret = account->value(QStringLiteral("auth/oauth2/web_server/ClientSecret")).toString().trimmed();
+ if (!clientSecret.isEmpty()) {
+ signonSessionData.insert(QStringLiteral("ClientSecret"), clientSecret);
+ }
+
+ signonSessionData.insert(QStringLiteral("UiPolicy"), SignOn::NoUserInteractionPolicy);
+
+ 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<Accounts::Account *>(account));
+ session->setProperty("identity", QVariant::fromValue<SignOn::Identity *>(identity));
+ session->process(SignOn::SessionData(signonSessionData), mechanism);
+}
+
+void MastodonShareServiceStatus::signOnResponse(const SignOn::SessionData &responseData)
+{
+ QVariantMap data;
+ Q_FOREACH (const QString &key, responseData.propertyNames()) {
+ data.insert(key, responseData.getProperty(key));
+ }
+
+ SignOn::AuthSession *session = qobject_cast<SignOn::AuthSession *>(sender());
+ Accounts::Account *account = session->property("account").value<Accounts::Account *>();
+ SignOn::Identity *identity = session->property("identity").value<SignOn::Identity *>();
+ const int accountId = account ? account->id() : 0;
+
+ QString accessToken = data.value(QLatin1String("AccessToken")).toString().trimmed();
+ if (accessToken.isEmpty()) {
+ accessToken = data.value(QLatin1String("access_token")).toString().trimmed();
+ }
+
+ 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<SignOn::AuthSession *>(sender());
+ Accounts::Account *account = session->property("account").value<Accounts::Account *>();
+ SignOn::Identity *identity = session->property("identity").value<SignOn::Identity *>();
+ 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, m_serviceName);
+ }
+
+ 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 = m_accountManager->account(id);
+
+ 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 (acc->enabled() && service.isValid() && serviceFound) {
+ if (acc->value(QStringLiteral("CredentialsNeedUpdate")).toBool()) {
+ qWarning() << Q_FUNC_INFO << "Credentials need update for account id:" << id;
+ continue;
+ }
+
+ acc->selectService(service);
+ if (acc->value(QStringLiteral("CredentialsNeedUpdate")).toBool()) {
+ qWarning() << Q_FUNC_INFO << "Credentials need update for account id:" << id;
+ acc->selectService(Accounts::Service());
+ continue;
+ }
+
+ if (!m_accountIdToDetailsIdx.contains(id)) {
+ AccountDetails details;
+ details.accountId = id;
+ details.apiHost = normalizeApiHost(acc->value(QStringLiteral("api/Host")).toString());
+
+ 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());
+ }
+ }
+
+ 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
new file mode 100644
index 0000000..571efd5
--- /dev/null
+++ b/transferengine-plugins/mastodonshareservicestatus.h
@@ -0,0 +1,75 @@
+#ifndef MASTODONSHARESERVICESTATUS_H
+#define MASTODONSHARESERVICESTATUS_H
+
+#include <QtCore/QHash>
+#include <QtCore/QObject>
+#include <QtCore/QVector>
+
+#include <accountauthenticator.h>
+
+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
+ };
+
+ static QString normalizeApiHost(const QString &rawHost);
+ void setAccountDetailsState(int accountId, AccountDetailsState state);
+ void signIn(int accountId);
+
+ AccountAuthenticator *m_auth;
+ Accounts::Manager *m_accountManager;
+ QString m_serviceName;
+ QVector<AccountDetails> m_accountDetails;
+ QHash<int, int> m_accountIdToDetailsIdx;
+ QHash<int, AccountDetailsState> m_accountDetailsState;
+};
+
+#endif // MASTODONSHARESERVICESTATUS_H
diff --git a/transferengine-plugins/mastodontransferplugin/mastodonapi.cpp b/transferengine-plugins/mastodontransferplugin/mastodonapi.cpp
new file mode 100644
index 0000000..a4b40a9
--- /dev/null
+++ b/transferengine-plugins/mastodontransferplugin/mastodonapi.cpp
@@ -0,0 +1,244 @@
+#include "mastodonapi.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>
+
+MastodonApi::MastodonApi(QNetworkAccessManager *qnam, QObject *parent)
+ : QObject(parent)
+ , m_qnam(qnam)
+{
+}
+
+MastodonApi::~MastodonApi()
+{
+}
+
+QString MastodonApi::normalizeApiHost(const QString &rawHost)
+{
+ QString host = rawHost.trimmed();
+ if (host.isEmpty()) {
+ host = QStringLiteral("https://mastodon.social");
+ }
+
+ if (!host.startsWith(QLatin1String("https://"))
+ && !host.startsWith(QLatin1String("http://"))) {
+ host.prepend(QStringLiteral("https://"));
+ }
+
+ QUrl url(host);
+ if (!url.isValid() || url.host().isEmpty()) {
+ return QStringLiteral("https://mastodon.social");
+ }
+
+ QString normalized = QString::fromLatin1(url.toEncoded(QUrl::RemovePath
+ | QUrl::RemoveQuery
+ | QUrl::RemoveFragment));
+ if (normalized.endsWith(QLatin1Char('/'))) {
+ normalized.chop(1);
+ }
+
+ return normalized;
+}
+
+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_apiHost = 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 &mediaId)
+{
+ if (mediaId.isEmpty()) {
+ qWarning() << Q_FUNC_INFO << "media id is empty";
+ return false;
+ }
+
+ QUrlQuery query;
+ if (!m_statusText.isEmpty()) {
+ query.addQueryItem(QStringLiteral("status"), m_statusText);
+ }
+ 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;
+ }
+
+ const QList<QNetworkReply*> replies = m_replies.keys();
+ Q_FOREACH (QNetworkReply *reply, replies) {
+ reply->abort();
+ }
+ m_replies.clear();
+}
+
+void MastodonApi::replyError(QNetworkReply::NetworkError error)
+{
+ Q_UNUSED(error)
+}
+
+void MastodonApi::uploadProgress(qint64 sent, qint64 total)
+{
+ if (total > 0) {
+ emit transferProgressUpdated(sent / static_cast<qreal>(total));
+ }
+}
+
+void MastodonApi::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 (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 (!postStatus(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)
+{
+ 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
new file mode 100644
index 0000000..0ec3653
--- /dev/null
+++ b/transferengine-plugins/mastodontransferplugin/mastodonapi.h
@@ -0,0 +1,56 @@
+#ifndef MASTODONAPI_H
+#define MASTODONAPI_H
+
+#include <QtCore/QMap>
+#include <QtCore/QObject>
+
+#include <QtNetwork/QNetworkAccessManager>
+#include <QtNetwork/QNetworkReply>
+
+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);
+
+ 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:
+ static QString normalizeApiHost(const QString &rawHost);
+ bool postStatus(const QString &mediaId);
+ void finishTransfer(QNetworkReply::NetworkError error, int httpCode, const QByteArray &data);
+
+ QMap<QNetworkReply*, API_CALL> m_replies;
+ 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
new file mode 100644
index 0000000..2ee4cd0
--- /dev/null
+++ b/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.cpp
@@ -0,0 +1,25 @@
+#include "mastodontransferplugin.h"
+#include "mastodonuploader.h"
+
+#include <QtPlugin>
+#include <QNetworkAccessManager>
+
+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
new file mode 100644
index 0000000..68cf188
--- /dev/null
+++ b/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.h
@@ -0,0 +1,27 @@
+#ifndef MASTODONTRANSFERPLUGIN_H
+#define MASTODONTRANSFERPLUGIN_H
+
+#include <QtCore/QObject>
+
+#include <transferplugininterface.h>
+
+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
new file mode 100644
index 0000000..b37bf17
--- /dev/null
+++ b/transferengine-plugins/mastodontransferplugin/mastodontransferplugin.pro
@@ -0,0 +1,24 @@
+TEMPLATE = lib
+TARGET = $$qtLibraryTarget(mastodontransferplugin)
+CONFIG += plugin
+DEPENDPATH += .
+INCLUDEPATH += ..
+
+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
new file mode 100644
index 0000000..9e2fa1a
--- /dev/null
+++ b/transferengine-plugins/mastodontransferplugin/mastodonuploader.cpp
@@ -0,0 +1,200 @@
+#include "mastodonuploader.h"
+#include "mastodonapi.h"
+
+#include <imageoperation.h>
+#include <mediaitem.h>
+
+#include <QtCore/QFile>
+#include <QtCore/QMimeDatabase>
+#include <QtCore/QMimeType>
+
+#include <QtDebug>
+
+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/graphic-s-service-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;
+ }
+
+ postImage();
+}
+
+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 isJpeg = 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 (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;
+ }
+
+ 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);
+ }
+
+ 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 image";
+ }
+}
diff --git a/transferengine-plugins/mastodontransferplugin/mastodonuploader.h b/transferengine-plugins/mastodontransferplugin/mastodonuploader.h
new file mode 100644
index 0000000..b0ea263
--- /dev/null
+++ b/transferengine-plugins/mastodontransferplugin/mastodonuploader.h
@@ -0,0 +1,51 @@
+#ifndef MASTODONUPLOADER_H
+#define MASTODONUPLOADER_H
+
+#include <QtNetwork/QNetworkAccessManager>
+
+#include <mediatransferinterface.h>
+
+#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 postImage();
+
+ 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
new file mode 100644
index 0000000..fef3cf5
--- /dev/null
+++ b/transferengine-plugins/transferengine-plugins.pro
@@ -0,0 +1,2 @@
+TEMPLATE = subdirs
+SUBDIRS = mastodonshareplugin mastodontransferplugin