summaryrefslogtreecommitdiff
path: root/buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostssyncadaptor.cpp
diff options
context:
space:
mode:
authorAndrew Branson <andrew.branson@jolla.com>2026-04-03 22:55:30 +0200
committerAndrew Branson <andrew.branson@jolla.com>2026-04-04 11:55:25 +0200
commita35c9fa159173388d88ef77e1d31f53488aad094 (patch)
treee4691b5bbf054ca13e35d98d9df653bf9cdc0054 /buteo-plugins/buteo-sync-plugin-fediverse-posts/fediversepostssyncadaptor.cpp
parent5f999f7a4712c4a4d1c89054b544064cfd4b769e (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.cpp260
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 &timestampString)
+{
+ return FediverseTextUtils::parseTimestamp(timestampString);
+}
+
+void FediversePostsSyncAdaptor::requestPosts(int accountId, const QString &accessToken)
+{
+ QUrl url(apiHost(accountId) + QStringLiteral("/api/v1/timelines/home"));
+
+ QUrlQuery query(url);
+ query.addQueryItem(QStringLiteral("limit"), QStringLiteral("20"));
+ url.setQuery(query);
+
+ QNetworkRequest request(url);
+ request.setRawHeader("Authorization", QStringLiteral("Bearer %1").arg(accessToken).toUtf8());
+
+ QNetworkReply *reply = m_networkAccessManager->get(request);
+ if (reply) {
+ reply->setProperty("accountId", accountId);
+ connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError)));
+ connect(reply, SIGNAL(sslErrors(QList<QSslError>)), this, SLOT(sslErrorsHandler(QList<QSslError>)));
+ connect(reply, SIGNAL(finished()), this, SLOT(finishedPostsHandler()));
+
+ incrementSemaphore(accountId);
+ setupReplyTimeout(accountId, reply);
+ } else {
+ qCWarning(lcFediversePostsSync) << "unable to request home timeline posts from Fediverse account with id" << accountId;
+ }
+}
+
+void FediversePostsSyncAdaptor::finishedPostsHandler()
+{
+ QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
+ if (!reply) {
+ return;
+ }
+
+ const bool isError = reply->property("isError").toBool();
+ const int accountId = reply->property("accountId").toInt();
+ QByteArray replyData = reply->readAll();
+
+ disconnect(reply);
+ reply->deleteLater();
+ removeReplyTimeout(accountId, reply);
+
+ bool ok = false;
+ QJsonArray statuses = parseJsonArrayReplyData(replyData, &ok);
+ if (!isError && ok) {
+ m_db.removePosts(accountId);
+
+ if (!statuses.size()) {
+ qCDebug(lcFediversePostsSync) << "no feed posts received for account" << accountId;
+ decrementSemaphore(accountId);
+ return;
+ }
+
+ const int sinceSpan = m_accountSyncProfile
+ ? m_accountSyncProfile->key(Buteo::KEY_SYNC_SINCE_DAYS_PAST, QStringLiteral("7")).toInt()
+ : 7;
+
+ foreach (const QJsonValue &statusValue, statuses) {
+ const QJsonObject statusObject = statusValue.toObject();
+ if (statusObject.isEmpty()) {
+ continue;
+ }
+
+ QJsonObject postObject = statusObject;
+ QString boostedBy;
+ if (statusObject.contains(QStringLiteral("reblog"))
+ && statusObject.value(QStringLiteral("reblog")).isObject()
+ && !statusObject.value(QStringLiteral("reblog")).isNull()) {
+ boostedBy = displayNameForAccount(statusObject.value(QStringLiteral("account")).toObject());
+ postObject = statusObject.value(QStringLiteral("reblog")).toObject();
+ }
+
+ QDateTime eventTimestamp = parseTimestamp(statusObject.value(QStringLiteral("created_at")).toString());
+ if (!eventTimestamp.isValid()) {
+ eventTimestamp = parseTimestamp(postObject.value(QStringLiteral("created_at")).toString());
+ }
+ if (!eventTimestamp.isValid()) {
+ continue;
+ }
+
+ if (eventTimestamp.daysTo(QDateTime::currentDateTime()) > sinceSpan) {
+ continue;
+ }
+
+ const QJsonObject account = postObject.value(QStringLiteral("account")).toObject();
+ const QString displayName = displayNameForAccount(account);
+ const QString accountName = account.value(QStringLiteral("acct")).toString();
+ QString icon = account.value(QStringLiteral("avatar_static")).toString();
+ if (icon.isEmpty()) {
+ icon = account.value(QStringLiteral("avatar")).toString();
+ }
+
+ QString identifier = postObject.value(QStringLiteral("id")).toVariant().toString();
+ if (identifier.isEmpty()) {
+ continue;
+ }
+
+ QString url = postObject.value(QStringLiteral("url")).toString();
+ if (url.isEmpty() && !accountName.isEmpty()) {
+ url = QStringLiteral("%1/@%2/%3").arg(apiHost(accountId), accountName, identifier);
+ }
+
+ const QString body = sanitizeContent(postObject.value(QStringLiteral("content")).toString());
+ const int repliesCount = postObject.value(QStringLiteral("replies_count")).toInt();
+ const int favouritesCount = postObject.value(QStringLiteral("favourites_count")).toInt();
+ const int reblogsCount = postObject.value(QStringLiteral("reblogs_count")).toInt();
+ const bool favourited = postObject.value(QStringLiteral("favourited")).toBool();
+ const bool reblogged = postObject.value(QStringLiteral("reblogged")).toBool();
+
+ QList<QPair<QString, SocialPostImage::ImageType> > imageList;
+ const QJsonArray mediaAttachments = postObject.value(QStringLiteral("media_attachments")).toArray();
+ foreach (const QJsonValue &attachmentValue, mediaAttachments) {
+ const QJsonObject attachment = attachmentValue.toObject();
+ const QString mediaType = attachment.value(QStringLiteral("type")).toString();
+
+ QString mediaUrl;
+ SocialPostImage::ImageType imageType = SocialPostImage::Invalid;
+ if (mediaType == QLatin1String("image")) {
+ mediaUrl = attachment.value(QStringLiteral("url")).toString();
+ imageType = SocialPostImage::Photo;
+ } else if (mediaType == QLatin1String("video") || mediaType == QLatin1String("gifv")) {
+ mediaUrl = attachment.value(QStringLiteral("preview_url")).toString();
+ if (mediaUrl.isEmpty()) {
+ mediaUrl = attachment.value(QStringLiteral("url")).toString();
+ }
+ imageType = SocialPostImage::Video;
+ }
+
+ if (!mediaUrl.isEmpty() && imageType != SocialPostImage::Invalid) {
+ imageList.append(qMakePair(mediaUrl, imageType));
+ }
+ }
+
+ m_db.addFediversePost(identifier,
+ displayName,
+ accountName,
+ body,
+ eventTimestamp,
+ icon,
+ imageList,
+ url,
+ boostedBy,
+ repliesCount,
+ favouritesCount,
+ reblogsCount,
+ favourited,
+ reblogged,
+ apiHost(accountId),
+ iconPath(accountId),
+ accountId);
+ }
+ } else {
+ qCWarning(lcFediversePostsSync) << "unable to parse event feed data from request with account" << accountId
+ << ", got:" << QString::fromUtf8(replyData);
+ }
+
+ decrementSemaphore(accountId);
+}