diff --git a/spec/test-utils/test-utils.ts b/spec/test-utils/test-utils.ts index 0187b12f5..288e0355b 100644 --- a/spec/test-utils/test-utils.ts +++ b/spec/test-utils/test-utils.ts @@ -74,6 +74,7 @@ interface IEventOpts { sender?: string; skey?: string; content: IContent; + prev_content?: IContent; user?: string; unsigned?: IUnsigned; redacts?: string; @@ -103,6 +104,7 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC room_id: opts.room, sender: opts.sender || opts.user, // opts.user for backwards-compat content: opts.content, + prev_content: opts.prev_content, unsigned: opts.unsigned || {}, event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(), txn_id: "~" + Math.random(), diff --git a/spec/unit/pushprocessor.spec.ts b/spec/unit/pushprocessor.spec.ts index 3bbdd5233..db4d2a417 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 { PushProcessor } from "../../src/pushprocessor"; -import { EventType, MatrixClient, MatrixEvent } from "../../src"; +import { IActionsObject, PushProcessor } from "../../src/pushprocessor"; +import { EventType, IContent, MatrixClient, MatrixEvent } from "../../src"; describe('NotificationService', function() { const testUserId = "@ali:matrix.org"; @@ -336,4 +336,102 @@ describe('NotificationService', function() { enabled: true, }, testEvent)).toBe(true); }); + + describe("performCustomEventHandling()", () => { + const getActionsForEvent = (prevContent: IContent, content: IContent): IActionsObject => { + testEvent = utils.mkEvent({ + type: "org.matrix.msc3401.call", + room: testRoomId, + user: "@alice:foo", + skey: "state_key", + event: true, + content: content, + prev_content: prevContent, + }); + + return pushProcessor.actionsForEvent(testEvent); + }; + + const assertDoesNotify = (actions: IActionsObject): void => { + expect(actions.notify).toBeTruthy(); + expect(actions.tweaks.sound).toBeTruthy(); + expect(actions.tweaks.highlight).toBeFalsy(); + }; + + const assertDoesNotNotify = (actions: IActionsObject): void => { + expect(actions.notify).toBeFalsy(); + expect(actions.tweaks.sound).toBeFalsy(); + expect(actions.tweaks.highlight).toBeFalsy(); + }; + + it.each( + ["m.ring", "m.prompt"], + )("should notify when new group call event appears with %s intent", (intent: string) => { + assertDoesNotify(getActionsForEvent({}, { + "m.intent": intent, + "m.type": "m.voice", + "m.name": "Call", + })); + }); + + it("should notify when a call is un-terminated", () => { + assertDoesNotify(getActionsForEvent({ + "m.intent": "m.ring", + "m.type": "m.voice", + "m.name": "Call", + "m.terminated": "All users left", + }, { + "m.intent": "m.ring", + "m.type": "m.voice", + "m.name": "Call", + })); + }); + + it("should not notify when call is terminated", () => { + assertDoesNotNotify(getActionsForEvent({ + "m.intent": "m.ring", + "m.type": "m.voice", + "m.name": "Call", + }, { + "m.intent": "m.ring", + "m.type": "m.voice", + "m.name": "Call", + "m.terminated": "All users left", + })); + }); + + it("should ignore with m.room intent", () => { + assertDoesNotNotify(getActionsForEvent({}, { + "m.intent": "m.room", + "m.type": "m.voice", + "m.name": "Call", + })); + }); + + describe("ignoring non-relevant state changes", () => { + it("should ignore intent changes", () => { + assertDoesNotNotify(getActionsForEvent({ + "m.intent": "m.ring", + "m.type": "m.voice", + "m.name": "Call", + }, { + "m.intent": "m.ring", + "m.type": "m.video", + "m.name": "Call", + })); + }); + + it("should ignore name changes", () => { + assertDoesNotNotify(getActionsForEvent({ + "m.intent": "m.ring", + "m.type": "m.voice", + "m.name": "Call", + }, { + "m.intent": "m.ring", + "m.type": "m.voice", + "m.name": "New call", + })); + }); + }); + }); }); diff --git a/src/pushprocessor.ts b/src/pushprocessor.ts index 4e736c3a1..1caa78953 100644 --- a/src/pushprocessor.ts +++ b/src/pushprocessor.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { escapeRegExp, globToRegexp, isNullOrUndefined } from "./utils"; +import { deepCompare, escapeRegExp, globToRegexp, isNullOrUndefined } from "./utils"; import { logger } from './logger'; import { MatrixClient } from "./client"; import { MatrixEvent } from "./models/event"; @@ -91,6 +91,20 @@ const DEFAULT_OVERRIDE_RULES: IPushRule[] = [ ], actions: [], }, + { + // For homeservers which don't support MSC3401 yet + rule_id: ".org.matrix.msc3401.rule.room.call", + default: true, + enabled: true, + conditions: [ + { + kind: ConditionKind.EventMatch, + key: "type", + pattern: "org.matrix.msc3401.call", + }, + ], + actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Sound, value: "default" }], + }, ]; export interface IActionsObject { @@ -424,7 +438,7 @@ export class PushProcessor { return {} as IActionsObject; } - const actionObj = PushProcessor.actionListToActionsObject(rule.actions); + let actionObj = PushProcessor.actionListToActionsObject(rule.actions); // Some actions are implicit in some situations: we add those here if (actionObj.tweaks.highlight === undefined) { @@ -433,6 +447,30 @@ export class PushProcessor { actionObj.tweaks.highlight = (rule.kind == PushRuleKind.ContentSpecific); } + actionObj = this.performCustomEventHandling(ev, actionObj); + + return actionObj; + } + + /** + * Some events require custom event handling e.g. due to missing server support + */ + private performCustomEventHandling(ev: MatrixEvent, actionObj: IActionsObject): IActionsObject { + switch (ev.getType()) { + case "m.call": + case "org.matrix.msc3401.call": + // Since servers don't support properly sending push notification + // about MSC3401 call events, we do the handling ourselves + if ( + ev.getContent()["m.intent"] === "m.room" + || ("m.terminated" in ev.getContent()) + || !("m.terminated" in ev.getPrevContent()) && !deepCompare(ev.getPrevContent(), {}) + ) { + actionObj.notify = false; + actionObj.tweaks = {}; + } + } + return actionObj; }