summaryrefslogtreecommitdiff
path: root/buteo-plugins/buteo-sync-plugin-fediverse-posts
diff options
context:
space:
mode:
authorAndrew Branson <andrew.branson@jolla.com>2026-04-03 22:55:30 +0200
committerAndrew Branson <andrew.branson@jolla.com>2026-04-04 11:55:25 +0200
commita35c9fa159173388d88ef77e1d31f53488aad094 (patch)
treee4691b5bbf054ca13e35d98d9df653bf9cdc0054 /buteo-plugins/buteo-sync-plugin-fediverse-posts
parent5f999f7a4712c4a4d1c89054b544064cfd4b769e (diff)
Generalize for all fediverse accounts
Diffstat (limited to 'buteo-plugins/buteo-sync-plugin-fediverse-posts')
-rw-r--r--buteo-plugins/buteo-sync-plugin-fediverse-posts/buteo-sync-plugin-fediverse-posts.pro41
-rw-r--r--buteo-plugins/buteo-sync-plugin-fediverse-posts/fediverse-posts.xml6
-rw-r--r--buteo-plugins/buteo-sync-plugin-fediverse-posts/fediverse.Posts.xml17
-rw-r--r--buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversedatatypesyncadaptor.cpp240
-rw-r--r--buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversedatatypesyncadaptor.h71
-rw-r--r--buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostsplugin.cpp49
-rw-r--r--buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostsplugin.h54
-rw-r--r--buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostssyncadaptor.cpp260
-rw-r--r--buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostssyncadaptor.h61
9 files changed, 799 insertions, 0 deletions
diff --git a/buteo-plugins/buteo-sync-plugin-fediverse-posts/buteo-sync-plugin-fediverse-posts.pro b/buteo-plugins/buteo-sync-plugin-fediverse-posts/buteo-sync-plugin-fediverse-posts.pro
new file mode 100644
index 0000000..d9936b0
--- /dev/null
+++ b/buteo-plugins/buteo-sync-plugin-fediverse-posts/buteo-sync-plugin-fediverse-posts.pro
@@ -0,0 +1,41 @@
+# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+TARGET = fediverse-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/fediversedatatypesyncadaptor.cpp \
+ $$PWD/fediversepostsplugin.cpp \
+ $$PWD/fediversepostssyncadaptor.cpp
+
+HEADERS += \
+ $$PWD/fediversedatatypesyncadaptor.h \
+ $$PWD/fediversepostsplugin.h \
+ $$PWD/fediversepostssyncadaptor.h
+
+OTHER_FILES += \
+ $$PWD/fediverse-posts.xml \
+ $$PWD/fediverse.Posts.xml
+
+TEMPLATE = lib
+CONFIG += plugin
+target.path = $$[QT_INSTALL_LIBS]/buteo-plugins-qt5/oopp
+
+sync.path = /etc/buteo/profiles/sync
+sync.files = fediverse.Posts.xml
+
+client.path = /etc/buteo/profiles/client
+client.files = fediverse-posts.xml
+
+INSTALLS += target sync client
diff --git a/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediverse-posts.xml b/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediverse-posts.xml
new file mode 100644
index 0000000..4397ff0
--- /dev/null
+++ b/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediverse-posts.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. -->
+<!-- SPDX-License-Identifier: BSD-3-Clause -->
+<profile name="fediverse-posts" type="client" >
+ <field name="Sync Direction" />
+</profile>
diff --git a/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediverse.Posts.xml b/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediverse.Posts.xml
new file mode 100644
index 0000000..6601313
--- /dev/null
+++ b/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediverse.Posts.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. -->
+<!-- SPDX-License-Identifier: BSD-3-Clause -->
+<profile name="fediverse.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="Fediverse Posts"/>
+
+ <schedule enabled="false" interval="30" days="1,2,3,4,5,6,7" syncconfiguredtime="" time="" />
+
+ <profile name="fediverse-posts" type="client" >
+ <key name="Sync Direction" value="from-remote" />
+ </profile>
+</profile>
diff --git a/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversedatatypesyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversedatatypesyncadaptor.cpp
new file mode 100644
index 0000000..3ef6f35
--- /dev/null
+++ b/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversedatatypesyncadaptor.cpp
@@ -0,0 +1,240 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2013-2026 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 "fediversedatatypesyncadaptor.h"
+#include "fediverseauthutils.h"
+
+#include <QtCore/QLoggingCategory>
+#include <QtCore/QVariantMap>
+#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>
+
+Q_LOGGING_CATEGORY(lcFediverseSync, "buteo.plugin.fediverse.sync", QtWarningMsg)
+
+FediverseDataTypeSyncAdaptor::FediverseDataTypeSyncAdaptor(
+ SocialNetworkSyncAdaptor::DataType dataType,
+ QObject *parent)
+ : SocialNetworkSyncAdaptor(QStringLiteral("fediverse"), dataType, 0, parent)
+{
+}
+
+FediverseDataTypeSyncAdaptor::~FediverseDataTypeSyncAdaptor()
+{
+}
+
+void FediverseDataTypeSyncAdaptor::sync(const QString &dataTypeString, int accountId)
+{
+ if (dataTypeString != SocialNetworkSyncAdaptor::dataTypeName(m_dataType)) {
+ qCWarning(lcFediverseSync) << "Fediverse" << SocialNetworkSyncAdaptor::dataTypeName(m_dataType)
+ << "sync adaptor was asked to sync" << dataTypeString;
+ setStatus(SocialNetworkSyncAdaptor::Error);
+ return;
+ }
+
+ setStatus(SocialNetworkSyncAdaptor::Busy);
+ updateDataForAccount(accountId);
+ qCDebug(lcFediverseSync) << "successfully triggered sync with profile:" << m_accountSyncProfile->name();
+}
+
+void FediverseDataTypeSyncAdaptor::updateDataForAccount(int accountId)
+{
+ Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this);
+ if (!account) {
+ qCWarning(lcFediverseSync) << "existing account with id" << accountId << "couldn't be retrieved";
+ setStatus(SocialNetworkSyncAdaptor::Error);
+ return;
+ }
+
+ incrementSemaphore(accountId);
+ signIn(account);
+}
+
+QString FediverseDataTypeSyncAdaptor::apiHost(int accountId) const
+{
+ return m_apiHosts.value(accountId, FediverseAuthUtils::defaultApiHost());
+}
+
+QString FediverseDataTypeSyncAdaptor::iconPath(int accountId) const
+{
+ return m_iconPaths.value(accountId, QStringLiteral("image://theme/icon-l-fediverse"));
+}
+
+void FediverseDataTypeSyncAdaptor::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(lcFediverseSync) << 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 FediverseDataTypeSyncAdaptor::sslErrorsHandler(const QList<QSslError> &errs)
+{
+ QString sslerrs;
+ foreach (const QSslError &e, errs) {
+ sslerrs += e.errorString() + QLatin1String("; ");
+ }
+ if (!sslerrs.isEmpty()) {
+ sslerrs.chop(2);
+ }
+
+ qCWarning(lcFediverseSync) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType)
+ << "request with account" << sender()->property("accountId").toInt()
+ << "experienced ssl errors:" << sslerrs;
+ sender()->setProperty("isError", QVariant::fromValue<bool>(true));
+}
+
+void FediverseDataTypeSyncAdaptor::setCredentialsNeedUpdate(Accounts::Account *account)
+{
+ qCInfo(lcFediverseSync) << "sociald:Fediverse: 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-fediverse")));
+ account->selectService(Accounts::Service());
+ account->syncAndBlock();
+}
+
+void FediverseDataTypeSyncAdaptor::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(lcFediverseSync) << "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(lcFediverseSync) << "could not create signon session for account" << accountId;
+ identity->deleteLater();
+ decrementSemaphore(accountId);
+ return;
+ }
+
+ QVariantMap signonSessionData = accSrv.authData().parameters();
+ FediverseAuthUtils::addSignOnSessionParameters(account, &signonSessionData);
+
+ connect(session, SIGNAL(response(SignOn::SessionData)),
+ this, SLOT(signOnResponse(SignOn::SessionData)),
+ Qt::UniqueConnection);
+ connect(session, SIGNAL(error(SignOn::Error)),
+ this, SLOT(signOnError(SignOn::Error)),
+ Qt::UniqueConnection);
+
+ session->setProperty("account", QVariant::fromValue<Accounts::Account*>(account));
+ session->setProperty("identity", QVariant::fromValue<SignOn::Identity*>(identity));
+ session->process(SignOn::SessionData(signonSessionData), mechanism);
+}
+
+void FediverseDataTypeSyncAdaptor::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(lcFediverseSync) << "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 FediverseDataTypeSyncAdaptor::signOnResponse(const SignOn::SessionData &responseData)
+{
+ const QVariantMap data = FediverseAuthUtils::responseDataToMap(responseData);
+
+ 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 = FediverseAuthUtils::accessToken(data);
+ if (accessToken.isEmpty()) {
+ qCWarning(lcFediverseSync) << "signon response for account with id" << accountId
+ << "contained no access token; keys:" << data.keys();
+ }
+
+ m_apiHosts.insert(accountId, FediverseAuthUtils::normalizeApiHost(account->value(QStringLiteral("api/Host")).toString()));
+ m_iconPaths.insert(accountId, account->value(QStringLiteral("iconPath")).toString().trimmed());
+
+ 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-fediverse-posts/fediversedatatypesyncadaptor.h b/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversedatatypesyncadaptor.h
new file mode 100644
index 0000000..4511a26
--- /dev/null
+++ b/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversedatatypesyncadaptor.h
@@ -0,0 +1,71 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2013-2026 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
+ **
+ ****************************************************************************/
+
+#ifndef FEDIVERSEDATATYPESYNCADAPTOR_H
+#define FEDIVERSEDATATYPESYNCADAPTOR_H
+
+#include "socialnetworksyncadaptor.h"
+
+#include <QtCore/QMap>
+#include <QtNetwork/QNetworkReply>
+#include <QtNetwork/QSslError>
+
+namespace Accounts {
+ class Account;
+}
+namespace SignOn {
+ class Error;
+ class SessionData;
+}
+
+class FediverseDataTypeSyncAdaptor : public SocialNetworkSyncAdaptor
+{
+ Q_OBJECT
+
+public:
+ FediverseDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent);
+ virtual ~FediverseDataTypeSyncAdaptor();
+
+ void sync(const QString &dataTypeString, int accountId) override;
+
+protected:
+ QString apiHost(int accountId) const;
+ QString iconPath(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:
+ void setCredentialsNeedUpdate(Accounts::Account *account);
+ void signIn(Accounts::Account *account);
+
+private:
+ QMap<int, QString> m_apiHosts;
+ QMap<int, QString> m_iconPaths;
+};
+
+#endif // FEDIVERSEDATATYPESYNCADAPTOR_H
diff --git a/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostsplugin.cpp b/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostsplugin.cpp
new file mode 100644
index 0000000..c794c00
--- /dev/null
+++ b/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostsplugin.cpp
@@ -0,0 +1,49 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2013-2026 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 "fediversepostsplugin.h"
+#include "fediversepostssyncadaptor.h"
+#include "socialnetworksyncadaptor.h"
+
+FediversePostsPlugin::FediversePostsPlugin(const QString& pluginName,
+ const Buteo::SyncProfile& profile,
+ Buteo::PluginCbInterface *callbackInterface)
+ : SocialdButeoPlugin(pluginName, profile, callbackInterface,
+ QStringLiteral("fediverse"),
+ SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Posts))
+{
+}
+
+FediversePostsPlugin::~FediversePostsPlugin()
+{
+}
+
+SocialNetworkSyncAdaptor *FediversePostsPlugin::createSocialNetworkSyncAdaptor()
+{
+ return new FediversePostsSyncAdaptor(this);
+}
+
+Buteo::ClientPlugin* FediversePostsPluginLoader::createClientPlugin(
+ const QString& pluginName,
+ const Buteo::SyncProfile& profile,
+ Buteo::PluginCbInterface* cbInterface)
+{
+ return new FediversePostsPlugin(pluginName, profile, cbInterface);
+}
diff --git a/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostsplugin.h b/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostsplugin.h
new file mode 100644
index 0000000..933cd97
--- /dev/null
+++ b/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostsplugin.h
@@ -0,0 +1,54 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2013-2026 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
+ **
+ ****************************************************************************/
+
+#ifndef FEDIVERSEPOSTSPLUGIN_H
+#define FEDIVERSEPOSTSPLUGIN_H
+
+#include "socialdbuteoplugin.h"
+
+#include <buteosyncfw5/SyncPluginLoader.h>
+
+class Q_DECL_EXPORT FediversePostsPlugin : public SocialdButeoPlugin
+{
+ Q_OBJECT
+
+public:
+ FediversePostsPlugin(const QString& pluginName,
+ const Buteo::SyncProfile& profile,
+ Buteo::PluginCbInterface *cbInterface);
+ ~FediversePostsPlugin();
+
+protected:
+ SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor() override;
+};
+
+class FediversePostsPluginLoader : public Buteo::SyncPluginLoader
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.FediversePostsPluginLoader")
+ Q_INTERFACES(Buteo::SyncPluginLoader)
+
+public:
+ Buteo::ClientPlugin* createClientPlugin(const QString& pluginName,
+ const Buteo::SyncProfile& profile,
+ Buteo::PluginCbInterface* cbInterface) override;
+};
+
+#endif // FEDIVERSEPOSTSPLUGIN_H
diff --git a/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostssyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostssyncadaptor.cpp
new file mode 100644
index 0000000..59e37bf
--- /dev/null
+++ b/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostssyncadaptor.cpp
@@ -0,0 +1,260 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2013-2026 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 "fediversepostssyncadaptor.h"
+#include "fediversetextutils.h"
+
+#include <QtCore/QLoggingCategory>
+#include <QtCore/QJsonArray>
+#include <QtCore/QJsonObject>
+#include <QtCore/QJsonValue>
+#include <QtCore/QUrl>
+#include <QtCore/QUrlQuery>
+#include <QtNetwork/QNetworkRequest>
+
+namespace {
+ Q_LOGGING_CATEGORY(lcFediversePostsSync, "buteo.plugin.fediverse.posts.sync", QtWarningMsg)
+
+ 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();
+ }
+}
+
+FediversePostsSyncAdaptor::FediversePostsSyncAdaptor(QObject *parent)
+ : FediverseDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Posts, parent)
+{
+ setInitialActive(m_db.isValid());
+}
+
+FediversePostsSyncAdaptor::~FediversePostsSyncAdaptor()
+{
+}
+
+QString FediversePostsSyncAdaptor::syncServiceName() const
+{
+ return QStringLiteral("fediverse-microblog");
+}
+
+void FediversePostsSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode)
+{
+ m_db.removePosts(oldId);
+ m_db.commit();
+ m_db.wait();
+ m_db.refresh();
+ m_db.wait();
+
+ purgeCachedImages(&m_imageCacheDb, oldId);
+}
+
+void FediversePostsSyncAdaptor::beginSync(int accountId, const QString &accessToken)
+{
+ requestPosts(accountId, accessToken);
+}
+
+void FediversePostsSyncAdaptor::finalize(int accountId)
+{
+ if (syncAborted()) {
+ qCInfo(lcFediversePostsSync) << "sync aborted, won't commit database changes";
+ } else {
+ m_db.commit();
+ m_db.wait();
+ m_db.refresh();
+ m_db.wait();
+ purgeExpiredImages(&m_imageCacheDb, accountId);
+ }
+}
+
+QString FediversePostsSyncAdaptor::sanitizeContent(const QString &content)
+{
+ return FediverseTextUtils::sanitizeContent(content);
+}
+
+QDateTime FediversePostsSyncAdaptor::parseTimestamp(const QString &timestampString)
+{
+ return FediverseTextUtils::parseTimestamp(timestampString);
+}
+
+void FediversePostsSyncAdaptor::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(lcFediversePostsSync) << "unable to request home timeline posts from Fediverse account with id" << accountId;
+ }
+}
+
+void FediversePostsSyncAdaptor::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) {
+ m_db.removePosts(accountId);
+
+ if (!statuses.size()) {
+ qCDebug(lcFediversePostsSync) << "no feed posts received for account" << accountId;
+ decrementSemaphore(accountId);
+ return;
+ }
+
+ 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());
+ const int repliesCount = postObject.value(QStringLiteral("replies_count")).toInt();
+ const int favouritesCount = postObject.value(QStringLiteral("favourites_count")).toInt();
+ const int reblogsCount = postObject.value(QStringLiteral("reblogs_count")).toInt();
+ const bool favourited = postObject.value(QStringLiteral("favourited")).toBool();
+ const bool reblogged = postObject.value(QStringLiteral("reblogged")).toBool();
+
+ 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.addFediversePost(identifier,
+ displayName,
+ accountName,
+ body,
+ eventTimestamp,
+ icon,
+ imageList,
+ url,
+ boostedBy,
+ repliesCount,
+ favouritesCount,
+ reblogsCount,
+ favourited,
+ reblogged,
+ apiHost(accountId),
+ iconPath(accountId),
+ accountId);
+ }
+ } else {
+ qCWarning(lcFediversePostsSync) << "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-fediverse-posts/fediversepostssyncadaptor.h b/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostssyncadaptor.h
new file mode 100644
index 0000000..341e049
--- /dev/null
+++ b/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostssyncadaptor.h
@@ -0,0 +1,61 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2013-2026 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
+ **
+ ****************************************************************************/
+
+#ifndef FEDIVERSEPOSTSSYNCADAPTOR_H
+#define FEDIVERSEPOSTSSYNCADAPTOR_H
+
+#include "fediversedatatypesyncadaptor.h"
+
+#include <QtCore/QDateTime>
+#include <QtNetwork/QNetworkReply>
+
+#include "fediversepostsdatabase.h"
+#include <socialimagesdatabase.h>
+
+class FediversePostsSyncAdaptor : public FediverseDataTypeSyncAdaptor
+{
+ Q_OBJECT
+
+public:
+ FediversePostsSyncAdaptor(QObject *parent);
+ ~FediversePostsSyncAdaptor();
+
+ 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:
+ FediversePostsDatabase m_db;
+ SocialImagesDatabase m_imageCacheDb;
+};
+
+#endif // FEDIVERSEPOSTSSYNCADAPTOR_H