1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-25 05:23:13 +03:00

Merge branch 'develop' into gsouquet/threaded-messaging-2349

This commit is contained in:
Germain Souquet
2021-08-17 11:14:10 +01:00
32 changed files with 1084 additions and 746 deletions

View File

@@ -0,0 +1,12 @@
name: Preview Changelog
on:
pull_request_target:
types: [ opened, edited, labeled ]
jobs:
changelog:
runs-on: ubuntu-latest
steps:
- name: Preview Changelog
uses: matrix-org/allchange@main
with:
ghToken: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,4 +1,28 @@
Changes in [12.2.0](https://github.com/vector-im/element-desktop/releases/tag/v12.2.0) (2021-07-02) Changes in [12.3.1](https://github.com/vector-im/element-desktop/releases/tag/v12.3.1) (2021-08-17)
===================================================================================================
## 🐛 Bug Fixes
* Fix multiple VoIP regressions ([\#1860](https://github.com/matrix-org/matrix-js-sdk/pull/1860)).
Changes in [12.3.0](https://github.com/vector-im/element-desktop/releases/tag/v12.3.0) (2021-08-16)
===================================================================================================
## ✨ Features
* Support for MSC3291: Muting in VoIP calls ([\#1812](https://github.com/matrix-org/matrix-js-sdk/pull/1812)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Support for screen-sharing using multi-stream VoIP (MSC3077) ([\#1685](https://github.com/matrix-org/matrix-js-sdk/pull/1685)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Handle DTMF support ([\#1813](https://github.com/matrix-org/matrix-js-sdk/pull/1813)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
## 🐛 Bug Fixes
* [Release] Fix glare related regressions ([\#1854](https://github.com/matrix-org/matrix-js-sdk/pull/1854)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix the types in shipped package ([\#1842](https://github.com/matrix-org/matrix-js-sdk/pull/1842)). Fixes vector-im/element-web#18503 and vector-im/element-web#18503.
* Fix error on turning off screensharing ([\#1833](https://github.com/matrix-org/matrix-js-sdk/pull/1833)). Fixes vector-im/element-web#18449. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Fix blank profile in join events ([\#1837](https://github.com/matrix-org/matrix-js-sdk/pull/1837)). Fixes vector-im/element-web#18321.
* fix TURN by fixing regression preventing multiple ICE candidates from sending. ([\#1838](https://github.com/matrix-org/matrix-js-sdk/pull/1838)).
* Send `user_hangup` reason if the opponent supports it ([\#1820](https://github.com/matrix-org/matrix-js-sdk/pull/1820)). Fixes vector-im/element-web#18219. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
* Apply hidden char check to rawDisplayName too ([\#1816](https://github.com/matrix-org/matrix-js-sdk/pull/1816)).
* Only clear bit 63 when we create the IV ([\#1819](https://github.com/matrix-org/matrix-js-sdk/pull/1819)).
Changes in [12.2.0](https://github.com/vector-im/element-desktop/releases/tag/v12.2.0) (2021-08-02)
=================================================================================================== ===================================================================================================
## ✨ Features ## ✨ Features

View File

@@ -1,6 +1,6 @@
{ {
"name": "matrix-js-sdk", "name": "matrix-js-sdk",
"version": "12.2.0", "version": "12.3.1",
"description": "Matrix Client-Server SDK for Javascript", "description": "Matrix Client-Server SDK for Javascript",
"scripts": { "scripts": {
"prepublishOnly": "yarn build", "prepublishOnly": "yarn build",
@@ -9,9 +9,9 @@
"clean": "rimraf lib dist", "clean": "rimraf lib dist",
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser", "build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser",
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types", "build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"build:types": "tsc --emitDeclarationOnly", "build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src", "build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js", "build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js", "build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "jsdoc -c jsdoc.json -P package.json", "gendoc": "jsdoc -c jsdoc.json -P package.json",
"lint": "yarn lint:types && yarn lint:js", "lint": "yarn lint:types && yarn lint:js",
@@ -81,7 +81,7 @@
"@types/request": "^2.48.5", "@types/request": "^2.48.5",
"@typescript-eslint/eslint-plugin": "^4.17.0", "@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0", "@typescript-eslint/parser": "^4.17.0",
"allchange": "github:matrix-org/allchange", "allchange": "^1.0.0",
"babel-jest": "^26.6.3", "babel-jest": "^26.6.3",
"babelify": "^10.0.0", "babelify": "^10.0.0",
"better-docs": "^2.4.0-beta.9", "better-docs": "^2.4.0-beta.9",

View File

@@ -191,7 +191,10 @@ git commit package.json $pkglock -m "$tag"
# figure out if we should be signing this release # figure out if we should be signing this release
signing_id= signing_id=
if [ -f release_config.yaml ]; then if [ -f release_config.yaml ]; then
signing_id=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']"` result=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']" 2> /dev/null || true`
if [ "$?" -eq 0 ]; then
signing_id=$result
fi
fi fi

View File

@@ -237,6 +237,7 @@ describe("MatrixClient", function() {
it("should get (unstable) file trees with valid state", async () => { it("should get (unstable) file trees with valid state", async () => {
const roomId = "!room:example.org"; const roomId = "!room:example.org";
const mockRoom = { const mockRoom = {
getMyMembership: () => "join",
currentState: { currentState: {
getStateEvents: (eventType, stateKey) => { getStateEvents: (eventType, stateKey) => {
if (eventType === EventType.RoomCreate) { if (eventType === EventType.RoomCreate) {
@@ -270,9 +271,33 @@ describe("MatrixClient", function() {
expect(tree.room).toBe(mockRoom); expect(tree.room).toBe(mockRoom);
}); });
it("should not get (unstable) file trees if not joined", async () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "leave", // "not join"
};
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
return mockRoom;
};
const tree = client.unstableGetFileTreeSpace(roomId);
expect(tree).toBeFalsy();
});
it("should not get (unstable) file trees for unknown rooms", async () => {
const roomId = "!room:example.org";
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
return null; // imply unknown
};
const tree = client.unstableGetFileTreeSpace(roomId);
expect(tree).toBeFalsy();
});
it("should not get (unstable) file trees with invalid create contents", async () => { it("should not get (unstable) file trees with invalid create contents", async () => {
const roomId = "!room:example.org"; const roomId = "!room:example.org";
const mockRoom = { const mockRoom = {
getMyMembership: () => "join",
currentState: { currentState: {
getStateEvents: (eventType, stateKey) => { getStateEvents: (eventType, stateKey) => {
if (eventType === EventType.RoomCreate) { if (eventType === EventType.RoomCreate) {
@@ -307,6 +332,7 @@ describe("MatrixClient", function() {
it("should not get (unstable) file trees with invalid purpose/subtype contents", async () => { it("should not get (unstable) file trees with invalid purpose/subtype contents", async () => {
const roomId = "!room:example.org"; const roomId = "!room:example.org";
const mockRoom = { const mockRoom = {
getMyMembership: () => "join",
currentState: { currentState: {
getStateEvents: (eventType, stateKey) => { getStateEvents: (eventType, stateKey) => {
if (eventType === EventType.RoomCreate) { if (eventType === EventType.RoomCreate) {

View File

@@ -16,7 +16,6 @@ limitations under the License.
import { MatrixClient } from "../../../src"; import { MatrixClient } from "../../../src";
import { Room } from "../../../src/models/room"; import { Room } from "../../../src/models/room";
import { MatrixEvent } from "../../../src/models/event";
import { UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event"; import { UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
import { EventTimelineSet } from "../../../src/models/event-timeline-set"; import { EventTimelineSet } from "../../../src/models/event-timeline-set";
import { EventTimeline } from "../../../src/models/event-timeline"; import { EventTimeline } from "../../../src/models/event-timeline";
@@ -25,7 +24,7 @@ import { MSC3089Branch } from "../../../src/models/MSC3089Branch";
describe("MSC3089Branch", () => { describe("MSC3089Branch", () => {
let client: MatrixClient; let client: MatrixClient;
// @ts-ignore - TS doesn't know that this is a type // @ts-ignore - TS doesn't know that this is a type
let indexEvent: MatrixEvent; let indexEvent: any;
let branch: MSC3089Branch; let branch: MSC3089Branch;
const branchRoomId = "!room:example.org"; const branchRoomId = "!room:example.org";
@@ -47,10 +46,10 @@ describe("MSC3089Branch", () => {
} }
}, },
}; };
indexEvent = { indexEvent = ({
getRoomId: () => branchRoomId, getRoomId: () => branchRoomId,
getStateKey: () => fileEventId, getStateKey: () => fileEventId,
}; });
branch = new MSC3089Branch(client, indexEvent); branch = new MSC3089Branch(client, indexEvent);
}); });

View File

@@ -29,7 +29,7 @@ import { MatrixError } from "../../../src/http-api";
describe("MSC3089TreeSpace", () => { describe("MSC3089TreeSpace", () => {
let client: MatrixClient; let client: MatrixClient;
let room: Room; let room: any;
let tree: MSC3089TreeSpace; let tree: MSC3089TreeSpace;
const roomId = "!tree:localhost"; const roomId = "!tree:localhost";
const targetUser = "@target:example.org"; const targetUser = "@target:example.org";
@@ -170,7 +170,7 @@ describe("MSC3089TreeSpace", () => {
expect(userIds).toMatchObject([target]); expect(userIds).toMatchObject([target]);
return Promise.resolve(); return Promise.resolve();
}); });
client.invite = () => Promise.resolve(); // we're not testing this here - see other tests client.invite = () => Promise.resolve({}); // we're not testing this here - see other tests
client.sendSharedHistoryKeys = sendKeysFn; client.sendSharedHistoryKeys = sendKeysFn;
// Mock the history check as best as possible // Mock the history check as best as possible
@@ -198,7 +198,7 @@ describe("MSC3089TreeSpace", () => {
expect(userIds).toMatchObject([target]); expect(userIds).toMatchObject([target]);
return Promise.resolve(); return Promise.resolve();
}); });
client.invite = () => Promise.resolve(); // we're not testing this here - see other tests client.invite = () => Promise.resolve({}); // we're not testing this here - see other tests
client.sendSharedHistoryKeys = sendKeysFn; client.sendSharedHistoryKeys = sendKeysFn;
const historyVis = "joined"; // NOTE: Changed. const historyVis = "joined"; // NOTE: Changed.
@@ -446,9 +446,9 @@ describe("MSC3089TreeSpace", () => {
// Danger: these are partial implementations for testing purposes only // Danger: these are partial implementations for testing purposes only
// @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important // @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important
let childState: { [roomId: string]: MatrixEvent[] } = {}; let childState: { [roomId: string]: any[] } = {};
// @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important // @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important
let parentState: MatrixEvent[] = []; let parentState: any[] = [];
let parentRoom: Room; let parentRoom: Room;
let childTrees: MSC3089TreeSpace[]; let childTrees: MSC3089TreeSpace[];
let rooms: { [roomId: string]: Room }; let rooms: { [roomId: string]: Room };

View File

@@ -16,11 +16,13 @@ limitations under the License.
import { EventTimelineSet } from "../../src/models/event-timeline-set"; import { EventTimelineSet } from "../../src/models/event-timeline-set";
import { MatrixEvent } from "../../src/models/event"; import { MatrixEvent } from "../../src/models/event";
import { Room } from "../../src/models/room";
import { Relations } from "../../src/models/relations"; import { Relations } from "../../src/models/relations";
describe("Relations", function() { describe("Relations", function() {
it("should deduplicate annotations", function() { it("should deduplicate annotations", function() {
const relations = new Relations("m.annotation", "m.reaction"); const room = new Room("room123", null, null);
const relations = new Relations("m.annotation", "m.reaction", room);
// Create an instance of an annotation // Create an instance of an annotation
const eventData = { const eventData = {
@@ -95,10 +97,8 @@ describe("Relations", function() {
}); });
// Stub the room // Stub the room
const room = {
getPendingEvent() { return null; }, const room = new Room("room123", null, null);
getUnfilteredTimelineSet() { return null; },
};
// Add the target event first, then the relation event // Add the target event first, then the relation event
{ {

View File

@@ -90,8 +90,9 @@ export interface ISenderNotificationPermissionCondition
key: string; key: string;
} }
export type PushRuleCondition = IPushRuleCondition<string> // XXX: custom conditions are possible but always fail, and break the typescript discriminated union so ignore them here
| IEventMatchCondition // IPushRuleCondition<Exclude<string, ConditionKind>> unfortunately does not resolve this at the time of writing.
export type PushRuleCondition = IEventMatchCondition
| IContainsDisplayNameCondition | IContainsDisplayNameCondition
| IRoomMemberCountCondition | IRoomMemberCountCondition
| ISenderNotificationPermissionCondition; | ISenderNotificationPermissionCondition;

View File

@@ -30,7 +30,7 @@ import * as utils from './utils';
import { sleep } from './utils'; import { sleep } from './utils';
import { Group } from "./models/group"; import { Group } from "./models/group";
import { Direction, EventTimeline } from "./models/event-timeline"; import { Direction, EventTimeline } from "./models/event-timeline";
import { PushAction, PushProcessor } from "./pushprocessor"; import { IActionsObject, PushProcessor } from "./pushprocessor";
import { AutoDiscovery } from "./autodiscovery"; import { AutoDiscovery } from "./autodiscovery";
import * as olmlib from "./crypto/olmlib"; import * as olmlib from "./crypto/olmlib";
import { decodeBase64, encodeBase64 } from "./crypto/olmlib"; import { decodeBase64, encodeBase64 } from "./crypto/olmlib";
@@ -2931,6 +2931,7 @@ export class MatrixClient extends EventEmitter {
* has been emitted. * has been emitted.
* @param {string} groupId The group ID * @param {string} groupId The group ID
* @return {Group} The Group or null if the group is not known or there is no data store. * @return {Group} The Group or null if the group is not known or there is no data store.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public getGroup(groupId: string): Group { public getGroup(groupId: string): Group {
return this.store.getGroup(groupId); return this.store.getGroup(groupId);
@@ -2939,6 +2940,7 @@ export class MatrixClient extends EventEmitter {
/** /**
* Retrieve all known groups. * Retrieve all known groups.
* @return {Group[]} A list of groups, or an empty list if there is no data store. * @return {Group[]} A list of groups, or an empty list if there is no data store.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public getGroups(): Group[] { public getGroups(): Group[] {
return this.store.getGroups(); return this.store.getGroups();
@@ -4340,7 +4342,7 @@ export class MatrixClient extends EventEmitter {
* @param {MatrixEvent} event The event to get push actions for. * @param {MatrixEvent} event The event to get push actions for.
* @return {module:pushprocessor~PushAction} A dict of actions to perform. * @return {module:pushprocessor~PushAction} A dict of actions to perform.
*/ */
public getPushActionsForEvent(event: MatrixEvent): PushAction { public getPushActionsForEvent(event: MatrixEvent): IActionsObject {
if (!event.getPushActions()) { if (!event.getPushActions()) {
event.setPushActions(this.pushProcessor.actionsForEvent(event)); event.setPushActions(this.pushProcessor.actionsForEvent(event));
} }
@@ -5184,11 +5186,11 @@ export class MatrixClient extends EventEmitter {
* The operation also updates MatrixClient.pushRules at the end. * The operation also updates MatrixClient.pushRules at the end.
* @param {string} scope "global" or device-specific. * @param {string} scope "global" or device-specific.
* @param {string} roomId the id of the room. * @param {string} roomId the id of the room.
* @param {string} mute the mute state. * @param {boolean} mute the mute state.
* @return {Promise} Resolves: result object * @return {Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
public setRoomMutePushRule(scope: string, roomId: string, mute: string): Promise<void> | void { public setRoomMutePushRule(scope: string, roomId: string, mute: boolean): Promise<void> | void {
let deferred; let deferred;
let hasDontNotifyRule; let hasDontNotifyRule;
@@ -8060,7 +8062,7 @@ export class MatrixClient extends EventEmitter {
*/ */
public unstableGetFileTreeSpace(roomId: string): MSC3089TreeSpace { public unstableGetFileTreeSpace(roomId: string): MSC3089TreeSpace {
const room = this.getRoom(roomId); const room = this.getRoom(roomId);
if (!room) return null; if (room?.getMyMembership() !== 'join') return null;
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, ""); const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, "");
const purposeEvent = room.currentState.getStateEvents( const purposeEvent = room.currentState.getStateEvents(
@@ -8085,6 +8087,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} groupId * @param {string} groupId
* @return {Promise} Resolves: Group summary object * @return {Promise} Resolves: Group summary object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public getGroupSummary(groupId: string): Promise<any> { public getGroupSummary(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/summary", { $groupId: groupId }); const path = utils.encodeUri("/groups/$groupId/summary", { $groupId: groupId });
@@ -8095,6 +8098,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} groupId * @param {string} groupId
* @return {Promise} Resolves: Group profile object * @return {Promise} Resolves: Group profile object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public getGroupProfile(groupId: string): Promise<any> { public getGroupProfile(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId }); const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId });
@@ -8110,6 +8114,7 @@ export class MatrixClient extends EventEmitter {
* @param {string=} profile.long_description A longer HTML description of the room * @param {string=} profile.long_description A longer HTML description of the room
* @return {Promise} Resolves: Empty object * @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public setGroupProfile(groupId: string, profile: any): Promise<any> { public setGroupProfile(groupId: string, profile: any): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId }); const path = utils.encodeUri("/groups/$groupId/profile", { $groupId: groupId });
@@ -8126,6 +8131,7 @@ export class MatrixClient extends EventEmitter {
* required to join. * required to join.
* @return {Promise} Resolves: Empty object * @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public setGroupJoinPolicy(groupId: string, policy: any): Promise<any> { public setGroupJoinPolicy(groupId: string, policy: any): Promise<any> {
const path = utils.encodeUri( const path = utils.encodeUri(
@@ -8143,6 +8149,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} groupId * @param {string} groupId
* @return {Promise} Resolves: Group users list object * @return {Promise} Resolves: Group users list object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public getGroupUsers(groupId: string): Promise<any> { public getGroupUsers(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/users", { $groupId: groupId }); const path = utils.encodeUri("/groups/$groupId/users", { $groupId: groupId });
@@ -8153,6 +8160,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} groupId * @param {string} groupId
* @return {Promise} Resolves: Group users list object * @return {Promise} Resolves: Group users list object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public getGroupInvitedUsers(groupId: string): Promise<any> { public getGroupInvitedUsers(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/invited_users", { $groupId: groupId }); const path = utils.encodeUri("/groups/$groupId/invited_users", { $groupId: groupId });
@@ -8163,6 +8171,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} groupId * @param {string} groupId
* @return {Promise} Resolves: Group rooms list object * @return {Promise} Resolves: Group rooms list object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public getGroupRooms(groupId: string): Promise<any> { public getGroupRooms(groupId: string): Promise<any> {
const path = utils.encodeUri("/groups/$groupId/rooms", { $groupId: groupId }); const path = utils.encodeUri("/groups/$groupId/rooms", { $groupId: groupId });
@@ -8174,6 +8183,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} userId * @param {string} userId
* @return {Promise} Resolves: Empty object * @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public inviteUserToGroup(groupId: string, userId: string): Promise<any> { public inviteUserToGroup(groupId: string, userId: string): Promise<any> {
const path = utils.encodeUri( const path = utils.encodeUri(
@@ -8188,6 +8198,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} userId * @param {string} userId
* @return {Promise} Resolves: Empty object * @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public removeUserFromGroup(groupId: string, userId: string): Promise<any> { public removeUserFromGroup(groupId: string, userId: string): Promise<any> {
const path = utils.encodeUri( const path = utils.encodeUri(
@@ -8203,6 +8214,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} roleId Optional. * @param {string} roleId Optional.
* @return {Promise} Resolves: Empty object * @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public addUserToGroupSummary(groupId: string, userId: string, roleId: string): Promise<any> { public addUserToGroupSummary(groupId: string, userId: string, roleId: string): Promise<any> {
const path = utils.encodeUri( const path = utils.encodeUri(
@@ -8219,6 +8231,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} userId * @param {string} userId
* @return {Promise} Resolves: Empty object * @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public removeUserFromGroupSummary(groupId: string, userId: string): Promise<any> { public removeUserFromGroupSummary(groupId: string, userId: string): Promise<any> {
const path = utils.encodeUri( const path = utils.encodeUri(
@@ -8234,6 +8247,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} categoryId Optional. * @param {string} categoryId Optional.
* @return {Promise} Resolves: Empty object * @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public addRoomToGroupSummary(groupId: string, roomId: string, categoryId: string): Promise<any> { public addRoomToGroupSummary(groupId: string, roomId: string, categoryId: string): Promise<any> {
const path = utils.encodeUri( const path = utils.encodeUri(
@@ -8250,6 +8264,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} roomId * @param {string} roomId
* @return {Promise} Resolves: Empty object * @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public removeRoomFromGroupSummary(groupId: string, roomId: string): Promise<any> { public removeRoomFromGroupSummary(groupId: string, roomId: string): Promise<any> {
const path = utils.encodeUri( const path = utils.encodeUri(
@@ -8265,6 +8280,7 @@ export class MatrixClient extends EventEmitter {
* @param {boolean} isPublic Whether the room-group association is visible to non-members * @param {boolean} isPublic Whether the room-group association is visible to non-members
* @return {Promise} Resolves: Empty object * @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public addRoomToGroup(groupId: string, roomId: string, isPublic: boolean): Promise<any> { public addRoomToGroup(groupId: string, roomId: string, isPublic: boolean): Promise<any> {
if (isPublic === undefined) { if (isPublic === undefined) {
@@ -8286,6 +8302,7 @@ export class MatrixClient extends EventEmitter {
* @param {boolean} isPublic Whether the room-group association is visible to non-members * @param {boolean} isPublic Whether the room-group association is visible to non-members
* @return {Promise} Resolves: Empty object * @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public updateGroupRoomVisibility(groupId: string, roomId: string, isPublic: boolean): Promise<any> { public updateGroupRoomVisibility(groupId: string, roomId: string, isPublic: boolean): Promise<any> {
// NB: The /config API is generic but there's not much point in exposing this yet as synapse // NB: The /config API is generic but there's not much point in exposing this yet as synapse
@@ -8306,6 +8323,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} roomId * @param {string} roomId
* @return {Promise} Resolves: Empty object * @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public removeRoomFromGroup(groupId: string, roomId: string): Promise<any> { public removeRoomFromGroup(groupId: string, roomId: string): Promise<any> {
const path = utils.encodeUri( const path = utils.encodeUri(
@@ -8320,6 +8338,7 @@ export class MatrixClient extends EventEmitter {
* @param {Object} opts Additional options to send alongside the acceptance. * @param {Object} opts Additional options to send alongside the acceptance.
* @return {Promise} Resolves: Empty object * @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public acceptGroupInvite(groupId: string, opts = null): Promise<any> { public acceptGroupInvite(groupId: string, opts = null): Promise<any> {
const path = utils.encodeUri( const path = utils.encodeUri(
@@ -8333,6 +8352,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} groupId * @param {string} groupId
* @return {Promise} Resolves: Empty object * @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public joinGroup(groupId: string): Promise<any> { public joinGroup(groupId: string): Promise<any> {
const path = utils.encodeUri( const path = utils.encodeUri(
@@ -8346,6 +8366,7 @@ export class MatrixClient extends EventEmitter {
* @param {string} groupId * @param {string} groupId
* @return {Promise} Resolves: Empty object * @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public leaveGroup(groupId: string): Promise<any> { public leaveGroup(groupId: string): Promise<any> {
const path = utils.encodeUri( const path = utils.encodeUri(
@@ -8358,6 +8379,7 @@ export class MatrixClient extends EventEmitter {
/** /**
* @return {Promise} Resolves: The groups to which the user is joined * @return {Promise} Resolves: The groups to which the user is joined
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public getJoinedGroups(): Promise<any> { public getJoinedGroups(): Promise<any> {
const path = utils.encodeUri("/joined_groups", {}); const path = utils.encodeUri("/joined_groups", {});
@@ -8370,6 +8392,7 @@ export class MatrixClient extends EventEmitter {
* @param {Object} content.profile Group profile object * @param {Object} content.profile Group profile object
* @return {Promise} Resolves: Object with key group_id: id of the created group * @return {Promise} Resolves: Object with key group_id: id of the created group
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public createGroup(content: any): Promise<any> { public createGroup(content: any): Promise<any> {
const path = utils.encodeUri("/create_group", {}); const path = utils.encodeUri("/create_group", {});
@@ -8390,6 +8413,7 @@ export class MatrixClient extends EventEmitter {
* } * }
* } * }
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public getPublicisedGroups(userIds: string[]): Promise<any> { public getPublicisedGroups(userIds: string[]): Promise<any> {
const path = utils.encodeUri("/publicised_groups", {}); const path = utils.encodeUri("/publicised_groups", {});
@@ -8403,6 +8427,7 @@ export class MatrixClient extends EventEmitter {
* @param {boolean} isPublic Whether the user's membership of this group is made public * @param {boolean} isPublic Whether the user's membership of this group is made public
* @return {Promise} Resolves: Empty object * @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public setGroupPublicity(groupId: string, isPublic: boolean): Promise<any> { public setGroupPublicity(groupId: string, isPublic: boolean): Promise<any> {
const path = utils.encodeUri( const path = utils.encodeUri(
@@ -8567,6 +8592,7 @@ export class MatrixClient extends EventEmitter {
* is experimental and may change.</strong> * is experimental and may change.</strong>
* @event module:client~MatrixClient#"Group" * @event module:client~MatrixClient#"Group"
* @param {Group} group The newly created, fully populated group. * @param {Group} group The newly created, fully populated group.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
* @example * @example
* matrixClient.on("Group", function(group){ * matrixClient.on("Group", function(group){
* var groupId = group.groupId; * var groupId = group.groupId;

View File

@@ -356,7 +356,7 @@ class SSSSCryptoCallbacks {
public async getSecretStorageKey( public async getSecretStorageKey(
{ keys }: { keys: Record<string, ISecretStorageKeyInfo> }, { keys }: { keys: Record<string, ISecretStorageKeyInfo> },
name: string, name: string,
): Promise<[string, Uint8Array]> { ): Promise<[string, Uint8Array]|null> {
for (const keyId of Object.keys(keys)) { for (const keyId of Object.keys(keys)) {
const privateKey = this.privateKeys.get(keyId); const privateKey = this.privateKeys.get(keyId);
if (privateKey) { if (privateKey) {
@@ -374,6 +374,7 @@ class SSSSCryptoCallbacks {
} }
return result; return result;
} }
return null;
} }
public addPrivateKey(keyId: string, keyInfo: ISecretStorageKeyInfo, privKey: Uint8Array): void { public addPrivateKey(keyId: string, keyInfo: ISecretStorageKeyInfo, privKey: Uint8Array): void {

View File

@@ -20,10 +20,90 @@ limitations under the License.
import * as utils from "./utils"; import * as utils from "./utils";
import { logger } from './logger'; import { logger } from './logger';
import { MatrixClient } from "./client";
import { defer, IDeferred } from "./utils";
import { MatrixError } from "./http-api";
const EMAIL_STAGE_TYPE = "m.login.email.identity"; const EMAIL_STAGE_TYPE = "m.login.email.identity";
const MSISDN_STAGE_TYPE = "m.login.msisdn"; const MSISDN_STAGE_TYPE = "m.login.msisdn";
interface IFlow {
stages: AuthType[];
}
export interface IInputs {
emailAddress?: string;
phoneCountry?: string;
phoneNumber?: string;
}
export interface IStageStatus {
emailSid?: string;
errcode?: string;
error?: string;
}
export interface IAuthData {
session?: string;
completed?: string[];
flows?: IFlow[];
params?: Record<string, Record<string, any>>;
errcode?: string;
error?: MatrixError;
}
export enum AuthType {
Password = "m.login.password",
Recaptcha = "m.login.recaptcha",
Terms = "m.login.terms",
Email = "m.login.email.identity",
Msisdn = "m.login.msisdn",
Sso = "m.login.sso",
SsoUnstable = "org.matrix.login.sso",
Dummy = "m.login.dummy",
}
export interface IAuthDict {
// [key: string]: any;
type?: string;
// session?: string; // TODO
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/vector-im/element-web/issues/10312
user?: string;
identifier?: any;
password?: string;
response?: string;
// TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/vector-im/element-web/issues/10312
// See https://github.com/matrix-org/matrix-doc/issues/2220
// eslint-disable-next-line camelcase
threepid_creds?: any;
threepidCreds?: any;
}
class NoAuthFlowFoundError extends Error {
public name = "NoAuthFlowFoundError";
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
constructor(m: string, public readonly required_stages: string[], public readonly flows: IFlow[]) {
super(m);
}
}
interface IOpts {
matrixClient: MatrixClient;
authData?: IAuthData;
inputs?: IInputs;
sessionId?: string;
clientSecret?: string;
emailSid?: string;
doRequest(auth: IAuthData, background: boolean): Promise<IAuthData>;
stateUpdated(nextStage: AuthType, status: IStageStatus): void;
requestEmailToken(email: string, secret: string, attempt: number, session: string): Promise<{ sid: string }>;
busyChanged?(busy: boolean): void;
startAuthStage?(nextStage: string): Promise<void>; // LEGACY
}
/** /**
* Abstracts the logic used to drive the interactive auth process. * Abstracts the logic used to drive the interactive auth process.
* *
@@ -50,12 +130,12 @@ const MSISDN_STAGE_TYPE = "m.login.msisdn";
* called with the new auth dict to submit the request. Also passes a * called with the new auth dict to submit the request. Also passes a
* second deprecated arg which is a flag set to true if this request * second deprecated arg which is a flag set to true if this request
* is a background request. The busyChanged callback should be used * is a background request. The busyChanged callback should be used
* instead of the backfround flag. Should return a promise which resolves * instead of the background flag. Should return a promise which resolves
* to the successful response or rejects with a MatrixError. * to the successful response or rejects with a MatrixError.
* *
* @param {function(bool): Promise} opts.busyChanged * @param {function(boolean): Promise} opts.busyChanged
* called whenever the interactive auth logic becomes busy submitting * called whenever the interactive auth logic becomes busy submitting
* information provided by the user or finsihes. After this has been * information provided by the user or finishes. After this has been
* called with true the UI should indicate that a request is in progress * called with true the UI should indicate that a request is in progress
* until it is called again with false. * until it is called again with false.
* *
@@ -101,33 +181,41 @@ const MSISDN_STAGE_TYPE = "m.login.msisdn";
* attemptAuth promise. * attemptAuth promise.
* *
*/ */
export function InteractiveAuth(opts) { export class InteractiveAuth {
this._matrixClient = opts.matrixClient; private readonly matrixClient: MatrixClient;
this._data = opts.authData || {}; private readonly inputs: IInputs;
this._requestCallback = opts.doRequest; private readonly clientSecret: string;
this._busyChangedCallback = opts.busyChanged; private readonly requestCallback: IOpts["doRequest"];
// startAuthStage included for backwards compat private readonly busyChangedCallback?: IOpts["busyChanged"];
this._stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage; private readonly stateUpdatedCallback: IOpts["stateUpdated"];
this._resolveFunc = null; private readonly requestEmailTokenCallback: IOpts["requestEmailToken"];
this._rejectFunc = null;
this._inputs = opts.inputs || {};
this._requestEmailTokenCallback = opts.requestEmailToken;
if (opts.sessionId) this._data.session = opts.sessionId; private data: IAuthData;
this._clientSecret = opts.clientSecret || this._matrixClient.generateClientSecret(); private emailSid?: string;
this._emailSid = opts.emailSid; private requestingEmailToken = false;
if (this._emailSid === undefined) this._emailSid = null; private attemptAuthDeferred: IDeferred<IAuthData> = null;
this._requestingEmailToken = false; private chosenFlow: IFlow = null;
private currentStage: string = null;
this._chosenFlow = null;
this._currentStage = null;
// if we are currently trying to submit an auth dict (which includes polling) // if we are currently trying to submit an auth dict (which includes polling)
// the promise the will resolve/reject when it completes // the promise the will resolve/reject when it completes
this._submitPromise = null; private submitPromise: Promise<void> = null;
}
constructor(opts: IOpts) {
this.matrixClient = opts.matrixClient;
this.data = opts.authData || {};
this.requestCallback = opts.doRequest;
this.busyChangedCallback = opts.busyChanged;
// startAuthStage included for backwards compat
this.stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage;
this.requestEmailTokenCallback = opts.requestEmailToken;
this.inputs = opts.inputs || {};
if (opts.sessionId) this.data.session = opts.sessionId;
this.clientSecret = opts.clientSecret || this.matrixClient.generateClientSecret();
this.emailSid = opts.emailSid ?? null;
}
InteractiveAuth.prototype = {
/** /**
* begin the authentication process. * begin the authentication process.
* *
@@ -135,58 +223,57 @@ InteractiveAuth.prototype = {
* or rejects with the error on failure. Rejects with NoAuthFlowFoundError if * or rejects with the error on failure. Rejects with NoAuthFlowFoundError if
* no suitable authentication flow can be found * no suitable authentication flow can be found
*/ */
attemptAuth: function() { public attemptAuth(): Promise<IAuthData> {
// This promise will be quite long-lived and will resolve when the // This promise will be quite long-lived and will resolve when the
// request is authenticated and completes successfully. // request is authenticated and completes successfully.
return new Promise((resolve, reject) => { this.attemptAuthDeferred = defer();
this._resolveFunc = resolve; // pluck the promise out now, as doRequest may clear before we return
this._rejectFunc = reject; const promise = this.attemptAuthDeferred.promise;
const hasFlows = this._data && this._data.flows; // if we have no flows, try a request to acquire the flows
if (!this.data?.flows) {
// if we have no flows, try a request to acquire the flows this.busyChangedCallback?.(true);
if (!hasFlows) { // use the existing sessionId, if one is present.
if (this._busyChangedCallback) this._busyChangedCallback(true); let auth = null;
// use the existing sessionid, if one is present. if (this.data.session) {
let auth = null; auth = {
if (this._data.session) { session: this.data.session,
auth = { };
session: this._data.session,
};
}
this._doRequest(auth).finally(() => {
if (this._busyChangedCallback) this._busyChangedCallback(false);
});
} else {
this._startNextAuthStage();
} }
}); this.doRequest(auth).finally(() => {
}, this.busyChangedCallback?.(false);
});
} else {
this.startNextAuthStage();
}
return promise;
}
/** /**
* Poll to check if the auth session or current stage has been * Poll to check if the auth session or current stage has been
* completed out-of-band. If so, the attemptAuth promise will * completed out-of-band. If so, the attemptAuth promise will
* be resolved. * be resolved.
*/ */
poll: async function() { public async poll(): Promise<void> {
if (!this._data.session) return; if (!this.data.session) return;
// likewise don't poll if there is no auth session in progress // likewise don't poll if there is no auth session in progress
if (!this._resolveFunc) return; if (!this.attemptAuthDeferred) return;
// if we currently have a request in flight, there's no point making // if we currently have a request in flight, there's no point making
// another just to check what the status is // another just to check what the status is
if (this._submitPromise) return; if (this.submitPromise) return;
let authDict = {}; let authDict: IAuthDict = {};
if (this._currentStage == EMAIL_STAGE_TYPE) { if (this.currentStage == EMAIL_STAGE_TYPE) {
// The email can be validated out-of-band, but we need to provide the // The email can be validated out-of-band, but we need to provide the
// creds so the HS can go & check it. // creds so the HS can go & check it.
if (this._emailSid) { if (this.emailSid) {
const creds = { const creds: Record<string, string> = {
sid: this._emailSid, sid: this.emailSid,
client_secret: this._clientSecret, client_secret: this.clientSecret,
}; };
if (await this._matrixClient.doesServerRequireIdServerParam()) { if (await this.matrixClient.doesServerRequireIdServerParam()) {
const idServerParsedUrl = new URL(this._matrixClient.getIdentityServerUrl()); const idServerParsedUrl = new URL(this.matrixClient.getIdentityServerUrl());
creds.id_server = idServerParsedUrl.host; creds.id_server = idServerParsedUrl.host;
} }
authDict = { authDict = {
@@ -201,16 +288,16 @@ InteractiveAuth.prototype = {
} }
this.submitAuthDict(authDict, true); this.submitAuthDict(authDict, true);
}, }
/** /**
* get the auth session ID * get the auth session ID
* *
* @return {string} session id * @return {string} session id
*/ */
getSessionId: function() { public getSessionId(): string {
return this._data ? this._data.session : undefined; return this.data ? this.data.session : undefined;
}, }
/** /**
* get the client secret used for validation sessions * get the client secret used for validation sessions
@@ -218,9 +305,9 @@ InteractiveAuth.prototype = {
* *
* @return {string} client secret * @return {string} client secret
*/ */
getClientSecret: function() { public getClientSecret(): string {
return this._clientSecret; return this.clientSecret;
}, }
/** /**
* get the server params for a given stage * get the server params for a given stage
@@ -228,17 +315,13 @@ InteractiveAuth.prototype = {
* @param {string} loginType login type for the stage * @param {string} loginType login type for the stage
* @return {object?} any parameters from the server for this stage * @return {object?} any parameters from the server for this stage
*/ */
getStageParams: function(loginType) { public getStageParams(loginType: string): Record<string, any> {
let params = {}; return this.data.params?.[loginType];
if (this._data && this._data.params) { }
params = this._data.params;
}
return params[loginType];
},
getChosenFlow() { public getChosenFlow(): IFlow {
return this._chosenFlow; return this.chosenFlow;
}, }
/** /**
* submit a new auth dict and fire off the request. This will either * submit a new auth dict and fire off the request. This will either
@@ -246,38 +329,38 @@ InteractiveAuth.prototype = {
* to be called for a new stage. * to be called for a new stage.
* *
* @param {object} authData new auth dict to send to the server. Should * @param {object} authData new auth dict to send to the server. Should
* include a `type` propterty denoting the login type, as well as any * include a `type` property denoting the login type, as well as any
* other params for that stage. * other params for that stage.
* @param {bool} background If true, this request failing will not result * @param {boolean} background If true, this request failing will not result
* in the attemptAuth promise being rejected. This can be set to true * in the attemptAuth promise being rejected. This can be set to true
* for requests that just poll to see if auth has been completed elsewhere. * for requests that just poll to see if auth has been completed elsewhere.
*/ */
submitAuthDict: async function(authData, background) { public async submitAuthDict(authData: IAuthDict, background = false): Promise<void> {
if (!this._resolveFunc) { if (!this.attemptAuthDeferred) {
throw new Error("submitAuthDict() called before attemptAuth()"); throw new Error("submitAuthDict() called before attemptAuth()");
} }
if (!background && this._busyChangedCallback) { if (!background) {
this._busyChangedCallback(true); this.busyChangedCallback?.(true);
} }
// if we're currently trying a request, wait for it to finish // if we're currently trying a request, wait for it to finish
// as otherwise we can get multiple 200 responses which can mean // as otherwise we can get multiple 200 responses which can mean
// things like multiple logins for register requests. // things like multiple logins for register requests.
// (but discard any expections as we only care when its done, // (but discard any exceptions as we only care when its done,
// not whether it worked or not) // not whether it worked or not)
while (this._submitPromise) { while (this.submitPromise) {
try { try {
await this._submitPromise; await this.submitPromise;
} catch (e) { } catch (e) {
} }
} }
// use the sessionid from the last request, if one is present. // use the sessionid from the last request, if one is present.
let auth; let auth;
if (this._data.session) { if (this.data.session) {
auth = { auth = {
session: this._data.session, session: this.data.session,
}; };
utils.extend(auth, authData); utils.extend(auth, authData);
} else { } else {
@@ -287,15 +370,15 @@ InteractiveAuth.prototype = {
try { try {
// NB. the 'background' flag is deprecated by the busyChanged // NB. the 'background' flag is deprecated by the busyChanged
// callback and is here for backwards compat // callback and is here for backwards compat
this._submitPromise = this._doRequest(auth, background); this.submitPromise = this.doRequest(auth, background);
await this._submitPromise; await this.submitPromise;
} finally { } finally {
this._submitPromise = null; this.submitPromise = null;
if (!background && this._busyChangedCallback) { if (!background) {
this._busyChangedCallback(false); this.busyChangedCallback?.(false);
} }
} }
}, }
/** /**
* Gets the sid for the email validation session * Gets the sid for the email validation session
@@ -303,9 +386,9 @@ InteractiveAuth.prototype = {
* *
* @returns {string} The sid of the email auth session * @returns {string} The sid of the email auth session
*/ */
getEmailSid: function() { public getEmailSid(): string {
return this._emailSid; return this.emailSid;
}, }
/** /**
* Sets the sid for the email validation session * Sets the sid for the email validation session
@@ -315,9 +398,9 @@ InteractiveAuth.prototype = {
* *
* @param {string} sid The sid for the email validation session * @param {string} sid The sid for the email validation session
*/ */
setEmailSid: function(sid) { public setEmailSid(sid: string): void {
this._emailSid = sid; this.emailSid = sid;
}, }
/** /**
* Fire off a request, and either resolve the promise, or call * Fire off a request, and either resolve the promise, or call
@@ -325,33 +408,29 @@ InteractiveAuth.prototype = {
* *
* @private * @private
* @param {object?} auth new auth dict, including session id * @param {object?} auth new auth dict, including session id
* @param {bool?} background If true, this request is a background poll, so it * @param {boolean?} background If true, this request is a background poll, so it
* failing will not result in the attemptAuth promise being rejected. * failing will not result in the attemptAuth promise being rejected.
* This can be set to true for requests that just poll to see if auth has * This can be set to true for requests that just poll to see if auth has
* been completed elsewhere. * been completed elsewhere.
*/ */
_doRequest: async function(auth, background) { private async doRequest(auth: IAuthData, background = false): Promise<void> {
try { try {
const result = await this._requestCallback(auth, background); const result = await this.requestCallback(auth, background);
this._resolveFunc(result); this.attemptAuthDeferred.resolve(result);
this._resolveFunc = null; this.attemptAuthDeferred = null;
this._rejectFunc = null;
} catch (error) { } catch (error) {
// sometimes UI auth errors don't come with flows // sometimes UI auth errors don't come with flows
const errorFlows = error.data ? error.data.flows : null; const errorFlows = error.data?.flows ?? null;
const haveFlows = this._data.flows || Boolean(errorFlows); const haveFlows = this.data.flows || Boolean(errorFlows);
if (error.httpStatus !== 401 || !error.data || !haveFlows) { if (error.httpStatus !== 401 || !error.data || !haveFlows) {
// doesn't look like an interactive-auth failure. // doesn't look like an interactive-auth failure.
if (!background) { if (!background) {
this._rejectFunc(error); this.attemptAuthDeferred?.reject(error);
} else { } else {
// We ignore all failures here (even non-UI auth related ones) // We ignore all failures here (even non-UI auth related ones)
// since we don't want to suddenly fail if the internet connection // since we don't want to suddenly fail if the internet connection
// had a blip whilst we were polling // had a blip whilst we were polling
logger.log( logger.log("Background poll request failed doing UI auth: ignoring", error);
"Background poll request failed doing UI auth: ignoring",
error,
);
} }
} }
// if the error didn't come with flows, completed flows or session ID, // if the error didn't come with flows, completed flows or session ID,
@@ -360,37 +439,36 @@ InteractiveAuth.prototype = {
// has not yet been validated). This appears to be a Synapse bug, which // has not yet been validated). This appears to be a Synapse bug, which
// we workaround here. // we workaround here.
if (!error.data.flows && !error.data.completed && !error.data.session) { if (!error.data.flows && !error.data.completed && !error.data.session) {
error.data.flows = this._data.flows; error.data.flows = this.data.flows;
error.data.completed = this._data.completed; error.data.completed = this.data.completed;
error.data.session = this._data.session; error.data.session = this.data.session;
} }
this._data = error.data; this.data = error.data;
try { try {
this._startNextAuthStage(); this.startNextAuthStage();
} catch (e) { } catch (e) {
this._rejectFunc(e); this.attemptAuthDeferred.reject(e);
this._resolveFunc = null; this.attemptAuthDeferred = null;
this._rejectFunc = null;
} }
if ( if (
!this._emailSid && !this.emailSid &&
!this._requestingEmailToken && !this.requestingEmailToken &&
this._chosenFlow.stages.includes('m.login.email.identity') this.chosenFlow.stages.includes(AuthType.Email)
) { ) {
// If we've picked a flow with email auth, we send the email // If we've picked a flow with email auth, we send the email
// now because we want the request to fail as soon as possible // now because we want the request to fail as soon as possible
// if the email address is not valid (ie. already taken or not // if the email address is not valid (ie. already taken or not
// registered, depending on what the operation is). // registered, depending on what the operation is).
this._requestingEmailToken = true; this.requestingEmailToken = true;
try { try {
const requestTokenResult = await this._requestEmailTokenCallback( const requestTokenResult = await this.requestEmailTokenCallback(
this._inputs.emailAddress, this.inputs.emailAddress,
this._clientSecret, this.clientSecret,
1, // TODO: Multiple send attempts? 1, // TODO: Multiple send attempts?
this._data.session, this.data.session,
); );
this._emailSid = requestTokenResult.sid; this.emailSid = requestTokenResult.sid;
// NB. promise is not resolved here - at some point, doRequest // NB. promise is not resolved here - at some point, doRequest
// will be called again and if the user has jumped through all // will be called again and if the user has jumped through all
// the hoops correctly, auth will be complete and the request // the hoops correctly, auth will be complete and the request
@@ -404,15 +482,14 @@ InteractiveAuth.prototype = {
// to do) or it could be a network failure. Either way, pass // to do) or it could be a network failure. Either way, pass
// the failure up as the user can't complete auth if we can't // the failure up as the user can't complete auth if we can't
// send the email, for whatever reason. // send the email, for whatever reason.
this._rejectFunc(e); this.attemptAuthDeferred.reject(e);
this._resolveFunc = null; this.attemptAuthDeferred = null;
this._rejectFunc = null;
} finally { } finally {
this._requestingEmailToken = false; this.requestingEmailToken = false;
} }
} }
} }
}, }
/** /**
* Pick the next stage and call the callback * Pick the next stage and call the callback
@@ -420,34 +497,34 @@ InteractiveAuth.prototype = {
* @private * @private
* @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found
*/ */
_startNextAuthStage: function() { private startNextAuthStage(): void {
const nextStage = this._chooseStage(); const nextStage = this.chooseStage();
if (!nextStage) { if (!nextStage) {
throw new Error("No incomplete flows from the server"); throw new Error("No incomplete flows from the server");
} }
this._currentStage = nextStage; this.currentStage = nextStage;
if (nextStage === 'm.login.dummy') { if (nextStage === AuthType.Dummy) {
this.submitAuthDict({ this.submitAuthDict({
type: 'm.login.dummy', type: 'm.login.dummy',
}); });
return; return;
} }
if (this._data && this._data.errcode || this._data.error) { if (this.data && this.data.errcode || this.data.error) {
this._stateUpdatedCallback(nextStage, { this.stateUpdatedCallback(nextStage, {
errcode: this._data.errcode || "", errcode: this.data.errcode || "",
error: this._data.error || "", error: this.data.error || "",
}); });
return; return;
} }
const stageStatus = {}; const stageStatus: IStageStatus = {};
if (nextStage == EMAIL_STAGE_TYPE) { if (nextStage == EMAIL_STAGE_TYPE) {
stageStatus.emailSid = this._emailSid; stageStatus.emailSid = this.emailSid;
} }
this._stateUpdatedCallback(nextStage, stageStatus); this.stateUpdatedCallback(nextStage, stageStatus);
}, }
/** /**
* Pick the next auth stage * Pick the next auth stage
@@ -456,15 +533,15 @@ InteractiveAuth.prototype = {
* @return {string?} login type * @return {string?} login type
* @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found
*/ */
_chooseStage: function() { private chooseStage(): AuthType {
if (this._chosenFlow === null) { if (this.chosenFlow === null) {
this._chosenFlow = this._chooseFlow(); this.chosenFlow = this.chooseFlow();
} }
logger.log("Active flow => %s", JSON.stringify(this._chosenFlow)); logger.log("Active flow => %s", JSON.stringify(this.chosenFlow));
const nextStage = this._firstUncompletedStage(this._chosenFlow); const nextStage = this.firstUncompletedStage(this.chosenFlow);
logger.log("Next stage: %s", nextStage); logger.log("Next stage: %s", nextStage);
return nextStage; return nextStage;
}, }
/** /**
* Pick one of the flows from the returned list * Pick one of the flows from the returned list
@@ -472,7 +549,7 @@ InteractiveAuth.prototype = {
* be returned, otherwise, null will be returned. * be returned, otherwise, null will be returned.
* *
* Only flows using all given inputs are chosen because it * Only flows using all given inputs are chosen because it
* is likley to be surprising if the user provides a * is likely to be surprising if the user provides a
* credential and it is not used. For example, for registration, * credential and it is not used. For example, for registration,
* this could result in the email not being used which would leave * this could result in the email not being used which would leave
* the account with no means to reset a password. * the account with no means to reset a password.
@@ -481,14 +558,14 @@ InteractiveAuth.prototype = {
* @return {object} flow * @return {object} flow
* @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found
*/ */
_chooseFlow: function() { private chooseFlow(): IFlow {
const flows = this._data.flows || []; const flows = this.data.flows || [];
// we've been given an email or we've already done an email part // we've been given an email or we've already done an email part
const haveEmail = Boolean(this._inputs.emailAddress) || Boolean(this._emailSid); const haveEmail = Boolean(this.inputs.emailAddress) || Boolean(this.emailSid);
const haveMsisdn = ( const haveMsisdn = (
Boolean(this._inputs.phoneCountry) && Boolean(this.inputs.phoneCountry) &&
Boolean(this._inputs.phoneNumber) Boolean(this.inputs.phoneNumber)
); );
for (const flow of flows) { for (const flow of flows) {
@@ -506,16 +583,14 @@ InteractiveAuth.prototype = {
return flow; return flow;
} }
} }
const requiredStages: string[] = [];
if (haveEmail) requiredStages.push(EMAIL_STAGE_TYPE);
if (haveMsisdn) requiredStages.push(MSISDN_STAGE_TYPE);
// Throw an error with a fairly generic description, but with more // Throw an error with a fairly generic description, but with more
// information such that the app can give a better one if so desired. // information such that the app can give a better one if so desired.
const err = new Error("No appropriate authentication flow found"); throw new NoAuthFlowFoundError("No appropriate authentication flow found", requiredStages, flows);
err.name = 'NoAuthFlowFoundError'; }
err.required_stages = [];
if (haveEmail) err.required_stages.push(EMAIL_STAGE_TYPE);
if (haveMsisdn) err.required_stages.push(MSISDN_STAGE_TYPE);
err.available_flows = flows;
throw err;
},
/** /**
* Get the first uncompleted stage in the given flow * Get the first uncompleted stage in the given flow
@@ -524,14 +599,13 @@ InteractiveAuth.prototype = {
* @param {object} flow * @param {object} flow
* @return {string} login type * @return {string} login type
*/ */
_firstUncompletedStage: function(flow) { private firstUncompletedStage(flow: IFlow): AuthType {
const completed = (this._data || {}).completed || []; const completed = this.data.completed || [];
for (let i = 0; i < flow.stages.length; ++i) { for (let i = 0; i < flow.stages.length; ++i) {
const stageType = flow.stages[i]; const stageType = flow.stages[i];
if (completed.indexOf(stageType) === -1) { if (completed.indexOf(stageType) === -1) {
return stageType; return stageType;
} }
} }
}, }
}; }

View File

@@ -82,6 +82,19 @@ export class MSC3089Branch {
* @returns {Promise<{info: IEncryptedFile, httpUrl: string}>} Information about the file. * @returns {Promise<{info: IEncryptedFile, httpUrl: string}>} Information about the file.
*/ */
public async getFileInfo(): Promise<{ info: IEncryptedFile, httpUrl: string }> { public async getFileInfo(): Promise<{ info: IEncryptedFile, httpUrl: string }> {
const event = await this.getFileEvent();
const file = event.getContent()['file'];
const httpUrl = this.client.mxcUrlToHttp(file['url']);
return { info: file, httpUrl: httpUrl };
}
/**
* Gets the event the file points to.
* @returns {Promise<MatrixEvent>} Resolves to the file's event.
*/
public async getFileEvent(): Promise<MatrixEvent> {
const room = this.client.getRoom(this.roomId); const room = this.client.getRoom(this.roomId);
if (!room) throw new Error("Unknown room"); if (!room) throw new Error("Unknown room");
@@ -94,9 +107,6 @@ export class MSC3089Branch {
// Sometimes the event context doesn't decrypt for us, so do that. // Sometimes the event context doesn't decrypt for us, so do that.
await this.client.decryptEventIfNeeded(event, { emit: false, isRetry: false }); await this.client.decryptEventIfNeeded(event, { emit: false, isRetry: false });
const file = event.getContent()['file']; return event;
const httpUrl = this.client.mxcUrlToHttp(file['url']);
return { info: file, httpUrl: httpUrl };
} }
} }

View File

@@ -193,6 +193,28 @@ export class MSC3089TreeSpace {
await this.client.sendStateEvent(this.roomId, EventType.RoomPowerLevels, pls, ""); await this.client.sendStateEvent(this.roomId, EventType.RoomPowerLevels, pls, "");
} }
/**
* Gets the current permissions of a user. Note that any users missing explicit permissions (or not
* in the space) will be considered Viewers. Appropriate membership checks need to be performed
* elsewhere.
* @param {string} userId The user ID to check permissions of.
* @returns {TreePermissions} The permissions for the user, defaulting to Viewer.
*/
public getPermissions(userId: string): TreePermissions {
const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels");
const pls = currentPls.getContent() || {};
const viewLevel = pls['users_default'] || 0;
const editLevel = pls['events_default'] || 50;
const adminLevel = pls['events']?.[EventType.RoomPowerLevels] || 100;
const userLevel = pls['users']?.[userId] || viewLevel;
if (userLevel >= adminLevel) return TreePermissions.Owner;
if (userLevel >= editLevel) return TreePermissions.Editor;
return TreePermissions.Viewer;
}
/** /**
* Creates a directory under this tree space, represented as another tree space. * Creates a directory under this tree space, represented as another tree space.
* @param {string} name The name for the directory. * @param {string} name The name for the directory.

View File

@@ -50,13 +50,15 @@ export class EventTimeline {
* @param {boolean} toStartOfTimeline if true the event's forwardLooking flag is set false * @param {boolean} toStartOfTimeline if true the event's forwardLooking flag is set false
*/ */
static setEventMetadata(event: MatrixEvent, stateContext: RoomState, toStartOfTimeline: boolean): void { static setEventMetadata(event: MatrixEvent, stateContext: RoomState, toStartOfTimeline: boolean): void {
// We always check if the event doesn't already have the property. We do // When we try to generate a sentinel member before we have that member
// this to avoid overriding non-sentinel members by sentinel ones when // in the members object, we still generate a sentinel but it doesn't
// adding the event to a filtered timeline // have a membership event, so test to see if events.member is set. We
if (!event.sender) { // check this to avoid overriding non-sentinel members by sentinel ones
// when adding the event to a filtered timeline
if (!event.sender?.events?.member) {
event.sender = stateContext.getSentinelMember(event.getSender()); event.sender = stateContext.getSentinelMember(event.getSender());
} }
if (!event.target && event.getType() === EventType.RoomMember) { if (!event.target?.events?.member && event.getType() === EventType.RoomMember) {
event.target = stateContext.getSentinelMember(event.getStateKey()); event.target = stateContext.getSentinelMember(event.getStateKey());
} }

View File

@@ -29,6 +29,7 @@ import { Crypto } from "../crypto";
import { deepSortedObjectEntries } from "../utils"; import { deepSortedObjectEntries } from "../utils";
import { RoomMember } from "./room-member"; import { RoomMember } from "./room-member";
import { Thread } from "./thread"; import { Thread } from "./thread";
import { IActionsObject } from '../pushprocessor';
/** /**
* Enum for event statuses. * Enum for event statuses.
@@ -149,7 +150,7 @@ export interface IDecryptOptions {
} }
export class MatrixEvent extends EventEmitter { export class MatrixEvent extends EventEmitter {
private pushActions: object = null; private pushActions: IActionsObject = null;
private _replacingEvent: MatrixEvent = null; private _replacingEvent: MatrixEvent = null;
private _localRedactionEvent: MatrixEvent = null; private _localRedactionEvent: MatrixEvent = null;
private _isCancelled = false; private _isCancelled = false;
@@ -960,7 +961,7 @@ export class MatrixEvent extends EventEmitter {
* *
* @return {?Object} push actions * @return {?Object} push actions
*/ */
public getPushActions(): object | null { public getPushActions(): IActionsObject | null {
return this.pushActions; return this.pushActions;
} }
@@ -969,7 +970,7 @@ export class MatrixEvent extends EventEmitter {
* *
* @param {Object} pushActions push actions * @param {Object} pushActions push actions
*/ */
public setPushActions(pushActions: object): void { public setPushActions(pushActions: IActionsObject): void {
this.pushActions = pushActions; this.pushActions = pushActions;
} }
@@ -1247,10 +1248,15 @@ export class MatrixEvent extends EventEmitter {
} }
/** /**
* Summarise the event as JSON for debugging. If encrypted, include both the * Summarise the event as JSON. This is currently used by React SDK's view
* decrypted and encrypted view of the event. This is named `toJSON` for use * event source feature and Seshat's event indexing, so take care when
* with `JSON.stringify` which checks objects for functions named `toJSON` * adjusting the output here.
* and will call them to customise the output if they are defined. *
* If encrypted, include both the decrypted and encrypted view of the event.
*
* This is named `toJSON` for use with `JSON.stringify` which checks objects
* for functions named `toJSON` and will call them to customise the output
* if they are defined.
* *
* @return {Object} * @return {Object}
*/ */

View File

@@ -17,6 +17,7 @@ limitations under the License.
/** /**
* @module models/group * @module models/group
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
import * as utils from "../utils"; import * as utils from "../utils";
@@ -34,6 +35,7 @@ import { EventEmitter } from "events";
* @prop {Object} inviter Infomation about the user who invited the logged in user * @prop {Object} inviter Infomation about the user who invited the logged in user
* to the group, if myMembership is 'invite'. * to the group, if myMembership is 'invite'.
* @prop {string} inviter.userId The user ID of the inviter * @prop {string} inviter.userId The user ID of the inviter
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
export function Group(groupId) { export function Group(groupId) {
this.groupId = groupId; this.groupId = groupId;
@@ -76,6 +78,7 @@ Group.prototype.setInviter = function(inviter) {
* This means the 'name' and 'avatarUrl' properties. * This means the 'name' and 'avatarUrl' properties.
* @event module:client~MatrixClient#"Group.profile" * @event module:client~MatrixClient#"Group.profile"
* @param {Group} group The group whose profile was updated. * @param {Group} group The group whose profile was updated.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
* @example * @example
* matrixClient.on("Group.profile", function(group){ * matrixClient.on("Group.profile", function(group){
* var name = group.name; * var name = group.name;
@@ -87,6 +90,7 @@ Group.prototype.setInviter = function(inviter) {
* the group is updated. * the group is updated.
* @event module:client~MatrixClient#"Group.myMembership" * @event module:client~MatrixClient#"Group.myMembership"
* @param {Group} group The group in which the user's membership changed * @param {Group} group The group in which the user's membership changed
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
* @example * @example
* matrixClient.on("Group.myMembership", function(group){ * matrixClient.on("Group.myMembership", function(group){
* var myMembership = group.myMembership; * var myMembership = group.myMembership;

View File

@@ -204,9 +204,9 @@ export class RoomState extends EventEmitter {
* @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was * @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was
* <code>undefined</code>, else a single event (or null if no match found). * <code>undefined</code>, else a single event (or null if no match found).
*/ */
public getStateEvents(eventType: string): MatrixEvent[]; public getStateEvents(eventType: EventType | string): MatrixEvent[];
public getStateEvents(eventType: string, stateKey: string): MatrixEvent; public getStateEvents(eventType: EventType | string, stateKey: string): MatrixEvent;
public getStateEvents(eventType: string, stateKey?: string) { public getStateEvents(eventType: EventType | string, stateKey?: string) {
if (!this.events.has(eventType)) { if (!this.events.has(eventType)) {
// no match // no match
return stateKey === undefined ? [] : null; return stateKey === undefined ? [] : null;

View File

@@ -31,7 +31,7 @@ export class SearchResult {
* @return {SearchResult} * @return {SearchResult}
*/ */
static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult { public static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult {
const jsonContext = jsonObj.context || {} as IResultContext; const jsonContext = jsonObj.context || {} as IResultContext;
const eventsBefore = jsonContext.events_before || []; const eventsBefore = jsonContext.events_before || [];
const eventsAfter = jsonContext.events_after || []; const eventsAfter = jsonContext.events_after || [];
@@ -57,4 +57,3 @@ export class SearchResult {
*/ */
constructor(public readonly rank: number, public readonly context: EventContext) {} constructor(public readonly rank: number, public readonly context: EventContext) {}
} }

View File

@@ -1,6 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -17,12 +16,36 @@ limitations under the License.
import { escapeRegExp, globToRegexp, isNullOrUndefined } from "./utils"; import { escapeRegExp, globToRegexp, isNullOrUndefined } from "./utils";
import { logger } from './logger'; import { logger } from './logger';
import { MatrixClient } from "./client";
import { MatrixEvent } from "./models/event";
import {
ConditionKind,
IAnnotatedPushRule,
IContainsDisplayNameCondition,
IEventMatchCondition,
IPushRule,
IPushRules,
IRoomMemberCountCondition,
ISenderNotificationPermissionCondition,
PushRuleAction,
PushRuleActionName,
PushRuleCondition,
PushRuleKind,
PushRuleSet,
TweakName,
} from "./@types/PushRules";
/** /**
* @module pushprocessor * @module pushprocessor
*/ */
const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride']; const RULEKINDS_IN_ORDER = [
PushRuleKind.Override,
PushRuleKind.ContentSpecific,
PushRuleKind.RoomSpecific,
PushRuleKind.SenderSpecific,
PushRuleKind.Underride,
];
// The default override rules to apply to the push rules that arrive from the server. // The default override rules to apply to the push rules that arrive from the server.
// We do this for two reasons: // We do this for two reasons:
@@ -31,7 +54,7 @@ const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'
// more details. // more details.
// 2. We often want to start using push rules ahead of the server supporting them, // 2. We often want to start using push rules ahead of the server supporting them,
// and so we can put them here. // and so we can put them here.
const DEFAULT_OVERRIDE_RULES = [ const DEFAULT_OVERRIDE_RULES: IPushRule[] = [
{ {
// For homeservers which don't support MSC1930 yet // For homeservers which don't support MSC1930 yet
rule_id: ".m.rule.tombstone", rule_id: ".m.rule.tombstone",
@@ -39,20 +62,20 @@ const DEFAULT_OVERRIDE_RULES = [
enabled: true, enabled: true,
conditions: [ conditions: [
{ {
kind: "event_match", kind: ConditionKind.EventMatch,
key: "type", key: "type",
pattern: "m.room.tombstone", pattern: "m.room.tombstone",
}, },
{ {
kind: "event_match", kind: ConditionKind.EventMatch,
key: "state_key", key: "state_key",
pattern: "", pattern: "",
}, },
], ],
actions: [ actions: [
"notify", PushRuleActionName.Notify,
{ {
set_tweak: "highlight", set_tweak: TweakName.Highlight,
value: true, value: true,
}, },
], ],
@@ -64,31 +87,97 @@ const DEFAULT_OVERRIDE_RULES = [
enabled: true, enabled: true,
conditions: [ conditions: [
{ {
kind: "event_match", kind: ConditionKind.EventMatch,
key: "type", key: "type",
pattern: "m.reaction", pattern: "m.reaction",
}, },
], ],
actions: [ actions: [
"dont_notify", PushRuleActionName.DontNotify,
], ],
}, },
]; ];
/** export interface IActionsObject {
* Construct a Push Processor. notify: boolean;
* @constructor tweaks: Partial<Record<TweakName, any>>;
* @param {Object} client The Matrix client object to use }
*/
export function PushProcessor(client) {
const cachedGlobToRegex = {
// $glob: RegExp,
};
const matchingRuleFromKindSet = (ev, kindset) => { export class PushProcessor {
for (let ruleKindIndex = 0; /**
ruleKindIndex < RULEKINDS_IN_ORDER.length; * Construct a Push Processor.
++ruleKindIndex) { * @constructor
* @param {Object} client The Matrix client object to use
*/
constructor(private readonly client: MatrixClient) {}
/**
* Convert a list of actions into a object with the actions as keys and their values
* eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ]
* becomes { notify: true, tweaks: { sound: 'default' } }
* @param {array} actionList The actions list
*
* @return {object} A object with key 'notify' (true or false) and an object of actions
*/
public static actionListToActionsObject(actionList: PushRuleAction[]): IActionsObject {
const actionObj: IActionsObject = { notify: false, tweaks: {} };
for (let i = 0; i < actionList.length; ++i) {
const action = actionList[i];
if (action === PushRuleActionName.Notify) {
actionObj.notify = true;
} else if (typeof action === 'object') {
if (action.value === undefined) {
action.value = true;
}
actionObj.tweaks[action.set_tweak] = action.value;
}
}
return actionObj;
}
/**
* Rewrites conditions on a client's push rules to match the defaults
* where applicable. Useful for upgrading push rules to more strict
* conditions when the server is falling behind on defaults.
* @param {object} incomingRules The client's existing push rules
* @returns {object} The rewritten rules
*/
public static rewriteDefaultRules(incomingRules: IPushRules): IPushRules {
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
// properties missing in practice.
if (!newRules) newRules = {} as IPushRules;
if (!newRules.global) newRules.global = {} as PushRuleSet;
if (!newRules.global.override) newRules.global.override = [];
// Merge the client-level defaults with the ones from the server
const globalOverrides = newRules.global.override;
for (const override of DEFAULT_OVERRIDE_RULES) {
const existingRule = globalOverrides
.find((r) => r.rule_id === override.rule_id);
if (existingRule) {
// Copy over the actions, default, and conditions. Don't touch the user's
// preference.
existingRule.default = override.default;
existingRule.conditions = override.conditions;
existingRule.actions = override.actions;
} else {
// Add the rule
const ruleId = override.rule_id;
logger.warn(`Adding default global override for ${ruleId}`);
globalOverrides.push(override);
}
}
return newRules;
}
private static cachedGlobToRegex: Record<string, RegExp> = {}; // $glob: RegExp
private matchingRuleFromKindSet(ev: MatrixEvent, kindset: PushRuleSet): IAnnotatedPushRule {
for (let ruleKindIndex = 0; ruleKindIndex < RULEKINDS_IN_ORDER.length; ++ruleKindIndex) {
const kind = RULEKINDS_IN_ORDER[ruleKindIndex]; const kind = RULEKINDS_IN_ORDER[ruleKindIndex];
const ruleset = kindset[kind]; const ruleset = kindset[kind];
if (!ruleset) { if (!ruleset) {
@@ -101,89 +190,96 @@ export function PushProcessor(client) {
continue; continue;
} }
const rawrule = templateRuleToRaw(kind, rule); const rawrule = this.templateRuleToRaw(kind, rule);
if (!rawrule) { if (!rawrule) {
continue; continue;
} }
if (this.ruleMatchesEvent(rawrule, ev)) { if (this.ruleMatchesEvent(rawrule, ev)) {
rule.kind = kind; return {
return rule; ...rule,
kind,
};
} }
} }
} }
return null; return null;
}; }
const templateRuleToRaw = function(kind, tprule) { private templateRuleToRaw(kind: PushRuleKind, tprule: any): any {
const rawrule = { const rawrule = {
'rule_id': tprule.rule_id, 'rule_id': tprule.rule_id,
'actions': tprule.actions, 'actions': tprule.actions,
'conditions': [], 'conditions': [],
}; };
switch (kind) { switch (kind) {
case 'underride': case PushRuleKind.Underride:
case 'override': case PushRuleKind.Override:
rawrule.conditions = tprule.conditions; rawrule.conditions = tprule.conditions;
break; break;
case 'room': case PushRuleKind.RoomSpecific:
if (!tprule.rule_id) { if (!tprule.rule_id) {
return null; return null;
} }
rawrule.conditions.push({ rawrule.conditions.push({
'kind': 'event_match', 'kind': ConditionKind.EventMatch,
'key': 'room_id', 'key': 'room_id',
'value': tprule.rule_id, 'value': tprule.rule_id,
}); });
break; break;
case 'sender': case PushRuleKind.SenderSpecific:
if (!tprule.rule_id) { if (!tprule.rule_id) {
return null; return null;
} }
rawrule.conditions.push({ rawrule.conditions.push({
'kind': 'event_match', 'kind': ConditionKind.EventMatch,
'key': 'user_id', 'key': 'user_id',
'value': tprule.rule_id, 'value': tprule.rule_id,
}); });
break; break;
case 'content': case PushRuleKind.ContentSpecific:
if (!tprule.pattern) { if (!tprule.pattern) {
return null; return null;
} }
rawrule.conditions.push({ rawrule.conditions.push({
'kind': 'event_match', 'kind': ConditionKind.EventMatch,
'key': 'content.body', 'key': 'content.body',
'pattern': tprule.pattern, 'pattern': tprule.pattern,
}); });
break; break;
} }
return rawrule; return rawrule;
}; }
const eventFulfillsCondition = function(cond, ev) { private eventFulfillsCondition(cond: PushRuleCondition, ev: MatrixEvent): boolean {
const condition_functions = { switch (cond.kind) {
"event_match": eventFulfillsEventMatchCondition, case ConditionKind.EventMatch:
"contains_display_name": eventFulfillsDisplayNameCondition, return this.eventFulfillsEventMatchCondition(cond, ev);
"room_member_count": eventFulfillsRoomMemberCountCondition, case ConditionKind.ContainsDisplayName:
"sender_notification_permission": eventFulfillsSenderNotifPermCondition, return this.eventFulfillsDisplayNameCondition(cond, ev);
}; case ConditionKind.RoomMemberCount:
if (condition_functions[cond.kind]) { return this.eventFulfillsRoomMemberCountCondition(cond, ev);
return condition_functions[cond.kind](cond, ev); case ConditionKind.SenderNotificationPermission:
return this.eventFulfillsSenderNotifPermCondition(cond, ev);
} }
// unknown conditions: we previously matched all unknown conditions, // unknown conditions: we previously matched all unknown conditions,
// but given that rules can be added to the base rules on a server, // but given that rules can be added to the base rules on a server,
// it's probably better to not match unknown conditions. // it's probably better to not match unknown conditions.
return false; return false;
}; }
const eventFulfillsSenderNotifPermCondition = function(cond, ev) { private eventFulfillsSenderNotifPermCondition(
cond: ISenderNotificationPermissionCondition,
ev: MatrixEvent,
): boolean {
const notifLevelKey = cond['key']; const notifLevelKey = cond['key'];
if (!notifLevelKey) { if (!notifLevelKey) {
return false; return false;
} }
const room = client.getRoom(ev.getRoomId()); const room = this.client.getRoom(ev.getRoomId());
if (!room || !room.currentState) { if (!room?.currentState) {
return false; return false;
} }
@@ -191,14 +287,14 @@ export function PushProcessor(client) {
// the point the event is in the DAG. Unfortunately the js-sdk does not store // the point the event is in the DAG. Unfortunately the js-sdk does not store
// this. // this.
return room.currentState.mayTriggerNotifOfType(notifLevelKey, ev.getSender()); return room.currentState.mayTriggerNotifOfType(notifLevelKey, ev.getSender());
}; }
const eventFulfillsRoomMemberCountCondition = function(cond, ev) { private eventFulfillsRoomMemberCountCondition(cond: IRoomMemberCountCondition, ev: MatrixEvent): boolean {
if (!cond.is) { if (!cond.is) {
return false; return false;
} }
const room = client.getRoom(ev.getRoomId()); const room = this.client.getRoom(ev.getRoomId());
if (!room || !room.currentState || !room.currentState.members) { if (!room || !room.currentState || !room.currentState.members) {
return false; return false;
} }
@@ -229,9 +325,9 @@ export function PushProcessor(client) {
default: default:
return false; return false;
} }
}; }
const eventFulfillsDisplayNameCondition = function(cond, ev) { private eventFulfillsDisplayNameCondition(cond: IContainsDisplayNameCondition, ev: MatrixEvent): boolean {
let content = ev.getContent(); let content = ev.getContent();
if (ev.isEncrypted() && ev.getClearContent()) { if (ev.isEncrypted() && ev.getClearContent()) {
content = ev.getClearContent(); content = ev.getClearContent();
@@ -240,26 +336,26 @@ export function PushProcessor(client) {
return false; return false;
} }
const room = client.getRoom(ev.getRoomId()); const room = this.client.getRoom(ev.getRoomId());
if (!room || !room.currentState || !room.currentState.members || if (!room || !room.currentState || !room.currentState.members ||
!room.currentState.getMember(client.credentials.userId)) { !room.currentState.getMember(this.client.credentials.userId)) {
return false; return false;
} }
const displayName = room.currentState.getMember(client.credentials.userId).name; const displayName = room.currentState.getMember(this.client.credentials.userId).name;
// N.B. we can't use \b as it chokes on unicode. however \W seems to be okay // N.B. we can't use \b as it chokes on unicode. however \W seems to be okay
// as shorthand for [^0-9A-Za-z_]. // as shorthand for [^0-9A-Za-z_].
const pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i'); const pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i');
return content.body.search(pat) > -1; return content.body.search(pat) > -1;
}; }
const eventFulfillsEventMatchCondition = function(cond, ev) { private eventFulfillsEventMatchCondition(cond: IEventMatchCondition, ev: MatrixEvent): boolean {
if (!cond.key) { if (!cond.key) {
return false; return false;
} }
const val = valueForDottedKey(cond.key, ev); const val = this.valueForDottedKey(cond.key, ev);
if (typeof val !== 'string') { if (typeof val !== 'string') {
return false; return false;
} }
@@ -275,26 +371,26 @@ export function PushProcessor(client) {
let regex; let regex;
if (cond.key == 'content.body') { if (cond.key == 'content.body') {
regex = createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)'); regex = this.createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)');
} else { } else {
regex = createCachedRegex('^', cond.pattern, '$'); regex = this.createCachedRegex('^', cond.pattern, '$');
} }
return !!val.match(regex); return !!val.match(regex);
}; }
const createCachedRegex = function(prefix, glob, suffix) { private createCachedRegex(prefix: string, glob: string, suffix: string): RegExp {
if (cachedGlobToRegex[glob]) { if (PushProcessor.cachedGlobToRegex[glob]) {
return cachedGlobToRegex[glob]; return PushProcessor.cachedGlobToRegex[glob];
} }
cachedGlobToRegex[glob] = new RegExp( PushProcessor.cachedGlobToRegex[glob] = new RegExp(
prefix + globToRegexp(glob) + suffix, prefix + globToRegexp(glob) + suffix,
'i', // Case insensitive 'i', // Case insensitive
); );
return cachedGlobToRegex[glob]; return PushProcessor.cachedGlobToRegex[glob];
}; }
const valueForDottedKey = function(key, ev) { private valueForDottedKey(key: string, ev: MatrixEvent): any {
const parts = key.split('.'); const parts = key.split('.');
let val; let val;
@@ -319,23 +415,23 @@ export function PushProcessor(client) {
val = val[thisPart]; val = val[thisPart];
} }
return val; return val;
}; }
const matchingRuleForEventWithRulesets = function(ev, rulesets) { private matchingRuleForEventWithRulesets(ev: MatrixEvent, rulesets): IAnnotatedPushRule {
if (!rulesets) { if (!rulesets) {
return null; return null;
} }
if (ev.getSender() === client.credentials.userId) { if (ev.getSender() === this.client.credentials.userId) {
return null; return null;
} }
return matchingRuleFromKindSet(ev, rulesets.global); return this.matchingRuleFromKindSet(ev, rulesets.global);
}; }
const pushActionsForEventAndRulesets = function(ev, rulesets) { private pushActionsForEventAndRulesets(ev: MatrixEvent, rulesets): IActionsObject {
const rule = matchingRuleForEventWithRulesets(ev, rulesets); const rule = this.matchingRuleForEventWithRulesets(ev, rulesets);
if (!rule) { if (!rule) {
return {}; return {} as IActionsObject;
} }
const actionObj = PushProcessor.actionListToActionsObject(rule.actions); const actionObj = PushProcessor.actionListToActionsObject(rule.actions);
@@ -344,21 +440,22 @@ export function PushProcessor(client) {
if (actionObj.tweaks.highlight === undefined) { if (actionObj.tweaks.highlight === undefined) {
// if it isn't specified, highlight if it's a content // if it isn't specified, highlight if it's a content
// rule but otherwise not // rule but otherwise not
actionObj.tweaks.highlight = (rule.kind == 'content'); actionObj.tweaks.highlight = (rule.kind == PushRuleKind.ContentSpecific);
} }
return actionObj; return actionObj;
}; }
this.ruleMatchesEvent = function(rule, ev) { public ruleMatchesEvent(rule: IPushRule, ev: MatrixEvent): boolean {
let ret = true; let ret = true;
for (let i = 0; i < rule.conditions.length; ++i) { for (let i = 0; i < rule.conditions.length; ++i) {
const cond = rule.conditions[i]; const cond = rule.conditions[i];
ret &= eventFulfillsCondition(cond, ev); // @ts-ignore
ret &= this.eventFulfillsCondition(cond, ev);
} }
//console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match")); //console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match"));
return ret; return ret;
}; }
/** /**
* Get the user's push actions for the given event * Get the user's push actions for the given event
@@ -367,9 +464,9 @@ export function PushProcessor(client) {
* *
* @return {PushAction} * @return {PushAction}
*/ */
this.actionsForEvent = function(ev) { public actionsForEvent(ev: MatrixEvent): IActionsObject {
return pushActionsForEventAndRulesets(ev, client.pushRules); return this.pushActionsForEventAndRulesets(ev, this.client.pushRules);
}; }
/** /**
* Get one of the users push rules by its ID * Get one of the users push rules by its ID
@@ -377,85 +474,22 @@ export function PushProcessor(client) {
* @param {string} ruleId The ID of the rule to search for * @param {string} ruleId The ID of the rule to search for
* @return {object} The push rule, or null if no such rule was found * @return {object} The push rule, or null if no such rule was found
*/ */
this.getPushRuleById = function(ruleId) { public getPushRuleById(ruleId: string): IPushRule {
for (const scope of ['global']) { for (const scope of ['global']) {
if (client.pushRules[scope] === undefined) continue; if (this.client.pushRules[scope] === undefined) continue;
for (const kind of RULEKINDS_IN_ORDER) { for (const kind of RULEKINDS_IN_ORDER) {
if (client.pushRules[scope][kind] === undefined) continue; if (this.client.pushRules[scope][kind] === undefined) continue;
for (const rule of client.pushRules[scope][kind]) { for (const rule of this.client.pushRules[scope][kind]) {
if (rule.rule_id === ruleId) return rule; if (rule.rule_id === ruleId) return rule;
} }
} }
} }
return null; return null;
}; }
} }
/**
* Convert a list of actions into a object with the actions as keys and their values
* eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ]
* becomes { notify: true, tweaks: { sound: 'default' } }
* @param {array} actionlist The actions list
*
* @return {object} A object with key 'notify' (true or false) and an object of actions
*/
PushProcessor.actionListToActionsObject = function(actionlist) {
const actionobj = { 'notify': false, 'tweaks': {} };
for (let i = 0; i < actionlist.length; ++i) {
const action = actionlist[i];
if (action === 'notify') {
actionobj.notify = true;
} else if (typeof action === 'object') {
if (action.value === undefined) {
action.value = true;
}
actionobj.tweaks[action.set_tweak] = action.value;
}
}
return actionobj;
};
/**
* Rewrites conditions on a client's push rules to match the defaults
* where applicable. Useful for upgrading push rules to more strict
* conditions when the server is falling behind on defaults.
* @param {object} incomingRules The client's existing push rules
* @returns {object} The rewritten rules
*/
PushProcessor.rewriteDefaultRules = function(incomingRules) {
let newRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone
// These lines are mostly to make the tests happy. We shouldn't run into these
// properties missing in practice.
if (!newRules) newRules = {};
if (!newRules.global) newRules.global = {};
if (!newRules.global.override) newRules.global.override = [];
// Merge the client-level defaults with the ones from the server
const globalOverrides = newRules.global.override;
for (const override of DEFAULT_OVERRIDE_RULES) {
const existingRule = globalOverrides
.find((r) => r.rule_id === override.rule_id);
if (existingRule) {
// Copy over the actions, default, and conditions. Don't touch the user's
// preference.
existingRule.default = override.default;
existingRule.conditions = override.conditions;
existingRule.actions = override.actions;
} else {
// Add the rule
const ruleId = override.rule_id;
logger.warn(`Adding default global override for ${ruleId}`);
globalOverrides.push(override);
}
}
return newRules;
};
/** /**
* @typedef {Object} PushAction * @typedef {Object} PushAction
* @type {Object} * @type {Object}

View File

@@ -1,327 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* This is an internal module which manages queuing, scheduling and retrying
* of requests.
* @module scheduler
*/
import * as utils from "./utils";
import { logger } from './logger';
const DEBUG = false; // set true to enable console logging.
/**
* Construct a scheduler for Matrix. Requires
* {@link module:scheduler~MatrixScheduler#setProcessFunction} to be provided
* with a way of processing events.
* @constructor
* @param {module:scheduler~retryAlgorithm} retryAlgorithm Optional. The retry
* algorithm to apply when determining when to try to send an event again.
* Defaults to {@link module:scheduler~MatrixScheduler.RETRY_BACKOFF_RATELIMIT}.
* @param {module:scheduler~queueAlgorithm} queueAlgorithm Optional. The queuing
* algorithm to apply when determining which events should be sent before the
* given event. Defaults to {@link module:scheduler~MatrixScheduler.QUEUE_MESSAGES}.
*/
export function MatrixScheduler(retryAlgorithm, queueAlgorithm) {
this.retryAlgorithm = retryAlgorithm || MatrixScheduler.RETRY_BACKOFF_RATELIMIT;
this.queueAlgorithm = queueAlgorithm || MatrixScheduler.QUEUE_MESSAGES;
this._queues = {
// queueName: [{
// event: MatrixEvent, // event to send
// defer: Deferred, // defer to resolve/reject at the END of the retries
// attempts: Number // number of times we've called processFn
// }, ...]
};
this._activeQueues = [];
this._procFn = null;
}
/**
* Retrieve a queue based on an event. The event provided does not need to be in
* the queue.
* @param {MatrixEvent} event An event to get the queue for.
* @return {?Array<MatrixEvent>} A shallow copy of events in the queue or null.
* Modifying this array will not modify the list itself. Modifying events in
* this array <i>will</i> modify the underlying event in the queue.
* @see MatrixScheduler.removeEventFromQueue To remove an event from the queue.
*/
MatrixScheduler.prototype.getQueueForEvent = function(event) {
const name = this.queueAlgorithm(event);
if (!name || !this._queues[name]) {
return null;
}
return this._queues[name].map(function(obj) {
return obj.event;
});
};
/**
* Remove this event from the queue. The event is equal to another event if they
* have the same ID returned from event.getId().
* @param {MatrixEvent} event The event to remove.
* @return {boolean} True if this event was removed.
*/
MatrixScheduler.prototype.removeEventFromQueue = function(event) {
const name = this.queueAlgorithm(event);
if (!name || !this._queues[name]) {
return false;
}
let removed = false;
utils.removeElement(this._queues[name], function(element) {
if (element.event.getId() === event.getId()) {
// XXX we should probably reject the promise?
// https://github.com/matrix-org/matrix-js-sdk/issues/496
removed = true;
return true;
}
});
return removed;
};
/**
* Set the process function. Required for events in the queue to be processed.
* If set after events have been added to the queue, this will immediately start
* processing them.
* @param {module:scheduler~processFn} fn The function that can process events
* in the queue.
*/
MatrixScheduler.prototype.setProcessFunction = function(fn) {
this._procFn = fn;
_startProcessingQueues(this);
};
/**
* Queue an event if it is required and start processing queues.
* @param {MatrixEvent} event The event that may be queued.
* @return {?Promise} A promise if the event was queued, which will be
* resolved or rejected in due time, else null.
*/
MatrixScheduler.prototype.queueEvent = function(event) {
const queueName = this.queueAlgorithm(event);
if (!queueName) {
return null;
}
// add the event to the queue and make a deferred for it.
if (!this._queues[queueName]) {
this._queues[queueName] = [];
}
const defer = utils.defer();
this._queues[queueName].push({
event: event,
defer: defer,
attempts: 0,
});
debuglog(
"Queue algorithm dumped event %s into queue '%s'",
event.getId(), queueName,
);
_startProcessingQueues(this);
return defer.promise;
};
/**
* Retries events up to 4 times using exponential backoff. This produces wait
* times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the
* failure was due to a rate limited request, the time specified in the error is
* waited before being retried.
* @param {MatrixEvent} event
* @param {Number} attempts
* @param {MatrixError} err
* @return {Number}
* @see module:scheduler~retryAlgorithm
*/
MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) {
// client error; no amount of retrying with save you now.
return -1;
}
// we ship with browser-request which returns { cors: rejected } when trying
// with no connection, so if we match that, give up since they have no conn.
if (err.cors === "rejected") {
return -1;
}
// if event that we are trying to send is too large in any way then retrying won't help
if (err.name === "M_TOO_LARGE") {
return -1;
}
if (err.name === "M_LIMIT_EXCEEDED") {
const waitTime = err.data.retry_after_ms;
if (waitTime > 0) {
return waitTime;
}
}
if (attempts > 4) {
return -1; // give up
}
return (1000 * Math.pow(2, attempts));
};
/**
* Queues <code>m.room.message</code> events and lets other events continue
* concurrently.
* @param {MatrixEvent} event
* @return {string}
* @see module:scheduler~queueAlgorithm
*/
MatrixScheduler.QUEUE_MESSAGES = function(event) {
// enqueue messages or events that associate with another event (redactions and relations)
if (event.getType() === "m.room.message" || event.hasAssocation()) {
// put these events in the 'message' queue.
return "message";
}
// allow all other events continue concurrently.
return null;
};
function _startProcessingQueues(scheduler) {
if (!scheduler._procFn) {
return;
}
// for each inactive queue with events in them
Object.keys(scheduler._queues)
.filter(function(queueName) {
return scheduler._activeQueues.indexOf(queueName) === -1 &&
scheduler._queues[queueName].length > 0;
})
.forEach(function(queueName) {
// mark the queue as active
scheduler._activeQueues.push(queueName);
// begin processing the head of the queue
debuglog("Spinning up queue: '%s'", queueName);
_processQueue(scheduler, queueName);
});
}
function _processQueue(scheduler, queueName) {
// get head of queue
const obj = _peekNextEvent(scheduler, queueName);
if (!obj) {
// queue is empty. Mark as inactive and stop recursing.
const index = scheduler._activeQueues.indexOf(queueName);
if (index >= 0) {
scheduler._activeQueues.splice(index, 1);
}
debuglog("Stopping queue '%s' as it is now empty", queueName);
return;
}
debuglog(
"Queue '%s' has %s pending events",
queueName, scheduler._queues[queueName].length,
);
// fire the process function and if it resolves, resolve the deferred. Else
// invoke the retry algorithm.
// First wait for a resolved promise, so the resolve handlers for
// the deferred of the previously sent event can run.
// This way enqueued relations/redactions to enqueued events can receive
// the remove id of their target before being sent.
Promise.resolve().then(() => {
return scheduler._procFn(obj.event);
}).then(function(res) {
// remove this from the queue
_removeNextEvent(scheduler, queueName);
debuglog("Queue '%s' sent event %s", queueName, obj.event.getId());
obj.defer.resolve(res);
// keep processing
_processQueue(scheduler, queueName);
}, function(err) {
obj.attempts += 1;
// ask the retry algorithm when/if we should try again
const waitTimeMs = scheduler.retryAlgorithm(obj.event, obj.attempts, err);
debuglog(
"retry(%s) err=%s event_id=%s waitTime=%s",
obj.attempts, err, obj.event.getId(), waitTimeMs,
);
if (waitTimeMs === -1) { // give up (you quitter!)
debuglog(
"Queue '%s' giving up on event %s", queueName, obj.event.getId(),
);
// remove this from the queue
_removeNextEvent(scheduler, queueName);
obj.defer.reject(err);
// process next event
_processQueue(scheduler, queueName);
} else {
setTimeout(function() {
_processQueue(scheduler, queueName);
}, waitTimeMs);
}
});
}
function _peekNextEvent(scheduler, queueName) {
const queue = scheduler._queues[queueName];
if (!Array.isArray(queue)) {
return null;
}
return queue[0];
}
function _removeNextEvent(scheduler, queueName) {
const queue = scheduler._queues[queueName];
if (!Array.isArray(queue)) {
return null;
}
return queue.shift();
}
function debuglog() {
if (DEBUG) {
logger.log(...arguments);
}
}
/**
* The retry algorithm to apply when retrying events. To stop retrying, return
* <code>-1</code>. If this event was part of a queue, it will be removed from
* the queue.
* @callback retryAlgorithm
* @param {MatrixEvent} event The event being retried.
* @param {Number} attempts The number of failed attempts. This will always be
* >= 1.
* @param {MatrixError} err The most recent error message received when trying
* to send this event.
* @return {Number} The number of milliseconds to wait before trying again. If
* this is 0, the request will be immediately retried. If this is
* <code>-1</code>, the event will be marked as
* {@link module:models/event.EventStatus.NOT_SENT} and will not be retried.
*/
/**
* The queuing algorithm to apply to events. This function must be idempotent as
* it may be called multiple times with the same event. All queues created are
* serviced in a FIFO manner. To send the event ASAP, return <code>null</code>
* which will not put this event in a queue. Events that fail to send that form
* part of a queue will be removed from the queue and the next event in the
* queue will be sent.
* @callback queueAlgorithm
* @param {MatrixEvent} event The event to be sent.
* @return {string} The name of the queue to put the event into. If a queue with
* this name does not exist, it will be created. If this is <code>null</code>,
* the event is not put into a queue and will be sent concurrently.
*/
/**
* The function to invoke to process (send) events in the queue.
* @callback processFn
* @param {MatrixEvent} event The event to send.
* @return {Promise} Resolved/rejected depending on the outcome of the request.
*/

329
src/scheduler.ts Normal file
View File

@@ -0,0 +1,329 @@
/*
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* This is an internal module which manages queuing, scheduling and retrying
* of requests.
* @module scheduler
*/
import * as utils from "./utils";
import { logger } from './logger';
import { MatrixEvent } from "./models/event";
import { EventType } from "./@types/event";
import { IDeferred } from "./utils";
import { MatrixError } from "./http-api";
import { ISendEventResponse } from "./@types/requests";
const DEBUG = false; // set true to enable console logging.
interface IQueueEntry<T> {
event: MatrixEvent;
defer: IDeferred<T>;
attempts: number;
}
type ProcessFunction<T> = (event: MatrixEvent) => Promise<T>;
/**
* Construct a scheduler for Matrix. Requires
* {@link module:scheduler~MatrixScheduler#setProcessFunction} to be provided
* with a way of processing events.
* @constructor
* @param {module:scheduler~retryAlgorithm} retryAlgorithm Optional. The retry
* algorithm to apply when determining when to try to send an event again.
* Defaults to {@link module:scheduler~MatrixScheduler.RETRY_BACKOFF_RATELIMIT}.
* @param {module:scheduler~queueAlgorithm} queueAlgorithm Optional. The queuing
* algorithm to apply when determining which events should be sent before the
* given event. Defaults to {@link module:scheduler~MatrixScheduler.QUEUE_MESSAGES}.
*/
// eslint-disable-next-line camelcase
export class MatrixScheduler<T = ISendEventResponse> {
/**
* Retries events up to 4 times using exponential backoff. This produces wait
* times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the
* failure was due to a rate limited request, the time specified in the error is
* waited before being retried.
* @param {MatrixEvent} event
* @param {Number} attempts
* @param {MatrixError} err
* @return {Number}
* @see module:scheduler~retryAlgorithm
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
public static RETRY_BACKOFF_RATELIMIT(event: MatrixEvent, attempts: number, err: MatrixError): number {
if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) {
// client error; no amount of retrying with save you now.
return -1;
}
// we ship with browser-request which returns { cors: rejected } when trying
// with no connection, so if we match that, give up since they have no conn.
if (err.cors === "rejected") {
return -1;
}
// if event that we are trying to send is too large in any way then retrying won't help
if (err.name === "M_TOO_LARGE") {
return -1;
}
if (err.name === "M_LIMIT_EXCEEDED") {
const waitTime = err.data.retry_after_ms;
if (waitTime > 0) {
return waitTime;
}
}
if (attempts > 4) {
return -1; // give up
}
return (1000 * Math.pow(2, attempts));
}
/**
* Queues <code>m.room.message</code> events and lets other events continue
* concurrently.
* @param {MatrixEvent} event
* @return {string}
* @see module:scheduler~queueAlgorithm
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
public static QUEUE_MESSAGES(event: MatrixEvent) {
// enqueue messages or events that associate with another event (redactions and relations)
if (event.getType() === EventType.RoomMessage || event.hasAssocation()) {
// put these events in the 'message' queue.
return "message";
}
// allow all other events continue concurrently.
return null;
}
// queueName: [{
// event: MatrixEvent, // event to send
// defer: Deferred, // defer to resolve/reject at the END of the retries
// attempts: Number // number of times we've called processFn
// }, ...]
private readonly queues: Record<string, IQueueEntry<T>[]> = {};
private activeQueues: string[] = [];
private procFn: ProcessFunction<T> = null;
constructor(
public readonly retryAlgorithm = MatrixScheduler.RETRY_BACKOFF_RATELIMIT,
public readonly queueAlgorithm = MatrixScheduler.QUEUE_MESSAGES,
) {}
/**
* Retrieve a queue based on an event. The event provided does not need to be in
* the queue.
* @param {MatrixEvent} event An event to get the queue for.
* @return {?Array<MatrixEvent>} A shallow copy of events in the queue or null.
* Modifying this array will not modify the list itself. Modifying events in
* this array <i>will</i> modify the underlying event in the queue.
* @see MatrixScheduler.removeEventFromQueue To remove an event from the queue.
*/
public getQueueForEvent(event: MatrixEvent): MatrixEvent[] {
const name = this.queueAlgorithm(event);
if (!name || !this.queues[name]) {
return null;
}
return this.queues[name].map(function(obj) {
return obj.event;
});
}
/**
* Remove this event from the queue. The event is equal to another event if they
* have the same ID returned from event.getId().
* @param {MatrixEvent} event The event to remove.
* @return {boolean} True if this event was removed.
*/
public removeEventFromQueue(event: MatrixEvent): boolean {
const name = this.queueAlgorithm(event);
if (!name || !this.queues[name]) {
return false;
}
let removed = false;
utils.removeElement(this.queues[name], (element) => {
if (element.event.getId() === event.getId()) {
// XXX we should probably reject the promise?
// https://github.com/matrix-org/matrix-js-sdk/issues/496
removed = true;
return true;
}
});
return removed;
}
/**
* Set the process function. Required for events in the queue to be processed.
* If set after events have been added to the queue, this will immediately start
* processing them.
* @param {module:scheduler~processFn} fn The function that can process events
* in the queue.
*/
public setProcessFunction(fn: ProcessFunction<T>): void {
this.procFn = fn;
this.startProcessingQueues();
}
/**
* Queue an event if it is required and start processing queues.
* @param {MatrixEvent} event The event that may be queued.
* @return {?Promise} A promise if the event was queued, which will be
* resolved or rejected in due time, else null.
*/
public queueEvent(event: MatrixEvent): Promise<T> | null {
const queueName = this.queueAlgorithm(event);
if (!queueName) {
return null;
}
// add the event to the queue and make a deferred for it.
if (!this.queues[queueName]) {
this.queues[queueName] = [];
}
const defer = utils.defer<T>();
this.queues[queueName].push({
event: event,
defer: defer,
attempts: 0,
});
debuglog("Queue algorithm dumped event %s into queue '%s'", event.getId(), queueName);
this.startProcessingQueues();
return defer.promise;
}
private startProcessingQueues(): void {
if (!this.procFn) return;
// for each inactive queue with events in them
Object.keys(this.queues)
.filter((queueName) => {
return this.activeQueues.indexOf(queueName) === -1 &&
this.queues[queueName].length > 0;
})
.forEach((queueName) => {
// mark the queue as active
this.activeQueues.push(queueName);
// begin processing the head of the queue
debuglog("Spinning up queue: '%s'", queueName);
this.processQueue(queueName);
});
}
private processQueue = (queueName: string): void => {
// get head of queue
const obj = this.peekNextEvent(queueName);
if (!obj) {
// queue is empty. Mark as inactive and stop recursing.
const index = this.activeQueues.indexOf(queueName);
if (index >= 0) {
this.activeQueues.splice(index, 1);
}
debuglog("Stopping queue '%s' as it is now empty", queueName);
return;
}
debuglog("Queue '%s' has %s pending events", queueName, this.queues[queueName].length);
// fire the process function and if it resolves, resolve the deferred. Else
// invoke the retry algorithm.
// First wait for a resolved promise, so the resolve handlers for
// the deferred of the previously sent event can run.
// This way enqueued relations/redactions to enqueued events can receive
// the remove id of their target before being sent.
Promise.resolve().then(() => {
return this.procFn(obj.event);
}).then((res) => {
// remove this from the queue
this.removeNextEvent(queueName);
debuglog("Queue '%s' sent event %s", queueName, obj.event.getId());
obj.defer.resolve(res);
// keep processing
this.processQueue(queueName);
}, (err) => {
obj.attempts += 1;
// ask the retry algorithm when/if we should try again
const waitTimeMs = this.retryAlgorithm(obj.event, obj.attempts, err);
debuglog("retry(%s) err=%s event_id=%s waitTime=%s", obj.attempts, err, obj.event.getId(), waitTimeMs);
if (waitTimeMs === -1) { // give up (you quitter!)
debuglog("Queue '%s' giving up on event %s", queueName, obj.event.getId());
// remove this from the queue
this.removeNextEvent(queueName);
obj.defer.reject(err);
// process next event
this.processQueue(queueName);
} else {
setTimeout(this.processQueue, waitTimeMs, queueName);
}
});
};
private peekNextEvent(queueName: string): IQueueEntry<T> {
const queue = this.queues[queueName];
if (!Array.isArray(queue)) {
return null;
}
return queue[0];
}
private removeNextEvent(queueName: string): IQueueEntry<T> {
const queue = this.queues[queueName];
if (!Array.isArray(queue)) {
return null;
}
return queue.shift();
}
}
function debuglog(...args) {
if (DEBUG) {
logger.log(...args);
}
}
/**
* The retry algorithm to apply when retrying events. To stop retrying, return
* <code>-1</code>. If this event was part of a queue, it will be removed from
* the queue.
* @callback retryAlgorithm
* @param {MatrixEvent} event The event being retried.
* @param {Number} attempts The number of failed attempts. This will always be
* >= 1.
* @param {MatrixError} err The most recent error message received when trying
* to send this event.
* @return {Number} The number of milliseconds to wait before trying again. If
* this is 0, the request will be immediately retried. If this is
* <code>-1</code>, the event will be marked as
* {@link module:models/event.EventStatus.NOT_SENT} and will not be retried.
*/
/**
* The queuing algorithm to apply to events. This function must be idempotent as
* it may be called multiple times with the same event. All queues created are
* serviced in a FIFO manner. To send the event ASAP, return <code>null</code>
* which will not put this event in a queue. Events that fail to send that form
* part of a queue will be removed from the queue and the next event in the
* queue will be sent.
* @callback queueAlgorithm
* @param {MatrixEvent} event The event to be sent.
* @return {string} The name of the queue to put the event into. If a queue with
* this name does not exist, it will be created. If this is <code>null</code>,
* the event is not put into a queue and will be sent concurrently.
*/
/**
* The function to invoke to process (send) events in the queue.
* @callback processFn
* @param {MatrixEvent} event The event to send.
* @return {Promise} Resolved/rejected depending on the outcome of the request.
*/

View File

@@ -56,6 +56,7 @@ export interface IStore {
/** /**
* No-op. * No-op.
* @param {Group} group * @param {Group} group
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
storeGroup(group: Group); storeGroup(group: Group);
@@ -63,12 +64,14 @@ export interface IStore {
* No-op. * No-op.
* @param {string} groupId * @param {string} groupId
* @return {null} * @return {null}
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
getGroup(groupId: string): Group | null; getGroup(groupId: string): Group | null;
/** /**
* No-op. * No-op.
* @return {Array} An empty array. * @return {Array} An empty array.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
getGroups(): Group[]; getGroups(): Group[];

View File

@@ -93,6 +93,7 @@ export class MemoryStore implements IStore {
/** /**
* Store the given room. * Store the given room.
* @param {Group} group The group to be stored * @param {Group} group The group to be stored
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public storeGroup(group: Group) { public storeGroup(group: Group) {
this.groups[group.groupId] = group; this.groups[group.groupId] = group;
@@ -102,6 +103,7 @@ export class MemoryStore implements IStore {
* Retrieve a group by its group ID. * Retrieve a group by its group ID.
* @param {string} groupId The group ID. * @param {string} groupId The group ID.
* @return {Group} The group or null. * @return {Group} The group or null.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public getGroup(groupId: string): Group | null { public getGroup(groupId: string): Group | null {
return this.groups[groupId] || null; return this.groups[groupId] || null;
@@ -110,6 +112,7 @@ export class MemoryStore implements IStore {
/** /**
* Retrieve all known groups. * Retrieve all known groups.
* @return {Group[]} A list of groups, which may be empty. * @return {Group[]} A list of groups, which may be empty.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public getGroups(): Group[] { public getGroups(): Group[] {
return Object.values(this.groups); return Object.values(this.groups);

View File

@@ -61,6 +61,7 @@ export class StubStore implements IStore {
/** /**
* No-op. * No-op.
* @param {Group} group * @param {Group} group
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public storeGroup(group: Group) {} public storeGroup(group: Group) {}
@@ -68,6 +69,7 @@ export class StubStore implements IStore {
* No-op. * No-op.
* @param {string} groupId * @param {string} groupId
* @return {null} * @return {null}
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public getGroup(groupId: string): Group | null { public getGroup(groupId: string): Group | null {
return null; return null;
@@ -76,6 +78,7 @@ export class StubStore implements IStore {
/** /**
* No-op. * No-op.
* @return {Array} An empty array. * @return {Array} An empty array.
* @deprecated groups/communities never made it to the spec and support for them is being discontinued.
*/ */
public getGroups(): Group[] { public getGroups(): Group[] {
return []; return [];

View File

@@ -53,6 +53,8 @@ import { MatrixEvent } from "./models/event";
import { MatrixError } from "./http-api"; import { MatrixError } from "./http-api";
import { ISavedSync } from "./store"; import { ISavedSync } from "./store";
import { Thread } from "./models/thread"; import { Thread } from "./models/thread";
import { EventType } from "./@types/event";
import { IPushRules } from "./@types/PushRules";
const DEBUG = true; const DEBUG = true;
@@ -1075,8 +1077,8 @@ export class SyncApi {
// honour push rules that were previously cached. Base rules // honour push rules that were previously cached. Base rules
// will be updated when we receive push rules via getPushRules // will be updated when we receive push rules via getPushRules
// (see sync) before syncing over the network. // (see sync) before syncing over the network.
if (accountDataEvent.getType() === 'm.push_rules') { if (accountDataEvent.getType() === EventType.PushRules) {
const rules = accountDataEvent.getContent(); const rules = accountDataEvent.getContent<IPushRules>();
client.pushRules = PushProcessor.rewriteDefaultRules(rules); client.pushRules = PushProcessor.rewriteDefaultRules(rules);
} }
const prevEvent = prevEventsMap[accountDataEvent.getId()]; const prevEvent = prevEventsMap[accountDataEvent.getId()];

View File

@@ -412,7 +412,7 @@ export function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
} }
export function globToRegexp(glob: string, extended: any): string { export function globToRegexp(glob: string, extended?: any): string {
extended = typeof(extended) === 'boolean' ? extended : true; extended = typeof(extended) === 'boolean' ? extended : true;
// From // From
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132 // https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
@@ -457,7 +457,7 @@ export interface IDeferred<T> {
} }
// Returns a Deferred // Returns a Deferred
export function defer<T>(): IDeferred<T> { export function defer<T = void>(): IDeferred<T> {
let resolve; let resolve;
let reject; let reject;

View File

@@ -535,6 +535,7 @@ export class MatrixCall extends EventEmitter {
this.emit(CallEvent.FeedsChanged, this.feeds); this.emit(CallEvent.FeedsChanged, this.feeds);
} }
// TODO: Find out what is going on here
// why do we enable audio (and only audio) tracks here? -- matthew // why do we enable audio (and only audio) tracks here? -- matthew
setTracksEnabled(stream.getAudioTracks(), true); setTracksEnabled(stream.getAudioTracks(), true);
@@ -708,8 +709,6 @@ export class MatrixCall extends EventEmitter {
this.getUserMediaFailed(e); this.getUserMediaFailed(e);
return; return;
} }
} else if (this.localUsermediaStream) {
this.gotUserMediaForAnswer(this.localUsermediaStream);
} else if (this.waitForLocalAVStream) { } else if (this.waitForLocalAVStream) {
this.setState(CallState.WaitLocalMedia); this.setState(CallState.WaitLocalMedia);
} }
@@ -721,14 +720,10 @@ export class MatrixCall extends EventEmitter {
* @param {MatrixCall} newCall The new call. * @param {MatrixCall} newCall The new call.
*/ */
replacedBy(newCall: MatrixCall) { replacedBy(newCall: MatrixCall) {
logger.debug(this.callId + " being replaced by " + newCall.callId);
if (this.state === CallState.WaitLocalMedia) { if (this.state === CallState.WaitLocalMedia) {
logger.debug("Telling new call to wait for local media"); logger.debug("Telling new call to wait for local media");
newCall.waitForLocalAVStream = true; newCall.waitForLocalAVStream = true;
} else if (this.state === CallState.CreateOffer) { } else if ([CallState.CreateOffer, CallState.InviteSent].includes(this.state)) {
logger.debug("Handing local stream to new call");
newCall.gotUserMediaForAnswer(this.localUsermediaStream);
} else if (this.state === CallState.InviteSent) {
logger.debug("Handing local stream to new call"); logger.debug("Handing local stream to new call");
newCall.gotUserMediaForAnswer(this.localUsermediaStream); newCall.gotUserMediaForAnswer(this.localUsermediaStream);
} }
@@ -750,9 +745,10 @@ export class MatrixCall extends EventEmitter {
// We don't want to send hangup here if we didn't even get to sending an invite // We don't want to send hangup here if we didn't even get to sending an invite
if (this.state === CallState.WaitLocalMedia) return; if (this.state === CallState.WaitLocalMedia) return;
const content = {}; const content = {};
// Continue to send no reason for user hangups temporarily, until // Don't send UserHangup reason to older clients
// clients understand the user_hangup reason (voip v1) if ((this.opponentVersion && this.opponentVersion >= 1) || reason !== CallErrorCode.UserHangup) {
if (reason !== CallErrorCode.UserHangup) content['reason'] = reason; content["reason"] = reason;
}
this.sendVoipEvent(EventType.CallHangup, content); this.sendVoipEvent(EventType.CallHangup, content);
} }
@@ -836,10 +832,10 @@ export class MatrixCall extends EventEmitter {
for (const sender of this.screensharingSenders) { for (const sender of this.screensharingSenders) {
this.peerConn.removeTrack(sender); this.peerConn.removeTrack(sender);
} }
this.deleteFeedByStream(this.localScreensharingStream);
for (const track of this.localScreensharingStream.getTracks()) { for (const track of this.localScreensharingStream.getTracks()) {
track.stop(); track.stop();
} }
this.deleteFeedByStream(this.localScreensharingStream);
return false; return false;
} }
} }
@@ -887,10 +883,10 @@ export class MatrixCall extends EventEmitter {
}); });
sender.replaceTrack(track); sender.replaceTrack(track);
this.deleteFeedByStream(this.localScreensharingStream);
for (const track of this.localScreensharingStream.getTracks()) { for (const track of this.localScreensharingStream.getTracks()) {
track.stop(); track.stop();
} }
this.deleteFeedByStream(this.localScreensharingStream);
return false; return false;
} }
@@ -1028,7 +1024,6 @@ export class MatrixCall extends EventEmitter {
this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia); this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia);
this.setState(CallState.CreateOffer); this.setState(CallState.CreateOffer);
logger.info("Got local AV stream with id " + this.localUsermediaStream.id);
logger.debug("gotUserMediaForInvite -> " + this.type); logger.debug("gotUserMediaForInvite -> " + this.type);
// Now we wait for the negotiationneeded event // Now we wait for the negotiationneeded event
}; };
@@ -1086,9 +1081,6 @@ export class MatrixCall extends EventEmitter {
} }
this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia); this.pushLocalFeed(stream, SDPStreamMetadataPurpose.Usermedia);
logger.info("Got local AV stream with id " + this.localUsermediaStream.id);
this.setState(CallState.CreateAnswer); this.setState(CallState.CreateAnswer);
let myAnswer; let myAnswer;
@@ -1285,7 +1277,7 @@ export class MatrixCall extends EventEmitter {
// https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation // https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation
const offerCollision = ( const offerCollision = (
(description.type === 'offer') && (description.type === 'offer') &&
(this.makingOffer || this.peerConn.signalingState != 'stable') (this.makingOffer || this.peerConn.signalingState !== 'stable')
); );
this.ignoreOffer = !polite && offerCollision; this.ignoreOffer = !polite && offerCollision;
@@ -1639,8 +1631,15 @@ export class MatrixCall extends EventEmitter {
} }
queueCandidate(content: RTCIceCandidate) { queueCandidate(content: RTCIceCandidate) {
// Sends candidates with are sent in a special way because we try to amalgamate // We partially de-trickle candidates by waiting for `delay` before sending them
// them into one message // amalgamated, in order to avoid sending too many m.call.candidates events and hitting
// rate limits in Matrix.
// In practice, it'd be better to remove rate limits for m.call.*
// N.B. this deliberately lets you queue and send blank candidates, which MSC2746
// currently proposes as the way to indicate that candidate gathering is complete.
// This will hopefully be changed to an explicit rather than implicit notification
// shortly.
this.candidateSendQueue.push(content); this.candidateSendQueue.push(content);
// Don't send the ICE candidates yet if the call is in the ringing state: this // Don't send the ICE candidates yet if the call is in the ringing state: this
@@ -1785,6 +1784,9 @@ export class MatrixCall extends EventEmitter {
logger.debug("Attempting to send " + candidates.length + " candidates"); logger.debug("Attempting to send " + candidates.length + " candidates");
try { try {
await this.sendVoipEvent(EventType.CallCandidates, content); await this.sendVoipEvent(EventType.CallCandidates, content);
// reset our retry count if we have successfully sent our candidates
// otherwise queueCandidate() will refuse to try to flush the queue
this.candidateSendTries = 0;
} catch (error) { } catch (error) {
// don't retry this event: we'll send another one later as we might // don't retry this event: we'll send another one later as we might
// have more candidates by then. // have more candidates by then.
@@ -1924,6 +1926,10 @@ export class MatrixCall extends EventEmitter {
} }
} }
} }
public get hasPeerConnection() {
return Boolean(this.peerConn);
}
} }
async function getScreensharingStream( async function getScreensharingStream(

View File

@@ -128,7 +128,7 @@ export class CallEventHandler {
return type.startsWith("m.call.") || type.startsWith("org.matrix.call."); return type.startsWith("m.call.") || type.startsWith("org.matrix.call.");
} }
private handleCallEvent(event: MatrixEvent) { private async handleCallEvent(event: MatrixEvent) {
const content = event.getContent(); const content = event.getContent();
const type = event.getType() as EventType; const type = event.getType() as EventType;
const weSentTheEvent = event.getSender() === this.client.credentials.userId; const weSentTheEvent = event.getSender() === this.client.credentials.userId;
@@ -169,7 +169,7 @@ export class CallEventHandler {
} }
call.callId = content.call_id; call.callId = content.call_id;
call.initWithInvite(event); const initWithInvitePromise = call.initWithInvite(event);
this.calls.set(call.callId, call); this.calls.set(call.callId, call);
// if we stashed candidate events for that call ID, play them back now // if we stashed candidate events for that call ID, play them back now
@@ -201,13 +201,17 @@ export class CallEventHandler {
// we've got an invite, pick the incoming call because we know // we've got an invite, pick the incoming call because we know
// we haven't sent our invite yet otherwise, pick whichever // we haven't sent our invite yet otherwise, pick whichever
// call has the lowest call ID (by string comparison) // call has the lowest call ID (by string comparison)
if (existingCall.state === CallState.WaitLocalMedia || if (
existingCall.state === CallState.CreateOffer || existingCall.state === CallState.WaitLocalMedia ||
existingCall.callId > call.callId) { existingCall.state === CallState.CreateOffer ||
existingCall.callId > call.callId
) {
logger.log( logger.log(
"Glare detected: answering incoming call " + call.callId + "Glare detected: answering incoming call " + call.callId +
" and canceling outgoing call " + existingCall.callId, " and canceling outgoing call " + existingCall.callId,
); );
// Await init with invite as we need a peerConn for the following methods
await initWithInvitePromise;
existingCall.replacedBy(call); existingCall.replacedBy(call);
call.answer(); call.answer();
} else { } else {
@@ -220,6 +224,7 @@ export class CallEventHandler {
} else { } else {
this.client.emit("Call.incoming", call); this.client.emit("Call.incoming", call);
} }
return;
} else if (type === EventType.CallCandidates) { } else if (type === EventType.CallCandidates) {
if (weSentTheEvent) return; if (weSentTheEvent) return;
@@ -232,6 +237,7 @@ export class CallEventHandler {
} else { } else {
call.onRemoteIceCandidatesReceived(event); call.onRemoteIceCandidatesReceived(event);
} }
return;
} else if ([EventType.CallHangup, EventType.CallReject].includes(type)) { } else if ([EventType.CallHangup, EventType.CallReject].includes(type)) {
// Note that we also observe our own hangups here so we can see // Note that we also observe our own hangups here so we can see
// if we've already rejected a call that would otherwise be valid // if we've already rejected a call that would otherwise be valid
@@ -255,10 +261,14 @@ export class CallEventHandler {
this.calls.delete(content.call_id); this.calls.delete(content.call_id);
} }
} }
return;
} }
// The following events need a call // The following events need a call and a peer connection
if (!call) return; if (!call || !call.hasPeerConnection) {
logger.warn("Discarding an event, we don't have a call/peerConn", type);
return;
}
// Ignore remote echo // Ignore remote echo
if (event.getContent().party_id === call.ourPartyId) return; if (event.getContent().party_id === call.ourPartyId) return;

13
tsconfig-build.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": true,
"noEmit": false,
"emitDecoratorMetadata": true,
"outDir": "./lib",
"rootDir": "src"
},
"exclude": [
"./spec/**/*.ts"
]
}

View File

@@ -1,20 +1,16 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2016",
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true, "esModuleInterop": true,
"module": "commonjs", "module": "commonjs",
"moduleResolution": "node", "moduleResolution": "node",
"target": "es2016",
"noImplicitAny": false, "noImplicitAny": false,
"sourceMap": true, "noEmit": true,
"outDir": "./lib", "declaration": true
"declaration": true,
"types": [
"node"
]
}, },
"include": [ "include": [
"./src/**/*.ts" "./src/**/*.ts",
"./spec/**/*.ts",
] ]
} }

View File

@@ -2,6 +2,28 @@
# yarn lockfile v1 # yarn lockfile v1
"@actions/core@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.4.0.tgz#cf2e6ee317e314b03886adfeb20e448d50d6e524"
integrity sha512-CGx2ilGq5i7zSLgiiGUtBCxhRRxibJYU6Fim0Q1Wg2aQL2LTnF27zbqZOrxfvFQ55eSBW0L8uVStgtKMpa0Qlg==
"@actions/github@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@actions/github/-/github-5.0.0.tgz#1754127976c50bd88b2e905f10d204d76d1472f8"
integrity sha512-QvE9eAAfEsS+yOOk0cylLBIO/d6WyWIOvsxxzdrPFaud39G6BOkUwScXZn1iBzQzHyu9SBkkLSWlohDWdsasAQ==
dependencies:
"@actions/http-client" "^1.0.11"
"@octokit/core" "^3.4.0"
"@octokit/plugin-paginate-rest" "^2.13.3"
"@octokit/plugin-rest-endpoint-methods" "^5.1.1"
"@actions/http-client@^1.0.11":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-1.0.11.tgz#c58b12e9aa8b159ee39e7dd6cbd0e91d905633c0"
integrity sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==
dependencies:
tunnel "0.0.6"
"@babel/cli@^7.12.10": "@babel/cli@^7.12.10":
version "7.14.8" version "7.14.8"
resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.14.8.tgz#fac73c0e2328a8af9fd3560c06b096bfa3730933" resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.14.8.tgz#fac73c0e2328a8af9fd3560c06b096bfa3730933"
@@ -1235,7 +1257,7 @@
dependencies: dependencies:
"@octokit/types" "^6.0.3" "@octokit/types" "^6.0.3"
"@octokit/core@^3.5.0": "@octokit/core@^3.4.0", "@octokit/core@^3.5.0":
version "3.5.1" version "3.5.1"
resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b" resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b"
integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw== integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==
@@ -1271,6 +1293,18 @@
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.3.0.tgz#160347858d727527901c6aae7f7d5c2414cc1f2e" resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.3.0.tgz#160347858d727527901c6aae7f7d5c2414cc1f2e"
integrity sha512-oz60hhL+mDsiOWhEwrj5aWXTOMVtQgcvP+sRzX4C3cH7WOK9QSAoEtjWh0HdOf6V3qpdgAmUMxnQPluzDWR7Fw== integrity sha512-oz60hhL+mDsiOWhEwrj5aWXTOMVtQgcvP+sRzX4C3cH7WOK9QSAoEtjWh0HdOf6V3qpdgAmUMxnQPluzDWR7Fw==
"@octokit/openapi-types@^9.5.0":
version "9.7.0"
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.7.0.tgz#9897cdefd629cd88af67b8dbe2e5fb19c63426b2"
integrity sha512-TUJ16DJU8mekne6+KVcMV5g6g/rJlrnIKn7aALG9QrNpnEipFc1xjoarh0PKaAWf2Hf+HwthRKYt+9mCm5RsRg==
"@octokit/plugin-paginate-rest@^2.13.3":
version "2.15.1"
resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.15.1.tgz#264189dd3ce881c6c33758824aac05a4002e056a"
integrity sha512-47r52KkhQDkmvUKZqXzA1lKvcyJEfYh3TKAIe5+EzMeyDM3d+/s5v11i2gTk8/n6No6DPi3k5Ind6wtDbo/AEg==
dependencies:
"@octokit/types" "^6.24.0"
"@octokit/plugin-paginate-rest@^2.6.2": "@octokit/plugin-paginate-rest@^2.6.2":
version "2.15.0" version "2.15.0"
resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.15.0.tgz#9c956c3710b2bd786eb3814eaf5a2b17392c150d" resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.15.0.tgz#9c956c3710b2bd786eb3814eaf5a2b17392c150d"
@@ -1291,6 +1325,14 @@
"@octokit/types" "^6.23.0" "@octokit/types" "^6.23.0"
deprecation "^2.3.1" deprecation "^2.3.1"
"@octokit/plugin-rest-endpoint-methods@^5.1.1":
version "5.8.0"
resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.8.0.tgz#33b342fe41f2603fdf8b958e6652103bb3ea3f3b"
integrity sha512-qeLZZLotNkoq+it6F+xahydkkbnvSK0iDjlXFo3jNTB+Ss0qIbYQb9V/soKLMkgGw8Q2sHjY5YEXiA47IVPp4A==
dependencies:
"@octokit/types" "^6.25.0"
deprecation "^2.3.1"
"@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": "@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0":
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677" resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677"
@@ -1329,6 +1371,13 @@
dependencies: dependencies:
"@octokit/openapi-types" "^9.3.0" "@octokit/openapi-types" "^9.3.0"
"@octokit/types@^6.24.0", "@octokit/types@^6.25.0":
version "6.25.0"
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.25.0.tgz#c8e37e69dbe7ce55ed98ee63f75054e7e808bf1a"
integrity sha512-bNvyQKfngvAd/08COlYIN54nRgxskmejgywodizQNyiKoXmWRAjKup2/LYwm+T9V0gsKH6tuld1gM0PzmOiB4Q==
dependencies:
"@octokit/openapi-types" "^9.5.0"
"@sinonjs/commons@^1.7.0": "@sinonjs/commons@^1.7.0":
version "1.8.3" version "1.8.3"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d"
@@ -1673,10 +1722,13 @@ align-text@^0.1.1, align-text@^0.1.3:
longest "^1.0.1" longest "^1.0.1"
repeat-string "^1.5.2" repeat-string "^1.5.2"
"allchange@github:matrix-org/allchange": allchange@^1.0.0:
version "0.0.1" version "1.0.0"
resolved "https://codeload.github.com/matrix-org/allchange/tar.gz/56b37b06339a3ac3fe771f3ec3d0bff798df8dab" resolved "https://registry.yarnpkg.com/allchange/-/allchange-1.0.0.tgz#f5177b7d97f8e97a2d059a1524db9a72d94dc6d2"
integrity sha512-O0VIaMIORxOaReyYEijDfKdpudJhbzzVYLdJR1aROyUgOLBEp9e5V/TDXQpjX23W90IFCSRZxsDb3exLRD05HA==
dependencies: dependencies:
"@actions/core" "^1.4.0"
"@actions/github" "^5.0.0"
"@octokit/rest" "^18.6.7" "@octokit/rest" "^18.6.7"
cli-color "^2.0.0" cli-color "^2.0.0"
js-yaml "^4.1.0" js-yaml "^4.1.0"
@@ -7267,6 +7319,11 @@ tunnel-agent@^0.6.0:
dependencies: dependencies:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
tunnel@0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
tweetnacl@^0.14.3, tweetnacl@~0.14.0: tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5" version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"