summaryrefslogtreecommitdiff
path: root/includes/external/matrix/node_modules/matrix-js-sdk/src/pushprocessor.ts
diff options
context:
space:
mode:
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src/pushprocessor.ts')
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/pushprocessor.ts770
1 files changed, 770 insertions, 0 deletions
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/pushprocessor.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/pushprocessor.ts
new file mode 100644
index 0000000..78d26fe
--- /dev/null
+++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/pushprocessor.ts
@@ -0,0 +1,770 @@
+/*
+Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { deepCompare, escapeRegExp, globToRegexp, isNullOrUndefined } from "./utils";
+import { logger } from "./logger";
+import { MatrixClient } from "./client";
+import { MatrixEvent } from "./models/event";
+import {
+ ConditionKind,
+ IAnnotatedPushRule,
+ ICallStartedCondition,
+ ICallStartedPrefixCondition,
+ IContainsDisplayNameCondition,
+ IEventMatchCondition,
+ IEventPropertyIsCondition,
+ IEventPropertyContainsCondition,
+ IPushRule,
+ IPushRules,
+ IRoomMemberCountCondition,
+ ISenderNotificationPermissionCondition,
+ PushRuleAction,
+ PushRuleActionName,
+ PushRuleCondition,
+ PushRuleKind,
+ PushRuleSet,
+ RuleId,
+ TweakName,
+} from "./@types/PushRules";
+import { EventType } from "./@types/event";
+
+const RULEKINDS_IN_ORDER = [
+ PushRuleKind.Override,
+ PushRuleKind.ContentSpecific,
+ PushRuleKind.RoomSpecific,
+ PushRuleKind.SenderSpecific,
+ PushRuleKind.Underride,
+];
+
+// The default override rules to apply to the push rules that arrive from the server.
+// We do this for two reasons:
+// 1. Synapse is unlikely to send us the push rule in an incremental sync - see
+// https://github.com/matrix-org/synapse/pull/4867#issuecomment-481446072 for
+// more details.
+// 2. We often want to start using push rules ahead of the server supporting them,
+// and so we can put them here.
+const DEFAULT_OVERRIDE_RULES: IPushRule[] = [
+ {
+ // For homeservers which don't support MSC2153 yet
+ rule_id: ".m.rule.reaction",
+ default: true,
+ enabled: true,
+ conditions: [
+ {
+ kind: ConditionKind.EventMatch,
+ key: "type",
+ pattern: "m.reaction",
+ },
+ ],
+ actions: [PushRuleActionName.DontNotify],
+ },
+ {
+ rule_id: RuleId.IsUserMention,
+ default: true,
+ enabled: true,
+ conditions: [
+ {
+ kind: ConditionKind.EventPropertyContains,
+ key: "content.org\\.matrix\\.msc3952\\.mentions.user_ids",
+ value: "", // The user ID is dynamically added in rewriteDefaultRules.
+ },
+ ],
+ actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight }],
+ },
+ {
+ rule_id: RuleId.IsRoomMention,
+ default: true,
+ enabled: true,
+ conditions: [
+ {
+ kind: ConditionKind.EventPropertyIs,
+ key: "content.org\\.matrix\\.msc3952\\.mentions.room",
+ value: true,
+ },
+ {
+ kind: ConditionKind.SenderNotificationPermission,
+ key: "room",
+ },
+ ],
+ actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight }],
+ },
+ {
+ // For homeservers which don't support MSC3786 yet
+ rule_id: ".org.matrix.msc3786.rule.room.server_acl",
+ default: true,
+ enabled: true,
+ conditions: [
+ {
+ kind: ConditionKind.EventMatch,
+ key: "type",
+ pattern: EventType.RoomServerAcl,
+ },
+ {
+ kind: ConditionKind.EventMatch,
+ key: "state_key",
+ pattern: "",
+ },
+ ],
+ actions: [],
+ },
+];
+
+const DEFAULT_UNDERRIDE_RULES: IPushRule[] = [
+ {
+ // For homeservers which don't support MSC3914 yet
+ rule_id: ".org.matrix.msc3914.rule.room.call",
+ default: true,
+ enabled: true,
+ conditions: [
+ {
+ kind: ConditionKind.EventMatch,
+ key: "type",
+ pattern: "org.matrix.msc3401.call",
+ },
+ {
+ kind: ConditionKind.CallStarted,
+ },
+ ],
+ actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Sound, value: "default" }],
+ },
+];
+
+export interface IActionsObject {
+ /** Whether this event should notify the user or not. */
+ notify: boolean;
+ /** How this event should be notified. */
+ tweaks: Partial<Record<TweakName, any>>;
+}
+
+export class PushProcessor {
+ /**
+ * Construct a Push Processor.
+ * @param client - The Matrix client object to use
+ */
+ public constructor(private readonly client: MatrixClient) {}
+
+ /**
+ * Maps the original key from the push rules to a list of property names
+ * after unescaping.
+ */
+ private readonly parsedKeys = new Map<string, string[]>();
+
+ /**
+ * Convert a list of actions into a object with the actions as keys and their values
+ * @example
+ * eg. `[ 'notify', { set_tweak: 'sound', value: 'default' } ]`
+ * becomes `{ notify: true, tweaks: { sound: 'default' } }`
+ * @param actionList - The actions list
+ *
+ * @returns A object with key 'notify' (true or false) and an object of actions
+ */
+ public static actionListToActionsObject(actionList: PushRuleAction[]): IActionsObject {
+ const actionObj: IActionsObject = { notify: false, tweaks: {} };
+ for (const action of actionList) {
+ if (action === PushRuleActionName.Notify) {
+ actionObj.notify = true;
+ } else if (typeof action === "object") {
+ if (action.value === undefined) {
+ action.value = true;
+ }
+ actionObj.tweaks[action.set_tweak] = action.value;
+ }
+ }
+ return actionObj;
+ }
+
+ /**
+ * Rewrites conditions on a client's push rules to match the defaults
+ * where applicable. Useful for upgrading push rules to more strict
+ * conditions when the server is falling behind on defaults.
+ * @param incomingRules - The client's existing push rules
+ * @param userId - The Matrix ID of the client.
+ * @returns The rewritten rules
+ */
+ public static rewriteDefaultRules(incomingRules: IPushRules, userId: string | undefined = undefined): IPushRules {
+ let newRules: IPushRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone
+
+ // These lines are mostly to make the tests happy. We shouldn't run into these
+ // properties missing in practice.
+ if (!newRules) newRules = {} as IPushRules;
+ if (!newRules.global) newRules.global = {} as PushRuleSet;
+ if (!newRules.global.override) newRules.global.override = [];
+ if (!newRules.global.underride) newRules.global.underride = [];
+
+ // Merge the client-level defaults with the ones from the server
+ const globalOverrides = newRules.global.override;
+ for (const originalOverride of DEFAULT_OVERRIDE_RULES) {
+ const existingRule = globalOverrides.find((r) => r.rule_id === originalOverride.rule_id);
+
+ // Dynamically add the user ID as the value for the is_user_mention rule.
+ let override: IPushRule;
+ if (originalOverride.rule_id === RuleId.IsUserMention) {
+ // If the user ID wasn't provided, skip the rule.
+ if (!userId) {
+ continue;
+ }
+
+ override = JSON.parse(JSON.stringify(originalOverride)); // deep clone
+ override.conditions![0].value = userId;
+ } else {
+ override = originalOverride;
+ }
+
+ if (existingRule) {
+ // Copy over the actions, default, and conditions. Don't touch the user's preference.
+ existingRule.default = override.default;
+ existingRule.conditions = override.conditions;
+ existingRule.actions = override.actions;
+ } else {
+ // Add the rule
+ const ruleId = override.rule_id;
+ logger.warn(`Adding default global override for ${ruleId}`);
+ globalOverrides.push(override);
+ }
+ }
+
+ const globalUnderrides = newRules.global.underride ?? [];
+ for (const underride of DEFAULT_UNDERRIDE_RULES) {
+ const existingRule = globalUnderrides.find((r) => r.rule_id === underride.rule_id);
+
+ if (existingRule) {
+ // Copy over the actions, default, and conditions. Don't touch the user's preference.
+ existingRule.default = underride.default;
+ existingRule.conditions = underride.conditions;
+ existingRule.actions = underride.actions;
+ } else {
+ // Add the rule
+ const ruleId = underride.rule_id;
+ logger.warn(`Adding default global underride for ${ruleId}`);
+ globalUnderrides.push(underride);
+ }
+ }
+
+ return newRules;
+ }
+
+ /**
+ * Pre-caches the parsed keys for push rules and cleans out any obsolete cache
+ * entries. Should be called after push rules are updated.
+ * @param newRules - The new push rules.
+ */
+ public updateCachedPushRuleKeys(newRules: IPushRules): void {
+ // These lines are mostly to make the tests happy. We shouldn't run into these
+ // properties missing in practice.
+ if (!newRules) newRules = {} as IPushRules;
+ if (!newRules.global) newRules.global = {} as PushRuleSet;
+ if (!newRules.global.override) newRules.global.override = [];
+ if (!newRules.global.room) newRules.global.room = [];
+ if (!newRules.global.sender) newRules.global.sender = [];
+ if (!newRules.global.underride) newRules.global.underride = [];
+
+ // Process the 'key' property on event_match conditions pre-cache the
+ // values and clean-out any unused values.
+ const toRemoveKeys = new Set(this.parsedKeys.keys());
+ for (const ruleset of [
+ newRules.global.override,
+ newRules.global.room,
+ newRules.global.sender,
+ newRules.global.underride,
+ ]) {
+ for (const rule of ruleset) {
+ if (!rule.conditions) {
+ continue;
+ }
+
+ for (const condition of rule.conditions) {
+ if (condition.kind !== ConditionKind.EventMatch) {
+ continue;
+ }
+
+ // Ensure we keep this key.
+ toRemoveKeys.delete(condition.key);
+
+ // Pre-process the key.
+ this.parsedKeys.set(condition.key, PushProcessor.partsForDottedKey(condition.key));
+ }
+ }
+ }
+ // Any keys that were previously cached, but are no longer needed should
+ // be removed.
+ toRemoveKeys.forEach((k) => this.parsedKeys.delete(k));
+ }
+
+ private static cachedGlobToRegex: Record<string, RegExp> = {}; // $glob: RegExp
+
+ private matchingRuleFromKindSet(ev: MatrixEvent, kindset: PushRuleSet): IAnnotatedPushRule | null {
+ for (const kind of RULEKINDS_IN_ORDER) {
+ const ruleset = kindset[kind];
+ if (!ruleset) {
+ continue;
+ }
+
+ for (const rule of ruleset) {
+ if (!rule.enabled) {
+ continue;
+ }
+
+ const rawrule = this.templateRuleToRaw(kind, rule);
+ if (!rawrule) {
+ continue;
+ }
+
+ if (this.ruleMatchesEvent(rawrule, ev)) {
+ return {
+ ...rule,
+ kind,
+ };
+ }
+ }
+ }
+ return null;
+ }
+
+ private templateRuleToRaw(
+ kind: PushRuleKind,
+ tprule: IPushRule,
+ ): Pick<IPushRule, "rule_id" | "actions" | "conditions"> | null {
+ const rawrule: Pick<IPushRule, "rule_id" | "actions" | "conditions"> = {
+ rule_id: tprule.rule_id,
+ actions: tprule.actions,
+ conditions: [],
+ };
+ switch (kind) {
+ case PushRuleKind.Underride:
+ case PushRuleKind.Override:
+ rawrule.conditions = tprule.conditions;
+ break;
+ case PushRuleKind.RoomSpecific:
+ if (!tprule.rule_id) {
+ return null;
+ }
+ rawrule.conditions!.push({
+ kind: ConditionKind.EventMatch,
+ key: "room_id",
+ value: tprule.rule_id,
+ });
+ break;
+ case PushRuleKind.SenderSpecific:
+ if (!tprule.rule_id) {
+ return null;
+ }
+ rawrule.conditions!.push({
+ kind: ConditionKind.EventMatch,
+ key: "user_id",
+ value: tprule.rule_id,
+ });
+ break;
+ case PushRuleKind.ContentSpecific:
+ if (!tprule.pattern) {
+ return null;
+ }
+ rawrule.conditions!.push({
+ kind: ConditionKind.EventMatch,
+ key: "content.body",
+ pattern: tprule.pattern,
+ });
+ break;
+ }
+ return rawrule;
+ }
+
+ private eventFulfillsCondition(cond: PushRuleCondition, ev: MatrixEvent): boolean {
+ switch (cond.kind) {
+ case ConditionKind.EventMatch:
+ return this.eventFulfillsEventMatchCondition(cond, ev);
+ case ConditionKind.EventPropertyIs:
+ return this.eventFulfillsEventPropertyIsCondition(cond, ev);
+ case ConditionKind.EventPropertyContains:
+ return this.eventFulfillsEventPropertyContains(cond, ev);
+ case ConditionKind.ContainsDisplayName:
+ return this.eventFulfillsDisplayNameCondition(cond, ev);
+ case ConditionKind.RoomMemberCount:
+ return this.eventFulfillsRoomMemberCountCondition(cond, ev);
+ case ConditionKind.SenderNotificationPermission:
+ return this.eventFulfillsSenderNotifPermCondition(cond, ev);
+ case ConditionKind.CallStarted:
+ case ConditionKind.CallStartedPrefix:
+ return this.eventFulfillsCallStartedCondition(cond, ev);
+ }
+
+ // unknown conditions: we previously matched all unknown conditions,
+ // but given that rules can be added to the base rules on a server,
+ // it's probably better to not match unknown conditions.
+ return false;
+ }
+
+ private eventFulfillsSenderNotifPermCondition(
+ cond: ISenderNotificationPermissionCondition,
+ ev: MatrixEvent,
+ ): boolean {
+ const notifLevelKey = cond["key"];
+ if (!notifLevelKey) {
+ return false;
+ }
+
+ const room = this.client.getRoom(ev.getRoomId());
+ if (!room?.currentState) {
+ return false;
+ }
+
+ // Note that this should not be the current state of the room but the state at
+ // the point the event is in the DAG. Unfortunately the js-sdk does not store
+ // this.
+ return room.currentState.mayTriggerNotifOfType(notifLevelKey, ev.getSender()!);
+ }
+
+ private eventFulfillsRoomMemberCountCondition(cond: IRoomMemberCountCondition, ev: MatrixEvent): boolean {
+ if (!cond.is) {
+ return false;
+ }
+
+ const room = this.client.getRoom(ev.getRoomId());
+ if (!room || !room.currentState || !room.currentState.members) {
+ return false;
+ }
+
+ const memberCount = room.currentState.getJoinedMemberCount();
+
+ const m = cond.is.match(/^([=<>]*)(\d*)$/);
+ if (!m) {
+ return false;
+ }
+ const ineq = m[1];
+ const rhs = parseInt(m[2]);
+ if (isNaN(rhs)) {
+ return false;
+ }
+ switch (ineq) {
+ case "":
+ case "==":
+ return memberCount == rhs;
+ case "<":
+ return memberCount < rhs;
+ case ">":
+ return memberCount > rhs;
+ case "<=":
+ return memberCount <= rhs;
+ case ">=":
+ return memberCount >= rhs;
+ default:
+ return false;
+ }
+ }
+
+ private eventFulfillsDisplayNameCondition(cond: IContainsDisplayNameCondition, ev: MatrixEvent): boolean {
+ let content = ev.getContent();
+ if (ev.isEncrypted() && ev.getClearContent()) {
+ content = ev.getClearContent()!;
+ }
+ if (!content || !content.body || typeof content.body != "string") {
+ return false;
+ }
+
+ const room = this.client.getRoom(ev.getRoomId());
+ const member = room?.currentState?.getMember(this.client.credentials.userId!);
+ if (!member) {
+ return false;
+ }
+
+ const displayName = member.name;
+
+ // N.B. we can't use \b as it chokes on unicode. however \W seems to be okay
+ // as shorthand for [^0-9A-Za-z_].
+ const pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", "i");
+ return content.body.search(pat) > -1;
+ }
+
+ /**
+ * Check whether the given event matches the push rule condition by fetching
+ * the property from the event and comparing against the condition's glob-based
+ * pattern.
+ * @param cond - The push rule condition to check for a match.
+ * @param ev - The event to check for a match.
+ */
+ private eventFulfillsEventMatchCondition(cond: IEventMatchCondition, ev: MatrixEvent): boolean {
+ if (!cond.key) {
+ return false;
+ }
+
+ const val = this.valueForDottedKey(cond.key, ev);
+ if (typeof val !== "string") {
+ return false;
+ }
+
+ // XXX This does not match in a case-insensitive manner.
+ //
+ // See https://spec.matrix.org/v1.5/client-server-api/#conditions-1
+ if (cond.value) {
+ return cond.value === val;
+ }
+
+ if (typeof cond.pattern !== "string") {
+ return false;
+ }
+
+ const regex =
+ cond.key === "content.body"
+ ? this.createCachedRegex("(^|\\W)", cond.pattern, "(\\W|$)")
+ : this.createCachedRegex("^", cond.pattern, "$");
+
+ return !!val.match(regex);
+ }
+
+ /**
+ * Check whether the given event matches the push rule condition by fetching
+ * the property from the event and comparing exactly against the condition's
+ * value.
+ * @param cond - The push rule condition to check for a match.
+ * @param ev - The event to check for a match.
+ */
+ private eventFulfillsEventPropertyIsCondition(cond: IEventPropertyIsCondition, ev: MatrixEvent): boolean {
+ if (!cond.key || cond.value === undefined) {
+ return false;
+ }
+ return cond.value === this.valueForDottedKey(cond.key, ev);
+ }
+
+ /**
+ * Check whether the given event matches the push rule condition by fetching
+ * the property from the event and comparing exactly against the condition's
+ * value.
+ * @param cond - The push rule condition to check for a match.
+ * @param ev - The event to check for a match.
+ */
+ private eventFulfillsEventPropertyContains(cond: IEventPropertyContainsCondition, ev: MatrixEvent): boolean {
+ if (!cond.key || cond.value === undefined) {
+ return false;
+ }
+ const val = this.valueForDottedKey(cond.key, ev);
+ if (!Array.isArray(val)) {
+ return false;
+ }
+ return val.includes(cond.value);
+ }
+
+ private eventFulfillsCallStartedCondition(
+ _cond: ICallStartedCondition | ICallStartedPrefixCondition,
+ ev: MatrixEvent,
+ ): boolean {
+ // Since servers don't support properly sending push notification
+ // about MSC3401 call events, we do the handling ourselves
+ return (
+ ["m.ring", "m.prompt"].includes(ev.getContent()["m.intent"]) &&
+ !("m.terminated" in ev.getContent()) &&
+ (ev.getPrevContent()["m.terminated"] !== ev.getContent()["m.terminated"] ||
+ deepCompare(ev.getPrevContent(), {}))
+ );
+ }
+
+ private createCachedRegex(prefix: string, glob: string, suffix: string): RegExp {
+ if (PushProcessor.cachedGlobToRegex[glob]) {
+ return PushProcessor.cachedGlobToRegex[glob];
+ }
+ PushProcessor.cachedGlobToRegex[glob] = new RegExp(
+ prefix + globToRegexp(glob) + suffix,
+ "i", // Case insensitive
+ );
+ return PushProcessor.cachedGlobToRegex[glob];
+ }
+
+ /**
+ * Parse the key into the separate fields to search by splitting on
+ * unescaped ".", and then removing any escape characters.
+ *
+ * @param str - The key of the push rule condition: a dotted field.
+ * @returns The unescaped parts to fetch.
+ * @internal
+ */
+ public static partsForDottedKey(str: string): string[] {
+ const result = [];
+
+ // The current field and whether the previous character was the escape
+ // character (a backslash).
+ let part = "";
+ let escaped = false;
+
+ // Iterate over each character, and decide whether to append to the current
+ // part (following the escape rules) or to start a new part (based on the
+ // field separator).
+ for (const c of str) {
+ // If the previous character was the escape character (a backslash)
+ // then decide what to append to the current part.
+ if (escaped) {
+ if (c === "\\" || c === ".") {
+ // An escaped backslash or dot just gets added.
+ part += c;
+ } else {
+ // A character that shouldn't be escaped gets the backslash prepended.
+ part += "\\" + c;
+ }
+ // This always resets being escaped.
+ escaped = false;
+ continue;
+ }
+
+ if (c == ".") {
+ // The field separator creates a new part.
+ result.push(part);
+ part = "";
+ } else if (c == "\\") {
+ // A backslash adds no characters, but starts an escape sequence.
+ escaped = true;
+ } else {
+ // Otherwise, just add the current character.
+ part += c;
+ }
+ }
+
+ // Ensure the final part is included. If there's an open escape sequence
+ // it should be included.
+ if (escaped) {
+ part += "\\";
+ }
+ result.push(part);
+
+ return result;
+ }
+
+ /**
+ * For a dotted field and event, fetch the value at that position, if one
+ * exists.
+ *
+ * @param key - The key of the push rule condition: a dotted field to fetch.
+ * @param ev - The matrix event to fetch the field from.
+ * @returns The value at the dotted path given by key.
+ */
+ private valueForDottedKey(key: string, ev: MatrixEvent): any {
+ // The key should already have been parsed via updateCachedPushRuleKeys,
+ // but if it hasn't (maybe via an old consumer of the SDK which hasn't
+ // been updated?) then lazily calculate it here.
+ let parts = this.parsedKeys.get(key);
+ if (parts === undefined) {
+ parts = PushProcessor.partsForDottedKey(key);
+ this.parsedKeys.set(key, parts);
+ }
+ let val: any;
+
+ // special-case the first component to deal with encrypted messages
+ const firstPart = parts[0];
+ let currentIndex = 0;
+ if (firstPart === "content") {
+ val = ev.getContent();
+ ++currentIndex;
+ } else if (firstPart === "type") {
+ val = ev.getType();
+ ++currentIndex;
+ } else {
+ // use the raw event for any other fields
+ val = ev.event;
+ }
+
+ for (; currentIndex < parts.length; ++currentIndex) {
+ // The previous iteration resulted in null or undefined, bail (and
+ // avoid the type error of attempting to retrieve a property).
+ if (isNullOrUndefined(val)) {
+ return undefined;
+ }
+
+ const thisPart = parts[currentIndex];
+ val = val[thisPart];
+ }
+ return val;
+ }
+
+ private matchingRuleForEventWithRulesets(ev: MatrixEvent, rulesets?: IPushRules): IAnnotatedPushRule | null {
+ if (!rulesets) {
+ return null;
+ }
+ if (ev.getSender() === this.client.credentials.userId) {
+ return null;
+ }
+
+ return this.matchingRuleFromKindSet(ev, rulesets.global);
+ }
+
+ private pushActionsForEventAndRulesets(ev: MatrixEvent, rulesets?: IPushRules): IActionsObject {
+ const rule = this.matchingRuleForEventWithRulesets(ev, rulesets);
+ if (!rule) {
+ return {} as IActionsObject;
+ }
+
+ const actionObj = PushProcessor.actionListToActionsObject(rule.actions);
+
+ // Some actions are implicit in some situations: we add those here
+ if (actionObj.tweaks.highlight === undefined) {
+ // if it isn't specified, highlight if it's a content
+ // rule but otherwise not
+ actionObj.tweaks.highlight = rule.kind == PushRuleKind.ContentSpecific;
+ }
+
+ return actionObj;
+ }
+
+ public ruleMatchesEvent(rule: Partial<IPushRule> & Pick<IPushRule, "conditions">, ev: MatrixEvent): boolean {
+ // Disable the deprecated mentions push rules if the new mentions property exists.
+ if (
+ this.client.supportsIntentionalMentions() &&
+ ev.getContent()["org.matrix.msc3952.mentions"] !== undefined &&
+ (rule.rule_id === RuleId.ContainsUserName ||
+ rule.rule_id === RuleId.ContainsDisplayName ||
+ rule.rule_id === RuleId.AtRoomNotification)
+ ) {
+ return false;
+ }
+
+ return !rule.conditions?.some((cond) => !this.eventFulfillsCondition(cond, ev));
+ }
+
+ /**
+ * Get the user's push actions for the given event
+ */
+ public actionsForEvent(ev: MatrixEvent): IActionsObject {
+ return this.pushActionsForEventAndRulesets(ev, this.client.pushRules);
+ }
+
+ /**
+ * Get one of the users push rules by its ID
+ *
+ * @param ruleId - The ID of the rule to search for
+ * @returns The push rule, or null if no such rule was found
+ */
+ public getPushRuleById(ruleId: string): IPushRule | null {
+ const result = this.getPushRuleAndKindById(ruleId);
+ return result?.rule ?? null;
+ }
+
+ /**
+ * Get one of the users push rules by its ID
+ *
+ * @param ruleId - The ID of the rule to search for
+ * @returns rule The push rule, or null if no such rule was found
+ * @returns kind - The PushRuleKind of the rule to search for
+ */
+ public getPushRuleAndKindById(ruleId: string): { rule: IPushRule; kind: PushRuleKind } | null {
+ for (const scope of ["global"] as const) {
+ if (this.client.pushRules?.[scope] === undefined) continue;
+
+ for (const kind of RULEKINDS_IN_ORDER) {
+ if (this.client.pushRules[scope][kind] === undefined) continue;
+
+ for (const rule of this.client.pushRules[scope][kind]!) {
+ if (rule.rule_id === ruleId) return { rule, kind };
+ }
+ }
+ }
+ return null;
+ }
+}