File indexing completed on 2024-12-08 07:33:45

0001 // SPDX-FileCopyrightText: 2022 James Graham <james.h.graham@protonmail.com>
0002 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0003 
0004 #include "pushrulemodel.h"
0005 
0006 #include <QDebug>
0007 
0008 #include <Quotient/connection.h>
0009 #include <Quotient/converters.h>
0010 #include <Quotient/csapi/definitions/push_ruleset.h>
0011 #include <Quotient/csapi/pushrules.h>
0012 #include <Quotient/jobs/basejob.h>
0013 
0014 #include "neochatconfig.h"
0015 
0016 #include <KLazyLocalizedString>
0017 
0018 // Alternate name text for default rules.
0019 static const QHash<QString, KLazyLocalizedString> defaultRuleNames = {
0020     {QStringLiteral(".m.rule.master"), kli18nc("Notification type", "Enable notifications for this account")},
0021     {QStringLiteral(".m.rule.room_one_to_one"), kli18nc("Notification type", "Messages in one-to-one chats")},
0022     {QStringLiteral(".m.rule.encrypted_room_one_to_one"), kli18nc("Notification type", "Encrypted messages in one-to-one chats")},
0023     {QStringLiteral(".m.rule.message"), kli18nc("Notification type", "Messages in group chats")},
0024     {QStringLiteral(".m.rule.encrypted"), kli18nc("Notification type", "Messages in encrypted group chats")},
0025     {QStringLiteral(".m.rule.tombstone"), kli18nc("Notification type", "Room upgrade messages")},
0026     {QStringLiteral(".m.rule.contains_display_name"), kli18nc("Notification type", "Messages containing my display name")},
0027     {QStringLiteral(".m.rule.is_user_mention"), kli18nc("Notification type", "Messages which mention my Matrix user ID")},
0028     {QStringLiteral(".m.rule.is_room_mention"), kli18nc("Notification type", "Messages which mention a room")},
0029     {QStringLiteral(".m.rule.contains_user_name"), kli18nc("Notification type", "Messages containing the local part of my Matrix ID")},
0030     {QStringLiteral(".m.rule.roomnotif"), kli18nc("Notification type", "Whole room (@room) notifications")},
0031     {QStringLiteral(".m.rule.invite_for_me"), kli18nc("Notification type", "Invites to a room")},
0032     {QStringLiteral(".m.rule.call"), kli18nc("Notification type", "Call invitation")},
0033 };
0034 
0035 // Sections for default rules.
0036 static const QHash<QString, PushRuleSection::Section> defaultSections = {
0037     {QStringLiteral(".m.rule.master"), PushRuleSection::Master},
0038     {QStringLiteral(".m.rule.room_one_to_one"), PushRuleSection::Room},
0039     {QStringLiteral(".m.rule.encrypted_room_one_to_one"), PushRuleSection::Room},
0040     {QStringLiteral(".m.rule.message"), PushRuleSection::Room},
0041     {QStringLiteral(".m.rule.encrypted"), PushRuleSection::Room},
0042     {QStringLiteral(".m.rule.tombstone"), PushRuleSection::Room},
0043     {QStringLiteral(".m.rule.contains_display_name"), PushRuleSection::Mentions},
0044     {QStringLiteral(".m.rule.is_user_mention"), PushRuleSection::Mentions},
0045     {QStringLiteral(".m.rule.is_room_mention"), PushRuleSection::Mentions},
0046     {QStringLiteral(".m.rule.contains_user_name"), PushRuleSection::Mentions},
0047     {QStringLiteral(".m.rule.roomnotif"), PushRuleSection::Mentions},
0048     {QStringLiteral(".m.rule.invite_for_me"), PushRuleSection::Invites},
0049     {QStringLiteral(".m.rule.call"), PushRuleSection::Undefined}, // TODO: make invites when VOIP added.
0050     {QStringLiteral(".m.rule.suppress_notices"), PushRuleSection::Undefined},
0051     {QStringLiteral(".m.rule.member_event"), PushRuleSection::Undefined},
0052     {QStringLiteral(".m.rule.reaction"), PushRuleSection::Undefined},
0053     {QStringLiteral(".m.rule.room.server_acl"), PushRuleSection::Undefined},
0054     {QStringLiteral(".im.vector.jitsi"), PushRuleSection::Undefined},
0055 };
0056 
0057 // Default rules that don't have a highlight option as it would lead to all messages
0058 // in a room being highlighted.
0059 static const QStringList noHighlight = {
0060     QStringLiteral(".m.rule.room_one_to_one"),
0061     QStringLiteral(".m.rule.encrypted_room_one_to_one"),
0062     QStringLiteral(".m.rule.message"),
0063     QStringLiteral(".m.rule.encrypted"),
0064 };
0065 
0066 PushRuleModel::PushRuleModel(QObject *parent)
0067     : QAbstractListModel(parent)
0068 {
0069     m_defaultKeywordAction = static_cast<PushRuleAction::Action>(NeoChatConfig::self()->keywordPushRuleDefault());
0070 }
0071 
0072 void PushRuleModel::updateNotificationRules(const QString &type)
0073 {
0074     if (type != QStringLiteral("m.push_rules")) {
0075         return;
0076     }
0077 
0078     const QJsonObject ruleDataJson = m_connection->accountDataJson(QStringLiteral("m.push_rules"));
0079     const Quotient::PushRuleset ruleData = Quotient::fromJson<Quotient::PushRuleset>(ruleDataJson[QStringLiteral("global")].toObject());
0080 
0081     beginResetModel();
0082     m_rules.clear();
0083 
0084     // Doing this 5 times because PushRuleset is a struct.
0085     setRules(ruleData.override, PushRuleKind::Override);
0086     setRules(ruleData.content, PushRuleKind::Content);
0087     setRules(ruleData.room, PushRuleKind::Room);
0088     setRules(ruleData.sender, PushRuleKind::Sender);
0089     setRules(ruleData.underride, PushRuleKind::Underride);
0090 
0091     Q_EMIT globalNotificationsEnabledChanged();
0092     Q_EMIT globalNotificationsSetChanged();
0093 
0094     endResetModel();
0095 }
0096 
0097 void PushRuleModel::setRules(QList<Quotient::PushRule> rules, PushRuleKind::Kind kind)
0098 {
0099     for (const auto &rule : rules) {
0100         QString roomId;
0101         if (rule.conditions.size() > 0) {
0102             for (const auto &condition : rule.conditions) {
0103                 if (condition.key == QStringLiteral("room_id")) {
0104                     roomId = condition.pattern;
0105                 }
0106             }
0107         }
0108 
0109         m_rules.append(Rule{
0110             rule.ruleId,
0111             kind,
0112             variantToAction(rule.actions, rule.enabled),
0113             getSection(rule),
0114             rule.enabled,
0115             roomId,
0116         });
0117     }
0118 }
0119 
0120 int PushRuleModel::getRuleIndex(const QString &ruleId) const
0121 {
0122     for (auto i = 0; i < m_rules.count(); i++) {
0123         if (m_rules[i].id == ruleId) {
0124             return i;
0125         }
0126     }
0127     return -1;
0128 }
0129 
0130 PushRuleSection::Section PushRuleModel::getSection(Quotient::PushRule rule)
0131 {
0132     auto ruleId = rule.ruleId;
0133 
0134     if (defaultSections.contains(ruleId)) {
0135         return defaultSections.value(ruleId);
0136     } else {
0137         if (rule.ruleId.startsWith(u'.')) {
0138             return PushRuleSection::Unknown;
0139         }
0140         /**
0141          * If the rule name resolves to a matrix id for a room that the user is part
0142          * of it shouldn't appear in the global list as it's overriding the global
0143          * state for that room.
0144          *
0145          * Rooms that the user hasn't joined shouldn't have a rule.
0146          */
0147         if (m_connection->room(ruleId) != nullptr) {
0148             return PushRuleSection::Undefined;
0149         }
0150         /**
0151          * If the rule name resolves to a matrix id for a user  it shouldn't appear
0152          * in the global list as it's a rule to block notifications from a user and
0153          * is handled elsewhere.
0154          */
0155         auto testUserId = ruleId;
0156         // Rules for user matrix IDs often don't have the @ on the beginning so add
0157         // if not there to avoid malformed ID.
0158         if (!testUserId.startsWith(u'@')) {
0159             testUserId.prepend(u'@');
0160         }
0161         if (testUserId.startsWith(u'@') && !Quotient::serverPart(testUserId).isEmpty() && m_connection->user(testUserId) != nullptr) {
0162             return PushRuleSection::Undefined;
0163         }
0164         // If the rule has push conditions and one is a room ID it is a room only keyword.
0165         if (!rule.conditions.isEmpty()) {
0166             for (auto condition : rule.conditions) {
0167                 if (condition.key == QStringLiteral("room_id")) {
0168                     return PushRuleSection::RoomKeywords;
0169                 }
0170             }
0171         }
0172         return PushRuleSection::Keywords;
0173     }
0174 }
0175 
0176 PushRuleAction::Action PushRuleModel::defaultState() const
0177 {
0178     return m_defaultKeywordAction;
0179 }
0180 
0181 void PushRuleModel::setDefaultState(PushRuleAction::Action defaultState)
0182 {
0183     if (defaultState == m_defaultKeywordAction) {
0184         return;
0185     }
0186     m_defaultKeywordAction = defaultState;
0187     NeoChatConfig::setKeywordPushRuleDefault(m_defaultKeywordAction);
0188     Q_EMIT defaultStateChanged();
0189 }
0190 
0191 bool PushRuleModel::globalNotificationsEnabled() const
0192 {
0193     auto masterIndex = getRuleIndex(QStringLiteral(".m.rule.master"));
0194     if (masterIndex > -1) {
0195         return !m_rules[masterIndex].enabled;
0196     }
0197     return false;
0198 }
0199 
0200 void PushRuleModel::setGlobalNotificationsEnabled(bool enabled)
0201 {
0202     setNotificationRuleEnabled(QStringLiteral("override"), QStringLiteral(".m.rule.master"), !enabled);
0203 }
0204 
0205 bool PushRuleModel::globalNotificationsSet() const
0206 {
0207     return getRuleIndex(QStringLiteral(".m.rule.master")) > -1;
0208 }
0209 
0210 QVariant PushRuleModel::data(const QModelIndex &index, int role) const
0211 {
0212     if (!index.isValid()) {
0213         return {};
0214     }
0215 
0216     if (index.row() >= rowCount()) {
0217         qDebug() << "PushRuleModel, something's wrong: index.row() >= m_rules.count()";
0218         return {};
0219     }
0220 
0221     if (role == NameRole) {
0222         auto ruleId = m_rules.at(index.row()).id;
0223         if (defaultRuleNames.contains(ruleId)) {
0224             return defaultRuleNames.value(ruleId).toString();
0225         } else {
0226             return ruleId;
0227         }
0228     }
0229     if (role == IdRole) {
0230         return m_rules.at(index.row()).id;
0231     }
0232     if (role == KindRole) {
0233         return m_rules.at(index.row()).kind;
0234     }
0235     if (role == ActionRole) {
0236         return m_rules.at(index.row()).action;
0237     }
0238     if (role == HighlightableRole) {
0239         return !noHighlight.contains(m_rules.at(index.row()).id);
0240     }
0241     if (role == DeletableRole) {
0242         return !m_rules.at(index.row()).id.startsWith(QStringLiteral("."));
0243     }
0244     if (role == SectionRole) {
0245         return m_rules.at(index.row()).section;
0246     }
0247     if (role == RoomIdRole) {
0248         return m_rules.at(index.row()).roomId;
0249     }
0250 
0251     return {};
0252 }
0253 
0254 int PushRuleModel::rowCount(const QModelIndex &parent) const
0255 {
0256     Q_UNUSED(parent)
0257 
0258     return m_rules.count();
0259 }
0260 
0261 QHash<int, QByteArray> PushRuleModel::roleNames() const
0262 {
0263     QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
0264     roles[NameRole] = "name";
0265     roles[IdRole] = "id";
0266     roles[KindRole] = "kind";
0267     roles[ActionRole] = "ruleAction";
0268     roles[HighlightableRole] = "highlightable";
0269     roles[DeletableRole] = "deletable";
0270     roles[SectionRole] = "section";
0271     roles[RoomIdRole] = "roomId";
0272     return roles;
0273 }
0274 
0275 void PushRuleModel::setPushRuleAction(const QString &id, PushRuleAction::Action action)
0276 {
0277     int index = getRuleIndex(id);
0278     if (index == -1) {
0279         return;
0280     }
0281 
0282     auto rule = m_rules[index];
0283 
0284     // Override rules need to be disabled when off so that other rules can match the message if they apply.
0285     if (action == PushRuleAction::Off && rule.kind == PushRuleKind::Override) {
0286         setNotificationRuleEnabled(PushRuleKind::kindString(rule.kind), rule.id, false);
0287     } else if (rule.kind == PushRuleKind::Override) {
0288         setNotificationRuleEnabled(PushRuleKind::kindString(rule.kind), rule.id, true);
0289     }
0290 
0291     setNotificationRuleActions(PushRuleKind::kindString(rule.kind), rule.id, action);
0292 }
0293 
0294 void PushRuleModel::addKeyword(const QString &keyword, const QString &roomId)
0295 {
0296     PushRuleKind::Kind kind = PushRuleKind::Content;
0297     const QList<QVariant> actions = actionToVariant(m_defaultKeywordAction);
0298     QList<Quotient::PushCondition> pushConditions;
0299     if (!roomId.isEmpty()) {
0300         kind = PushRuleKind::Override;
0301 
0302         Quotient::PushCondition roomCondition;
0303         roomCondition.kind = QStringLiteral("event_match");
0304         roomCondition.key = QStringLiteral("room_id");
0305         roomCondition.pattern = roomId;
0306         pushConditions.append(roomCondition);
0307 
0308         Quotient::PushCondition keywordCondition;
0309         keywordCondition.kind = QStringLiteral("event_match");
0310         keywordCondition.key = QStringLiteral("content.body");
0311         keywordCondition.pattern = keyword;
0312         pushConditions.append(keywordCondition);
0313     }
0314 
0315     auto job = m_connection->callApi<Quotient::SetPushRuleJob>(QLatin1String("global"),
0316                                                                PushRuleKind::kindString(kind),
0317                                                                keyword,
0318                                                                actions,
0319                                                                QString(),
0320                                                                QString(),
0321                                                                pushConditions,
0322                                                                roomId.isEmpty() ? keyword : QString());
0323     connect(job, &Quotient::BaseJob::failure, this, [job, keyword]() {
0324         qWarning() << QLatin1String("Unable to set push rule for keyword %1: ").arg(keyword) << job->errorString();
0325     });
0326 }
0327 
0328 /**
0329  * The rule never being removed from the list by this function is intentional. When
0330  * the server is updated the new push rule account data will be synced and it will
0331  * be removed when the model is updated then.
0332  */
0333 void PushRuleModel::removeKeyword(const QString &keyword)
0334 {
0335     int index = getRuleIndex(keyword);
0336     if (index == -1) {
0337         return;
0338     }
0339 
0340     auto kind = PushRuleKind::kindString(m_rules[index].kind);
0341     auto job = m_connection->callApi<Quotient::DeletePushRuleJob>(QStringLiteral("global"), kind, m_rules[index].id);
0342     connect(job, &Quotient::BaseJob::failure, this, [this, job, index]() {
0343         qWarning() << QLatin1String("Unable to remove push rule for keyword %1: ").arg(m_rules[index].id) << job->errorString();
0344     });
0345 }
0346 
0347 void PushRuleModel::setNotificationRuleEnabled(const QString &kind, const QString &ruleId, bool enabled)
0348 {
0349     auto job = m_connection->callApi<Quotient::IsPushRuleEnabledJob>(QStringLiteral("global"), kind, ruleId);
0350     connect(job, &Quotient::BaseJob::success, this, [job, kind, ruleId, enabled, this]() {
0351         if (job->enabled() != enabled) {
0352             m_connection->callApi<Quotient::SetPushRuleEnabledJob>(QStringLiteral("global"), kind, ruleId, enabled);
0353         }
0354     });
0355 }
0356 
0357 void PushRuleModel::setNotificationRuleActions(const QString &kind, const QString &ruleId, PushRuleAction::Action action)
0358 {
0359     QList<QVariant> actions;
0360     if (ruleId == QStringLiteral(".m.rule.call")) {
0361         actions = actionToVariant(action, QStringLiteral("ring"));
0362     } else {
0363         actions = actionToVariant(action);
0364     }
0365 
0366     m_connection->callApi<Quotient::SetPushRuleActionsJob>(QStringLiteral("global"), kind, ruleId, actions);
0367 }
0368 
0369 PushRuleAction::Action PushRuleModel::variantToAction(const QList<QVariant> &actions, bool enabled)
0370 {
0371     bool notify = false;
0372     bool isNoisy = false;
0373     bool highlightEnabled = false;
0374     for (const auto &i : actions) {
0375         auto actionString = i.toString();
0376         if (!actionString.isEmpty()) {
0377             if (actionString == QLatin1String("notify")) {
0378                 notify = true;
0379             }
0380             continue;
0381         }
0382 
0383         QJsonObject action = i.toJsonObject();
0384         if (action[QStringLiteral("set_tweak")].toString() == QStringLiteral("sound")) {
0385             isNoisy = true;
0386         } else if (action[QStringLiteral("set_tweak")].toString() == QStringLiteral("highlight")) {
0387             if (action[QStringLiteral("value")].toString() != QStringLiteral("false")) {
0388                 highlightEnabled = true;
0389             }
0390         }
0391     }
0392 
0393     if (!enabled) {
0394         return PushRuleAction::Off;
0395     }
0396 
0397     if (notify) {
0398         if (isNoisy && highlightEnabled) {
0399             return PushRuleAction::NoisyHighlight;
0400         } else if (isNoisy) {
0401             return PushRuleAction::Noisy;
0402         } else if (highlightEnabled) {
0403             return PushRuleAction::Highlight;
0404         } else {
0405             return PushRuleAction::On;
0406         }
0407     } else {
0408         return PushRuleAction::Off;
0409     }
0410 }
0411 
0412 QList<QVariant> PushRuleModel::actionToVariant(PushRuleAction::Action action, const QString &sound)
0413 {
0414     // The caller should never try to set the state to unknown.
0415     // It exists only as a default state to diable the settings options until the actual state is retrieved from the server.
0416     if (action == PushRuleAction::Unknown) {
0417         Q_ASSERT(false);
0418         return QList<QVariant>();
0419     }
0420 
0421     QList<QVariant> actions;
0422 
0423     if (action != PushRuleAction::Off) {
0424         actions.append(QStringLiteral("notify"));
0425     } else {
0426         actions.append(QStringLiteral("dont_notify"));
0427     }
0428     if (action == PushRuleAction::Noisy || action == PushRuleAction::NoisyHighlight) {
0429         QJsonObject soundTweak;
0430         soundTweak.insert(QStringLiteral("set_tweak"), QStringLiteral("sound"));
0431         soundTweak.insert(QStringLiteral("value"), sound);
0432         actions.append(soundTweak);
0433     }
0434     if (action == PushRuleAction::Highlight || action == PushRuleAction::NoisyHighlight) {
0435         QJsonObject highlightTweak;
0436         highlightTweak.insert(QStringLiteral("set_tweak"), QStringLiteral("highlight"));
0437         actions.append(highlightTweak);
0438     }
0439 
0440     return actions;
0441 }
0442 
0443 NeoChatConnection *PushRuleModel::connection() const
0444 {
0445     return m_connection;
0446 }
0447 
0448 void PushRuleModel::setConnection(NeoChatConnection *connection)
0449 {
0450     if (connection == m_connection) {
0451         return;
0452     }
0453     m_connection = connection;
0454     Q_EMIT connectionChanged();
0455 
0456     if (m_connection) {
0457         connect(m_connection, &Quotient::Connection::accountDataChanged, this, &PushRuleModel::updateNotificationRules);
0458         updateNotificationRules(QStringLiteral("m.push_rules"));
0459     }
0460 }
0461 
0462 #include "moc_pushrulemodel.cpp"