From a35c9fa159173388d88ef77e1d31f53488aad094 Mon Sep 17 00:00:00 2001 From: Andrew Branson Date: Fri, 3 Apr 2026 22:55:30 +0200 Subject: Generalize for all fediverse accounts --- .../buteo-sync-plugin-fediverse-posts.pro | 41 ++++ .../fediverse-posts.xml | 6 + .../fediverse.Posts.xml | 17 ++ .../fediversedatatypesyncadaptor.cpp | 240 +++++++++++++++++++ .../fediversedatatypesyncadaptor.h | 71 ++++++ .../fediversepostsplugin.cpp | 49 ++++ .../fediversepostsplugin.h | 54 +++++ .../fediversepostssyncadaptor.cpp | 260 +++++++++++++++++++++ .../fediversepostssyncadaptor.h | 61 +++++ 9 files changed, 799 insertions(+) create mode 100644 buteo-plugins/buteo-sync-plugin-fediverse-posts/buteo-sync-plugin-fediverse-posts.pro create mode 100644 buteo-plugins/buteo-sync-plugin-fediverse-posts/fediverse-posts.xml create mode 100644 buteo-plugins/buteo-sync-plugin-fediverse-posts/fediverse.Posts.xml create mode 100644 buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversedatatypesyncadaptor.cpp create mode 100644 buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversedatatypesyncadaptor.h create mode 100644 buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostsplugin.cpp create mode 100644 buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostsplugin.h create mode 100644 buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostssyncadaptor.cpp create mode 100644 buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostssyncadaptor.h (limited to 'buteo-plugins/buteo-sync-plugin-fediverse-posts') 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 @@ + + + + + + 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 @@ + + + + + + + + + + + + + + + + + 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 +#include +#include + +// libaccounts-qt5 +#include +#include +#include +#include + +// libsignon-qt5 +#include +#include +#include + +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(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(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 &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(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(true)); + account->setValue(QStringLiteral("CredentialsNeedUpdateFrom"), QVariant::fromValue(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(account)); + session->setProperty("identity", QVariant::fromValue(identity)); + session->process(SignOn::SessionData(signonSessionData), mechanism); +} + +void FediverseDataTypeSyncAdaptor::signOnError(const SignOn::Error &error) +{ + SignOn::AuthSession *session = qobject_cast(sender()); + Accounts::Account *account = session->property("account").value(); + SignOn::Identity *identity = session->property("identity").value(); + const int accountId = account->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(sender()); + Accounts::Account *account = session->property("account").value(); + SignOn::Identity *identity = session->property("identity").value(); + 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 +#include +#include + +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 &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 m_apiHosts; + QMap 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 + +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 +#include +#include +#include +#include +#include +#include + +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 ×tampString) +{ + 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)), this, SLOT(sslErrorsHandler(QList))); + 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(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 > 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 +#include + +#include "fediversepostsdatabase.h" +#include + +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 ×tampString); + + void requestPosts(int accountId, const QString &accessToken); + +private Q_SLOTS: + void finishedPostsHandler(); + +private: + FediversePostsDatabase m_db; + SocialImagesDatabase m_imageCacheDb; +}; + +#endif // FEDIVERSEPOSTSSYNCADAPTOR_H -- cgit v1.2.3