// SPDX-FileCopyrightText: 2020 Tobias Fella // SPDX-License-Identifier: GPL-2.0-or-later #include "notificationsmanager.h" #include #include #include #include #include #ifdef QUOTIENT_07 #include #else #include "neochataccountregistry.h" #endif #include #include #include #include #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->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 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]() { room->acceptInvitation(); notification->close(); }); connect(notification, &KNotification::action2Activated, this, [room, notification]() { RoomManager::instance().leaveRoom(room); notification->close(); }); connect(notification, &KNotification::closed, this, [this, room]() { 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 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("global", kind, ruleId); connect(job, &BaseJob::success, this, [job, kind, ruleId, enabled]() { if (job->enabled() != enabled) { Controller::instance().activeConnection()->callApi("global", kind, ruleId, enabled); } }); } void NotificationsManager::setNotificationRuleActions(const QString &kind, const QString &ruleId, PushNotificationAction::Action action) { QVector actions; if (ruleId == ".m.rule.call") { actions = toActions(action, "ring"); } else { actions = toActions(action); } Controller::instance().activeConnection()->callApi("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 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(); } QVector 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; }