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-plugins/buteo-common/buteo-common.pri | 2 +- buteo-plugins/buteo-common/buteo-common.pro | 2 +- buteo-plugins/buteo-plugins.pro | 8 +- .../buteo-sync-plugin-fediverse-notifications.pro | 66 ++ .../fediverse-notifications.xml | 6 + .../fediverse.Notifications.xml | 17 + .../fediversedatatypesyncadaptor.cpp | 240 +++++ .../fediversedatatypesyncadaptor.h | 70 ++ .../fediversenotificationsplugin.cpp | 96 ++ .../fediversenotificationsplugin.h | 54 ++ .../fediversenotificationssyncadaptor.cpp | 970 +++++++++++++++++++++ .../fediversenotificationssyncadaptor.h | 104 +++ .../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 ++ .../buteo-sync-plugin-mastodon-notifications.pro | 66 -- .../mastodon-notifications.xml | 6 - .../mastodon.Notifications.xml | 17 - .../mastodondatatypesyncadaptor.cpp | 240 ----- .../mastodondatatypesyncadaptor.h | 70 -- .../mastodonnotificationsplugin.cpp | 96 -- .../mastodonnotificationsplugin.h | 54 -- .../mastodonnotificationssyncadaptor.cpp | 970 --------------------- .../mastodonnotificationssyncadaptor.h | 104 --- .../buteo-sync-plugin-mastodon-posts.pro | 41 - .../mastodon-posts.xml | 6 - .../mastodon.Posts.xml | 17 - .../mastodondatatypesyncadaptor.cpp | 232 ----- .../mastodondatatypesyncadaptor.h | 69 -- .../mastodonpostsplugin.cpp | 49 -- .../mastodonpostsplugin.h | 54 -- .../mastodonpostssyncadaptor.cpp | 257 ------ .../mastodonpostssyncadaptor.h | 61 -- 39 files changed, 2428 insertions(+), 2415 deletions(-) create mode 100644 buteo-plugins/buteo-sync-plugin-fediverse-notifications/buteo-sync-plugin-fediverse-notifications.pro create mode 100644 buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediverse-notifications.xml create mode 100644 buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediverse.Notifications.xml create mode 100644 buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversedatatypesyncadaptor.cpp create mode 100644 buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversedatatypesyncadaptor.h create mode 100644 buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversenotificationsplugin.cpp create mode 100644 buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversenotificationsplugin.h create mode 100644 buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversenotificationssyncadaptor.cpp create mode 100644 buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversenotificationssyncadaptor.h 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 delete mode 100644 buteo-plugins/buteo-sync-plugin-mastodon-notifications/buteo-sync-plugin-mastodon-notifications.pro delete mode 100644 buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon-notifications.xml delete mode 100644 buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon.Notifications.xml delete mode 100644 buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.cpp delete mode 100644 buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.h delete mode 100644 buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationsplugin.cpp delete mode 100644 buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationsplugin.h delete mode 100644 buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.cpp delete mode 100644 buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.h delete mode 100644 buteo-plugins/buteo-sync-plugin-mastodon-posts/buteo-sync-plugin-mastodon-posts.pro delete mode 100644 buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon-posts.xml delete mode 100644 buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon.Posts.xml delete mode 100644 buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp delete mode 100644 buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.h delete mode 100644 buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.cpp delete mode 100644 buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.h delete mode 100644 buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp delete mode 100644 buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.h (limited to 'buteo-plugins') diff --git a/buteo-plugins/buteo-common/buteo-common.pri b/buteo-plugins/buteo-common/buteo-common.pri index 83452ac..422d98f 100644 --- a/buteo-plugins/buteo-common/buteo-common.pri +++ b/buteo-plugins/buteo-common/buteo-common.pri @@ -10,4 +10,4 @@ QT += dbus CONFIG += link_pkgconfig PKGCONFIG += accounts-qt5 buteosyncfw5 socialcache libsignon-qt5 libsailfishkeyprovider -LIBS += -L$$PWD -lmastodonbuteocommon +LIBS += -L$$PWD -lfediversebuteocommon diff --git a/buteo-plugins/buteo-common/buteo-common.pro b/buteo-plugins/buteo-common/buteo-common.pro index c0b84a9..70a8332 100644 --- a/buteo-plugins/buteo-common/buteo-common.pro +++ b/buteo-plugins/buteo-common/buteo-common.pro @@ -4,7 +4,7 @@ TEMPLATE = lib -TARGET = mastodonbuteocommon +TARGET = fediversebuteocommon TARGET = $$qtLibraryTarget($$TARGET) QT -= gui diff --git a/buteo-plugins/buteo-plugins.pro b/buteo-plugins/buteo-plugins.pro index ead60e6..7938956 100644 --- a/buteo-plugins/buteo-plugins.pro +++ b/buteo-plugins/buteo-plugins.pro @@ -5,8 +5,8 @@ TEMPLATE = subdirs SUBDIRS += \ buteo-common \ - buteo-sync-plugin-mastodon-posts \ - buteo-sync-plugin-mastodon-notifications + buteo-sync-plugin-fediverse-posts \ + buteo-sync-plugin-fediverse-notifications -buteo-sync-plugin-mastodon-posts.depends = buteo-common -buteo-sync-plugin-mastodon-notifications.depends = buteo-common +buteo-sync-plugin-fediverse-posts.depends = buteo-common +buteo-sync-plugin-fediverse-notifications.depends = buteo-common diff --git a/buteo-plugins/buteo-sync-plugin-fediverse-notifications/buteo-sync-plugin-fediverse-notifications.pro b/buteo-plugins/buteo-sync-plugin-fediverse-notifications/buteo-sync-plugin-fediverse-notifications.pro new file mode 100644 index 0000000..0ba9d2f --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-fediverse-notifications/buteo-sync-plugin-fediverse-notifications.pro @@ -0,0 +1,66 @@ +# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. +# +# SPDX-License-Identifier: BSD-3-Clause + +TARGET = fediverse-notifications-client + +QT -= gui + +include($$PWD/../buteo-common/buteo-common.pri) +include($$PWD/../../common/common.pri) + +TS_FILE = $$OUT_PWD/lipstick-jolla-home-fediverse-notifications.ts +EE_QM = $$OUT_PWD/lipstick-jolla-home-fediverse-notifications_eng_en.qm + +ts.commands += lupdate $$PWD -ts $$TS_FILE +ts.CONFIG += no_check_exist no_link +ts.output = $$TS_FILE +ts.input = . + +ts_install.files = $$TS_FILE +ts_install.path = /usr/share/translations/source +ts_install.CONFIG += no_check_exist + +engineering_english.commands += lrelease -idbased $$TS_FILE -qm $$EE_QM +engineering_english.CONFIG += no_check_exist no_link +engineering_english.depends = ts +engineering_english.input = $$TS_FILE +engineering_english.output = $$EE_QM + +engineering_english_install.path = /usr/share/translations +engineering_english_install.files = $$EE_QM +engineering_english_install.CONFIG += no_check_exist + +QMAKE_EXTRA_TARGETS += ts engineering_english +PRE_TARGETDEPS += ts engineering_english + +CONFIG += link_pkgconfig +PKGCONFIG += mlite5 nemonotifications-qt5 + +INCLUDEPATH += $$PWD + +SOURCES += \ + $$PWD/fediversedatatypesyncadaptor.cpp \ + $$PWD/fediversenotificationsplugin.cpp \ + $$PWD/fediversenotificationssyncadaptor.cpp + +HEADERS += \ + $$PWD/fediversedatatypesyncadaptor.h \ + $$PWD/fediversenotificationsplugin.h \ + $$PWD/fediversenotificationssyncadaptor.h + +OTHER_FILES += \ + $$PWD/fediverse-notifications.xml \ + $$PWD/fediverse.Notifications.xml + +TEMPLATE = lib +CONFIG += plugin +target.path = $$[QT_INSTALL_LIBS]/buteo-plugins-qt5/oopp + +sync.path = /etc/buteo/profiles/sync +sync.files = fediverse.Notifications.xml + +client.path = /etc/buteo/profiles/client +client.files = fediverse-notifications.xml + +INSTALLS += target sync client ts_install engineering_english_install diff --git a/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediverse-notifications.xml b/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediverse-notifications.xml new file mode 100644 index 0000000..81de349 --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediverse-notifications.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediverse.Notifications.xml b/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediverse.Notifications.xml new file mode 100644 index 0000000..bf0ecee --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediverse.Notifications.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversedatatypesyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversedatatypesyncadaptor.cpp new file mode 100644 index 0000000..3d71585 --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-fediverse-notifications/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(lcFediverseNotificationsSync, "buteo.plugin.fediverse.notifications.sync", QtWarningMsg) + +FediverseNotificationsDataTypeSyncAdaptor::FediverseNotificationsDataTypeSyncAdaptor( + SocialNetworkSyncAdaptor::DataType dataType, + QObject *parent) + : SocialNetworkSyncAdaptor(QStringLiteral("fediverse"), dataType, 0, parent) +{ +} + +FediverseNotificationsDataTypeSyncAdaptor::~FediverseNotificationsDataTypeSyncAdaptor() +{ +} + +void FediverseNotificationsDataTypeSyncAdaptor::sync(const QString &dataTypeString, int accountId) +{ + if (dataTypeString != SocialNetworkSyncAdaptor::dataTypeName(m_dataType)) { + qCWarning(lcFediverseNotificationsSync) << "Fediverse" << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) + << "sync adaptor was asked to sync" << dataTypeString; + setStatus(SocialNetworkSyncAdaptor::Error); + return; + } + + setStatus(SocialNetworkSyncAdaptor::Busy); + updateDataForAccount(accountId); + qCDebug(lcFediverseNotificationsSync) << "successfully triggered sync with profile:" << m_accountSyncProfile->name(); +} + +void FediverseNotificationsDataTypeSyncAdaptor::updateDataForAccount(int accountId) +{ + Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this); + if (!account) { + qCWarning(lcFediverseNotificationsSync) << "existing account with id" << accountId << "couldn't be retrieved"; + setStatus(SocialNetworkSyncAdaptor::Error); + return; + } + + incrementSemaphore(accountId); + signIn(account); +} + +QString FediverseNotificationsDataTypeSyncAdaptor::apiHost(int accountId) const +{ + return m_apiHosts.value(accountId, FediverseAuthUtils::defaultApiHost()); +} + +QString FediverseNotificationsDataTypeSyncAdaptor::authServiceName() const +{ + return syncServiceName(); +} + +void FediverseNotificationsDataTypeSyncAdaptor::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(lcFediverseNotificationsSync) << 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 FediverseNotificationsDataTypeSyncAdaptor::sslErrorsHandler(const QList &errs) +{ + QString sslerrs; + foreach (const QSslError &e, errs) { + sslerrs += e.errorString() + QLatin1String("; "); + } + if (!sslerrs.isEmpty()) { + sslerrs.chop(2); + } + + qCWarning(lcFediverseNotificationsSync) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) + << "request with account" << sender()->property("accountId").toInt() + << "experienced ssl errors:" << sslerrs; + sender()->setProperty("isError", QVariant::fromValue(true)); +} + +void FediverseNotificationsDataTypeSyncAdaptor::setCredentialsNeedUpdate(Accounts::Account *account) +{ + qCInfo(lcFediverseNotificationsSync) << "sociald:Fediverse: setting CredentialsNeedUpdate to true for account:" << account->id(); + Accounts::Service srv(m_accountManager->service(authServiceName())); + 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 FediverseNotificationsDataTypeSyncAdaptor::signIn(Accounts::Account *account) +{ + const int accountId = account->id(); + if (!checkAccount(account)) { + decrementSemaphore(accountId); + return; + } + + Accounts::Service srv(m_accountManager->service(authServiceName())); + account->selectService(srv); + + SignOn::Identity *identity = account->credentialsId() > 0 + ? SignOn::Identity::existingIdentity(account->credentialsId()) + : 0; + if (!identity) { + qCWarning(lcFediverseNotificationsSync) << "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(lcFediverseNotificationsSync) << "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 FediverseNotificationsDataTypeSyncAdaptor::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(lcFediverseNotificationsSync) << "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 FediverseNotificationsDataTypeSyncAdaptor::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(lcFediverseNotificationsSync) << "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())); + + 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-notifications/fediversedatatypesyncadaptor.h b/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversedatatypesyncadaptor.h new file mode 100644 index 0000000..0acbc87 --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversedatatypesyncadaptor.h @@ -0,0 +1,70 @@ +/**************************************************************************** + ** + ** 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 FEDIVERSENOTIFICATIONSDATATYPESYNCADAPTOR_H +#define FEDIVERSENOTIFICATIONSDATATYPESYNCADAPTOR_H + +#include "socialnetworksyncadaptor.h" + +#include +#include +#include + +namespace Accounts { + class Account; +} +namespace SignOn { + class Error; + class SessionData; +} + +class FediverseNotificationsDataTypeSyncAdaptor : public SocialNetworkSyncAdaptor +{ + Q_OBJECT + +public: + FediverseNotificationsDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent); + virtual ~FediverseNotificationsDataTypeSyncAdaptor(); + + void sync(const QString &dataTypeString, int accountId) override; + +protected: + QString apiHost(int accountId) const; + virtual void updateDataForAccount(int accountId); + virtual QString authServiceName() const; + 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; +}; + +#endif // FEDIVERSENOTIFICATIONSDATATYPESYNCADAPTOR_H diff --git a/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversenotificationsplugin.cpp b/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversenotificationsplugin.cpp new file mode 100644 index 0000000..c518e7e --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversenotificationsplugin.cpp @@ -0,0 +1,96 @@ +/**************************************************************************** + ** + ** 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 "fediversenotificationsplugin.h" +#include "fediversenotificationssyncadaptor.h" +#include "socialnetworksyncadaptor.h" + +#include +#include +#include + +namespace { +class AppTranslator : public QTranslator +{ +public: + explicit AppTranslator(QObject *parent) + : QTranslator(parent) + { + qApp->installTranslator(this); + } + + ~AppTranslator() override + { + qApp->removeTranslator(this); + } +}; + +void ensureNotificationTranslations() +{ + static bool initialized = false; + if (initialized) { + return; + } + + QCoreApplication *app = QCoreApplication::instance(); + if (!app) { + return; + } + + AppTranslator *engineeringEnglish = new AppTranslator(app); + engineeringEnglish->load(QStringLiteral("lipstick-jolla-home-fediverse-notifications_eng_en"), + QStringLiteral("/usr/share/translations")); + + AppTranslator *translator = new AppTranslator(app); + translator->load(QLocale(), + QStringLiteral("lipstick-jolla-home-fediverse-notifications"), + QStringLiteral("-"), + QStringLiteral("/usr/share/translations")); + + initialized = true; +} +} + +FediverseNotificationsPlugin::FediverseNotificationsPlugin(const QString& pluginName, + const Buteo::SyncProfile& profile, + Buteo::PluginCbInterface *callbackInterface) + : SocialdButeoPlugin(pluginName, profile, callbackInterface, + QStringLiteral("fediverse"), + SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Notifications)) +{ + ensureNotificationTranslations(); +} + +FediverseNotificationsPlugin::~FediverseNotificationsPlugin() +{ +} + +SocialNetworkSyncAdaptor *FediverseNotificationsPlugin::createSocialNetworkSyncAdaptor() +{ + return new FediverseNotificationsSyncAdaptor(this); +} + +Buteo::ClientPlugin* FediverseNotificationsPluginLoader::createClientPlugin( + const QString& pluginName, + const Buteo::SyncProfile& profile, + Buteo::PluginCbInterface* cbInterface) +{ + return new FediverseNotificationsPlugin(pluginName, profile, cbInterface); +} diff --git a/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversenotificationsplugin.h b/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversenotificationsplugin.h new file mode 100644 index 0000000..002aeb6 --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversenotificationsplugin.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 FEDIVERSENOTIFICATIONSPLUGIN_H +#define FEDIVERSENOTIFICATIONSPLUGIN_H + +#include "socialdbuteoplugin.h" + +#include + +class Q_DECL_EXPORT FediverseNotificationsPlugin : public SocialdButeoPlugin +{ + Q_OBJECT + +public: + FediverseNotificationsPlugin(const QString& pluginName, + const Buteo::SyncProfile& profile, + Buteo::PluginCbInterface *cbInterface); + ~FediverseNotificationsPlugin(); + +protected: + SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor() override; +}; + +class FediverseNotificationsPluginLoader : public Buteo::SyncPluginLoader +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.FediverseNotificationsPluginLoader") + Q_INTERFACES(Buteo::SyncPluginLoader) + +public: + Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, + const Buteo::SyncProfile& profile, + Buteo::PluginCbInterface* cbInterface) override; +}; + +#endif // FEDIVERSENOTIFICATIONSPLUGIN_H diff --git a/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversenotificationssyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversenotificationssyncadaptor.cpp new file mode 100644 index 0000000..2a84637 --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversenotificationssyncadaptor.cpp @@ -0,0 +1,970 @@ +/**************************************************************************** + ** + ** 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 "fediversenotificationssyncadaptor.h" +#include "fediversetextutils.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +// libaccounts-qt5 +#include +#include +#include + +#include + +#include + +#define OPEN_URL_ACTION(openUrl) \ + Notification::remoteAction( \ + "default", \ + "", \ + "org.sailfishos.fileservice", \ + "/", \ + "org.sailfishos.fileservice", \ + "openUrl", \ + QVariantList() << openUrl \ + ) + +namespace { + Q_LOGGING_CATEGORY(lcFediverseNotifications, "buteo.plugin.fediverse.notifications", QtWarningMsg) + + const char *const NotificationCategory = "x-nemo.social.fediverse.notification"; + const char *const NotificationIdHint = "x-nemo.sociald.notification-id"; + const char *const LastFetchedNotificationIdKey = "LastFetchedNotificationId"; + const int NotificationsPageLimit = 80; + const uint NotificationDismissedReason = 1; + + //% "mentioned you" + const char *const TrIdMentionedYou = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-mentioned_you"); + //% "boosted your post" + const char *const TrIdBoostedYourPost = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-boosted_your_post"); + //% "favourited your post" + const char *const TrIdFavouritedYourPost = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-favourited_your_post"); + //% "started following you" + const char *const TrIdStartedFollowingYou = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-started_following_you"); + //% "requested to follow you" + const char *const TrIdRequestedToFollowYou = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-requested_to_follow_you"); + //% "interacted with your poll" + const char *const TrIdInteractedWithYourPoll = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-interacted_with_your_poll"); + //% "posted" + const char *const TrIdPosted = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-posted"); + //% "updated a post" + const char *const TrIdUpdatedPost = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-updated_post"); + //% "signed up" + const char *const TrIdSignedUp = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-signed_up"); + //% "reported an account" + const char *const TrIdReportedAccount = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-reported_account"); + //% "received a moderation warning" + const char *const TrIdReceivedModerationWarning = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-received_moderation_warning"); + //% "quoted your post" + const char *const TrIdQuotedYourPost = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-quoted_your_post"); + //% "updated a post that quoted you" + const char *const TrIdUpdatedQuotedPost = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-updated_quoted_post"); + //% "sent you a notification" + const char *const TrIdSentNotification = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-sent_notification"); + + //% "An admin blocked an instance" + const char *const TrIdAdminBlockedInstance = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-admin_blocked_instance"); + //% "An admin blocked %1" + const char *const TrIdAdminBlockedTarget = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-admin_blocked_target"); + //% "You blocked an instance" + const char *const TrIdYouBlockedInstance = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-you_blocked_instance"); + //% "You blocked %1" + const char *const TrIdYouBlockedTarget = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-you_blocked_target"); + //% "An account was suspended" + const char *const TrIdAccountSuspended = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-account_suspended"); + //% "%1 was suspended" + const char *const TrIdTargetSuspended = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-target_suspended"); + //% "Some follow relationships were severed" + const char *const TrIdRelationshipsSevered = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-relationships_severed"); + //% "%1 (%2 followers, %3 following removed)" + const char *const TrIdRelationshipsSummary = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-relationships_summary"); + + //% "A moderator sent you a warning" + const char *const TrIdModeratorWarningNone = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-moderator_warning_none"); + //% "A moderator disabled your account" + const char *const TrIdModeratorWarningDisable = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-moderator_warning_disable"); + //% "A moderator marked specific posts as sensitive" + const char *const TrIdModeratorWarningSpecificSensitive = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-moderator_warning_specific_sensitive"); + //% "A moderator deleted specific posts" + const char *const TrIdModeratorWarningDeletePosts = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-moderator_warning_delete_posts"); + //% "A moderator marked all your posts as sensitive" + const char *const TrIdModeratorWarningAllSensitive = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-moderator_warning_all_sensitive"); + //% "A moderator limited your account" + const char *const TrIdModeratorWarningSilence = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-moderator_warning_silence"); + //% "A moderator suspended your account" + const char *const TrIdModeratorWarningSuspend = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-moderator_warning_suspend"); + + //% "Fediverse" + const char *const TrIdFediverse = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-fediverse"); + //% "New notification" + const char *const TrIdNewNotification = QT_TRID_NOOP("lipstick-jolla-home-la-fediverse-notification-new_notification"); + + 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(); + } + + QString actionText(const QString &type) + { + if (type == QLatin1String("mention")) { + return qtTrId(TrIdMentionedYou); + } else if (type == QLatin1String("reblog")) { + return qtTrId(TrIdBoostedYourPost); + } else if (type == QLatin1String("favourite")) { + return qtTrId(TrIdFavouritedYourPost); + } else if (type == QLatin1String("follow")) { + return qtTrId(TrIdStartedFollowingYou); + } else if (type == QLatin1String("follow_request")) { + return qtTrId(TrIdRequestedToFollowYou); + } else if (type == QLatin1String("poll")) { + return qtTrId(TrIdInteractedWithYourPoll); + } else if (type == QLatin1String("status")) { + return qtTrId(TrIdPosted); + } else if (type == QLatin1String("update")) { + return qtTrId(TrIdUpdatedPost); + } else if (type == QLatin1String("admin.sign_up")) { + return qtTrId(TrIdSignedUp); + } else if (type == QLatin1String("admin.report")) { + return qtTrId(TrIdReportedAccount); + } else if (type == QLatin1String("moderation_warning")) { + return qtTrId(TrIdReceivedModerationWarning); + } else if (type == QLatin1String("quote")) { + return qtTrId(TrIdQuotedYourPost); + } else if (type == QLatin1String("quoted_update")) { + return qtTrId(TrIdUpdatedQuotedPost); + } + + return qtTrId(TrIdSentNotification); + } + + bool useSystemSummary(const QString ¬ificationType) + { + return notificationType == QLatin1String("severed_relationships") + || notificationType == QLatin1String("moderation_warning"); + } + + QString severedRelationshipsText(const QJsonObject &eventObject) + { + const QString eventType = eventObject.value(QStringLiteral("type")).toString(); + const QString targetName = eventObject.value(QStringLiteral("target_name")).toString().trimmed(); + const int followersCount = eventObject.value(QStringLiteral("followers_count")).toInt(); + const int followingCount = eventObject.value(QStringLiteral("following_count")).toInt(); + + QString action; + if (eventType == QLatin1String("domain_block")) { + action = targetName.isEmpty() + ? qtTrId(TrIdAdminBlockedInstance) + : qtTrId(TrIdAdminBlockedTarget).arg(targetName); + } else if (eventType == QLatin1String("user_domain_block")) { + action = targetName.isEmpty() + ? qtTrId(TrIdYouBlockedInstance) + : qtTrId(TrIdYouBlockedTarget).arg(targetName); + } else if (eventType == QLatin1String("account_suspension")) { + action = targetName.isEmpty() + ? qtTrId(TrIdAccountSuspended) + : qtTrId(TrIdTargetSuspended).arg(targetName); + } else { + action = qtTrId(TrIdRelationshipsSevered); + } + + const int affectedCount = followersCount + followingCount; + if (affectedCount <= 0) { + return action; + } + + return qtTrId(TrIdRelationshipsSummary) + .arg(action) + .arg(followersCount) + .arg(followingCount); + } + + QString moderationWarningText(const QJsonObject &warningObject) + { + const QString warningText = warningObject.value(QStringLiteral("text")).toString().trimmed(); + if (!warningText.isEmpty()) { + return warningText; + } + + const QString action = warningObject.value(QStringLiteral("action")).toString(); + if (action == QLatin1String("none")) { + return qtTrId(TrIdModeratorWarningNone); + } else if (action == QLatin1String("disable")) { + return qtTrId(TrIdModeratorWarningDisable); + } else if (action == QLatin1String("mark_statuses_as_sensitive")) { + return qtTrId(TrIdModeratorWarningSpecificSensitive); + } else if (action == QLatin1String("delete_statuses")) { + return qtTrId(TrIdModeratorWarningDeletePosts); + } else if (action == QLatin1String("sensitive")) { + return qtTrId(TrIdModeratorWarningAllSensitive); + } else if (action == QLatin1String("silence")) { + return qtTrId(TrIdModeratorWarningSilence); + } else if (action == QLatin1String("suspend")) { + return qtTrId(TrIdModeratorWarningSuspend); + } + + return QString(); + } + + bool hasActiveNotificationsForAccount(int accountId, const Notification *ignoredNotification = 0) + { + bool hasActiveNotifications = false; + const QList notifications = Notification::notifications(); + foreach (QObject *object, notifications) { + Notification *notification = qobject_cast(object); + if (notification + && notification != ignoredNotification + && notification->category() == QLatin1String(NotificationCategory) + && notification->hintValue("x-nemo.sociald.account-id").toInt() == accountId) { + hasActiveNotifications = true; + } + + delete object; + } + + return hasActiveNotifications; + } + + QString authorizeInteractionUrl(const QString &apiHost, const QString &targetUrl) + { + const QUrl parsedApiHost(apiHost); + const QUrl parsedTargetUrl(targetUrl); + if (!parsedApiHost.isValid() + || parsedApiHost.scheme().isEmpty() + || parsedApiHost.host().isEmpty() + || !parsedTargetUrl.isValid() + || parsedTargetUrl.scheme().isEmpty() + || parsedTargetUrl.host().isEmpty()) { + return targetUrl; + } + + // Links on the account's own instance should open directly. + const bool sameScheme = QString::compare(parsedApiHost.scheme(), parsedTargetUrl.scheme(), Qt::CaseInsensitive) == 0; + const bool sameHost = QString::compare(parsedApiHost.host(), parsedTargetUrl.host(), Qt::CaseInsensitive) == 0; + const int apiPort = parsedApiHost.port(parsedApiHost.scheme() == QLatin1String("https") ? 443 : 80); + const int targetPort = parsedTargetUrl.port(parsedTargetUrl.scheme() == QLatin1String("https") ? 443 : 80); + if (sameScheme && sameHost && apiPort == targetPort) { + return targetUrl; + } + + QUrl authorizeUrl(parsedApiHost); + authorizeUrl.setPath(QStringLiteral("/authorize_interaction")); + authorizeUrl.setQuery(QStringLiteral("uri=") + QString::fromUtf8(QUrl::toPercentEncoding(targetUrl))); + return authorizeUrl.toString(); + } + +} + +FediverseNotificationsSyncAdaptor::FediverseNotificationsSyncAdaptor(QObject *parent) + : FediverseNotificationsDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Notifications, parent) +{ + setInitialActive(true); +} + +FediverseNotificationsSyncAdaptor::~FediverseNotificationsSyncAdaptor() +{ +} + +QString FediverseNotificationsSyncAdaptor::syncServiceName() const +{ + return QStringLiteral("fediverse-notifications"); +} + +QString FediverseNotificationsSyncAdaptor::authServiceName() const +{ + return QStringLiteral("fediverse-microblog"); +} + +void FediverseNotificationsSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode) +{ + closeAccountNotifications(oldId); + + m_accessTokens.remove(oldId); + m_pendingSyncStates.remove(oldId); + m_lastMarkedReadIds.remove(oldId); + saveLastFetchedId(oldId, QString()); +} + +void FediverseNotificationsSyncAdaptor::beginSync(int accountId, const QString &accessToken) +{ + m_accessTokens.insert(accountId, accessToken); + m_pendingSyncStates.remove(accountId); + requestUnreadMarker(accountId, accessToken); +} + +void FediverseNotificationsSyncAdaptor::finalize(int accountId) +{ + if (syncAborted()) { + qCInfo(lcFediverseNotifications) << "sync aborted, won't update notifications"; + } + + Q_UNUSED(accountId) +} + +QString FediverseNotificationsSyncAdaptor::sanitizeContent(const QString &content) +{ + return FediverseTextUtils::sanitizeContent(content); +} + +QDateTime FediverseNotificationsSyncAdaptor::parseTimestamp(const QString ×tampString) +{ + return FediverseTextUtils::parseTimestamp(timestampString); +} + +int FediverseNotificationsSyncAdaptor::compareNotificationIds(const QString &left, const QString &right) +{ + if (left == right) { + return 0; + } + + bool leftOk = false; + bool rightOk = false; + const qulonglong leftValue = left.toULongLong(&leftOk); + const qulonglong rightValue = right.toULongLong(&rightOk); + if (leftOk && rightOk) { + return leftValue < rightValue ? -1 : 1; + } + + if (left.size() != right.size()) { + return left.size() < right.size() ? -1 : 1; + } + return left < right ? -1 : 1; +} + +QString FediverseNotificationsSyncAdaptor::notificationObjectKey(int accountId, const QString ¬ificationId) +{ + return QString::number(accountId) + QLatin1Char(':') + notificationId; +} + +QString FediverseNotificationsSyncAdaptor::loadLastFetchedId(int accountId) const +{ + Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, 0); + if (!account) { + return QString(); + } + + Accounts::Service service(m_accountManager->service(syncServiceName())); + account->selectService(service); + const QString lastFetchedId = account->value(QString::fromLatin1(LastFetchedNotificationIdKey)).toString().trimmed(); + account->deleteLater(); + + return lastFetchedId; +} + +void FediverseNotificationsSyncAdaptor::saveLastFetchedId(int accountId, const QString &lastFetchedId) +{ + Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, 0); + if (!account) { + return; + } + + Accounts::Service service(m_accountManager->service(syncServiceName())); + account->selectService(service); + const QString storedId = account->value(QString::fromLatin1(LastFetchedNotificationIdKey)).toString().trimmed(); + if (storedId != lastFetchedId) { + account->setValue(QString::fromLatin1(LastFetchedNotificationIdKey), lastFetchedId); + account->syncAndBlock(); + } + + account->deleteLater(); +} + +void FediverseNotificationsSyncAdaptor::requestUnreadMarker(int accountId, const QString &accessToken) +{ + QUrl url(apiHost(accountId) + QStringLiteral("/api/v1/markers")); + + QUrlQuery query(url); + query.addQueryItem(QStringLiteral("timeline[]"), QStringLiteral("notifications")); + 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); + reply->setProperty("accessToken", accessToken); + 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(finishedUnreadMarkerHandler())); + + incrementSemaphore(accountId); + setupReplyTimeout(accountId, reply); + } else { + qCWarning(lcFediverseNotifications) << "unable to request notifications marker from Fediverse account with id" << accountId; + } +} + +void FediverseNotificationsSyncAdaptor::finishedUnreadMarkerHandler() +{ + QNetworkReply *reply = qobject_cast(sender()); + if (!reply) { + return; + } + + const bool isError = reply->property("isError").toBool(); + const int accountId = reply->property("accountId").toInt(); + const QString accessToken = reply->property("accessToken").toString(); + const QByteArray replyData = reply->readAll(); + + disconnect(reply); + reply->deleteLater(); + removeReplyTimeout(accountId, reply); + + bool ok = false; + const QJsonObject markerObject = parseJsonObjectReplyData(replyData, &ok); + if (isError || !ok) { + qCWarning(lcFediverseNotifications) << "unable to parse notifications marker data from request with account" + << accountId << ", got:" << QString::fromUtf8(replyData); + PendingSyncState fallbackState; + fallbackState.accessToken = accessToken; + fallbackState.lastFetchedId = loadLastFetchedId(accountId); + m_pendingSyncStates.insert(accountId, fallbackState); + requestNotifications(accountId, accessToken, fallbackState.lastFetchedId); + decrementSemaphore(accountId); + return; + } + + const QString markerId = markerObject.value(QStringLiteral("notifications")) + .toObject() + .value(QStringLiteral("last_read_id")) + .toVariant() + .toString() + .trimmed(); + + PendingSyncState state; + state.accessToken = accessToken; + state.markerKnown = true; + state.unreadFloorId = markerId; + state.lastFetchedId = loadLastFetchedId(accountId); + if (state.lastFetchedId.isEmpty() && !markerId.isEmpty()) { + // On first run, use the server unread marker floor to avoid historical flood. + state.lastFetchedId = markerId; + } + m_pendingSyncStates.insert(accountId, state); + requestNotifications(accountId, accessToken, markerId); + + decrementSemaphore(accountId); +} + +void FediverseNotificationsSyncAdaptor::requestNotifications(int accountId, + const QString &accessToken, + const QString &minId, + const QString &maxId) +{ + QUrl url(apiHost(accountId) + QStringLiteral("/api/v1/notifications")); + + QUrlQuery query(url); + query.addQueryItem(QStringLiteral("limit"), QString::number(NotificationsPageLimit)); + if (!minId.isEmpty()) { + query.addQueryItem(QStringLiteral("min_id"), minId); + } + if (!maxId.isEmpty()) { + query.addQueryItem(QStringLiteral("max_id"), maxId); + } + 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); + reply->setProperty("accessToken", accessToken); + reply->setProperty("minId", minId); + 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(finishedNotificationsHandler())); + + incrementSemaphore(accountId); + setupReplyTimeout(accountId, reply); + } else { + qCWarning(lcFediverseNotifications) << "unable to request notifications from Fediverse account with id" << accountId; + } +} + +void FediverseNotificationsSyncAdaptor::requestMarkRead(int accountId, + const QString &accessToken, + const QString &lastReadId) +{ + QUrl url(apiHost(accountId) + QStringLiteral("/api/v1/markers")); + QNetworkRequest request(url); + request.setRawHeader("Authorization", QStringLiteral("Bearer %1").arg(accessToken).toUtf8()); + request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded")); + + QUrlQuery query; + query.addQueryItem(QStringLiteral("notifications[last_read_id]"), lastReadId); + const QByteArray payload = query.toString(QUrl::FullyEncoded).toUtf8(); + + QNetworkReply *reply = m_networkAccessManager->post(request, payload); + if (reply) { + reply->setProperty("accountId", accountId); + reply->setProperty("lastReadId", lastReadId); + 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(finishedMarkReadHandler())); + + incrementSemaphore(accountId); + setupReplyTimeout(accountId, reply); + } else { + qCWarning(lcFediverseNotifications) << "unable to update notifications marker for Fediverse account with id" << accountId; + } +} + +void FediverseNotificationsSyncAdaptor::finishedNotificationsHandler() +{ + QNetworkReply *reply = qobject_cast(sender()); + if (!reply) { + return; + } + + const bool isError = reply->property("isError").toBool(); + const int accountId = reply->property("accountId").toInt(); + const QString accessToken = reply->property("accessToken").toString(); + const QString minId = reply->property("minId").toString(); + const QByteArray replyData = reply->readAll(); + + disconnect(reply); + reply->deleteLater(); + removeReplyTimeout(accountId, reply); + + PendingSyncState state = m_pendingSyncStates.value(accountId); + if (state.accessToken.isEmpty()) { + state.accessToken = accessToken; + } + if (state.unreadFloorId.isEmpty() && !minId.isEmpty()) { + state.unreadFloorId = minId; + } + if (state.lastFetchedId.isEmpty() && !state.unreadFloorId.isEmpty()) { + state.lastFetchedId = state.unreadFloorId; + } else if (state.lastFetchedId.isEmpty() && !minId.isEmpty()) { + state.lastFetchedId = minId; + } + + bool ok = false; + const QJsonArray notifications = parseJsonArrayReplyData(replyData, &ok); + if (!isError && ok) { + if (!notifications.size()) { + qCDebug(lcFediverseNotifications) << "no notifications received for account" << accountId; + if (state.markerKnown) { + closeAccountNotifications(accountId); + } + m_pendingSyncStates.remove(accountId); + decrementSemaphore(accountId); + return; + } + + QString pageMinNotificationId; + + foreach (const QJsonValue ¬ificationValue, notifications) { + const QJsonObject notificationObject = notificationValue.toObject(); + if (notificationObject.isEmpty()) { + continue; + } + + const QString notificationId = notificationObject.value(QStringLiteral("id")).toVariant().toString(); + if (notificationId.isEmpty()) { + continue; + } + + if (pageMinNotificationId.isEmpty() + || compareNotificationIds(notificationId, pageMinNotificationId) < 0) { + pageMinNotificationId = notificationId; + } + + if (state.markerKnown) { + state.unreadNotificationIds.insert(notificationId); + } + + if (state.maxFetchedId.isEmpty() + || compareNotificationIds(notificationId, state.maxFetchedId) > 0) { + state.maxFetchedId = notificationId; + } + + if (!state.lastFetchedId.isEmpty() + && compareNotificationIds(notificationId, state.lastFetchedId) <= 0) { + continue; + } + + const QString notificationType = notificationObject.value(QStringLiteral("type")).toString(); + const QJsonObject actorObject = notificationObject.value(QStringLiteral("account")).toObject(); + const QJsonValue statusValue = notificationObject.value(QStringLiteral("status")); + const QJsonObject statusObject = statusValue.isObject() && !statusValue.isNull() + ? statusValue.toObject() + : QJsonObject(); + const QJsonObject eventObject = notificationObject.value(QStringLiteral("event")).toObject(); + const QJsonObject warningObject = notificationObject.value(QStringLiteral("moderation_warning")).toObject(); + + QDateTime eventTimestamp = parseTimestamp(notificationObject.value(QStringLiteral("created_at")).toString()); + if (!eventTimestamp.isValid()) { + eventTimestamp = parseTimestamp(statusObject.value(QStringLiteral("created_at")).toString()); + } + if (!eventTimestamp.isValid()) { + continue; + } + + const QString displayName = displayNameForAccount(actorObject); + const QString accountName = actorObject.value(QStringLiteral("acct")).toString(); + + const QString statusBody = sanitizeContent(statusObject.value(QStringLiteral("content")).toString()); + const QString action = actionText(notificationType); + QString body; + if (notificationType == QLatin1String("severed_relationships")) { + body = severedRelationshipsText(eventObject); + } else if (notificationType == QLatin1String("moderation_warning")) { + const QString warningText = moderationWarningText(warningObject); + body = warningText.isEmpty() + ? action + : QStringLiteral("%1: %2").arg(action, warningText); + } else if (notificationType == QLatin1String("mention") + || notificationType == QLatin1String("status") + || notificationType == QLatin1String("update") + || notificationType == QLatin1String("quote") + || notificationType == QLatin1String("quoted_update")) { + body = statusBody.isEmpty() ? action : statusBody; + } else { + body = statusBody.isEmpty() ? action : QStringLiteral("%1: %2").arg(action, statusBody); + } + + const QString statusId = statusObject.value(QStringLiteral("id")).toVariant().toString(); + + QString url = statusObject.value(QStringLiteral("url")).toString(); + if (url.isEmpty()) { + url = statusObject.value(QStringLiteral("uri")).toString(); + } + if (url.isEmpty()) { + url = actorObject.value(QStringLiteral("url")).toString(); + } + if (url.isEmpty() && !accountName.isEmpty() && !statusId.isEmpty()) { + url = QStringLiteral("%1/@%2/%3").arg(apiHost(accountId), accountName, statusId); + } else if (url.isEmpty() && !accountName.isEmpty()) { + url = QStringLiteral("%1/@%2").arg(apiHost(accountId), accountName); + } + if (useSystemSummary(notificationType)) { + url.clear(); + } + + PendingNotification pendingNotification; + pendingNotification.notificationId = notificationId; + pendingNotification.summary = useSystemSummary(notificationType) + ? qtTrId(TrIdFediverse) + : displayName; + pendingNotification.body = body; + pendingNotification.link = url; + pendingNotification.timestamp = eventTimestamp; + state.pendingNotifications.insert(notificationId, pendingNotification); + } + + const QString historyBoundaryId = !state.unreadFloorId.isEmpty() + ? state.unreadFloorId + : state.lastFetchedId; + if (notifications.size() >= NotificationsPageLimit + && !pageMinNotificationId.isEmpty() + && !historyBoundaryId.isEmpty() + && compareNotificationIds(pageMinNotificationId, historyBoundaryId) > 0) { + m_pendingSyncStates.insert(accountId, state); + requestNotifications(accountId, state.accessToken, historyBoundaryId, pageMinNotificationId); + decrementSemaphore(accountId); + return; + } + + if (state.pendingNotifications.size() > 0) { + QStringList notificationIds = state.pendingNotifications.keys(); + std::sort(notificationIds.begin(), notificationIds.end(), [](const QString &left, const QString &right) { + return compareNotificationIds(left, right) > 0; + }); + + foreach (const QString ¬ificationId, notificationIds) { + const PendingNotification pendingNotification = state.pendingNotifications.value(notificationId); + publishSystemNotification(accountId, pendingNotification); + } + } + + if (state.markerKnown) { + closeAccountNotifications(accountId, state.unreadNotificationIds); + } + + if (!state.maxFetchedId.isEmpty() + && (state.lastFetchedId.isEmpty() + || compareNotificationIds(state.maxFetchedId, state.lastFetchedId) > 0)) { + saveLastFetchedId(accountId, state.maxFetchedId); + } + + const QString markerId = !state.maxFetchedId.isEmpty() + ? state.maxFetchedId + : state.lastFetchedId; + const QString currentMarkerId = m_lastMarkedReadIds.value(accountId); + if (!markerId.isEmpty() + && !state.accessToken.isEmpty() + && state.markerKnown + && (currentMarkerId.isEmpty() + || compareNotificationIds(markerId, currentMarkerId) > 0)) { + maybeMarkAccountNotificationsRead(accountId, state.accessToken); + } + } else { + qCWarning(lcFediverseNotifications) << "unable to parse notifications data from request with account" << accountId + << ", got:" << QString::fromUtf8(replyData); + } + + m_pendingSyncStates.remove(accountId); + decrementSemaphore(accountId); +} + +void FediverseNotificationsSyncAdaptor::finishedMarkReadHandler() +{ + QNetworkReply *reply = qobject_cast(sender()); + if (!reply) { + return; + } + + const bool isError = reply->property("isError").toBool(); + const int accountId = reply->property("accountId").toInt(); + const QString lastReadId = reply->property("lastReadId").toString(); + const QByteArray replyData = reply->readAll(); + + disconnect(reply); + reply->deleteLater(); + removeReplyTimeout(accountId, reply); + + bool ok = false; + parseJsonObjectReplyData(replyData, &ok); + if (!isError && ok) { + const QString currentMarkerId = m_lastMarkedReadIds.value(accountId); + if (currentMarkerId.isEmpty() || compareNotificationIds(lastReadId, currentMarkerId) > 0) { + m_lastMarkedReadIds.insert(accountId, lastReadId); + } + } else { + qCWarning(lcFediverseNotifications) << "unable to update notifications marker for account" << accountId + << ", got:" << QString::fromUtf8(replyData); + } + + decrementSemaphore(accountId); +} + +void FediverseNotificationsSyncAdaptor::publishSystemNotification(int accountId, + const PendingNotification ¬ificationData) +{ + Notification *notification = createNotification(accountId, notificationData.notificationId); + notification->setItemCount(1); + notification->setTimestamp(notificationData.timestamp.isValid() + ? notificationData.timestamp + : QDateTime::currentDateTimeUtc()); + notification->setSummary(notificationData.summary.isEmpty() + ? qtTrId(TrIdFediverse) + : notificationData.summary); + notification->setBody(notificationData.body.isEmpty() + ? qtTrId(TrIdNewNotification) + : notificationData.body); + notification->setPreviewSummary(notificationData.summary); + notification->setPreviewBody(notificationData.body); + + const QString openUrl = notificationData.link.isEmpty() + ? apiHost(accountId) + QStringLiteral("/notifications") + : notificationData.link; + const QUrl parsedOpenUrl(openUrl); + const QString fallbackUrl = apiHost(accountId) + QStringLiteral("/notifications"); + const QString safeOpenUrl = parsedOpenUrl.isValid() + && !parsedOpenUrl.scheme().isEmpty() + && !parsedOpenUrl.host().isEmpty() + ? openUrl + : fallbackUrl; + notification->setRemoteAction(OPEN_URL_ACTION(authorizeInteractionUrl(apiHost(accountId), safeOpenUrl))); + notification->publish(); + if (notification->replacesId() == 0) { + qCWarning(lcFediverseNotifications) << "failed to publish Fediverse notification" + << notificationData.notificationId; + } +} + +void FediverseNotificationsSyncAdaptor::notificationClosedWithReason(uint reason) +{ + Notification *notification = qobject_cast(sender()); + removeCachedNotification(notification); + if (reason == NotificationDismissedReason) { + markReadFromNotification(notification); + } +} + +void FediverseNotificationsSyncAdaptor::maybeMarkAccountNotificationsRead(int accountId, + const QString &accessToken, + Notification *ignoredNotification) +{ + if (accountId <= 0 || accessToken.isEmpty()) { + return; + } + + if (hasActiveNotificationsForAccount(accountId, ignoredNotification)) { + return; + } + + const QString lastReadId = loadLastFetchedId(accountId); + if (lastReadId.isEmpty()) { + return; + } + + const QString currentMarkerId = m_lastMarkedReadIds.value(accountId); + if (!currentMarkerId.isEmpty() && compareNotificationIds(lastReadId, currentMarkerId) <= 0) { + return; + } + + requestMarkRead(accountId, accessToken, lastReadId); +} + +void FediverseNotificationsSyncAdaptor::markReadFromNotification(Notification *notification) +{ + if (!notification) { + return; + } + + const int accountId = notification->hintValue("x-nemo.sociald.account-id").toInt(); + const QString accessToken = m_accessTokens.value(accountId).trimmed(); + if (accountId <= 0 || accessToken.isEmpty()) { + return; + } + + maybeMarkAccountNotificationsRead(accountId, accessToken, notification); +} + +void FediverseNotificationsSyncAdaptor::removeCachedNotification(Notification *notification) +{ + if (!notification) { + return; + } + + const int accountId = notification->hintValue("x-nemo.sociald.account-id").toInt(); + const QString notificationId = notification->hintValue(NotificationIdHint).toString(); + if (accountId <= 0 || notificationId.isEmpty()) { + return; + } + + m_notificationObjects.remove(notificationObjectKey(accountId, notificationId)); +} + +void FediverseNotificationsSyncAdaptor::closeAccountNotifications(int accountId, + const QSet &keepNotificationIds) +{ + QStringList cachedKeys = m_notificationObjects.keys(); + foreach (const QString &objectKey, cachedKeys) { + Notification *notification = m_notificationObjects.value(objectKey); + if (!notification + || notification->hintValue("x-nemo.sociald.account-id").toInt() != accountId) { + continue; + } + + const QString notificationId = notification->hintValue(NotificationIdHint).toString(); + if (!notificationId.isEmpty() && keepNotificationIds.contains(notificationId)) { + continue; + } + + notification->close(); + m_notificationObjects.remove(objectKey); + notification->deleteLater(); + } + + QList notifications = Notification::notifications(); + foreach (QObject *object, notifications) { + Notification *notification = qobject_cast(object); + if (!notification) { + delete object; + continue; + } + + if (notification->category() == QLatin1String(NotificationCategory) + && notification->hintValue("x-nemo.sociald.account-id").toInt() == accountId) { + const QString notificationId = notification->hintValue(NotificationIdHint).toString(); + if (notificationId.isEmpty() || !keepNotificationIds.contains(notificationId)) { + notification->close(); + } + } + + if (notification->parent() != this) { + delete notification; + } + } +} + +Notification *FediverseNotificationsSyncAdaptor::createNotification(int accountId, const QString ¬ificationId) +{ + const QString objectKey = notificationObjectKey(accountId, notificationId); + Notification *notification = m_notificationObjects.value(objectKey); + if (!notification) { + notification = findNotification(accountId, notificationId); + } + if (!notification) { + notification = new Notification(this); + } else if (notification->parent() != this) { + notification->setParent(this); + } + + notification->setAppName(QStringLiteral("Fediverse")); + notification->setAppIcon(QStringLiteral("icon-l-fediverse")); + notification->setHintValue("x-nemo.sociald.account-id", accountId); + notification->setHintValue(NotificationIdHint, notificationId); + notification->setHintValue("x-nemo-feedback", QStringLiteral("social")); + notification->setHintValue("x-nemo-priority", 100); // Show on lockscreen + notification->setCategory(QLatin1String(NotificationCategory)); + + connect(notification, SIGNAL(closed(uint)), this, SLOT(notificationClosedWithReason(uint)), Qt::UniqueConnection); + m_notificationObjects.insert(objectKey, notification); + + return notification; +} + +Notification *FediverseNotificationsSyncAdaptor::findNotification(int accountId, const QString ¬ificationId) +{ + Notification *notification = 0; + QList notifications = Notification::notifications(); + foreach (QObject *object, notifications) { + Notification *castedNotification = qobject_cast(object); + if (castedNotification + && castedNotification->category() == QLatin1String(NotificationCategory) + && castedNotification->hintValue("x-nemo.sociald.account-id").toInt() == accountId + && castedNotification->hintValue(NotificationIdHint).toString() == notificationId) { + notification = castedNotification; + break; + } + } + + if (notification) { + notifications.removeAll(notification); + } + + qDeleteAll(notifications); + + return notification; +} diff --git a/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversenotificationssyncadaptor.h b/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversenotificationssyncadaptor.h new file mode 100644 index 0000000..24f2745 --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-fediverse-notifications/fediversenotificationssyncadaptor.h @@ -0,0 +1,104 @@ +/**************************************************************************** + ** + ** 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 FEDIVERSENOTIFICATIONSSYNCADAPTOR_H +#define FEDIVERSENOTIFICATIONSSYNCADAPTOR_H + +#include "fediversedatatypesyncadaptor.h" + +#include +#include +#include +#include + +class Notification; + +class FediverseNotificationsSyncAdaptor : public FediverseNotificationsDataTypeSyncAdaptor +{ + Q_OBJECT + +public: + FediverseNotificationsSyncAdaptor(QObject *parent); + ~FediverseNotificationsSyncAdaptor(); + + QString syncServiceName() const override; + +protected: + QString authServiceName() const override; + void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode) override; + void beginSync(int accountId, const QString &accessToken) override; + void finalize(int accountId) override; + +private: + struct PendingNotification { + QString notificationId; + QString summary; + QString body; + QString link; + QDateTime timestamp; + }; + + struct PendingSyncState { + QString accessToken; + bool markerKnown = false; + QString unreadFloorId; + QString lastFetchedId; + QString maxFetchedId; + QSet unreadNotificationIds; + QHash pendingNotifications; + }; + + static QString sanitizeContent(const QString &content); + static QDateTime parseTimestamp(const QString ×tampString); + static int compareNotificationIds(const QString &left, const QString &right); + QString loadLastFetchedId(int accountId) const; + void saveLastFetchedId(int accountId, const QString &lastFetchedId); + + void requestUnreadMarker(int accountId, const QString &accessToken); + void requestNotifications(int accountId, + const QString &accessToken, + const QString &minId, + const QString &maxId = QString()); + void requestMarkRead(int accountId, const QString &accessToken, const QString &lastReadId); + void publishSystemNotification(int accountId, const PendingNotification ¬ificationData); + Notification *createNotification(int accountId, const QString ¬ificationId); + Notification *findNotification(int accountId, const QString ¬ificationId); + void closeAccountNotifications(int accountId, const QSet &keepNotificationIds = QSet()); + static QString notificationObjectKey(int accountId, const QString ¬ificationId); + void maybeMarkAccountNotificationsRead(int accountId, + const QString &accessToken, + Notification *ignoredNotification = 0); + void markReadFromNotification(Notification *notification); + void removeCachedNotification(Notification *notification); + +private Q_SLOTS: + void finishedUnreadMarkerHandler(); + void finishedNotificationsHandler(); + void finishedMarkReadHandler(); + void notificationClosedWithReason(uint reason); + +private: + QHash m_accessTokens; + QHash m_pendingSyncStates; + QHash m_lastMarkedReadIds; + QHash m_notificationObjects; +}; + +#endif // FEDIVERSENOTIFICATIONSSYNCADAPTOR_H 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 diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/buteo-sync-plugin-mastodon-notifications.pro b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/buteo-sync-plugin-mastodon-notifications.pro deleted file mode 100644 index d16cc3d..0000000 --- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/buteo-sync-plugin-mastodon-notifications.pro +++ /dev/null @@ -1,66 +0,0 @@ -# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. -# -# SPDX-License-Identifier: BSD-3-Clause - -TARGET = mastodon-notifications-client - -QT -= gui - -include($$PWD/../buteo-common/buteo-common.pri) -include($$PWD/../../common/common.pri) - -TS_FILE = $$OUT_PWD/lipstick-jolla-home-mastodon-notifications.ts -EE_QM = $$OUT_PWD/lipstick-jolla-home-mastodon-notifications_eng_en.qm - -ts.commands += lupdate $$PWD -ts $$TS_FILE -ts.CONFIG += no_check_exist no_link -ts.output = $$TS_FILE -ts.input = . - -ts_install.files = $$TS_FILE -ts_install.path = /usr/share/translations/source -ts_install.CONFIG += no_check_exist - -engineering_english.commands += lrelease -idbased $$TS_FILE -qm $$EE_QM -engineering_english.CONFIG += no_check_exist no_link -engineering_english.depends = ts -engineering_english.input = $$TS_FILE -engineering_english.output = $$EE_QM - -engineering_english_install.path = /usr/share/translations -engineering_english_install.files = $$EE_QM -engineering_english_install.CONFIG += no_check_exist - -QMAKE_EXTRA_TARGETS += ts engineering_english -PRE_TARGETDEPS += ts engineering_english - -CONFIG += link_pkgconfig -PKGCONFIG += mlite5 nemonotifications-qt5 - -INCLUDEPATH += $$PWD - -SOURCES += \ - $$PWD/mastodondatatypesyncadaptor.cpp \ - $$PWD/mastodonnotificationsplugin.cpp \ - $$PWD/mastodonnotificationssyncadaptor.cpp - -HEADERS += \ - $$PWD/mastodondatatypesyncadaptor.h \ - $$PWD/mastodonnotificationsplugin.h \ - $$PWD/mastodonnotificationssyncadaptor.h - -OTHER_FILES += \ - $$PWD/mastodon-notifications.xml \ - $$PWD/mastodon.Notifications.xml - -TEMPLATE = lib -CONFIG += plugin -target.path = $$[QT_INSTALL_LIBS]/buteo-plugins-qt5/oopp - -sync.path = /etc/buteo/profiles/sync -sync.files = mastodon.Notifications.xml - -client.path = /etc/buteo/profiles/client -client.files = mastodon-notifications.xml - -INSTALLS += target sync client ts_install engineering_english_install diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon-notifications.xml b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon-notifications.xml deleted file mode 100644 index 3284d61..0000000 --- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon-notifications.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon.Notifications.xml b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon.Notifications.xml deleted file mode 100644 index 05d5218..0000000 --- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon.Notifications.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.cpp deleted file mode 100644 index ddf6686..0000000 --- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.cpp +++ /dev/null @@ -1,240 +0,0 @@ -/**************************************************************************** - ** - ** 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 "mastodondatatypesyncadaptor.h" -#include "mastodonauthutils.h" - -#include -#include -#include - -// libaccounts-qt5 -#include -#include -#include -#include - -// libsignon-qt5 -#include -#include -#include - -Q_LOGGING_CATEGORY(lcMastodonNotificationsSync, "buteo.plugin.mastodon.notifications.sync", QtWarningMsg) - -MastodonNotificationsDataTypeSyncAdaptor::MastodonNotificationsDataTypeSyncAdaptor( - SocialNetworkSyncAdaptor::DataType dataType, - QObject *parent) - : SocialNetworkSyncAdaptor(QStringLiteral("mastodon"), dataType, 0, parent) -{ -} - -MastodonNotificationsDataTypeSyncAdaptor::~MastodonNotificationsDataTypeSyncAdaptor() -{ -} - -void MastodonNotificationsDataTypeSyncAdaptor::sync(const QString &dataTypeString, int accountId) -{ - if (dataTypeString != SocialNetworkSyncAdaptor::dataTypeName(m_dataType)) { - qCWarning(lcMastodonNotificationsSync) << "Mastodon" << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) - << "sync adaptor was asked to sync" << dataTypeString; - setStatus(SocialNetworkSyncAdaptor::Error); - return; - } - - setStatus(SocialNetworkSyncAdaptor::Busy); - updateDataForAccount(accountId); - qCDebug(lcMastodonNotificationsSync) << "successfully triggered sync with profile:" << m_accountSyncProfile->name(); -} - -void MastodonNotificationsDataTypeSyncAdaptor::updateDataForAccount(int accountId) -{ - Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this); - if (!account) { - qCWarning(lcMastodonNotificationsSync) << "existing account with id" << accountId << "couldn't be retrieved"; - setStatus(SocialNetworkSyncAdaptor::Error); - return; - } - - incrementSemaphore(accountId); - signIn(account); -} - -QString MastodonNotificationsDataTypeSyncAdaptor::apiHost(int accountId) const -{ - return m_apiHosts.value(accountId, QStringLiteral("https://mastodon.social")); -} - -QString MastodonNotificationsDataTypeSyncAdaptor::authServiceName() const -{ - return syncServiceName(); -} - -void MastodonNotificationsDataTypeSyncAdaptor::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(lcMastodonNotificationsSync) << 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 MastodonNotificationsDataTypeSyncAdaptor::sslErrorsHandler(const QList &errs) -{ - QString sslerrs; - foreach (const QSslError &e, errs) { - sslerrs += e.errorString() + QLatin1String("; "); - } - if (!sslerrs.isEmpty()) { - sslerrs.chop(2); - } - - qCWarning(lcMastodonNotificationsSync) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) - << "request with account" << sender()->property("accountId").toInt() - << "experienced ssl errors:" << sslerrs; - sender()->setProperty("isError", QVariant::fromValue(true)); -} - -void MastodonNotificationsDataTypeSyncAdaptor::setCredentialsNeedUpdate(Accounts::Account *account) -{ - qCInfo(lcMastodonNotificationsSync) << "sociald:Mastodon: setting CredentialsNeedUpdate to true for account:" << account->id(); - Accounts::Service srv(m_accountManager->service(authServiceName())); - account->selectService(srv); - account->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue(true)); - account->setValue(QStringLiteral("CredentialsNeedUpdateFrom"), QVariant::fromValue(QString::fromLatin1("sociald-mastodon"))); - account->selectService(Accounts::Service()); - account->syncAndBlock(); -} - -void MastodonNotificationsDataTypeSyncAdaptor::signIn(Accounts::Account *account) -{ - const int accountId = account->id(); - if (!checkAccount(account)) { - decrementSemaphore(accountId); - return; - } - - Accounts::Service srv(m_accountManager->service(authServiceName())); - account->selectService(srv); - - SignOn::Identity *identity = account->credentialsId() > 0 - ? SignOn::Identity::existingIdentity(account->credentialsId()) - : 0; - if (!identity) { - qCWarning(lcMastodonNotificationsSync) << "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(lcMastodonNotificationsSync) << "could not create signon session for account" << accountId; - identity->deleteLater(); - decrementSemaphore(accountId); - return; - } - - QVariantMap signonSessionData = accSrv.authData().parameters(); - MastodonAuthUtils::addSignOnSessionParameters(account, &signonSessionData); - - connect(session, SIGNAL(response(SignOn::SessionData)), - this, SLOT(signOnResponse(SignOn::SessionData)), - Qt::UniqueConnection); - connect(session, SIGNAL(error(SignOn::Error)), - this, SLOT(signOnError(SignOn::Error)), - Qt::UniqueConnection); - - session->setProperty("account", QVariant::fromValue(account)); - session->setProperty("identity", QVariant::fromValue(identity)); - session->process(SignOn::SessionData(signonSessionData), mechanism); -} - -void MastodonNotificationsDataTypeSyncAdaptor::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(lcMastodonNotificationsSync) << "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 MastodonNotificationsDataTypeSyncAdaptor::signOnResponse(const SignOn::SessionData &responseData) -{ - const QVariantMap data = MastodonAuthUtils::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 = MastodonAuthUtils::accessToken(data); - if (accessToken.isEmpty()) { - qCWarning(lcMastodonNotificationsSync) << "signon response for account with id" << accountId - << "contained no access token; keys:" << data.keys(); - } - - m_apiHosts.insert(accountId, MastodonAuthUtils::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-notifications/mastodondatatypesyncadaptor.h b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.h deleted file mode 100644 index 3c61ade..0000000 --- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.h +++ /dev/null @@ -1,70 +0,0 @@ -/**************************************************************************** - ** - ** 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 MASTODONNOTIFICATIONSDATATYPESYNCADAPTOR_H -#define MASTODONNOTIFICATIONSDATATYPESYNCADAPTOR_H - -#include "socialnetworksyncadaptor.h" - -#include -#include -#include - -namespace Accounts { - class Account; -} -namespace SignOn { - class Error; - class SessionData; -} - -class MastodonNotificationsDataTypeSyncAdaptor : public SocialNetworkSyncAdaptor -{ - Q_OBJECT - -public: - MastodonNotificationsDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::DataType dataType, QObject *parent); - virtual ~MastodonNotificationsDataTypeSyncAdaptor(); - - void sync(const QString &dataTypeString, int accountId) override; - -protected: - QString apiHost(int accountId) const; - virtual void updateDataForAccount(int accountId); - virtual QString authServiceName() const; - 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; -}; - -#endif // MASTODONNOTIFICATIONSDATATYPESYNCADAPTOR_H diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationsplugin.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationsplugin.cpp deleted file mode 100644 index 9dd3724..0000000 --- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationsplugin.cpp +++ /dev/null @@ -1,96 +0,0 @@ -/**************************************************************************** - ** - ** 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 "mastodonnotificationsplugin.h" -#include "mastodonnotificationssyncadaptor.h" -#include "socialnetworksyncadaptor.h" - -#include -#include -#include - -namespace { -class AppTranslator : public QTranslator -{ -public: - explicit AppTranslator(QObject *parent) - : QTranslator(parent) - { - qApp->installTranslator(this); - } - - ~AppTranslator() override - { - qApp->removeTranslator(this); - } -}; - -void ensureNotificationTranslations() -{ - static bool initialized = false; - if (initialized) { - return; - } - - QCoreApplication *app = QCoreApplication::instance(); - if (!app) { - return; - } - - AppTranslator *engineeringEnglish = new AppTranslator(app); - engineeringEnglish->load(QStringLiteral("lipstick-jolla-home-mastodon-notifications_eng_en"), - QStringLiteral("/usr/share/translations")); - - AppTranslator *translator = new AppTranslator(app); - translator->load(QLocale(), - QStringLiteral("lipstick-jolla-home-mastodon-notifications"), - QStringLiteral("-"), - QStringLiteral("/usr/share/translations")); - - initialized = true; -} -} - -MastodonNotificationsPlugin::MastodonNotificationsPlugin(const QString& pluginName, - const Buteo::SyncProfile& profile, - Buteo::PluginCbInterface *callbackInterface) - : SocialdButeoPlugin(pluginName, profile, callbackInterface, - QStringLiteral("mastodon"), - SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Notifications)) -{ - ensureNotificationTranslations(); -} - -MastodonNotificationsPlugin::~MastodonNotificationsPlugin() -{ -} - -SocialNetworkSyncAdaptor *MastodonNotificationsPlugin::createSocialNetworkSyncAdaptor() -{ - return new MastodonNotificationsSyncAdaptor(this); -} - -Buteo::ClientPlugin* MastodonNotificationsPluginLoader::createClientPlugin( - const QString& pluginName, - const Buteo::SyncProfile& profile, - Buteo::PluginCbInterface* cbInterface) -{ - return new MastodonNotificationsPlugin(pluginName, profile, cbInterface); -} diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationsplugin.h b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationsplugin.h deleted file mode 100644 index a5a7b37..0000000 --- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationsplugin.h +++ /dev/null @@ -1,54 +0,0 @@ -/**************************************************************************** - ** - ** 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 MASTODONNOTIFICATIONSPLUGIN_H -#define MASTODONNOTIFICATIONSPLUGIN_H - -#include "socialdbuteoplugin.h" - -#include - -class Q_DECL_EXPORT MastodonNotificationsPlugin : public SocialdButeoPlugin -{ - Q_OBJECT - -public: - MastodonNotificationsPlugin(const QString& pluginName, - const Buteo::SyncProfile& profile, - Buteo::PluginCbInterface *cbInterface); - ~MastodonNotificationsPlugin(); - -protected: - SocialNetworkSyncAdaptor *createSocialNetworkSyncAdaptor() override; -}; - -class MastodonNotificationsPluginLoader : public Buteo::SyncPluginLoader -{ - Q_OBJECT - Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.MastodonNotificationsPluginLoader") - Q_INTERFACES(Buteo::SyncPluginLoader) - -public: - Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, - const Buteo::SyncProfile& profile, - Buteo::PluginCbInterface* cbInterface) override; -}; - -#endif // MASTODONNOTIFICATIONSPLUGIN_H diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.cpp deleted file mode 100644 index 98dbbc8..0000000 --- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.cpp +++ /dev/null @@ -1,970 +0,0 @@ -/**************************************************************************** - ** - ** 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 "mastodonnotificationssyncadaptor.h" -#include "mastodontextutils.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -// libaccounts-qt5 -#include -#include -#include - -#include - -#include - -#define OPEN_URL_ACTION(openUrl) \ - Notification::remoteAction( \ - "default", \ - "", \ - "org.sailfishos.fileservice", \ - "/", \ - "org.sailfishos.fileservice", \ - "openUrl", \ - QVariantList() << openUrl \ - ) - -namespace { - Q_LOGGING_CATEGORY(lcMastodonNotifications, "buteo.plugin.mastodon.notifications", QtWarningMsg) - - const char *const NotificationCategory = "x-nemo.social.mastodon.notification"; - const char *const NotificationIdHint = "x-nemo.sociald.notification-id"; - const char *const LastFetchedNotificationIdKey = "LastFetchedNotificationId"; - const int NotificationsPageLimit = 80; - const uint NotificationDismissedReason = 1; - - //% "mentioned you" - const char *const TrIdMentionedYou = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-mentioned_you"); - //% "boosted your post" - const char *const TrIdBoostedYourPost = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-boosted_your_post"); - //% "favourited your post" - const char *const TrIdFavouritedYourPost = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-favourited_your_post"); - //% "started following you" - const char *const TrIdStartedFollowingYou = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-started_following_you"); - //% "requested to follow you" - const char *const TrIdRequestedToFollowYou = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-requested_to_follow_you"); - //% "interacted with your poll" - const char *const TrIdInteractedWithYourPoll = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-interacted_with_your_poll"); - //% "posted" - const char *const TrIdPosted = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-posted"); - //% "updated a post" - const char *const TrIdUpdatedPost = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-updated_post"); - //% "signed up" - const char *const TrIdSignedUp = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-signed_up"); - //% "reported an account" - const char *const TrIdReportedAccount = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-reported_account"); - //% "received a moderation warning" - const char *const TrIdReceivedModerationWarning = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-received_moderation_warning"); - //% "quoted your post" - const char *const TrIdQuotedYourPost = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-quoted_your_post"); - //% "updated a post that quoted you" - const char *const TrIdUpdatedQuotedPost = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-updated_quoted_post"); - //% "sent you a notification" - const char *const TrIdSentNotification = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-sent_notification"); - - //% "An admin blocked an instance" - const char *const TrIdAdminBlockedInstance = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-admin_blocked_instance"); - //% "An admin blocked %1" - const char *const TrIdAdminBlockedTarget = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-admin_blocked_target"); - //% "You blocked an instance" - const char *const TrIdYouBlockedInstance = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-you_blocked_instance"); - //% "You blocked %1" - const char *const TrIdYouBlockedTarget = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-you_blocked_target"); - //% "An account was suspended" - const char *const TrIdAccountSuspended = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-account_suspended"); - //% "%1 was suspended" - const char *const TrIdTargetSuspended = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-target_suspended"); - //% "Some follow relationships were severed" - const char *const TrIdRelationshipsSevered = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-relationships_severed"); - //% "%1 (%2 followers, %3 following removed)" - const char *const TrIdRelationshipsSummary = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-relationships_summary"); - - //% "A moderator sent you a warning" - const char *const TrIdModeratorWarningNone = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-moderator_warning_none"); - //% "A moderator disabled your account" - const char *const TrIdModeratorWarningDisable = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-moderator_warning_disable"); - //% "A moderator marked specific posts as sensitive" - const char *const TrIdModeratorWarningSpecificSensitive = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-moderator_warning_specific_sensitive"); - //% "A moderator deleted specific posts" - const char *const TrIdModeratorWarningDeletePosts = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-moderator_warning_delete_posts"); - //% "A moderator marked all your posts as sensitive" - const char *const TrIdModeratorWarningAllSensitive = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-moderator_warning_all_sensitive"); - //% "A moderator limited your account" - const char *const TrIdModeratorWarningSilence = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-moderator_warning_silence"); - //% "A moderator suspended your account" - const char *const TrIdModeratorWarningSuspend = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-moderator_warning_suspend"); - - //% "Mastodon" - const char *const TrIdMastodon = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-mastodon"); - //% "New notification" - const char *const TrIdNewNotification = QT_TRID_NOOP("lipstick-jolla-home-la-mastodon-notification-new_notification"); - - 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(); - } - - QString actionText(const QString &type) - { - if (type == QLatin1String("mention")) { - return qtTrId(TrIdMentionedYou); - } else if (type == QLatin1String("reblog")) { - return qtTrId(TrIdBoostedYourPost); - } else if (type == QLatin1String("favourite")) { - return qtTrId(TrIdFavouritedYourPost); - } else if (type == QLatin1String("follow")) { - return qtTrId(TrIdStartedFollowingYou); - } else if (type == QLatin1String("follow_request")) { - return qtTrId(TrIdRequestedToFollowYou); - } else if (type == QLatin1String("poll")) { - return qtTrId(TrIdInteractedWithYourPoll); - } else if (type == QLatin1String("status")) { - return qtTrId(TrIdPosted); - } else if (type == QLatin1String("update")) { - return qtTrId(TrIdUpdatedPost); - } else if (type == QLatin1String("admin.sign_up")) { - return qtTrId(TrIdSignedUp); - } else if (type == QLatin1String("admin.report")) { - return qtTrId(TrIdReportedAccount); - } else if (type == QLatin1String("moderation_warning")) { - return qtTrId(TrIdReceivedModerationWarning); - } else if (type == QLatin1String("quote")) { - return qtTrId(TrIdQuotedYourPost); - } else if (type == QLatin1String("quoted_update")) { - return qtTrId(TrIdUpdatedQuotedPost); - } - - return qtTrId(TrIdSentNotification); - } - - bool useSystemSummary(const QString ¬ificationType) - { - return notificationType == QLatin1String("severed_relationships") - || notificationType == QLatin1String("moderation_warning"); - } - - QString severedRelationshipsText(const QJsonObject &eventObject) - { - const QString eventType = eventObject.value(QStringLiteral("type")).toString(); - const QString targetName = eventObject.value(QStringLiteral("target_name")).toString().trimmed(); - const int followersCount = eventObject.value(QStringLiteral("followers_count")).toInt(); - const int followingCount = eventObject.value(QStringLiteral("following_count")).toInt(); - - QString action; - if (eventType == QLatin1String("domain_block")) { - action = targetName.isEmpty() - ? qtTrId(TrIdAdminBlockedInstance) - : qtTrId(TrIdAdminBlockedTarget).arg(targetName); - } else if (eventType == QLatin1String("user_domain_block")) { - action = targetName.isEmpty() - ? qtTrId(TrIdYouBlockedInstance) - : qtTrId(TrIdYouBlockedTarget).arg(targetName); - } else if (eventType == QLatin1String("account_suspension")) { - action = targetName.isEmpty() - ? qtTrId(TrIdAccountSuspended) - : qtTrId(TrIdTargetSuspended).arg(targetName); - } else { - action = qtTrId(TrIdRelationshipsSevered); - } - - const int affectedCount = followersCount + followingCount; - if (affectedCount <= 0) { - return action; - } - - return qtTrId(TrIdRelationshipsSummary) - .arg(action) - .arg(followersCount) - .arg(followingCount); - } - - QString moderationWarningText(const QJsonObject &warningObject) - { - const QString warningText = warningObject.value(QStringLiteral("text")).toString().trimmed(); - if (!warningText.isEmpty()) { - return warningText; - } - - const QString action = warningObject.value(QStringLiteral("action")).toString(); - if (action == QLatin1String("none")) { - return qtTrId(TrIdModeratorWarningNone); - } else if (action == QLatin1String("disable")) { - return qtTrId(TrIdModeratorWarningDisable); - } else if (action == QLatin1String("mark_statuses_as_sensitive")) { - return qtTrId(TrIdModeratorWarningSpecificSensitive); - } else if (action == QLatin1String("delete_statuses")) { - return qtTrId(TrIdModeratorWarningDeletePosts); - } else if (action == QLatin1String("sensitive")) { - return qtTrId(TrIdModeratorWarningAllSensitive); - } else if (action == QLatin1String("silence")) { - return qtTrId(TrIdModeratorWarningSilence); - } else if (action == QLatin1String("suspend")) { - return qtTrId(TrIdModeratorWarningSuspend); - } - - return QString(); - } - - bool hasActiveNotificationsForAccount(int accountId, const Notification *ignoredNotification = 0) - { - bool hasActiveNotifications = false; - const QList notifications = Notification::notifications(); - foreach (QObject *object, notifications) { - Notification *notification = qobject_cast(object); - if (notification - && notification != ignoredNotification - && notification->category() == QLatin1String(NotificationCategory) - && notification->hintValue("x-nemo.sociald.account-id").toInt() == accountId) { - hasActiveNotifications = true; - } - - delete object; - } - - return hasActiveNotifications; - } - - QString authorizeInteractionUrl(const QString &apiHost, const QString &targetUrl) - { - const QUrl parsedApiHost(apiHost); - const QUrl parsedTargetUrl(targetUrl); - if (!parsedApiHost.isValid() - || parsedApiHost.scheme().isEmpty() - || parsedApiHost.host().isEmpty() - || !parsedTargetUrl.isValid() - || parsedTargetUrl.scheme().isEmpty() - || parsedTargetUrl.host().isEmpty()) { - return targetUrl; - } - - // Links on the account's own instance should open directly. - const bool sameScheme = QString::compare(parsedApiHost.scheme(), parsedTargetUrl.scheme(), Qt::CaseInsensitive) == 0; - const bool sameHost = QString::compare(parsedApiHost.host(), parsedTargetUrl.host(), Qt::CaseInsensitive) == 0; - const int apiPort = parsedApiHost.port(parsedApiHost.scheme() == QLatin1String("https") ? 443 : 80); - const int targetPort = parsedTargetUrl.port(parsedTargetUrl.scheme() == QLatin1String("https") ? 443 : 80); - if (sameScheme && sameHost && apiPort == targetPort) { - return targetUrl; - } - - QUrl authorizeUrl(parsedApiHost); - authorizeUrl.setPath(QStringLiteral("/authorize_interaction")); - authorizeUrl.setQuery(QStringLiteral("uri=") + QString::fromUtf8(QUrl::toPercentEncoding(targetUrl))); - return authorizeUrl.toString(); - } - -} - -MastodonNotificationsSyncAdaptor::MastodonNotificationsSyncAdaptor(QObject *parent) - : MastodonNotificationsDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Notifications, parent) -{ - setInitialActive(true); -} - -MastodonNotificationsSyncAdaptor::~MastodonNotificationsSyncAdaptor() -{ -} - -QString MastodonNotificationsSyncAdaptor::syncServiceName() const -{ - return QStringLiteral("mastodon-notifications"); -} - -QString MastodonNotificationsSyncAdaptor::authServiceName() const -{ - return QStringLiteral("mastodon-microblog"); -} - -void MastodonNotificationsSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode) -{ - closeAccountNotifications(oldId); - - m_accessTokens.remove(oldId); - m_pendingSyncStates.remove(oldId); - m_lastMarkedReadIds.remove(oldId); - saveLastFetchedId(oldId, QString()); -} - -void MastodonNotificationsSyncAdaptor::beginSync(int accountId, const QString &accessToken) -{ - m_accessTokens.insert(accountId, accessToken); - m_pendingSyncStates.remove(accountId); - requestUnreadMarker(accountId, accessToken); -} - -void MastodonNotificationsSyncAdaptor::finalize(int accountId) -{ - if (syncAborted()) { - qCInfo(lcMastodonNotifications) << "sync aborted, won't update notifications"; - } - - Q_UNUSED(accountId) -} - -QString MastodonNotificationsSyncAdaptor::sanitizeContent(const QString &content) -{ - return MastodonTextUtils::sanitizeContent(content); -} - -QDateTime MastodonNotificationsSyncAdaptor::parseTimestamp(const QString ×tampString) -{ - return MastodonTextUtils::parseTimestamp(timestampString); -} - -int MastodonNotificationsSyncAdaptor::compareNotificationIds(const QString &left, const QString &right) -{ - if (left == right) { - return 0; - } - - bool leftOk = false; - bool rightOk = false; - const qulonglong leftValue = left.toULongLong(&leftOk); - const qulonglong rightValue = right.toULongLong(&rightOk); - if (leftOk && rightOk) { - return leftValue < rightValue ? -1 : 1; - } - - if (left.size() != right.size()) { - return left.size() < right.size() ? -1 : 1; - } - return left < right ? -1 : 1; -} - -QString MastodonNotificationsSyncAdaptor::notificationObjectKey(int accountId, const QString ¬ificationId) -{ - return QString::number(accountId) + QLatin1Char(':') + notificationId; -} - -QString MastodonNotificationsSyncAdaptor::loadLastFetchedId(int accountId) const -{ - Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, 0); - if (!account) { - return QString(); - } - - Accounts::Service service(m_accountManager->service(syncServiceName())); - account->selectService(service); - const QString lastFetchedId = account->value(QString::fromLatin1(LastFetchedNotificationIdKey)).toString().trimmed(); - account->deleteLater(); - - return lastFetchedId; -} - -void MastodonNotificationsSyncAdaptor::saveLastFetchedId(int accountId, const QString &lastFetchedId) -{ - Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, 0); - if (!account) { - return; - } - - Accounts::Service service(m_accountManager->service(syncServiceName())); - account->selectService(service); - const QString storedId = account->value(QString::fromLatin1(LastFetchedNotificationIdKey)).toString().trimmed(); - if (storedId != lastFetchedId) { - account->setValue(QString::fromLatin1(LastFetchedNotificationIdKey), lastFetchedId); - account->syncAndBlock(); - } - - account->deleteLater(); -} - -void MastodonNotificationsSyncAdaptor::requestUnreadMarker(int accountId, const QString &accessToken) -{ - QUrl url(apiHost(accountId) + QStringLiteral("/api/v1/markers")); - - QUrlQuery query(url); - query.addQueryItem(QStringLiteral("timeline[]"), QStringLiteral("notifications")); - 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); - reply->setProperty("accessToken", accessToken); - 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(finishedUnreadMarkerHandler())); - - incrementSemaphore(accountId); - setupReplyTimeout(accountId, reply); - } else { - qCWarning(lcMastodonNotifications) << "unable to request notifications marker from Mastodon account with id" << accountId; - } -} - -void MastodonNotificationsSyncAdaptor::finishedUnreadMarkerHandler() -{ - QNetworkReply *reply = qobject_cast(sender()); - if (!reply) { - return; - } - - const bool isError = reply->property("isError").toBool(); - const int accountId = reply->property("accountId").toInt(); - const QString accessToken = reply->property("accessToken").toString(); - const QByteArray replyData = reply->readAll(); - - disconnect(reply); - reply->deleteLater(); - removeReplyTimeout(accountId, reply); - - bool ok = false; - const QJsonObject markerObject = parseJsonObjectReplyData(replyData, &ok); - if (isError || !ok) { - qCWarning(lcMastodonNotifications) << "unable to parse notifications marker data from request with account" - << accountId << ", got:" << QString::fromUtf8(replyData); - PendingSyncState fallbackState; - fallbackState.accessToken = accessToken; - fallbackState.lastFetchedId = loadLastFetchedId(accountId); - m_pendingSyncStates.insert(accountId, fallbackState); - requestNotifications(accountId, accessToken, fallbackState.lastFetchedId); - decrementSemaphore(accountId); - return; - } - - const QString markerId = markerObject.value(QStringLiteral("notifications")) - .toObject() - .value(QStringLiteral("last_read_id")) - .toVariant() - .toString() - .trimmed(); - - PendingSyncState state; - state.accessToken = accessToken; - state.markerKnown = true; - state.unreadFloorId = markerId; - state.lastFetchedId = loadLastFetchedId(accountId); - if (state.lastFetchedId.isEmpty() && !markerId.isEmpty()) { - // On first run, use the server unread marker floor to avoid historical flood. - state.lastFetchedId = markerId; - } - m_pendingSyncStates.insert(accountId, state); - requestNotifications(accountId, accessToken, markerId); - - decrementSemaphore(accountId); -} - -void MastodonNotificationsSyncAdaptor::requestNotifications(int accountId, - const QString &accessToken, - const QString &minId, - const QString &maxId) -{ - QUrl url(apiHost(accountId) + QStringLiteral("/api/v1/notifications")); - - QUrlQuery query(url); - query.addQueryItem(QStringLiteral("limit"), QString::number(NotificationsPageLimit)); - if (!minId.isEmpty()) { - query.addQueryItem(QStringLiteral("min_id"), minId); - } - if (!maxId.isEmpty()) { - query.addQueryItem(QStringLiteral("max_id"), maxId); - } - 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); - reply->setProperty("accessToken", accessToken); - reply->setProperty("minId", minId); - 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(finishedNotificationsHandler())); - - incrementSemaphore(accountId); - setupReplyTimeout(accountId, reply); - } else { - qCWarning(lcMastodonNotifications) << "unable to request notifications from Mastodon account with id" << accountId; - } -} - -void MastodonNotificationsSyncAdaptor::requestMarkRead(int accountId, - const QString &accessToken, - const QString &lastReadId) -{ - QUrl url(apiHost(accountId) + QStringLiteral("/api/v1/markers")); - QNetworkRequest request(url); - request.setRawHeader("Authorization", QStringLiteral("Bearer %1").arg(accessToken).toUtf8()); - request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded")); - - QUrlQuery query; - query.addQueryItem(QStringLiteral("notifications[last_read_id]"), lastReadId); - const QByteArray payload = query.toString(QUrl::FullyEncoded).toUtf8(); - - QNetworkReply *reply = m_networkAccessManager->post(request, payload); - if (reply) { - reply->setProperty("accountId", accountId); - reply->setProperty("lastReadId", lastReadId); - 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(finishedMarkReadHandler())); - - incrementSemaphore(accountId); - setupReplyTimeout(accountId, reply); - } else { - qCWarning(lcMastodonNotifications) << "unable to update notifications marker for Mastodon account with id" << accountId; - } -} - -void MastodonNotificationsSyncAdaptor::finishedNotificationsHandler() -{ - QNetworkReply *reply = qobject_cast(sender()); - if (!reply) { - return; - } - - const bool isError = reply->property("isError").toBool(); - const int accountId = reply->property("accountId").toInt(); - const QString accessToken = reply->property("accessToken").toString(); - const QString minId = reply->property("minId").toString(); - const QByteArray replyData = reply->readAll(); - - disconnect(reply); - reply->deleteLater(); - removeReplyTimeout(accountId, reply); - - PendingSyncState state = m_pendingSyncStates.value(accountId); - if (state.accessToken.isEmpty()) { - state.accessToken = accessToken; - } - if (state.unreadFloorId.isEmpty() && !minId.isEmpty()) { - state.unreadFloorId = minId; - } - if (state.lastFetchedId.isEmpty() && !state.unreadFloorId.isEmpty()) { - state.lastFetchedId = state.unreadFloorId; - } else if (state.lastFetchedId.isEmpty() && !minId.isEmpty()) { - state.lastFetchedId = minId; - } - - bool ok = false; - const QJsonArray notifications = parseJsonArrayReplyData(replyData, &ok); - if (!isError && ok) { - if (!notifications.size()) { - qCDebug(lcMastodonNotifications) << "no notifications received for account" << accountId; - if (state.markerKnown) { - closeAccountNotifications(accountId); - } - m_pendingSyncStates.remove(accountId); - decrementSemaphore(accountId); - return; - } - - QString pageMinNotificationId; - - foreach (const QJsonValue ¬ificationValue, notifications) { - const QJsonObject notificationObject = notificationValue.toObject(); - if (notificationObject.isEmpty()) { - continue; - } - - const QString notificationId = notificationObject.value(QStringLiteral("id")).toVariant().toString(); - if (notificationId.isEmpty()) { - continue; - } - - if (pageMinNotificationId.isEmpty() - || compareNotificationIds(notificationId, pageMinNotificationId) < 0) { - pageMinNotificationId = notificationId; - } - - if (state.markerKnown) { - state.unreadNotificationIds.insert(notificationId); - } - - if (state.maxFetchedId.isEmpty() - || compareNotificationIds(notificationId, state.maxFetchedId) > 0) { - state.maxFetchedId = notificationId; - } - - if (!state.lastFetchedId.isEmpty() - && compareNotificationIds(notificationId, state.lastFetchedId) <= 0) { - continue; - } - - const QString notificationType = notificationObject.value(QStringLiteral("type")).toString(); - const QJsonObject actorObject = notificationObject.value(QStringLiteral("account")).toObject(); - const QJsonValue statusValue = notificationObject.value(QStringLiteral("status")); - const QJsonObject statusObject = statusValue.isObject() && !statusValue.isNull() - ? statusValue.toObject() - : QJsonObject(); - const QJsonObject eventObject = notificationObject.value(QStringLiteral("event")).toObject(); - const QJsonObject warningObject = notificationObject.value(QStringLiteral("moderation_warning")).toObject(); - - QDateTime eventTimestamp = parseTimestamp(notificationObject.value(QStringLiteral("created_at")).toString()); - if (!eventTimestamp.isValid()) { - eventTimestamp = parseTimestamp(statusObject.value(QStringLiteral("created_at")).toString()); - } - if (!eventTimestamp.isValid()) { - continue; - } - - const QString displayName = displayNameForAccount(actorObject); - const QString accountName = actorObject.value(QStringLiteral("acct")).toString(); - - const QString statusBody = sanitizeContent(statusObject.value(QStringLiteral("content")).toString()); - const QString action = actionText(notificationType); - QString body; - if (notificationType == QLatin1String("severed_relationships")) { - body = severedRelationshipsText(eventObject); - } else if (notificationType == QLatin1String("moderation_warning")) { - const QString warningText = moderationWarningText(warningObject); - body = warningText.isEmpty() - ? action - : QStringLiteral("%1: %2").arg(action, warningText); - } else if (notificationType == QLatin1String("mention") - || notificationType == QLatin1String("status") - || notificationType == QLatin1String("update") - || notificationType == QLatin1String("quote") - || notificationType == QLatin1String("quoted_update")) { - body = statusBody.isEmpty() ? action : statusBody; - } else { - body = statusBody.isEmpty() ? action : QStringLiteral("%1: %2").arg(action, statusBody); - } - - const QString statusId = statusObject.value(QStringLiteral("id")).toVariant().toString(); - - QString url = statusObject.value(QStringLiteral("url")).toString(); - if (url.isEmpty()) { - url = statusObject.value(QStringLiteral("uri")).toString(); - } - if (url.isEmpty()) { - url = actorObject.value(QStringLiteral("url")).toString(); - } - if (url.isEmpty() && !accountName.isEmpty() && !statusId.isEmpty()) { - url = QStringLiteral("%1/@%2/%3").arg(apiHost(accountId), accountName, statusId); - } else if (url.isEmpty() && !accountName.isEmpty()) { - url = QStringLiteral("%1/@%2").arg(apiHost(accountId), accountName); - } - if (useSystemSummary(notificationType)) { - url.clear(); - } - - PendingNotification pendingNotification; - pendingNotification.notificationId = notificationId; - pendingNotification.summary = useSystemSummary(notificationType) - ? qtTrId(TrIdMastodon) - : displayName; - pendingNotification.body = body; - pendingNotification.link = url; - pendingNotification.timestamp = eventTimestamp; - state.pendingNotifications.insert(notificationId, pendingNotification); - } - - const QString historyBoundaryId = !state.unreadFloorId.isEmpty() - ? state.unreadFloorId - : state.lastFetchedId; - if (notifications.size() >= NotificationsPageLimit - && !pageMinNotificationId.isEmpty() - && !historyBoundaryId.isEmpty() - && compareNotificationIds(pageMinNotificationId, historyBoundaryId) > 0) { - m_pendingSyncStates.insert(accountId, state); - requestNotifications(accountId, state.accessToken, historyBoundaryId, pageMinNotificationId); - decrementSemaphore(accountId); - return; - } - - if (state.pendingNotifications.size() > 0) { - QStringList notificationIds = state.pendingNotifications.keys(); - std::sort(notificationIds.begin(), notificationIds.end(), [](const QString &left, const QString &right) { - return compareNotificationIds(left, right) > 0; - }); - - foreach (const QString ¬ificationId, notificationIds) { - const PendingNotification pendingNotification = state.pendingNotifications.value(notificationId); - publishSystemNotification(accountId, pendingNotification); - } - } - - if (state.markerKnown) { - closeAccountNotifications(accountId, state.unreadNotificationIds); - } - - if (!state.maxFetchedId.isEmpty() - && (state.lastFetchedId.isEmpty() - || compareNotificationIds(state.maxFetchedId, state.lastFetchedId) > 0)) { - saveLastFetchedId(accountId, state.maxFetchedId); - } - - const QString markerId = !state.maxFetchedId.isEmpty() - ? state.maxFetchedId - : state.lastFetchedId; - const QString currentMarkerId = m_lastMarkedReadIds.value(accountId); - if (!markerId.isEmpty() - && !state.accessToken.isEmpty() - && state.markerKnown - && (currentMarkerId.isEmpty() - || compareNotificationIds(markerId, currentMarkerId) > 0)) { - maybeMarkAccountNotificationsRead(accountId, state.accessToken); - } - } else { - qCWarning(lcMastodonNotifications) << "unable to parse notifications data from request with account" << accountId - << ", got:" << QString::fromUtf8(replyData); - } - - m_pendingSyncStates.remove(accountId); - decrementSemaphore(accountId); -} - -void MastodonNotificationsSyncAdaptor::finishedMarkReadHandler() -{ - QNetworkReply *reply = qobject_cast(sender()); - if (!reply) { - return; - } - - const bool isError = reply->property("isError").toBool(); - const int accountId = reply->property("accountId").toInt(); - const QString lastReadId = reply->property("lastReadId").toString(); - const QByteArray replyData = reply->readAll(); - - disconnect(reply); - reply->deleteLater(); - removeReplyTimeout(accountId, reply); - - bool ok = false; - parseJsonObjectReplyData(replyData, &ok); - if (!isError && ok) { - const QString currentMarkerId = m_lastMarkedReadIds.value(accountId); - if (currentMarkerId.isEmpty() || compareNotificationIds(lastReadId, currentMarkerId) > 0) { - m_lastMarkedReadIds.insert(accountId, lastReadId); - } - } else { - qCWarning(lcMastodonNotifications) << "unable to update notifications marker for account" << accountId - << ", got:" << QString::fromUtf8(replyData); - } - - decrementSemaphore(accountId); -} - -void MastodonNotificationsSyncAdaptor::publishSystemNotification(int accountId, - const PendingNotification ¬ificationData) -{ - Notification *notification = createNotification(accountId, notificationData.notificationId); - notification->setItemCount(1); - notification->setTimestamp(notificationData.timestamp.isValid() - ? notificationData.timestamp - : QDateTime::currentDateTimeUtc()); - notification->setSummary(notificationData.summary.isEmpty() - ? qtTrId(TrIdMastodon) - : notificationData.summary); - notification->setBody(notificationData.body.isEmpty() - ? qtTrId(TrIdNewNotification) - : notificationData.body); - notification->setPreviewSummary(notificationData.summary); - notification->setPreviewBody(notificationData.body); - - const QString openUrl = notificationData.link.isEmpty() - ? apiHost(accountId) + QStringLiteral("/notifications") - : notificationData.link; - const QUrl parsedOpenUrl(openUrl); - const QString fallbackUrl = apiHost(accountId) + QStringLiteral("/notifications"); - const QString safeOpenUrl = parsedOpenUrl.isValid() - && !parsedOpenUrl.scheme().isEmpty() - && !parsedOpenUrl.host().isEmpty() - ? openUrl - : fallbackUrl; - notification->setRemoteAction(OPEN_URL_ACTION(authorizeInteractionUrl(apiHost(accountId), safeOpenUrl))); - notification->publish(); - if (notification->replacesId() == 0) { - qCWarning(lcMastodonNotifications) << "failed to publish Mastodon notification" - << notificationData.notificationId; - } -} - -void MastodonNotificationsSyncAdaptor::notificationClosedWithReason(uint reason) -{ - Notification *notification = qobject_cast(sender()); - removeCachedNotification(notification); - if (reason == NotificationDismissedReason) { - markReadFromNotification(notification); - } -} - -void MastodonNotificationsSyncAdaptor::maybeMarkAccountNotificationsRead(int accountId, - const QString &accessToken, - Notification *ignoredNotification) -{ - if (accountId <= 0 || accessToken.isEmpty()) { - return; - } - - if (hasActiveNotificationsForAccount(accountId, ignoredNotification)) { - return; - } - - const QString lastReadId = loadLastFetchedId(accountId); - if (lastReadId.isEmpty()) { - return; - } - - const QString currentMarkerId = m_lastMarkedReadIds.value(accountId); - if (!currentMarkerId.isEmpty() && compareNotificationIds(lastReadId, currentMarkerId) <= 0) { - return; - } - - requestMarkRead(accountId, accessToken, lastReadId); -} - -void MastodonNotificationsSyncAdaptor::markReadFromNotification(Notification *notification) -{ - if (!notification) { - return; - } - - const int accountId = notification->hintValue("x-nemo.sociald.account-id").toInt(); - const QString accessToken = m_accessTokens.value(accountId).trimmed(); - if (accountId <= 0 || accessToken.isEmpty()) { - return; - } - - maybeMarkAccountNotificationsRead(accountId, accessToken, notification); -} - -void MastodonNotificationsSyncAdaptor::removeCachedNotification(Notification *notification) -{ - if (!notification) { - return; - } - - const int accountId = notification->hintValue("x-nemo.sociald.account-id").toInt(); - const QString notificationId = notification->hintValue(NotificationIdHint).toString(); - if (accountId <= 0 || notificationId.isEmpty()) { - return; - } - - m_notificationObjects.remove(notificationObjectKey(accountId, notificationId)); -} - -void MastodonNotificationsSyncAdaptor::closeAccountNotifications(int accountId, - const QSet &keepNotificationIds) -{ - QStringList cachedKeys = m_notificationObjects.keys(); - foreach (const QString &objectKey, cachedKeys) { - Notification *notification = m_notificationObjects.value(objectKey); - if (!notification - || notification->hintValue("x-nemo.sociald.account-id").toInt() != accountId) { - continue; - } - - const QString notificationId = notification->hintValue(NotificationIdHint).toString(); - if (!notificationId.isEmpty() && keepNotificationIds.contains(notificationId)) { - continue; - } - - notification->close(); - m_notificationObjects.remove(objectKey); - notification->deleteLater(); - } - - QList notifications = Notification::notifications(); - foreach (QObject *object, notifications) { - Notification *notification = qobject_cast(object); - if (!notification) { - delete object; - continue; - } - - if (notification->category() == QLatin1String(NotificationCategory) - && notification->hintValue("x-nemo.sociald.account-id").toInt() == accountId) { - const QString notificationId = notification->hintValue(NotificationIdHint).toString(); - if (notificationId.isEmpty() || !keepNotificationIds.contains(notificationId)) { - notification->close(); - } - } - - if (notification->parent() != this) { - delete notification; - } - } -} - -Notification *MastodonNotificationsSyncAdaptor::createNotification(int accountId, const QString ¬ificationId) -{ - const QString objectKey = notificationObjectKey(accountId, notificationId); - Notification *notification = m_notificationObjects.value(objectKey); - if (!notification) { - notification = findNotification(accountId, notificationId); - } - if (!notification) { - notification = new Notification(this); - } else if (notification->parent() != this) { - notification->setParent(this); - } - - notification->setAppName(QStringLiteral("Mastodon")); - notification->setAppIcon(QStringLiteral("icon-l-mastodon")); - notification->setHintValue("x-nemo.sociald.account-id", accountId); - notification->setHintValue(NotificationIdHint, notificationId); - notification->setHintValue("x-nemo-feedback", QStringLiteral("social")); - notification->setHintValue("x-nemo-priority", 100); // Show on lockscreen - notification->setCategory(QLatin1String(NotificationCategory)); - - connect(notification, SIGNAL(closed(uint)), this, SLOT(notificationClosedWithReason(uint)), Qt::UniqueConnection); - m_notificationObjects.insert(objectKey, notification); - - return notification; -} - -Notification *MastodonNotificationsSyncAdaptor::findNotification(int accountId, const QString ¬ificationId) -{ - Notification *notification = 0; - QList notifications = Notification::notifications(); - foreach (QObject *object, notifications) { - Notification *castedNotification = qobject_cast(object); - if (castedNotification - && castedNotification->category() == QLatin1String(NotificationCategory) - && castedNotification->hintValue("x-nemo.sociald.account-id").toInt() == accountId - && castedNotification->hintValue(NotificationIdHint).toString() == notificationId) { - notification = castedNotification; - break; - } - } - - if (notification) { - notifications.removeAll(notification); - } - - qDeleteAll(notifications); - - return notification; -} diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.h b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.h deleted file mode 100644 index 0e9106c..0000000 --- a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.h +++ /dev/null @@ -1,104 +0,0 @@ -/**************************************************************************** - ** - ** 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 MASTODONNOTIFICATIONSSYNCADAPTOR_H -#define MASTODONNOTIFICATIONSSYNCADAPTOR_H - -#include "mastodondatatypesyncadaptor.h" - -#include -#include -#include -#include - -class Notification; - -class MastodonNotificationsSyncAdaptor : public MastodonNotificationsDataTypeSyncAdaptor -{ - Q_OBJECT - -public: - MastodonNotificationsSyncAdaptor(QObject *parent); - ~MastodonNotificationsSyncAdaptor(); - - QString syncServiceName() const override; - -protected: - QString authServiceName() const override; - void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode) override; - void beginSync(int accountId, const QString &accessToken) override; - void finalize(int accountId) override; - -private: - struct PendingNotification { - QString notificationId; - QString summary; - QString body; - QString link; - QDateTime timestamp; - }; - - struct PendingSyncState { - QString accessToken; - bool markerKnown = false; - QString unreadFloorId; - QString lastFetchedId; - QString maxFetchedId; - QSet unreadNotificationIds; - QHash pendingNotifications; - }; - - static QString sanitizeContent(const QString &content); - static QDateTime parseTimestamp(const QString ×tampString); - static int compareNotificationIds(const QString &left, const QString &right); - QString loadLastFetchedId(int accountId) const; - void saveLastFetchedId(int accountId, const QString &lastFetchedId); - - void requestUnreadMarker(int accountId, const QString &accessToken); - void requestNotifications(int accountId, - const QString &accessToken, - const QString &minId, - const QString &maxId = QString()); - void requestMarkRead(int accountId, const QString &accessToken, const QString &lastReadId); - void publishSystemNotification(int accountId, const PendingNotification ¬ificationData); - Notification *createNotification(int accountId, const QString ¬ificationId); - Notification *findNotification(int accountId, const QString ¬ificationId); - void closeAccountNotifications(int accountId, const QSet &keepNotificationIds = QSet()); - static QString notificationObjectKey(int accountId, const QString ¬ificationId); - void maybeMarkAccountNotificationsRead(int accountId, - const QString &accessToken, - Notification *ignoredNotification = 0); - void markReadFromNotification(Notification *notification); - void removeCachedNotification(Notification *notification); - -private Q_SLOTS: - void finishedUnreadMarkerHandler(); - void finishedNotificationsHandler(); - void finishedMarkReadHandler(); - void notificationClosedWithReason(uint reason); - -private: - QHash m_accessTokens; - QHash m_pendingSyncStates; - QHash m_lastMarkedReadIds; - QHash m_notificationObjects; -}; - -#endif // MASTODONNOTIFICATIONSSYNCADAPTOR_H 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 deleted file mode 100644 index a9f65af..0000000 --- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/buteo-sync-plugin-mastodon-posts.pro +++ /dev/null @@ -1,41 +0,0 @@ -# SPDX-FileCopyrightText: 2013 - 2026 Jolla Ltd. -# -# SPDX-License-Identifier: BSD-3-Clause - -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 deleted file mode 100644 index c1e25ae..0000000 --- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon-posts.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon.Posts.xml b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon.Posts.xml deleted file mode 100644 index c7e2448..0000000 --- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodon.Posts.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp deleted file mode 100644 index 7b47fe8..0000000 --- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp +++ /dev/null @@ -1,232 +0,0 @@ -/**************************************************************************** - ** - ** 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 "mastodondatatypesyncadaptor.h" -#include "mastodonauthutils.h" -#include "trace.h" - -#include -#include - -// libaccounts-qt5 -#include -#include -#include -#include - -// libsignon-qt5 -#include -#include -#include - -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(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(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 &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(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(true)); - account->setValue(QStringLiteral("CredentialsNeedUpdateFrom"), QVariant::fromValue(QString::fromLatin1("sociald-mastodon"))); - account->selectService(Accounts::Service()); - account->syncAndBlock(); -} - -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(); - MastodonAuthUtils::addSignOnSessionParameters(account, &signonSessionData); - - connect(session, SIGNAL(response(SignOn::SessionData)), - this, SLOT(signOnResponse(SignOn::SessionData)), - Qt::UniqueConnection); - connect(session, SIGNAL(error(SignOn::Error)), - this, SLOT(signOnError(SignOn::Error)), - Qt::UniqueConnection); - - session->setProperty("account", QVariant::fromValue(account)); - session->setProperty("identity", QVariant::fromValue(identity)); - session->process(SignOn::SessionData(signonSessionData), mechanism); -} - -void MastodonDataTypeSyncAdaptor::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(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) -{ - const QVariantMap data = MastodonAuthUtils::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 = MastodonAuthUtils::accessToken(data); - if (accessToken.isEmpty()) { - qCWarning(lcSocialPlugin) << "signon response for account with id" << accountId - << "contained no access token; keys:" << data.keys(); - } - - m_apiHosts.insert(accountId, MastodonAuthUtils::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 deleted file mode 100644 index 3ebbbf5..0000000 --- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.h +++ /dev/null @@ -1,69 +0,0 @@ -/**************************************************************************** - ** - ** 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 MASTODONDATATYPESYNCADAPTOR_H -#define MASTODONDATATYPESYNCADAPTOR_H - -#include "socialnetworksyncadaptor.h" - -#include -#include -#include - -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 &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; -}; - -#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 deleted file mode 100644 index a196180..0000000 --- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.cpp +++ /dev/null @@ -1,49 +0,0 @@ -/**************************************************************************** - ** - ** 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 "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 deleted file mode 100644 index c8a1d6b..0000000 --- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.h +++ /dev/null @@ -1,54 +0,0 @@ -/**************************************************************************** - ** - ** 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 MASTODONPOSTSPLUGIN_H -#define MASTODONPOSTSPLUGIN_H - -#include "socialdbuteoplugin.h" - -#include - -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 deleted file mode 100644 index 160d6cc..0000000 --- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp +++ /dev/null @@ -1,257 +0,0 @@ -/**************************************************************************** - ** - ** 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 "mastodonpostssyncadaptor.h" -#include "trace.h" -#include "mastodontextutils.h" - -#include -#include -#include -#include -#include -#include - -namespace { - 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(); - m_db.refresh(); - 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(); - m_db.refresh(); - m_db.wait(); - purgeExpiredImages(&m_imageCacheDb, accountId); - } -} - -QString MastodonPostsSyncAdaptor::sanitizeContent(const QString &content) -{ - return MastodonTextUtils::sanitizeContent(content); -} - -QDateTime MastodonPostsSyncAdaptor::parseTimestamp(const QString ×tampString) -{ - return MastodonTextUtils::parseTimestamp(timestampString); -} - -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)), this, SLOT(sslErrorsHandler(QList))); - 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(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(lcSocialPlugin) << "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.addMastodonPost(identifier, - displayName, - accountName, - body, - eventTimestamp, - icon, - imageList, - url, - boostedBy, - repliesCount, - favouritesCount, - reblogsCount, - favourited, - reblogged, - 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 deleted file mode 100644 index d2ca464..0000000 --- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.h +++ /dev/null @@ -1,61 +0,0 @@ -/**************************************************************************** - ** - ** 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 MASTODONPOSTSSYNCADAPTOR_H -#define MASTODONPOSTSSYNCADAPTOR_H - -#include "mastodondatatypesyncadaptor.h" - -#include -#include - -#include "mastodonpostsdatabase.h" -#include - -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 ×tampString); - - void requestPosts(int accountId, const QString &accessToken); - -private Q_SLOTS: - void finishedPostsHandler(); - -private: - MastodonPostsDatabase m_db; - SocialImagesDatabase m_imageCacheDb; -}; - -#endif // MASTODONPOSTSSYNCADAPTOR_H -- cgit v1.2.3