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"