From db183385e1b18a998106d2a876971dc92adc4a85 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 27 Feb 2023 15:40:27 -0500 Subject: [PATCH] Support MSC3758 to exactly match values in push rule conditions. --- spec/unit/pushprocessor.spec.ts | 75 ++++++++++++++++++++++++++++++++- src/@types/PushRules.ts | 7 +++ src/pushprocessor.ts | 26 ++++++++++-- 3 files changed, 104 insertions(+), 4 deletions(-) diff --git a/spec/unit/pushprocessor.spec.ts b/spec/unit/pushprocessor.spec.ts index f43d5f8aa6c..af192551b2f 100644 --- a/spec/unit/pushprocessor.spec.ts +++ b/spec/unit/pushprocessor.spec.ts @@ -1,6 +1,6 @@ import * as utils from "../test-utils/test-utils"; import { IActionsObject, PushProcessor } from "../../src/pushprocessor"; -import { ConditionKind, EventType, IContent, MatrixClient, MatrixEvent } from "../../src"; +import { ConditionKind, EventType, IContent, MatrixClient, MatrixEvent, PushRuleActionName } from "../../src"; describe("NotificationService", function () { const testUserId = "@ali:matrix.org"; @@ -505,6 +505,79 @@ describe("NotificationService", function () { }); }); + describe("Test exact event matching", () => { + it.each([ + // Simple string matching. + { value: "bar", eventValue: "bar", expected: true }, + // Matches are case-sensitive. + { value: "bar", eventValue: "BAR", expected: false }, + // Matches must match the full string. + { value: "bar", eventValue: "barbar", expected: false }, + // Values should not be type-coerced. + { value: "bar", eventValue: true, expected: false }, + { value: "bar", eventValue: 1, expected: false }, + { value: "bar", eventValue: false, expected: false }, + // Boolean matching. + { value: true, eventValue: true, expected: true }, + { value: false, eventValue: false, expected: true }, + // Types should not be coerced. + { value: true, eventValue: "true", expected: false }, + { value: true, eventValue: 1, expected: false }, + { value: false, eventValue: null, expected: false }, + // Null matching. + { value: null, eventValue: null, expected: true }, + // Types should not be coerced + { value: null, eventValue: false, expected: false }, + { value: null, eventValue: 0, expected: false }, + { value: null, eventValue: "", expected: false }, + { value: null, eventValue: undefined, expected: false }, + // Compound values should never be matched. + { value: "bar", eventValue: ["bar"], expected: false }, + { value: "bar", eventValue: { bar: true }, expected: false }, + { value: true, eventValue: [true], expected: false }, + { value: true, eventValue: { true: true }, expected: false }, + { value: null, eventValue: [], expected: false }, + { value: null, eventValue: {}, expected: false }, + ])("test $value against $eventValue", ({ value, eventValue, expected }) => { + matrixClient.pushRules! = { + global: { + override: [ + { + actions: [PushRuleActionName.Notify], + conditions: [ + { + kind: ConditionKind.ExactEventMatchPrefix, + key: "content.foo", + value: value, + }, + ], + default: true, + enabled: true, + rule_id: ".m.rule.test", + }, + ], + }, + }; + + testEvent = utils.mkEvent({ + type: "m.room.message", + room: testRoomId, + user: "@alfred:localhost", + event: true, + content: { + foo: eventValue, + }, + }); + + const actions = pushProcessor.actionsForEvent(testEvent); + if (expected) { + expect(actions?.notify).toBeTruthy(); + } else { + expect(actions?.notify).toBeFalsy(); + } + }); + }); + it.each([ // The properly escaped key works. { key: "content.m\\.test.foo", pattern: "bar", expected: true }, diff --git a/src/@types/PushRules.ts b/src/@types/PushRules.ts index f77dcc4159f..0147d7d7638 100644 --- a/src/@types/PushRules.ts +++ b/src/@types/PushRules.ts @@ -62,6 +62,7 @@ export function isDmMemberCountCondition(condition: AnyMemberCountCondition): bo export enum ConditionKind { EventMatch = "event_match", + ExactEventMatchPrefix = "com.beeper.msc3758.exact_event_match", ContainsDisplayName = "contains_display_name", RoomMemberCount = "room_member_count", SenderNotificationPermission = "sender_notification_permission", @@ -82,6 +83,11 @@ export interface IEventMatchCondition extends IPushRuleCondition { + key: string; + value: string | boolean | null | number; +} + export interface IContainsDisplayNameCondition extends IPushRuleCondition { // no additional fields } @@ -107,6 +113,7 @@ export interface ICallStartedPrefixCondition extends IPushRuleCondition> unfortunately does not resolve this at the time of writing. export type PushRuleCondition = | IEventMatchCondition + | IExactEventMatchPrefixCondition | IContainsDisplayNameCondition | IRoomMemberCountCondition | ISenderNotificationPermissionCondition diff --git a/src/pushprocessor.ts b/src/pushprocessor.ts index 805d70f2505..42d9c426570 100644 --- a/src/pushprocessor.ts +++ b/src/pushprocessor.ts @@ -25,6 +25,7 @@ import { ICallStartedPrefixCondition, IContainsDisplayNameCondition, IEventMatchCondition, + IExactEventMatchPrefixCondition, IPushRule, IPushRules, IRoomMemberCountCondition, @@ -337,6 +338,8 @@ export class PushProcessor { switch (cond.kind) { case ConditionKind.EventMatch: return this.eventFulfillsEventMatchCondition(cond, ev); + case ConditionKind.ExactEventMatchPrefix: + return this.eventFulfillsExactEventMatchCondition(cond, ev); case ConditionKind.ContainsDisplayName: return this.eventFulfillsDisplayNameCondition(cond, ev); case ConditionKind.RoomMemberCount: @@ -471,6 +474,20 @@ export class PushProcessor { 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 eventFulfillsExactEventMatchCondition(cond: IExactEventMatchPrefixCondition, ev: MatrixEvent): boolean { + if (!cond.key || cond.value === undefined) { + return false; + } + return cond.value === this.valueForDottedKey(cond.key, ev); + } + private eventFulfillsCallStartedCondition( _cond: ICallStartedCondition | ICallStartedPrefixCondition, ev: MatrixEvent, @@ -588,10 +605,13 @@ export class PushProcessor { } for (; currentIndex < parts.length; ++currentIndex) { - const thisPart = parts[currentIndex]; - if (isNullOrUndefined(val[thisPart])) { - return null; + // 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;