Files
neochat/src/notificationsmanager.cpp
Shooting Star 8479e51051 Check if room is null before enter room
There will be some case, like the notify is still exist, but user have
logout their account, user leave the room, these cause the room become a
nullptr, and when click the notify message, it make neochat coredump
2023-04-17 19:25:53 +00:00

460 lines
18 KiB
C++

// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "notificationsmanager.h"
#include <memory>
#include <QJsonArray>
#include <KLocalizedString>
#include <KNotification>
#include <KNotificationReplyAction>
#ifdef QUOTIENT_07
#include <accountregistry.h>
#else
#include "neochataccountregistry.h"
#endif
#include <connection.h>
#include <csapi/pushrules.h>
#include <jobs/basejob.h>
#include <user.h>
#include "controller.h"
#include "neochatconfig.h"
#include "neochatroom.h"
#include "roommanager.h"
#include "texthandler.h"
#include "windowcontroller.h"
using namespace Quotient;
NotificationsManager &NotificationsManager::instance()
{
static NotificationsManager _instance;
return _instance;
}
NotificationsManager::NotificationsManager(QObject *parent)
: QObject(parent)
{
// Can't connect the signal up until the active connection has been established by the controller
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this]() {
connect(Controller::instance().activeConnection(), &Connection::accountDataChanged, this, &NotificationsManager::updateNotificationRules);
// Ensure that the push rule states are retrieved after the connection is changed
updateNotificationRules("m.push_rules");
});
}
void NotificationsManager::postNotification(NeoChatRoom *room,
const QString &sender,
const QString &text,
const QImage &icon,
const QString &replyEventId,
bool canReply)
{
QPixmap img;
img.convertFromImage(icon);
KNotification *notification = new KNotification("message");
if (sender == room->displayName()) {
notification->setTitle(sender);
} else {
notification->setTitle(i18n("%1 (%2)", sender, room->displayName()));
}
notification->setText(text.toHtmlEscaped());
notification->setPixmap(img);
notification->setDefaultAction(i18n("Open NeoChat in this room"));
connect(notification, &KNotification::defaultActivated, this, [=]() {
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
if (!room) {
return;
}
if (room->localUser()->id() != Controller::instance().activeConnection()->userId()) {
#ifdef QUOTIENT_07
Controller::instance().setActiveConnection(Accounts.get(room->localUser()->id()));
#else
Controller::instance().setActiveConnection(AccountRegistry::instance().get(room->localUser()->id()));
#endif
}
RoomManager::instance().enterRoom(room);
});
if (canReply) {
std::unique_ptr<KNotificationReplyAction> replyAction(new KNotificationReplyAction(i18n("Reply")));
replyAction->setPlaceholderText(i18n("Reply..."));
connect(replyAction.get(), &KNotificationReplyAction::replied, this, [room, replyEventId](const QString &text) {
TextHandler textHandler;
textHandler.setData(text);
room->postMessage(text, textHandler.handleSendText(), RoomMessageEvent::MsgType::Text, replyEventId, QString());
});
notification->setReplyAction(std::move(replyAction));
}
notification->setHint(QStringLiteral("x-kde-origin-name"), room->localUser()->id());
notification->sendEvent();
m_notifications.insert(room->id(), notification);
}
void NotificationsManager::postInviteNotification(NeoChatRoom *room, const QString &title, const QString &sender, const QImage &icon)
{
QPixmap img;
img.convertFromImage(icon);
KNotification *notification = new KNotification("invite");
notification->setText(i18n("%1 invited you to a room", sender));
notification->setTitle(title);
notification->setPixmap(img);
notification->setFlags(KNotification::Persistent);
notification->setDefaultAction(i18n("Open this invitation in NeoChat"));
connect(notification, &KNotification::defaultActivated, this, [=]() {
WindowController::instance().showAndRaiseWindow(notification->xdgActivationToken());
notification->close();
RoomManager::instance().enterRoom(room);
});
notification->setActions({i18n("Accept Invitation"), i18n("Reject Invitation")});
connect(notification, &KNotification::action1Activated, this, [room, notification]() {
if (!room) {
return;
}
room->acceptInvitation();
notification->close();
});
connect(notification, &KNotification::action2Activated, this, [room, notification]() {
if (!room) {
return;
}
RoomManager::instance().leaveRoom(room);
notification->close();
});
connect(notification, &KNotification::closed, this, [this, room]() {
if (!room) {
return;
}
m_invitations.remove(room->id());
});
notification->setHint(QStringLiteral("x-kde-origin-name"), room->localUser()->id());
notification->sendEvent();
m_invitations.insert(room->id(), notification);
}
void NotificationsManager::clearInvitationNotification(const QString &roomId)
{
if (m_invitations.contains(roomId)) {
m_invitations[roomId]->close();
}
}
/**
* The master push rule sets all notifications to off when enabled
* see https://spec.matrix.org/v1.3/client-server-api/#default-override-rules
* therefore to enable push rules the master rule needs to be disabled and vice versa
*/
void NotificationsManager::setGlobalNotificationsEnabled(bool enabled)
{
setNotificationRuleEnabled("override", ".m.rule.master", !enabled);
}
void NotificationsManager::setOneToOneNotificationAction(PushNotificationAction::Action action)
{
setNotificationRuleActions("underride", ".m.rule.room_one_to_one", action);
}
void NotificationsManager::setEncryptedOneToOneNotificationAction(PushNotificationAction::Action action)
{
setNotificationRuleActions("underride", ".m.rule.encrypted_room_one_to_one", action);
}
void NotificationsManager::setGroupChatNotificationAction(PushNotificationAction::Action action)
{
setNotificationRuleActions("underride", ".m.rule.message", action);
}
void NotificationsManager::setEncryptedGroupChatNotificationAction(PushNotificationAction::Action action)
{
setNotificationRuleActions("underride", ".m.rule.encrypted", action);
}
/*
* .m.rule.contains_display_name is an override rule so it needs to be disabled when off
* so that other rules can match the message if they apply.
*/
void NotificationsManager::setDisplayNameNotificationAction(PushNotificationAction::Action action)
{
if (action == PushNotificationAction::Off) {
setNotificationRuleEnabled("override", ".m.rule.contains_display_name", false);
} else {
setNotificationRuleActions("override", ".m.rule.contains_display_name", action);
setNotificationRuleEnabled("override", ".m.rule.contains_display_name", true);
}
}
/*
* .m.rule.roomnotif is an override rule so it needs to be disabled when off
* so that other rules can match the message if they apply.
*/
void NotificationsManager::setRoomNotificationAction(PushNotificationAction::Action action)
{
if (action == PushNotificationAction::Off) {
setNotificationRuleEnabled("override", ".m.rule.roomnotif", false);
} else {
setNotificationRuleActions("override", ".m.rule.roomnotif", action);
setNotificationRuleEnabled("override", ".m.rule.roomnotif", true);
}
}
void NotificationsManager::initializeKeywordNotificationAction()
{
m_keywordNotificationAction = PushNotificationAction::Highlight;
Q_EMIT keywordNotificationActionChanged(m_keywordNotificationAction);
}
void NotificationsManager::deactivateKeywordNotificationAction()
{
m_keywordNotificationAction = PushNotificationAction::Off;
Q_EMIT keywordNotificationActionChanged(m_keywordNotificationAction);
}
QVector<QVariant> NotificationsManager::getKeywordNotificationActions()
{
return toActions(m_keywordNotificationAction);
}
void NotificationsManager::setKeywordNotificationAction(PushNotificationAction::Action action)
{
// Unlike the other rules this needs to be set here for the case where there are no keyords.
m_keywordNotificationAction = action;
Q_EMIT keywordNotificationActionChanged(m_keywordNotificationAction);
const QJsonObject accountData = Controller::instance().activeConnection()->accountDataJson("m.push_rules");
const QJsonArray contentRuleArray = accountData["global"].toObject()["content"].toArray();
for (const auto &i : contentRuleArray) {
const QJsonObject contentRule = i.toObject();
if (contentRule["rule_id"].toString()[0] != '.') {
setNotificationRuleActions("content", contentRule["rule_id"].toString(), action);
}
}
}
/*
* .m.rule.invite_for_me is an override rule so it needs to be disabled when off
* so that other rules can match the message if they apply.
*/
void NotificationsManager::setInviteNotificationAction(PushNotificationAction::Action action)
{
if (action == PushNotificationAction::Off) {
setNotificationRuleEnabled("override", ".m.rule.invite_for_me", false);
} else {
setNotificationRuleActions("override", ".m.rule.invite_for_me", action);
setNotificationRuleEnabled("override", ".m.rule.invite_for_me", true);
}
}
void NotificationsManager::setCallInviteNotificationAction(PushNotificationAction::Action action)
{
setNotificationRuleActions("underride", ".m.rule.call", action);
}
/*
* .m.rule.tombstone is an override rule so it needs to be disabled when off
* so that other rules can match the message if they apply.
*/
void NotificationsManager::setTombstoneNotificationAction(PushNotificationAction::Action action)
{
if (action == PushNotificationAction::Off) {
setNotificationRuleEnabled("override", ".m.rule.tombstone", false);
} else {
setNotificationRuleActions("override", ".m.rule.tombstone", action);
setNotificationRuleEnabled("override", ".m.rule.tombstone", true);
}
}
void NotificationsManager::updateNotificationRules(const QString &type)
{
if (type != "m.push_rules") {
return;
}
if (!Controller::instance().activeConnection()) {
return;
}
const QJsonObject accountData = Controller::instance().activeConnection()->accountDataJson("m.push_rules");
// Update override rules
const QJsonArray overrideRuleArray = accountData["global"].toObject()["override"].toArray();
for (const auto &i : overrideRuleArray) {
const QJsonObject overrideRule = i.toObject();
if (overrideRule["rule_id"] == ".m.rule.master") {
bool ruleEnabled = overrideRule["enabled"].toBool();
m_globalNotificationsEnabled = !ruleEnabled;
if (!m_globalNotificationsSet) {
m_globalNotificationsSet = true;
}
Q_EMIT globalNotificationsEnabledChanged(m_globalNotificationsEnabled);
}
const PushNotificationAction::Action action = toAction(overrideRule);
if (overrideRule["rule_id"] == ".m.rule.contains_display_name") {
m_displayNameNotificationAction = action;
Q_EMIT displayNameNotificationActionChanged(m_displayNameNotificationAction);
} else if (overrideRule["rule_id"] == ".m.rule.roomnotif") {
m_roomNotificationAction = action;
Q_EMIT roomNotificationActionChanged(m_roomNotificationAction);
} else if (overrideRule["rule_id"] == ".m.rule.invite_for_me") {
m_inviteNotificationAction = action;
Q_EMIT inviteNotificationActionChanged(m_inviteNotificationAction);
} else if (overrideRule["rule_id"] == ".m.rule.tombstone") {
m_tombstoneNotificationAction = action;
Q_EMIT tombstoneNotificationActionChanged(m_tombstoneNotificationAction);
}
}
// Update content rules
const QJsonArray contentRuleArray = accountData["global"].toObject()["content"].toArray();
PushNotificationAction::Action keywordAction = PushNotificationAction::Unknown;
for (const auto &i : contentRuleArray) {
const QJsonObject contentRule = i.toObject();
const PushNotificationAction::Action action = toAction(contentRule);
bool actionMismatch = false;
if (contentRule["rule_id"].toString()[0] != '.' && !actionMismatch) {
if (keywordAction == PushNotificationAction::Unknown) {
keywordAction = action;
m_keywordNotificationAction = action;
Q_EMIT keywordNotificationActionChanged(m_keywordNotificationAction);
} else if (action != keywordAction) {
actionMismatch = true;
m_keywordNotificationAction = PushNotificationAction::On;
Q_EMIT keywordNotificationActionChanged(m_keywordNotificationAction);
}
}
}
// If there are no keywords set the state to off, this is the only time it'll be in the off state
if (keywordAction == PushNotificationAction::Unknown) {
m_keywordNotificationAction = PushNotificationAction::Off;
Q_EMIT keywordNotificationActionChanged(m_keywordNotificationAction);
}
// Update underride rules
const QJsonArray underrideRuleArray = accountData["global"].toObject()["underride"].toArray();
for (const auto &i : underrideRuleArray) {
const QJsonObject underrideRule = i.toObject();
const PushNotificationAction::Action action = toAction(underrideRule);
if (underrideRule["rule_id"] == ".m.rule.room_one_to_one") {
m_oneToOneNotificationAction = action;
Q_EMIT oneToOneNotificationActionChanged(m_oneToOneNotificationAction);
} else if (underrideRule["rule_id"] == ".m.rule.encrypted_room_one_to_one") {
m_encryptedOneToOneNotificationAction = action;
Q_EMIT encryptedOneToOneNotificationActionChanged(m_encryptedOneToOneNotificationAction);
} else if (underrideRule["rule_id"] == ".m.rule.message") {
m_groupChatNotificationAction = action;
Q_EMIT groupChatNotificationActionChanged(m_groupChatNotificationAction);
} else if (underrideRule["rule_id"] == ".m.rule.encrypted") {
m_encryptedGroupChatNotificationAction = action;
Q_EMIT encryptedGroupChatNotificationActionChanged(m_encryptedGroupChatNotificationAction);
} else if (underrideRule["rule_id"] == ".m.rule.call") {
m_callInviteNotificationAction = action;
Q_EMIT callInviteNotificationActionChanged(m_callInviteNotificationAction);
}
}
}
void NotificationsManager::setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled)
{
auto job = Controller::instance().activeConnection()->callApi<IsPushRuleEnabledJob>("global", kind, ruleId);
connect(job, &BaseJob::success, this, [job, kind, ruleId, enabled]() {
if (job->enabled() != enabled) {
Controller::instance().activeConnection()->callApi<SetPushRuleEnabledJob>("global", kind, ruleId, enabled);
}
});
}
void NotificationsManager::setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action)
{
QVector<QVariant> actions;
if (ruleId == ".m.rule.call") {
actions = toActions(action, "ring");
} else {
actions = toActions(action);
}
Controller::instance().activeConnection()->callApi<SetPushRuleActionsJob>("global", kind, ruleId, actions);
}
PushNotificationAction::Action NotificationsManager::toAction(const QJsonObject &rule)
{
const QJsonArray actions = rule["actions"].toArray();
bool isNoisy = false;
bool highlightEnabled = false;
const bool enabled = rule["enabled"].toBool();
for (const auto &i : actions) {
QJsonObject action = i.toObject();
if (action["set_tweak"].toString() == "sound") {
isNoisy = true;
} else if (action["set_tweak"].toString() == "highlight") {
if (action["value"].toString() != "false") {
highlightEnabled = true;
}
}
}
if (!enabled) {
return PushNotificationAction::Off;
}
if (actions[0] == "notify") {
if (isNoisy && highlightEnabled) {
return PushNotificationAction::NoisyHighlight;
} else if (isNoisy) {
return PushNotificationAction::Noisy;
} else if (highlightEnabled) {
return PushNotificationAction::Highlight;
} else {
return PushNotificationAction::On;
}
} else {
return PushNotificationAction::Off;
}
}
QVector<QVariant> NotificationsManager::toActions(PushNotificationAction::Action action, const QString &sound)
{
// The caller should never try to set the state to unknown.
// It exists only as a default state to diable the settings options until the actual state is retrieved from the server.
if (action == PushNotificationAction::Unknown) {
Q_ASSERT(false);
return QVector<QVariant>();
}
QVector<QVariant> actions;
if (action != PushNotificationAction::Off) {
actions.append("notify");
} else {
actions.append("dont_notify");
}
if (action == PushNotificationAction::Noisy || action == PushNotificationAction::NoisyHighlight) {
QJsonObject soundTweak;
soundTweak.insert("set_tweak", "sound");
soundTweak.insert("value", sound);
actions.append(soundTweak);
}
if (action == PushNotificationAction::Highlight || action == PushNotificationAction::NoisyHighlight) {
QJsonObject highlightTweak;
highlightTweak.insert("set_tweak", "highlight");
actions.append(highlightTweak);
}
return actions;
}