/**************************************************************************** ** ** 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 #include #include #include #include #include #include // libaccounts-qt5 #include #include #include #include #include #define OPEN_URL_ACTION(openUrl) \ Notification::remoteAction( \ "default", \ "", \ "org.sailfishos.fileservice", \ "/", \ "org.sailfishos.fileservice", \ "openUrl", \ QVariantList() << openUrl \ ) namespace { const char *const NotificationCategory = "x-nemo.social.mastodon.notification"; const char *const NotificationIdHint = "x-nemo.sociald.notification-id"; const char *const LastFetchedNotificationIdKey = "LastFetchedNotificationId"; const int NotificationsPageLimit = 80; 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"); } bool hasActiveNotificationsForAccount(int accountId) { bool hasActiveNotifications = false; const QList notifications = Notification::notifications(); foreach (QObject *object, notifications) { Notification *notification = qobject_cast(object); if (notification && notification->category() == QLatin1String(NotificationCategory) && notification->hintValue("x-nemo.sociald.account-id").toInt() == accountId) { hasActiveNotifications = true; } delete object; } return hasActiveNotifications; } } MastodonNotificationsSyncAdaptor::MastodonNotificationsSyncAdaptor(QObject *parent) : MastodonNotificationsDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Notifications, parent) { setInitialActive(true); } MastodonNotificationsSyncAdaptor::~MastodonNotificationsSyncAdaptor() { } QString MastodonNotificationsSyncAdaptor::syncServiceName() const { return QStringLiteral("mastodon-microblog"); } void MastodonNotificationsSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode) { closeAccountNotifications(oldId); m_pendingSyncStates.remove(oldId); m_lastMarkedReadIds.remove(oldId); saveLastFetchedId(oldId, QString()); } void MastodonNotificationsSyncAdaptor::beginSync(int accountId, const QString &accessToken) { m_pendingSyncStates.remove(accountId); requestUnreadMarker(accountId, accessToken); } void MastodonNotificationsSyncAdaptor::finalize(int accountId) { if (syncAborted()) { qCInfo(lcSocialPlugin) << "sync aborted, won't update notifications"; } Q_UNUSED(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; } QString MastodonNotificationsSyncAdaptor::notificationObjectKey(int accountId, const QString ¬ificationId) { return QString::number(accountId) + QLatin1Char(':') + notificationId; } QString MastodonNotificationsSyncAdaptor::loadLastFetchedId(int accountId) const { Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, 0); if (!account) { return QString(); } Accounts::Service service(m_accountManager->service(syncServiceName())); account->selectService(service); const QString lastFetchedId = account->value(QString::fromLatin1(LastFetchedNotificationIdKey)).toString().trimmed(); account->deleteLater(); return lastFetchedId; } void MastodonNotificationsSyncAdaptor::saveLastFetchedId(int accountId, const QString &lastFetchedId) { Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, 0); if (!account) { return; } Accounts::Service service(m_accountManager->service(syncServiceName())); account->selectService(service); const QString storedId = account->value(QString::fromLatin1(LastFetchedNotificationIdKey)).toString().trimmed(); if (storedId != lastFetchedId) { account->setValue(QString::fromLatin1(LastFetchedNotificationIdKey), lastFetchedId); account->syncAndBlock(); } account->deleteLater(); } void MastodonNotificationsSyncAdaptor::requestUnreadMarker(int accountId, const QString &accessToken) { QUrl url(apiHost(accountId) + QStringLiteral("/api/v1/markers")); QUrlQuery query(url); query.addQueryItem(QStringLiteral("timeline[]"), QStringLiteral("notifications")); url.setQuery(query); QNetworkRequest request(url); request.setRawHeader("Authorization", QStringLiteral("Bearer %1").arg(accessToken).toUtf8()); QNetworkReply *reply = m_networkAccessManager->get(request); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(finishedUnreadMarkerHandler())); incrementSemaphore(accountId); setupReplyTimeout(accountId, reply); } else { qCWarning(lcSocialPlugin) << "unable to request notifications marker from Mastodon account with id" << accountId; } } void MastodonNotificationsSyncAdaptor::finishedUnreadMarkerHandler() { QNetworkReply *reply = qobject_cast(sender()); if (!reply) { return; } const bool isError = reply->property("isError").toBool(); const int accountId = reply->property("accountId").toInt(); const QString accessToken = reply->property("accessToken").toString(); const QByteArray replyData = reply->readAll(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); bool ok = false; const QJsonObject markerObject = parseJsonObjectReplyData(replyData, &ok); if (isError || !ok) { qCWarning(lcSocialPlugin) << "unable to parse notifications marker data from request with account" << accountId << ", got:" << QString::fromUtf8(replyData); decrementSemaphore(accountId); return; } const QString markerId = markerObject.value(QStringLiteral("notifications")) .toObject() .value(QStringLiteral("last_read_id")) .toVariant() .toString() .trimmed(); PendingSyncState state; state.accessToken = accessToken; state.unreadFloorId = markerId; state.lastFetchedId = loadLastFetchedId(accountId); if (state.lastFetchedId.isEmpty() && !markerId.isEmpty()) { // On first run, use the server unread marker floor to avoid historical flood. state.lastFetchedId = markerId; } m_pendingSyncStates.insert(accountId, state); requestNotifications(accountId, accessToken, markerId); decrementSemaphore(accountId); } void MastodonNotificationsSyncAdaptor::requestNotifications(int accountId, const QString &accessToken, const QString &minId, const QString &maxId) { QUrl url(apiHost(accountId) + QStringLiteral("/api/v1/notifications")); QUrlQuery query(url); query.addQueryItem(QStringLiteral("limit"), QString::number(NotificationsPageLimit)); if (!minId.isEmpty()) { query.addQueryItem(QStringLiteral("min_id"), minId); } if (!maxId.isEmpty()) { query.addQueryItem(QStringLiteral("max_id"), maxId); } url.setQuery(query); QNetworkRequest request(url); request.setRawHeader("Authorization", QStringLiteral("Bearer %1").arg(accessToken).toUtf8()); QNetworkReply *reply = m_networkAccessManager->get(request); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("accessToken", accessToken); reply->setProperty("minId", minId); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(finishedNotificationsHandler())); incrementSemaphore(accountId); setupReplyTimeout(accountId, reply); } else { qCWarning(lcSocialPlugin) << "unable to request notifications from Mastodon account with id" << accountId; } } void MastodonNotificationsSyncAdaptor::requestMarkRead(int accountId, const QString &accessToken, const QString &lastReadId) { QUrl url(apiHost(accountId) + QStringLiteral("/api/v1/markers")); QNetworkRequest request(url); request.setRawHeader("Authorization", QStringLiteral("Bearer %1").arg(accessToken).toUtf8()); request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded")); QUrlQuery query; query.addQueryItem(QStringLiteral("notifications[last_read_id]"), lastReadId); const QByteArray payload = query.toString(QUrl::FullyEncoded).toUtf8(); QNetworkReply *reply = m_networkAccessManager->post(request, payload); if (reply) { reply->setProperty("accountId", accountId); reply->setProperty("lastReadId", lastReadId); connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(errorHandler(QNetworkReply::NetworkError))); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsHandler(QList))); connect(reply, SIGNAL(finished()), this, SLOT(finishedMarkReadHandler())); incrementSemaphore(accountId); setupReplyTimeout(accountId, reply); } else { qCWarning(lcSocialPlugin) << "unable to update notifications marker for Mastodon account with id" << accountId; } } void MastodonNotificationsSyncAdaptor::finishedNotificationsHandler() { QNetworkReply *reply = qobject_cast(sender()); if (!reply) { return; } const bool isError = reply->property("isError").toBool(); const int accountId = reply->property("accountId").toInt(); const QString accessToken = reply->property("accessToken").toString(); const QString minId = reply->property("minId").toString(); const QByteArray replyData = reply->readAll(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); PendingSyncState state = m_pendingSyncStates.value(accountId); if (state.accessToken.isEmpty()) { state.accessToken = accessToken; } if (state.unreadFloorId.isEmpty() && !minId.isEmpty()) { state.unreadFloorId = minId; } if (state.lastFetchedId.isEmpty() && !state.unreadFloorId.isEmpty()) { state.lastFetchedId = state.unreadFloorId; } else if (state.lastFetchedId.isEmpty() && !minId.isEmpty()) { state.lastFetchedId = minId; } bool ok = false; const QJsonArray notifications = parseJsonArrayReplyData(replyData, &ok); if (!isError && ok) { if (!notifications.size()) { qCDebug(lcSocialPlugin) << "no notifications received for account" << accountId; m_pendingSyncStates.remove(accountId); decrementSemaphore(accountId); return; } QString pageMinNotificationId; foreach (const QJsonValue ¬ificationValue, notifications) { const QJsonObject notificationObject = notificationValue.toObject(); if (notificationObject.isEmpty()) { continue; } const QString notificationId = notificationObject.value(QStringLiteral("id")).toVariant().toString(); if (notificationId.isEmpty()) { continue; } if (pageMinNotificationId.isEmpty() || compareNotificationIds(notificationId, pageMinNotificationId) < 0) { pageMinNotificationId = notificationId; } if (state.maxFetchedId.isEmpty() || compareNotificationIds(notificationId, state.maxFetchedId) > 0) { state.maxFetchedId = notificationId; } if (!state.lastFetchedId.isEmpty() && compareNotificationIds(notificationId, state.lastFetchedId) <= 0) { continue; } const QString notificationType = notificationObject.value(QStringLiteral("type")).toString(); const QJsonObject actorObject = notificationObject.value(QStringLiteral("account")).toObject(); const QJsonValue statusValue = notificationObject.value(QStringLiteral("status")); const QJsonObject statusObject = statusValue.isObject() && !statusValue.isNull() ? statusValue.toObject() : QJsonObject(); QDateTime eventTimestamp = parseTimestamp(notificationObject.value(QStringLiteral("created_at")).toString()); if (!eventTimestamp.isValid()) { eventTimestamp = parseTimestamp(statusObject.value(QStringLiteral("created_at")).toString()); } if (!eventTimestamp.isValid()) { continue; } const QString displayName = displayNameForAccount(actorObject); const QString accountName = actorObject.value(QStringLiteral("acct")).toString(); const QString statusBody = sanitizeContent(statusObject.value(QStringLiteral("content")).toString()); const QString action = actionText(notificationType); QString body; if (notificationType == QLatin1String("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); } PendingNotification pendingNotification; pendingNotification.notificationId = notificationId; pendingNotification.summary = displayName; pendingNotification.body = body; pendingNotification.link = url; pendingNotification.timestamp = eventTimestamp; state.pendingNotifications.insert(notificationId, pendingNotification); } if (notifications.size() >= NotificationsPageLimit && !pageMinNotificationId.isEmpty() && (state.unreadFloorId.isEmpty() || compareNotificationIds(pageMinNotificationId, state.unreadFloorId) > 0)) { m_pendingSyncStates.insert(accountId, state); requestNotifications(accountId, state.accessToken, state.unreadFloorId, pageMinNotificationId); decrementSemaphore(accountId); return; } if (state.pendingNotifications.size() > 0) { QStringList notificationIds = state.pendingNotifications.keys(); std::sort(notificationIds.begin(), notificationIds.end(), [](const QString &left, const QString &right) { return compareNotificationIds(left, right) > 0; }); foreach (const QString ¬ificationId, notificationIds) { const PendingNotification pendingNotification = state.pendingNotifications.value(notificationId); publishSystemNotification(accountId, pendingNotification); } } if (!state.maxFetchedId.isEmpty() && (state.lastFetchedId.isEmpty() || compareNotificationIds(state.maxFetchedId, state.lastFetchedId) > 0)) { saveLastFetchedId(accountId, state.maxFetchedId); } const QString markerId = !state.maxFetchedId.isEmpty() ? state.maxFetchedId : state.lastFetchedId; const QString currentMarkerId = m_lastMarkedReadIds.value(accountId); if (!markerId.isEmpty() && !state.accessToken.isEmpty() && !hasActiveNotificationsForAccount(accountId) && (currentMarkerId.isEmpty() || compareNotificationIds(markerId, currentMarkerId) > 0)) { requestMarkRead(accountId, state.accessToken, markerId); } } 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::finishedMarkReadHandler() { QNetworkReply *reply = qobject_cast(sender()); if (!reply) { return; } const bool isError = reply->property("isError").toBool(); const int accountId = reply->property("accountId").toInt(); const QString lastReadId = reply->property("lastReadId").toString(); const QByteArray replyData = reply->readAll(); disconnect(reply); reply->deleteLater(); removeReplyTimeout(accountId, reply); bool ok = false; parseJsonObjectReplyData(replyData, &ok); if (!isError && ok) { const QString currentMarkerId = m_lastMarkedReadIds.value(accountId); if (currentMarkerId.isEmpty() || compareNotificationIds(lastReadId, currentMarkerId) > 0) { m_lastMarkedReadIds.insert(accountId, lastReadId); } } else { qCWarning(lcSocialPlugin) << "unable to update notifications marker for account" << accountId << ", got:" << QString::fromUtf8(replyData); } decrementSemaphore(accountId); } void MastodonNotificationsSyncAdaptor::publishSystemNotification(int accountId, const PendingNotification ¬ificationData) { Notification *notification = createNotification(accountId, notificationData.notificationId); notification->setItemCount(1); notification->setTimestamp(notificationData.timestamp.isValid() ? notificationData.timestamp : QDateTime::currentDateTimeUtc()); notification->setSummary(notificationData.summary.isEmpty() ? QStringLiteral("Mastodon") : notificationData.summary); notification->setBody(notificationData.body.isEmpty() ? QStringLiteral("New notification") : notificationData.body); notification->setPreviewSummary(notificationData.summary); notification->setPreviewBody(notificationData.body); const QString openUrl = notificationData.link.isEmpty() ? apiHost(accountId) + QStringLiteral("/notifications") : notificationData.link; const QUrl parsedOpenUrl(openUrl); const QString fallbackUrl = apiHost(accountId) + QStringLiteral("/notifications"); const QString safeOpenUrl = parsedOpenUrl.isValid() && !parsedOpenUrl.scheme().isEmpty() && !parsedOpenUrl.host().isEmpty() ? openUrl : fallbackUrl; notification->setRemoteAction(OPEN_URL_ACTION(safeOpenUrl)); notification->publish(); if (notification->replacesId() == 0) { qCWarning(lcSocialPlugin) << "failed to publish Mastodon notification" << notificationData.notificationId; } } void MastodonNotificationsSyncAdaptor::closeAccountNotifications(int accountId, const QSet &keepNotificationIds) { QStringList cachedKeys = m_notificationObjects.keys(); foreach (const QString &objectKey, cachedKeys) { Notification *notification = m_notificationObjects.value(objectKey); if (!notification || notification->hintValue("x-nemo.sociald.account-id").toInt() != accountId) { continue; } const QString notificationId = notification->hintValue(NotificationIdHint).toString(); if (!notificationId.isEmpty() && keepNotificationIds.contains(notificationId)) { continue; } notification->close(); m_notificationObjects.remove(objectKey); notification->deleteLater(); } QList notifications = Notification::notifications(); foreach (QObject *object, notifications) { Notification *notification = qobject_cast(object); if (!notification) { delete object; continue; } if (notification->category() == QLatin1String(NotificationCategory) && notification->hintValue("x-nemo.sociald.account-id").toInt() == accountId) { const QString notificationId = notification->hintValue(NotificationIdHint).toString(); if (notificationId.isEmpty() || !keepNotificationIds.contains(notificationId)) { notification->close(); } } if (notification->parent() != this) { delete notification; } } } Notification *MastodonNotificationsSyncAdaptor::createNotification(int accountId, const QString ¬ificationId) { const QString objectKey = notificationObjectKey(accountId, notificationId); Notification *notification = m_notificationObjects.value(objectKey); if (!notification) { notification = findNotification(accountId, notificationId); } if (!notification) { notification = new Notification(this); } else if (notification->parent() != this) { notification->setParent(this); } notification->setAppName(QStringLiteral("Mastodon")); notification->setAppIcon(QStringLiteral("icon-l-mastodon")); notification->setHintValue("x-nemo.sociald.account-id", accountId); notification->setHintValue(NotificationIdHint, notificationId); notification->setHintValue("x-nemo-feedback", QStringLiteral("social")); notification->setHintValue("x-nemo-priority", 100); // Show on lockscreen notification->setCategory(QLatin1String(NotificationCategory)); m_notificationObjects.insert(objectKey, notification); return notification; } Notification *MastodonNotificationsSyncAdaptor::findNotification(int accountId, const QString ¬ificationId) { Notification *notification = 0; QList notifications = Notification::notifications(); foreach (QObject *object, notifications) { Notification *castedNotification = qobject_cast(object); if (castedNotification && castedNotification->category() == QLatin1String(NotificationCategory) && castedNotification->hintValue("x-nemo.sociald.account-id").toInt() == accountId && castedNotification->hintValue(NotificationIdHint).toString() == notificationId) { notification = castedNotification; break; } } if (notification) { notifications.removeAll(notification); } qDeleteAll(notifications); return notification; }