Skip to content

Commit fc55c4c

Browse files
authored
Implement MSC3952: intentional mentions (#3092)
* Add experimental push rules. * Update for changes to MSC3952: Use event_property_is and event_property_contains. * Revert custom user/room mention conditions. * Skip legacy rule processing if mentions exist. * Add client option for intentional mentions. * Fix tests. * Test leagcy behavior with intentional mentions. * Handle simple review comments.
1 parent f795577 commit fc55c4c

File tree

5 files changed

+117
-5
lines changed

5 files changed

+117
-5
lines changed

spec/unit/pushprocessor.spec.ts

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as utils from "../test-utils/test-utils";
22
import { IActionsObject, PushProcessor } from "../../src/pushprocessor";
3-
import { ConditionKind, EventType, IContent, MatrixClient, MatrixEvent, PushRuleActionName } from "../../src";
3+
import { ConditionKind, EventType, IContent, MatrixClient, MatrixEvent, PushRuleActionName, RuleId } from "../../src";
44

55
describe("NotificationService", function () {
66
const testUserId = "@ali:matrix.org";
@@ -48,6 +48,7 @@ describe("NotificationService", function () {
4848
credentials: {
4949
userId: testUserId,
5050
},
51+
supportsIntentionalMentions: () => true,
5152
pushRules: {
5253
device: {},
5354
global: {
@@ -712,6 +713,37 @@ describe("NotificationService", function () {
712713
});
713714
});
714715
});
716+
717+
describe("test intentional mentions behaviour", () => {
718+
it.each([RuleId.ContainsUserName, RuleId.ContainsDisplayName, RuleId.AtRoomNotification])(
719+
"Rule %s matches unless intentional mentions are enabled",
720+
(ruleId) => {
721+
const rule = {
722+
rule_id: ruleId,
723+
actions: [],
724+
conditions: [],
725+
default: false,
726+
enabled: true,
727+
};
728+
expect(pushProcessor.ruleMatchesEvent(rule, testEvent)).toBe(true);
729+
730+
// Add the mentions property to the event and the rule is now disabled.
731+
testEvent = utils.mkEvent({
732+
type: "m.room.message",
733+
room: testRoomId,
734+
user: "@alfred:localhost",
735+
event: true,
736+
content: {
737+
"body": "",
738+
"msgtype": "m.text",
739+
"org.matrix.msc3952.mentions": {},
740+
},
741+
});
742+
743+
expect(pushProcessor.ruleMatchesEvent(rule, testEvent)).toBe(false);
744+
},
745+
);
746+
});
715747
});
716748

717749
describe("Test PushProcessor.partsForDottedKey", function () {

src/@types/PushRules.ts

+2
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ export enum PushRuleKind {
137137

138138
export enum RuleId {
139139
Master = ".m.rule.master",
140+
IsUserMention = ".org.matrix.msc3952.is_user_mention",
141+
IsRoomMention = ".org.matrix.msc3952.is_room_mention",
140142
ContainsDisplayName = ".m.rule.contains_display_name",
141143
ContainsUserName = ".m.rule.contains_user_name",
142144
AtRoomNotification = ".m.rule.roomnotif",

src/client.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,11 @@ export interface IStartClientOpts {
461461
* @experimental
462462
*/
463463
slidingSync?: SlidingSync;
464+
465+
/**
466+
* @experimental
467+
*/
468+
intentionalMentions?: boolean;
464469
}
465470

466471
export interface IStoredClientOpts extends IStartClientOpts {}
@@ -8575,7 +8580,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
85758580
*/
85768581
public setPushRules(rules: IPushRules): void {
85778582
// Fix-up defaults, if applicable.
8578-
this.pushRules = PushProcessor.rewriteDefaultRules(rules);
8583+
this.pushRules = PushProcessor.rewriteDefaultRules(rules, this.getUserId()!);
85798584
// Pre-calculate any necessary caches.
85808585
this.pushProcessor.updateCachedPushRuleKeys(this.pushRules);
85818586
}
@@ -9472,6 +9477,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
94729477
return this.clientOpts?.threadSupport || false;
94739478
}
94749479

9480+
/**
9481+
* A helper to determine intentional mentions support
9482+
* @returns a boolean to determine if intentional mentions are enabled
9483+
* @experimental
9484+
*/
9485+
public supportsIntentionalMentions(): boolean {
9486+
return this.clientOpts?.intentionalMentions || false;
9487+
}
9488+
94759489
/**
94769490
* Fetches the summary of a room as defined by an initial version of MSC3266 and implemented in Synapse
94779491
* Proposed at https://github.com/matrix-org/matrix-doc/pull/3266

src/models/event.ts

+7
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export interface IContent {
4848
"avatar_url"?: string;
4949
"displayname"?: string;
5050
"m.relates_to"?: IEventRelation;
51+
52+
"org.matrix.msc3952.mentions"?: IMentions;
5153
}
5254

5355
type StrippedState = Required<Pick<IEvent, "content" | "state_key" | "type" | "sender">>;
@@ -114,6 +116,11 @@ export interface IEventRelation {
114116
"key"?: string;
115117
}
116118

119+
export interface IMentions {
120+
user_ids?: string[];
121+
room?: boolean;
122+
}
123+
117124
/**
118125
* When an event is a visibility change event, as per MSC3531,
119126
* the visibility change implied by the event.

src/pushprocessor.ts

+60-3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
PushRuleCondition,
3737
PushRuleKind,
3838
PushRuleSet,
39+
RuleId,
3940
TweakName,
4041
} from "./@types/PushRules";
4142
import { EventType } from "./@types/event";
@@ -70,6 +71,36 @@ const DEFAULT_OVERRIDE_RULES: IPushRule[] = [
7071
],
7172
actions: [PushRuleActionName.DontNotify],
7273
},
74+
{
75+
rule_id: RuleId.IsUserMention,
76+
default: true,
77+
enabled: true,
78+
conditions: [
79+
{
80+
kind: ConditionKind.EventPropertyContains,
81+
key: "content.org\\.matrix\\.msc3952\\.mentions.user_ids",
82+
value: "", // The user ID is dynamically added in rewriteDefaultRules.
83+
},
84+
],
85+
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight }],
86+
},
87+
{
88+
rule_id: RuleId.IsRoomMention,
89+
default: true,
90+
enabled: true,
91+
conditions: [
92+
{
93+
kind: ConditionKind.EventPropertyIs,
94+
key: "content.org\\.matrix\\.msc3952\\.mentions.room",
95+
value: true,
96+
},
97+
{
98+
kind: ConditionKind.SenderNotificationPermission,
99+
key: "room",
100+
},
101+
],
102+
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight }],
103+
},
73104
{
74105
// For homeservers which don't support MSC3786 yet
75106
rule_id: ".org.matrix.msc3786.rule.room.server_acl",
@@ -160,9 +191,10 @@ export class PushProcessor {
160191
* where applicable. Useful for upgrading push rules to more strict
161192
* conditions when the server is falling behind on defaults.
162193
* @param incomingRules - The client's existing push rules
194+
* @param userId - The Matrix ID of the client.
163195
* @returns The rewritten rules
164196
*/
165-
public static rewriteDefaultRules(incomingRules: IPushRules): IPushRules {
197+
public static rewriteDefaultRules(incomingRules: IPushRules, userId: string | undefined = undefined): IPushRules {
166198
let newRules: IPushRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone
167199

168200
// These lines are mostly to make the tests happy. We shouldn't run into these
@@ -174,8 +206,22 @@ export class PushProcessor {
174206

175207
// Merge the client-level defaults with the ones from the server
176208
const globalOverrides = newRules.global.override;
177-
for (const override of DEFAULT_OVERRIDE_RULES) {
178-
const existingRule = globalOverrides.find((r) => r.rule_id === override.rule_id);
209+
for (const originalOverride of DEFAULT_OVERRIDE_RULES) {
210+
const existingRule = globalOverrides.find((r) => r.rule_id === originalOverride.rule_id);
211+
212+
// Dynamically add the user ID as the value for the is_user_mention rule.
213+
let override: IPushRule;
214+
if (originalOverride.rule_id === RuleId.IsUserMention) {
215+
// If the user ID wasn't provided, skip the rule.
216+
if (!userId) {
217+
continue;
218+
}
219+
220+
override = JSON.parse(JSON.stringify(originalOverride)); // deep clone
221+
override.conditions![0].value = userId;
222+
} else {
223+
override = originalOverride;
224+
}
179225

180226
if (existingRule) {
181227
// Copy over the actions, default, and conditions. Don't touch the user's preference.
@@ -668,6 +714,17 @@ export class PushProcessor {
668714
}
669715

670716
public ruleMatchesEvent(rule: Partial<IPushRule> & Pick<IPushRule, "conditions">, ev: MatrixEvent): boolean {
717+
// Disable the deprecated mentions push rules if the new mentions property exists.
718+
if (
719+
this.client.supportsIntentionalMentions() &&
720+
ev.getContent()["org.matrix.msc3952.mentions"] !== undefined &&
721+
(rule.rule_id === RuleId.ContainsUserName ||
722+
rule.rule_id === RuleId.ContainsDisplayName ||
723+
rule.rule_id === RuleId.AtRoomNotification)
724+
) {
725+
return false;
726+
}
727+
671728
return !rule.conditions?.some((cond) => !this.eventFulfillsCondition(cond, ev));
672729
}
673730

0 commit comments

Comments
 (0)