You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-06 12:02:40 +03:00
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.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import * as utils from "../test-utils/test-utils";
|
import * as utils from "../test-utils/test-utils";
|
||||||
import { IActionsObject, PushProcessor } from "../../src/pushprocessor";
|
import { IActionsObject, PushProcessor } from "../../src/pushprocessor";
|
||||||
import { ConditionKind, EventType, IContent, MatrixClient, MatrixEvent, PushRuleActionName } from "../../src";
|
import { ConditionKind, EventType, IContent, MatrixClient, MatrixEvent, PushRuleActionName, RuleId } from "../../src";
|
||||||
|
|
||||||
describe("NotificationService", function () {
|
describe("NotificationService", function () {
|
||||||
const testUserId = "@ali:matrix.org";
|
const testUserId = "@ali:matrix.org";
|
||||||
@@ -48,6 +48,7 @@ describe("NotificationService", function () {
|
|||||||
credentials: {
|
credentials: {
|
||||||
userId: testUserId,
|
userId: testUserId,
|
||||||
},
|
},
|
||||||
|
supportsIntentionalMentions: () => true,
|
||||||
pushRules: {
|
pushRules: {
|
||||||
device: {},
|
device: {},
|
||||||
global: {
|
global: {
|
||||||
@@ -712,6 +713,37 @@ describe("NotificationService", function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("test intentional mentions behaviour", () => {
|
||||||
|
it.each([RuleId.ContainsUserName, RuleId.ContainsDisplayName, RuleId.AtRoomNotification])(
|
||||||
|
"Rule %s matches unless intentional mentions are enabled",
|
||||||
|
(ruleId) => {
|
||||||
|
const rule = {
|
||||||
|
rule_id: ruleId,
|
||||||
|
actions: [],
|
||||||
|
conditions: [],
|
||||||
|
default: false,
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
expect(pushProcessor.ruleMatchesEvent(rule, testEvent)).toBe(true);
|
||||||
|
|
||||||
|
// Add the mentions property to the event and the rule is now disabled.
|
||||||
|
testEvent = utils.mkEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
room: testRoomId,
|
||||||
|
user: "@alfred:localhost",
|
||||||
|
event: true,
|
||||||
|
content: {
|
||||||
|
"body": "",
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"org.matrix.msc3952.mentions": {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(pushProcessor.ruleMatchesEvent(rule, testEvent)).toBe(false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test PushProcessor.partsForDottedKey", function () {
|
describe("Test PushProcessor.partsForDottedKey", function () {
|
||||||
|
@@ -137,6 +137,8 @@ export enum PushRuleKind {
|
|||||||
|
|
||||||
export enum RuleId {
|
export enum RuleId {
|
||||||
Master = ".m.rule.master",
|
Master = ".m.rule.master",
|
||||||
|
IsUserMention = ".org.matrix.msc3952.is_user_mention",
|
||||||
|
IsRoomMention = ".org.matrix.msc3952.is_room_mention",
|
||||||
ContainsDisplayName = ".m.rule.contains_display_name",
|
ContainsDisplayName = ".m.rule.contains_display_name",
|
||||||
ContainsUserName = ".m.rule.contains_user_name",
|
ContainsUserName = ".m.rule.contains_user_name",
|
||||||
AtRoomNotification = ".m.rule.roomnotif",
|
AtRoomNotification = ".m.rule.roomnotif",
|
||||||
|
@@ -461,6 +461,11 @@ export interface IStartClientOpts {
|
|||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
slidingSync?: SlidingSync;
|
slidingSync?: SlidingSync;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
intentionalMentions?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IStoredClientOpts extends IStartClientOpts {}
|
export interface IStoredClientOpts extends IStartClientOpts {}
|
||||||
@@ -8575,7 +8580,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
*/
|
*/
|
||||||
public setPushRules(rules: IPushRules): void {
|
public setPushRules(rules: IPushRules): void {
|
||||||
// Fix-up defaults, if applicable.
|
// Fix-up defaults, if applicable.
|
||||||
this.pushRules = PushProcessor.rewriteDefaultRules(rules);
|
this.pushRules = PushProcessor.rewriteDefaultRules(rules, this.getUserId()!);
|
||||||
// Pre-calculate any necessary caches.
|
// Pre-calculate any necessary caches.
|
||||||
this.pushProcessor.updateCachedPushRuleKeys(this.pushRules);
|
this.pushProcessor.updateCachedPushRuleKeys(this.pushRules);
|
||||||
}
|
}
|
||||||
@@ -9472,6 +9477,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
return this.clientOpts?.threadSupport || false;
|
return this.clientOpts?.threadSupport || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper to determine intentional mentions support
|
||||||
|
* @returns a boolean to determine if intentional mentions are enabled
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
public supportsIntentionalMentions(): boolean {
|
||||||
|
return this.clientOpts?.intentionalMentions || false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the summary of a room as defined by an initial version of MSC3266 and implemented in Synapse
|
* Fetches the summary of a room as defined by an initial version of MSC3266 and implemented in Synapse
|
||||||
* Proposed at https://github.com/matrix-org/matrix-doc/pull/3266
|
* Proposed at https://github.com/matrix-org/matrix-doc/pull/3266
|
||||||
|
@@ -48,6 +48,8 @@ export interface IContent {
|
|||||||
"avatar_url"?: string;
|
"avatar_url"?: string;
|
||||||
"displayname"?: string;
|
"displayname"?: string;
|
||||||
"m.relates_to"?: IEventRelation;
|
"m.relates_to"?: IEventRelation;
|
||||||
|
|
||||||
|
"org.matrix.msc3952.mentions"?: IMentions;
|
||||||
}
|
}
|
||||||
|
|
||||||
type StrippedState = Required<Pick<IEvent, "content" | "state_key" | "type" | "sender">>;
|
type StrippedState = Required<Pick<IEvent, "content" | "state_key" | "type" | "sender">>;
|
||||||
@@ -114,6 +116,11 @@ export interface IEventRelation {
|
|||||||
"key"?: string;
|
"key"?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IMentions {
|
||||||
|
user_ids?: string[];
|
||||||
|
room?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When an event is a visibility change event, as per MSC3531,
|
* When an event is a visibility change event, as per MSC3531,
|
||||||
* the visibility change implied by the event.
|
* the visibility change implied by the event.
|
||||||
|
@@ -36,6 +36,7 @@ import {
|
|||||||
PushRuleCondition,
|
PushRuleCondition,
|
||||||
PushRuleKind,
|
PushRuleKind,
|
||||||
PushRuleSet,
|
PushRuleSet,
|
||||||
|
RuleId,
|
||||||
TweakName,
|
TweakName,
|
||||||
} from "./@types/PushRules";
|
} from "./@types/PushRules";
|
||||||
import { EventType } from "./@types/event";
|
import { EventType } from "./@types/event";
|
||||||
@@ -70,6 +71,36 @@ const DEFAULT_OVERRIDE_RULES: IPushRule[] = [
|
|||||||
],
|
],
|
||||||
actions: [PushRuleActionName.DontNotify],
|
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
|
// For homeservers which don't support MSC3786 yet
|
||||||
rule_id: ".org.matrix.msc3786.rule.room.server_acl",
|
rule_id: ".org.matrix.msc3786.rule.room.server_acl",
|
||||||
@@ -160,9 +191,10 @@ export class PushProcessor {
|
|||||||
* where applicable. Useful for upgrading push rules to more strict
|
* where applicable. Useful for upgrading push rules to more strict
|
||||||
* conditions when the server is falling behind on defaults.
|
* conditions when the server is falling behind on defaults.
|
||||||
* @param incomingRules - The client's existing push rules
|
* @param incomingRules - The client's existing push rules
|
||||||
|
* @param userId - The Matrix ID of the client.
|
||||||
* @returns The rewritten rules
|
* @returns The rewritten rules
|
||||||
*/
|
*/
|
||||||
public static rewriteDefaultRules(incomingRules: IPushRules): IPushRules {
|
public static rewriteDefaultRules(incomingRules: IPushRules, userId: string | undefined = undefined): IPushRules {
|
||||||
let newRules: IPushRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone
|
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
|
// These lines are mostly to make the tests happy. We shouldn't run into these
|
||||||
@@ -174,8 +206,22 @@ export class PushProcessor {
|
|||||||
|
|
||||||
// Merge the client-level defaults with the ones from the server
|
// Merge the client-level defaults with the ones from the server
|
||||||
const globalOverrides = newRules.global.override;
|
const globalOverrides = newRules.global.override;
|
||||||
for (const override of DEFAULT_OVERRIDE_RULES) {
|
for (const originalOverride of DEFAULT_OVERRIDE_RULES) {
|
||||||
const existingRule = globalOverrides.find((r) => r.rule_id === override.rule_id);
|
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) {
|
if (existingRule) {
|
||||||
// Copy over the actions, default, and conditions. Don't touch the user's preference.
|
// Copy over the actions, default, and conditions. Don't touch the user's preference.
|
||||||
@@ -668,6 +714,17 @@ export class PushProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ruleMatchesEvent(rule: Partial<IPushRule> & Pick<IPushRule, "conditions">, ev: MatrixEvent): boolean {
|
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));
|
return !rule.conditions?.some((cond) => !this.eventFulfillsCondition(cond, ev));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user