Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions spec/unit/pushprocessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1049,3 +1049,98 @@ describe("getPushRuleGlobRegex", () => {
expect(input.split(regex)).toEqual(["Foo ", "@room", " Bar"]);
});
});

describe("recipient_permission condition (MSC4506 knock push rule)", () => {
const roomId = "!knockroom:server";
const adminId = "@admin:server";
const knockerId = "@knocker:server";

const knockRule: IPushRule = {
rule_id: ".org.matrix.msc4506.rule.knock",
default: true,
enabled: true,
conditions: [
{ kind: ConditionKind.EventPropertyIs, key: "type", value: "m.room.member" },
{ kind: ConditionKind.EventPropertyIs, key: "content.membership", value: "knock" },
{ kind: ConditionKind.RecipientPermissionPrefix, key: "invite" },
],
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Sound, value: "default" }],
};

const makeClient = (plContent: IContent): MatrixClient =>
({
getRoom: () => ({
currentState: {
getStateEvents: (type: string, stateKey: string) =>
type === EventType.RoomPowerLevels && stateKey === "" ? { getContent: () => plContent } : null,
getMember: () => null,
getJoinedMemberCount: () => 2,
members: {},
},
}),
...mockClientMethodsUser(adminId),
supportsIntentionalMentions: () => true,
pushRules: {
device: {},
global: {
override: [
knockRule,
{
rule_id: ".m.rule.member_event",
default: true,
enabled: true,
conditions: [{ kind: ConditionKind.EventPropertyIs, key: "type", value: "m.room.member" }],
actions: [],
},
],
},
},
}) as unknown as MatrixClient;

const mkKnock = (): MatrixEvent =>
utils.mkEvent({
type: "m.room.member",
room: roomId,
user: knockerId,
skey: knockerId,
event: true,
content: { membership: "knock" },
});

const actionsFor = (plContent: IContent): IActionsObject => {
const pushProcessor = new PushProcessor(makeClient(plContent));
return pushProcessor.actionsForEvent(mkKnock());
};

it("notifies a user whose power level allows them to invite", () => {
const actions = actionsFor({ invite: 50, users: { [adminId]: 100 } });
expect(actions.notify).toBeTruthy();
});

it("does not notify a user below the required invite level", () => {
const actions = actionsFor({ invite: 50, users: { [adminId]: 0 } });
expect(actions?.notify).toBeFalsy();
});

it("uses the spec default invite level (0) when absent", () => {
// invite defaults to 0, users_default defaults to 0 -> everyone can invite
const actions = actionsFor({ users: {} });
expect(actions.notify).toBeTruthy();
});

it("respects users_default for the recipient's level", () => {
const actions = actionsFor({ invite: 25, users_default: 30 });
expect(actions.notify).toBeTruthy();
});

it("does not match an unknown permission key", () => {
const pushProcessor = new PushProcessor(makeClient({ invite: 0, frobnicate: 0, users: { [adminId]: 100 } }));
const ruleWithBadKey: IPushRule = {
...knockRule,
conditions: [{ kind: ConditionKind.RecipientPermissionPrefix, key: "frobnicate" }],
};
expect(
pushProcessor.ruleMatchesEvent({ ...ruleWithBadKey, rule_id: "test", kind: "override" } as any, mkKnock()),
).toBe(false);
});
});
11 changes: 11 additions & 0 deletions src/@types/PushRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ export enum ConditionKind {
ContainsDisplayName = "contains_display_name",
RoomMemberCount = "room_member_count",
SenderNotificationPermission = "sender_notification_permission",
RecipientPermission = "recipient_permission",
RecipientPermissionPrefix = "org.matrix.msc4506.recipient_permission",
CallStarted = "call_started",
CallStartedPrefix = "org.matrix.msc3914.call_started",
}
Expand Down Expand Up @@ -103,6 +105,12 @@ export interface ISenderNotificationPermissionCondition extends IPushRuleConditi
key: string;
}

export interface IRecipientPermissionCondition extends IPushRuleCondition<
ConditionKind.RecipientPermission | ConditionKind.RecipientPermissionPrefix
> {
key: string;
}

export interface ICallStartedCondition extends IPushRuleCondition<ConditionKind.CallStarted> {
// no additional fields
}
Expand All @@ -120,6 +128,7 @@ export type PushRuleCondition =
| IContainsDisplayNameCondition
| IRoomMemberCountCondition
| ISenderNotificationPermissionCondition
| IRecipientPermissionCondition
| ICallStartedCondition
| ICallStartedPrefixCondition;

Expand All @@ -143,6 +152,8 @@ export enum RuleId {
Message = ".m.rule.message",
EncryptedMessage = ".m.rule.encrypted",
InviteToSelf = ".m.rule.invite_for_me",
Knock = ".m.rule.knock",
KnockUnstable = ".org.matrix.msc4506.rule.knock",
MemberEvent = ".m.rule.member_event",
IncomingCall = ".m.rule.call",
SuppressNotices = ".m.rule.suppress_notices",
Expand Down
39 changes: 39 additions & 0 deletions src/pushprocessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
type IEventPropertyIsCondition,
type IPushRule,
type IPushRules,
type IRecipientPermissionCondition,
type IRoomMemberCountCondition,
type ISenderNotificationPermissionCondition,
type PushRuleAction,
Expand Down Expand Up @@ -479,6 +480,9 @@
return this.eventFulfillsRoomMemberCountCondition(cond, ev);
case ConditionKind.SenderNotificationPermission:
return this.eventFulfillsSenderNotifPermCondition(cond, ev);
case ConditionKind.RecipientPermission:
case ConditionKind.RecipientPermissionPrefix:
return this.eventFulfillsRecipientPermCondition(cond, ev);
case ConditionKind.CallStarted:
case ConditionKind.CallStartedPrefix:
return this.eventFulfillsCallStartedCondition(cond, ev);
Expand Down Expand Up @@ -510,6 +514,41 @@
return room.currentState.mayTriggerNotifOfType(notifLevelKey, ev.getSender()!);
}

/**
* MSC4506 `recipient_permission` condition: matches if the user these push
* rules are being evaluated for (i.e. us) has a power level at least that
* required to perform the `m.room.power_levels` action named by `key`
* (e.g. "invite"), in the room the event is in.
*/
private eventFulfillsRecipientPermCondition(cond: IRecipientPermissionCondition, ev: MatrixEvent): boolean {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be gated behind an opt-in as it is relying on an MSC.

const actionKey = cond["key"];
// Only the power-levels permission actions are valid keys.
const defaultLevels: Record<string, number> = { invite: 0, kick: 50, ban: 50, redact: 50 };
if (!actionKey || !(actionKey in defaultLevels)) {
return false;
}

const room = this.client.getRoom(ev.getRoomId());
const userId = this.client.getUserId();
if (!room?.currentState || !userId) {

Check warning on line 533 in src/pushprocessor.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'currentState' is deprecated.

See more on https://sonarcloud.io/project/issues?id=matrix-js-sdk&issues=AZ9qs35_JIwfjAv6ztm2&open=AZ9qs35_JIwfjAv6ztm2&pullRequest=5425
return false;

Check warning on line 534 in src/pushprocessor.ts

View workflow job for this annotation

GitHub Actions / Tests

Uncovered Line

Line 534 is not covered by tests
}

// 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.
const plContent = room.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() ?? {};

Check warning on line 540 in src/pushprocessor.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'currentState' is deprecated.

See more on https://sonarcloud.io/project/issues?id=matrix-js-sdk&issues=AZ9qs35_JIwfjAv6ztm3&open=AZ9qs35_JIwfjAv6ztm3&pullRequest=5425
const requiredLevel =
typeof plContent[actionKey] === "number" ? plContent[actionKey] : defaultLevels[actionKey];
const ourLevel =
typeof plContent.users?.[userId] === "number"
? plContent.users[userId]
: typeof plContent.users_default === "number"
? plContent.users_default
: 0;

Check warning on line 548 in src/pushprocessor.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=matrix-js-sdk&issues=AZ9qs35_JIwfjAv6ztm4&open=AZ9qs35_JIwfjAv6ztm4&pullRequest=5425
return ourLevel >= requiredLevel;
}

private eventFulfillsRoomMemberCountCondition(cond: IRoomMemberCountCondition, ev: MatrixEvent): boolean {
if (!cond.is) {
return false;
Expand Down
Loading