summaryrefslogtreecommitdiff
path: root/buteo-plugins
diff options
context:
space:
mode:
Diffstat (limited to 'buteo-plugins')
-rw-r--r--buteo-plugins/buteo-common/buteo-common.pri9
-rw-r--r--buteo-plugins/buteo-common/buteo-common.pro29
-rw-r--r--buteo-plugins/buteo-common/buteosyncfw_p.h39
-rw-r--r--buteo-plugins/buteo-common/socialdbuteoplugin.cpp338
-rw-r--r--buteo-plugins/buteo-common/socialdbuteoplugin.h75
-rw-r--r--buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.cpp37
-rw-r--r--buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.h40
-rw-r--r--buteo-plugins/buteo-common/socialnetworksyncadaptor.cpp470
-rw-r--r--buteo-plugins/buteo-common/socialnetworksyncadaptor.h155
-rw-r--r--buteo-plugins/buteo-common/trace.cpp25
-rw-r--r--buteo-plugins/buteo-common/trace.h30
-rw-r--r--buteo-plugins/buteo-plugins.pro6
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-posts/buteo-sync-plugin-mastodon-posts.pro37
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon-posts.xml4
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon.Posts.xml15
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp313
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.h70
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.cpp49
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.h54
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp316
-rw-r--r--buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.h61
21 files changed, 2172 insertions, 0 deletions
diff --git a/buteo-plugins/buteo-common/buteo-common.pri b/buteo-plugins/buteo-common/buteo-common.pri
new file mode 100644
index 0000000..192da35
--- /dev/null
+++ b/buteo-plugins/buteo-common/buteo-common.pri
@@ -0,0 +1,9 @@
+INCLUDEPATH += $$PWD
+DEPENDPATH += .
+
+QT += dbus
+
+CONFIG += link_pkgconfig
+PKGCONFIG += accounts-qt5 buteosyncfw5 socialcache libsignon-qt5 libsailfishkeyprovider
+
+LIBS += -L$$PWD -lmastodonbuteocommon
diff --git a/buteo-plugins/buteo-common/buteo-common.pro b/buteo-plugins/buteo-common/buteo-common.pro
new file mode 100644
index 0000000..c1aa569
--- /dev/null
+++ b/buteo-plugins/buteo-common/buteo-common.pro
@@ -0,0 +1,29 @@
+TEMPLATE = lib
+
+TARGET = mastodonbuteocommon
+TARGET = $$qtLibraryTarget($$TARGET)
+
+QT -= gui
+QT += network dbus
+CONFIG += link_pkgconfig
+PKGCONFIG += accounts-qt5 buteosyncfw5 socialcache
+
+INCLUDEPATH += $$PWD
+
+HEADERS += \
+ $$PWD/buteosyncfw_p.h \
+ $$PWD/socialdbuteoplugin.h \
+ $$PWD/socialnetworksyncadaptor.h \
+ $$PWD/socialdnetworkaccessmanager_p.h \
+ $$PWD/trace.h
+
+SOURCES += \
+ $$PWD/socialdbuteoplugin.cpp \
+ $$PWD/socialnetworksyncadaptor.cpp \
+ $$PWD/socialdnetworkaccessmanager_p.cpp \
+ $$PWD/trace.cpp
+
+TARGETPATH = $$[QT_INSTALL_LIBS]
+target.path = $$TARGETPATH
+
+INSTALLS += target
diff --git a/buteo-plugins/buteo-common/buteosyncfw_p.h b/buteo-plugins/buteo-common/buteosyncfw_p.h
new file mode 100644
index 0000000..5924731
--- /dev/null
+++ b/buteo-plugins/buteo-common/buteosyncfw_p.h
@@ -0,0 +1,39 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2014 Jolla Ltd.
+ ** Contact: Chris Adams <chris.adams@jollamobile.com>
+ **
+ ** This program/library is free software; you can redistribute it and/or
+ ** modify it under the terms of the GNU Lesser General Public License
+ ** version 2.1 as published by the Free Software Foundation.
+ **
+ ** This program/library is distributed in the hope that it will be useful,
+ ** but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ ** Lesser General Public License for more details.
+ **
+ ** You should have received a copy of the GNU Lesser General Public
+ ** License along with this program/library; if not, write to the Free
+ ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ ** 02110-1301 USA
+ **
+ ****************************************************************************/
+
+#ifndef SOCIALD_BUTEOSYNCFW_P_H
+#define SOCIALD_BUTEOSYNCFW_P_H
+
+#include <SyncCommonDefs.h>
+#include <SyncPluginBase.h>
+#include <ProfileManager.h>
+#include <ClientPlugin.h>
+#include <SyncResults.h>
+#include <ProfileEngineDefs.h>
+#include <SyncProfile.h>
+#include <Profile.h>
+#include <PluginCbInterface.h>
+
+#ifndef SOCIALD_TEST_DEFINE
+#define PRIVILEGED_DATA_DIR QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + QLatin1String("/.local/share/system/privileged")
+#endif
+
+#endif // SOCIALD_BUTEOSYNCFW_P_H
diff --git a/buteo-plugins/buteo-common/socialdbuteoplugin.cpp b/buteo-plugins/buteo-common/socialdbuteoplugin.cpp
new file mode 100644
index 0000000..8b27f84
--- /dev/null
+++ b/buteo-plugins/buteo-common/socialdbuteoplugin.cpp
@@ -0,0 +1,338 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2014 Jolla Ltd.
+ ** Contact: Chris Adams <chris.adams@jolla.com>
+ **
+ ** This program/library is free software; you can redistribute it and/or
+ ** modify it under the terms of the GNU Lesser General Public License
+ ** version 2.1 as published by the Free Software Foundation.
+ **
+ ** This program/library is distributed in the hope that it will be useful,
+ ** but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ ** Lesser General Public License for more details.
+ **
+ ** You should have received a copy of the GNU Lesser General Public
+ ** License along with this program/library; if not, write to the Free
+ ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ ** 02110-1301 USA
+ **
+ ****************************************************************************/
+
+#include "socialdbuteoplugin.h"
+#include "socialnetworksyncadaptor.h"
+#include "trace.h"
+
+#include <QCoreApplication>
+#include <QTranslator>
+
+#include <QDBusMessage>
+#include <QDBusConnection>
+#include <QDBusPendingCall>
+
+#include "buteosyncfw_p.h"
+
+#include <Accounts/Manager>
+#include <Accounts/Account>
+#include <Accounts/Service>
+
+namespace {
+ const QString SyncProfileTemplatesKey = QStringLiteral("sync_profile_templates");
+
+ QString SyncProfileIdKey(const QString &templateProfileName)
+ {
+ return QStringLiteral("%1/%2").arg(templateProfileName).arg(Buteo::KEY_PROFILE_ID);
+ }
+
+ QString createProfile(Buteo::ProfileManager *profileManager,
+ const QString &templateProfileName,
+ Accounts::Account *account,
+ const Accounts::Service &srv,
+ bool enableProfile,
+ const QVariantMap &properties)
+ {
+ if (!account || !srv.isValid()) {
+ qWarning() << "Invalid account or service";
+ return QString();
+ }
+ if (templateProfileName.isEmpty()) {
+ qWarning() << "Invalid templateProfileName";
+ return QString();
+ }
+
+ Accounts::Service prevService = account->selectedService();
+ account->selectService(srv);
+
+ Buteo::SyncProfile *templateProfile = profileManager->syncProfile(templateProfileName);
+ if (!templateProfile) {
+ account->selectService(prevService);
+ qWarning() << "Unable to load template profile:" << templateProfileName;
+ return QString();
+ }
+
+ Buteo::SyncProfile *profile = templateProfile->clone();
+ if (!profile) {
+ delete templateProfile;
+ account->selectService(prevService);
+ qWarning() << "unable to clone template profile:" << templateProfileName;
+ return QString();
+ }
+
+ QString accountIdStr = QString::number(account->id());
+ profile->setName(templateProfileName + "-" + accountIdStr);
+ profile->setKey(Buteo::KEY_DISPLAY_NAME, templateProfileName + "-" + account->displayName().toHtmlEscaped());
+ profile->setKey(Buteo::KEY_ACCOUNT_ID, accountIdStr);
+ profile->setBoolKey(Buteo::KEY_USE_ACCOUNTS, true);
+ profile->setEnabled(enableProfile);
+
+ // enable the profile schedule
+ Buteo::SyncSchedule schedule = profile->syncSchedule();
+ schedule.setScheduleEnabled(true);
+ profile->setSyncSchedule(schedule);
+
+ // set custom properties; note this may override any properties already set
+ Q_FOREACH (const QString &key, properties.keys()) {
+ profile->setKey(key, properties[key].toString());
+ }
+
+ QString profileId = profileManager->updateProfile(*profile);
+ if (profileId.isEmpty()) {
+ qWarning() << "Unable to save sync profile" << templateProfile->name();
+ } else {
+ account->setValue(SyncProfileIdKey(templateProfile->name()), profile->name());
+ }
+
+ account->selectService(prevService);
+ delete profile;
+ delete templateProfile;
+
+ return profileId;
+ }
+}
+
+SocialdButeoPlugin::SocialdButeoPlugin(const QString& pluginName,
+ const Buteo::SyncProfile& profile,
+ Buteo::PluginCbInterface *callbackInterface,
+ const QString &socialServiceName,
+ const QString &dataTypeName)
+ : ClientPlugin(pluginName, profile, callbackInterface)
+ , m_socialNetworkSyncAdaptor(nullptr)
+ , m_socialServiceName(socialServiceName)
+ , m_dataTypeName(dataTypeName)
+ , m_profileAccountId(0)
+{
+}
+
+SocialdButeoPlugin::~SocialdButeoPlugin()
+{
+}
+
+bool SocialdButeoPlugin::init()
+{
+ m_profileAccountId = profile().key(Buteo::KEY_ACCOUNT_ID).toInt();
+ m_socialNetworkSyncAdaptor = createSocialNetworkSyncAdaptor();
+ if (m_socialNetworkSyncAdaptor) {
+ connect(m_socialNetworkSyncAdaptor, &SocialNetworkSyncAdaptor::statusChanged,
+ this, &SocialdButeoPlugin::syncStatusChanged);
+ return true;
+ }
+
+ return false;
+}
+
+bool SocialdButeoPlugin::uninit()
+{
+ delete m_socialNetworkSyncAdaptor;
+ m_socialNetworkSyncAdaptor = nullptr;
+ return true;
+}
+
+bool SocialdButeoPlugin::startSync()
+{
+ // if the profile being triggered is the template profile, then we
+ // need to ensure that the appropriate per-account profiles exist.
+ if (m_profileAccountId == 0) {
+ QList<Buteo::SyncProfile*> perAccountProfiles = ensurePerAccountSyncProfilesExist();
+ m_socialNetworkSyncAdaptor->setAccountSyncProfile(NULL);
+
+ // we need to trigger sync with each profile separately,
+ // or (due to scheduling/etc) another plugin instance might
+ // be created to sync that profile at the same time, and
+ // we don't handle concurrency.
+ foreach (Buteo::SyncProfile *perAccountProfile, perAccountProfiles) {
+ QDBusMessage message = QDBusMessage::createMethodCall(
+ "com.meego.msyncd", "/synchronizer", "com.meego.msyncd", "startSync");
+ message.setArguments(QVariantList() << perAccountProfile->name());
+ QDBusConnection::sessionBus().asyncCall(message);
+ }
+ } else {
+ m_socialNetworkSyncAdaptor->setAccountSyncProfile(profile().clone());
+ }
+
+ // now perform sync. Note that for the template profile case, this will
+ // result in a purge operation occurring (checking for removed accounts and
+ // purging any synced data associated with those accounts).
+ if (m_socialNetworkSyncAdaptor && m_socialNetworkSyncAdaptor->enabled()) {
+ if (m_socialNetworkSyncAdaptor->status() == SocialNetworkSyncAdaptor::Inactive) {
+ qCDebug(lcSocialPlugin) << "performing sync of" << m_dataTypeName << "from" << m_socialServiceName
+ << "for account" << m_profileAccountId;
+ m_socialNetworkSyncAdaptor->sync(m_dataTypeName, m_profileAccountId);
+ return true;
+ } else {
+ qCDebug(lcSocialPlugin) << m_socialServiceName << "sync adaptor for" << m_dataTypeName
+ << "is still busy with last sync of account" << m_profileAccountId;
+ }
+ } else {
+ qCDebug(lcSocialPlugin) << "no enabled" << m_socialServiceName << "sync adaptor for" << m_dataTypeName;
+ }
+ return false;
+}
+
+void SocialdButeoPlugin::abortSync(Sync::SyncStatus status)
+{
+ // note: it seems buteo automatically calls abortSync on network connectivity loss...
+ qCInfo(lcSocialPlugin) << "aborting sync with status:" << status;
+ m_socialNetworkSyncAdaptor->abortSync(status);
+}
+
+bool SocialdButeoPlugin::cleanUp()
+{
+ m_profileAccountId = profile().key(Buteo::KEY_ACCOUNT_ID).toInt();
+ if (!m_socialNetworkSyncAdaptor) {
+ // might have already been initialized by the OOP framework via init().
+ m_socialNetworkSyncAdaptor = createSocialNetworkSyncAdaptor();
+ }
+
+ if (m_socialNetworkSyncAdaptor && m_profileAccountId > 0) {
+ m_socialNetworkSyncAdaptor->purgeDataForOldAccount(m_profileAccountId,
+ SocialNetworkSyncAdaptor::CleanUpPurge);
+ }
+
+ return true;
+}
+
+Buteo::SyncResults SocialdButeoPlugin::getSyncResults() const
+{
+ return m_syncResults;
+}
+
+void SocialdButeoPlugin::connectivityStateChanged(Sync::ConnectivityType type, bool state)
+{
+ // See TransportTracker.cpp:149 for example
+ // Sync::CONNECTIVITY_INTERNET, true|false
+ qCInfo(lcSocialPlugin) << "notified of connectivity change:" << type << state;
+ if (type == Sync::CONNECTIVITY_INTERNET && state == false) {
+ // we lost connectivity during sync.
+ abortSync(Sync::SYNC_CONNECTION_ERROR);
+ }
+}
+
+void SocialdButeoPlugin::syncStatusChanged()
+{
+ if (m_socialNetworkSyncAdaptor) {
+ SocialNetworkSyncAdaptor::Status syncStatus = m_socialNetworkSyncAdaptor->status();
+ // Busy change comes when sync starts -> let's ignore that.
+ if (syncStatus == SocialNetworkSyncAdaptor::Inactive) {
+ updateResults(Buteo::SyncResults(QDateTime::currentDateTime(),
+ Buteo::SyncResults::SYNC_RESULT_SUCCESS,
+ Buteo::SyncResults::NO_ERROR));
+ emit success(getProfileName(), QString("%1 update succeeded").arg(getProfileName()));
+ } else if (syncStatus != SocialNetworkSyncAdaptor::Busy) {
+ updateResults(Buteo::SyncResults(QDateTime::currentDateTime(),
+ Buteo::SyncResults::SYNC_RESULT_FAILED,
+ Buteo::SyncResults::ABORTED));
+ emit error(getProfileName(), QString("%1 update failed").arg(getProfileName()),
+ Buteo::SyncResults::ABORTED);
+ }
+ } else {
+ updateResults(Buteo::SyncResults(QDateTime::currentDateTime(),
+ Buteo::SyncResults::SYNC_RESULT_FAILED,
+ Buteo::SyncResults::ABORTED));
+ emit error(getProfileName(), QString("%1 update failed").arg(getProfileName()), Buteo::SyncResults::ABORTED);
+ }
+}
+
+void SocialdButeoPlugin::updateResults(const Buteo::SyncResults &results)
+{
+ m_syncResults = results;
+ m_syncResults.setScheduled(true);
+}
+
+// This function is called when the non-per-account profile is triggered.
+// The implementation does:
+// - get all profiles from the ProfileManager
+// - get all accounts from the AccountManager
+// - build a mapping of profile -> account for the current data type. (should be one-to-one for the datatype).
+// - any account which doesn't have a profile, print an error.
+// - check the enabled status of the account -> ensure that the enabled status is reflected in the profile.
+// It then returns a list of the appropriate (per account for this data-type) sync profiles.
+// The caller takes ownership of the list.
+QList<Buteo::SyncProfile*> SocialdButeoPlugin::ensurePerAccountSyncProfilesExist()
+{
+ Accounts::Manager am;
+ Accounts::AccountIdList accountIds = am.accountList();
+ QList<Buteo::SyncProfile*> syncProfiles = m_profileManager.allSyncProfiles();
+ QMap<Accounts::Account*, Buteo::SyncProfile*> perAccountProfiles;
+
+ Accounts::Service dataTypeSyncService = am.service(m_socialNetworkSyncAdaptor->syncServiceName());
+ if (!dataTypeSyncService.isValid()) {
+ qWarning() << Q_FUNC_INFO << "Invalid data type sync service name specified:"
+ << m_socialNetworkSyncAdaptor->syncServiceName();
+ return QList<Buteo::SyncProfile*>();
+ }
+
+ for (int i = 0; i < accountIds.size(); ++i) {
+ Accounts::Account *currAccount = Accounts::Account::fromId(&am, accountIds.at(i), this);
+ if (!currAccount || currAccount->id() == 0
+ || m_socialNetworkSyncAdaptor->syncServiceName().split('-').first() != currAccount->providerName()) {
+ // we only generate per-account sync profiles for accounts which
+ // are provided by the provider which provides our sync service.
+ continue;
+ }
+
+ // for the current account, find the associated sync profile.
+ bool foundProfile = false;
+ for (int j = 0; j < syncProfiles.size(); ++j) {
+ if (syncProfiles[j]->key(Buteo::KEY_ACCOUNT_ID).toInt() == QString::number(currAccount->id()).toInt()
+ && syncProfiles[j]->clientProfile() != NULL
+ && syncProfiles[j]->clientProfile()->name() == profile().clientProfile()->name()) {
+ // we have found the sync profile for this datatype for this account.
+ foundProfile = true;
+ perAccountProfiles.insert(currAccount, syncProfiles.takeAt(j));
+ break;
+ }
+ }
+
+ if (!foundProfile) {
+ // it should have been generated for the account when the account was added.
+ qCInfo(lcSocialPlugin) << "no per-account" << profile().name()
+ << "sync profile exists for account:" << currAccount->id();
+
+ // create the per-account profile... we shouldn't need to do this...
+ QString profileName = createProfile(&m_profileManager, profile().name(), currAccount, dataTypeSyncService, true, QVariantMap());
+ Buteo::SyncProfile *newProfile = m_profileManager.syncProfile(profileName);
+ if (!newProfile) {
+ qCWarning(lcSocialPlugin) << "unable to create per-account" << profile().name()
+ << "sync profile for account:" << currAccount->id();
+ } else {
+ // enable the sync schedule for the profile.
+ Buteo::SyncSchedule schedule = newProfile->syncSchedule();
+ schedule.setScheduleEnabled(true);
+ newProfile->setSyncSchedule(schedule);
+ m_profileManager.updateProfile(*newProfile);
+ // and return the profile in the map.
+ perAccountProfiles.insert(currAccount, newProfile);
+ }
+ }
+ }
+
+ // Every account now has the appropriate sync profile.
+ qDeleteAll(syncProfiles); // these are for the wrong data type, ignore them.
+ QList<Buteo::SyncProfile *> retn;
+ foreach (Accounts::Account *acc, perAccountProfiles.keys()) {
+ retn.append(perAccountProfiles[acc]);
+ acc->deleteLater();
+ }
+
+ return retn;
+}
diff --git a/buteo-plugins/buteo-common/socialdbuteoplugin.h b/buteo-plugins/buteo-common/socialdbuteoplugin.h
new file mode 100644
index 0000000..57de171
--- /dev/null
+++ b/buteo-plugins/buteo-common/socialdbuteoplugin.h
@@ -0,0 +1,75 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2013-2014 Jolla Ltd.
+ ** Contact: Raine Makelainen <raine.makelainen@jollamobile.com>
+ **
+ ** This program/library is free software; you can redistribute it and/or
+ ** modify it under the terms of the GNU Lesser General Public License
+ ** version 2.1 as published by the Free Software Foundation.
+ **
+ ** This program/library is distributed in the hope that it will be useful,
+ ** but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ ** Lesser General Public License for more details.
+ **
+ ** You should have received a copy of the GNU Lesser General Public
+ ** License along with this program/library; if not, write to the Free
+ ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ ** 02110-1301 USA
+ **
+ ****************************************************************************/
+
+#ifndef SOCIALDBUTEOPLUGIN_H
+#define SOCIALDBUTEOPLUGIN_H
+
+#include <QtCore/qglobal.h>
+#include "buteosyncfw_p.h"
+
+/*
+ Datatype-specific implementations of this class
+ allow per-account sync profiles for that data type.
+*/
+
+class SocialNetworkSyncAdaptor;
+class Q_DECL_EXPORT SocialdButeoPlugin : public Buteo::ClientPlugin
+{
+ Q_OBJECT
+
+protected:
+ virtual SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor() = 0;
+
+public:
+ SocialdButeoPlugin(const QString& pluginName,
+ const Buteo::SyncProfile& profile,
+ Buteo::PluginCbInterface *cbInterface,
+ const QString &socialServiceName,
+ const QString &dataTypeName);
+ virtual ~SocialdButeoPlugin();
+
+ bool init() override;
+ bool uninit() override;
+ bool startSync() override;
+ void abortSync(Sync::SyncStatus status = Sync::SYNC_ABORTED) override;
+ Buteo::SyncResults getSyncResults() const override;
+ bool cleanUp() override;
+
+public Q_SLOTS:
+ void connectivityStateChanged(Sync::ConnectivityType type, bool state) override;
+
+private Q_SLOTS:
+ void syncStatusChanged();
+
+protected:
+ QList<Buteo::SyncProfile*> ensurePerAccountSyncProfilesExist();
+
+private:
+ void updateResults(const Buteo::SyncResults &results);
+ Buteo::SyncResults m_syncResults;
+ Buteo::ProfileManager m_profileManager;
+ SocialNetworkSyncAdaptor *m_socialNetworkSyncAdaptor;
+ QString m_socialServiceName;
+ QString m_dataTypeName;
+ int m_profileAccountId;
+};
+
+#endif // SOCIALDBUTEOPLUGIN_H
diff --git a/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.cpp b/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.cpp
new file mode 100644
index 0000000..7030ce3
--- /dev/null
+++ b/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.cpp
@@ -0,0 +1,37 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2013-2014 Jolla Ltd.
+ ** Contact: Chris Adams <chris.adams@jollamobile.com>
+ **
+ ** This program/library is free software; you can redistribute it and/or
+ ** modify it under the terms of the GNU Lesser General Public License
+ ** version 2.1 as published by the Free Software Foundation.
+ **
+ ** This program/library is distributed in the hope that it will be useful,
+ ** but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ ** Lesser General Public License for more details.
+ **
+ ** You should have received a copy of the GNU Lesser General Public
+ ** License along with this program/library; if not, write to the Free
+ ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ ** 02110-1301 USA
+ **
+ ****************************************************************************/
+
+#include "socialdnetworkaccessmanager_p.h"
+
+/* The default implementation is just a normal QNetworkAccessManager */
+
+SocialdNetworkAccessManager::SocialdNetworkAccessManager(QObject *parent)
+ : QNetworkAccessManager(parent)
+{
+}
+
+QNetworkReply *SocialdNetworkAccessManager::createRequest(
+ QNetworkAccessManager::Operation op,
+ const QNetworkRequest &req,
+ QIODevice *outgoingData)
+{
+ return QNetworkAccessManager::createRequest(op, req, outgoingData);
+}
diff --git a/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.h b/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.h
new file mode 100644
index 0000000..846489b
--- /dev/null
+++ b/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.h
@@ -0,0 +1,40 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2013-2014 Jolla Ltd.
+ ** Contact: Chris Adams <chris.adams@jollamobile.com>
+ **
+ ** This program/library is free software; you can redistribute it and/or
+ ** modify it under the terms of the GNU Lesser General Public License
+ ** version 2.1 as published by the Free Software Foundation.
+ **
+ ** This program/library is distributed in the hope that it will be useful,
+ ** but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ ** Lesser General Public License for more details.
+ **
+ ** You should have received a copy of the GNU Lesser General Public
+ ** License along with this program/library; if not, write to the Free
+ ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ ** 02110-1301 USA
+ **
+ ****************************************************************************/
+
+#ifndef SOCIALD_QNAMFACTORY_P_H
+#define SOCIALD_QNAMFACTORY_P_H
+
+#include <QNetworkAccessManager>
+
+class SocialdNetworkAccessManager : public QNetworkAccessManager
+{
+ Q_OBJECT
+
+public:
+ SocialdNetworkAccessManager(QObject *parent = 0);
+
+protected:
+ QNetworkReply *createRequest(QNetworkAccessManager::Operation op,
+ const QNetworkRequest &req,
+ QIODevice *outgoingData = 0) override;
+};
+
+#endif
diff --git a/buteo-plugins/buteo-common/socialnetworksyncadaptor.cpp b/buteo-plugins/buteo-common/socialnetworksyncadaptor.cpp
new file mode 100644
index 0000000..4451d5f
--- /dev/null
+++ b/buteo-plugins/buteo-common/socialnetworksyncadaptor.cpp
@@ -0,0 +1,470 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2013-2014 Jolla Ltd.
+ ** Contact: Chris Adams <chris.adams@jollamobile.com>
+ **
+ ** This program/library is free software; you can redistribute it and/or
+ ** modify it under the terms of the GNU Lesser General Public License
+ ** version 2.1 as published by the Free Software Foundation.
+ **
+ ** This program/library is distributed in the hope that it will be useful,
+ ** but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ ** Lesser General Public License for more details.
+ **
+ ** You should have received a copy of the GNU Lesser General Public
+ ** License along with this program/library; if not, write to the Free
+ ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ ** 02110-1301 USA
+ **
+ ****************************************************************************/
+
+#include "socialnetworksyncadaptor.h"
+#include "socialdnetworkaccessmanager_p.h"
+#include "trace.h"
+
+#include <QtCore/QJsonDocument>
+#include <QtCore/QTimer>
+#include <QtSql/QSqlDatabase>
+#include <QtSql/QSqlQuery>
+#include <QtSql/QSqlError>
+#include <QtSql/QSqlRecord>
+
+#include <QtNetwork/QNetworkAccessManager>
+#include <QtNetwork/QNetworkReply>
+
+#include "buteosyncfw_p.h"
+
+// libaccounts-qt5
+#include <Accounts/Manager>
+#include <Accounts/Account>
+#include <Accounts/Service>
+
+// libsocialcache
+#include <socialcache/socialimagesdatabase.h>
+#include <socialcache/socialnetworksyncdatabase.h>
+
+namespace {
+ QStringList validDataTypesInitialiser()
+ {
+ return QStringList()
+ << QStringLiteral("Contacts")
+ << QStringLiteral("Calendars")
+ << QStringLiteral("Notifications")
+ << QStringLiteral("Images")
+ << QStringLiteral("Videos")
+ << QStringLiteral("Posts")
+ << QStringLiteral("Messages")
+ << QStringLiteral("Emails")
+ << QStringLiteral("Signon")
+ << QStringLiteral("Backup")
+ << QStringLiteral("BackupQuery")
+ << QStringLiteral("BackupRestore");
+ }
+}
+
+SocialNetworkSyncAdaptor::SocialNetworkSyncAdaptor(const QString &serviceName,
+ SocialNetworkSyncAdaptor::DataType dataType,
+ QNetworkAccessManager *qnam,
+ QObject *parent)
+ : QObject(parent)
+ , m_dataType(dataType)
+ , m_accountManager(new Accounts::Manager(this))
+ , m_networkAccessManager(qnam != 0 ? qnam : new SocialdNetworkAccessManager)
+ , m_accountSyncProfile(NULL)
+ , m_syncDb(new SocialNetworkSyncDatabase())
+ , m_status(SocialNetworkSyncAdaptor::Invalid)
+ , m_enabled(false)
+ , m_syncAborted(false)
+ , m_serviceName(serviceName)
+{
+}
+
+SocialNetworkSyncAdaptor::~SocialNetworkSyncAdaptor()
+{
+ delete m_networkAccessManager;
+ delete m_accountSyncProfile;
+ delete m_syncDb;
+}
+
+// The SocialNetworkSyncAdaptor takes ownership of the sync profiles.
+void SocialNetworkSyncAdaptor::setAccountSyncProfile(Buteo::SyncProfile* perAccountSyncProfile)
+{
+ delete m_accountSyncProfile;
+ m_accountSyncProfile = perAccountSyncProfile;
+}
+
+SocialNetworkSyncAdaptor::Status SocialNetworkSyncAdaptor::status() const
+{
+ return m_status;
+}
+
+bool SocialNetworkSyncAdaptor::enabled() const
+{
+ return m_enabled;
+}
+
+QString SocialNetworkSyncAdaptor::serviceName() const
+{
+ return m_serviceName;
+}
+
+bool SocialNetworkSyncAdaptor::syncAborted() const
+{
+ return m_syncAborted;
+}
+
+void SocialNetworkSyncAdaptor::sync(const QString &dataType, int accountId)
+{
+ Q_UNUSED(dataType)
+ Q_UNUSED(accountId)
+ qCWarning(lcSocialPlugin) << "sync() must be overridden by derived types";
+}
+
+void SocialNetworkSyncAdaptor::abortSync(Sync::SyncStatus status)
+{
+ qCInfo(lcSocialPlugin) << "forcing timeout of outstanding replies due to abort:" << status;
+ m_syncAborted = true;
+ triggerReplyTimeouts();
+}
+
+/*!
+ * \brief SocialNetworkSyncAdaptor::checkAccount
+ * \param account
+ * \return true if synchronization of this adaptor's datatype is enabled for the account
+ *
+ * The default implementation checks that the account is enabled
+ * with the accounts&sso service associated with this sync adaptor.
+ */
+bool SocialNetworkSyncAdaptor::checkAccount(Accounts::Account *account)
+{
+ bool globallyEnabled = account->enabled();
+ Accounts::Service srv(m_accountManager->service(syncServiceName()));
+ if (!srv.isValid()) {
+ qCInfo(lcSocialPlugin) << "invalid service" << syncServiceName() << "specified, account" << account->id()
+ << "will be disabled for" << m_serviceName << dataTypeName(m_dataType) << "sync";
+ return false;
+ }
+ account->selectService(srv);
+ bool serviceEnabled = account->enabled();
+ account->selectService(Accounts::Service());
+ return globallyEnabled && serviceEnabled;
+}
+
+/*!
+ \internal
+ Called when the semaphores for all accounts have been decreased
+ to zero. This is the final function which is called prior to
+ telling buteo that the sync plugin can be destroyed.
+ The implementation MUST be synchronous.
+*/
+void SocialNetworkSyncAdaptor::finalCleanup()
+{
+}
+
+/*!
+ \internal
+ Called when the semaphores decreased to 0, this method is used
+ to finalize something, like saving all data to a database.
+
+ You can call incrementSemaphore to perform asynchronous tasks
+ in this method. finalize will then be called again when the
+ asynchronous task is finished (and when decrementSemaphore is
+ called), be sure to have a condition check in order not to run
+ into an infinite loop.
+
+ It is unsafe to call decrementSemaphore in this method, as
+ the semaphore handling method will find that the semaphore
+ went to 0 twice and will perform cleanup operations twice.
+ Please call decrementSemaphore at the end of the asynchronous
+ task (preferably in a slot), and only call incrementSemaphore
+ for asynchronous tasks.
+ */
+void SocialNetworkSyncAdaptor::finalize(int accountId)
+{
+ Q_UNUSED(accountId)
+}
+
+/*!
+ \internal
+ Returns the last sync timestamp for the given service, account and data type.
+ If data from prior to this timestamp is received in subsequent requests, it does not need to be synced.
+ This function will return an invalid QDateTime if no synchronisation has occurred.
+*/
+QDateTime SocialNetworkSyncAdaptor::lastSyncTimestamp(const QString &serviceName,
+ const QString &dataType,
+ int accountId) const
+{
+ return m_syncDb->lastSyncTimestamp(serviceName, dataType, accountId);
+}
+
+/*!
+ \internal
+ Updates the last sync timestamp for the given service, account and data type to the given \a timestamp.
+*/
+bool SocialNetworkSyncAdaptor::updateLastSyncTimestamp(const QString &serviceName,
+ const QString &dataType,
+ int accountId,
+ const QDateTime &timestamp)
+{
+ // Workaround
+ // TODO: do better, with a queue
+ m_syncDb->addSyncTimestamp(serviceName, dataType, accountId, timestamp);
+ m_syncDb->commit();
+ m_syncDb->wait();
+ return m_syncDb->writeStatus() == AbstractSocialCacheDatabase::Finished;
+}
+
+/*!
+ \internal
+ Returns the list of identifiers of accounts which have been synced for
+ the given \a dataType.
+*/
+QList<int> SocialNetworkSyncAdaptor::syncedAccounts(const QString &dataType)
+{
+ return m_syncDb->syncedAccounts(m_serviceName, dataType);
+}
+
+/*!
+ * \internal
+ * Changes status if there is real change and emits statusChanged() signal.
+ */
+void SocialNetworkSyncAdaptor::setStatus(Status status)
+{
+ if (m_status != status) {
+ m_status = status;
+ emit statusChanged();
+ }
+}
+
+/*!
+ * \internal
+ * Should be used in constructors to set the initial state
+ * of enabled and status, without emitting signals
+ *
+ */
+void SocialNetworkSyncAdaptor::setInitialActive(bool enabled)
+{
+ m_enabled = enabled;
+ if (enabled) {
+ m_status = Inactive;
+ } else {
+ m_status = Invalid;
+ }
+}
+
+/*!
+ * \internal
+ * Should be called by any specific sync adapter when
+ * they've finished syncing data. The transition from
+ * busy status to inactive status is what causes the
+ * Buteo plugin to emit the sync results (and allows
+ * subsequent syncs to occur).
+ */
+void SocialNetworkSyncAdaptor::setFinishedInactive()
+{
+ finalCleanup();
+ qCInfo(lcSocialPlugin) << "Finished" << m_serviceName << SocialNetworkSyncAdaptor::dataTypeName(m_dataType)
+ << "sync at:" << QDateTime::currentDateTime().toString(Qt::ISODate);
+ setStatus(SocialNetworkSyncAdaptor::Inactive);
+}
+
+void SocialNetworkSyncAdaptor::incrementSemaphore(int accountId)
+{
+ int semaphoreValue = m_accountSyncSemaphores.value(accountId);
+ semaphoreValue += 1;
+ m_accountSyncSemaphores.insert(accountId, semaphoreValue);
+ qCDebug(lcSocialPlugin) << "incremented busy semaphore for account" << accountId << "to:" << semaphoreValue;
+}
+
+void SocialNetworkSyncAdaptor::decrementSemaphore(int accountId)
+{
+ if (!m_accountSyncSemaphores.contains(accountId)) {
+ qCWarning(lcSocialPlugin) << "no such semaphore for account" << accountId;
+ return;
+ }
+
+ int semaphoreValue = m_accountSyncSemaphores.value(accountId);
+ semaphoreValue -= 1;
+ qCDebug(lcSocialPlugin) << "decremented busy semaphore for account" << accountId << "to:" << semaphoreValue;
+ if (semaphoreValue < 0) {
+ qCWarning(lcSocialPlugin) << "busy semaphore is negative for account" << accountId;
+ return;
+ }
+ m_accountSyncSemaphores.insert(accountId, semaphoreValue);
+
+ if (semaphoreValue == 0) {
+ finalize(accountId);
+
+ // With the newer implementation, in finalize we can raise semaphores,
+ // so if after calling finalize, the semaphore count is not the same anymore,
+ // we shouldn't update the sync timestamp
+ if (m_accountSyncSemaphores.value(accountId) > 0) {
+ return;
+ }
+
+ // finished all outstanding sync requests for this account.
+ // update the sync time in the global sociald database.
+ updateLastSyncTimestamp(m_serviceName,
+ SocialNetworkSyncAdaptor::dataTypeName(m_dataType), accountId,
+ QDateTime::currentDateTime().toTimeSpec(Qt::UTC));
+
+ // if all outstanding requests for all accounts have finished,
+ // then update our status to Inactive / ready to handle more sync requests.
+ bool allAreZero = true;
+ QList<int> semaphores = m_accountSyncSemaphores.values();
+ foreach (int sv, semaphores) {
+ if (sv != 0) {
+ allAreZero = false;
+ break;
+ }
+ }
+
+ if (allAreZero) {
+ setFinishedInactive(); // Finished!
+ }
+ }
+}
+
+void SocialNetworkSyncAdaptor::timeoutReply()
+{
+ QTimer *timer = qobject_cast<QTimer*>(sender());
+ QNetworkReply *reply = timer->property("networkReply").value<QNetworkReply*>();
+ int accountId = timer->property("accountId").toInt();
+
+ qCWarning(lcSocialPlugin) << "network request timed out while performing sync with account" << accountId;
+
+ m_networkReplyTimeouts[accountId].remove(reply);
+ reply->setProperty("isError", QVariant::fromValue<bool>(true));
+ reply->finished(); // invoke finished, so that the error handling there decrements the semaphore etc.
+ reply->disconnect();
+}
+
+void SocialNetworkSyncAdaptor::setupReplyTimeout(int accountId, QNetworkReply *reply, int msecs)
+{
+ // this function should be called whenever a new network request is performed.
+ QTimer *timer = new QTimer(this);
+ timer->setSingleShot(true);
+ timer->setInterval(msecs);
+ timer->setProperty("accountId", accountId);
+ timer->setProperty("networkReply", QVariant::fromValue<QNetworkReply*>(reply));
+ connect(timer, &QTimer::timeout, this, &SocialNetworkSyncAdaptor::timeoutReply);
+ timer->start();
+ m_networkReplyTimeouts[accountId].insert(reply, timer);
+}
+
+void SocialNetworkSyncAdaptor::removeReplyTimeout(int accountId, QNetworkReply *reply)
+{
+ // this function should be called by the finished() handler for the reply.
+ QTimer *timer = m_networkReplyTimeouts[accountId].value(reply);
+ if (!reply) {
+ return;
+ }
+
+ delete timer;
+ m_networkReplyTimeouts[accountId].remove(reply);
+}
+
+void SocialNetworkSyncAdaptor::triggerReplyTimeouts()
+{
+ // if we've lost network connectivity, we should immediately timeout all replies.
+ Q_FOREACH (int accountId, m_networkReplyTimeouts.keys()) {
+ Q_FOREACH (QTimer *timer, m_networkReplyTimeouts[accountId]) {
+ timer->stop();
+ timer->setInterval(1);
+ timer->start();
+ }
+ }
+}
+
+QJsonObject SocialNetworkSyncAdaptor::parseJsonObjectReplyData(const QByteArray &replyData, bool *ok)
+{
+ QJsonDocument jsonDocument = QJsonDocument::fromJson(replyData);
+ *ok = !jsonDocument.isEmpty();
+ if (*ok && jsonDocument.isObject()) {
+ return jsonDocument.object();
+ }
+ *ok = false;
+ return QJsonObject();
+}
+
+QJsonArray SocialNetworkSyncAdaptor::parseJsonArrayReplyData(const QByteArray &replyData, bool *ok)
+{
+ QJsonDocument jsonDocument = QJsonDocument::fromJson(replyData);
+ *ok = !jsonDocument.isEmpty();
+ if (*ok && jsonDocument.isArray()) {
+ return jsonDocument.array();
+ }
+ *ok = false;
+ return QJsonArray();
+}
+
+/*
+ Valid data types are data types which are known to the API.
+ Note that just because a data type is valid does not mean
+ that it will necessarily be supported by a given social network
+ sync adaptor.
+*/
+QStringList SocialNetworkSyncAdaptor::validDataTypes()
+{
+ static QStringList retn(validDataTypesInitialiser());
+ return retn;
+}
+
+/*
+ String for Enum since the DBus API uses strings
+*/
+QString SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::DataType t)
+{
+ switch (t) {
+ case SocialNetworkSyncAdaptor::Contacts: return QStringLiteral("Contacts");
+ case SocialNetworkSyncAdaptor::Calendars: return QStringLiteral("Calendars");
+ case SocialNetworkSyncAdaptor::Notifications: return QStringLiteral("Notifications");
+ case SocialNetworkSyncAdaptor::Images: return QStringLiteral("Images");
+ case SocialNetworkSyncAdaptor::Videos: return QStringLiteral("Videos");
+ case SocialNetworkSyncAdaptor::Posts: return QStringLiteral("Posts");
+ case SocialNetworkSyncAdaptor::Messages: return QStringLiteral("Messages");
+ case SocialNetworkSyncAdaptor::Emails: return QStringLiteral("Emails");
+ case SocialNetworkSyncAdaptor::Signon: return QStringLiteral("Signon");
+ case SocialNetworkSyncAdaptor::Backup: return QStringLiteral("Backup");
+ case SocialNetworkSyncAdaptor::BackupQuery: return QStringLiteral("BackupQuery");
+ case SocialNetworkSyncAdaptor::BackupRestore: return QStringLiteral("BackupRestore");
+ default: break;
+ }
+
+ return QString();
+}
+
+void SocialNetworkSyncAdaptor::purgeCachedImages(SocialImagesDatabase *database,
+ int accountId)
+{
+ database->queryImages(accountId);
+ database->wait();
+
+ QList<SocialImage::ConstPtr> images = database->images();
+ foreach (SocialImage::ConstPtr image, images) {
+ qCDebug(lcSocialPlugin) << "Purge cached image " << image->imageFile() << " for account " << image->accountId();
+ QFile::remove(image->imageFile());
+ }
+
+ database->removeImages(images);
+ database->commit();
+ database->wait();
+}
+
+void SocialNetworkSyncAdaptor::purgeExpiredImages(SocialImagesDatabase *database,
+ int accountId)
+{
+ database->queryExpired(accountId);
+ database->wait();
+
+ QList<SocialImage::ConstPtr> images = database->images();
+ foreach (SocialImage::ConstPtr image, images) {
+ qCDebug(lcSocialPlugin) << "Purge expired image " << image->imageFile() << " for account " << image->accountId();
+ QFile::remove(image->imageFile());
+ }
+
+ database->removeImages(images);
+ database->commit();
+ database->wait();
+}
diff --git a/buteo-plugins/buteo-common/socialnetworksyncadaptor.h b/buteo-plugins/buteo-common/socialnetworksyncadaptor.h
new file mode 100644
index 0000000..99adeb3
--- /dev/null
+++ b/buteo-plugins/buteo-common/socialnetworksyncadaptor.h
@@ -0,0 +1,155 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2013-2014 Jolla Ltd.
+ ** Contact: Chris Adams <chris.adams@jollamobile.com>
+ **
+ ** This program/library is free software; you can redistribute it and/or
+ ** modify it under the terms of the GNU Lesser General Public License
+ ** version 2.1 as published by the Free Software Foundation.
+ **
+ ** This program/library is distributed in the hope that it will be useful,
+ ** but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ ** Lesser General Public License for more details.
+ **
+ ** You should have received a copy of the GNU Lesser General Public
+ ** License along with this program/library; if not, write to the Free
+ ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ ** 02110-1301 USA
+ **
+ ****************************************************************************/
+
+#ifndef SOCIALNETWORKSYNCADAPTOR_H
+#define SOCIALNETWORKSYNCADAPTOR_H
+
+#include <QtCore/QObject>
+#include <QtCore/QDateTime>
+#include <QtCore/QString>
+#include <QtCore/QJsonObject>
+#include <QtCore/QJsonArray>
+#include <QtCore/QMap>
+#include <QtCore/QList>
+
+#include "buteosyncfw_p.h"
+
+class QSqlDatabase;
+class QNetworkAccessManager;
+class QTimer;
+class QNetworkReply;
+class SocialNetworkSyncDatabase;
+class SocialImagesDatabase;
+
+namespace Accounts {
+ class Account;
+ class Manager;
+}
+
+class SocialNetworkSyncAdaptor : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(Status status READ status NOTIFY statusChanged)
+ Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged)
+
+public:
+ enum Status {
+ Initializing = 0,
+ Inactive,
+ Busy,
+ Error,
+ Invalid
+ };
+
+ enum PurgeMode {
+ SyncPurge = 0,
+ CleanUpPurge
+ };
+
+ enum DataType {
+ Contacts = 1, // "Contacts"
+ Calendars, // "Calendars"
+ Notifications, // "Notifications"
+ Images, // "Images"
+ Videos, // "Videos"
+ Posts, // "Posts"
+ Messages, // "Messages"
+ Emails, // "Emails"
+ Signon, // "Signon" -- for refreshing AccessTokens etc.
+ Backup, // "Backup"
+ BackupQuery, // "BackupQuery"
+ BackupRestore // "BackupRestore"
+ };
+ static QStringList validDataTypes();
+ static QString dataTypeName(DataType t);
+
+public:
+ SocialNetworkSyncAdaptor(const QString &serviceName, SocialNetworkSyncAdaptor::DataType dataType,
+ QNetworkAccessManager *qnam, QObject *parent);
+ virtual ~SocialNetworkSyncAdaptor();
+
+ virtual QString syncServiceName() const = 0;
+ void setAccountSyncProfile(Buteo::SyncProfile* perAccountSyncProfile);
+
+ Status status() const;
+ bool enabled() const;
+ QString serviceName() const;
+
+ virtual void sync(const QString &dataType, int accountId = 0);
+ virtual void purgeDataForOldAccount(int accountId, PurgeMode mode = SyncPurge) = 0;
+ virtual void abortSync(Sync::SyncStatus status);
+
+Q_SIGNALS:
+ void statusChanged();
+ void enabledChanged();
+
+protected:
+ virtual bool checkAccount(Accounts::Account *account);
+ virtual void finalCleanup();
+ virtual void finalize(int accountId);
+ QDateTime lastSyncTimestamp(const QString &serviceName, const QString &dataType,
+ int accountId) const;
+ bool updateLastSyncTimestamp(const QString &serviceName, const QString &dataType,
+ int accountId, const QDateTime &timestamp);
+ QList<int> syncedAccounts(const QString &dataType);
+ void setStatus(Status status);
+ void setInitialActive(bool enabled);
+ void setFinishedInactive();
+
+ // whether the sync has been aborted (perhaps due to network connection loss)
+ bool syncAborted() const;
+
+ // Semaphore system
+ void incrementSemaphore(int accountId);
+ void decrementSemaphore(int accountId);
+
+ // network reply timeouts
+ void setupReplyTimeout(int accountId, QNetworkReply *reply, int msecs = 60000);
+ void removeReplyTimeout(int accountId, QNetworkReply *reply);
+ void triggerReplyTimeouts();
+
+ // Parsing methods
+ static QJsonObject parseJsonObjectReplyData(const QByteArray &replyData, bool *ok);
+ static QJsonArray parseJsonArrayReplyData(const QByteArray &replyData, bool *ok);
+
+ // Cache management
+ void purgeCachedImages(SocialImagesDatabase *database, int accountId);
+ void purgeExpiredImages(SocialImagesDatabase *database, int accountId);
+
+ const SocialNetworkSyncAdaptor::DataType m_dataType;
+ Accounts::Manager * const m_accountManager;
+ QNetworkAccessManager * const m_networkAccessManager;
+ Buteo::SyncProfile *m_accountSyncProfile;
+
+protected Q_SLOTS:
+ virtual void timeoutReply();
+
+private:
+ SocialNetworkSyncDatabase *m_syncDb;
+ SocialNetworkSyncAdaptor::Status m_status;
+ bool m_enabled;
+ bool m_syncAborted;
+ QString m_serviceName;
+ QMap<int, int> m_accountSyncSemaphores;
+ QMap<int, QMap<QNetworkReply*, QTimer *> > m_networkReplyTimeouts;
+};
+
+#endif // SOCIALNETWORKSYNCADAPTOR_H
diff --git a/buteo-plugins/buteo-common/trace.cpp b/buteo-plugins/buteo-common/trace.cpp
new file mode 100644
index 0000000..ffcdf5b
--- /dev/null
+++ b/buteo-plugins/buteo-common/trace.cpp
@@ -0,0 +1,25 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2021 Jolla Ltd.
+ **
+ ** This program/library is free software; you can redistribute it and/or
+ ** modify it under the terms of the GNU Lesser General Public License
+ ** version 2.1 as published by the Free Software Foundation.
+ **
+ ** This program/library is distributed in the hope that it will be useful,
+ ** but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ ** Lesser General Public License for more details.
+ **
+ ** You should have received a copy of the GNU Lesser General Public
+ ** License along with this program/library; if not, write to the Free
+ ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ ** 02110-1301 USA
+ **
+ ****************************************************************************/
+
+#include "trace.h"
+
+Q_LOGGING_CATEGORY(lcSocialPlugin, "buteo.plugin.social", QtWarningMsg)
+Q_LOGGING_CATEGORY(lcSocialPluginTrace, "buteo.plugin.social.trace", QtWarningMsg)
+
diff --git a/buteo-plugins/buteo-common/trace.h b/buteo-plugins/buteo-common/trace.h
new file mode 100644
index 0000000..b0aeeeb
--- /dev/null
+++ b/buteo-plugins/buteo-common/trace.h
@@ -0,0 +1,30 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2013-2014 Jolla Ltd.
+ ** Contact: Chris Adams <chris.adams@jollamobile.com>
+ **
+ ** This program/library is free software; you can redistribute it and/or
+ ** modify it under the terms of the GNU Lesser General Public License
+ ** version 2.1 as published by the Free Software Foundation.
+ **
+ ** This program/library is distributed in the hope that it will be useful,
+ ** but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ ** Lesser General Public License for more details.
+ **
+ ** You should have received a copy of the GNU Lesser General Public
+ ** License along with this program/library; if not, write to the Free
+ ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ ** 02110-1301 USA
+ **
+ ****************************************************************************/
+
+#ifndef TRACE_H
+#define TRACE_H
+
+#include <QLoggingCategory>
+
+Q_DECLARE_LOGGING_CATEGORY(lcSocialPlugin)
+Q_DECLARE_LOGGING_CATEGORY(lcSocialPluginTrace)
+
+#endif // TRACE_H
diff --git a/buteo-plugins/buteo-plugins.pro b/buteo-plugins/buteo-plugins.pro
new file mode 100644
index 0000000..5a48850
--- /dev/null
+++ b/buteo-plugins/buteo-plugins.pro
@@ -0,0 +1,6 @@
+TEMPLATE = subdirs
+SUBDIRS += \
+ buteo-common \
+ buteo-sync-plugin-mastodon-posts
+
+buteo-sync-plugin-mastodon-posts.depends = buteo-common
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/buteo-sync-plugin-mastodon-posts.pro b/buteo-plugins/buteo-sync-plugin-mastodon-posts/buteo-sync-plugin-mastodon-posts.pro
new file mode 100644
index 0000000..86387b2
--- /dev/null
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/buteo-sync-plugin-mastodon-posts.pro
@@ -0,0 +1,37 @@
+TARGET = mastodon-posts-client
+
+QT -= gui
+
+include($$PWD/../buteo-common/buteo-common.pri)
+include($$PWD/../../common/common.pri)
+
+CONFIG += link_pkgconfig
+PKGCONFIG += mlite5 nemonotifications-qt5
+
+INCLUDEPATH += $$PWD
+
+SOURCES += \
+ $$PWD/mastodondatatypesyncadaptor.cpp \
+ $$PWD/mastodonpostsplugin.cpp \
+ $$PWD/mastodonpostssyncadaptor.cpp
+
+HEADERS += \
+ $$PWD/mastodondatatypesyncadaptor.h \
+ $$PWD/mastodonpostsplugin.h \
+ $$PWD/mastodonpostssyncadaptor.h
+
+OTHER_FILES += \
+ $$PWD/mastodon-posts.xml \
+ $$PWD/mastodon.Posts.xml
+
+TEMPLATE = lib
+CONFIG += plugin
+target.path = $$[QT_INSTALL_LIBS]/buteo-plugins-qt5/oopp
+
+sync.path = /etc/buteo/profiles/sync
+sync.files = mastodon.Posts.xml
+
+client.path = /etc/buteo/profiles/client
+client.files = mastodon-posts.xml
+
+INSTALLS += target sync client
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon-posts.xml b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon-posts.xml
new file mode 100644
index 0000000..7f1fec2
--- /dev/null
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon-posts.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<profile name="mastodon-posts" type="client" >
+ <field name="Sync Direction" />
+</profile>
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon.Posts.xml b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon.Posts.xml
new file mode 100644
index 0000000..44183a6
--- /dev/null
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon.Posts.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<profile name="mastodon.Posts" type="sync" >
+ <key name="category" value="eventfeed" />
+ <key name="enabled" value="false" />
+ <key name="use_accounts" value="false" />
+ <key name="destinationtype" value="online" />
+ <key name="hidden" value="true" />
+ <key name="displayname" value="Mastodon Posts"/>
+
+ <schedule enabled="false" interval="30" days="1,2,3,4,5,6,7" syncconfiguredtime="" time="" />
+
+ <profile name="mastodon-posts" type="client" >
+ <key name="Sync Direction" value="from-remote" />
+ </profile>
+</profile>
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp
new file mode 100644
index 0000000..977b33b
--- /dev/null
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp
@@ -0,0 +1,313 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2026 Open Mobile Platform LLC.
+ **
+ ** This program/library is free software; you can redistribute it and/or
+ ** modify it under the terms of the GNU Lesser General Public License
+ ** version 2.1 as published by the Free Software Foundation.
+ **
+ ** This program/library is distributed in the hope that it will be useful,
+ ** but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ ** Lesser General Public License for more details.
+ **
+ ** You should have received a copy of the GNU Lesser General Public
+ ** License along with this program/library; if not, write to the Free
+ ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ ** 02110-1301 USA
+ **
+ ****************************************************************************/
+
+#include "mastodondatatypesyncadaptor.h"
+#include "trace.h"
+
+#include <QtCore/QVariantMap>
+#include <QtCore/QUrl>
+#include <QtNetwork/QNetworkRequest>
+
+// libaccounts-qt5
+#include <Accounts/Manager>
+#include <Accounts/Account>
+#include <Accounts/Service>
+#include <Accounts/AccountService>
+
+// libsignon-qt5
+#include <SignOn/Identity>
+#include <SignOn/AuthSession>
+#include <SignOn/SessionData>
+
+MastodonDataTypeSyncAdaptor::MastodonDataTypeSyncAdaptor(
+ SocialNetworkSyncAdaptor::DataType dataType,
+ QObject *parent)
+ : SocialNetworkSyncAdaptor(QStringLiteral("mastodon"), dataType, 0, parent)
+{
+}
+
+MastodonDataTypeSyncAdaptor::~MastodonDataTypeSyncAdaptor()
+{
+}
+
+void MastodonDataTypeSyncAdaptor::sync(const QString &dataTypeString, int accountId)
+{
+ if (dataTypeString != SocialNetworkSyncAdaptor::dataTypeName(m_dataType)) {
+ qCWarning(lcSocialPlugin) << "Mastodon" << SocialNetworkSyncAdaptor::dataTypeName(m_dataType)
+ << "sync adaptor was asked to sync" << dataTypeString;
+ setStatus(SocialNetworkSyncAdaptor::Error);
+ return;
+ }
+
+ setStatus(SocialNetworkSyncAdaptor::Busy);
+ updateDataForAccount(accountId);
+ qCDebug(lcSocialPlugin) << "successfully triggered sync with profile:" << m_accountSyncProfile->name();
+}
+
+void MastodonDataTypeSyncAdaptor::updateDataForAccount(int accountId)
+{
+ Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this);
+ if (!account) {
+ qCWarning(lcSocialPlugin) << "existing account with id" << accountId << "couldn't be retrieved";
+ setStatus(SocialNetworkSyncAdaptor::Error);
+ return;
+ }
+
+ incrementSemaphore(accountId);
+ signIn(account);
+}
+
+QString MastodonDataTypeSyncAdaptor::apiHost(int accountId) const
+{
+ return m_apiHosts.value(accountId, QStringLiteral("https://mastodon.social"));
+}
+
+void MastodonDataTypeSyncAdaptor::errorHandler(QNetworkReply::NetworkError err)
+{
+ QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
+ if (!reply) {
+ return;
+ }
+
+ const int accountId = reply->property("accountId").toInt();
+ const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType)
+ << "request with account" << accountId
+ << "experienced error:" << err
+ << "HTTP:" << httpStatus;
+
+ reply->setProperty("isError", QVariant::fromValue<bool>(true));
+
+ if (httpStatus == 401 || err == QNetworkReply::AuthenticationRequiredError) {
+ Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this);
+ if (account) {
+ setCredentialsNeedUpdate(account);
+ }
+ }
+}
+
+void MastodonDataTypeSyncAdaptor::sslErrorsHandler(const QList<QSslError> &errs)
+{
+ QString sslerrs;
+ foreach (const QSslError &e, errs) {
+ sslerrs += e.errorString() + QLatin1String("; ");
+ }
+ if (!sslerrs.isEmpty()) {
+ sslerrs.chop(2);
+ }
+
+ qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType)
+ << "request with account" << sender()->property("accountId").toInt()
+ << "experienced ssl errors:" << sslerrs;
+ sender()->setProperty("isError", QVariant::fromValue<bool>(true));
+}
+
+void MastodonDataTypeSyncAdaptor::setCredentialsNeedUpdate(Accounts::Account *account)
+{
+ qCInfo(lcSocialPlugin) << "sociald:Mastodon: setting CredentialsNeedUpdate to true for account:" << account->id();
+ Accounts::Service srv(m_accountManager->service(syncServiceName()));
+ account->selectService(srv);
+ account->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue<bool>(true));
+ account->setValue(QStringLiteral("CredentialsNeedUpdateFrom"), QVariant::fromValue<QString>(QString::fromLatin1("sociald-mastodon")));
+ account->selectService(Accounts::Service());
+ account->syncAndBlock();
+}
+
+QString MastodonDataTypeSyncAdaptor::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 MastodonDataTypeSyncAdaptor::signIn(Accounts::Account *account)
+{
+ const int accountId = account->id();
+ if (!checkAccount(account)) {
+ decrementSemaphore(accountId);
+ return;
+ }
+
+ Accounts::Service srv(m_accountManager->service(syncServiceName()));
+ account->selectService(srv);
+ SignOn::Identity *identity = account->credentialsId() > 0
+ ? SignOn::Identity::existingIdentity(account->credentialsId())
+ : 0;
+ if (!identity) {
+ qCWarning(lcSocialPlugin) << "account" << accountId << "has no valid credentials, cannot sign in";
+ decrementSemaphore(accountId);
+ return;
+ }
+
+ Accounts::AccountService accSrv(account, srv);
+ const QString method = accSrv.authData().method();
+ const QString mechanism = accSrv.authData().mechanism();
+ SignOn::AuthSession *session = identity->createSession(method);
+ if (!session) {
+ qCWarning(lcSocialPlugin) << "could not create signon session for account" << accountId;
+ identity->deleteLater();
+ decrementSemaphore(accountId);
+ return;
+ }
+
+ QVariantMap signonSessionData = accSrv.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);
+ }
+ 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 MastodonDataTypeSyncAdaptor::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->id();
+
+ qCWarning(lcSocialPlugin) << "credentials for account with id" << accountId
+ << "couldn't be retrieved:" << error.type() << error.message();
+
+ if (error.type() == SignOn::Error::UserInteraction) {
+ setCredentialsNeedUpdate(account);
+ }
+
+ session->disconnect(this);
+ identity->destroySession(session);
+ identity->deleteLater();
+ account->deleteLater();
+
+ setStatus(SocialNetworkSyncAdaptor::Error);
+ decrementSemaphore(accountId);
+}
+
+void MastodonDataTypeSyncAdaptor::signOnResponse(const SignOn::SessionData &responseData)
+{
+ QVariantMap data;
+ foreach (const QString &key, responseData.propertyNames()) {
+ data.insert(key, responseData.getProperty(key));
+ }
+
+ QString accessToken;
+ 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->id();
+
+ accessToken = data.value(QLatin1String("AccessToken")).toString().trimmed();
+ if (accessToken.isEmpty()) {
+ accessToken = data.value(QLatin1String("access_token")).toString().trimmed();
+ }
+ if (accessToken.isEmpty()) {
+ qCWarning(lcSocialPlugin) << "signon response for account with id" << accountId
+ << "contained no access token; keys:" << data.keys();
+ }
+
+ m_apiHosts.insert(accountId, normalizeApiHost(account->value(QStringLiteral("api/Host")).toString()));
+
+ session->disconnect(this);
+ identity->destroySession(session);
+ identity->deleteLater();
+ account->deleteLater();
+
+ if (!accessToken.isEmpty()) {
+ beginSync(accountId, accessToken);
+ } else {
+ setStatus(SocialNetworkSyncAdaptor::Error);
+ }
+
+ decrementSemaphore(accountId);
+}
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.h b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.h
new file mode 100644
index 0000000..f295cd8
--- /dev/null
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.h
@@ -0,0 +1,70 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2026 Open Mobile Platform LLC.
+ **
+ ** This program/library is free software; you can redistribute it and/or
+ ** modify it under the terms of the GNU Lesser General Public License
+ ** version 2.1 as published by the Free Software Foundation.
+ **
+ ** This program/library is distributed in the hope that it will be useful,
+ ** but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ ** Lesser General Public License for more details.
+ **
+ ** You should have received a copy of the GNU Lesser General Public
+ ** License along with this program/library; if not, write to the Free
+ ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ ** 02110-1301 USA
+ **
+ ****************************************************************************/
+
+#ifndef MASTODONDATATYPESYNCADAPTOR_H
+#define MASTODONDATATYPESYNCADAPTOR_H
+
+#include "socialnetworksyncadaptor.h"
+
+#include <QtCore/QMap>
+#include <QtNetwork/QNetworkReply>
+#include <QtNetwork/QSslError>
+
+namespace Accounts {
+ class Account;
+}
+namespace SignOn {
+ class Error;
+ class SessionData;
+}
+
+class MastodonDataTypeSyncAdaptor : public SocialNetworkSyncAdaptor
+{
+ Q_OBJECT
+
+public:
+ MastodonDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent);
+ virtual ~MastodonDataTypeSyncAdaptor();
+
+ void sync(const QString &dataTypeString, int accountId) override;
+
+protected:
+ QString apiHost(int accountId) const;
+ virtual void updateDataForAccount(int accountId);
+ virtual void beginSync(int accountId, const QString &accessToken) = 0;
+
+protected Q_SLOTS:
+ virtual void errorHandler(QNetworkReply::NetworkError err);
+ virtual void sslErrorsHandler(const QList<QSslError> &errs);
+
+private Q_SLOTS:
+ void signOnError(const SignOn::Error &error);
+ void signOnResponse(const SignOn::SessionData &responseData);
+
+private:
+ static QString normalizeApiHost(const QString &rawHost);
+ void setCredentialsNeedUpdate(Accounts::Account *account);
+ void signIn(Accounts::Account *account);
+
+private:
+ QMap<int, QString> m_apiHosts;
+};
+
+#endif // MASTODONDATATYPESYNCADAPTOR_H
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.cpp
new file mode 100644
index 0000000..d3c8d50
--- /dev/null
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.cpp
@@ -0,0 +1,49 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2026 Open Mobile Platform LLC.
+ **
+ ** This program/library is free software; you can redistribute it and/or
+ ** modify it under the terms of the GNU Lesser General Public License
+ ** version 2.1 as published by the Free Software Foundation.
+ **
+ ** This program/library is distributed in the hope that it will be useful,
+ ** but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ ** Lesser General Public License for more details.
+ **
+ ** You should have received a copy of the GNU Lesser General Public
+ ** License along with this program/library; if not, write to the Free
+ ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ ** 02110-1301 USA
+ **
+ ****************************************************************************/
+
+#include "mastodonpostsplugin.h"
+#include "mastodonpostssyncadaptor.h"
+#include "socialnetworksyncadaptor.h"
+
+MastodonPostsPlugin::MastodonPostsPlugin(const QString& pluginName,
+ const Buteo::SyncProfile& profile,
+ Buteo::PluginCbInterface *callbackInterface)
+ : SocialdButeoPlugin(pluginName, profile, callbackInterface,
+ QStringLiteral("mastodon"),
+ SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Posts))
+{
+}
+
+MastodonPostsPlugin::~MastodonPostsPlugin()
+{
+}
+
+SocialNetworkSyncAdaptor *MastodonPostsPlugin::createSocialNetworkSyncAdaptor()
+{
+ return new MastodonPostsSyncAdaptor(this);
+}
+
+Buteo::ClientPlugin* MastodonPostsPluginLoader::createClientPlugin(
+ const QString& pluginName,
+ const Buteo::SyncProfile& profile,
+ Buteo::PluginCbInterface* cbInterface)
+{
+ return new MastodonPostsPlugin(pluginName, profile, cbInterface);
+}
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.h b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.h
new file mode 100644
index 0000000..35dbb56
--- /dev/null
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.h
@@ -0,0 +1,54 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2026 Open Mobile Platform LLC.
+ **
+ ** This program/library is free software; you can redistribute it and/or
+ ** modify it under the terms of the GNU Lesser General Public License
+ ** version 2.1 as published by the Free Software Foundation.
+ **
+ ** This program/library is distributed in the hope that it will be useful,
+ ** but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ ** Lesser General Public License for more details.
+ **
+ ** You should have received a copy of the GNU Lesser General Public
+ ** License along with this program/library; if not, write to the Free
+ ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ ** 02110-1301 USA
+ **
+ ****************************************************************************/
+
+#ifndef MASTODONPOSTSPLUGIN_H
+#define MASTODONPOSTSPLUGIN_H
+
+#include "socialdbuteoplugin.h"
+
+#include <buteosyncfw5/SyncPluginLoader.h>
+
+class Q_DECL_EXPORT MastodonPostsPlugin : public SocialdButeoPlugin
+{
+ Q_OBJECT
+
+public:
+ MastodonPostsPlugin(const QString& pluginName,
+ const Buteo::SyncProfile& profile,
+ Buteo::PluginCbInterface *cbInterface);
+ ~MastodonPostsPlugin();
+
+protected:
+ SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor() override;
+};
+
+class MastodonPostsPluginLoader : public Buteo::SyncPluginLoader
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.MastodonPostsPluginLoader")
+ Q_INTERFACES(Buteo::SyncPluginLoader)
+
+public:
+ Buteo::ClientPlugin* createClientPlugin(const QString& pluginName,
+ const Buteo::SyncProfile& profile,
+ Buteo::PluginCbInterface* cbInterface) override;
+};
+
+#endif // MASTODONPOSTSPLUGIN_H
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp
new file mode 100644
index 0000000..7ccf0a2
--- /dev/null
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp
@@ -0,0 +1,316 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2026 Open Mobile Platform LLC.
+ **
+ ** This program/library is free software; you can redistribute it and/or
+ ** modify it under the terms of the GNU Lesser General Public License
+ ** version 2.1 as published by the Free Software Foundation.
+ **
+ ** This program/library is distributed in the hope that it will be useful,
+ ** but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ ** Lesser General Public License for more details.
+ **
+ ** You should have received a copy of the GNU Lesser General Public
+ ** License along with this program/library; if not, write to the Free
+ ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ ** 02110-1301 USA
+ **
+ ****************************************************************************/
+
+#include "mastodonpostssyncadaptor.h"
+#include "trace.h"
+
+#include <QtCore/QJsonArray>
+#include <QtCore/QJsonObject>
+#include <QtCore/QJsonValue>
+#include <QtCore/QRegularExpression>
+#include <QtCore/QUrl>
+#include <QtCore/QUrlQuery>
+#include <QtNetwork/QNetworkRequest>
+
+namespace {
+ QString decodeHtmlEntities(QString text)
+ {
+ text.replace(QStringLiteral("&quot;"), QStringLiteral("\""));
+ text.replace(QStringLiteral("&apos;"), QStringLiteral("'"));
+ text.replace(QStringLiteral("&lt;"), QStringLiteral("<"));
+ text.replace(QStringLiteral("&gt;"), QStringLiteral(">"));
+ text.replace(QStringLiteral("&amp;"), QStringLiteral("&"));
+ text.replace(QStringLiteral("&nbsp;"), QStringLiteral(" "));
+
+ static const QRegularExpression decimalEntity(QStringLiteral("&#(\\d+);"));
+ QRegularExpressionMatch match;
+ int index = 0;
+ while ((index = text.indexOf(decimalEntity, index, &match)) != -1) {
+ const uint value = match.captured(1).toUInt();
+ const QString replacement = value > 0 ? QString(QChar(value)) : QString();
+ text.replace(index, match.capturedLength(0), replacement);
+ index += replacement.size();
+ }
+
+ static const QRegularExpression hexEntity(QStringLiteral("&#x([0-9a-fA-F]+);"));
+ index = 0;
+ while ((index = text.indexOf(hexEntity, index, &match)) != -1) {
+ bool ok = false;
+ const uint value = match.captured(1).toUInt(&ok, 16);
+ const QString replacement = ok && value > 0 ? QString(QChar(value)) : QString();
+ text.replace(index, match.capturedLength(0), replacement);
+ index += replacement.size();
+ }
+
+ return text;
+ }
+
+ QString displayNameForAccount(const QJsonObject &account)
+ {
+ const QString displayName = account.value(QStringLiteral("display_name")).toString().trimmed();
+ if (!displayName.isEmpty()) {
+ return displayName;
+ }
+
+ const QString username = account.value(QStringLiteral("username")).toString().trimmed();
+ if (!username.isEmpty()) {
+ return username;
+ }
+
+ return account.value(QStringLiteral("acct")).toString().trimmed();
+ }
+}
+
+MastodonPostsSyncAdaptor::MastodonPostsSyncAdaptor(QObject *parent)
+ : MastodonDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Posts, parent)
+{
+ setInitialActive(m_db.isValid());
+}
+
+MastodonPostsSyncAdaptor::~MastodonPostsSyncAdaptor()
+{
+}
+
+QString MastodonPostsSyncAdaptor::syncServiceName() const
+{
+ return QStringLiteral("mastodon-microblog");
+}
+
+void MastodonPostsSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode)
+{
+ m_db.removePosts(oldId);
+ m_db.commit();
+ m_db.wait();
+
+ purgeCachedImages(&m_imageCacheDb, oldId);
+}
+
+void MastodonPostsSyncAdaptor::beginSync(int accountId, const QString &accessToken)
+{
+ requestPosts(accountId, accessToken);
+}
+
+void MastodonPostsSyncAdaptor::finalize(int accountId)
+{
+ if (syncAborted()) {
+ qCInfo(lcSocialPlugin) << "sync aborted, won't commit database changes";
+ } else {
+ m_db.commit();
+ m_db.wait();
+ purgeExpiredImages(&m_imageCacheDb, accountId);
+ }
+}
+
+QString MastodonPostsSyncAdaptor::sanitizeContent(const QString &content)
+{
+ QString plain = content;
+ plain.replace(QRegularExpression(QStringLiteral("<\\s*br\\s*/?\\s*>"), QRegularExpression::CaseInsensitiveOption), QStringLiteral("\n"));
+ plain.replace(QRegularExpression(QStringLiteral("<\\s*/\\s*p\\s*>"), QRegularExpression::CaseInsensitiveOption), QStringLiteral("\n"));
+ plain.remove(QRegularExpression(QStringLiteral("<[^>]+>"), QRegularExpression::CaseInsensitiveOption));
+
+ return decodeHtmlEntities(plain).trimmed();
+}
+
+QDateTime MastodonPostsSyncAdaptor::parseTimestamp(const QString &timestampString)
+{
+ QDateTime timestamp;
+
+#if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)
+ timestamp = QDateTime::fromString(timestampString, Qt::ISODateWithMs);
+ if (timestamp.isValid()) {
+ return timestamp;
+ }
+#endif
+
+ timestamp = QDateTime::fromString(timestampString, Qt::ISODate);
+ if (timestamp.isValid()) {
+ return timestamp;
+ }
+
+ // Qt 5.6 cannot parse ISO-8601 timestamps with fractional seconds.
+ const int timeSeparator = timestampString.indexOf(QLatin1Char('T'));
+ const int fractionSeparator = timestampString.indexOf(QLatin1Char('.'), timeSeparator + 1);
+ if (timeSeparator > -1 && fractionSeparator > -1) {
+ int timezoneSeparator = timestampString.indexOf(QLatin1Char('Z'), fractionSeparator + 1);
+ if (timezoneSeparator == -1) {
+ timezoneSeparator = timestampString.indexOf(QLatin1Char('+'), fractionSeparator + 1);
+ }
+ if (timezoneSeparator == -1) {
+ timezoneSeparator = timestampString.indexOf(QLatin1Char('-'), fractionSeparator + 1);
+ }
+
+ QString stripped = timestampString;
+ if (timezoneSeparator > -1) {
+ stripped.remove(fractionSeparator, timezoneSeparator - fractionSeparator);
+ } else {
+ stripped.truncate(fractionSeparator);
+ }
+
+ timestamp = QDateTime::fromString(stripped, Qt::ISODate);
+ }
+
+ return timestamp;
+}
+
+void MastodonPostsSyncAdaptor::requestPosts(int accountId, const QString &accessToken)
+{
+ QUrl url(apiHost(accountId) + QStringLiteral("/api/v1/timelines/home"));
+
+ QUrlQuery query(url);
+ query.addQueryItem(QStringLiteral("limit"), QStringLiteral("20"));
+ url.setQuery(query);
+
+ QNetworkRequest request(url);
+ request.setRawHeader("Authorization", QStringLiteral("Bearer %1").arg(accessToken).toUtf8());
+
+ QNetworkReply *reply = m_networkAccessManager->get(request);
+ if (reply) {
+ reply->setProperty("accountId", accountId);
+ connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError)));
+ connect(reply, SIGNAL(sslErrors(QList<QSslError>)), this, SLOT(sslErrorsHandler(QList<QSslError>)));
+ connect(reply, SIGNAL(finished()), this, SLOT(finishedPostsHandler()));
+
+ incrementSemaphore(accountId);
+ setupReplyTimeout(accountId, reply);
+ } else {
+ qCWarning(lcSocialPlugin) << "unable to request home timeline posts from Mastodon account with id" << accountId;
+ }
+}
+
+void MastodonPostsSyncAdaptor::finishedPostsHandler()
+{
+ QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
+ if (!reply) {
+ return;
+ }
+
+ const bool isError = reply->property("isError").toBool();
+ const int accountId = reply->property("accountId").toInt();
+ QByteArray replyData = reply->readAll();
+
+ disconnect(reply);
+ reply->deleteLater();
+ removeReplyTimeout(accountId, reply);
+
+ bool ok = false;
+ QJsonArray statuses = parseJsonArrayReplyData(replyData, &ok);
+ if (!isError && ok) {
+ if (!statuses.size()) {
+ qCDebug(lcSocialPlugin) << "no feed posts received for account" << accountId;
+ decrementSemaphore(accountId);
+ return;
+ }
+
+ m_db.removePosts(accountId);
+
+ const int sinceSpan = m_accountSyncProfile
+ ? m_accountSyncProfile->key(Buteo::KEY_SYNC_SINCE_DAYS_PAST, QStringLiteral("7")).toInt()
+ : 7;
+
+ foreach (const QJsonValue &statusValue, statuses) {
+ const QJsonObject statusObject = statusValue.toObject();
+ if (statusObject.isEmpty()) {
+ continue;
+ }
+
+ QJsonObject postObject = statusObject;
+ QString boostedBy;
+ if (statusObject.contains(QStringLiteral("reblog"))
+ && statusObject.value(QStringLiteral("reblog")).isObject()
+ && !statusObject.value(QStringLiteral("reblog")).isNull()) {
+ boostedBy = displayNameForAccount(statusObject.value(QStringLiteral("account")).toObject());
+ postObject = statusObject.value(QStringLiteral("reblog")).toObject();
+ }
+
+ QDateTime eventTimestamp = parseTimestamp(statusObject.value(QStringLiteral("created_at")).toString());
+ if (!eventTimestamp.isValid()) {
+ eventTimestamp = parseTimestamp(postObject.value(QStringLiteral("created_at")).toString());
+ }
+ if (!eventTimestamp.isValid()) {
+ continue;
+ }
+
+ if (eventTimestamp.daysTo(QDateTime::currentDateTime()) > sinceSpan) {
+ continue;
+ }
+
+ const QJsonObject account = postObject.value(QStringLiteral("account")).toObject();
+ const QString displayName = displayNameForAccount(account);
+ const QString accountName = account.value(QStringLiteral("acct")).toString();
+ QString icon = account.value(QStringLiteral("avatar_static")).toString();
+ if (icon.isEmpty()) {
+ icon = account.value(QStringLiteral("avatar")).toString();
+ }
+
+ QString identifier = postObject.value(QStringLiteral("id")).toVariant().toString();
+ if (identifier.isEmpty()) {
+ continue;
+ }
+
+ QString url = postObject.value(QStringLiteral("url")).toString();
+ if (url.isEmpty() && !accountName.isEmpty()) {
+ url = QStringLiteral("%1/@%2/%3").arg(apiHost(accountId), accountName, identifier);
+ }
+
+ const QString body = sanitizeContent(postObject.value(QStringLiteral("content")).toString());
+
+ QList<QPair<QString, SocialPostImage::ImageType> > imageList;
+ const QJsonArray mediaAttachments = postObject.value(QStringLiteral("media_attachments")).toArray();
+ foreach (const QJsonValue &attachmentValue, mediaAttachments) {
+ const QJsonObject attachment = attachmentValue.toObject();
+ const QString mediaType = attachment.value(QStringLiteral("type")).toString();
+
+ QString mediaUrl;
+ SocialPostImage::ImageType imageType = SocialPostImage::Invalid;
+ if (mediaType == QLatin1String("image")) {
+ mediaUrl = attachment.value(QStringLiteral("url")).toString();
+ imageType = SocialPostImage::Photo;
+ } else if (mediaType == QLatin1String("video") || mediaType == QLatin1String("gifv")) {
+ mediaUrl = attachment.value(QStringLiteral("preview_url")).toString();
+ if (mediaUrl.isEmpty()) {
+ mediaUrl = attachment.value(QStringLiteral("url")).toString();
+ }
+ imageType = SocialPostImage::Video;
+ }
+
+ if (!mediaUrl.isEmpty() && imageType != SocialPostImage::Invalid) {
+ imageList.append(qMakePair(mediaUrl, imageType));
+ }
+ }
+
+ m_db.addMastodonPost(identifier,
+ displayName,
+ accountName,
+ body,
+ eventTimestamp,
+ icon,
+ imageList,
+ url,
+ boostedBy,
+ apiHost(accountId),
+ accountId);
+ }
+ } else {
+ qCWarning(lcSocialPlugin) << "unable to parse event feed data from request with account" << accountId
+ << ", got:" << QString::fromUtf8(replyData);
+ }
+
+ decrementSemaphore(accountId);
+}
diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.h b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.h
new file mode 100644
index 0000000..10f8b1c
--- /dev/null
+++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.h
@@ -0,0 +1,61 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2026 Open Mobile Platform LLC.
+ **
+ ** This program/library is free software; you can redistribute it and/or
+ ** modify it under the terms of the GNU Lesser General Public License
+ ** version 2.1 as published by the Free Software Foundation.
+ **
+ ** This program/library is distributed in the hope that it will be useful,
+ ** but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ ** Lesser General Public License for more details.
+ **
+ ** You should have received a copy of the GNU Lesser General Public
+ ** License along with this program/library; if not, write to the Free
+ ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ ** 02110-1301 USA
+ **
+ ****************************************************************************/
+
+#ifndef MASTODONPOSTSSYNCADAPTOR_H
+#define MASTODONPOSTSSYNCADAPTOR_H
+
+#include "mastodondatatypesyncadaptor.h"
+
+#include <QtCore/QDateTime>
+#include <QtNetwork/QNetworkReply>
+
+#include "mastodonpostsdatabase.h"
+#include <socialcache/socialimagesdatabase.h>
+
+class MastodonPostsSyncAdaptor : public MastodonDataTypeSyncAdaptor
+{
+ Q_OBJECT
+
+public:
+ MastodonPostsSyncAdaptor(QObject *parent);
+ ~MastodonPostsSyncAdaptor();
+
+ QString syncServiceName() const override;
+
+protected:
+ void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode) override;
+ void beginSync(int accountId, const QString &accessToken) override;
+ void finalize(int accountId) override;
+
+private:
+ static QString sanitizeContent(const QString &content);
+ static QDateTime parseTimestamp(const QString &timestampString);
+
+ void requestPosts(int accountId, const QString &accessToken);
+
+private Q_SLOTS:
+ void finishedPostsHandler();
+
+private:
+ MastodonPostsDatabase m_db;
+ SocialImagesDatabase m_imageCacheDb;
+};
+
+#endif // MASTODONPOSTSSYNCADAPTOR_H