diff options
| author | Andrew Branson <andrew.branson@jolla.com> | 2026-04-03 22:55:30 +0200 |
|---|---|---|
| committer | Andrew Branson <andrew.branson@jolla.com> | 2026-04-04 11:55:25 +0200 |
| commit | a35c9fa159173388d88ef77e1d31f53488aad094 (patch) | |
| tree | e4691b5bbf054ca13e35d98d9df653bf9cdc0054 /buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostssyncadaptor.cpp | |
| parent | 5f999f7a4712c4a4d1c89054b544064cfd4b769e (diff) | |
Generalize for all fediverse accounts
Diffstat (limited to 'buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostssyncadaptor.cpp')
| -rw-r--r-- | buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostssyncadaptor.cpp | 260 |
1 files changed, 260 insertions, 0 deletions
diff --git a/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostssyncadaptor.cpp b/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostssyncadaptor.cpp new file mode 100644 index 0000000..59e37bf --- /dev/null +++ b/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostssyncadaptor.cpp @@ -0,0 +1,260 @@ +/**************************************************************************** + ** + ** Copyright (C) 2013-2026 Jolla Ltd. + ** + ** This program/library is free software; you can redistribute it and/or + ** modify it under the terms of the GNU Lesser General Public License + ** version 2.1 as published by the Free Software Foundation. + ** + ** This program/library is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + ** Lesser General Public License for more details. + ** + ** You should have received a copy of the GNU Lesser General Public + ** License along with this program/library; if not, write to the Free + ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + ** 02110-1301 USA + ** + ****************************************************************************/ + +#include "fediversepostssyncadaptor.h" +#include "fediversetextutils.h" + +#include <QtCore/QLoggingCategory> +#include <QtCore/QJsonArray> +#include <QtCore/QJsonObject> +#include <QtCore/QJsonValue> +#include <QtCore/QUrl> +#include <QtCore/QUrlQuery> +#include <QtNetwork/QNetworkRequest> + +namespace { + Q_LOGGING_CATEGORY(lcFediversePostsSync, "buteo.plugin.fediverse.posts.sync", QtWarningMsg) + + QString displayNameForAccount(const QJsonObject &account) + { + const QString displayName = account.value(QStringLiteral("display_name")).toString().trimmed(); + if (!displayName.isEmpty()) { + return displayName; + } + + const QString username = account.value(QStringLiteral("username")).toString().trimmed(); + if (!username.isEmpty()) { + return username; + } + + return account.value(QStringLiteral("acct")).toString().trimmed(); + } +} + +FediversePostsSyncAdaptor::FediversePostsSyncAdaptor(QObject *parent) + : FediverseDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Posts, parent) +{ + setInitialActive(m_db.isValid()); +} + +FediversePostsSyncAdaptor::~FediversePostsSyncAdaptor() +{ +} + +QString FediversePostsSyncAdaptor::syncServiceName() const +{ + return QStringLiteral("fediverse-microblog"); +} + +void FediversePostsSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode) +{ + m_db.removePosts(oldId); + m_db.commit(); + m_db.wait(); + m_db.refresh(); + m_db.wait(); + + purgeCachedImages(&m_imageCacheDb, oldId); +} + +void FediversePostsSyncAdaptor::beginSync(int accountId, const QString &accessToken) +{ + requestPosts(accountId, accessToken); +} + +void FediversePostsSyncAdaptor::finalize(int accountId) +{ + if (syncAborted()) { + qCInfo(lcFediversePostsSync) << "sync aborted, won't commit database changes"; + } else { + m_db.commit(); + m_db.wait(); + m_db.refresh(); + m_db.wait(); + purgeExpiredImages(&m_imageCacheDb, accountId); + } +} + +QString FediversePostsSyncAdaptor::sanitizeContent(const QString &content) +{ + return FediverseTextUtils::sanitizeContent(content); +} + +QDateTime FediversePostsSyncAdaptor::parseTimestamp(const QString ×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<QSslError>)), this, SLOT(sslErrorsHandler(QList<QSslError>))); + connect(reply, SIGNAL(finished()), this, SLOT(finishedPostsHandler())); + + incrementSemaphore(accountId); + setupReplyTimeout(accountId, reply); + } else { + qCWarning(lcFediversePostsSync) << "unable to request home timeline posts from Fediverse account with id" << accountId; + } +} + +void FediversePostsSyncAdaptor::finishedPostsHandler() +{ + QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender()); + if (!reply) { + return; + } + + const bool isError = reply->property("isError").toBool(); + const int accountId = reply->property("accountId").toInt(); + QByteArray replyData = reply->readAll(); + + disconnect(reply); + reply->deleteLater(); + removeReplyTimeout(accountId, reply); + + bool ok = false; + QJsonArray statuses = parseJsonArrayReplyData(replyData, &ok); + if (!isError && ok) { + m_db.removePosts(accountId); + + if (!statuses.size()) { + qCDebug(lcFediversePostsSync) << "no feed posts received for account" << accountId; + decrementSemaphore(accountId); + return; + } + + const int sinceSpan = m_accountSyncProfile + ? m_accountSyncProfile->key(Buteo::KEY_SYNC_SINCE_DAYS_PAST, QStringLiteral("7")).toInt() + : 7; + + foreach (const QJsonValue &statusValue, statuses) { + const QJsonObject statusObject = statusValue.toObject(); + if (statusObject.isEmpty()) { + continue; + } + + QJsonObject postObject = statusObject; + QString boostedBy; + if (statusObject.contains(QStringLiteral("reblog")) + && statusObject.value(QStringLiteral("reblog")).isObject() + && !statusObject.value(QStringLiteral("reblog")).isNull()) { + boostedBy = displayNameForAccount(statusObject.value(QStringLiteral("account")).toObject()); + postObject = statusObject.value(QStringLiteral("reblog")).toObject(); + } + + QDateTime eventTimestamp = parseTimestamp(statusObject.value(QStringLiteral("created_at")).toString()); + if (!eventTimestamp.isValid()) { + eventTimestamp = parseTimestamp(postObject.value(QStringLiteral("created_at")).toString()); + } + if (!eventTimestamp.isValid()) { + continue; + } + + if (eventTimestamp.daysTo(QDateTime::currentDateTime()) > sinceSpan) { + continue; + } + + const QJsonObject account = postObject.value(QStringLiteral("account")).toObject(); + const QString displayName = displayNameForAccount(account); + const QString accountName = account.value(QStringLiteral("acct")).toString(); + QString icon = account.value(QStringLiteral("avatar_static")).toString(); + if (icon.isEmpty()) { + icon = account.value(QStringLiteral("avatar")).toString(); + } + + QString identifier = postObject.value(QStringLiteral("id")).toVariant().toString(); + if (identifier.isEmpty()) { + continue; + } + + QString url = postObject.value(QStringLiteral("url")).toString(); + if (url.isEmpty() && !accountName.isEmpty()) { + url = QStringLiteral("%1/@%2/%3").arg(apiHost(accountId), accountName, identifier); + } + + const QString body = sanitizeContent(postObject.value(QStringLiteral("content")).toString()); + const int repliesCount = postObject.value(QStringLiteral("replies_count")).toInt(); + const int favouritesCount = postObject.value(QStringLiteral("favourites_count")).toInt(); + const int reblogsCount = postObject.value(QStringLiteral("reblogs_count")).toInt(); + const bool favourited = postObject.value(QStringLiteral("favourited")).toBool(); + const bool reblogged = postObject.value(QStringLiteral("reblogged")).toBool(); + + QList<QPair<QString, SocialPostImage::ImageType> > imageList; + const QJsonArray mediaAttachments = postObject.value(QStringLiteral("media_attachments")).toArray(); + foreach (const QJsonValue &attachmentValue, mediaAttachments) { + const QJsonObject attachment = attachmentValue.toObject(); + const QString mediaType = attachment.value(QStringLiteral("type")).toString(); + + QString mediaUrl; + SocialPostImage::ImageType imageType = SocialPostImage::Invalid; + if (mediaType == QLatin1String("image")) { + mediaUrl = attachment.value(QStringLiteral("url")).toString(); + imageType = SocialPostImage::Photo; + } else if (mediaType == QLatin1String("video") || mediaType == QLatin1String("gifv")) { + mediaUrl = attachment.value(QStringLiteral("preview_url")).toString(); + if (mediaUrl.isEmpty()) { + mediaUrl = attachment.value(QStringLiteral("url")).toString(); + } + imageType = SocialPostImage::Video; + } + + if (!mediaUrl.isEmpty() && imageType != SocialPostImage::Invalid) { + imageList.append(qMakePair(mediaUrl, imageType)); + } + } + + m_db.addFediversePost(identifier, + displayName, + accountName, + body, + eventTimestamp, + icon, + imageList, + url, + boostedBy, + repliesCount, + favouritesCount, + reblogsCount, + favourited, + reblogged, + apiHost(accountId), + iconPath(accountId), + accountId); + } + } else { + qCWarning(lcFediversePostsSync) << "unable to parse event feed data from request with account" << accountId + << ", got:" << QString::fromUtf8(replyData); + } + + decrementSemaphore(accountId); +} |
