File indexing completed on 2024-05-12 16:25:06

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