diff options
| author | Andrew Branson <andrew.branson@jolla.com> | 2026-02-10 17:29:54 +0100 |
|---|---|---|
| committer | Andrew Branson <andrew.branson@jolla.com> | 2026-02-10 21:13:57 +0100 |
| commit | 0b9869b23af94f1935839568d62fa7b849f9e5d0 (patch) | |
| tree | 4485ad8750116e396a3341b874143ca3e88122c3 | |
| parent | 4351f4627ba9e71775438dd26c9acddd002c7e11 (diff) | |
Notifications
46 files changed, 1607 insertions, 107 deletions
@@ -2,70 +2,77 @@ Sailfish OS account integration for Mastodon. -## What This Repository Contains - -This repository is the initial public form of the Mastodon plugin and contains all of its account integration components. +## Repository Components ### `common/` - Shared C++ library code used by multiple plugins. -- Includes Mastodon posts database support built on `socialcache`. +- Includes socialcache-backed databases for Mastodon posts and notifications. ### `settings/` -- Sailfish Accounts provider and services definitions. -- Account UI QML files for account creation/settings/credentials update. -- Uses OAuth2 (`web_server`) account flow with per-instance Mastodon app registration. -- Current services: - - `mastodon-microblog` (sync posts) - - `mastodon-sharing` (Transfer Engine sharing) +- Sailfish Accounts provider, service definitions, and account UI. +- OAuth2 (`web_server`) account flow with per-instance Mastodon app registration. +- Services: + - `mastodon-microblog`: sync service for posts and notifications. + - `mastodon-sharing`: Transfer Engine sharing service. ### `buteo-plugins/` -- Social sync plugins for Buteo. -- Includes shared Buteo social plugin framework code and Mastodon posts sync plugin/profile files. -- Installs Buteo client and sync profile XML files. +- Buteo sync plugins and shared social sync framework code. +- Includes: + - `buteo-sync-plugin-mastodon-posts` + - `buteo-sync-plugin-mastodon-notifications` +- Installs Buteo client profile and sync profile XML files. ### `eventsview-plugins/` -- Events view extension QML/C++ plugin for Mastodon posts. +- Events view extension for Mastodon posts. - Includes delegate/feed item QML and `MastodonPostsModel`. ### `transferengine-plugins/` - Transfer Engine integration for Mastodon image sharing. -- `mastodonshareplugin/`: sharing method discovery and share UI metadata. -- `mastodontransferplugin/`: upload implementation (upload media + create status). -- Shared account credential/status helper: `mastodonshareservicestatus.*`. +- `mastodonshareplugin/`: sharing method discovery + metadata. +- `mastodontransferplugin/`: media upload + status creation. ### `icons/` -- Mastodon SVG icon assets and Sailfish icon conversion setup (`sailfish-svg2png`). -- Provides themed service icons used by provider/settings/transfer UI. - +- Mastodon SVG assets and `sailfish-svg2png` conversion setup. +- Uses canonical icon names only: + - `icons/icon-l-mastodon` ### `rpm/` -- RPM spec for packaging all components. -- Defines subpackages: +- Packaging for all modules in `rpm/sailfish-account-mastodon.spec`. +- Subpackages: - `sailfish-account-mastodon` - `buteo-sync-plugin-mastodon-posts` + - `buteo-sync-plugin-mastodon-notifications` - `eventsview-extensions-mastodon` - `transferengine-plugin-mastodon` - - `sailfish-account-mastodon-features-all` +- Main package requires all feature subpackages. + +### Root project +- `sailfish-account-mastodon.pro` ties subprojects together. + +## Current Notification Behavior -### Root project file -- `sailfish-account-mastodon.pro` ties all subprojects together. +- Events view shows Mastodon posts (not notification entries). +- System notifications are produced by `buteo-sync-plugin-mastodon-notifications`. +- Notifications sync fetches unread items using Mastodon markers (`last_read_id`). +- Dismissing the Sailfish notification marks those items as read on Mastodon via markers API. +- Notification template profile dispatches per-account sync profiles on schedule (default every 30 minutes), not only at boot. ## Build Requirements -This project is designed for the Sailfish OS build environment. +This project targets Sailfish OS build tooling. -A full build is not possible without Sailfish SDK access (including target sysroot/tooling and Sailfish-specific development packages). +Full build/package validation is not possible without Sailfish SDK access (target sysroot + Sailfish packages). -In particular, dependencies like these must come from the Sailfish SDK target environment: +Required SDK-provided dependencies include (not exhaustive): - `buteosyncfw5` - `socialcache` - `sailfishaccounts` - `nemotransferengine-qt5` -- other Sailfish/Qt account stack packages listed in `rpm/sailfish-account-mastodon.spec` +- related Qt/account stack packages listed in `rpm/sailfish-account-mastodon.spec` ## Typical Build Flow (Inside Sailfish SDK) 1. Enter Sailfish SDK shell/target. -2. Run qmake build from repository root. +2. Build from repository root (`qmake` / `make`). 3. Build RPM package(s) from `rpm/sailfish-account-mastodon.spec`. -If you are outside Sailfish SDK, use static checks and code review only. +Outside Sailfish SDK, only static validation (wiring, paths, spec consistency) should be considered reliable. diff --git a/buteo-plugins/buteo-common/buteosyncfw_p.h b/buteo-plugins/buteo-common/buteosyncfw_p.h index 5924731..9fd6a77 100644 --- a/buteo-plugins/buteo-common/buteosyncfw_p.h +++ b/buteo-plugins/buteo-common/buteosyncfw_p.h @@ -1,6 +1,6 @@ /**************************************************************************** ** - ** Copyright (C) 2014 Jolla Ltd. + ** Copyright (C) 2013-2026 Jolla Ltd. ** Contact: Chris Adams <chris.adams@jollamobile.com> ** ** This program/library is free software; you can redistribute it and/or diff --git a/buteo-plugins/buteo-common/socialdbuteoplugin.cpp b/buteo-plugins/buteo-common/socialdbuteoplugin.cpp index 8b27f84..0eb5f91 100644 --- a/buteo-plugins/buteo-common/socialdbuteoplugin.cpp +++ b/buteo-plugins/buteo-common/socialdbuteoplugin.cpp @@ -1,6 +1,6 @@ /**************************************************************************** ** - ** Copyright (C) 2014 Jolla Ltd. + ** Copyright (C) 2013-2026 Jolla Ltd. ** Contact: Chris Adams <chris.adams@jolla.com> ** ** This program/library is free software; you can redistribute it and/or @@ -149,11 +149,16 @@ bool SocialdButeoPlugin::uninit() bool SocialdButeoPlugin::startSync() { + if (!m_socialNetworkSyncAdaptor || !m_socialNetworkSyncAdaptor->enabled()) { + qCDebug(lcSocialPlugin) << "no enabled" << m_socialServiceName << "sync adaptor for" << m_dataTypeName; + return false; + } + // if the profile being triggered is the template profile, then we // need to ensure that the appropriate per-account profiles exist. if (m_profileAccountId == 0) { QList<Buteo::SyncProfile*> perAccountProfiles = ensurePerAccountSyncProfilesExist(); - m_socialNetworkSyncAdaptor->setAccountSyncProfile(NULL); + m_socialNetworkSyncAdaptor->setAccountSyncProfile(nullptr); // we need to trigger sync with each profile separately, // or (due to scheduling/etc) another plugin instance might @@ -165,25 +170,37 @@ bool SocialdButeoPlugin::startSync() message.setArguments(QVariantList() << perAccountProfile->name()); QDBusConnection::sessionBus().asyncCall(message); } - } else { - m_socialNetworkSyncAdaptor->setAccountSyncProfile(profile().clone()); + qDeleteAll(perAccountProfiles); + + // This template profile only dispatches account-specific sync profiles. + // Those child sync runs report their own individual results. + updateResults(Buteo::SyncResults(QDateTime::currentDateTime(), + Buteo::SyncResults::SYNC_RESULT_SUCCESS, + Buteo::SyncResults::NO_ERROR)); + emit success(getProfileName(), QString("%1 update dispatched").arg(getProfileName())); + return true; } - // now perform sync. Note that for the template profile case, this will - // result in a purge operation occurring (checking for removed accounts and - // purging any synced data associated with those accounts). - if (m_socialNetworkSyncAdaptor && m_socialNetworkSyncAdaptor->enabled()) { - if (m_socialNetworkSyncAdaptor->status() == SocialNetworkSyncAdaptor::Inactive) { - qCDebug(lcSocialPlugin) << "performing sync of" << m_dataTypeName << "from" << m_socialServiceName - << "for account" << m_profileAccountId; - m_socialNetworkSyncAdaptor->sync(m_dataTypeName, m_profileAccountId); - return true; - } else { - qCDebug(lcSocialPlugin) << m_socialServiceName << "sync adaptor for" << m_dataTypeName - << "is still busy with last sync of account" << m_profileAccountId; - } + Buteo::SyncProfile *accountSyncProfile = profile().clone(); + if (accountSyncProfile + && m_dataTypeName == SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Notifications)) { + accountSyncProfile->setEnabled(true); + Buteo::SyncSchedule schedule = accountSyncProfile->syncSchedule(); + schedule.setScheduleEnabled(true); + accountSyncProfile->setSyncSchedule(schedule); + m_profileManager.updateProfile(*accountSyncProfile); + } + m_socialNetworkSyncAdaptor->setAccountSyncProfile(accountSyncProfile); + + // Now perform sync for the account-specific profile. + if (m_socialNetworkSyncAdaptor->status() == SocialNetworkSyncAdaptor::Inactive) { + qCDebug(lcSocialPlugin) << "performing sync of" << m_dataTypeName << "from" << m_socialServiceName + << "for account" << m_profileAccountId; + m_socialNetworkSyncAdaptor->sync(m_dataTypeName, m_profileAccountId); + return true; } else { - qCDebug(lcSocialPlugin) << "no enabled" << m_socialServiceName << "sync adaptor for" << m_dataTypeName; + qCDebug(lcSocialPlugin) << m_socialServiceName << "sync adaptor for" << m_dataTypeName + << "is still busy with last sync of account" << m_profileAccountId; } return false; } diff --git a/buteo-plugins/buteo-common/socialdbuteoplugin.h b/buteo-plugins/buteo-common/socialdbuteoplugin.h index 57de171..2988d27 100644 --- a/buteo-plugins/buteo-common/socialdbuteoplugin.h +++ b/buteo-plugins/buteo-common/socialdbuteoplugin.h @@ -1,6 +1,6 @@ /**************************************************************************** ** - ** Copyright (C) 2013-2014 Jolla Ltd. + ** Copyright (C) 2013-2026 Jolla Ltd. ** Contact: Raine Makelainen <raine.makelainen@jollamobile.com> ** ** This program/library is free software; you can redistribute it and/or diff --git a/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.cpp b/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.cpp index 7030ce3..37b4687 100644 --- a/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.cpp +++ b/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.cpp @@ -1,6 +1,6 @@ /**************************************************************************** ** - ** Copyright (C) 2013-2014 Jolla Ltd. + ** Copyright (C) 2013-2026 Jolla Ltd. ** Contact: Chris Adams <chris.adams@jollamobile.com> ** ** This program/library is free software; you can redistribute it and/or diff --git a/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.h b/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.h index 846489b..7adde92 100644 --- a/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.h +++ b/buteo-plugins/buteo-common/socialdnetworkaccessmanager_p.h @@ -1,6 +1,6 @@ /**************************************************************************** ** - ** Copyright (C) 2013-2014 Jolla Ltd. + ** Copyright (C) 2013-2026 Jolla Ltd. ** Contact: Chris Adams <chris.adams@jollamobile.com> ** ** This program/library is free software; you can redistribute it and/or diff --git a/buteo-plugins/buteo-common/socialnetworksyncadaptor.cpp b/buteo-plugins/buteo-common/socialnetworksyncadaptor.cpp index 4451d5f..42e6d95 100644 --- a/buteo-plugins/buteo-common/socialnetworksyncadaptor.cpp +++ b/buteo-plugins/buteo-common/socialnetworksyncadaptor.cpp @@ -1,6 +1,6 @@ /**************************************************************************** ** - ** Copyright (C) 2013-2014 Jolla Ltd. + ** Copyright (C) 2013-2026 Jolla Ltd. ** Contact: Chris Adams <chris.adams@jollamobile.com> ** ** This program/library is free software; you can redistribute it and/or diff --git a/buteo-plugins/buteo-common/socialnetworksyncadaptor.h b/buteo-plugins/buteo-common/socialnetworksyncadaptor.h index 99adeb3..7c76564 100644 --- a/buteo-plugins/buteo-common/socialnetworksyncadaptor.h +++ b/buteo-plugins/buteo-common/socialnetworksyncadaptor.h @@ -1,6 +1,6 @@ /**************************************************************************** ** - ** Copyright (C) 2013-2014 Jolla Ltd. + ** Copyright (C) 2013-2026 Jolla Ltd. ** Contact: Chris Adams <chris.adams@jollamobile.com> ** ** This program/library is free software; you can redistribute it and/or diff --git a/buteo-plugins/buteo-common/trace.cpp b/buteo-plugins/buteo-common/trace.cpp index ffcdf5b..05e3508 100644 --- a/buteo-plugins/buteo-common/trace.cpp +++ b/buteo-plugins/buteo-common/trace.cpp @@ -1,6 +1,6 @@ /**************************************************************************** ** - ** Copyright (C) 2021 Jolla Ltd. + ** 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 diff --git a/buteo-plugins/buteo-common/trace.h b/buteo-plugins/buteo-common/trace.h index b0aeeeb..2b9c1ae 100644 --- a/buteo-plugins/buteo-common/trace.h +++ b/buteo-plugins/buteo-common/trace.h @@ -1,6 +1,6 @@ /**************************************************************************** ** - ** Copyright (C) 2013-2014 Jolla Ltd. + ** Copyright (C) 2013-2026 Jolla Ltd. ** Contact: Chris Adams <chris.adams@jollamobile.com> ** ** This program/library is free software; you can redistribute it and/or diff --git a/buteo-plugins/buteo-plugins.pro b/buteo-plugins/buteo-plugins.pro index 5a48850..179aebf 100644 --- a/buteo-plugins/buteo-plugins.pro +++ b/buteo-plugins/buteo-plugins.pro @@ -1,6 +1,8 @@ TEMPLATE = subdirs SUBDIRS += \ buteo-common \ - buteo-sync-plugin-mastodon-posts + buteo-sync-plugin-mastodon-posts \ + buteo-sync-plugin-mastodon-notifications buteo-sync-plugin-mastodon-posts.depends = buteo-common +buteo-sync-plugin-mastodon-notifications.depends = buteo-common 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 new file mode 100644 index 0000000..81060b1 --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/buteo-sync-plugin-mastodon-notifications.pro @@ -0,0 +1,37 @@ +TARGET = mastodon-notifications-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/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 diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon-notifications.xml b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon-notifications.xml new file mode 100644 index 0000000..6648a44 --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon-notifications.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<profile name="mastodon-notifications" type="client" > + <field name="Sync Direction" /> +</profile> diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon.Notifications.xml b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon.Notifications.xml new file mode 100644 index 0000000..85f24e6 --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodon.Notifications.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<profile name="mastodon.Notifications" type="sync" > + <key name="category" value="eventfeed" /> + <key name="enabled" value="true" /> + <key name="use_accounts" value="false" /> + <key name="destinationtype" value="online" /> + <key name="hidden" value="true" /> + <key name="displayname" value="Mastodon Notifications"/> + + <schedule enabled="true" interval="30" days="1,2,3,4,5,6,7" syncconfiguredtime="" time="" /> + + <profile name="mastodon-notifications" type="client" > + <key name="Sync Direction" value="from-remote" /> + </profile> +</profile> diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.cpp new file mode 100644 index 0000000..f915507 --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.cpp @@ -0,0 +1,313 @@ +/**************************************************************************** + ** + ** 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 "trace.h" + +#include <QtCore/QVariantMap> +#include <QtCore/QUrl> +#include <QtNetwork/QNetworkRequest> + +// libaccounts-qt5 +#include <Accounts/Manager> +#include <Accounts/Account> +#include <Accounts/Service> +#include <Accounts/AccountService> + +// libsignon-qt5 +#include <SignOn/Identity> +#include <SignOn/AuthSession> +#include <SignOn/SessionData> + +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(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 MastodonNotificationsDataTypeSyncAdaptor::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 MastodonNotificationsDataTypeSyncAdaptor::apiHost(int accountId) const +{ + return m_apiHosts.value(accountId, QStringLiteral("https://mastodon.social")); +} + +void MastodonNotificationsDataTypeSyncAdaptor::errorHandler(QNetworkReply::NetworkError err) +{ + QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender()); + if (!reply) { + return; + } + + const int accountId = reply->property("accountId").toInt(); + const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) + << "request with account" << accountId + << "experienced error:" << err + << "HTTP:" << httpStatus; + + reply->setProperty("isError", QVariant::fromValue<bool>(true)); + + if (httpStatus == 401 || err == QNetworkReply::AuthenticationRequiredError) { + Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this); + if (account) { + setCredentialsNeedUpdate(account); + } + } +} + +void MastodonNotificationsDataTypeSyncAdaptor::sslErrorsHandler(const QList<QSslError> &errs) +{ + QString sslerrs; + foreach (const QSslError &e, errs) { + sslerrs += e.errorString() + QLatin1String("; "); + } + if (!sslerrs.isEmpty()) { + sslerrs.chop(2); + } + + qCWarning(lcSocialPlugin) << SocialNetworkSyncAdaptor::dataTypeName(m_dataType) + << "request with account" << sender()->property("accountId").toInt() + << "experienced ssl errors:" << sslerrs; + sender()->setProperty("isError", QVariant::fromValue<bool>(true)); +} + +void MastodonNotificationsDataTypeSyncAdaptor::setCredentialsNeedUpdate(Accounts::Account *account) +{ + qCInfo(lcSocialPlugin) << "sociald:Mastodon: setting CredentialsNeedUpdate to true for account:" << account->id(); + Accounts::Service srv(m_accountManager->service(syncServiceName())); + account->selectService(srv); + account->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue<bool>(true)); + account->setValue(QStringLiteral("CredentialsNeedUpdateFrom"), QVariant::fromValue<QString>(QString::fromLatin1("sociald-mastodon"))); + account->selectService(Accounts::Service()); + account->syncAndBlock(); +} + +QString MastodonNotificationsDataTypeSyncAdaptor::normalizeApiHost(const QString &rawHost) +{ + QString host = rawHost.trimmed(); + if (host.isEmpty()) { + host = QStringLiteral("https://mastodon.social"); + } + if (!host.startsWith(QLatin1String("https://")) + && !host.startsWith(QLatin1String("http://"))) { + host.prepend(QStringLiteral("https://")); + } + + QUrl url(host); + if (!url.isValid() || url.host().isEmpty()) { + return QStringLiteral("https://mastodon.social"); + } + + QString normalized = QString::fromLatin1(url.toEncoded(QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment)); + if (normalized.endsWith(QLatin1Char('/'))) { + normalized.chop(1); + } + return normalized; +} + +void MastodonNotificationsDataTypeSyncAdaptor::signIn(Accounts::Account *account) +{ + const int accountId = account->id(); + if (!checkAccount(account)) { + decrementSemaphore(accountId); + return; + } + + Accounts::Service srv(m_accountManager->service(syncServiceName())); + account->selectService(srv); + SignOn::Identity *identity = account->credentialsId() > 0 + ? SignOn::Identity::existingIdentity(account->credentialsId()) + : 0; + if (!identity) { + qCWarning(lcSocialPlugin) << "account" << accountId << "has no valid credentials, cannot sign in"; + decrementSemaphore(accountId); + return; + } + + Accounts::AccountService accSrv(account, srv); + const QString method = accSrv.authData().method(); + const QString mechanism = accSrv.authData().mechanism(); + SignOn::AuthSession *session = identity->createSession(method); + if (!session) { + qCWarning(lcSocialPlugin) << "could not create signon session for account" << accountId; + identity->deleteLater(); + decrementSemaphore(accountId); + return; + } + + QVariantMap signonSessionData = accSrv.authData().parameters(); + QString configuredHost = account->value(QStringLiteral("auth/oauth2/web_server/Host")).toString().trimmed(); + if (configuredHost.isEmpty()) { + configuredHost = normalizeApiHost(account->value(QStringLiteral("api/Host")).toString()); + } + if (configuredHost.startsWith(QLatin1String("https://"))) { + configuredHost.remove(0, 8); + } else if (configuredHost.startsWith(QLatin1String("http://"))) { + configuredHost.remove(0, 7); + } + while (configuredHost.endsWith(QLatin1Char('/'))) { + configuredHost.chop(1); + } + if (configuredHost.isEmpty()) { + configuredHost = QStringLiteral("mastodon.social"); + } + signonSessionData.insert(QStringLiteral("Host"), configuredHost); + + const QString authPath = account->value(QStringLiteral("auth/oauth2/web_server/AuthPath")).toString().trimmed(); + if (!authPath.isEmpty()) { + signonSessionData.insert(QStringLiteral("AuthPath"), authPath); + } + + const QString tokenPath = account->value(QStringLiteral("auth/oauth2/web_server/TokenPath")).toString().trimmed(); + if (!tokenPath.isEmpty()) { + signonSessionData.insert(QStringLiteral("TokenPath"), tokenPath); + } + + const QString responseType = account->value(QStringLiteral("auth/oauth2/web_server/ResponseType")).toString().trimmed(); + if (!responseType.isEmpty()) { + signonSessionData.insert(QStringLiteral("ResponseType"), responseType); + } + + const QString redirectUri = account->value(QStringLiteral("auth/oauth2/web_server/RedirectUri")).toString().trimmed(); + if (!redirectUri.isEmpty()) { + signonSessionData.insert(QStringLiteral("RedirectUri"), redirectUri); + } + + const QVariant scopeValue = account->value(QStringLiteral("auth/oauth2/web_server/Scope")); + if (scopeValue.isValid()) { + signonSessionData.insert(QStringLiteral("Scope"), scopeValue); + } + + const QString clientId = account->value(QStringLiteral("auth/oauth2/web_server/ClientId")).toString().trimmed(); + if (!clientId.isEmpty()) { + signonSessionData.insert(QStringLiteral("ClientId"), clientId); + } + + const QString clientSecret = account->value(QStringLiteral("auth/oauth2/web_server/ClientSecret")).toString().trimmed(); + if (!clientSecret.isEmpty()) { + signonSessionData.insert(QStringLiteral("ClientSecret"), clientSecret); + } + + signonSessionData.insert(QStringLiteral("UiPolicy"), SignOn::NoUserInteractionPolicy); + + connect(session, SIGNAL(response(SignOn::SessionData)), + this, SLOT(signOnResponse(SignOn::SessionData)), + Qt::UniqueConnection); + connect(session, SIGNAL(error(SignOn::Error)), + this, SLOT(signOnError(SignOn::Error)), + Qt::UniqueConnection); + + session->setProperty("account", QVariant::fromValue<Accounts::Account*>(account)); + session->setProperty("identity", QVariant::fromValue<SignOn::Identity*>(identity)); + session->process(SignOn::SessionData(signonSessionData), mechanism); +} + +void MastodonNotificationsDataTypeSyncAdaptor::signOnError(const SignOn::Error &error) +{ + SignOn::AuthSession *session = qobject_cast<SignOn::AuthSession*>(sender()); + Accounts::Account *account = session->property("account").value<Accounts::Account*>(); + SignOn::Identity *identity = session->property("identity").value<SignOn::Identity*>(); + const int accountId = account->id(); + + qCWarning(lcSocialPlugin) << "credentials for account with id" << accountId + << "couldn't be retrieved:" << error.type() << error.message(); + + if (error.type() == SignOn::Error::UserInteraction) { + setCredentialsNeedUpdate(account); + } + + session->disconnect(this); + identity->destroySession(session); + identity->deleteLater(); + account->deleteLater(); + + setStatus(SocialNetworkSyncAdaptor::Error); + decrementSemaphore(accountId); +} + +void MastodonNotificationsDataTypeSyncAdaptor::signOnResponse(const SignOn::SessionData &responseData) +{ + QVariantMap data; + foreach (const QString &key, responseData.propertyNames()) { + data.insert(key, responseData.getProperty(key)); + } + + QString accessToken; + SignOn::AuthSession *session = qobject_cast<SignOn::AuthSession*>(sender()); + Accounts::Account *account = session->property("account").value<Accounts::Account*>(); + SignOn::Identity *identity = session->property("identity").value<SignOn::Identity*>(); + const int accountId = account->id(); + + accessToken = data.value(QLatin1String("AccessToken")).toString().trimmed(); + if (accessToken.isEmpty()) { + accessToken = data.value(QLatin1String("access_token")).toString().trimmed(); + } + if (accessToken.isEmpty()) { + qCWarning(lcSocialPlugin) << "signon response for account with id" << accountId + << "contained no access token; keys:" << data.keys(); + } + + m_apiHosts.insert(accountId, normalizeApiHost(account->value(QStringLiteral("api/Host")).toString())); + + session->disconnect(this); + identity->destroySession(session); + identity->deleteLater(); + account->deleteLater(); + + if (!accessToken.isEmpty()) { + beginSync(accountId, accessToken); + } else { + setStatus(SocialNetworkSyncAdaptor::Error); + } + + decrementSemaphore(accountId); +} diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.h b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.h new file mode 100644 index 0000000..1c2d13f --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodondatatypesyncadaptor.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 MASTODONNOTIFICATIONSDATATYPESYNCADAPTOR_H +#define MASTODONNOTIFICATIONSDATATYPESYNCADAPTOR_H + +#include "socialnetworksyncadaptor.h" + +#include <QtCore/QMap> +#include <QtNetwork/QNetworkReply> +#include <QtNetwork/QSslError> + +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 void beginSync(int accountId, const QString &accessToken) = 0; + +protected Q_SLOTS: + virtual void errorHandler(QNetworkReply::NetworkError err); + virtual void sslErrorsHandler(const QList<QSslError> &errs); + +private Q_SLOTS: + void signOnError(const SignOn::Error &error); + void signOnResponse(const SignOn::SessionData &responseData); + +private: + static QString normalizeApiHost(const QString &rawHost); + void setCredentialsNeedUpdate(Accounts::Account *account); + void signIn(Accounts::Account *account); + +private: + QMap<int, QString> m_apiHosts; +}; + +#endif // MASTODONNOTIFICATIONSDATATYPESYNCADAPTOR_H diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationsplugin.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationsplugin.cpp new file mode 100644 index 0000000..fe9f989 --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationsplugin.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 "mastodonnotificationsplugin.h" +#include "mastodonnotificationssyncadaptor.h" +#include "socialnetworksyncadaptor.h" + +MastodonNotificationsPlugin::MastodonNotificationsPlugin(const QString& pluginName, + const Buteo::SyncProfile& profile, + Buteo::PluginCbInterface *callbackInterface) + : SocialdButeoPlugin(pluginName, profile, callbackInterface, + QStringLiteral("mastodon"), + SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Notifications)) +{ +} + +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 new file mode 100644 index 0000000..a5a7b37 --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationsplugin.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 MASTODONNOTIFICATIONSPLUGIN_H +#define MASTODONNOTIFICATIONSPLUGIN_H + +#include "socialdbuteoplugin.h" + +#include <buteosyncfw5/SyncPluginLoader.h> + +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 new file mode 100644 index 0000000..aa1089c --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.cpp @@ -0,0 +1,701 @@ +/**************************************************************************** + ** + ** 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 "trace.h" + +#include <QtCore/QJsonArray> +#include <QtCore/QJsonObject> +#include <QtCore/QJsonValue> +#include <QtCore/QRegularExpression> +#include <QtCore/QUrl> +#include <QtCore/QUrlQuery> +#include <QtNetwork/QNetworkRequest> + +#include <notification.h> + +#define OPEN_BROWSER_ACTION(openUrlArgs) \ + Notification::remoteAction( \ + "default", \ + "", \ + "org.sailfishos.browser", \ + "/", \ + "org.sailfishos.browser", \ + "openUrl", \ + QVariantList() << openUrlArgs \ + ) + +namespace { + const char *const NotificationCategory = "x-nemo.social.mastodon.notification"; + const char *const LastReadIdProperty = "mastodonLastReadId"; + const int NotificationsPageLimit = 80; + const uint NotificationDismissedReason = 2; + + QString decodeHtmlEntities(QString text) + { + text.replace(QStringLiteral("""), QStringLiteral("\"")); + text.replace(QStringLiteral("'"), QStringLiteral("'")); + text.replace(QStringLiteral("<"), QStringLiteral("<")); + text.replace(QStringLiteral(">"), QStringLiteral(">")); + text.replace(QStringLiteral("&"), QStringLiteral("&")); + text.replace(QStringLiteral(" "), QStringLiteral(" ")); + + static const QRegularExpression decimalEntity(QStringLiteral("&#(\\d+);")); + QRegularExpressionMatch match; + int index = 0; + while ((index = text.indexOf(decimalEntity, index, &match)) != -1) { + const uint value = match.captured(1).toUInt(); + const QString replacement = value > 0 ? QString(QChar(value)) : QString(); + text.replace(index, match.capturedLength(0), replacement); + index += replacement.size(); + } + + static const QRegularExpression hexEntity(QStringLiteral("&#x([0-9a-fA-F]+);")); + index = 0; + while ((index = text.indexOf(hexEntity, index, &match)) != -1) { + bool ok = false; + const uint value = match.captured(1).toUInt(&ok, 16); + const QString replacement = ok && value > 0 ? QString(QChar(value)) : QString(); + text.replace(index, match.capturedLength(0), replacement); + index += replacement.size(); + } + + return text; + } + + QString displayNameForAccount(const QJsonObject &account) + { + const QString displayName = account.value(QStringLiteral("display_name")).toString().trimmed(); + if (!displayName.isEmpty()) { + return displayName; + } + + const QString username = account.value(QStringLiteral("username")).toString().trimmed(); + if (!username.isEmpty()) { + return username; + } + + return account.value(QStringLiteral("acct")).toString().trimmed(); + } + + QString actionText(const QString &type) + { + if (type == QLatin1String("mention")) { + return QStringLiteral("mentioned you"); + } else if (type == QLatin1String("reblog")) { + return QStringLiteral("boosted your post"); + } else if (type == QLatin1String("favourite")) { + return QStringLiteral("favourited your post"); + } else if (type == QLatin1String("follow")) { + return QStringLiteral("started following you"); + } else if (type == QLatin1String("follow_request")) { + return QStringLiteral("requested to follow you"); + } else if (type == QLatin1String("poll")) { + return QStringLiteral("interacted with your poll"); + } else if (type == QLatin1String("status")) { + return QStringLiteral("posted"); + } else if (type == QLatin1String("update")) { + return QStringLiteral("updated a post"); + } + + return QStringLiteral("sent you a notification"); + } + + QList<QPair<QString, SocialPostImage::ImageType> > parseMediaAttachments(const QJsonObject &statusObject) + { + QList<QPair<QString, SocialPostImage::ImageType> > imageList; + + const QJsonArray mediaAttachments = statusObject.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)); + } + } + + return imageList; + } +} + +MastodonNotificationsSyncAdaptor::MastodonNotificationsSyncAdaptor(QObject *parent) + : MastodonNotificationsDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Notifications, parent) +{ + setInitialActive(m_db.isValid()); +} + +MastodonNotificationsSyncAdaptor::~MastodonNotificationsSyncAdaptor() +{ +} + +QString MastodonNotificationsSyncAdaptor::syncServiceName() const +{ + return QStringLiteral("mastodon-microblog"); +} + +void MastodonNotificationsSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode) +{ + Notification *notification = findNotification(oldId); + if (notification) { + notification->close(); + notification->deleteLater(); + } + + m_db.removePosts(oldId); + m_db.commit(); + m_db.wait(); + + purgeCachedImages(&m_imageCacheDb, oldId); + + m_pendingSyncStates.remove(oldId); + m_accessTokens.remove(oldId); + m_lastMarkedReadIds.remove(oldId); +} + +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(lcSocialPlugin) << "sync aborted, won't commit database changes"; + } else { + m_db.commit(); + m_db.wait(); + purgeExpiredImages(&m_imageCacheDb, accountId); + } +} + +QString MastodonNotificationsSyncAdaptor::sanitizeContent(const QString &content) +{ + QString plain = content; + plain.replace(QRegularExpression(QStringLiteral("<\\s*br\\s*/?\\s*>"), QRegularExpression::CaseInsensitiveOption), QStringLiteral("\n")); + plain.replace(QRegularExpression(QStringLiteral("<\\s*/\\s*p\\s*>"), QRegularExpression::CaseInsensitiveOption), QStringLiteral("\n")); + plain.remove(QRegularExpression(QStringLiteral("<[^>]+>"), QRegularExpression::CaseInsensitiveOption)); + + return decodeHtmlEntities(plain).trimmed(); +} + +QDateTime MastodonNotificationsSyncAdaptor::parseTimestamp(const QString ×tampString) +{ + QDateTime timestamp; + +#if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0) + timestamp = QDateTime::fromString(timestampString, Qt::ISODateWithMs); + if (timestamp.isValid()) { + return timestamp; + } +#endif + + timestamp = QDateTime::fromString(timestampString, Qt::ISODate); + if (timestamp.isValid()) { + return timestamp; + } + + const int timeSeparator = timestampString.indexOf(QLatin1Char('T')); + const int fractionSeparator = timestampString.indexOf(QLatin1Char('.'), timeSeparator + 1); + if (timeSeparator > -1 && fractionSeparator > -1) { + int timezoneSeparator = timestampString.indexOf(QLatin1Char('Z'), fractionSeparator + 1); + if (timezoneSeparator == -1) { + timezoneSeparator = timestampString.indexOf(QLatin1Char('+'), fractionSeparator + 1); + } + if (timezoneSeparator == -1) { + timezoneSeparator = timestampString.indexOf(QLatin1Char('-'), fractionSeparator + 1); + } + + QString stripped = timestampString; + if (timezoneSeparator > -1) { + stripped.remove(fractionSeparator, timezoneSeparator - fractionSeparator); + } else { + stripped.truncate(fractionSeparator); + } + + timestamp = QDateTime::fromString(stripped, Qt::ISODate); + } + + return timestamp; +} + +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; +} + +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<QSslError>)), this, SLOT(sslErrorsHandler(QList<QSslError>))); + connect(reply, SIGNAL(finished()), this, SLOT(finishedUnreadMarkerHandler())); + + incrementSemaphore(accountId); + setupReplyTimeout(accountId, reply); + } else { + qCWarning(lcSocialPlugin) << "unable to request notifications marker from Mastodon account with id" << 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<QSslError>)), this, SLOT(sslErrorsHandler(QList<QSslError>))); + connect(reply, SIGNAL(finished()), this, SLOT(finishedNotificationsHandler())); + + incrementSemaphore(accountId); + setupReplyTimeout(accountId, reply); + } else { + qCWarning(lcSocialPlugin) << "unable to request notifications from Mastodon account with id" << accountId; + } +} + +void MastodonNotificationsSyncAdaptor::finishedUnreadMarkerHandler() +{ + QNetworkReply *reply = qobject_cast<QNetworkReply*>(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); + + QString minReadId; + bool ok = false; + const QJsonObject markerObject = parseJsonObjectReplyData(replyData, &ok); + if (!isError && ok) { + minReadId = markerObject.value(QStringLiteral("notifications")) + .toObject() + .value(QStringLiteral("last_read_id")) + .toVariant() + .toString() + .trimmed(); + } else { + qCWarning(lcSocialPlugin) << "unable to parse notifications marker data from request with account" << accountId + << ", got:" << QString::fromUtf8(replyData); + decrementSemaphore(accountId); + return; + } + + PendingSyncState state; + state.accessToken = accessToken; + state.minReadId = minReadId; + state.maxNotificationId = minReadId; + m_pendingSyncStates.insert(accountId, state); + + requestNotifications(accountId, accessToken, minReadId); + decrementSemaphore(accountId); +} + +void MastodonNotificationsSyncAdaptor::finishedNotificationsHandler() +{ + QNetworkReply *reply = qobject_cast<QNetworkReply*>(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; + state.minReadId = minId; + state.maxNotificationId = minId; + } + + bool ok = false; + const QJsonArray notifications = parseJsonArrayReplyData(replyData, &ok); + if (!isError && ok) { + if (!notifications.size()) { + if (!state.dbCleared) { + m_db.removePosts(accountId); + state.dbCleared = true; + } + Notification *notification = findNotification(accountId); + if (notification) { + notification->close(); + notification->deleteLater(); + } + qCDebug(lcSocialPlugin) << "no notifications received for account" << accountId; + m_pendingSyncStates.remove(accountId); + decrementSemaphore(accountId); + return; + } + + if (!state.dbCleared) { + m_db.removePosts(accountId); + state.dbCleared = true; + } + + 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.maxNotificationId.isEmpty() + || compareNotificationIds(notificationId, state.maxNotificationId) > 0) { + state.maxNotificationId = notificationId; + } + + 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(); + + 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(); + + QString icon = actorObject.value(QStringLiteral("avatar_static")).toString(); + if (icon.isEmpty()) { + icon = actorObject.value(QStringLiteral("avatar")).toString(); + } + + const QString statusBody = sanitizeContent(statusObject.value(QStringLiteral("content")).toString()); + const QString action = actionText(notificationType); + QString body; + if (notificationType == QLatin1String("mention") + || notificationType == QLatin1String("status") + || notificationType == QLatin1String("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); + } + + QString boostedBy; + if (notificationType == QLatin1String("reblog") + || notificationType == QLatin1String("favourite")) { + boostedBy = displayName; + } + + const QList<QPair<QString, SocialPostImage::ImageType> > imageList = parseMediaAttachments(statusObject); + + m_db.addMastodonNotification(QStringLiteral("n:%1").arg(notificationId), + displayName, + accountName, + body, + eventTimestamp, + icon, + imageList, + url, + boostedBy, + apiHost(accountId), + accountId); + + ++state.newNotificationCount; + if (state.newNotificationCount == 1) { + state.singleSummary = displayName; + state.singleBody = body; + state.singleLink = url; + state.singleTimestamp = eventTimestamp; + } + } + + if (notifications.size() >= NotificationsPageLimit + && !pageMinNotificationId.isEmpty() + && (state.minReadId.isEmpty() || compareNotificationIds(pageMinNotificationId, state.minReadId) > 0)) { + m_pendingSyncStates.insert(accountId, state); + requestNotifications(accountId, state.accessToken, state.minReadId, pageMinNotificationId); + decrementSemaphore(accountId); + return; + } + + if (state.newNotificationCount > 0) { + publishSystemNotification(accountId, state); + } + } else { + qCWarning(lcSocialPlugin) << "unable to parse notifications data from request with account" << accountId + << ", got:" << QString::fromUtf8(replyData); + } + + m_pendingSyncStates.remove(accountId); + decrementSemaphore(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<QSslError>)), this, SLOT(sslErrorsHandler(QList<QSslError>))); + connect(reply, SIGNAL(finished()), this, SLOT(finishedMarkReadHandler())); + + incrementSemaphore(accountId); + setupReplyTimeout(accountId, reply); + } else { + qCWarning(lcSocialPlugin) << "unable to mark notifications read for Mastodon account with id" << accountId; + } +} + +void MastodonNotificationsSyncAdaptor::finishedMarkReadHandler() +{ + QNetworkReply *reply = qobject_cast<QNetworkReply *>(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) { + m_lastMarkedReadIds.insert(accountId, lastReadId); + } else { + qCWarning(lcSocialPlugin) << "unable to mark notifications read for account" << accountId + << ", got:" << QString::fromUtf8(replyData); + } + + decrementSemaphore(accountId); +} + +void MastodonNotificationsSyncAdaptor::publishSystemNotification(int accountId, const PendingSyncState &state) +{ + Notification *notification = createNotification(accountId); + notification->setItemCount(state.newNotificationCount); + + QStringList openUrlArgs; + if (notification->itemCount() == 1) { + notification->setTimestamp(state.singleTimestamp.isValid() ? state.singleTimestamp : QDateTime::currentDateTimeUtc()); + notification->setSummary(state.singleSummary.isEmpty() ? QStringLiteral("Mastodon") : state.singleSummary); + notification->setBody(state.singleBody.isEmpty() ? QStringLiteral("New notification") : state.singleBody); + openUrlArgs << (state.singleLink.isEmpty() ? apiHost(accountId) + QStringLiteral("/notifications") : state.singleLink); + } else { + notification->setTimestamp(QDateTime::currentDateTimeUtc()); + notification->setSummary(QStringLiteral("Mastodon")); + notification->setBody(QStringLiteral("You have %1 new notifications").arg(notification->itemCount())); + openUrlArgs << apiHost(accountId) + QStringLiteral("/notifications"); + } + + notification->setProperty(LastReadIdProperty, state.maxNotificationId); + notification->setUrgency(Notification::Low); + notification->setRemoteAction(OPEN_BROWSER_ACTION(openUrlArgs)); + notification->publish(); + if (notification->replacesId() == 0) { + qCWarning(lcSocialPlugin) << "failed to publish Mastodon notification"; + } +} + +void MastodonNotificationsSyncAdaptor::notificationClosedWithReason(uint reason) +{ + if (reason == NotificationDismissedReason) { + markReadFromNotification(qobject_cast<Notification *>(sender())); + } +} + +void MastodonNotificationsSyncAdaptor::markReadFromNotification(Notification *notification) +{ + if (!notification) { + return; + } + + const int accountId = notification->hintValue("x-nemo.sociald.account-id").toInt(); + const QString lastReadId = notification->property(LastReadIdProperty).toString().trimmed(); + if (accountId <= 0 || lastReadId.isEmpty()) { + return; + } + + if (m_lastMarkedReadIds.value(accountId) == lastReadId) { + return; + } + + const QString accessToken = m_accessTokens.value(accountId).trimmed(); + if (accessToken.isEmpty()) { + qCWarning(lcSocialPlugin) << "cannot mark notifications read for account" << accountId + << "- missing access token"; + return; + } + + notification->setProperty(LastReadIdProperty, QString()); + requestMarkRead(accountId, accessToken, lastReadId); +} + +Notification *MastodonNotificationsSyncAdaptor::createNotification(int accountId) +{ + Notification *notification = findNotification(accountId); + if (!notification) { + notification = new Notification(this); + notification->setAppName(QStringLiteral("Mastodon")); + notification->setAppIcon(QStringLiteral("icon-l-mastodon")); + notification->setHintValue("x-nemo.sociald.account-id", accountId); + notification->setHintValue("x-nemo-feedback", QStringLiteral("social")); + notification->setCategory(QLatin1String(NotificationCategory)); + } + connect(notification, SIGNAL(closed(uint)), this, SLOT(notificationClosedWithReason(uint)), Qt::UniqueConnection); + + return notification; +} + +Notification *MastodonNotificationsSyncAdaptor::findNotification(int accountId) +{ + Notification *notification = 0; + QList<QObject *> notifications = Notification::notifications(); + foreach (QObject *object, notifications) { + Notification *castedNotification = static_cast<Notification *>(object); + if (castedNotification->category() == QLatin1String(NotificationCategory) + && castedNotification->hintValue("x-nemo.sociald.account-id").toInt() == accountId) { + 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 new file mode 100644 index 0000000..8c79d7d --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-mastodon-notifications/mastodonnotificationssyncadaptor.h @@ -0,0 +1,98 @@ +/**************************************************************************** + ** + ** 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 <QtCore/QDateTime> +#include <QtCore/QHash> +#include <QtNetwork/QNetworkReply> + +#include "mastodonnotificationsdatabase.h" +#include <socialcache/socialimagesdatabase.h> + +class Notification; + +class MastodonNotificationsSyncAdaptor : public MastodonNotificationsDataTypeSyncAdaptor +{ + Q_OBJECT + +public: + MastodonNotificationsSyncAdaptor(QObject *parent); + ~MastodonNotificationsSyncAdaptor(); + + 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: + struct PendingSyncState { + QString accessToken; + QString minReadId; + QString maxNotificationId; + int newNotificationCount; + QString singleSummary; + QString singleBody; + QString singleLink; + QDateTime singleTimestamp; + bool dbCleared; + + PendingSyncState() + : newNotificationCount(0) + , dbCleared(false) + { + } + }; + + static QString sanitizeContent(const QString &content); + static QDateTime parseTimestamp(const QString ×tampString); + static int compareNotificationIds(const QString &left, const QString &right); + + 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 PendingSyncState &state); + Notification *createNotification(int accountId); + Notification *findNotification(int accountId); + void markReadFromNotification(Notification *notification); + +private Q_SLOTS: + void finishedUnreadMarkerHandler(); + void finishedNotificationsHandler(); + void finishedMarkReadHandler(); + void notificationClosedWithReason(uint reason); + +private: + MastodonNotificationsDatabase m_db; + SocialImagesDatabase m_imageCacheDb; + QHash<int, PendingSyncState> m_pendingSyncStates; + QHash<int, QString> m_accessTokens; + QHash<int, QString> m_lastMarkedReadIds; +}; + +#endif // MASTODONNOTIFICATIONSSYNCADAPTOR_H diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp index 977b33b..83a5249 100644 --- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp +++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.cpp @@ -1,6 +1,6 @@ /**************************************************************************** ** - ** Copyright (C) 2026 Open Mobile Platform LLC. + ** 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 diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.h b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.h index f295cd8..ad8321d 100644 --- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.h +++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodondatatypesyncadaptor.h @@ -1,6 +1,6 @@ /**************************************************************************** ** - ** Copyright (C) 2026 Open Mobile Platform LLC. + ** 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 diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.cpp index d3c8d50..a196180 100644 --- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.cpp +++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.cpp @@ -1,6 +1,6 @@ /**************************************************************************** ** - ** Copyright (C) 2026 Open Mobile Platform LLC. + ** 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 diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.h b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.h index 35dbb56..c8a1d6b 100644 --- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.h +++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostsplugin.h @@ -1,6 +1,6 @@ /**************************************************************************** ** - ** Copyright (C) 2026 Open Mobile Platform LLC. + ** 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 diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp index 7ccf0a2..c7c696e 100644 --- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp +++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.cpp @@ -1,6 +1,6 @@ /**************************************************************************** ** - ** Copyright (C) 2026 Open Mobile Platform LLC. + ** 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 diff --git a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.h b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.h index 10f8b1c..d2ca464 100644 --- a/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.h +++ b/buteo-plugins/buteo-sync-plugin-mastodon-posts/mastodonpostssyncadaptor.h @@ -1,6 +1,6 @@ /**************************************************************************** ** - ** Copyright (C) 2026 Open Mobile Platform LLC. + ** 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 diff --git a/common/common.pro b/common/common.pro index eec5b5f..89f0117 100644 --- a/common/common.pro +++ b/common/common.pro @@ -10,10 +10,12 @@ TARGET = mastodoncommon TARGET = $$qtLibraryTarget($$TARGET) HEADERS += \ - $$PWD/mastodonpostsdatabase.h + $$PWD/mastodonpostsdatabase.h \ + $$PWD/mastodonnotificationsdatabase.h SOURCES += \ - $$PWD/mastodonpostsdatabase.cpp + $$PWD/mastodonpostsdatabase.cpp \ + $$PWD/mastodonnotificationsdatabase.cpp TARGETPATH = $$[QT_INSTALL_LIBS] target.path = $$TARGETPATH diff --git a/common/mastodonnotificationsdatabase.cpp b/common/mastodonnotificationsdatabase.cpp new file mode 100644 index 0000000..abb55b9 --- /dev/null +++ b/common/mastodonnotificationsdatabase.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2013-2026 Jolla Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This 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 library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "mastodonnotificationsdatabase.h" + +static const char *DB_NAME = "mastodonNotifications.db"; +static const char *ACCOUNT_NAME_KEY = "account_name"; +static const char *URL_KEY = "url"; +static const char *BOOSTED_BY_KEY = "boosted_by"; +static const char *INSTANCE_URL_KEY = "instance_url"; + +MastodonNotificationsDatabase::MastodonNotificationsDatabase() + : AbstractSocialPostCacheDatabase(QStringLiteral("mastodon"), QLatin1String(DB_NAME)) +{ +} + +MastodonNotificationsDatabase::~MastodonNotificationsDatabase() +{ +} + +void MastodonNotificationsDatabase::addMastodonNotification( + const QString &identifier, + const QString &name, + const QString &accountName, + const QString &body, + const QDateTime ×tamp, + const QString &icon, + const QList<QPair<QString, SocialPostImage::ImageType> > &images, + const QString &url, + const QString &boostedBy, + const QString &instanceUrl, + int account) +{ + QVariantMap extra; + extra.insert(ACCOUNT_NAME_KEY, accountName); + extra.insert(URL_KEY, url); + extra.insert(BOOSTED_BY_KEY, boostedBy); + extra.insert(INSTANCE_URL_KEY, instanceUrl); + addPost(identifier, name, body, timestamp, icon, images, extra, account); +} + +QString MastodonNotificationsDatabase::accountName(const SocialPost::ConstPtr &post) +{ + if (post.isNull()) { + return QString(); + } + return post->extra().value(ACCOUNT_NAME_KEY).toString(); +} + +QString MastodonNotificationsDatabase::url(const SocialPost::ConstPtr &post) +{ + if (post.isNull()) { + return QString(); + } + return post->extra().value(URL_KEY).toString(); +} + +QString MastodonNotificationsDatabase::boostedBy(const SocialPost::ConstPtr &post) +{ + if (post.isNull()) { + return QString(); + } + return post->extra().value(BOOSTED_BY_KEY).toString(); +} + +QString MastodonNotificationsDatabase::instanceUrl(const SocialPost::ConstPtr &post) +{ + if (post.isNull()) { + return QString(); + } + return post->extra().value(INSTANCE_URL_KEY).toString(); +} diff --git a/common/mastodonnotificationsdatabase.h b/common/mastodonnotificationsdatabase.h new file mode 100644 index 0000000..cc69095 --- /dev/null +++ b/common/mastodonnotificationsdatabase.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2013-2026 Jolla Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This 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 library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef MASTODONNOTIFICATIONSDATABASE_H +#define MASTODONNOTIFICATIONSDATABASE_H + +#include <socialcache/abstractsocialpostcachedatabase.h> + +class MastodonNotificationsDatabase: public AbstractSocialPostCacheDatabase +{ + Q_OBJECT +public: + MastodonNotificationsDatabase(); + ~MastodonNotificationsDatabase(); + + void addMastodonNotification(const QString &identifier, const QString &name, + const QString &accountName, const QString &body, + const QDateTime ×tamp, + const QString &icon, + const QList<QPair<QString, SocialPostImage::ImageType> > &images, + const QString &url, const QString &boostedBy, + const QString &instanceUrl, + int account); + + static QString accountName(const SocialPost::ConstPtr &post); + static QString url(const SocialPost::ConstPtr &post); + static QString boostedBy(const SocialPost::ConstPtr &post); + static QString instanceUrl(const SocialPost::ConstPtr &post); +}; + +#endif // MASTODONNOTIFICATIONSDATABASE_H diff --git a/common/mastodonpostsdatabase.cpp b/common/mastodonpostsdatabase.cpp index fa6ddd3..08fc777 100644 --- a/common/mastodonpostsdatabase.cpp +++ b/common/mastodonpostsdatabase.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2026 Open Mobile Platform LLC. + * Copyright (C) 2013-2026 Jolla Ltd. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public diff --git a/common/mastodonpostsdatabase.h b/common/mastodonpostsdatabase.h index d5fdea7..b27b626 100644 --- a/common/mastodonpostsdatabase.h +++ b/common/mastodonpostsdatabase.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2026 Open Mobile Platform LLC. + * Copyright (C) 2013-2026 Jolla Ltd. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public diff --git a/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml b/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml index 231b814..c96fb8a 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml +++ b/eventsview-plugins/eventsview-plugin-mastodon/MastodonFeedItem.qml @@ -1,6 +1,6 @@ /**************************************************************************** ** - ** Copyright (C) 2026 Open Mobile Platform LLC. + ** Copyright (C) 2013-2026 Jolla Ltd. ** ****************************************************************************/ diff --git a/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel.cpp b/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel.cpp index 6d33d48..895ad72 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel.cpp +++ b/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Jolla Ltd. + * Copyright (C) 2013-2026 Jolla Ltd. * Contact: Lucien Xu <lucien.xu@jollamobile.com> * * This library is free software; you can redistribute it and/or diff --git a/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel.h b/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel.h index 1e6394f..c3c3ffe 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel.h +++ b/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Jolla Ltd. + * Copyright (C) 2013-2026 Jolla Ltd. * Contact: Lucien Xu <lucien.xu@jollamobile.com> * * This library is free software; you can redistribute it and/or diff --git a/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel_p.h b/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel_p.h index 6c92655..fa62ac5 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel_p.h +++ b/eventsview-plugins/eventsview-plugin-mastodon/abstractsocialcachemodel_p.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Jolla Ltd. + * Copyright (C) 2013-2026 Jolla Ltd. * Contact: Lucien Xu <lucien.xu@jollamobile.com> * * This library is free software; you can redistribute it and/or diff --git a/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml b/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml index ed79fdb..906dc9c 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml +++ b/eventsview-plugins/eventsview-plugin-mastodon/mastodon-delegate.qml @@ -1,6 +1,6 @@ /**************************************************************************** ** - ** Copyright (C) 2026 Open Mobile Platform LLC. + ** Copyright (C) 2013-2026 Jolla Ltd. ** ****************************************************************************/ @@ -17,10 +17,10 @@ SocialMediaAccountDelegate { //: Mastodon posts //% "Posts" headerText: qsTrId("lipstick-jolla-home-la-mastodon_posts") - headerIcon: "image://theme/graphic-service-mastodon" + headerIcon: "image://theme/icon-l-mastodon" showRemainingCount: false - services: ["Posts", "Notifications"] + services: ["Posts"] socialNetwork: 9 dataType: SocialSync.Posts providerName: "mastodon" diff --git a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.cpp b/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.cpp index 3e54b8b..74d239e 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.cpp +++ b/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2026 Open Mobile Platform LLC. + * Copyright (C) 2013-2026 Jolla Ltd. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public diff --git a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.h b/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.h index 9692729..150438a 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.h +++ b/eventsview-plugins/eventsview-plugin-mastodon/mastodonpostsmodel.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2026 Open Mobile Platform LLC. + * Copyright (C) 2013-2026 Jolla Ltd. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public diff --git a/eventsview-plugins/eventsview-plugin-mastodon/postimagehelper_p.h b/eventsview-plugins/eventsview-plugin-mastodon/postimagehelper_p.h index fe61212..0d70ffa 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/postimagehelper_p.h +++ b/eventsview-plugins/eventsview-plugin-mastodon/postimagehelper_p.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Jolla Ltd. + * Copyright (C) 2013-2026 Jolla Ltd. * Contact: Lucien Xu <lucien.xu@jollamobile.com> * * This library is free software; you can redistribute it and/or diff --git a/eventsview-plugins/eventsview-plugin-mastodon/synchronizelists_p.h b/eventsview-plugins/eventsview-plugin-mastodon/synchronizelists_p.h index 1e09e86..78d5863 100644 --- a/eventsview-plugins/eventsview-plugin-mastodon/synchronizelists_p.h +++ b/eventsview-plugins/eventsview-plugin-mastodon/synchronizelists_p.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Jolla Mobile <andrew.den.exter@jollamobile.com> + * Copyright (C) 2013-2026 Jolla Ltd. * * You may use this file under the terms of the BSD license as follows: * diff --git a/rpm/sailfish-account-mastodon.spec b/rpm/sailfish-account-mastodon.spec index 1fac766..1187379 100644 --- a/rpm/sailfish-account-mastodon.spec +++ b/rpm/sailfish-account-mastodon.spec @@ -23,6 +23,10 @@ BuildRequires: pkgconfig(nemotransferengine-qt5) >= 2.0.0 BuildRequires: pkgconfig(nemonotifications-qt5) Requires: jolla-settings-accounts-extensions-onlinesync Requires: qmf-oauth2-plugin >= 0.0.7 +Requires: buteo-sync-plugin-mastodon-posts +Requires: buteo-sync-plugin-mastodon-notifications +Requires: eventsview-extensions-mastodon +Requires: transferengine-plugin-mastodon Requires(post): %{_libexecdir}/manage-groups Requires(postun): %{_libexecdir}/manage-groups @@ -39,6 +43,17 @@ Requires(post): systemd %description -n buteo-sync-plugin-mastodon-posts Provides synchronisation of Mastodon posts. +%package -n buteo-sync-plugin-mastodon-notifications +Summary: Provides synchronisation of Mastodon notifications +Requires: %{name} = %{version}-%{release} +Requires: buteo-syncfw-qt5-msyncd +Requires: systemd +Requires(post): systemd + +%description -n buteo-sync-plugin-mastodon-notifications +Provides synchronisation of Mastodon notifications. + + %package -n eventsview-extensions-mastodon Summary: Provides integration of Mastodon posts into Events view Requires: lipstick-jolla-home-qt5-components >= 1.2.50 @@ -57,16 +72,6 @@ Requires: %{name} = %{version}-%{release} %description -n transferengine-plugin-mastodon Mastodon image sharing plugin for Transfer Engine. -%package features-all -Summary: Meta package to include all Mastodon account features -Requires: %{name} = %{version}-%{release} -Requires: buteo-sync-plugin-mastodon-posts -Requires: eventsview-extensions-mastodon -Requires: transferengine-plugin-mastodon - -%description features-all -This package includes all Mastodon account features. - %prep %setup -q -n %{name}-%{version} @@ -78,12 +83,6 @@ This package includes all Mastodon account features. %qmake5_install cd icons make INSTALL_ROOT=%{buildroot} install -for icon in $(find %{buildroot}%{_datadir}/themes/sailfish-default/silica -type f -name icon-l-mastodon.png); do - dir=$(dirname "$icon") - cp -a "$icon" "$dir/graphic-service-mastodon.png" - cp -a "$icon" "$dir/graphic-m-service-mastodon.png" - cp -a "$icon" "$dir/graphic-s-service-mastodon.png" -done %post /sbin/ldconfig @@ -107,9 +106,6 @@ fi %{_datadir}/accounts/ui/mastodon.qml %{_datadir}/accounts/ui/mastodon-settings.qml %{_datadir}/accounts/ui/mastodon-update.qml -%{_datadir}/themes/sailfish-default/silica/*/icons/graphic-service-mastodon.png -%{_datadir}/themes/sailfish-default/silica/*/icons/graphic-m-service-mastodon.png -%{_datadir}/themes/sailfish-default/silica/*/icons/graphic-s-service-mastodon.png %{_datadir}/themes/sailfish-default/silica/*/icons/icon-l-mastodon.png %files -n buteo-sync-plugin-mastodon-posts @@ -117,6 +113,11 @@ fi %config %{_sysconfdir}/buteo/profiles/client/mastodon-posts.xml %config %{_sysconfdir}/buteo/profiles/sync/mastodon.Posts.xml +%files -n buteo-sync-plugin-mastodon-notifications +%{_libdir}/buteo-plugins-qt5/oopp/libmastodon-notifications-client.so +%config %{_sysconfdir}/buteo/profiles/client/mastodon-notifications.xml +%config %{_sysconfdir}/buteo/profiles/sync/mastodon.Notifications.xml + %files -n eventsview-extensions-mastodon %{_libdir}/qt5/qml/com/jolla/eventsview/mastodon/* %{_datadir}/lipstick/eventfeed/mastodon-delegate.qml @@ -128,6 +129,3 @@ fi %{_libdir}/nemo-transferengine/plugins/sharing/libmastodonshareplugin.so %{_libdir}/nemo-transferengine/plugins/transfer/libmastodontransferplugin.so %{_datadir}/nemo-transferengine/plugins/sharing/MastodonShareImage.qml - -%files features-all -# Empty by design. diff --git a/settings/accounts/providers/mastodon.provider b/settings/accounts/providers/mastodon.provider index acd9d91..8523691 100644 --- a/settings/accounts/providers/mastodon.provider +++ b/settings/accounts/providers/mastodon.provider @@ -3,7 +3,7 @@ <provider version="1.0" id="mastodon"> <name>Mastodon</name> <description>Mastodon social network</description> - <icon>image://theme/graphic-service-mastodon</icon> + <icon>image://theme/icon-l-mastodon</icon> <template> <group name="auth"> diff --git a/settings/accounts/services/mastodon-microblog.service b/settings/accounts/services/mastodon-microblog.service index 550333e..bbc5945 100644 --- a/settings/accounts/services/mastodon-microblog.service +++ b/settings/accounts/services/mastodon-microblog.service @@ -2,11 +2,11 @@ <service id="mastodon-microblog"> <type>microblogging</type> <name>Posts</name> - <icon>image://theme/icon-m-events</icon> + <icon>image://theme/icon-l-mastodon</icon> <provider>mastodon</provider> <template> - <setting name="sync_profile_templates" type="as">["mastodon.Posts"]</setting> + <setting name="sync_profile_templates" type="as">["mastodon.Posts", "mastodon.Notifications"]</setting> <group name="auth"> <setting name="method">oauth2</setting> <setting name="mechanism">web_server</setting> diff --git a/settings/accounts/services/mastodon-sharing.service b/settings/accounts/services/mastodon-sharing.service index c3ecf37..19e3e24 100644 --- a/settings/accounts/services/mastodon-sharing.service +++ b/settings/accounts/services/mastodon-sharing.service @@ -2,7 +2,7 @@ <service id="mastodon-sharing"> <type>sharing</type> <name>Sharing</name> - <icon>image://theme/icon-m-share</icon> + <icon>image://theme/icon-l-mastodon</icon> <provider>mastodon</provider> <template> diff --git a/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.cpp b/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.cpp index 405b86e..bc66752 100644 --- a/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.cpp +++ b/transferengine-plugins/mastodonshareplugin/mastodonplugininfo.cpp @@ -41,7 +41,7 @@ void MastodonPluginInfo::serviceReady() info.setAccountId(details.accountId); info.setMethodId(QLatin1String("Mastodon")); - info.setMethodIcon(QLatin1String("image://theme/graphic-m-service-mastodon")); + info.setMethodIcon(QLatin1String("image://theme/icon-l-mastodon")); info.setShareUIPath(QLatin1String("/usr/share/nemo-transferengine/plugins/sharing/MastodonShareImage.qml")); info.setCapabilities(m_capabilities); diff --git a/transferengine-plugins/mastodontransferplugin/mastodonuploader.cpp b/transferengine-plugins/mastodontransferplugin/mastodonuploader.cpp index 9e2fa1a..d85e138 100644 --- a/transferengine-plugins/mastodontransferplugin/mastodonuploader.cpp +++ b/transferengine-plugins/mastodontransferplugin/mastodonuploader.cpp @@ -30,7 +30,7 @@ QString MastodonUploader::displayName() const QUrl MastodonUploader::serviceIcon() const { - return QUrl(QStringLiteral("image://theme/graphic-s-service-mastodon")); + return QUrl(QStringLiteral("image://theme/icon-l-mastodon")); } bool MastodonUploader::cancelEnabled() const |
