Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c87048bd9f | |||
| 02269f33b7 | |||
| 7f46ae7b97 | |||
| 9a504af18e | |||
| 26663e67fd | |||
| 0cfc67c679 | |||
| 13d95c8219 | |||
| e119dc4e89 | |||
| b4cdc5a923 | |||
| a82e22b5de | |||
| c894d09d8c | |||
| 585ce07260 | |||
| 8cbbdaa239 | |||
| cd526a254d | |||
| e782a2afa3 | |||
| 565339b1fd | |||
| 493203050a | |||
| 41782c4593 | |||
| 86256a4e74 | |||
| 933a0c9909 | |||
| c8a4d9b88a | |||
| 437128d11b | |||
| c18d09fd22 | |||
| 2a5e5e6a59 | |||
| 68e354752b | |||
| d80b7499fd | |||
| 9c8093eb3e | |||
| aec1c11037 | |||
| e2e9986059 | |||
| d70ffdbc02 | |||
| 21cc9c3d8a |
@@ -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 }}
|
||||
|
||||
@@ -19,3 +19,4 @@ out
|
||||
|
||||
.vscode
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
+21
-7
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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()!;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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) };
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
@@ -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" });
|
||||
@@ -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,
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("MemoryStore", () => {
|
||||
await store.deleteAllData();
|
||||
|
||||
// empty object
|
||||
expect(store.accountData).toEqual({});
|
||||
expect(store.accountData).toEqual(new Map());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+456
-278
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-23
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,4 +14,4 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export * from "./MSC3903ECDHv1RendezvousChannel";
|
||||
export * from "./MSC3903ECDHv2RendezvousChannel";
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user