Compare commits

..

31 Commits

Author SHA1 Message Date
RiotRobot c87048bd9f v24.0.0 2023-03-28 14:10:53 +01:00
RiotRobot 02269f33b7 Prepare changelog for v24.0.0 2023-03-28 14:10:50 +01:00
Michael Weimann 7f46ae7b97 Further changes for v24.0.0 2023-03-28 14:05:24 +01:00
Michael Weimann 9a504af18e Changes for v24.0.0 2023-03-28 11:22:02 +01:00
RiotRobot 26663e67fd v23.5.0 2023-03-15 12:39:41 +00:00
RiotRobot 0cfc67c679 Prepare changelog for v23.5.0 2023-03-15 12:39:38 +00:00
RiotRobot 13d95c8219 v23.5.0-rc.1 2023-03-07 11:19:25 +00:00
RiotRobot e119dc4e89 Prepare changelog for v23.5.0-rc.1 2023-03-07 11:19:22 +00:00
Patrick Cloke b4cdc5a923 Implement MSC3758: a push rule condition to match event properties exactly (#3179)
* Add some comments.

* Support MSC3758 to exactly match values in push rule conditions.

* Update to stable prefix.
2023-03-06 14:52:43 +00:00
Andy Balaam a82e22b5de Remove items incorrectly included in changelog for 23.4.0 (#3190) 2023-03-03 13:30:38 +00:00
Patrick Cloke c894d09d8c Room call is an underride, not an override. (#3185) 2023-03-03 13:30:28 +00:00
Richard van der Hoff 585ce07260 Stub out the rust crypto implementation for browserify (#3187)
Fixes #3182.
2023-03-03 10:18:45 +00:00
Enrico Schwendig 8cbbdaa239 groupCall: make no media call param optional (#3186)
- ensure group call backwards-compatibility and move `allowCallWithoutVideoAndAudio`to the end of the `CroupCall` constructor
2023-03-02 18:55:09 +00:00
Damir Jelić cd526a254d Stop requesting room keys from other users (#2982)
* Refactor the room key handling method

* Fix the forwarded room key test to use the same user ids.

We have some tests that check if receiving a forwarded room key works.
These claim to use the same user id, but in fact they change the user id
in the last moment before the event is passed into the client.

Let's change this so we're always operating with the same user id.

* Stop requesting room keys from other users

We never accept such room keys, so there isn't a point in requesting
them.

* fixup! Refactor the room key handling method

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* fixup! Refactor the room key handling method

* fixup! Apply suggestions from code review

* fixup! Refactor the room key handling method

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-03-02 16:38:24 +00:00
Enrico Schwendig e782a2afa3 Enable group calls without video and audio track by configuration of MatrixClient (#3162)
* groupCall: add configuration param to allow no audio and no camera

* groupCall: enable datachannel to do no media group calls

* groupCall: changed call no media property as object property

* groupCall: fix existing unit tests

* groupCall: remove not needed flag

* groupCall: rename property to allow no media calls

* groupCall: mute unmute even without device

* groupCall: switch to promise callbacks

* groupCall: switch to try catch

* test: filter dummy code from coverage

* test: extend media mute tests

* groupCall: move permission check to device handler

* mediaHandler: add error in log statement
2023-03-02 16:35:52 +00:00
Hugh Nimmo-Smith 565339b1fd Remove experimental support for MSC3903 v1 (#3184)
* v2 of MSC3903 implementation

This is a deliberate breaking change on an unstable feature.

* Reinstate v1 support to make this a non-breaking change

Deprecates several experimental types

* Remove MSC3903 v1 support

This is a breaking change in code marked unstable/experimental

Revert "Reinstate v1 support to make this a non-breaking change"

This reverts commit 89773458b9a1e5f332938e5574f35b16d204d75d.
2023-03-02 16:34:22 +00:00
Hugh Nimmo-Smith 493203050a Support for v2 of MSC3903 (#3155)
* v2 of MSC3903 implementation

This is a deliberate breaking change on an unstable feature.

* Test correct protocol version

* Fix up test

* v2 of MSC3903 implementation

This is a deliberate breaking change on an unstable feature.

* Test correct protocol version

* Fix up test

* Reinstate v1 support to make this a non-breaking change

Deprecates several experimental types
2023-03-02 13:15:17 +00:00
renovate[bot] 41782c4593 Update dependency eslint-plugin-jsdoc to v40 (#3173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-02 07:23:55 +00:00
renovate[bot] 86256a4e74 Update definitelyTyped (#3170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-02 07:23:42 +00:00
Kerry 933a0c9909 Polls push rules (#3181)
* add poll push rule ids

* add getPushRuleAndKindById method to pushprocessor
2023-03-01 20:30:40 +00:00
Patrick Cloke c8a4d9b88a Implement MSC3873 to handle escaped dots in push rule keys (#3134)
* Add comments.

* Implment MSC3873 to handle escaped dots in keys.

* Add some comments about tests.

* Clarify spec behavior.

* Fix typo.

* Don't manually iterate string.

* Clean-up tests.

* Simplify tests.

* Add more tests & fix bug with empty parts.

* Add more edge cases.

* Add a regular expression solution.

This is ~80% slower than the basic split(".").

* Split on a simpler regular expression.

This is ~50% slower than a simple split(".").

* Remove redundant case in regex.

* Enable sticky regex.

* Rollback use of regex.

* Cache values in the PushProcessor.

* Use more each in tests.

* Pre-calculate the key parts instead of caching them.

* Fix typo.

* Switch back to external cache, but clean out obsolete cached values.

* Remove obsolete property.

* Remove more obsolete properties.
2023-03-01 12:23:40 +00:00
RiotRobot 437128d11b Resetting package fields for development 2023-02-28 10:37:23 +00:00
RiotRobot c18d09fd22 Merge branch 'master' into develop 2023-02-28 10:36:51 +00:00
RiotRobot 2a5e5e6a59 v23.4.0 2023-02-28 10:25:34 +00:00
RiotRobot 68e354752b Prepare changelog for v23.4.0 2023-02-28 10:25:31 +00:00
Michael Telatyński d80b7499fd Fix spec compliance issue around encrypted m.relates_to (#3178)
* Fix spec compliance issue around encrypted `m.relates_to`

* Add test
2023-02-27 22:12:45 +00:00
Janne Mareike Koschinski 9c8093eb3e Fix reactions in threads sometimes causing stuck notifications (#3146)
* Associate event with thread before adding it to the thread timeline

* Make sure events can be added to thread correctly

* Write initial test case

* Add additional comment for why the code had to be reordered
2023-02-24 13:12:06 +00:00
renovate[bot] aec1c11037 Update all non-major dependencies (#3171)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-24 12:17:19 +00:00
renovate[bot] e2e9986059 Update babel monorepo to v7.21.0 (#3177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-23 16:43:01 +00:00
Michael Telatynski d70ffdbc02 Improve types (#3175)
* Improve types

* Add test
2023-02-22 17:39:37 +00:00
renovate[bot] 21cc9c3d8a Update jest monorepo to v29.4.3 (#3169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-22 10:23:29 +00:00
63 changed files with 2638 additions and 1352 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
- name: 📥 Download artifact
uses: dawidd6/action-download-artifact@bd10f381a96414ce2b13a11bfa89902ba7cea07f # v2.24.3
uses: dawidd6/action-download-artifact@5e780fc7bbd0cac69fc73271ed86edf5dcb72d67 # v2.26.0
with:
workflow: static_analysis.yml
run_id: ${{ github.event.workflow_run.id }}
+1
View File
@@ -19,3 +19,4 @@ out
.vscode
.vscode/
.idea/
+21 -7
View File
@@ -1,13 +1,27 @@
Changes in [23.4.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.4.0-rc.2) (2023-02-22)
============================================================================================================
Changes in [24.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v24.0.0) (2023-03-28)
==================================================================================================
## 🐛 Bug Fixes
* Changes for matrix-js-sdk v24.0.0
Changes in [23.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.5.0) (2023-03-15)
==================================================================================================
## ✨ Features
* Implement MSC3758: a push rule condition to match event properties exactly ([\#3179](https://github.com/matrix-org/matrix-js-sdk/pull/3179)).
* Enable group calls without video and audio track by configuration of MatrixClient ([\#3162](https://github.com/matrix-org/matrix-js-sdk/pull/3162)). Contributed by @EnricoSchw.
* Updates to protocol used for Sign in with QR code ([\#3155](https://github.com/matrix-org/matrix-js-sdk/pull/3155)). Contributed by @hughns.
* Implement MSC3873 to handle escaped dots in push rule keys ([\#3134](https://github.com/matrix-org/matrix-js-sdk/pull/3134)). Fixes undefined/matrix-js-sdk#1454.
## 🐛 Bug Fixes
* Fix spec compliance issue around encrypted `m.relates_to` ([\#3178](https://github.com/matrix-org/matrix-js-sdk/pull/3178)).
* Fix reactions in threads sometimes causing stuck notifications ([\#3146](https://github.com/matrix-org/matrix-js-sdk/pull/3146)). Fixes vector-im/element-web#24000. Contributed by @justjanne.
Changes in [23.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.4.0) (2023-02-28)
==================================================================================================
## ✨ Features
* Add easy way to determine if the decryption failure is due to "DecryptionError: The sender has disabled encrypting to unverified devices." ([\#3167](https://github.com/matrix-org/matrix-js-sdk/pull/3167)). Contributed by @florianduros.
Changes in [23.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.4.0-rc.1) (2023-02-21)
============================================================================================================
## ✨ Features
* Polls: expose end event id on poll model ([\#3160](https://github.com/matrix-org/matrix-js-sdk/pull/3160)). Contributed by @kerryarchibald.
* Polls: count undecryptable poll relations ([\#3163](https://github.com/matrix-org/matrix-js-sdk/pull/3163)). Contributed by @kerryarchibald.
+26 -5
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "23.4.0-rc.2",
"version": "24.0.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=16.0.0"
@@ -14,7 +14,7 @@
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "mkdir dist && browserify -d src/browser-index.ts -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:compile-browser": "mkdir dist && BROWSERIFYSWAP_ENV='no-rust-crypto' browserify -d src/browser-index.ts -p [ tsify -p ./tsconfig-build.json ] | 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",
"gendoc": "typedoc",
"lint": "yarn lint:types && yarn lint:js",
@@ -97,16 +97,17 @@
"babelify": "^10.0.0",
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"browserify-swap": "^0.2.2",
"debug": "^4.3.4",
"docdash": "^2.0.0",
"domexception": "^4.0.0",
"eslint": "8.33.0",
"eslint": "8.34.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^27.1.6",
"eslint-plugin-jsdoc": "^39.6.4",
"eslint-plugin-jsdoc": "^40.0.0",
"eslint-plugin-matrix-org": "^1.0.0",
"eslint-plugin-tsdoc": "^0.2.17",
"eslint-plugin-unicorn": "^45.0.0",
@@ -118,7 +119,7 @@
"jest-localstorage-mock": "^2.4.6",
"jest-mock": "^29.0.0",
"matrix-mock-request": "^2.5.0",
"prettier": "2.8.3",
"prettier": "2.8.4",
"rimraf": "^4.0.0",
"terser": "^5.5.1",
"tsify": "^5.0.2",
@@ -148,5 +149,25 @@
"outputName": "jest-sonar-report.xml",
"relativePaths": true
},
"browserify": {
"transform": [
"browserify-swap",
[
"babelify",
{
"sourceMaps": "inline",
"presets": [
"@babel/preset-env",
"@babel/preset-typescript"
]
}
]
]
},
"browserify-swap": {
"no-rust-crypto": {
"src/rust-crypto/index.ts$": "./src/rust-crypto/browserify-index.ts"
}
},
"typings": "./lib/index.d.ts"
}
+8 -8
View File
@@ -543,7 +543,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// if we're using the old crypto impl, stub out some methods in the device manager.
// TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic.
if (aliceClient.crypto) {
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve(new Map());
aliceClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
}
@@ -603,7 +603,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// if we're using the old crypto impl, stub out some methods in the device manager.
// TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic.
if (aliceClient.crypto) {
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve(new Map());
aliceClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
}
@@ -671,7 +671,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// if we're using the old crypto impl, stub out some methods in the device manager.
// TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic.
if (aliceClient.crypto) {
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve(new Map());
aliceClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
}
@@ -1027,8 +1027,8 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
throw new Error("sendTextMessage succeeded on an unknown device");
} catch (e) {
expect((e as any).name).toEqual("UnknownDeviceError");
expect(Object.keys((e as any).devices)).toEqual([aliceClient.getUserId()!]);
expect(Object.keys((e as any)?.devices[aliceClient.getUserId()!])).toEqual(["DEVICE_ID"]);
expect([...(e as any).devices.keys()]).toEqual([aliceClient.getUserId()!]);
expect((e as any).devices.get(aliceClient.getUserId()!).has("DEVICE_ID"));
}
// mark the device as known, and resend.
@@ -1099,7 +1099,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// if we're using the old crypto impl, stub out some methods in the device manager.
// TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic.
if (aliceClient.crypto) {
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve(new Map());
aliceClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
}
@@ -1255,7 +1255,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// if we're using the old crypto impl, stub out some methods in the device manager.
// TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic.
if (aliceClient.crypto) {
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve(new Map());
aliceClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
}
@@ -1322,7 +1322,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// if we're using the old crypto impl, stub out some methods in the device manager.
// TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic.
if (aliceClient.crypto) {
aliceClient.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
aliceClient.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
aliceClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
aliceClient.crypto!.deviceList.getUserByIdentityKey = () => beccaTestClient.client.getUserId()!;
}
+11 -4
View File
@@ -603,14 +603,14 @@ describe("MatrixClient", function () {
});
const prom = client!.downloadKeys(["boris", "chaz"]).then(function (res) {
assertObjectContains(res.boris.dev1, {
assertObjectContains(res.get("boris")!.get("dev1")!, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev1": ed25519key },
algorithms: ["1"],
unsigned: { abc: "def" },
});
assertObjectContains(res.chaz.dev2, {
assertObjectContains(res.get("chaz")!.get("dev2")!, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev2": ed25519key },
algorithms: ["2"],
@@ -1336,18 +1336,25 @@ describe("MatrixClient", function () {
it.each([
{
userId: "alice@localhost",
powerLevel: 100,
expectation: {
"alice@localhost": 100,
},
},
{
userId: ["alice@localhost", "bob@localhost"],
powerLevel: 100,
expectation: {
"alice@localhost": 100,
"bob@localhost": 100,
},
},
])("should modify power levels of $userId correctly", async ({ userId, expectation }) => {
{
userId: "alice@localhost",
powerLevel: undefined,
expectation: {},
},
])("should modify power levels of $userId correctly", async ({ userId, powerLevel, expectation }) => {
const event = {
getType: () => "m.room.power_levels",
getContent: () => ({
@@ -1364,7 +1371,7 @@ describe("MatrixClient", function () {
})
.respond(200, {});
const prom = client!.setPowerLevel("!room_id:server", userId, 100, event);
const prom = client!.setPowerLevel("!room_id:server", userId, powerLevel, event);
await httpBackend!.flushAllExpected();
await prom;
});
@@ -18,8 +18,21 @@ import "fake-indexeddb/auto";
import HttpBackend from "matrix-mock-request";
import { Category, ISyncResponse, MatrixClient, NotificationCountType, Room } from "../../src";
import {
Category,
ClientEvent,
EventType,
ISyncResponse,
MatrixClient,
MatrixEvent,
NotificationCountType,
RelationType,
Room,
} from "../../src";
import { TestClient } from "../TestClient";
import { ReceiptType } from "../../src/@types/read_receipts";
import { mkThread } from "../test-utils/thread";
import { SyncState } from "../../src/sync";
describe("MatrixClient syncing", () => {
const userA = "@alice:localhost";
@@ -51,6 +64,86 @@ describe("MatrixClient syncing", () => {
return httpBackend!.stop();
});
it("reactions in thread set the correct timeline to unread", async () => {
const roomId = "!room:localhost";
// start the client, and wait for it to initialise
httpBackend!.when("GET", "/sync").respond(200, {
next_batch: "s_5_3",
rooms: {
[Category.Join]: {},
[Category.Leave]: {},
[Category.Invite]: {},
},
});
client!.startClient({ threadSupport: true });
await Promise.all([
httpBackend?.flushAllExpected(),
new Promise<void>((resolve) => {
client!.on(ClientEvent.Sync, (state) => state === SyncState.Syncing && resolve());
}),
]);
const room = new Room(roomId, client!, selfUserId);
jest.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
const threadReply = thread.events.at(-1)!;
room.addLiveEvents([thread.rootEvent]);
// Initialize read receipt datastructure before testing the reaction
room.addReceiptToStructure(thread.rootEvent.getId()!, ReceiptType.Read, selfUserId, { ts: 1 }, false);
thread.thread.addReceiptToStructure(
threadReply.getId()!,
ReceiptType.Read,
selfUserId,
{ thread_id: thread.thread.id, ts: 1 },
false,
);
expect(room.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(thread.rootEvent.getId());
expect(thread.thread.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(threadReply.getId());
const reactionEventId = `$9-${Math.random()}-${Math.random()}`;
let lastEvent: MatrixEvent | null = null;
jest.spyOn(client! as any, "sendEventHttpRequest").mockImplementation((event) => {
lastEvent = event as MatrixEvent;
return { event_id: reactionEventId };
});
await client!.sendEvent(roomId, EventType.Reaction, {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: threadReply.getId(),
key: "",
},
});
expect(lastEvent!.getId()).toEqual(reactionEventId);
room.handleRemoteEcho(new MatrixEvent(lastEvent!.event), lastEvent!);
// Our ideal state after this is the following:
//
// Room: [synthetic: threadroot, actual: threadroot]
// Thread: [synthetic: threadreaction, actual: threadreply]
//
// The reaction and reply are both in the thread, and their receipts should be isolated to the thread.
// The reaction has not been acknowledged in a dedicated read receipt message, so only the synthetic receipt
// should be updated.
// Ensure the synthetic receipt for the room has not been updated
expect(room.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(thread.rootEvent.getId());
expect(room.getEventReadUpTo(selfUserId, false)).toEqual(thread.rootEvent.getId());
// Ensure the actual receipt for the room has not been updated
expect(room.getReadReceiptForUserId(selfUserId, true)?.eventId).toEqual(thread.rootEvent.getId());
expect(room.getEventReadUpTo(selfUserId, true)).toEqual(thread.rootEvent.getId());
// Ensure the synthetic receipt for the thread has been updated
expect(thread.thread.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(reactionEventId);
expect(thread.thread.getEventReadUpTo(selfUserId, false)).toEqual(reactionEventId);
// Ensure the actual receipt for the thread has not been updated
expect(thread.thread.getReadReceiptForUserId(selfUserId, true)?.eventId).toEqual(threadReply.getId());
expect(thread.thread.getEventReadUpTo(selfUserId, true)).toEqual(threadReply.getId());
});
describe("Stuck unread notifications integration tests", () => {
const ROOM_ID = "!room:localhost";
+3 -3
View File
@@ -472,7 +472,7 @@ describe("MatrixClient crypto", () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
@@ -483,7 +483,7 @@ describe("MatrixClient crypto", () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
@@ -545,7 +545,7 @@ describe("MatrixClient crypto", () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
+2 -5
View File
@@ -30,6 +30,7 @@ import {
RoomState,
RoomStateEvent,
RoomStateEventHandlerMap,
SendToDeviceContentMap,
} from "../../src";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { ReEmitter } from "../../src/ReEmitter";
@@ -443,11 +444,7 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
>();
public sendToDevice = jest.fn<
Promise<{}>,
[
eventType: string,
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
txnId?: string,
]
[eventType: string, contentMap: SendToDeviceContentMap, txnId?: string]
>();
public isInitialSyncComplete(): boolean {
+39 -26
View File
@@ -304,20 +304,24 @@ describe("Crypto", function () {
describe("Key requests", function () {
let aliceClient: MatrixClient;
let secondAliceClient: MatrixClient;
let bobClient: MatrixClient;
let claraClient: MatrixClient;
beforeEach(async function () {
aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
secondAliceClient = new TestClient("@alice:example.com", "secondAliceDevice").client;
bobClient = new TestClient("@bob:example.com", "bobdevice").client;
claraClient = new TestClient("@clara:example.com", "claradevice").client;
await aliceClient.initCrypto();
await secondAliceClient.initCrypto();
await bobClient.initCrypto();
await claraClient.initCrypto();
});
afterEach(async function () {
aliceClient.stopClient();
secondAliceClient.stopClient();
bobClient.stopClient();
claraClient.stopClient();
});
@@ -401,7 +405,7 @@ describe("Crypto", function () {
// the first message can't be decrypted yet, but the second one
// can
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
bobClient.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
bobClient.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
await bobDecryptor.onRoomKeyEvent(ksEvent);
await decryptEventsPromise;
@@ -568,17 +572,17 @@ describe("Crypto", function () {
expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId);
});
it("should accept forwarded keys which it requested", async function () {
it("should accept forwarded keys it requested from one of its own user's other devices", async function () {
const encryptionCfg = {
algorithm: "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
const bobRoom = new Room(roomId, secondAliceClient, "@alice:example.com", {});
aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom);
secondAliceClient.store.storeRoom(bobRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
await secondAliceClient.setRoomEncryption(roomId, encryptionCfg);
const events = [
new MatrixEvent({
type: "m.room.message",
@@ -614,7 +618,7 @@ describe("Crypto", function () {
// @ts-ignore private properties
event.claimedEd25519Key = null;
try {
await bobClient.crypto!.decryptEvent(event);
await secondAliceClient.crypto!.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
@@ -623,10 +627,11 @@ describe("Crypto", function () {
);
const device = new DeviceInfo(aliceClient.deviceId!);
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
device.verified = DeviceInfo.DeviceVerification.VERIFIED;
secondAliceClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
secondAliceClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const cryptoStore = bobClient.crypto!.cryptoStore;
const cryptoStore = secondAliceClient.crypto!.cryptoStore;
const eventContent = events[0].getWireContent();
const senderKey = eventContent.sender_key;
const sessionId = eventContent.session_id;
@@ -642,7 +647,7 @@ describe("Crypto", function () {
state: RoomKeyRequestState.Sent,
});
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
const bobDecryptor = secondAliceClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
const decryptEventsPromise = Promise.all(
events.map((ev) => {
@@ -651,7 +656,7 @@ describe("Crypto", function () {
);
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
const key = await secondAliceClient.crypto!.olmDevice.getInboundGroupSessionKey(
roomId,
events[0].getWireContent().sender_key,
events[0].getWireContent().session_id,
@@ -755,7 +760,7 @@ describe("Crypto", function () {
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
});
it("should accept forwarded keys from one of its own user's other devices", async function () {
it("should not accept requested forwarded keys from other users", async function () {
const encryptionCfg = {
algorithm: "m.megolm.v1.aes-sha2",
};
@@ -809,31 +814,39 @@ describe("Crypto", function () {
}),
);
const device = new DeviceInfo(claraClient.deviceId!);
const cryptoStore = bobClient.crypto!.cryptoStore;
const eventContent = events[0].getWireContent();
const senderKey = eventContent.sender_key;
const sessionId = eventContent.session_id;
const roomKeyRequestBody = {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: roomId,
sender_key: senderKey,
session_id: sessionId,
};
const outgoingReq = await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody);
expect(outgoingReq).toBeDefined();
await cryptoStore.updateOutgoingRoomKeyRequest(outgoingReq!.requestId, RoomKeyRequestState.Unsent, {
state: RoomKeyRequestState.Sent,
});
const device = new DeviceInfo(aliceClient.deviceId!);
device.verified = DeviceInfo.DeviceVerification.VERIFIED;
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@bob:example.com";
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
const decryptEventsPromise = Promise.all(
events.map((ev) => {
return awaitEvent(ev, "Event.decrypted");
}),
);
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
ksEvent.event.sender = bobClient.getUserId()!;
ksEvent.sender = new RoomMember(roomId, bobClient.getUserId()!);
ksEvent.event.sender = aliceClient.getUserId()!;
ksEvent.sender = new RoomMember(roomId, aliceClient.getUserId()!);
await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
roomId,
events[0].getWireContent().sender_key,
events[0].getWireContent().session_id,
);
expect(key).not.toBeNull();
await decryptEventsPromise;
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
expect(key).toBeNull();
});
it("should not accept unexpected forwarded keys for a room it's in", async function () {
@@ -1026,7 +1039,7 @@ describe("Crypto", function () {
beforeEach(async () => {
ensureOlmSessionsForDevices = jest.spyOn(olmlib, "ensureOlmSessionsForDevices");
ensureOlmSessionsForDevices.mockResolvedValue({});
ensureOlmSessionsForDevices.mockResolvedValue(new Map());
encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice");
encryptMessageForDevice.mockImplementation(async (...[result, , , , , , payload]) => {
result.plaintext = { type: 0, body: JSON.stringify(payload) };
+58 -44
View File
@@ -34,6 +34,7 @@ import { ClientEvent, MatrixClient, RoomMember } from "../../../../src";
import { DeviceInfo, IDevice } from "../../../../src/crypto/deviceinfo";
import { DeviceTrustLevel } from "../../../../src/crypto/CrossSigning";
import { MegolmEncryption as MegolmEncryptionClass } from "../../../../src/crypto/algorithms/megolm";
import { recursiveMapToObject } from "../../../../src/utils";
import { sleep } from "../../../../src/utils";
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
@@ -183,14 +184,22 @@ describe("MegolmDecryption", function () {
const deviceInfo = {} as DeviceInfo;
mockCrypto.getStoredDevice.mockReturnValue(deviceInfo);
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue({
"@alice:foo": {
alidevice: {
sessionId: "alisession",
device: new DeviceInfo("alidevice"),
},
},
});
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue(
new Map([
[
"@alice:foo",
new Map([
[
"alidevice",
{
sessionId: "alisession",
device: new DeviceInfo("alidevice"),
},
],
]),
],
]),
);
const awaitEncryptForDevice = new Promise<void>((res, rej) => {
mockOlmLib.encryptMessageForDevice.mockImplementation(() => {
@@ -357,11 +366,7 @@ describe("MegolmDecryption", function () {
} as unknown as DeviceInfo;
mockCrypto.downloadKeys.mockReturnValue(
Promise.resolve({
"@alice:home.server": {
aliceDevice: aliceDeviceInfo,
},
}),
Promise.resolve(new Map([["@alice:home.server", new Map([["aliceDevice", aliceDeviceInfo]])]])),
);
mockCrypto.checkDeviceTrust.mockReturnValue({
@@ -523,23 +528,32 @@ describe("MegolmDecryption", function () {
let megolm: MegolmEncryptionClass;
let room: jest.Mocked<Room>;
const deviceMap: DeviceInfoMap = {
"user-a": {
"device-a": new DeviceInfo("device-a"),
"device-b": new DeviceInfo("device-b"),
"device-c": new DeviceInfo("device-c"),
},
"user-b": {
"device-d": new DeviceInfo("device-d"),
"device-e": new DeviceInfo("device-e"),
"device-f": new DeviceInfo("device-f"),
},
"user-c": {
"device-g": new DeviceInfo("device-g"),
"device-h": new DeviceInfo("device-h"),
"device-i": new DeviceInfo("device-i"),
},
};
const deviceMap: DeviceInfoMap = new Map([
[
"user-a",
new Map([
["device-a", new DeviceInfo("device-a")],
["device-b", new DeviceInfo("device-b")],
["device-c", new DeviceInfo("device-c")],
]),
],
[
"user-b",
new Map([
["device-d", new DeviceInfo("device-d")],
["device-e", new DeviceInfo("device-e")],
["device-f", new DeviceInfo("device-f")],
]),
],
[
"user-c",
new Map([
["device-g", new DeviceInfo("device-g")],
["device-h", new DeviceInfo("device-h")],
["device-i", new DeviceInfo("device-i")],
]),
],
]);
beforeEach(() => {
room = testUtils.mock(Room, "Room") as jest.Mocked<Room>;
@@ -572,8 +586,8 @@ describe("MegolmDecryption", function () {
//@ts-ignore private member access, gross
await megolm.encryptionPreparation?.promise;
for (const userId in deviceMap) {
for (const deviceId in deviceMap[userId]) {
for (const [userId, devices] of deviceMap) {
for (const deviceId of devices.keys()) {
expect(mockCrypto.checkDeviceTrust).toHaveBeenCalledWith(userId, deviceId);
}
}
@@ -658,20 +672,20 @@ describe("MegolmDecryption", function () {
expect(aliceClient.sendToDevice).toHaveBeenCalled();
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
delete contentMap["@bob:example.com"].bobdevice1.session_id;
delete contentMap["@bob:example.com"].bobdevice1["org.matrix.msgid"];
delete contentMap["@bob:example.com"].bobdevice2.session_id;
delete contentMap["@bob:example.com"].bobdevice2["org.matrix.msgid"];
expect(contentMap).toStrictEqual({
"@bob:example.com": {
bobdevice1: {
delete contentMap.get("@bob:example.com")?.get("bobdevice1")?.["session_id"];
delete contentMap.get("@bob:example.com")?.get("bobdevice1")?.["org.matrix.msgid"];
delete contentMap.get("@bob:example.com")?.get("bobdevice2")?.["session_id"];
delete contentMap.get("@bob:example.com")?.get("bobdevice2")?.["org.matrix.msgid"];
expect(recursiveMapToObject(contentMap)).toStrictEqual({
["@bob:example.com"]: {
["bobdevice1"]: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
code: "m.unverified",
reason: "The sender has disabled encrypting to unverified devices.",
sender_key: aliceDevice.deviceCurve25519Key,
},
bobdevice2: {
["bobdevice2"]: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
code: "m.blacklisted",
@@ -839,10 +853,10 @@ describe("MegolmDecryption", function () {
expect(aliceClient.sendToDevice).toHaveBeenCalled();
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
delete contentMap["@bob:example.com"]["bobdevice"]["org.matrix.msgid"];
expect(contentMap).toStrictEqual({
"@bob:example.com": {
bobdevice: {
delete contentMap.get("@bob:example.com")?.get("bobdevice")?.["org.matrix.msgid"];
expect(recursiveMapToObject(contentMap)).toStrictEqual({
["@bob:example.com"]: {
["bobdevice"]: {
algorithm: "m.megolm.v1.aes-sha2",
code: "m.no_olm",
reason: "Unable to establish a secure channel.",
+15 -16
View File
@@ -146,18 +146,21 @@ describe("OlmDevice", function () {
});
},
} as unknown as MockedObject<MatrixClient>;
const devicesByUser = {
"@bob:example.com": [
DeviceInfo.fromStorage(
{
keys: {
"curve25519:ABCDEFG": "akey",
const devicesByUser = new Map([
[
"@bob:example.com",
[
DeviceInfo.fromStorage(
{
keys: {
"curve25519:ABCDEFG": "akey",
},
},
},
"ABCDEFG",
),
"ABCDEFG",
),
],
],
};
]);
// start two tasks that try to ensure that there's an olm session
const promises = Promise.all([
@@ -218,12 +221,8 @@ describe("OlmDevice", function () {
// There's no required ordering of devices per user, so here we
// create two different orderings so that each task reserves a
// device the other task needs before continuing.
const devicesByUserAB = {
"@bob:example.com": [deviceBobA, deviceBobB],
};
const devicesByUserBA = {
"@bob:example.com": [deviceBobB, deviceBobA],
};
const devicesByUserAB = new Map([["@bob:example.com", [deviceBobA, deviceBobB]]]);
const devicesByUserBA = new Map([["@bob:example.com", [deviceBobB, deviceBobA]]]);
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUserAB));
+2 -2
View File
@@ -45,7 +45,7 @@ async function makeTestClient(
await client.initCrypto();
// No need to download keys for these tests
jest.spyOn(client.crypto!, "downloadKeys").mockResolvedValue({});
jest.spyOn(client.crypto!, "downloadKeys").mockResolvedValue(new Map());
return client;
}
@@ -274,7 +274,7 @@ describe("Secrets", function () {
Object.values(otks)[0],
);
osborne2.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
osborne2.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
osborne2.client.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const request = await secretStorage.request("foo", ["VAX"]);
+22 -11
View File
@@ -121,12 +121,12 @@ describe("SAS verification", function () {
alice.client.crypto!.deviceList.storeDevicesForUser("@bob:example.com", BOB_DEVICES);
alice.client.downloadKeys = () => {
return Promise.resolve({});
return Promise.resolve(new Map());
};
bob.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", ALICE_DEVICES);
bob.client.downloadKeys = () => {
return Promise.resolve({});
return Promise.resolve(new Map());
};
aliceSasEvent = null;
@@ -176,6 +176,7 @@ describe("SAS verification", function () {
}
});
});
afterEach(async () => {
await Promise.all([alice.stop(), bob.stop()]);
@@ -186,10 +187,14 @@ describe("SAS verification", function () {
let macMethod;
let keyAgreement;
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = function (type, map) {
bob.client.sendToDevice = async (type, map) => {
if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
keyAgreement = map[alice.client.getUserId()!][alice.client.deviceId!].key_agreement_protocol;
macMethod = map
.get(alice.client.getUserId()!)
?.get(alice.client.deviceId!)?.message_authentication_code;
keyAgreement = map
.get(alice.client.getUserId()!)
?.get(alice.client.deviceId!)?.key_agreement_protocol;
}
return origSendToDevice(type, map);
};
@@ -237,7 +242,7 @@ describe("SAS verification", function () {
// has, since it is the same object. If this does not
// happen, the verification will fail due to a hash
// commitment mismatch.
map[bob.client.getUserId()!][bob.client.deviceId!].message_authentication_codes = [
map.get(bob.client.getUserId()!)!.get(bob.client.deviceId!)!.message_authentication_codes = [
"hkdf-hmac-sha256",
];
}
@@ -246,7 +251,9 @@ describe("SAS verification", function () {
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = (type, map) => {
if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
macMethod = map
.get(alice.client.getUserId()!)!
.get(alice.client.deviceId!)!.message_authentication_code;
}
return bobOrigSendToDevice(type, map);
};
@@ -291,14 +298,18 @@ describe("SAS verification", function () {
// has, since it is the same object. If this does not
// happen, the verification will fail due to a hash
// commitment mismatch.
map[bob.client.getUserId()!][bob.client.deviceId!].message_authentication_codes = ["hmac-sha256"];
map.get(bob.client.getUserId()!)!.get(bob.client.deviceId!)!.message_authentication_codes = [
"hmac-sha256",
];
}
return aliceOrigSendToDevice(type, map);
};
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = (type, map) => {
if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
macMethod = map
.get(alice.client.getUserId()!)!
.get(alice.client.deviceId!)!.message_authentication_code;
}
return bobOrigSendToDevice(type, map);
};
@@ -454,7 +465,7 @@ describe("SAS verification", function () {
);
};
alice.client.downloadKeys = () => {
return Promise.resolve({});
return Promise.resolve(new Map());
};
bob.client.crypto!.setDeviceVerification = jest.fn();
@@ -472,7 +483,7 @@ describe("SAS verification", function () {
return "bob+base64+ed25519+key";
};
bob.client.downloadKeys = () => {
return Promise.resolve({});
return Promise.resolve(new Map());
};
aliceSasEvent = null;
+5 -5
View File
@@ -20,7 +20,7 @@ import { IContent, MatrixEvent } from "../../../../src/models/event";
import { IRoomTimelineData } from "../../../../src/models/event-timeline-set";
import { Room, RoomEvent } from "../../../../src/models/room";
import { logger } from "../../../../src/logger";
import { MatrixClient, ClientEvent, ICreateClientOpts } from "../../../../src/client";
import { MatrixClient, ClientEvent, ICreateClientOpts, SendToDeviceContentMap } from "../../../../src/client";
interface UserInfo {
userId: string;
@@ -36,16 +36,16 @@ export async function makeTestClients(
const clientMap: Record<string, Record<string, MatrixClient>> = {};
const makeSendToDevice =
(matrixClient: MatrixClient): MatrixClient["sendToDevice"] =>
async (type, map) => {
async (type: string, contentMap: SendToDeviceContentMap) => {
// logger.log(this.getUserId(), "sends", type, map);
for (const [userId, devMap] of Object.entries(map)) {
for (const [userId, deviceMessages] of contentMap) {
if (userId in clientMap) {
for (const [deviceId, msg] of Object.entries(devMap)) {
for (const [deviceId, message] of deviceMessages) {
if (deviceId in clientMap[userId]) {
const event = new MatrixEvent({
sender: matrixClient.getUserId()!,
type: type,
content: msg,
content: message,
});
const client = clientMap[userId][deviceId];
const decryptionPromise = event.isEncrypted()
@@ -25,6 +25,7 @@ import { IContent, MatrixEvent } from "../../../../src/models/event";
import { MatrixClient } from "../../../../src/client";
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
import { VerificationBase } from "../../../../src/crypto/verification/Base";
import { MapWithDefault } from "../../../../src/utils";
type MockClient = MatrixClient & {
popEvents: () => MatrixEvent[];
@@ -33,7 +34,9 @@ type MockClient = MatrixClient & {
function makeMockClient(userId: string, deviceId: string): MockClient {
let counter = 1;
let events: MatrixEvent[] = [];
const deviceEvents: Record<string, Record<string, MatrixEvent[]>> = {};
const deviceEvents: MapWithDefault<string, MapWithDefault<string, MatrixEvent[]>> = new MapWithDefault(
() => new MapWithDefault(() => []),
);
return {
getUserId() {
return userId;
@@ -58,15 +61,11 @@ function makeMockClient(userId: string, deviceId: string): MockClient {
return Promise.resolve({ event_id: eventId });
},
sendToDevice(type: string, msgMap: Record<string, Record<string, IContent>>) {
for (const userId of Object.keys(msgMap)) {
const deviceMap = msgMap[userId];
for (const deviceId of Object.keys(deviceMap)) {
const content = deviceMap[deviceId];
sendToDevice(type: string, msgMap: Map<string, Map<string, IContent>>) {
for (const [userId, deviceMessages] of msgMap) {
for (const [deviceId, content] of deviceMessages) {
const event = new MatrixEvent({ content, type });
deviceEvents[userId] = deviceEvents[userId] || {};
deviceEvents[userId][deviceId] = deviceEvents[userId][deviceId] || [];
deviceEvents[userId][deviceId].push(event);
deviceEvents.getOrCreate(userId).getOrCreate(deviceId).push(event);
}
}
return Promise.resolve({});
@@ -79,14 +78,9 @@ function makeMockClient(userId: string, deviceId: string): MockClient {
return e;
},
// @ts-ignore special testing fn
popDeviceEvents(userId: string, deviceId: string): MatrixEvent[] {
const forDevice = deviceEvents[userId];
const events = forDevice && forDevice[deviceId];
const result = events || [];
if (events) {
delete forDevice[deviceId];
}
const result = deviceEvents.get(userId)?.get(deviceId) || [];
deviceEvents?.get(userId)?.delete(deviceId);
return result;
},
} as unknown as MockClient;
+10 -5
View File
@@ -204,9 +204,14 @@ describe("RoomWidgetClient", () => {
});
describe("to-device messages", () => {
const unencryptedContentMap = {
"@alice:example.org": { "*": { hello: "alice!" } },
"@bob:example.org": { bobDesktop: { hello: "bob!" } },
const unencryptedContentMap = new Map([
["@alice:example.org", new Map([["*", { hello: "alice!" }]])],
["@bob:example.org", new Map([["bobDesktop", { hello: "bob!" }]])],
]);
const expectedRequestData = {
["@alice:example.org"]: { ["*"]: { hello: "alice!" } },
["@bob:example.org"]: { ["bobDesktop"]: { hello: "bob!" } },
};
it("sends unencrypted (sendToDevice)", async () => {
@@ -214,7 +219,7 @@ describe("RoomWidgetClient", () => {
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
await client.sendToDevice("org.example.foo", unencryptedContentMap);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, expectedRequestData);
});
it("sends unencrypted (queueToDevice)", async () => {
@@ -229,7 +234,7 @@ describe("RoomWidgetClient", () => {
],
};
await client.queueToDevice(batch);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, expectedRequestData);
});
it("sends encrypted (encryptAndSendToDevices)", async () => {
+35 -1
View File
@@ -16,7 +16,7 @@ limitations under the License.
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
import { emitPromise } from "../../test-utils/test-utils";
import { Crypto } from "../../../src/crypto";
import { Crypto, IEventDecryptionResult } from "../../../src/crypto";
describe("MatrixEvent", () => {
it("should create copies of itself", () => {
@@ -182,4 +182,38 @@ describe("MatrixEvent", () => {
expect(encryptedEvent.getType()).toEqual("m.room.message");
});
});
describe("replyEventId", () => {
it("should ignore 'm.relates_to' from encrypted content even if cleartext lacks one", async () => {
const eventId = "test_encrypted_event";
const encryptedEvent = new MatrixEvent({
event_id: eventId,
type: "m.room.encrypted",
content: {
ciphertext: "secrets",
},
});
const crypto = {
decryptEvent: jest.fn().mockImplementationOnce(() => {
return Promise.resolve<IEventDecryptionResult>({
clearEvent: {
type: "m.room.message",
content: {
"m.relates_to": {
"m.in_reply_to": {
event_id: "!anotherEvent",
},
},
},
},
});
}),
} as unknown as Crypto;
await encryptedEvent.attemptDecryption(crypto);
expect(encryptedEvent.getType()).toEqual("m.room.message");
expect(encryptedEvent.replyEventId).toBeUndefined();
});
});
});
+204 -17
View File
@@ -1,6 +1,6 @@
import * as utils from "../test-utils/test-utils";
import { IActionsObject, PushProcessor } from "../../src/pushprocessor";
import { EventType, IContent, MatrixClient, MatrixEvent } from "../../src";
import { ConditionKind, EventType, IContent, MatrixClient, MatrixEvent, PushRuleActionName } from "../../src";
describe("NotificationService", function () {
const testUserId = "@ali:matrix.org";
@@ -11,6 +11,23 @@ describe("NotificationService", function () {
let pushProcessor: PushProcessor;
const msc3914RoomCallRule = {
rule_id: ".org.matrix.msc3914.rule.room.call",
default: true,
enabled: true,
conditions: [
{
kind: "event_match",
key: "type",
pattern: "org.matrix.msc3401.call",
},
{
kind: "call_started",
},
],
actions: ["notify", { set_tweak: "sound", value: "default" }],
};
// These would be better if individual rules were configured in the tests themselves.
const matrixClient = {
getRoom: function () {
@@ -163,26 +180,11 @@ describe("NotificationService", function () {
enabled: true,
rule_id: ".m.rule.room_one_to_one",
},
{
rule_id: ".org.matrix.msc3914.rule.room.call",
default: true,
enabled: true,
conditions: [
{
kind: "event_match",
key: "type",
pattern: "org.matrix.msc3401.call",
},
{
kind: "call_started",
},
],
actions: ["notify", { set_tweak: "sound", value: "default" }],
},
],
room: [],
sender: [],
underride: [
msc3914RoomCallRule,
{
actions: ["dont-notify"],
conditions: [
@@ -287,6 +289,13 @@ describe("NotificationService", function () {
expect(actions.tweaks.highlight).toEqual(true);
});
// TODO: This is not spec compliant behaviour.
//
// See https://spec.matrix.org/v1.5/client-server-api/#conditions-1 which
// describes pattern should glob:
//
// 1. * matches 0 or more characters;
// 2. ? matches exactly one character
it("should bing on character group ([abc]) bing words.", function () {
testEvent.event.content!.body = "Ping!";
let actions = pushProcessor.actionsForEvent(testEvent);
@@ -296,12 +305,14 @@ describe("NotificationService", function () {
expect(actions.tweaks.highlight).toEqual(true);
});
// TODO: This is not spec compliant behaviour. (See above.)
it("should bing on character range ([a-z]) bing words.", function () {
testEvent.event.content!.body = "I ate 6 pies";
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
// TODO: This is not spec compliant behaviour. (See above.)
it("should bing on character negation ([!a]) bing words.", function () {
testEvent.event.content!.body = "boke";
let actions = pushProcessor.actionsForEvent(testEvent);
@@ -330,6 +341,8 @@ describe("NotificationService", function () {
// invalid
it("should gracefully handle bad input.", function () {
// The following body is an object (not a string) and thus is invalid
// for matching against.
testEvent.event.content!.body = { foo: "bar" };
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(false);
@@ -493,4 +506,178 @@ describe("NotificationService", function () {
});
});
});
describe("Test exact event matching", () => {
it.each([
// Simple string matching.
{ value: "bar", eventValue: "bar", expected: true },
// Matches are case-sensitive.
{ value: "bar", eventValue: "BAR", expected: false },
// Matches must match the full string.
{ value: "bar", eventValue: "barbar", expected: false },
// Values should not be type-coerced.
{ value: "bar", eventValue: true, expected: false },
{ value: "bar", eventValue: 1, expected: false },
{ value: "bar", eventValue: false, expected: false },
// Boolean matching.
{ value: true, eventValue: true, expected: true },
{ value: false, eventValue: false, expected: true },
// Types should not be coerced.
{ value: true, eventValue: "true", expected: false },
{ value: true, eventValue: 1, expected: false },
{ value: false, eventValue: null, expected: false },
// Null matching.
{ value: null, eventValue: null, expected: true },
// Types should not be coerced
{ value: null, eventValue: false, expected: false },
{ value: null, eventValue: 0, expected: false },
{ value: null, eventValue: "", expected: false },
{ value: null, eventValue: undefined, expected: false },
// Compound values should never be matched.
{ value: "bar", eventValue: ["bar"], expected: false },
{ value: "bar", eventValue: { bar: true }, expected: false },
{ value: true, eventValue: [true], expected: false },
{ value: true, eventValue: { true: true }, expected: false },
{ value: null, eventValue: [], expected: false },
{ value: null, eventValue: {}, expected: false },
])("test $value against $eventValue", ({ value, eventValue, expected }) => {
matrixClient.pushRules! = {
global: {
override: [
{
actions: [PushRuleActionName.Notify],
conditions: [
{
kind: ConditionKind.EventPropertyIs,
key: "content.foo",
value: value,
},
],
default: true,
enabled: true,
rule_id: ".m.rule.test",
},
],
},
};
testEvent = utils.mkEvent({
type: "m.room.message",
room: testRoomId,
user: "@alfred:localhost",
event: true,
content: {
foo: eventValue,
},
});
const actions = pushProcessor.actionsForEvent(testEvent);
if (expected) {
expect(actions?.notify).toBeTruthy();
} else {
expect(actions?.notify).toBeFalsy();
}
});
});
it.each([
// The properly escaped key works.
{ key: "content.m\\.test.foo", pattern: "bar", expected: true },
// An unescaped version does not match.
{ key: "content.m.test.foo", pattern: "bar", expected: false },
// Over escaping does not match.
{ key: "content.m\\.test\\.foo", pattern: "bar", expected: false },
// Escaping backslashes should match.
{ key: "content.m\\\\example", pattern: "baz", expected: true },
// An unnecessary escape sequence leaves the backslash and still matches.
{ key: "content.m\\example", pattern: "baz", expected: true },
])("test against escaped dotted paths '$key'", ({ key, pattern, expected }) => {
testEvent = utils.mkEvent({
type: "m.room.message",
room: testRoomId,
user: "@alfred:localhost",
event: true,
content: {
// A dot in the field name.
"m.test": { foo: "bar" },
// A backslash in a field name.
"m\\example": "baz",
},
});
expect(
pushProcessor.ruleMatchesEvent(
{
rule_id: "rule1",
actions: [],
conditions: [
{
kind: ConditionKind.EventMatch,
key: key,
pattern: pattern,
},
],
default: false,
enabled: true,
},
testEvent,
),
).toBe(expected);
});
describe("getPushRuleById()", () => {
it("returns null when rule id is not in rule set", () => {
expect(pushProcessor.getPushRuleById("non-existant-rule")).toBeNull();
});
it("returns push rule when it is found in rule set", () => {
expect(pushProcessor.getPushRuleById(".org.matrix.msc3914.rule.room.call")).toEqual(msc3914RoomCallRule);
});
});
describe("getPushRuleAndKindById()", () => {
it("returns null when rule id is not in rule set", () => {
expect(pushProcessor.getPushRuleAndKindById("non-existant-rule")).toBeNull();
});
it("returns push rule when it is found in rule set", () => {
expect(pushProcessor.getPushRuleAndKindById(".org.matrix.msc3914.rule.room.call")).toEqual({
kind: "underride",
rule: msc3914RoomCallRule,
});
});
});
});
describe("Test PushProcessor.partsForDottedKey", function () {
it.each([
// A field with no dots.
["m", ["m"]],
// Simple dotted fields.
["m.foo", ["m", "foo"]],
["m.foo.bar", ["m", "foo", "bar"]],
// Backslash is used as an escape character.
["m\\.foo", ["m.foo"]],
["m\\\\.foo", ["m\\", "foo"]],
["m\\\\\\.foo", ["m\\.foo"]],
["m\\\\\\\\.foo", ["m\\\\", "foo"]],
["m\\foo", ["m\\foo"]],
["m\\\\foo", ["m\\foo"]],
["m\\\\\\foo", ["m\\\\foo"]],
["m\\\\\\\\foo", ["m\\\\foo"]],
// Ensure that escapes at the end don't cause issues.
["m.foo\\", ["m", "foo\\"]],
["m.foo\\\\", ["m", "foo\\"]],
["m.foo\\.", ["m", "foo."]],
["m.foo\\\\.", ["m", "foo\\", ""]],
["m.foo\\\\\\.", ["m", "foo\\."]],
// Empty parts (corresponding to properties which are an empty string) are allowed.
[".m", ["", "m"]],
["..m", ["", "", "m"]],
["m.", ["m", ""]],
["m..", ["m", "", ""]],
["m..foo", ["m", "", "foo"]],
])("partsFotDottedKey for %s", (path: string, expected: string[]) => {
expect(PushProcessor.partsForDottedKey(path)).toStrictEqual(expected);
});
});
@@ -16,7 +16,7 @@ limitations under the License.
import "../../olm-loader";
import { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from "../../../src/rendezvous/channels";
import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "../../../src/rendezvous/channels";
import { decodeBase64 } from "../../../src/crypto/olmlib";
import { DummyTransport } from "./DummyTransport";
@@ -24,7 +24,7 @@ function makeTransport(name: string) {
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: "dummy" });
}
describe("ECDHv1", function () {
describe("ECDHv2", function () {
beforeAll(async function () {
await global.Olm.init();
});
@@ -37,9 +37,9 @@ describe("ECDHv1", function () {
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
@@ -62,9 +62,9 @@ describe("ECDHv1", function () {
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
@@ -87,9 +87,9 @@ describe("ECDHv1", function () {
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
@@ -109,9 +109,9 @@ describe("ECDHv1", function () {
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
@@ -135,9 +135,9 @@ describe("ECDHv1", function () {
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
@@ -159,7 +159,7 @@ describe("ECDHv1", function () {
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
await bobTransport.send({ iv: "dummy", ciphertext: "dummy" });
+22 -22
View File
@@ -19,9 +19,9 @@ import MockHttpBackend from "matrix-mock-request";
import "../../olm-loader";
import { MSC3906Rendezvous, RendezvousCode, RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
import {
ECDHv1RendezvousCode,
ECDHv2RendezvousCode as ECDHRendezvousCode,
MSC3903ECDHPayload,
MSC3903ECDHv1RendezvousChannel,
MSC3903ECDHv2RendezvousChannel as MSC3903ECDHRendezvousChannel,
} from "../../../src/rendezvous/channels";
import { MatrixClient } from "../../../src";
import {
@@ -126,7 +126,7 @@ describe("Rendezvous", function () {
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
expect(aliceRz.code).toBeUndefined();
@@ -143,7 +143,7 @@ describe("Rendezvous", function () {
const code = JSON.parse(aliceRz.code!) as RendezvousCode;
expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE);
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256");
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256");
expect(code.rendezvous?.transport.type).toEqual("org.matrix.msc3886.http.v1");
expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri).toEqual(
"https://fallbackserver/rz/123",
@@ -181,11 +181,11 @@ describe("Rendezvous", function () {
msc3882Enabled: false,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
@@ -193,7 +193,7 @@ describe("Rendezvous", function () {
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
@@ -235,11 +235,11 @@ describe("Rendezvous", function () {
msc3882Enabled: true,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
@@ -247,7 +247,7 @@ describe("Rendezvous", function () {
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
@@ -293,11 +293,11 @@ describe("Rendezvous", function () {
msc3882Enabled: true,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
@@ -305,7 +305,7 @@ describe("Rendezvous", function () {
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
@@ -351,11 +351,11 @@ describe("Rendezvous", function () {
msc3882Enabled: true,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
@@ -363,7 +363,7 @@ describe("Rendezvous", function () {
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
@@ -411,11 +411,11 @@ describe("Rendezvous", function () {
msc3882Enabled: true,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
@@ -423,7 +423,7 @@ describe("Rendezvous", function () {
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
@@ -485,11 +485,11 @@ describe("Rendezvous", function () {
master: "mmmmm",
},
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
@@ -497,7 +497,7 @@ describe("Rendezvous", function () {
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
+1 -1
View File
@@ -59,7 +59,7 @@ describe("MemoryStore", () => {
await store.deleteAllData();
// empty object
expect(store.accountData).toEqual({});
expect(store.accountData).toEqual(new Map());
});
});
});
+102
View File
@@ -24,9 +24,12 @@ import {
lexicographicCompare,
nextString,
prevString,
recursiveMapToObject,
simpleRetryOperation,
stringToBase,
sortEventsByLatestContentTimestamp,
safeSet,
MapWithDefault,
} from "../../src/utils";
import { logger } from "../../src/logger";
import { mkMessage } from "../test-utils/test-utils";
@@ -606,6 +609,105 @@ describe("utils", function () {
});
});
describe("recursiveMapToObject", () => {
it.each([
// empty map
{
map: new Map(),
expected: {},
},
// one level map
{
map: new Map<any, any>([
["key1", "value 1"],
["key2", 23],
["key3", undefined],
["key4", null],
["key5", [1, 2, 3]],
]),
expected: { key1: "value 1", key2: 23, key3: undefined, key4: null, key5: [1, 2, 3] },
},
// two level map
{
map: new Map<any, any>([
[
"key1",
new Map<any, any>([
["key1_1", "value 1"],
["key1_2", "value 1.2"],
]),
],
["key2", "value 2"],
]),
expected: { key1: { key1_1: "value 1", key1_2: "value 1.2" }, key2: "value 2" },
},
// multi level map
{
map: new Map<any, any>([
["key1", new Map<any, any>([["key1_1", new Map<any, any>([["key1_1_1", "value 1.1.1"]])]])],
]),
expected: { key1: { key1_1: { key1_1_1: "value 1.1.1" } } },
},
// list of maps
{
map: new Map<any, any>([
[
"key1",
[new Map<any, any>([["key1_1", "value 1.1"]]), new Map<any, any>([["key1_2", "value 1.2"]])],
],
]),
expected: { key1: [{ key1_1: "value 1.1" }, { key1_2: "value 1.2" }] },
},
// map → array → array → map
{
map: new Map<any, any>([["key1", [[new Map<any, any>([["key2", "value 2"]])]]]]),
expected: {
key1: [
[
{
key2: "value 2",
},
],
],
},
},
])("%# should convert the value", ({ map, expected }) => {
expect(recursiveMapToObject(map)).toStrictEqual(expected);
});
});
describe("safeSet", () => {
it("should set a value", () => {
const obj = {};
safeSet(obj, "testProp", "test value");
expect(obj).toEqual({ testProp: "test value" });
});
it.each(["__proto__", "prototype", "constructor"])("should raise an error when setting »%s«", (prop) => {
expect(() => {
safeSet({}, prop, "teset value");
}).toThrow("Trying to modify prototype or constructor");
});
});
describe("MapWithDefault", () => {
it("getOrCreate should create the value if it does not exist", () => {
const newValue = {};
const map = new MapWithDefault(() => newValue);
// undefined before getOrCreate
expect(map.get("test")).toBeUndefined();
expect(map.getOrCreate("test")).toBe(newValue);
// default value after getOrCreate
expect(map.get("test")).toBe(newValue);
// test that it always returns the same value
expect(map.getOrCreate("test")).toBe(newValue);
});
});
describe("sleep", () => {
it("resolves", async () => {
await utils.sleep(0);
+23 -7
View File
@@ -688,15 +688,15 @@ describe("Group Call", function () {
expect(client1.sendToDevice.mock.calls[0][0]).toBe("m.call.invite");
const toDeviceCallContent = client1.sendToDevice.mock.calls[0][1];
expect(Object.keys(toDeviceCallContent).length).toBe(1);
expect(Object.keys(toDeviceCallContent)[0]).toBe(FAKE_USER_ID_2);
expect(toDeviceCallContent.size).toBe(1);
expect(toDeviceCallContent.has(FAKE_USER_ID_2)).toBe(true);
const toDeviceBobDevices = toDeviceCallContent[FAKE_USER_ID_2];
expect(Object.keys(toDeviceBobDevices).length).toBe(1);
expect(Object.keys(toDeviceBobDevices)[0]).toBe(FAKE_DEVICE_ID_2);
const toDeviceBobDevices = toDeviceCallContent.get(FAKE_USER_ID_2);
expect(toDeviceBobDevices?.size).toBe(1);
expect(toDeviceBobDevices?.has(FAKE_DEVICE_ID_2)).toBe(true);
const bobDeviceMessage = toDeviceBobDevices[FAKE_DEVICE_ID_2];
expect(bobDeviceMessage.conf_id).toBe(FAKE_CONF_ID);
const bobDeviceMessage = toDeviceBobDevices?.get(FAKE_DEVICE_ID_2);
expect(bobDeviceMessage?.conf_id).toBe(FAKE_CONF_ID);
} finally {
await Promise.all([groupCall1.leave(), groupCall2.leave()]);
}
@@ -882,11 +882,27 @@ describe("Group Call", function () {
expect(await groupCall.setMicrophoneMuted(false)).toBe(false);
});
it("returns false when no permission for audio stream", async () => {
const groupCall = await createAndEnterGroupCall(mockClient, room);
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValueOnce(
new Error("No Permission"),
);
expect(await groupCall.setMicrophoneMuted(false)).toBe(false);
});
it("returns false when unmuting video with no video device", async () => {
const groupCall = await createAndEnterGroupCall(mockClient, room);
jest.spyOn(mockClient.getMediaHandler(), "hasVideoDevice").mockResolvedValue(false);
expect(await groupCall.setLocalVideoMuted(false)).toBe(false);
});
it("returns false when no permission for video stream", async () => {
const groupCall = await createAndEnterGroupCall(mockClient, room);
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValueOnce(
new Error("No Permission"),
);
expect(await groupCall.setLocalVideoMuted(false)).toBe(false);
});
});
describe("remote muting", () => {
+10
View File
@@ -242,6 +242,11 @@ describe("Media Handler", function () {
);
expect(await mediaHandler.hasAudioDevice()).toEqual(false);
});
it("returns false if the system not permitting access audio inputs", async () => {
mockMediaDevices.enumerateDevices.mockRejectedValueOnce(new Error("No Permission"));
expect(await mediaHandler.hasAudioDevice()).toEqual(false);
});
});
describe("hasVideoDevice", () => {
@@ -255,6 +260,11 @@ describe("Media Handler", function () {
);
expect(await mediaHandler.hasVideoDevice()).toEqual(false);
});
it("returns false if the system not permitting access video inputs", async () => {
mockMediaDevices.enumerateDevices.mockRejectedValueOnce(new Error("No Permission"));
expect(await mediaHandler.hasVideoDevice()).toEqual(false);
});
});
describe("getUserMediaStream", () => {
+1 -1
View File
@@ -20,5 +20,5 @@ export interface IIdentityServerProvider {
* for the associated client.
* @returns Promise which resolves to the access token.
*/
getAccessToken(): Promise<string>;
getAccessToken(): Promise<string | null>;
}
+17
View File
@@ -62,6 +62,7 @@ export function isDmMemberCountCondition(condition: AnyMemberCountCondition): bo
export enum ConditionKind {
EventMatch = "event_match",
EventPropertyIs = "event_property_is",
ContainsDisplayName = "contains_display_name",
RoomMemberCount = "room_member_count",
SenderNotificationPermission = "sender_notification_permission",
@@ -77,9 +78,16 @@ export interface IPushRuleCondition<N extends ConditionKind | string> {
export interface IEventMatchCondition extends IPushRuleCondition<ConditionKind.EventMatch> {
key: string;
pattern?: string;
// Note that value property is an optimization for patterns which do not do
// any globbing and when the key is not "content.body".
value?: string;
}
export interface IEventPropertyIsCondition extends IPushRuleCondition<ConditionKind.EventPropertyIs> {
key: string;
value: string | boolean | null | number;
}
export interface IContainsDisplayNameCondition extends IPushRuleCondition<ConditionKind.ContainsDisplayName> {
// no additional fields
}
@@ -105,6 +113,7 @@ export interface ICallStartedPrefixCondition extends IPushRuleCondition<Conditio
// IPushRuleCondition<Exclude<string, ConditionKind>> unfortunately does not resolve this at the time of writing.
export type PushRuleCondition =
| IEventMatchCondition
| IEventPropertyIsCondition
| IContainsDisplayNameCondition
| IRoomMemberCountCondition
| ISenderNotificationPermissionCondition
@@ -133,6 +142,14 @@ export enum RuleId {
IncomingCall = ".m.rule.call",
SuppressNotices = ".m.rule.suppress_notices",
Tombstone = ".m.rule.tombstone",
PollStart = ".m.rule.poll_start",
PollStartUnstable = ".org.matrix.msc3930.rule.poll_start",
PollEnd = ".m.rule.poll_end",
PollEndUnstable = ".org.matrix.msc3930.rule.poll_end",
PollStartOneToOne = ".m.rule.poll_start_one_to_one",
PollStartOneToOneUnstable = ".org.matrix.msc3930.rule.poll_start_one_to_one",
PollEndOneToOne = ".m.rule.poll_end_one_to_one",
PollEndOneToOneUnstable = ".org.matrix.msc3930.rule.poll_end_one_to_one",
}
export type PushRuleSet = {
+3 -6
View File
@@ -38,7 +38,7 @@ export interface CachedReceipt {
data: Receipt;
}
export type ReceiptCache = { [eventId: string]: CachedReceipt[] };
export type ReceiptCache = Map<string, CachedReceipt[]>;
export interface ReceiptContent {
[eventId: string]: {
@@ -49,11 +49,8 @@ export interface ReceiptContent {
}
// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer.
export type Receipts = {
[receiptType: string]: {
[userId: string]: [WrappedReceipt | null, WrappedReceipt | null]; // Pair<real receipt, synthetic receipt> (both nullable)
};
};
// map: receipt type → user Id → receipt
export type Receipts = Map<string, Map<string, [real: WrappedReceipt | null, synthetic: WrappedReceipt | null]>>;
export type CachedReceiptStructure = {
eventId: string;
+3 -5
View File
@@ -21,6 +21,7 @@ import { MatrixError } from "./http-api";
import { IndexedToDeviceBatch, ToDeviceBatch, ToDeviceBatchWithTxnId, ToDevicePayload } from "./models/ToDeviceMessage";
import { MatrixScheduler } from "./scheduler";
import { SyncState } from "./sync";
import { MapWithDefault } from "./utils";
const MAX_BATCH_SIZE = 20;
@@ -122,12 +123,9 @@ export class ToDeviceMessageQueue {
* Attempts to send a batch of to-device messages.
*/
private async sendBatch(batch: IndexedToDeviceBatch): Promise<void> {
const contentMap: Record<string, Record<string, ToDevicePayload>> = {};
const contentMap: MapWithDefault<string, Map<string, ToDevicePayload>> = new MapWithDefault(() => new Map());
for (const item of batch.batch) {
if (!contentMap[item.userId]) {
contentMap[item.userId] = {};
}
contentMap[item.userId][item.deviceId] = item.payload;
contentMap.getOrCreate(item.userId).set(item.deviceId, item.payload);
}
logger.info(
+73 -41
View File
@@ -37,7 +37,7 @@ import { Filter, IFilterDefinition, IRoomEventFilter } from "./filter";
import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from "./webrtc/callEventHandler";
import { GroupCallEventHandlerEvent, GroupCallEventHandlerEventHandlerMap } from "./webrtc/groupCallEventHandler";
import * as utils from "./utils";
import { replaceParam, QueryDict, sleep } from "./utils";
import { replaceParam, QueryDict, sleep, noUnsafeEventProps } from "./utils";
import { Direction, EventTimeline } from "./models/event-timeline";
import { IActionsObject, PushProcessor } from "./pushprocessor";
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
@@ -79,7 +79,7 @@ import {
VerificationMethod,
IRoomKeyRequestBody,
} from "./crypto";
import { DeviceInfo, IDevice } from "./crypto/deviceinfo";
import { DeviceInfo } from "./crypto/deviceinfo";
import { decodeRecoveryKey } from "./crypto/recoverykey";
import { keyFromAuthData } from "./crypto/key_passphrase";
import { User, UserEvent, UserEventHandlerMap } from "./models/user";
@@ -206,6 +206,7 @@ import { LocalNotificationSettings } from "./@types/local_notifications";
import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature";
import { CryptoBackend } from "./common-crypto/CryptoBackend";
import { RUST_SDK_STORE_PREFIX } from "./rust-crypto/constants";
import { DeviceInfoMap } from "./crypto/DeviceList";
export type Store = IStore;
@@ -371,6 +372,13 @@ export interface ICreateClientOpts {
* Defaults to a built-in English handler with basic pluralisation.
*/
roomNameGenerator?: (roomId: string, state: RoomNameState) => string | null;
/**
* If true, participant can join group call without video and audio this has to be allowed. By default, a local
* media stream is needed to establish a group call.
* Default: false.
*/
isVoipWithNoMediaAllowed?: boolean;
}
export interface IMatrixClientCreateOpts extends ICreateClientOpts {
@@ -498,6 +506,8 @@ enum CrossSigningKeyType {
export type CrossSigningKeys = Record<CrossSigningKeyType, ICrossSigningKey>;
export type SendToDeviceContentMap = Map<string, Map<string, Record<string, any>>>;
export interface ISignedKey {
keys: Record<string, string>;
signatures: ISignatures;
@@ -1169,6 +1179,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public iceCandidatePoolSize = 0; // XXX: Intended private, used in code.
public idBaseUrl?: string;
public baseUrl: string;
public readonly isVoipWithNoMediaAllowed;
// Note: these are all `protected` to let downstream consumers make mistakes if they want to.
// We don't technically support this usage, but have reasons to do this.
@@ -1313,6 +1324,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.iceCandidatePoolSize = opts.iceCandidatePoolSize === undefined ? 0 : opts.iceCandidatePoolSize;
this.supportsCallTransfer = opts.supportsCallTransfer || false;
this.fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false;
this.isVoipWithNoMediaAllowed = opts.isVoipWithNoMediaAllowed || false;
if (opts.useE2eForGroupCall !== undefined) this.useE2eForGroupCall = opts.useE2eForGroupCall;
@@ -1880,6 +1892,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
throw new Error(`Cannot find room ${roomId}`);
}
// Because without Media section a WebRTC connection is not possible, so need a RTCDataChannel to set up a
// no media WebRTC connection anyway.
return new GroupCall(
this,
room,
@@ -1887,8 +1901,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
isPtt,
intent,
undefined,
dataChannelsEnabled,
dataChannelsEnabled || this.isVoipWithNoMediaAllowed,
dataChannelOptions,
this.isVoipWithNoMediaAllowed,
).create();
}
@@ -2231,7 +2246,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @returns A promise which resolves to a map userId-\>deviceId-\>{@link DeviceInfo}
*/
public downloadKeys(userIds: string[], forceDownload?: boolean): Promise<Record<string, Record<string, IDevice>>> {
public downloadKeys(userIds: string[], forceDownload?: boolean): Promise<DeviceInfoMap> {
if (!this.crypto) {
return Promise.reject(new Error("End-to-end encryption disabled"));
}
@@ -3748,9 +3763,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
const deviceInfos = await this.crypto.downloadKeys(userIds);
const devicesByUser: Record<string, DeviceInfo[]> = {};
for (const [userId, devices] of Object.entries(deviceInfos)) {
devicesByUser[userId] = Object.values(devices);
const devicesByUser: Map<string, DeviceInfo[]> = new Map();
for (const [userId, devices] of deviceInfos) {
devicesByUser.set(userId, Array.from(devices.values()));
}
// XXX: Private member access
@@ -4130,7 +4145,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public setPowerLevel(
roomId: string,
userId: string | string[],
powerLevel: number,
powerLevel: number | undefined,
event: MatrixEvent | null,
): Promise<ISendEventResponse> {
let content = {
@@ -4141,13 +4156,16 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// existing client state with a failed power level change
content = utils.deepCopy(event.getContent());
}
if (Array.isArray(userId)) {
for (const user of userId) {
const users = Array.isArray(userId) ? userId : [userId];
for (const user of users) {
if (powerLevel == null) {
delete content.users[user];
} else {
content.users[user] = powerLevel;
}
} else {
content.users[userId] = powerLevel;
}
const path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", {
$roomId: roomId,
});
@@ -5962,6 +5980,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const token = res.next_token;
const matrixEvents: MatrixEvent[] = [];
res.notifications = res.notifications.filter(noUnsafeEventProps);
for (let i = 0; i < res.notifications.length; i++) {
const notification = res.notifications[i];
const event = this.getEventMapper()(notification.event);
@@ -6008,11 +6028,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
.then((res) => {
if (res.state) {
const roomState = eventTimeline.getState(dir)!;
const stateEvents = res.state.map(this.getEventMapper());
const stateEvents = res.state.filter(noUnsafeEventProps).map(this.getEventMapper());
roomState.setUnknownStateEvents(stateEvents);
}
const token = res.end;
const matrixEvents = res.chunk.map(this.getEventMapper());
const matrixEvents = res.chunk.filter(noUnsafeEventProps).map(this.getEventMapper());
const timelineSet = eventTimeline.getTimelineSet();
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
@@ -6044,7 +6064,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
})
.then(async (res) => {
const mapper = this.getEventMapper();
const matrixEvents = res.chunk.map(mapper);
const matrixEvents = res.chunk.filter(noUnsafeEventProps).map(mapper);
// Process latest events first
for (const event of matrixEvents.slice().reverse()) {
@@ -6092,11 +6112,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
.then((res) => {
if (res.state) {
const roomState = eventTimeline.getState(dir)!;
const stateEvents = res.state.map(this.getEventMapper());
const stateEvents = res.state.filter(noUnsafeEventProps).map(this.getEventMapper());
roomState.setUnknownStateEvents(stateEvents);
}
const token = res.end;
const matrixEvents = res.chunk.map(this.getEventMapper());
const matrixEvents = res.chunk.filter(noUnsafeEventProps).map(this.getEventMapper());
const timelineSet = eventTimeline.getTimelineSet();
const [timelineEvents] = room.partitionThreadedEvents(matrixEvents);
@@ -8496,10 +8516,22 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/
public getPushRules(): Promise<IPushRules> {
return this.http.authedRequest<IPushRules>(Method.Get, "/pushrules/").then((rules: IPushRules) => {
return PushProcessor.rewriteDefaultRules(rules);
this.setPushRules(rules);
return this.pushRules!;
});
}
/**
* Update the push rules for the account. This should be called whenever
* updated push rules are available.
*/
public setPushRules(rules: IPushRules): void {
// Fix-up defaults, if applicable.
this.pushRules = PushProcessor.rewriteDefaultRules(rules);
// Pre-calculate any necessary caches.
this.pushProcessor.updateCachedPushRuleKeys(this.pushRules);
}
/**
* @returns Promise which resolves: an empty object `{}`
* @returns Rejects: with an error response.
@@ -8746,18 +8778,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
email: string,
clientSecret: string,
sendAttempt: number,
nextLink: string,
nextLink?: string,
identityAccessToken?: string,
): Promise<any> {
// TODO: Types
const params = {
): Promise<IRequestTokenResponse> {
const params: Record<string, string> = {
client_secret: clientSecret,
email: email,
send_attempt: sendAttempt?.toString(),
next_link: nextLink,
};
if (nextLink) {
params.next_link = nextLink;
}
return this.http.idServerRequest(
return this.http.idServerRequest<IRequestTokenResponse>(
Method.Post,
"/validate/email/requestToken",
params,
@@ -8788,7 +8821,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param identityAccessToken - The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @returns Promise which resolves: TODO
* @returns Promise which resolves to an object with a sid string
* @returns Rejects: with an error response.
* @throws Error if no identity server is set
*/
@@ -8797,19 +8830,20 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
phoneNumber: string,
clientSecret: string,
sendAttempt: number,
nextLink: string,
nextLink?: string,
identityAccessToken?: string,
): Promise<any> {
// TODO: Types
const params = {
): Promise<IRequestMsisdnTokenResponse> {
const params: Record<string, string> = {
client_secret: clientSecret,
country: phoneCountry,
phone_number: phoneNumber,
send_attempt: sendAttempt?.toString(),
next_link: nextLink,
};
if (nextLink) {
params.next_link = nextLink;
}
return this.http.idServerRequest(
return this.http.idServerRequest<IRequestMsisdnTokenResponse>(
Method.Post,
"/validate/msisdn/requestToken",
params,
@@ -9100,24 +9134,22 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* supplied.
* @returns Promise which resolves: to an empty object `{}`
*/
public sendToDevice(
eventType: string,
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
txnId?: string,
): Promise<{}> {
public sendToDevice(eventType: string, contentMap: SendToDeviceContentMap, txnId?: string): Promise<{}> {
const path = utils.encodeUri("/sendToDevice/$eventType/$txnId", {
$eventType: eventType,
$txnId: txnId ? txnId : this.makeTxnId(),
});
const body = {
messages: contentMap,
messages: utils.recursiveMapToObject(contentMap),
};
const targets = Object.keys(contentMap).reduce<Record<string, string[]>>((obj, key) => {
obj[key] = Object.keys(contentMap[key]);
return obj;
}, {});
const targets = new Map<string, string[]>();
for (const [userId, deviceMessages] of contentMap) {
targets.set(userId, Array.from(deviceMessages.keys()));
}
logger.log(`PUT ${path}`, targets);
return this.http.authedRequest(Method.Put, path, undefined, body);
+8 -7
View File
@@ -58,7 +58,8 @@ export enum TrackingStatus {
UpToDate,
}
export type DeviceInfoMap = Record<string, Record<string, DeviceInfo>>;
// user-Id → device-Id → DeviceInfo
export type DeviceInfoMap = Map<string, Map<string, DeviceInfo>>;
type EmittedEvents = CryptoEvent.WillUpdateDevices | CryptoEvent.DevicesUpdated | CryptoEvent.UserCrossSigningUpdated;
@@ -301,13 +302,13 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
* @returns userId-\>deviceId-\>{@link DeviceInfo}.
*/
private getDevicesFromStore(userIds: string[]): DeviceInfoMap {
const stored: DeviceInfoMap = {};
userIds.forEach((u) => {
stored[u] = {};
const devices = this.getStoredDevicesForUser(u) || [];
devices.forEach(function (dev) {
stored[u][dev.deviceId] = dev;
const stored: DeviceInfoMap = new Map();
userIds.forEach((userId) => {
const deviceMap = new Map();
this.getStoredDevicesForUser(userId)?.forEach(function (device) {
deviceMap.set(device.deviceId, device);
});
stored.set(userId, deviceMap);
});
return stored;
}
+3 -3
View File
@@ -61,7 +61,7 @@ export class EncryptionSetupBuilder {
* @param accountData - pre-existing account data, will only be read, not written.
* @param delegateCryptoCallbacks - crypto callbacks to delegate to if the key isn't in cache yet
*/
public constructor(accountData: Record<string, MatrixEvent>, delegateCryptoCallbacks?: ICryptoCallbacks) {
public constructor(accountData: Map<string, MatrixEvent>, delegateCryptoCallbacks?: ICryptoCallbacks) {
this.accountDataClientAdapter = new AccountDataClientAdapter(accountData);
this.crossSigningCallbacks = new CrossSigningCallbacks();
this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks);
@@ -246,7 +246,7 @@ class AccountDataClientAdapter
/**
* @param existingValues - existing account data
*/
public constructor(private readonly existingValues: Record<string, MatrixEvent>) {
public constructor(private readonly existingValues: Map<string, MatrixEvent>) {
super();
}
@@ -265,7 +265,7 @@ class AccountDataClientAdapter
if (modifiedValue) {
return modifiedValue;
}
const existingValue = this.existingValues[type];
const existingValue = this.existingValues.get(type);
if (existingValue) {
return existingValue.getContent();
}
+5 -6
View File
@@ -21,6 +21,7 @@ import { MatrixClient } from "../client";
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "./index";
import { CryptoStore, OutgoingRoomKeyRequest } from "./store/base";
import { EventType, ToDeviceMessageId } from "../@types/event";
import { MapWithDefault } from "../utils";
/**
* Internal module. Management of outgoing room key requests.
@@ -460,15 +461,13 @@ export class OutgoingRoomKeyRequestManager {
recipients: IRoomKeyRequestRecipient[],
txnId?: string,
): Promise<{}> {
const contentMap: Record<string, Record<string, Record<string, any>>> = {};
const contentMap = new MapWithDefault<string, Map<string, Record<string, any>>>(() => new Map());
for (const recip of recipients) {
if (!contentMap[recip.userId]) {
contentMap[recip.userId] = {};
}
contentMap[recip.userId][recip.deviceId] = {
const userDeviceMap = contentMap.getOrCreate(recip.userId);
userDeviceMap.set(recip.deviceId, {
...message,
[ToDeviceMessageId]: uuidv4(),
};
});
}
return this.baseApis.sendToDevice(EventType.RoomKeyRequest, contentMap, txnId);
+12 -18
View File
@@ -367,13 +367,11 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
requesting_device_id: this.baseApis.deviceId,
request_id: requestId,
};
const toDevice: Record<string, typeof cancelData> = {};
const toDevice: Map<string, typeof cancelData> = new Map();
for (const device of devices) {
toDevice[device] = cancelData;
toDevice.set(device, cancelData);
}
this.baseApis.sendToDevice("m.secret.request", {
[this.baseApis.getUserId()!]: toDevice,
});
this.baseApis.sendToDevice("m.secret.request", new Map([[this.baseApis.getUserId()!, toDevice]]));
// and reject the promise so that anyone waiting on it will be
// notified
@@ -388,14 +386,12 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
request_id: requestId,
[ToDeviceMessageId]: uuidv4(),
};
const toDevice: Record<string, typeof requestData> = {};
const toDevice: Map<string, typeof requestData> = new Map();
for (const device of devices) {
toDevice[device] = requestData;
toDevice.set(device, requestData);
}
logger.info(`Request secret ${name} from ${devices}, id ${requestId}`);
this.baseApis.sendToDevice("m.secret.request", {
[this.baseApis.getUserId()!]: toDevice,
});
this.baseApis.sendToDevice("m.secret.request", new Map([[this.baseApis.getUserId()!, toDevice]]));
return {
requestId,
@@ -469,9 +465,11 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
ciphertext: {},
[ToDeviceMessageId]: uuidv4(),
};
await olmlib.ensureOlmSessionsForDevices(this.baseApis.crypto!.olmDevice, this.baseApis, {
[sender]: [this.baseApis.getStoredDevice(sender, deviceId)!],
});
await olmlib.ensureOlmSessionsForDevices(
this.baseApis.crypto!.olmDevice,
this.baseApis,
new Map([[sender, [this.baseApis.getStoredDevice(sender, deviceId)!]]]),
);
await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this.baseApis.getUserId()!,
@@ -481,11 +479,7 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
this.baseApis.getStoredDevice(sender, deviceId)!,
payload,
);
const contentMap = {
[sender]: {
[deviceId]: encryptedContent,
},
};
const contentMap = new Map([[sender, new Map([[deviceId, encryptedContent]])]]);
logger.info(`Sending ${content.name} secret for ${deviceId}`);
this.baseApis.sendToDevice("m.room.encrypted", contentMap);
+3 -6
View File
@@ -26,6 +26,7 @@ import { IContent, MatrixEvent, RoomMember } from "../../matrix";
import { Crypto, IEncryptedContent, IEventDecryptionResult, IncomingRoomKeyRequest } from "..";
import { DeviceInfo } from "../deviceinfo";
import { IRoomEncryption } from "../RoomList";
import { DeviceInfoMap } from "../DeviceList";
/**
* Map of registered encryption algorithm classes. A map from string to {@link EncryptionAlgorithm} class
@@ -195,7 +196,7 @@ export abstract class DecryptionAlgorithm {
}
public onRoomKeyWithheldEvent?(event: MatrixEvent): Promise<void>;
public sendSharedHistoryInboundSessions?(devicesByUser: Record<string, DeviceInfo[]>): Promise<void>;
public sendSharedHistoryInboundSessions?(devicesByUser: Map<string, DeviceInfo[]>): Promise<void>;
}
/**
@@ -241,11 +242,7 @@ export class UnknownDeviceError extends Error {
* @param msg - message describing the problem
* @param devices - set of unknown devices per user we're warning about
*/
public constructor(
msg: string,
public readonly devices: Record<string, Record<string, object>>,
public event?: MatrixEvent,
) {
public constructor(msg: string, public readonly devices: DeviceInfoMap, public event?: MatrixEvent) {
super(msg);
this.name = "UnknownDeviceError";
this.devices = devices;
File diff suppressed because it is too large Load Diff
+4 -6
View File
@@ -25,7 +25,7 @@ import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
import { DeviceInfo } from "./deviceinfo";
import { DeviceTrustLevel } from "./CrossSigning";
import { keyFromPassphrase } from "./key_passphrase";
import { sleep } from "../utils";
import { safeSet, sleep } from "../utils";
import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
import { encodeRecoveryKey } from "./recoverykey";
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes";
@@ -498,9 +498,7 @@ export class BackupManager {
const rooms: IKeyBackup["rooms"] = {};
for (const session of sessions) {
const roomId = session.sessionData!.room_id;
if (rooms[roomId] === undefined) {
rooms[roomId] = { sessions: {} };
}
safeSet(rooms, roomId, rooms[roomId] || { sessions: {} });
const sessionData = this.baseApis.crypto!.olmDevice.exportInboundGroupSession(
session.senderKey,
@@ -517,12 +515,12 @@ export class BackupManager {
undefined;
const verified = this.baseApis.crypto!.checkDeviceInfoTrust(userId!, device).isVerified();
rooms[roomId]["sessions"][session.sessionId] = {
safeSet(rooms[roomId]["sessions"], session.sessionId, {
first_message_index: sessionData.first_known_index,
forwarded_count: forwardedCount,
is_verified: verified,
session_data: await this.algorithm!.encryptSession(sessionData),
};
});
}
await this.baseApis.sendKeyBackup(undefined, undefined, this.backupInfo!.version, { rooms });
+29 -20
View File
@@ -90,6 +90,7 @@ import { ISignatures } from "../@types/signed";
import { IMessage } from "./algorithms/olm";
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
import { RoomState, RoomStateEvent } from "../models/room-state";
import { MapWithDefault, recursiveMapToObject } from "../utils";
const DeviceVerification = DeviceInfo.DeviceVerification;
@@ -399,7 +400,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// deviceId: 1234567890000,
// },
// }
private lastNewSessionForced: Record<string, Record<string, number>> = {};
// Map: user Id → device Id → timestamp
private lastNewSessionForced: MapWithDefault<string, MapWithDefault<string, number>> = new MapWithDefault(
() => new MapWithDefault(() => 0),
);
// This flag will be unset whilst the client processes a sync response
// so that we don't start requesting keys until we've actually finished
@@ -2690,11 +2694,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
public ensureOlmSessionsForUsers(
users: string[],
force?: boolean,
): Promise<Record<string, Record<string, olmlib.IOlmSessionResult>>> {
const devicesByUser: Record<string, DeviceInfo[]> = {};
): Promise<Map<string, Map<string, olmlib.IOlmSessionResult>>> {
// map user Id → DeviceInfo[]
const devicesByUser: Map<string, DeviceInfo[]> = new Map();
for (const userId of users) {
devicesByUser[userId] = [];
const userDevices: DeviceInfo[] = [];
devicesByUser.set(userId, userDevices);
const devices = this.getStoredDevicesForUser(userId) || [];
for (const deviceInfo of devices) {
@@ -2708,7 +2714,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
continue;
}
devicesByUser[userId].push(deviceInfo);
userDevices.push(deviceInfo);
}
}
@@ -3146,7 +3152,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
payload: encryptedContent,
});
await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, { [userId]: [deviceInfo] });
await olmlib.ensureOlmSessionsForDevices(
this.olmDevice,
this.baseApis,
new Map([[userId, [deviceInfo]]]),
);
await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this.userId,
@@ -3448,8 +3458,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// check when we last forced a new session with this device: if we've already done so
// recently, don't do it again.
this.lastNewSessionForced[sender] = this.lastNewSessionForced[sender] || {};
const lastNewSessionForced = this.lastNewSessionForced[sender][deviceKey] || 0;
const lastNewSessionDevices = this.lastNewSessionForced.getOrCreate(sender);
const lastNewSessionForced = lastNewSessionDevices.getOrCreate(deviceKey);
if (lastNewSessionForced + MIN_FORCE_SESSION_INTERVAL_MS > Date.now()) {
logger.debug(
"New session already forced with device " +
@@ -3482,11 +3492,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return;
}
}
const devicesByUser: Record<string, DeviceInfo[]> = {};
devicesByUser[sender] = [device];
const devicesByUser = new Map([[sender, [device]]]);
await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, true);
this.lastNewSessionForced[sender][deviceKey] = Date.now();
lastNewSessionDevices.set(deviceKey, Date.now());
// Now send a blank message on that session so the other side knows about it.
// (The keyshare request is sent in the clear so that won't do)
@@ -3513,11 +3522,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
await this.olmDevice.recordSessionProblem(deviceKey, "wedged", true);
retryDecryption();
await this.baseApis.sendToDevice("m.room.encrypted", {
[sender]: {
[device.deviceId]: encryptedContent,
},
});
await this.baseApis.sendToDevice(
"m.room.encrypted",
new Map([[sender, new Map([[device.deviceId, encryptedContent]])]]),
);
// Most of the time this probably won't be necessary since we'll have queued up a key request when
// we failed to decrypt the message and will be waiting a bit for the key to arrive before sending
@@ -3824,15 +3832,16 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @param obj - Object to which we will add a 'signatures' property
*/
public async signObject<T extends ISignableObject & object>(obj: T): Promise<void> {
const sigs = obj.signatures || {};
const sigs = new Map(Object.entries(obj.signatures || {}));
const unsigned = obj.unsigned;
delete obj.signatures;
delete obj.unsigned;
sigs[this.userId] = sigs[this.userId] || {};
sigs[this.userId]["ed25519:" + this.deviceId] = await this.olmDevice.sign(anotherjson.stringify(obj));
obj.signatures = sigs;
const userSignatures = sigs.get(this.userId) || {};
sigs.set(this.userId, userSignatures);
userSignatures["ed25519:" + this.deviceId] = await this.olmDevice.sign(anotherjson.stringify(obj));
obj.signatures = recursiveMapToObject(sigs);
if (unsigned !== undefined) obj.unsigned = unsigned;
}
}
+38 -37
View File
@@ -30,6 +30,7 @@ import { ISignatures } from "../@types/signed";
import { MatrixEvent } from "../models/event";
import { EventType } from "../@types/event";
import { IMessage } from "./algorithms/olm";
import { MapWithDefault } from "../utils";
enum Algorithm {
Olm = "m.olm.v1.curve25519-aes-sha2",
@@ -154,9 +155,11 @@ export async function getExistingOlmSessions(
olmDevice: OlmDevice,
baseApis: MatrixClient,
devicesByUser: Record<string, DeviceInfo[]>,
): Promise<[Record<string, DeviceInfo[]>, Record<string, Record<string, IExistingOlmSession>>]> {
const devicesWithoutSession: { [userId: string]: DeviceInfo[] } = {};
const sessions: { [userId: string]: { [deviceId: string]: IExistingOlmSession } } = {};
): Promise<[Map<string, DeviceInfo[]>, Map<string, Map<string, IExistingOlmSession>>]> {
// map user Id → DeviceInfo[]
const devicesWithoutSession: MapWithDefault<string, DeviceInfo[]> = new MapWithDefault(() => []);
// map user Id → device Id → IExistingOlmSession
const sessions: MapWithDefault<string, Map<string, IExistingOlmSession>> = new MapWithDefault(() => new Map());
const promises: Promise<void>[] = [];
@@ -168,14 +171,12 @@ export async function getExistingOlmSessions(
(async (): Promise<void> => {
const sessionId = await olmDevice.getSessionIdForDevice(key, true);
if (sessionId === null) {
devicesWithoutSession[userId] = devicesWithoutSession[userId] || [];
devicesWithoutSession[userId].push(deviceInfo);
devicesWithoutSession.getOrCreate(userId).push(deviceInfo);
} else {
sessions[userId] = sessions[userId] || {};
sessions[userId][deviceId] = {
sessions.getOrCreate(userId).set(deviceId, {
device: deviceInfo,
sessionId: sessionId,
};
});
}
})(),
);
@@ -210,24 +211,26 @@ export async function getExistingOlmSessions(
export async function ensureOlmSessionsForDevices(
olmDevice: OlmDevice,
baseApis: MatrixClient,
devicesByUser: Record<string, DeviceInfo[]>,
devicesByUser: Map<string, DeviceInfo[]>,
force = false,
otkTimeout?: number,
failedServers?: string[],
log = logger,
): Promise<Record<string, Record<string, IOlmSessionResult>>> {
): Promise<Map<string, Map<string, IOlmSessionResult>>> {
const devicesWithoutSession: [string, string][] = [
// [userId, deviceId], ...
];
const result: { [userId: string]: { [deviceId: string]: IExistingOlmSession } } = {};
const resolveSession: Record<string, (sessionId?: string) => void> = {};
// map user Id → device Id → IExistingOlmSession
const result: Map<string, Map<string, IExistingOlmSession>> = new Map();
// map device key → resolve session fn
const resolveSession: Map<string, (sessionId?: string) => void> = new Map();
// Mark all sessions this task intends to update as in progress. It is
// important to do this for all devices this task cares about in a single
// synchronous operation, as otherwise it is possible to have deadlocks
// where multiple tasks wait indefinitely on another task to update some set
// of common devices.
for (const [, devices] of Object.entries(devicesByUser)) {
for (const devices of devicesByUser.values()) {
for (const deviceInfo of devices) {
const key = deviceInfo.getIdentityKey();
@@ -242,17 +245,19 @@ export async function ensureOlmSessionsForDevices(
// conditions. If we find that we already have a session, then
// we'll resolve
olmDevice.sessionsInProgress[key] = new Promise((resolve) => {
resolveSession[key] = (v: any): void => {
resolveSession.set(key, (v: any): void => {
delete olmDevice.sessionsInProgress[key];
resolve(v);
};
});
});
}
}
}
for (const [userId, devices] of Object.entries(devicesByUser)) {
result[userId] = {};
for (const [userId, devices] of devicesByUser) {
const resultDevices = new Map();
result.set(userId, resultDevices);
for (const deviceInfo of devices) {
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
@@ -268,20 +273,21 @@ export async function ensureOlmSessionsForDevices(
log.info("Attempted to start session with ourself! Ignoring");
// We must fill in the section in the return value though, as callers
// expect it to be there.
result[userId][deviceId] = {
resultDevices.set(deviceId, {
device: deviceInfo,
sessionId: null,
};
});
continue;
}
const forWhom = `for ${key} (${userId}:${deviceId})`;
const sessionId = await olmDevice.getSessionIdForDevice(key, !!resolveSession[key], log);
if (sessionId !== null && resolveSession[key]) {
const sessionId = await olmDevice.getSessionIdForDevice(key, !!resolveSession.get(key), log);
const resolveSessionFn = resolveSession.get(key);
if (sessionId !== null && resolveSessionFn) {
// we found a session, but we had marked the session as
// in-progress, so resolve it now, which will unmark it and
// unblock anything that was waiting
resolveSession[key]();
resolveSessionFn();
}
if (sessionId === null || force) {
if (force) {
@@ -291,10 +297,10 @@ export async function ensureOlmSessionsForDevices(
}
devicesWithoutSession.push([userId, deviceId]);
}
result[userId][deviceId] = {
resultDevices.set(deviceId, {
device: deviceInfo,
sessionId: sessionId,
};
});
}
}
@@ -310,7 +316,7 @@ export async function ensureOlmSessionsForDevices(
res = await baseApis.claimOneTimeKeys(devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout);
log.debug(`Claimed ${taskDetail}`);
} catch (e) {
for (const resolver of Object.values(resolveSession)) {
for (const resolver of resolveSession.values()) {
resolver();
}
log.log(`Failed to claim ${taskDetail}`, e, devicesWithoutSession);
@@ -323,7 +329,7 @@ export async function ensureOlmSessionsForDevices(
const otkResult = res.one_time_keys || ({} as IClaimOTKsResult["one_time_keys"]);
const promises: Promise<void>[] = [];
for (const [userId, devices] of Object.entries(devicesByUser)) {
for (const [userId, devices] of devicesByUser) {
const userRes = otkResult[userId] || {};
for (const deviceInfo of devices) {
const deviceId = deviceInfo.deviceId;
@@ -336,7 +342,7 @@ export async function ensureOlmSessionsForDevices(
continue;
}
if (result[userId][deviceId].sessionId && !force) {
if (result.get(userId)?.get(deviceId)?.sessionId && !force) {
// we already have a result for this device
continue;
}
@@ -351,24 +357,19 @@ export async function ensureOlmSessionsForDevices(
if (!oneTimeKey) {
log.warn(`No one-time keys (alg=${oneTimeKeyAlgorithm}) ` + `for device ${userId}:${deviceId}`);
if (resolveSession[key]) {
resolveSession[key]();
}
resolveSession.get(key)?.();
continue;
}
promises.push(
_verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo).then(
(sid) => {
if (resolveSession[key]) {
resolveSession[key](sid ?? undefined);
}
result[userId][deviceId].sessionId = sid;
resolveSession.get(key)?.(sid ?? undefined);
const deviceInfo = result.get(userId)?.get(deviceId);
if (deviceInfo) deviceInfo.sessionId = sid;
},
(e) => {
if (resolveSession[key]) {
resolveSession[key]();
}
resolveSession.get(key)?.();
throw e;
},
),
@@ -21,6 +21,7 @@ import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { ICrossSigningKey } from "../../client";
import { InboundGroupSessionData } from "../OlmDevice";
import { safeSet } from "../../utils";
/**
* Internal module. Partial localStorage backed storage for e2e.
@@ -178,11 +179,11 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
if (userId in notifiedErrorDevices) {
if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
ret.push(device);
notifiedErrorDevices[userId][deviceInfo.deviceId] = true;
safeSet(notifiedErrorDevices[userId], deviceInfo.deviceId, true);
}
} else {
ret.push(device);
notifiedErrorDevices[userId] = { [deviceInfo.deviceId]: true };
safeSet(notifiedErrorDevices, userId, { [deviceInfo.deviceId]: true });
}
}
+3 -2
View File
@@ -33,6 +33,7 @@ import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../OlmDevice";
import { safeSet } from "../../utils";
/**
* Internal module. in-memory storage for e2e.
@@ -375,11 +376,11 @@ export class MemoryCryptoStore implements CryptoStore {
if (userId in notifiedErrorDevices) {
if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
ret.push(device);
notifiedErrorDevices[userId][deviceInfo.deviceId] = true;
safeSet(notifiedErrorDevices[userId], deviceInfo.deviceId, true);
}
} else {
ret.push(device);
notifiedErrorDevices[userId] = { [deviceInfo.deviceId]: true };
safeSet(notifiedErrorDevices, userId, { [deviceInfo.deviceId]: true });
}
}
@@ -269,12 +269,12 @@ export class ToDeviceChannel implements IVerificationChannel {
private async sendToDevices(type: string, content: Record<string, any>, devices: string[]): Promise<void> {
if (devices.length) {
const msgMap: Record<string, Record<string, any>> = {};
const deviceMessages: Map<string, Record<string, any>> = new Map();
for (const deviceId of devices) {
msgMap[deviceId] = content;
deviceMessages.set(deviceId, content);
}
await this.client.sendToDevice(type, { [this.userId]: msgMap });
await this.client.sendToDevice(type, new Map([[this.userId, deviceMessages]]));
}
}
+13 -15
View File
@@ -29,15 +29,16 @@ import { IEvent, IContent, EventStatus } from "./models/event";
import { ISendEventResponse } from "./@types/requests";
import { EventType } from "./@types/event";
import { logger } from "./logger";
import { MatrixClient, ClientEvent, IMatrixClientCreateOpts, IStartClientOpts } from "./client";
import { MatrixClient, ClientEvent, IMatrixClientCreateOpts, IStartClientOpts, SendToDeviceContentMap } from "./client";
import { SyncApi, SyncState } from "./sync";
import { SlidingSyncSdk } from "./sliding-sync-sdk";
import { MatrixEvent } from "./models/event";
import { User } from "./models/user";
import { Room } from "./models/room";
import { ToDeviceBatch } from "./models/ToDeviceMessage";
import { ToDeviceBatch, ToDevicePayload } from "./models/ToDeviceMessage";
import { DeviceInfo } from "./crypto/deviceinfo";
import { IOlmDevice } from "./crypto/algorithms/megolm";
import { MapWithDefault, recursiveMapToObject } from "./utils";
interface IStateEventRequest {
eventType: string;
@@ -234,35 +235,32 @@ export class RoomWidgetClient extends MatrixClient {
return await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId);
}
public async sendToDevice(
eventType: string,
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
): Promise<{}> {
await this.widgetApi.sendToDevice(eventType, false, contentMap);
public async sendToDevice(eventType: string, contentMap: SendToDeviceContentMap): Promise<{}> {
await this.widgetApi.sendToDevice(eventType, false, recursiveMapToObject(contentMap));
return {};
}
public async queueToDevice({ eventType, batch }: ToDeviceBatch): Promise<void> {
const contentMap: { [userId: string]: { [deviceId: string]: object } } = {};
// map: user Id → device Id → payload
const contentMap: MapWithDefault<string, Map<string, ToDevicePayload>> = new MapWithDefault(() => new Map());
for (const { userId, deviceId, payload } of batch) {
if (!contentMap[userId]) contentMap[userId] = {};
contentMap[userId][deviceId] = payload;
contentMap.getOrCreate(userId).set(deviceId, payload);
}
await this.widgetApi.sendToDevice(eventType, false, contentMap);
await this.widgetApi.sendToDevice(eventType, false, recursiveMapToObject(contentMap));
}
public async encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice<DeviceInfo>[], payload: object): Promise<void> {
const contentMap: { [userId: string]: { [deviceId: string]: object } } = {};
// map: user Id → device Id → payload
const contentMap: MapWithDefault<string, Map<string, object>> = new MapWithDefault(() => new Map());
for (const {
userId,
deviceInfo: { deviceId },
} of userDeviceInfoArr) {
if (!contentMap[userId]) contentMap[userId] = {};
contentMap[userId][deviceId] = payload;
contentMap.getOrCreate(userId).set(deviceId, payload);
}
await this.widgetApi.sendToDevice((payload as { type: string }).type, true, contentMap);
await this.widgetApi.sendToDevice((payload as { type: string }).type, true, recursiveMapToObject(contentMap));
}
// Overridden since we get TURN servers automatically over the widget API,
+1 -1
View File
@@ -70,7 +70,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
this.opts.idBaseUrl = url;
}
public idServerRequest<T extends Record<string, unknown>>(
public idServerRequest<T extends {} = Record<string, unknown>>(
method: Method,
path: string,
params: Record<string, string | string[]> | undefined,
+3 -17
View File
@@ -576,13 +576,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
}
public get replyEventId(): string | undefined {
// We're prefer ev.getContent() over ev.getWireContent() to make sure
// we grab the latest edit with potentially new relations. But we also
// can't just rely on ev.getContent() by itself because historically we
// still show the reply from the original message even though the edit
// event does not include the relation reply.
const mRelatesTo = this.getContent()["m.relates_to"] || this.getWireContent()["m.relates_to"];
return mRelatesTo?.["m.in_reply_to"]?.event_id;
return this.getWireContent()["m.relates_to"]?.["m.in_reply_to"]?.event_id;
}
public get relationEventId(): string | undefined {
@@ -799,22 +793,14 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* @returns array of recipients
*/
public getKeyRequestRecipients(userId: string): IKeyRequestRecipient[] {
// send the request to all of our own devices, and the
// original sending device if it wasn't us.
const wireContent = this.getWireContent();
// send the request to all of our own devices
const recipients = [
{
userId,
deviceId: "*",
},
];
const sender = this.getSender();
if (sender !== userId) {
recipients.push({
userId: sender!,
deviceId: wireContent.device_id,
});
}
return recipients;
}
+26 -21
View File
@@ -16,7 +16,6 @@ import {
MAIN_ROOM_TIMELINE,
Receipt,
ReceiptCache,
Receipts,
ReceiptType,
WrappedReceipt,
} from "../@types/read_receipts";
@@ -25,6 +24,7 @@ import * as utils from "../utils";
import { MatrixEvent } from "./event";
import { EventType } from "../@types/event";
import { EventTimelineSet } from "./event-timeline-set";
import { MapWithDefault } from "../utils";
import { NotificationCountType } from "./room";
export function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent {
@@ -56,8 +56,11 @@ export abstract class ReadReceipt<
// the form of this structure. This is sub-optimal for the exposed APIs
// which pass in an event ID and get back some receipts, so we also store
// a pre-cached list for this purpose.
private receipts: Receipts = {}; // { receipt_type: { user_id: Receipt } }
private receiptCacheByEventId: ReceiptCache = {}; // { event_id: CachedReceipt[] }
// Map: receipt type user Id → receipt
private receipts = new MapWithDefault<string, Map<string, [WrappedReceipt | null, WrappedReceipt | null]>>(
() => new Map(),
);
private receiptCacheByEventId: ReceiptCache = new Map();
public abstract getUnfilteredTimelineSet(): EventTimelineSet;
public abstract timeline: MatrixEvent[];
@@ -74,7 +77,7 @@ export abstract class ReadReceipt<
ignoreSynthesized = false,
receiptType = ReceiptType.Read,
): WrappedReceipt | null {
const [realReceipt, syntheticReceipt] = this.receipts[receiptType]?.[userId] ?? [];
const [realReceipt, syntheticReceipt] = this.receipts.get(receiptType)?.get(userId) ?? [null, null];
if (ignoreSynthesized) {
return realReceipt;
}
@@ -126,14 +129,13 @@ export abstract class ReadReceipt<
receipt: Receipt,
synthetic: boolean,
): void {
if (!this.receipts[receiptType]) {
this.receipts[receiptType] = {};
}
if (!this.receipts[receiptType][userId]) {
this.receipts[receiptType][userId] = [null, null];
}
const receiptTypesMap = this.receipts.getOrCreate(receiptType);
let pair = receiptTypesMap.get(userId);
const pair = this.receipts[receiptType][userId];
if (!pair) {
pair = [null, null];
receiptTypesMap.set(userId, pair);
}
let existingReceipt = pair[ReceiptPairRealIndex];
if (synthetic) {
@@ -185,23 +187,26 @@ export abstract class ReadReceipt<
if (cachedReceipt === newCachedReceipt) return;
// clean up any previous cache entry
if (cachedReceipt && this.receiptCacheByEventId[cachedReceipt.eventId]) {
if (cachedReceipt && this.receiptCacheByEventId.get(cachedReceipt.eventId)) {
const previousEventId = cachedReceipt.eventId;
// Remove the receipt we're about to clobber out of existence from the cache
this.receiptCacheByEventId[previousEventId] = this.receiptCacheByEventId[previousEventId].filter((r) => {
return r.type !== receiptType || r.userId !== userId;
});
this.receiptCacheByEventId.set(
previousEventId,
this.receiptCacheByEventId.get(previousEventId)!.filter((r) => {
return r.type !== receiptType || r.userId !== userId;
}),
);
if (this.receiptCacheByEventId[previousEventId].length < 1) {
delete this.receiptCacheByEventId[previousEventId]; // clean up the cache keys
if (this.receiptCacheByEventId.get(previousEventId)!.length < 1) {
this.receiptCacheByEventId.delete(previousEventId); // clean up the cache keys
}
}
// cache the new one
if (!this.receiptCacheByEventId[eventId]) {
this.receiptCacheByEventId[eventId] = [];
if (!this.receiptCacheByEventId.get(eventId)) {
this.receiptCacheByEventId.set(eventId, []);
}
this.receiptCacheByEventId[eventId].push({
this.receiptCacheByEventId.get(eventId)!.push({
userId: userId,
type: receiptType as ReceiptType,
data: receipt,
@@ -215,7 +220,7 @@ export abstract class ReadReceipt<
* an empty list.
*/
public getReceiptsForEvent(event: MatrixEvent): CachedReceipt[] {
return this.receiptCacheByEventId[event.getId()!] || [];
return this.receiptCacheByEventId.get(event.getId()!) || [];
}
public abstract addReceipt(event: MatrixEvent, synthetic: boolean): void;
+22 -17
View File
@@ -25,7 +25,7 @@ import {
import { Direction, EventTimeline } from "./event-timeline";
import { getHttpUriForMxc } from "../content-repo";
import * as utils from "../utils";
import { normalize } from "../utils";
import { normalize, noUnsafeEventProps } from "../utils";
import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event";
import { EventStatus } from "./event-status";
import { RoomMember } from "./room-member";
@@ -311,7 +311,7 @@ export type RoomEventHandlerMap = {
export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
public readonly reEmitter: TypedReEmitter<RoomEmittedEvents, RoomEventHandlerMap>;
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
private txnToEvent: Map<string, MatrixEvent> = new Map(); // Pending in-flight requests { string: MatrixEvent }
private notificationCounts: NotificationCount = {};
private readonly threadNotifications = new Map<string, NotificationCount>();
public readonly cachedThreadReadReceipts = new Map<string, CachedReceiptStructure[]>();
@@ -356,7 +356,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
* accountData Dict of per-room account_data events; the keys are the
* event type and the values are the events.
*/
public accountData: Record<string, MatrixEvent> = {}; // $eventType: $event
public accountData: Map<string, MatrixEvent> = new Map(); // $eventType: $event
/**
* The room summary.
*/
@@ -902,7 +902,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
rawMembersEvents = await this.loadMembersFromServer();
logger.log(`LL: got ${rawMembersEvents.length} ` + `members from server for room ${this.roomId}`);
}
const memberEvents = rawMembersEvents.map(this.client.getEventMapper());
const memberEvents = rawMembersEvents.filter(noUnsafeEventProps).map(this.client.getEventMapper());
return { memberEvents, fromServer };
}
@@ -2151,14 +2151,17 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
// a reference to the cached receipts anymore.
this.cachedThreadReadReceipts.delete(threadId);
// If we managed to create a thread and figure out its `id` then we can use it
// This has to happen before thread.addEvents, because that adds events to the eventtimeline, and the
// eventtimeline sometimes looks up thread information via the room.
this.threads.set(thread.id, thread);
// This is necessary to be able to jump to events in threads:
// If we jump to an event in a thread where neither the event, nor the root,
// nor any thread event are loaded yet, we'll load the event as well as the thread root, create the thread,
// and pass the event through this.
thread.addEvents(events, false);
// If we managed to create a thread and figure out its `id` then we can use it
this.threads.set(thread.id, thread);
this.reEmitter.reEmit(thread, [
ThreadEvent.Delete,
ThreadEvent.Update,
@@ -2249,8 +2252,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
const txnId = event.getUnsigned().transaction_id;
if (!txnId && event.getSender() === this.myUserId) {
// check the txn map for a matching event ID
for (const tid in this.txnToEvent) {
const localEvent = this.txnToEvent[tid];
for (const [tid, localEvent] of this.txnToEvent) {
if (localEvent.getId() === event.getId()) {
logger.debug("processLiveEvent: found sent event without txn ID: ", tid, event.getId());
// update the unsigned field so we can re-use the same codepaths
@@ -2325,7 +2327,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
throw new Error("addPendingEvent called on an event with status " + event.status);
}
if (this.txnToEvent[txnId]) {
if (this.txnToEvent.get(txnId)) {
throw new Error("addPendingEvent called on an event with known txnId " + txnId);
}
@@ -2334,7 +2336,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
// on the unfiltered timelineSet.
EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS)!, false);
this.txnToEvent[txnId] = event;
this.txnToEvent.set(txnId, event);
if (this.pendingEventList) {
if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
logger.warn("Setting event as NOT_SENT due to messages in the same state");
@@ -2426,8 +2428,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
this.relations.aggregateChildEvent(event);
}
public getEventForTxnId(txnId: string): MatrixEvent {
return this.txnToEvent[txnId];
public getEventForTxnId(txnId: string): MatrixEvent | undefined {
return this.txnToEvent.get(txnId);
}
/**
@@ -2454,7 +2456,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
logger.debug(`Got remote echo for event ${oldEventId} -> ${newEventId} old status ${oldStatus}`);
// no longer pending
delete this.txnToEvent[remoteEvent.getUnsigned().transaction_id!];
this.txnToEvent.delete(remoteEvent.getUnsigned().transaction_id!);
// if it's in the pending list, remove it
if (this.pendingEventList) {
@@ -2467,6 +2469,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(remoteEvent);
const thread = threadId ? this.getThread(threadId) : null;
thread?.setEventMetadata(localEvent);
thread?.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
if (shouldLiveInRoom) {
@@ -2548,6 +2551,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(event);
const thread = threadId ? this.getThread(threadId) : undefined;
thread?.setEventMetadata(event);
thread?.timelineSet.replaceEventId(oldEventId, newEventId!);
if (shouldLiveInRoom) {
@@ -2665,7 +2669,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
this.processLiveEvent(event);
if (event.getUnsigned().transaction_id) {
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id!];
const existingEvent = this.txnToEvent.get(event.getUnsigned().transaction_id!);
if (existingEvent) {
// remote echo of an event we sent earlier
this.handleRemoteEcho(event, existingEvent);
@@ -2934,8 +2938,9 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
if (event.getType() === "m.tag") {
this.addTags(event);
}
const lastEvent = this.accountData[event.getType()];
this.accountData[event.getType()] = event;
const eventType = event.getType();
const lastEvent = this.accountData.get(eventType);
this.accountData.set(eventType, event);
this.emit(RoomEvent.AccountData, event, this, lastEvent);
}
}
@@ -2946,7 +2951,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
* @returns the account_data event in question
*/
public getAccountData(type: EventType | string): MatrixEvent | undefined {
return this.accountData[type];
return this.accountData.get(type);
}
/**
+178 -9
View File
@@ -25,6 +25,7 @@ import {
ICallStartedPrefixCondition,
IContainsDisplayNameCondition,
IEventMatchCondition,
IEventPropertyIsCondition,
IPushRule,
IPushRules,
IRoomMemberCountCondition,
@@ -123,6 +124,12 @@ export class PushProcessor {
*/
public constructor(private readonly client: MatrixClient) {}
/**
* Maps the original key from the push rules to a list of property names
* after unescaping.
*/
private readonly parsedKeys = new Map<string, string[]>();
/**
* Convert a list of actions into a object with the actions as keys and their values
* @example
@@ -162,7 +169,7 @@ export class PushProcessor {
if (!newRules) newRules = {} as IPushRules;
if (!newRules.global) newRules.global = {} as PushRuleSet;
if (!newRules.global.override) newRules.global.override = [];
if (!newRules.global.override) newRules.global.underride = [];
if (!newRules.global.underride) newRules.global.underride = [];
// Merge the client-level defaults with the ones from the server
const globalOverrides = newRules.global.override;
@@ -202,6 +209,53 @@ export class PushProcessor {
return newRules;
}
/**
* Pre-caches the parsed keys for push rules and cleans out any obsolete cache
* entries. Should be called after push rules are updated.
* @param newRules - The new push rules.
*/
public updateCachedPushRuleKeys(newRules: IPushRules): void {
// 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 = [];
if (!newRules.global.room) newRules.global.room = [];
if (!newRules.global.sender) newRules.global.sender = [];
if (!newRules.global.underride) newRules.global.underride = [];
// Process the 'key' property on event_match conditions pre-cache the
// values and clean-out any unused values.
const toRemoveKeys = new Set(this.parsedKeys.keys());
for (const ruleset of [
newRules.global.override,
newRules.global.room,
newRules.global.sender,
newRules.global.underride,
]) {
for (const rule of ruleset) {
if (!rule.conditions) {
continue;
}
for (const condition of rule.conditions) {
if (condition.kind !== ConditionKind.EventMatch) {
continue;
}
// Ensure we keep this key.
toRemoveKeys.delete(condition.key);
// Pre-process the key.
this.parsedKeys.set(condition.key, PushProcessor.partsForDottedKey(condition.key));
}
}
}
// Any keys that were previously cached, but are no longer needed should
// be removed.
toRemoveKeys.forEach((k) => this.parsedKeys.delete(k));
}
private static cachedGlobToRegex: Record<string, RegExp> = {}; // $glob: RegExp
private matchingRuleFromKindSet(ev: MatrixEvent, kindset: PushRuleSet): IAnnotatedPushRule | null {
@@ -284,6 +338,8 @@ export class PushProcessor {
switch (cond.kind) {
case ConditionKind.EventMatch:
return this.eventFulfillsEventMatchCondition(cond, ev);
case ConditionKind.EventPropertyIs:
return this.eventFulfillsEventPropertyIsCondition(cond, ev);
case ConditionKind.ContainsDisplayName:
return this.eventFulfillsDisplayNameCondition(cond, ev);
case ConditionKind.RoomMemberCount:
@@ -382,6 +438,13 @@ export class PushProcessor {
return content.body.search(pat) > -1;
}
/**
* Check whether the given event matches the push rule condition by fetching
* the property from the event and comparing against the condition's glob-based
* pattern.
* @param cond - The push rule condition to check for a match.
* @param ev - The event to check for a match.
*/
private eventFulfillsEventMatchCondition(cond: IEventMatchCondition, ev: MatrixEvent): boolean {
if (!cond.key) {
return false;
@@ -392,6 +455,9 @@ export class PushProcessor {
return false;
}
// XXX This does not match in a case-insensitive manner.
//
// See https://spec.matrix.org/v1.5/client-server-api/#conditions-1
if (cond.value) {
return cond.value === val;
}
@@ -408,6 +474,20 @@ export class PushProcessor {
return !!val.match(regex);
}
/**
* Check whether the given event matches the push rule condition by fetching
* the property from the event and comparing exactly against the condition's
* value.
* @param cond - The push rule condition to check for a match.
* @param ev - The event to check for a match.
*/
private eventFulfillsEventPropertyIsCondition(cond: IEventPropertyIsCondition, ev: MatrixEvent): boolean {
if (!cond.key || cond.value === undefined) {
return false;
}
return cond.value === this.valueForDottedKey(cond.key, ev);
}
private eventFulfillsCallStartedCondition(
_cond: ICallStartedCondition | ICallStartedPrefixCondition,
ev: MatrixEvent,
@@ -433,28 +513,105 @@ export class PushProcessor {
return PushProcessor.cachedGlobToRegex[glob];
}
/**
* Parse the key into the separate fields to search by splitting on
* unescaped ".", and then removing any escape characters.
*
* @param str - The key of the push rule condition: a dotted field.
* @returns The unescaped parts to fetch.
* @internal
*/
public static partsForDottedKey(str: string): string[] {
const result = [];
// The current field and whether the previous character was the escape
// character (a backslash).
let part = "";
let escaped = false;
// Iterate over each character, and decide whether to append to the current
// part (following the escape rules) or to start a new part (based on the
// field separator).
for (const c of str) {
// If the previous character was the escape character (a backslash)
// then decide what to append to the current part.
if (escaped) {
if (c === "\\" || c === ".") {
// An escaped backslash or dot just gets added.
part += c;
} else {
// A character that shouldn't be escaped gets the backslash prepended.
part += "\\" + c;
}
// This always resets being escaped.
escaped = false;
continue;
}
if (c == ".") {
// The field separator creates a new part.
result.push(part);
part = "";
} else if (c == "\\") {
// A backslash adds no characters, but starts an escape sequence.
escaped = true;
} else {
// Otherwise, just add the current character.
part += c;
}
}
// Ensure the final part is included. If there's an open escape sequence
// it should be included.
if (escaped) {
part += "\\";
}
result.push(part);
return result;
}
/**
* For a dotted field and event, fetch the value at that position, if one
* exists.
*
* @param key - The key of the push rule condition: a dotted field to fetch.
* @param ev - The matrix event to fetch the field from.
* @returns The value at the dotted path given by key.
*/
private valueForDottedKey(key: string, ev: MatrixEvent): any {
const parts = key.split(".");
// The key should already have been parsed via updateCachedPushRuleKeys,
// but if it hasn't (maybe via an old consumer of the SDK which hasn't
// been updated?) then lazily calculate it here.
let parts = this.parsedKeys.get(key);
if (parts === undefined) {
parts = PushProcessor.partsForDottedKey(key);
this.parsedKeys.set(key, parts);
}
let val: any;
// special-case the first component to deal with encrypted messages
const firstPart = parts[0];
let currentIndex = 0;
if (firstPart === "content") {
val = ev.getContent();
parts.shift();
++currentIndex;
} else if (firstPart === "type") {
val = ev.getType();
parts.shift();
++currentIndex;
} else {
// use the raw event for any other fields
val = ev.event;
}
while (parts.length > 0) {
const thisPart = parts.shift()!;
if (isNullOrUndefined(val[thisPart])) {
return null;
for (; currentIndex < parts.length; ++currentIndex) {
// The previous iteration resulted in null or undefined, bail (and
// avoid the type error of attempting to retrieve a property).
if (isNullOrUndefined(val)) {
return undefined;
}
const thisPart = parts[currentIndex];
val = val[thisPart];
}
return val;
@@ -507,6 +664,18 @@ export class PushProcessor {
* @returns The push rule, or null if no such rule was found
*/
public getPushRuleById(ruleId: string): IPushRule | null {
const result = this.getPushRuleAndKindById(ruleId);
return result?.rule ?? null;
}
/**
* Get one of the users push rules by its ID
*
* @param ruleId - The ID of the rule to search for
* @returns rule The push rule, or null if no such rule was found
* @returns kind - The PushRuleKind of the rule to search for
*/
public getPushRuleAndKindById(ruleId: string): { rule: IPushRule; kind: PushRuleKind } | null {
for (const scope of ["global"] as const) {
if (this.client.pushRules?.[scope] === undefined) continue;
@@ -514,7 +683,7 @@ export class PushProcessor {
if (this.client.pushRules[scope][kind] === undefined) continue;
for (const rule of this.client.pushRules[scope][kind]!) {
if (rule.rule_id === ruleId) return rule;
if (rule.rule_id === ruleId) return { rule, kind };
}
}
}
@@ -1,5 +1,5 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Copyright 2023 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.
@@ -25,20 +25,20 @@ import {
RendezvousTransport,
RendezvousFailureReason,
} from "..";
import { encodeBase64, decodeBase64 } from "../../crypto/olmlib";
import { encodeUnpaddedBase64, decodeBase64 } from "../../crypto/olmlib";
import { crypto, subtleCrypto, TextEncoder } from "../../crypto/crypto";
import { generateDecimalSas } from "../../crypto/verification/SASDecimal";
import { UnstableValue } from "../../NamespacedValue";
const ECDH_V1 = new UnstableValue(
"m.rendezvous.v1.curve25519-aes-sha256",
"org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256",
const ECDH_V2 = new UnstableValue(
"m.rendezvous.v2.curve25519-aes-sha256",
"org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256",
);
export interface ECDHv1RendezvousCode extends RendezvousCode {
export interface ECDHv2RendezvousCode extends RendezvousCode {
rendezvous: {
transport: RendezvousTransportDetails;
algorithm: typeof ECDH_V1.name | typeof ECDH_V1.altName;
algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName;
key: string;
};
}
@@ -46,7 +46,7 @@ export interface ECDHv1RendezvousCode extends RendezvousCode {
export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload;
export interface PlainTextPayload {
algorithm: typeof ECDH_V1.name | typeof ECDH_V1.altName;
algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName;
key?: string;
}
@@ -70,7 +70,7 @@ async function importKey(key: Uint8Array): Promise<CryptoKey> {
* X25519/ECDH key agreement based secure rendezvous channel.
* Note that this is UNSTABLE and may have breaking changes without notice.
*/
export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
export class MSC3903ECDHv2RendezvousChannel<T> implements RendezvousChannel<T> {
private olmSAS?: SAS;
private ourPublicKey: Uint8Array;
private aesKey?: CryptoKey;
@@ -85,17 +85,17 @@ export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
this.ourPublicKey = decodeBase64(this.olmSAS.get_pubkey());
}
public async generateCode(intent: RendezvousIntent): Promise<ECDHv1RendezvousCode> {
public async generateCode(intent: RendezvousIntent): Promise<ECDHv2RendezvousCode> {
if (this.transport.ready) {
throw new Error("Code already generated");
}
await this.transport.send({ algorithm: ECDH_V1.name });
await this.transport.send({ algorithm: ECDH_V2.name });
const rendezvous: ECDHv1RendezvousCode = {
const rendezvous: ECDHv2RendezvousCode = {
rendezvous: {
algorithm: ECDH_V1.name,
key: encodeBase64(this.ourPublicKey),
algorithm: ECDH_V2.name,
key: encodeUnpaddedBase64(this.ourPublicKey),
transport: await this.transport.details(),
},
intent,
@@ -123,7 +123,7 @@ export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
}
const res = rawRes as Partial<PlainTextPayload>;
const { key, algorithm } = res;
if (!algorithm || !ECDH_V1.matches(algorithm) || !key) {
if (!algorithm || !ECDH_V2.matches(algorithm) || !key) {
throw new RendezvousError(
"Unsupported algorithm: " + algorithm,
RendezvousFailureReason.UnsupportedAlgorithm,
@@ -134,20 +134,20 @@ export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
} else {
// send our public key unencrypted
await this.transport.send({
algorithm: ECDH_V1.name,
key: encodeBase64(this.ourPublicKey),
algorithm: ECDH_V2.name,
key: encodeUnpaddedBase64(this.ourPublicKey),
});
}
this.connected = true;
this.olmSAS.set_their_key(encodeBase64(this.theirPublicKey!));
this.olmSAS.set_their_key(encodeUnpaddedBase64(this.theirPublicKey!));
const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!;
const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey;
let aesInfo = ECDH_V1.name;
aesInfo += `|${encodeBase64(initiatorKey)}`;
aesInfo += `|${encodeBase64(recipientKey)}`;
let aesInfo = ECDH_V2.name;
aesInfo += `|${encodeUnpaddedBase64(initiatorKey)}`;
aesInfo += `|${encodeUnpaddedBase64(recipientKey)}`;
const aesKeyBytes = this.olmSAS.generate_bytes(aesInfo, 32);
@@ -181,8 +181,8 @@ export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
);
return {
iv: encodeBase64(iv),
ciphertext: encodeBase64(ciphertext),
iv: encodeUnpaddedBase64(iv),
ciphertext: encodeUnpaddedBase64(ciphertext),
};
}
+1 -1
View File
@@ -14,4 +14,4 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export * from "./MSC3903ECDHv1RendezvousChannel";
export * from "./MSC3903ECDHv2RendezvousChannel";
+31
View File
@@ -0,0 +1,31 @@
/*
Copyright 2023 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 file replaces rust-crypto/index.ts when the js-sdk is being built for browserify.
*
* It is a stub, so that we do not import the whole of the base64'ed wasm artifact into the browserify bundle.
* It deliberately does nothing except raise an exception.
*/
import { IHttpOpts, MatrixHttpApi } from "../http-api";
export async function initRustCrypto(
_http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
_userId: string,
_deviceId: string,
): Promise<Crypto> {
throw new Error("Rust crypto is not supported under browserify.");
}
+1 -2
View File
@@ -43,7 +43,6 @@ import {
} from "./sliding-sync";
import { EventType } from "./@types/event";
import { IPushRules } from "./@types/PushRules";
import { PushProcessor } from "./pushprocessor";
import { RoomStateEvent } from "./models/room-state";
import { RoomMemberEvent } from "./models/room-member";
@@ -262,7 +261,7 @@ class ExtensionAccountData implements Extension<ExtensionAccountDataRequest, Ext
// (see sync) before syncing over the network.
if (accountDataEvent.getType() === EventType.PushRules) {
const rules = accountDataEvent.getContent<IPushRules>();
this.client.pushRules = PushProcessor.rewriteDefaultRules(rules);
this.client.setPushRules(rules);
}
const prevEvent = prevEventsMap[accountDataEvent.getType()];
this.client.emit(ClientEvent.AccountData, accountDataEvent, prevEvent);
+1 -1
View File
@@ -36,7 +36,7 @@ export interface ISavedSync {
* A store for most of the data js-sdk needs to store, apart from crypto data
*/
export interface IStore {
readonly accountData: Record<string, MatrixEvent>; // type : content
readonly accountData: Map<string, MatrixEvent>; // type : content
// XXX: The indexeddb store exposes a non-standard emitter for the "degraded" event
// for when it falls back to being a memory store due to errors.
+14 -25
View File
@@ -31,6 +31,7 @@ import { ISyncResponse } from "../sync-accumulator";
import { IStateEventWithRoomId } from "../@types/search";
import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDeviceMessage";
import { IStoredClientOpts } from "../client";
import { MapWithDefault } from "../utils";
function isValidFilterId(filterId?: string | number | null): boolean {
const isValidStr =
@@ -54,10 +55,10 @@ export class MemoryStore implements IStore {
// userId: {
// filterId: Filter
// }
private filters: Record<string, Record<string, Filter>> = {};
public accountData: Record<string, MatrixEvent> = {}; // type : content
private filters: MapWithDefault<string, Map<string, Filter>> = new MapWithDefault(() => new Map());
public accountData: Map<string, MatrixEvent> = new Map(); // type: content
protected readonly localStorage?: Storage;
private oobMembers: Record<string, IStateEventWithRoomId[]> = {}; // roomId: [member events]
private oobMembers: Map<string, IStateEventWithRoomId[]> = new Map(); // roomId: [member events]
private pendingEvents: { [roomId: string]: Partial<IEvent>[] } = {};
private clientOptions?: IStoredClientOpts;
private pendingToDeviceBatches: IndexedToDeviceBatch[] = [];
@@ -220,10 +221,7 @@ export class MemoryStore implements IStore {
*/
public storeFilter(filter: Filter): void {
if (!filter?.userId || !filter?.filterId) return;
if (!this.filters[filter.userId]) {
this.filters[filter.userId] = {};
}
this.filters[filter.userId][filter.filterId] = filter;
this.filters.getOrCreate(filter.userId).set(filter.filterId, filter);
}
/**
@@ -231,10 +229,7 @@ export class MemoryStore implements IStore {
* @returns A filter or null.
*/
public getFilter(userId: string, filterId: string): Filter | null {
if (!this.filters[userId] || !this.filters[userId][filterId]) {
return null;
}
return this.filters[userId][filterId];
return this.filters.get(userId)?.get(filterId) || null;
}
/**
@@ -289,9 +284,9 @@ export class MemoryStore implements IStore {
// MSC3391: an event with content of {} should be interpreted as deleted
const isDeleted = !Object.keys(event.getContent()).length;
if (isDeleted) {
delete this.accountData[event.getType()];
this.accountData.delete(event.getType());
} else {
this.accountData[event.getType()] = event;
this.accountData.set(event.getType(), event);
}
});
}
@@ -302,7 +297,7 @@ export class MemoryStore implements IStore {
* @returns the user account_data event of given type, if any
*/
public getAccountData(eventType: EventType | string): MatrixEvent | undefined {
return this.accountData[eventType];
return this.accountData.get(eventType);
}
/**
@@ -368,14 +363,8 @@ export class MemoryStore implements IStore {
// userId: User
};
this.syncToken = null;
this.filters = {
// userId: {
// filterId: Filter
// }
};
this.accountData = {
// type : content
};
this.filters = new MapWithDefault(() => new Map());
this.accountData = new Map(); // type : content
return Promise.resolve();
}
@@ -386,7 +375,7 @@ export class MemoryStore implements IStore {
* @returns in case the members for this room haven't been stored yet
*/
public getOutOfBandMembers(roomId: string): Promise<IStateEventWithRoomId[] | null> {
return Promise.resolve(this.oobMembers[roomId] || null);
return Promise.resolve(this.oobMembers.get(roomId) || null);
}
/**
@@ -397,12 +386,12 @@ export class MemoryStore implements IStore {
* @returns when all members have been stored
*/
public setOutOfBandMembers(roomId: string, membershipEvents: IStateEventWithRoomId[]): Promise<void> {
this.oobMembers[roomId] = membershipEvents;
this.oobMembers.set(roomId, membershipEvents);
return Promise.resolve();
}
public clearOutOfBandMembers(roomId: string): Promise<void> {
this.oobMembers = {};
this.oobMembers.delete(roomId);
return Promise.resolve();
}
+1 -1
View File
@@ -34,7 +34,7 @@ import { IStoredClientOpts } from "../client";
* Construct a stub store. This does no-ops on most store methods.
*/
export class StubStore implements IStore {
public readonly accountData = {}; // stub
public readonly accountData = new Map(); // stub
private fromToken: string | null = null;
/** @returns whether or not the database was newly created in this session. */
+18 -16
View File
@@ -19,7 +19,7 @@ limitations under the License.
*/
import { logger } from "./logger";
import { deepCopy, isSupportedReceiptType } from "./utils";
import { deepCopy, isSupportedReceiptType, MapWithDefault, recursiveMapToObject } from "./utils";
import { IContent, IUnsigned } from "./models/event";
import { IRoomSummary } from "./models/room-summary";
import { EventType } from "./@types/event";
@@ -585,29 +585,31 @@ export class SyncAccumulator {
} as IContent,
};
const receiptEventContent: MapWithDefault<
string,
MapWithDefault<ReceiptType, Map<string, object>>
> = new MapWithDefault(() => new MapWithDefault(() => new Map()));
for (const [userId, receiptData] of Object.entries(roomData._readReceipts)) {
if (!receiptEvent.content[receiptData.eventId]) {
receiptEvent.content[receiptData.eventId] = {};
}
if (!receiptEvent.content[receiptData.eventId][receiptData.type]) {
receiptEvent.content[receiptData.eventId][receiptData.type] = {};
}
receiptEvent.content[receiptData.eventId][receiptData.type][userId] = receiptData.data;
receiptEventContent
.getOrCreate(receiptData.eventId)
.getOrCreate(receiptData.type)
.set(userId, receiptData.data);
}
for (const threadReceipts of Object.values(roomData._threadReadReceipts)) {
for (const [userId, receiptData] of Object.entries(threadReceipts)) {
if (!receiptEvent.content[receiptData.eventId]) {
receiptEvent.content[receiptData.eventId] = {};
}
if (!receiptEvent.content[receiptData.eventId][receiptData.type]) {
receiptEvent.content[receiptData.eventId][receiptData.type] = {};
}
receiptEvent.content[receiptData.eventId][receiptData.type][userId] = receiptData.data;
receiptEventContent
.getOrCreate(receiptData.eventId)
.getOrCreate(receiptData.type)
.set(userId, receiptData.data);
}
}
receiptEvent.content = recursiveMapToObject(receiptEventContent);
// add only if we have some receipt data
if (Object.keys(receiptEvent.content).length > 0) {
if (receiptEventContent.size > 0) {
roomJson.ephemeral.events.push(receiptEvent as IMinimalEvent);
}
+32 -29
View File
@@ -29,10 +29,9 @@ import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend";
import { User, UserEvent } from "./models/user";
import { NotificationCountType, Room, RoomEvent } from "./models/room";
import * as utils from "./utils";
import { IDeferred } from "./utils";
import { IDeferred, noUnsafeEventProps, unsafeProp } from "./utils";
import { Filter } from "./filter";
import { EventTimeline } from "./models/event-timeline";
import { PushProcessor } from "./pushprocessor";
import { logger } from "./logger";
import { InvalidStoreError, InvalidStoreState } from "./errors";
import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering, ResetTimelineCallback } from "./client";
@@ -1134,22 +1133,24 @@ export class SyncApi {
// handle presence events (User objects)
if (Array.isArray(data.presence?.events)) {
data.presence!.events.map(client.getEventMapper()).forEach(function (presenceEvent) {
let user = client.store.getUser(presenceEvent.getSender()!);
if (user) {
user.setPresenceEvent(presenceEvent);
} else {
user = createNewUser(client, presenceEvent.getSender()!);
user.setPresenceEvent(presenceEvent);
client.store.storeUser(user);
}
client.emit(ClientEvent.Event, presenceEvent);
});
data.presence!.events.filter(noUnsafeEventProps)
.map(client.getEventMapper())
.forEach(function (presenceEvent) {
let user = client.store.getUser(presenceEvent.getSender()!);
if (user) {
user.setPresenceEvent(presenceEvent);
} else {
user = createNewUser(client, presenceEvent.getSender()!);
user.setPresenceEvent(presenceEvent);
client.store.storeUser(user);
}
client.emit(ClientEvent.Event, presenceEvent);
});
}
// handle non-room account_data
if (Array.isArray(data.account_data?.events)) {
const events = data.account_data.events.map(client.getEventMapper());
const events = data.account_data.events.filter(noUnsafeEventProps).map(client.getEventMapper());
const prevEventsMap = events.reduce<Record<string, MatrixEvent | undefined>>((m, c) => {
m[c.getType()!] = client.store.getAccountData(c.getType());
return m;
@@ -1162,7 +1163,7 @@ export class SyncApi {
// (see sync) before syncing over the network.
if (accountDataEvent.getType() === EventType.PushRules) {
const rules = accountDataEvent.getContent<IPushRules>();
client.pushRules = PushProcessor.rewriteDefaultRules(rules);
client.setPushRules(rules);
}
const prevEvent = prevEventsMap[accountDataEvent.getType()!];
client.emit(ClientEvent.AccountData, accountDataEvent, prevEvent);
@@ -1172,7 +1173,7 @@ export class SyncApi {
// handle to-device events
if (data.to_device && Array.isArray(data.to_device.events) && data.to_device.events.length > 0) {
let toDeviceMessages: IToDeviceEvent[] = data.to_device.events;
let toDeviceMessages: IToDeviceEvent[] = data.to_device.events.filter(noUnsafeEventProps);
if (this.syncOpts.cryptoCallbacks) {
toDeviceMessages = await this.syncOpts.cryptoCallbacks.preprocessToDeviceMessages(toDeviceMessages);
@@ -1636,18 +1637,20 @@ export class SyncApi {
// to
// [{stuff+Room+isBrandNewRoom}, {stuff+Room+isBrandNewRoom}]
const client = this.client;
return Object.keys(obj).map((roomId) => {
const arrObj = obj[roomId] as T & { room: Room; isBrandNewRoom: boolean };
let room = client.store.getRoom(roomId);
let isBrandNewRoom = false;
if (!room) {
room = this.createRoom(roomId);
isBrandNewRoom = true;
}
arrObj.room = room;
arrObj.isBrandNewRoom = isBrandNewRoom;
return arrObj;
});
return Object.keys(obj)
.filter((k) => !unsafeProp(k))
.map((roomId) => {
const arrObj = obj[roomId] as T & { room: Room; isBrandNewRoom: boolean };
let room = client.store.getRoom(roomId);
let isBrandNewRoom = false;
if (!room) {
room = this.createRoom(roomId);
isBrandNewRoom = true;
}
arrObj.room = room;
arrObj.isBrandNewRoom = isBrandNewRoom;
return arrObj;
});
}
private mapSyncEventsFormat(
@@ -1660,7 +1663,7 @@ export class SyncApi {
}
const mapper = this.client.getEventMapper({ decrypt });
type TaggedEvent = (IStrippedState | IRoomEvent | IStateEvent | IMinimalEvent) & { room_id?: string };
return (obj.events as TaggedEvent[]).map(function (e) {
return (obj.events as TaggedEvent[]).filter(noUnsafeEventProps).map(function (e) {
if (room) {
e.room_id = room.roomId;
}
+66 -1
View File
@@ -22,7 +22,7 @@ import unhomoglyph from "unhomoglyph";
import promiseRetry from "p-retry";
import { Optional } from "matrix-events-sdk";
import { MatrixEvent } from "./models/event";
import { IEvent, MatrixEvent } from "./models/event";
import { M_TIMESTAMP } from "./@types/location";
import { ReceiptType } from "./@types/read_receipts";
@@ -703,3 +703,68 @@ export function mapsEqual<K, V>(x: Map<K, V>, y: Map<K, V>, eq = (v1: V, v2: V):
}
return true;
}
function processMapToObjectValue(value: any): any {
if (value instanceof Map) {
// Value is a Map. Recursively map it to an object.
return recursiveMapToObject(value);
} else if (Array.isArray(value)) {
// Value is an Array. Recursively map the value (e.g. to cover Array of Arrays).
return value.map((v) => processMapToObjectValue(v));
} else {
return value;
}
}
/**
* Recursively converts Maps to plain objects.
* Also supports sub-lists of Maps.
*/
export function recursiveMapToObject(map: Map<any, any>): any {
const targetMap = new Map();
for (const [key, value] of map) {
targetMap.set(key, processMapToObjectValue(value));
}
return Object.fromEntries(targetMap.entries());
}
export function unsafeProp<K extends keyof any | undefined>(prop: K): boolean {
return prop === "__proto__" || prop === "prototype" || prop === "constructor";
}
export function safeSet<K extends keyof any>(obj: Record<any, any>, prop: K, value: any): void {
if (unsafeProp(prop)) {
throw new Error("Trying to modify prototype or constructor");
}
obj[prop] = value;
}
export function noUnsafeEventProps(event: Partial<IEvent>): boolean {
return !(
unsafeProp(event.room_id) ||
unsafeProp(event.sender) ||
unsafeProp(event.user_id) ||
unsafeProp(event.event_id)
);
}
export class MapWithDefault<K, V> extends Map<K, V> {
public constructor(private createDefault: () => V) {
super();
}
/**
* Returns the value if the key already exists.
* If not, it creates a new value under that key using the ctor callback and returns it.
*/
public getOrCreate(key: K): V {
if (!this.has(key)) {
this.set(key, this.createDefault());
}
return this.get(key)!;
}
}
+15 -7
View File
@@ -390,6 +390,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// Used to keep the timer for the delay before actually stopping our
// video track after muting (see setLocalVideoMuted)
private stopVideoTrackTimer?: ReturnType<typeof setTimeout>;
// Used to allow connection without Video and Audio. To establish a webrtc connection without media a Data channel is
// needed At the moment this property is true if we allow MatrixClient with isVoipWithNoMediaAllowed = true
private readonly isOnlyDataChannelAllowed: boolean;
/**
* Construct a new Matrix Call.
@@ -420,6 +423,8 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
utils.checkObjectHasKeys(server, ["urls"]);
}
this.callId = genCallID();
// If the Client provides calls without audio and video we need a datachannel for a webrtc connection
this.isOnlyDataChannelAllowed = this.client.isVoipWithNoMediaAllowed;
}
/**
@@ -595,7 +600,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
if (!userId) throw new Error("Couldn't find opponent user ID to init crypto");
const deviceInfoMap = await this.client.crypto.deviceList.downloadKeys([userId], false);
this.opponentDeviceInfo = deviceInfoMap[userId][this.opponentDeviceId];
this.opponentDeviceInfo = deviceInfoMap.get(userId)?.get(this.opponentDeviceId);
if (this.opponentDeviceInfo === undefined) {
throw new GroupCallUnknownDeviceError(userId);
}
@@ -944,7 +949,11 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// According to previous comments in this file, firefox at some point did not
// add streams until media started arriving on them. Testing latest firefox
// (81 at time of writing), this is no longer a problem, so let's do it the correct way.
if (!remoteStream || remoteStream.getTracks().length === 0) {
//
// For example in case of no media webrtc connections like screen share only call we have to allow webrtc
// connections without remote media. In this case we always use a data channel. At the moment we allow as well
// only data channel as media in the WebRTC connection with this setup here.
if (!this.isOnlyDataChannelAllowed && (!remoteStream || remoteStream.getTracks().length === 0)) {
logger.error(
`Call ${this.callId} initWithInvite() no remote stream or no tracks after setting remote description!`,
);
@@ -2389,11 +2398,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
},
);
} else {
await this.client.sendToDevice(eventType, {
[userId]: {
[this.opponentDeviceId]: content,
},
});
await this.client.sendToDevice(
eventType,
new Map<string, any>([[userId, new Map([[this.opponentDeviceId, content]])]]),
);
}
} else {
this.emit(CallEvent.SendVoipEvent, {
+51 -6
View File
@@ -196,6 +196,7 @@ export class GroupCall extends TypedEventEmitter<
public readonly userMediaFeeds: CallFeed[] = [];
public readonly screenshareFeeds: CallFeed[] = [];
public groupCallId: string;
public readonly allowCallWithoutVideoAndAudio: boolean;
private readonly calls = new Map<string, Map<string, MatrixCall>>(); // user_id -> device_id -> MatrixCall
private callHandlers = new Map<string, Map<string, ICallHandlers>>(); // user_id -> device_id -> ICallHandlers
@@ -219,6 +220,7 @@ export class GroupCall extends TypedEventEmitter<
groupCallId?: string,
private dataChannelsEnabled?: boolean,
private dataChannelOptions?: IGroupCallDataChannelOptions,
isCallWithoutVideoAndAudio?: boolean,
) {
super();
this.reEmitter = new ReEmitter(this);
@@ -231,6 +233,7 @@ export class GroupCall extends TypedEventEmitter<
this.on(GroupCallEvent.ParticipantsChanged, this.onParticipantsChanged);
this.on(GroupCallEvent.GroupCallStateChanged, this.onStateChanged);
this.on(GroupCallEvent.LocalScreenshareStateChanged, this.onLocalFeedsChanged);
this.allowCallWithoutVideoAndAudio = !!isCallWithoutVideoAndAudio;
}
public async create(): Promise<GroupCall> {
@@ -374,8 +377,15 @@ export class GroupCall extends TypedEventEmitter<
try {
stream = await this.client.getMediaHandler().getUserMediaStream(true, this.type === GroupCallType.Video);
} catch (error) {
this.state = GroupCallState.LocalCallFeedUninitialized;
throw error;
// If is allowed to join a call without a media stream, then we
// don't throw an error here. But we need an empty Local Feed to establish
// a connection later.
if (this.allowCallWithoutVideoAndAudio) {
stream = new MediaStream();
} else {
this.state = GroupCallState.LocalCallFeedUninitialized;
throw error;
}
}
// The call could've been disposed while we were waiting, and could
@@ -584,6 +594,31 @@ export class GroupCall extends TypedEventEmitter<
logger.log(
`GroupCall ${this.groupCallId} setMicrophoneMuted() (streamId=${this.localCallFeed.stream.id}, muted=${muted})`,
);
// We needed this here to avoid an error in case user join a call without a device.
// I can not use .then .catch functions because linter :-(
try {
if (!muted) {
const stream = await this.client
.getMediaHandler()
.getUserMediaStream(true, !this.localCallFeed.isVideoMuted());
if (stream === null) {
// if case permission denied to get a stream stop this here
/* istanbul ignore next */
logger.log(
`GroupCall ${this.groupCallId} setMicrophoneMuted() no device to receive local stream, muted=${muted}`,
);
return false;
}
}
} catch (e) {
/* istanbul ignore next */
logger.log(
`GroupCall ${this.groupCallId} setMicrophoneMuted() no device or permission to receive local stream, muted=${muted}`,
);
return false;
}
this.localCallFeed.setAudioVideoMuted(muted, null);
// I don't believe its actually necessary to enable these tracks: they
// are the one on the GroupCall's own CallFeed and are cloned before being
@@ -617,14 +652,24 @@ export class GroupCall extends TypedEventEmitter<
}
if (this.localCallFeed) {
/* istanbul ignore next */
logger.log(
`GroupCall ${this.groupCallId} setLocalVideoMuted() (stream=${this.localCallFeed.stream.id}, muted=${muted})`,
);
const stream = await this.client.getMediaHandler().getUserMediaStream(true, !muted);
await this.updateLocalUsermediaStream(stream);
this.localCallFeed.setAudioVideoMuted(null, muted);
setTracksEnabled(this.localCallFeed.stream.getVideoTracks(), !muted);
try {
const stream = await this.client.getMediaHandler().getUserMediaStream(true, !muted);
await this.updateLocalUsermediaStream(stream);
this.localCallFeed.setAudioVideoMuted(null, muted);
setTracksEnabled(this.localCallFeed.stream.getVideoTracks(), !muted);
} catch (_) {
// No permission to video device
/* istanbul ignore next */
logger.log(
`GroupCall ${this.groupCallId} setLocalVideoMuted() no device or permission to receive local stream, muted=${muted}`,
);
return false;
}
} else {
logger.log(`GroupCall ${this.groupCallId} setLocalVideoMuted() no stream muted (muted=${muted})`);
this.initWithVideoMuted = muted;
+4 -1
View File
@@ -184,8 +184,11 @@ export class GroupCallEventHandler {
isPtt,
callIntent,
groupCallId,
content?.dataChannelsEnabled,
// Because without Media section a WebRTC connection is not possible, so need a RTCDataChannel to set up a
// no media WebRTC connection anyway.
content?.dataChannelsEnabled || this.client.isVoipWithNoMediaAllowed,
dataChannelOptions,
this.client.isVoipWithNoMediaAllowed,
);
this.groupCalls.set(room.roomId, groupCall);
+14 -4
View File
@@ -185,13 +185,23 @@ export class MediaHandler extends TypedEventEmitter<
}
public async hasAudioDevice(): Promise<boolean> {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter((device) => device.kind === "audioinput").length > 0;
try {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter((device) => device.kind === "audioinput").length > 0;
} catch (err) {
logger.log(`MediaHandler hasAudioDevice() calling navigator.mediaDevices.enumerateDevices with error`, err);
return false;
}
}
public async hasVideoDevice(): Promise<boolean> {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter((device) => device.kind === "videoinput").length > 0;
try {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter((device) => device.kind === "videoinput").length > 0;
} catch (err) {
logger.log(`MediaHandler hasVideoDevice() calling navigator.mediaDevices.enumerateDevices with error`, err);
return false;
}
}
/**
+721 -494
View File
File diff suppressed because it is too large Load Diff