File indexing completed on 2025-01-12 13:02:24
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"