Improve the handling of notifications

The aim is to put some additional filtering in place to better stop floods of old notifications. This is achieved with a couple of new filters and better tracking of old notifications.

- Make sure to paginate through all notification on initialization to ensure they are all added to old notifications. While we were not previously putting a limit on the number of returned notifications the server can and will do this when there are a very large amount.
- Find the newest timestamp for each connection on initialization and don't post any notifications with an earlier timestamp.
- Track old notifications on a per-connection basis.

Closes network/neochat#358 and network/neochat#423
This commit is contained in:
James Graham
2023-06-05 16:42:01 +00:00
parent af136943c3
commit b5c781212c
4 changed files with 170 additions and 83 deletions

View File

@@ -45,7 +45,6 @@
#include <qt_connection_util.h>
#ifdef QUOTIENT_07
#include <csapi/notifications.h>
#include <eventstats.h>
#endif
@@ -120,8 +119,8 @@ Controller::Controller(QObject *parent)
connect(&Accounts, &AccountRegistry::accountCountChanged, this, [this]() {
if (Accounts.size() > oldAccountCount) {
auto connection = Accounts.accounts()[Accounts.size() - 1];
connect(connection, &Connection::syncDone, this, [this, connection]() {
handleNotifications(connection);
connect(connection, &Connection::syncDone, this, [connection]() {
NotificationsManager::instance().handleNotifications(connection);
});
}
oldAccountCount = Accounts.size();
@@ -129,81 +128,6 @@ Controller::Controller(QObject *parent)
#endif
}
#ifdef QUOTIENT_07
void Controller::handleNotifications(QPointer<Quotient::Connection> connection)
{
static QStringList initial;
static QStringList oldNotifications;
auto job = connection->callApi<GetNotificationsJob>();
connect(job, &BaseJob::success, this, [job, connection]() {
const auto notifications = job->jsonData()["notifications"].toArray();
if (!initial.contains(connection->user()->id())) {
initial.append(connection->user()->id());
for (const auto &n : notifications) {
oldNotifications += n.toObject()["event"].toObject()["event_id"].toString();
}
return;
}
for (const auto &n : notifications) {
const auto notification = n.toObject();
if (notification["read"].toBool()) {
continue;
}
if (oldNotifications.contains(notification["event"].toObject()["event_id"].toString())) {
continue;
}
oldNotifications += notification["event"].toObject()["event_id"].toString();
auto room = connection->room(notification["room_id"].toString());
// If room exists, room is NOT active OR the application is NOT active, show notification
if (room
&& !(RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id()
&& QGuiApplication::applicationState() == Qt::ApplicationActive)) {
// The room might have been deleted (for example rejected invitation).
auto sender = room->user(notification["event"].toObject()["sender"].toString());
QString body;
if (notification["event"].toObject()["type"].toString() == "org.matrix.msc3381.poll.start") {
body = notification["event"]
.toObject()["content"]
.toObject()["org.matrix.msc3381.poll.start"]
.toObject()["question"]
.toObject()["body"]
.toString();
} else {
body = notification["event"].toObject()["content"].toObject()["body"].toString();
}
if (notification["event"]["type"] == "m.room.encrypted") {
#ifdef Quotient_E2EE_ENABLED
auto decrypted = connection->decryptNotification(notification);
body = decrypted["content"].toObject()["body"].toString();
#endif
if (body.isEmpty()) {
body = i18n("Encrypted Message");
}
}
QImage avatar_image;
if (!sender->avatarUrl(room).isEmpty()) {
avatar_image = sender->avatar(128, room);
} else {
avatar_image = room->avatar(128);
}
NotificationsManager::instance().postNotification(dynamic_cast<NeoChatRoom *>(room),
sender->displayname(room),
body,
avatar_image,
notification["event"].toObject()["event_id"].toString(),
true);
}
}
});
}
#endif
Controller &Controller::instance()
{
static Controller _instance;

View File

@@ -230,9 +230,6 @@ private:
QMap<Quotient::Room *, int> m_notificationCounts;
bool hasWindowSystem() const;
#ifdef QUOTIENT_07
void handleNotifications(QPointer<Quotient::Connection> connection);
#endif
private Q_SLOTS:
void invokeLogin();

View File

@@ -5,7 +5,7 @@
#include <memory>
#include <QJsonArray>
#include <QGuiApplication>
#include <KLocalizedString>
#include <KNotification>
@@ -18,6 +18,7 @@
#endif
#include <connection.h>
#include <csapi/notifications.h>
#include <csapi/pushrules.h>
#include <jobs/basejob.h>
#include <user.h>
@@ -48,6 +49,148 @@ NotificationsManager::NotificationsManager(QObject *parent)
});
}
#ifdef QUOTIENT_07
void NotificationsManager::handleNotifications(QPointer<Connection> connection)
{
if (!m_connActiveJob.contains(connection->user()->id())) {
auto job = connection->callApi<GetNotificationsJob>();
m_connActiveJob.append(connection->user()->id());
connect(job, &BaseJob::success, this, [this, job, connection]() {
m_connActiveJob.removeAll(connection->user()->id());
processNotificationJob(connection, job, !m_oldNotifications.contains(connection->user()->id()));
});
}
}
#endif
void NotificationsManager::processNotificationJob(QPointer<Quotient::Connection> connection, Quotient::GetNotificationsJob *job, bool initialization)
{
if (job == nullptr) {
return;
}
if (connection == nullptr) {
qWarning() << QStringLiteral("No connection for GetNotificationsJob %1").arg(job->objectName());
return;
}
const auto connectionId = connection->user()->id();
// If pagination has occurred set off the next job
auto nextToken = job->jsonData()["next_token"].toString();
if (!nextToken.isEmpty()) {
auto nextJob = connection->callApi<GetNotificationsJob>(nextToken);
m_connActiveJob.append(connectionId);
connect(nextJob, &BaseJob::success, this, [this, nextJob, connection, initialization]() {
m_connActiveJob.removeAll(connection->user()->id());
processNotificationJob(connection, nextJob, initialization);
});
}
const auto notifications = job->jsonData()["notifications"].toArray();
if (initialization) {
m_oldNotifications[connectionId] = QStringList();
for (const auto &n : notifications) {
if (!m_initialTimestamp.contains(connectionId)) {
m_initialTimestamp[connectionId] = n.toObject()["ts"].toDouble();
} else {
qint64 timestamp = n.toObject()["ts"].toDouble();
if (timestamp > m_initialTimestamp[connectionId]) {
m_initialTimestamp[connectionId] = timestamp;
}
}
auto connectionNotifications = m_oldNotifications.value(connectionId);
connectionNotifications += n.toObject()["event"].toObject()["event_id"].toString();
m_oldNotifications[connectionId] = connectionNotifications;
}
return;
}
for (const auto &n : notifications) {
const auto notification = n.toObject();
if (notification["read"].toBool()) {
continue;
}
auto connectionNotifications = m_oldNotifications.value(connectionId);
if (connectionNotifications.contains(notification["event"].toObject()["event_id"].toString())) {
continue;
}
connectionNotifications += notification["event"].toObject()["event_id"].toString();
m_oldNotifications[connectionId] = connectionNotifications;
auto room = connection->room(notification["room_id"].toString());
if (shouldPostNotification(connection, n)) {
// The room might have been deleted (for example rejected invitation).
auto sender = room->user(notification["event"].toObject()["sender"].toString());
QString body;
if (notification["event"].toObject()["type"].toString() == "org.matrix.msc3381.poll.start") {
body = notification["event"]
.toObject()["content"]
.toObject()["org.matrix.msc3381.poll.start"]
.toObject()["question"]
.toObject()["body"]
.toString();
} else {
body = notification["event"].toObject()["content"].toObject()["body"].toString();
}
if (notification["event"]["type"] == "m.room.encrypted") {
#ifdef Quotient_E2EE_ENABLED
auto decrypted = connection->decryptNotification(notification);
body = decrypted["content"].toObject()["body"].toString();
#endif
if (body.isEmpty()) {
body = i18n("Encrypted Message");
}
}
QImage avatar_image;
if (!sender->avatarUrl(room).isEmpty()) {
avatar_image = sender->avatar(128, room);
} else {
avatar_image = room->avatar(128);
}
postNotification(dynamic_cast<NeoChatRoom *>(room),
sender->displayname(room),
body,
avatar_image,
notification["event"].toObject()["event_id"].toString(),
true);
}
}
}
bool NotificationsManager::shouldPostNotification(QPointer<Quotient::Connection> connection, const QJsonValue &notification)
{
if (connection == nullptr) {
return false;
}
auto room = connection->room(notification["room_id"].toString());
if (room == nullptr) {
return false;
}
// If the room is the current room and the application is active the notification
// should not be shown.
// This is setup so that if the application is inactive the notification will
// always be posted, even if the room is the current room.
bool isCurrentRoom = RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id();
if (isCurrentRoom && QGuiApplication::applicationState() == Qt::ApplicationActive) {
return false;
}
// If the notification timestamp is earlier than the initial timestamp assume
// the notification is old and shouldn't be posted.
qint64 timestamp = notification["ts"].toDouble();
if (timestamp < m_initialTimestamp[connection->user()->id()]) {
return false;
}
return true;
}
void NotificationsManager::postNotification(NeoChatRoom *room,
const QString &sender,
const QString &text,

View File

@@ -4,11 +4,18 @@
#pragma once
#include <QImage>
#include <QJsonObject>
#include <QMap>
#include <QObject>
#include <QPointer>
#include <QString>
#include <QJsonObject>
#include <csapi/notifications.h>
#include <jobs/basejob.h>
namespace Quotient
{
class Connection;
}
class KNotification;
class NeoChatRoom;
@@ -181,9 +188,23 @@ public:
*/
QVector<QVariant> getKeywordNotificationActions();
#ifdef QUOTIENT_07
/**
* @brief Handle the notifications for the given connection.
*/
void handleNotifications(QPointer<Quotient::Connection> connection);
#endif
private:
NotificationsManager(QObject *parent = nullptr);
QHash<QString, qint64> m_initialTimestamp;
QHash<QString, QStringList> m_oldNotifications;
QStringList m_connActiveJob;
bool shouldPostNotification(QPointer<Quotient::Connection> connection, const QJsonValue &notification);
QHash<QString, KNotification *> m_notifications;
QHash<QString, QPointer<KNotification>> m_invitations;
@@ -218,6 +239,8 @@ private:
QVector<QVariant> toActions(PushNotificationAction::Action action, const QString &sound = "default");
private Q_SLOTS:
void processNotificationJob(QPointer<Quotient::Connection> connection, Quotient::GetNotificationsJob *job, bool initialization);
void updateNotificationRules(const QString &type);
Q_SIGNALS: