Compare commits

...

27 Commits

Author SHA1 Message Date
RiotRobot 26663e67fd v23.5.0 2023-03-15 12:39:41 +00:00
RiotRobot 0cfc67c679 Prepare changelog for v23.5.0 2023-03-15 12:39:38 +00:00
RiotRobot 13d95c8219 v23.5.0-rc.1 2023-03-07 11:19:25 +00:00
RiotRobot e119dc4e89 Prepare changelog for v23.5.0-rc.1 2023-03-07 11:19:22 +00:00
Patrick Cloke b4cdc5a923 Implement MSC3758: a push rule condition to match event properties exactly (#3179)
* Add some comments.

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

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

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

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

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

* Stop requesting room keys from other users

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

* fixup! Refactor the room key handling method

* Apply suggestions from code review

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

* fixup! Refactor the room key handling method

* fixup! Apply suggestions from code review

* fixup! Refactor the room key handling method

---------

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

* groupCall: enable datachannel to do no media group calls

* groupCall: changed call no media property as object property

* groupCall: fix existing unit tests

* groupCall: remove not needed flag

* groupCall: rename property to allow no media calls

* groupCall: mute unmute even without device

* groupCall: switch to promise callbacks

* groupCall: switch to try catch

* test: filter dummy code from coverage

* test: extend media mute tests

* groupCall: move permission check to device handler

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

This is a deliberate breaking change on an unstable feature.

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

Deprecates several experimental types

* Remove MSC3903 v1 support

This is a breaking change in code marked unstable/experimental

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

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

This is a deliberate breaking change on an unstable feature.

* Test correct protocol version

* Fix up test

* v2 of MSC3903 implementation

This is a deliberate breaking change on an unstable feature.

* Test correct protocol version

* Fix up test

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

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

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

* Implment MSC3873 to handle escaped dots in keys.

* Add some comments about tests.

* Clarify spec behavior.

* Fix typo.

* Don't manually iterate string.

* Clean-up tests.

* Simplify tests.

* Add more tests & fix bug with empty parts.

* Add more edge cases.

* Add a regular expression solution.

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

* Split on a simpler regular expression.

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

* Remove redundant case in regex.

* Enable sticky regex.

* Rollback use of regex.

* Cache values in the PushProcessor.

* Use more each in tests.

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

* Fix typo.

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

* Remove obsolete property.

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

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

* Make sure events can be added to thread correctly

* Write initial test case

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

* Add test
2023-02-22 17:39:37 +00:00
renovate[bot] 21cc9c3d8a Update jest monorepo to v29.4.3 (#3169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-22 10:23:29 +00:00
31 changed files with 1921 additions and 794 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
- name: 📥 Download artifact
uses: dawidd6/action-download-artifact@bd10f381a96414ce2b13a11bfa89902ba7cea07f # v2.24.3
uses: dawidd6/action-download-artifact@5e780fc7bbd0cac69fc73271ed86edf5dcb72d67 # v2.26.0
with:
workflow: static_analysis.yml
run_id: ${{ github.event.workflow_run.id }}
+1
View File
@@ -19,3 +19,4 @@ out
.vscode
.vscode/
.idea/
+15 -7
View File
@@ -1,13 +1,21 @@
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 [23.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.5.0) (2023-03-15)
==================================================================================================
## ✨ Features
* Implement MSC3758: a push rule condition to match event properties exactly ([\#3179](https://github.com/matrix-org/matrix-js-sdk/pull/3179)).
* Enable group calls without video and audio track by configuration of MatrixClient ([\#3162](https://github.com/matrix-org/matrix-js-sdk/pull/3162)). Contributed by @EnricoSchw.
* Updates to protocol used for Sign in with QR code ([\#3155](https://github.com/matrix-org/matrix-js-sdk/pull/3155)). Contributed by @hughns.
* Implement MSC3873 to handle escaped dots in push rule keys ([\#3134](https://github.com/matrix-org/matrix-js-sdk/pull/3134)). Fixes undefined/matrix-js-sdk#1454.
## 🐛 Bug Fixes
* Fix spec compliance issue around encrypted `m.relates_to` ([\#3178](https://github.com/matrix-org/matrix-js-sdk/pull/3178)).
* Fix reactions in threads sometimes causing stuck notifications ([\#3146](https://github.com/matrix-org/matrix-js-sdk/pull/3146)). Fixes vector-im/element-web#24000. Contributed by @justjanne.
Changes in [23.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.4.0) (2023-02-28)
==================================================================================================
## ✨ Features
* Add easy way to determine if the decryption failure is due to "DecryptionError: The sender has disabled encrypting to unverified devices." ([\#3167](https://github.com/matrix-org/matrix-js-sdk/pull/3167)). Contributed by @florianduros.
Changes in [23.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.4.0-rc.1) (2023-02-21)
============================================================================================================
## ✨ Features
* Polls: expose end event id on poll model ([\#3160](https://github.com/matrix-org/matrix-js-sdk/pull/3160)). Contributed by @kerryarchibald.
* Polls: count undecryptable poll relations ([\#3163](https://github.com/matrix-org/matrix-js-sdk/pull/3163)). Contributed by @kerryarchibald.
+26 -5
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "23.4.0-rc.2",
"version": "23.5.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"
}
+9 -2
View File
@@ -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";
+37 -24
View File
@@ -304,20 +304,24 @@ describe("Crypto", function () {
describe("Key requests", function () {
let aliceClient: MatrixClient;
let secondAliceClient: MatrixClient;
let bobClient: MatrixClient;
let claraClient: MatrixClient;
beforeEach(async function () {
aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
secondAliceClient = new TestClient("@alice:example.com", "secondAliceDevice").client;
bobClient = new TestClient("@bob:example.com", "bobdevice").client;
claraClient = new TestClient("@clara:example.com", "claradevice").client;
await aliceClient.initCrypto();
await secondAliceClient.initCrypto();
await bobClient.initCrypto();
await claraClient.initCrypto();
});
afterEach(async function () {
aliceClient.stopClient();
secondAliceClient.stopClient();
bobClient.stopClient();
claraClient.stopClient();
});
@@ -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 () {
+35 -1
View File
@@ -16,7 +16,7 @@ limitations under the License.
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
import { emitPromise } from "../../test-utils/test-utils";
import { Crypto } from "../../../src/crypto";
import { Crypto, IEventDecryptionResult } from "../../../src/crypto";
describe("MatrixEvent", () => {
it("should create copies of itself", () => {
@@ -182,4 +182,38 @@ describe("MatrixEvent", () => {
expect(encryptedEvent.getType()).toEqual("m.room.message");
});
});
describe("replyEventId", () => {
it("should ignore 'm.relates_to' from encrypted content even if cleartext lacks one", async () => {
const eventId = "test_encrypted_event";
const encryptedEvent = new MatrixEvent({
event_id: eventId,
type: "m.room.encrypted",
content: {
ciphertext: "secrets",
},
});
const crypto = {
decryptEvent: jest.fn().mockImplementationOnce(() => {
return Promise.resolve<IEventDecryptionResult>({
clearEvent: {
type: "m.room.message",
content: {
"m.relates_to": {
"m.in_reply_to": {
event_id: "!anotherEvent",
},
},
},
},
});
}),
} as unknown as Crypto;
await encryptedEvent.attemptDecryption(crypto);
expect(encryptedEvent.getType()).toEqual("m.room.message");
expect(encryptedEvent.replyEventId).toBeUndefined();
});
});
});
+204 -17
View File
@@ -1,6 +1,6 @@
import * as utils from "../test-utils/test-utils";
import { IActionsObject, PushProcessor } from "../../src/pushprocessor";
import { EventType, IContent, MatrixClient, MatrixEvent } from "../../src";
import { ConditionKind, EventType, IContent, MatrixClient, MatrixEvent, PushRuleActionName } from "../../src";
describe("NotificationService", function () {
const testUserId = "@ali:matrix.org";
@@ -11,6 +11,23 @@ describe("NotificationService", function () {
let pushProcessor: PushProcessor;
const msc3914RoomCallRule = {
rule_id: ".org.matrix.msc3914.rule.room.call",
default: true,
enabled: true,
conditions: [
{
kind: "event_match",
key: "type",
pattern: "org.matrix.msc3401.call",
},
{
kind: "call_started",
},
],
actions: ["notify", { set_tweak: "sound", value: "default" }],
};
// These would be better if individual rules were configured in the tests themselves.
const matrixClient = {
getRoom: function () {
@@ -163,26 +180,11 @@ describe("NotificationService", function () {
enabled: true,
rule_id: ".m.rule.room_one_to_one",
},
{
rule_id: ".org.matrix.msc3914.rule.room.call",
default: true,
enabled: true,
conditions: [
{
kind: "event_match",
key: "type",
pattern: "org.matrix.msc3401.call",
},
{
kind: "call_started",
},
],
actions: ["notify", { set_tweak: "sound", value: "default" }],
},
],
room: [],
sender: [],
underride: [
msc3914RoomCallRule,
{
actions: ["dont-notify"],
conditions: [
@@ -287,6 +289,13 @@ describe("NotificationService", function () {
expect(actions.tweaks.highlight).toEqual(true);
});
// TODO: This is not spec compliant behaviour.
//
// See https://spec.matrix.org/v1.5/client-server-api/#conditions-1 which
// describes pattern should glob:
//
// 1. * matches 0 or more characters;
// 2. ? matches exactly one character
it("should bing on character group ([abc]) bing words.", function () {
testEvent.event.content!.body = "Ping!";
let actions = pushProcessor.actionsForEvent(testEvent);
@@ -296,12 +305,14 @@ describe("NotificationService", function () {
expect(actions.tweaks.highlight).toEqual(true);
});
// TODO: This is not spec compliant behaviour. (See above.)
it("should bing on character range ([a-z]) bing words.", function () {
testEvent.event.content!.body = "I ate 6 pies";
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
// TODO: This is not spec compliant behaviour. (See above.)
it("should bing on character negation ([!a]) bing words.", function () {
testEvent.event.content!.body = "boke";
let actions = pushProcessor.actionsForEvent(testEvent);
@@ -330,6 +341,8 @@ describe("NotificationService", function () {
// invalid
it("should gracefully handle bad input.", function () {
// The following body is an object (not a string) and thus is invalid
// for matching against.
testEvent.event.content!.body = { foo: "bar" };
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(false);
@@ -493,4 +506,178 @@ describe("NotificationService", function () {
});
});
});
describe("Test exact event matching", () => {
it.each([
// Simple string matching.
{ value: "bar", eventValue: "bar", expected: true },
// Matches are case-sensitive.
{ value: "bar", eventValue: "BAR", expected: false },
// Matches must match the full string.
{ value: "bar", eventValue: "barbar", expected: false },
// Values should not be type-coerced.
{ value: "bar", eventValue: true, expected: false },
{ value: "bar", eventValue: 1, expected: false },
{ value: "bar", eventValue: false, expected: false },
// Boolean matching.
{ value: true, eventValue: true, expected: true },
{ value: false, eventValue: false, expected: true },
// Types should not be coerced.
{ value: true, eventValue: "true", expected: false },
{ value: true, eventValue: 1, expected: false },
{ value: false, eventValue: null, expected: false },
// Null matching.
{ value: null, eventValue: null, expected: true },
// Types should not be coerced
{ value: null, eventValue: false, expected: false },
{ value: null, eventValue: 0, expected: false },
{ value: null, eventValue: "", expected: false },
{ value: null, eventValue: undefined, expected: false },
// Compound values should never be matched.
{ value: "bar", eventValue: ["bar"], expected: false },
{ value: "bar", eventValue: { bar: true }, expected: false },
{ value: true, eventValue: [true], expected: false },
{ value: true, eventValue: { true: true }, expected: false },
{ value: null, eventValue: [], expected: false },
{ value: null, eventValue: {}, expected: false },
])("test $value against $eventValue", ({ value, eventValue, expected }) => {
matrixClient.pushRules! = {
global: {
override: [
{
actions: [PushRuleActionName.Notify],
conditions: [
{
kind: ConditionKind.EventPropertyIs,
key: "content.foo",
value: value,
},
],
default: true,
enabled: true,
rule_id: ".m.rule.test",
},
],
},
};
testEvent = utils.mkEvent({
type: "m.room.message",
room: testRoomId,
user: "@alfred:localhost",
event: true,
content: {
foo: eventValue,
},
});
const actions = pushProcessor.actionsForEvent(testEvent);
if (expected) {
expect(actions?.notify).toBeTruthy();
} else {
expect(actions?.notify).toBeFalsy();
}
});
});
it.each([
// The properly escaped key works.
{ key: "content.m\\.test.foo", pattern: "bar", expected: true },
// An unescaped version does not match.
{ key: "content.m.test.foo", pattern: "bar", expected: false },
// Over escaping does not match.
{ key: "content.m\\.test\\.foo", pattern: "bar", expected: false },
// Escaping backslashes should match.
{ key: "content.m\\\\example", pattern: "baz", expected: true },
// An unnecessary escape sequence leaves the backslash and still matches.
{ key: "content.m\\example", pattern: "baz", expected: true },
])("test against escaped dotted paths '$key'", ({ key, pattern, expected }) => {
testEvent = utils.mkEvent({
type: "m.room.message",
room: testRoomId,
user: "@alfred:localhost",
event: true,
content: {
// A dot in the field name.
"m.test": { foo: "bar" },
// A backslash in a field name.
"m\\example": "baz",
},
});
expect(
pushProcessor.ruleMatchesEvent(
{
rule_id: "rule1",
actions: [],
conditions: [
{
kind: ConditionKind.EventMatch,
key: key,
pattern: pattern,
},
],
default: false,
enabled: true,
},
testEvent,
),
).toBe(expected);
});
describe("getPushRuleById()", () => {
it("returns null when rule id is not in rule set", () => {
expect(pushProcessor.getPushRuleById("non-existant-rule")).toBeNull();
});
it("returns push rule when it is found in rule set", () => {
expect(pushProcessor.getPushRuleById(".org.matrix.msc3914.rule.room.call")).toEqual(msc3914RoomCallRule);
});
});
describe("getPushRuleAndKindById()", () => {
it("returns null when rule id is not in rule set", () => {
expect(pushProcessor.getPushRuleAndKindById("non-existant-rule")).toBeNull();
});
it("returns push rule when it is found in rule set", () => {
expect(pushProcessor.getPushRuleAndKindById(".org.matrix.msc3914.rule.room.call")).toEqual({
kind: "underride",
rule: msc3914RoomCallRule,
});
});
});
});
describe("Test PushProcessor.partsForDottedKey", function () {
it.each([
// A field with no dots.
["m", ["m"]],
// Simple dotted fields.
["m.foo", ["m", "foo"]],
["m.foo.bar", ["m", "foo", "bar"]],
// Backslash is used as an escape character.
["m\\.foo", ["m.foo"]],
["m\\\\.foo", ["m\\", "foo"]],
["m\\\\\\.foo", ["m\\.foo"]],
["m\\\\\\\\.foo", ["m\\\\", "foo"]],
["m\\foo", ["m\\foo"]],
["m\\\\foo", ["m\\foo"]],
["m\\\\\\foo", ["m\\\\foo"]],
["m\\\\\\\\foo", ["m\\\\foo"]],
// Ensure that escapes at the end don't cause issues.
["m.foo\\", ["m", "foo\\"]],
["m.foo\\\\", ["m", "foo\\"]],
["m.foo\\.", ["m", "foo."]],
["m.foo\\\\.", ["m", "foo\\", ""]],
["m.foo\\\\\\.", ["m", "foo\\."]],
// Empty parts (corresponding to properties which are an empty string) are allowed.
[".m", ["", "m"]],
["..m", ["", "", "m"]],
["m.", ["m", ""]],
["m..", ["m", "", ""]],
["m..foo", ["m", "", "foo"]],
])("partsFotDottedKey for %s", (path: string, expected: string[]) => {
expect(PushProcessor.partsForDottedKey(path)).toStrictEqual(expected);
});
});
@@ -16,7 +16,7 @@ limitations under the License.
import "../../olm-loader";
import { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from "../../../src/rendezvous/channels";
import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "../../../src/rendezvous/channels";
import { decodeBase64 } from "../../../src/crypto/olmlib";
import { DummyTransport } from "./DummyTransport";
@@ -24,7 +24,7 @@ function makeTransport(name: string) {
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: "dummy" });
}
describe("ECDHv1", function () {
describe("ECDHv2", function () {
beforeAll(async function () {
await global.Olm.init();
});
@@ -37,9 +37,9 @@ describe("ECDHv1", function () {
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
@@ -62,9 +62,9 @@ describe("ECDHv1", function () {
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
@@ -87,9 +87,9 @@ describe("ECDHv1", function () {
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
@@ -109,9 +109,9 @@ describe("ECDHv1", function () {
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
@@ -135,9 +135,9 @@ describe("ECDHv1", function () {
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
const bobChecksum = await bob.connect();
const aliceChecksum = await alice.connect();
@@ -159,7 +159,7 @@ describe("ECDHv1", function () {
bobTransport.otherParty = aliceTransport;
// alice is signing in initiates and generates a code
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
await bobTransport.send({ iv: "dummy", ciphertext: "dummy" });
+22 -22
View File
@@ -19,9 +19,9 @@ import MockHttpBackend from "matrix-mock-request";
import "../../olm-loader";
import { MSC3906Rendezvous, RendezvousCode, RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
import {
ECDHv1RendezvousCode,
ECDHv2RendezvousCode as ECDHRendezvousCode,
MSC3903ECDHPayload,
MSC3903ECDHv1RendezvousChannel,
MSC3903ECDHv2RendezvousChannel as MSC3903ECDHRendezvousChannel,
} from "../../../src/rendezvous/channels";
import { MatrixClient } from "../../../src";
import {
@@ -126,7 +126,7 @@ describe("Rendezvous", function () {
fallbackRzServer: "https://fallbackserver/rz",
fetchFn,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
expect(aliceRz.code).toBeUndefined();
@@ -143,7 +143,7 @@ describe("Rendezvous", function () {
const code = JSON.parse(aliceRz.code!) as RendezvousCode;
expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE);
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256");
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256");
expect(code.rendezvous?.transport.type).toEqual("org.matrix.msc3886.http.v1");
expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri).toEqual(
"https://fallbackserver/rz/123",
@@ -181,11 +181,11 @@ describe("Rendezvous", function () {
msc3882Enabled: false,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
@@ -193,7 +193,7 @@ describe("Rendezvous", function () {
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
@@ -235,11 +235,11 @@ describe("Rendezvous", function () {
msc3882Enabled: true,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
@@ -247,7 +247,7 @@ describe("Rendezvous", function () {
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
@@ -293,11 +293,11 @@ describe("Rendezvous", function () {
msc3882Enabled: true,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
@@ -305,7 +305,7 @@ describe("Rendezvous", function () {
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
@@ -351,11 +351,11 @@ describe("Rendezvous", function () {
msc3882Enabled: true,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
@@ -363,7 +363,7 @@ describe("Rendezvous", function () {
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
@@ -411,11 +411,11 @@ describe("Rendezvous", function () {
msc3882Enabled: true,
msc3886Enabled: false,
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
@@ -423,7 +423,7 @@ describe("Rendezvous", function () {
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
@@ -485,11 +485,11 @@ describe("Rendezvous", function () {
master: "mmmmm",
},
});
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
aliceTransport.onCancelled = aliceOnFailure;
await aliceRz.generateCode();
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
expect(code.rendezvous.key).toBeDefined();
@@ -497,7 +497,7 @@ describe("Rendezvous", function () {
// bob is try to sign in and scans the code
const bobOnFailure = jest.fn();
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
const bobEcdh = new MSC3903ECDHRendezvousChannel(
bobTransport,
decodeBase64(code.rendezvous.key), // alice's public key
bobOnFailure,
+16
View File
@@ -882,11 +882,27 @@ describe("Group Call", function () {
expect(await groupCall.setMicrophoneMuted(false)).toBe(false);
});
it("returns false when no permission for audio stream", async () => {
const groupCall = await createAndEnterGroupCall(mockClient, room);
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValueOnce(
new Error("No Permission"),
);
expect(await groupCall.setMicrophoneMuted(false)).toBe(false);
});
it("returns false when unmuting video with no video device", async () => {
const groupCall = await createAndEnterGroupCall(mockClient, room);
jest.spyOn(mockClient.getMediaHandler(), "hasVideoDevice").mockResolvedValue(false);
expect(await groupCall.setLocalVideoMuted(false)).toBe(false);
});
it("returns false when no permission for video stream", async () => {
const groupCall = await createAndEnterGroupCall(mockClient, room);
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValueOnce(
new Error("No Permission"),
);
expect(await groupCall.setLocalVideoMuted(false)).toBe(false);
});
});
describe("remote muting", () => {
+10
View File
@@ -242,6 +242,11 @@ describe("Media Handler", function () {
);
expect(await mediaHandler.hasAudioDevice()).toEqual(false);
});
it("returns false if the system not permitting access audio inputs", async () => {
mockMediaDevices.enumerateDevices.mockRejectedValueOnce(new Error("No Permission"));
expect(await mediaHandler.hasAudioDevice()).toEqual(false);
});
});
describe("hasVideoDevice", () => {
@@ -255,6 +260,11 @@ describe("Media Handler", function () {
);
expect(await mediaHandler.hasVideoDevice()).toEqual(false);
});
it("returns false if the system not permitting access video inputs", async () => {
mockMediaDevices.enumerateDevices.mockRejectedValueOnce(new Error("No Permission"));
expect(await mediaHandler.hasVideoDevice()).toEqual(false);
});
});
describe("getUserMediaStream", () => {
+1 -1
View File
@@ -20,5 +20,5 @@ export interface IIdentityServerProvider {
* for the associated client.
* @returns Promise which resolves to the access token.
*/
getAccessToken(): Promise<string>;
getAccessToken(): Promise<string | null>;
}
+17
View File
@@ -62,6 +62,7 @@ export function isDmMemberCountCondition(condition: AnyMemberCountCondition): bo
export enum ConditionKind {
EventMatch = "event_match",
EventPropertyIs = "event_property_is",
ContainsDisplayName = "contains_display_name",
RoomMemberCount = "room_member_count",
SenderNotificationPermission = "sender_notification_permission",
@@ -77,9 +78,16 @@ export interface IPushRuleCondition<N extends ConditionKind | string> {
export interface IEventMatchCondition extends IPushRuleCondition<ConditionKind.EventMatch> {
key: string;
pattern?: string;
// Note that value property is an optimization for patterns which do not do
// any globbing and when the key is not "content.body".
value?: string;
}
export interface IEventPropertyIsCondition extends IPushRuleCondition<ConditionKind.EventPropertyIs> {
key: string;
value: string | boolean | null | number;
}
export interface IContainsDisplayNameCondition extends IPushRuleCondition<ConditionKind.ContainsDisplayName> {
// no additional fields
}
@@ -105,6 +113,7 @@ export interface ICallStartedPrefixCondition extends IPushRuleCondition<Conditio
// IPushRuleCondition<Exclude<string, ConditionKind>> unfortunately does not resolve this at the time of writing.
export type PushRuleCondition =
| IEventMatchCondition
| IEventPropertyIsCondition
| IContainsDisplayNameCondition
| IRoomMemberCountCondition
| ISenderNotificationPermissionCondition
@@ -133,6 +142,14 @@ export enum RuleId {
IncomingCall = ".m.rule.call",
SuppressNotices = ".m.rule.suppress_notices",
Tombstone = ".m.rule.tombstone",
PollStart = ".m.rule.poll_start",
PollStartUnstable = ".org.matrix.msc3930.rule.poll_start",
PollEnd = ".m.rule.poll_end",
PollEndUnstable = ".org.matrix.msc3930.rule.poll_end",
PollStartOneToOne = ".m.rule.poll_start_one_to_one",
PollStartOneToOneUnstable = ".org.matrix.msc3930.rule.poll_start_one_to_one",
PollEndOneToOne = ".m.rule.poll_end_one_to_one",
PollEndOneToOneUnstable = ".org.matrix.msc3930.rule.poll_end_one_to_one",
}
export type PushRuleSet = {
+49 -20
View File
@@ -371,6 +371,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 {
@@ -1169,6 +1176,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 +1321,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 +1889,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 +1898,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
isPtt,
intent,
undefined,
dataChannelsEnabled,
dataChannelsEnabled || this.isVoipWithNoMediaAllowed,
dataChannelOptions,
this.isVoipWithNoMediaAllowed,
).create();
}
@@ -4130,7 +4142,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 +4153,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,
});
@@ -8496,10 +8511,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 +8773,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 +8816,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 +8825,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,
+325 -117
View File
@@ -74,6 +74,30 @@ export interface IOlmDevice<T = DeviceInfo> {
deviceInfo: T;
}
/** The result of parsing the an `m.room_key` or `m.forwarded_room_key` to-device event */
interface RoomKey {
/**
* The Curve25519 key of the megolm session creator.
*
* For `m.room_key`, this is also the sender of the `m.room_key` to-device event.
* For `m.forwarded_room_key`, the two are different (and the key of the sender of the
* `m.forwarded_room_key` event is included in `forwardingKeyChain`)
*/
senderKey: string;
sessionId: string;
sessionKey: string;
exportFormat: boolean;
roomId: string;
algorithm: string;
/**
* A list of the curve25519 keys of the users involved in forwarding this key, most recent last.
* For `m.room_key` events, this is empty.
*/
forwardingKeyChain: string[];
keysClaimed: Partial<Record<"ed25519", string>>;
extraSessionData: OlmGroupSessionExtraData;
}
export interface IOutboundGroupSessionKey {
chain_index: number;
key: string;
@@ -1475,13 +1499,20 @@ export class MegolmDecryption extends DecryptionAlgorithm {
}
}
public async onRoomKeyEvent(event: MatrixEvent): Promise<void> {
/**
* Parse a RoomKey out of an `m.room_key` event.
*
* @param event - the event containing the room key.
*
* @returns The `RoomKey` if it could be successfully parsed out of the
* event.
*
* @internal
*
*/
private roomKeyFromEvent(event: MatrixEvent): RoomKey | undefined {
const senderKey = event.getSenderKey()!;
const content = event.getContent<Partial<IMessage["content"]>>();
let senderKey = event.getSenderKey()!;
let forwardingKeyChain: string[] = [];
let exportFormat = false;
let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>;
const extraSessionData: OlmGroupSessionExtraData = {};
if (!content.room_id || !content.session_key || !content.session_id || !content.algorithm) {
@@ -1498,154 +1529,331 @@ export class MegolmDecryption extends DecryptionAlgorithm {
extraSessionData.sharedHistory = true;
}
if (event.getType() == "m.forwarded_room_key") {
const deviceInfo = this.crypto.deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, senderKey);
const senderKeyUser = this.baseApis.crypto!.deviceList.getUserByIdentityKey(
olmlib.OLM_ALGORITHM,
senderKey,
);
if (senderKeyUser !== event.getSender()) {
this.prefixedLogger.error("sending device does not belong to the user it claims to be from");
return;
}
const outgoingRequests = deviceInfo
? await this.crypto.cryptoStore.getOutgoingRoomKeyRequestsByTarget(
event.getSender()!,
deviceInfo.deviceId,
[RoomKeyRequestState.Sent],
)
: [];
const weRequested = outgoingRequests.some(
(req) =>
req.requestBody.room_id === content.room_id && req.requestBody.session_id === content.session_id,
);
const room = this.baseApis.getRoom(content.room_id);
const memberEvent = room?.getMember(this.userId)?.events.member;
const fromInviter =
memberEvent?.getSender() === event.getSender() ||
(memberEvent?.getUnsigned()?.prev_sender === event.getSender() &&
memberEvent?.getPrevContent()?.membership === "invite");
const fromUs = event.getSender() === this.baseApis.getUserId();
const roomKey: RoomKey = {
senderKey: senderKey,
sessionId: content.session_id,
sessionKey: content.session_key,
extraSessionData,
exportFormat: false,
roomId: content.room_id,
algorithm: content.algorithm,
forwardingKeyChain: [],
keysClaimed: event.getKeysClaimed(),
};
if (!weRequested && !fromUs) {
// If someone sends us an unsolicited key and they're
// not one of our other devices and it's not shared
// history, ignore it
if (!extraSessionData.sharedHistory) {
this.prefixedLogger.log("forwarded key not shared history - ignoring");
return;
}
return roomKey;
}
// If someone sends us an unsolicited key for a room
// we're already in, and they're not one of our other
// devices or the one who invited us, ignore it
if (room && !fromInviter) {
this.prefixedLogger.log("forwarded key not from inviter or from us - ignoring");
return;
}
}
/**
* Parse a RoomKey out of an `m.forwarded_room_key` event.
*
* @param event - the event containing the forwarded room key.
*
* @returns The `RoomKey` if it could be successfully parsed out of the
* event.
*
* @internal
*
*/
private forwardedRoomKeyFromEvent(event: MatrixEvent): RoomKey | undefined {
// the properties in m.forwarded_room_key are a superset of those in m.room_key, so
// start by parsing the m.room_key fields.
const roomKey = this.roomKeyFromEvent(event);
exportFormat = true;
forwardingKeyChain = Array.isArray(content.forwarding_curve25519_key_chain)
? content.forwarding_curve25519_key_chain
: [];
if (!roomKey) {
return;
}
// copy content before we modify it
forwardingKeyChain = forwardingKeyChain.slice();
forwardingKeyChain.push(senderKey);
const senderKey = event.getSenderKey()!;
const content = event.getContent<Partial<IMessage["content"]>>();
if (!content.sender_key) {
this.prefixedLogger.error("forwarded_room_key event is missing sender_key field");
return;
}
const senderKeyUser = this.baseApis.crypto!.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, senderKey);
const ed25519Key = content.sender_claimed_ed25519_key;
if (!ed25519Key) {
this.prefixedLogger.error(`forwarded_room_key_event is missing sender_claimed_ed25519_key field`);
return;
}
// We received this to-device event from event.getSenderKey(), but the original
// creator of the room key is claimed in the content.
const claimedCurve25519Key = content.sender_key;
const claimedEd25519Key = content.sender_claimed_ed25519_key;
keysClaimed = {
ed25519: ed25519Key,
};
let forwardingKeyChain = Array.isArray(content.forwarding_curve25519_key_chain)
? content.forwarding_curve25519_key_chain
: [];
// If this is a key for a room we're not in, don't load it
// yet, just park it in case *this sender* invites us to
// that room later
if (!room) {
const parkedData = {
senderId: event.getSender()!,
senderKey: content.sender_key,
sessionId: content.session_id,
sessionKey: content.session_key,
keysClaimed,
forwardingCurve25519KeyChain: forwardingKeyChain,
};
await this.crypto.cryptoStore.doTxn(
"readwrite",
["parked_shared_history"],
(txn) => this.crypto.cryptoStore.addParkedSharedHistory(content.room_id!, parkedData, txn),
this.prefixedLogger.withPrefix("[addParkedSharedHistory]"),
);
return;
}
// copy content before we modify it
forwardingKeyChain = forwardingKeyChain.slice();
forwardingKeyChain.push(senderKey);
const sendingDevice =
this.crypto.deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, senderKey) ?? undefined;
const deviceTrust = this.crypto.checkDeviceInfoTrust(event.getSender()!, sendingDevice);
// Check if we have all the fields we need.
if (senderKeyUser !== event.getSender()) {
this.prefixedLogger.error("sending device does not belong to the user it claims to be from");
return;
}
if (fromUs && !deviceTrust.isVerified()) {
return;
}
if (!claimedCurve25519Key) {
this.prefixedLogger.error("forwarded_room_key event is missing sender_key field");
return;
}
// forwarded keys are always untrusted
extraSessionData.untrusted = true;
if (!claimedEd25519Key) {
this.prefixedLogger.error(`forwarded_room_key_event is missing sender_claimed_ed25519_key field`);
return;
}
// replace the sender key with the sender key of the session
// creator for storage
senderKey = content.sender_key;
const keysClaimed = {
ed25519: claimedEd25519Key,
};
// FIXME: We're reusing the same field to track both:
//
// 1. The Olm identity we've received this room key from.
// 2. The Olm identity deduced (in the trusted case) or claiming (in the
// untrusted case) to be the original creator of this room key.
//
// We now overwrite the value tracking usage 1 with the value tracking usage 2.
roomKey.senderKey = claimedCurve25519Key;
// Replace our keysClaimed as well.
roomKey.keysClaimed = keysClaimed;
roomKey.exportFormat = true;
roomKey.forwardingKeyChain = forwardingKeyChain;
// forwarded keys are always untrusted
roomKey.extraSessionData.untrusted = true;
return roomKey;
}
/**
* Determine if we should accept the forwarded room key that was found in the given
* event.
*
* @param event - An `m.forwarded_room_key` event.
* @param roomKey - The room key that was found in the event.
*
* @returns promise that will resolve to a boolean telling us if it's ok to
* accept the given forwarded room key.
*
* @internal
*
*/
private async shouldAcceptForwardedKey(event: MatrixEvent, roomKey: RoomKey): Promise<boolean> {
const senderKey = event.getSenderKey()!;
const sendingDevice =
this.crypto.deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, senderKey) ?? undefined;
const deviceTrust = this.crypto.checkDeviceInfoTrust(event.getSender()!, sendingDevice);
// Using the plaintext sender here is fine since we checked that the
// sender matches to the user id in the device keys when this event was
// originally decrypted. This can obviously only happen if the device
// keys have been downloaded, but if they haven't the
// `deviceTrust.isVerified()` flag would be false as well.
//
// It would still be far nicer if the `sendingDevice` had a user ID
// attached to it that went through signature checks.
const fromUs = event.getSender() === this.baseApis.getUserId();
const keyFromOurVerifiedDevice = deviceTrust.isVerified() && fromUs;
const weRequested = await this.wasRoomKeyRequested(event, roomKey);
const fromInviter = this.wasRoomKeyForwardedByInviter(event, roomKey);
const sharedAsHistory = this.wasRoomKeyForwardedAsHistory(roomKey);
return (weRequested && keyFromOurVerifiedDevice) || (fromInviter && sharedAsHistory);
}
/**
* Did we ever request the given room key from the event sender and its
* accompanying device.
*
* @param event - An `m.forwarded_room_key` event.
* @param roomKey - The room key that was found in the event.
*
* @internal
*
*/
private async wasRoomKeyRequested(event: MatrixEvent, roomKey: RoomKey): Promise<boolean> {
// We send the `m.room_key_request` out as a wildcard to-device request,
// otherwise we would have to duplicate the same content for each
// device. This is why we need to pass in "*" as the device id here.
const outgoingRequests = await this.crypto.cryptoStore.getOutgoingRoomKeyRequestsByTarget(
event.getSender()!,
"*",
[RoomKeyRequestState.Sent],
);
return outgoingRequests.some(
(req) => req.requestBody.room_id === roomKey.roomId && req.requestBody.session_id === roomKey.sessionId,
);
}
private wasRoomKeyForwardedByInviter(event: MatrixEvent, roomKey: RoomKey): boolean {
// TODO: This is supposed to have a time limit. We should only accept
// such keys if we happen to receive them for a recently joined room.
const room = this.baseApis.getRoom(roomKey.roomId);
const senderKey = event.getSenderKey();
if (!senderKey) {
return false;
}
const senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, senderKey);
if (!senderKeyUser) {
return false;
}
const memberEvent = room?.getMember(this.userId)?.events.member;
const fromInviter =
memberEvent?.getSender() === senderKeyUser ||
(memberEvent?.getUnsigned()?.prev_sender === senderKeyUser &&
memberEvent?.getPrevContent()?.membership === "invite");
if (room && fromInviter) {
return true;
} else {
keysClaimed = event.getKeysClaimed();
return false;
}
}
if (content["org.matrix.msc3061.shared_history"]) {
extraSessionData.sharedHistory = true;
private wasRoomKeyForwardedAsHistory(roomKey: RoomKey): boolean {
const room = this.baseApis.getRoom(roomKey.roomId);
// If the key is not for a known room, then something fishy is going on,
// so we reject the key out of caution. In practice, this is a bit moot
// because we'll only accept shared_history forwarded by the inviter, and
// we won't know who was the inviter for an unknown room, so we'll reject
// it anyway.
if (room && roomKey.extraSessionData.sharedHistory) {
return true;
} else {
return false;
}
}
/**
* Check if a forwarded room key should be parked.
*
* A forwarded room key should be parked if it's a key for a room we're not
* in. We park the forwarded room key in case *this sender* invites us to
* that room later.
*/
private shouldParkForwardedKey(roomKey: RoomKey): boolean {
const room = this.baseApis.getRoom(roomKey.roomId);
if (!room && roomKey.extraSessionData.sharedHistory) {
return true;
} else {
return false;
}
}
/**
* Park the given room key to our store.
*
* @param event - An `m.forwarded_room_key` event.
* @param roomKey - The room key that was found in the event.
*
* @internal
*
*/
private async parkForwardedKey(event: MatrixEvent, roomKey: RoomKey): Promise<void> {
const parkedData = {
senderId: event.getSender()!,
senderKey: roomKey.senderKey,
sessionId: roomKey.sessionId,
sessionKey: roomKey.sessionKey,
keysClaimed: roomKey.keysClaimed,
forwardingCurve25519KeyChain: roomKey.forwardingKeyChain,
};
await this.crypto.cryptoStore.doTxn(
"readwrite",
["parked_shared_history"],
(txn) => this.crypto.cryptoStore.addParkedSharedHistory(roomKey.roomId, parkedData, txn),
logger.withPrefix("[addParkedSharedHistory]"),
);
}
/**
* Add the given room key to our store.
*
* @param roomKey - The room key that should be added to the store.
*
* @internal
*
*/
private async addRoomKey(roomKey: RoomKey): Promise<void> {
try {
await this.olmDevice.addInboundGroupSession(
content.room_id,
senderKey,
forwardingKeyChain,
content.session_id,
content.session_key,
keysClaimed,
exportFormat,
extraSessionData,
roomKey.roomId,
roomKey.senderKey,
roomKey.forwardingKeyChain,
roomKey.sessionId,
roomKey.sessionKey,
roomKey.keysClaimed,
roomKey.exportFormat,
roomKey.extraSessionData,
);
// have another go at decrypting events sent with this session.
if (await this.retryDecryption(senderKey, content.session_id, !extraSessionData.untrusted)) {
if (await this.retryDecryption(roomKey.senderKey, roomKey.sessionId, !roomKey.extraSessionData.untrusted)) {
// cancel any outstanding room key requests for this session.
// Only do this if we managed to decrypt every message in the
// session, because if we didn't, we leave the other key
// requests in the hopes that someone sends us a key that
// includes an earlier index.
this.crypto.cancelRoomKeyRequest({
algorithm: content.algorithm,
room_id: content.room_id,
session_id: content.session_id,
sender_key: senderKey,
algorithm: roomKey.algorithm,
room_id: roomKey.roomId,
session_id: roomKey.sessionId,
sender_key: roomKey.senderKey,
});
}
// don't wait for the keys to be backed up for the server
await this.crypto.backupManager.backupGroupSession(senderKey, content.session_id);
await this.crypto.backupManager.backupGroupSession(roomKey.senderKey, roomKey.sessionId);
} catch (e) {
this.prefixedLogger.error(`Error handling m.room_key_event: ${e}`);
}
}
/**
* Handle room keys that have been forwarded to us as an
* `m.forwarded_room_key` event.
*
* Forwarded room keys need special handling since we have no way of knowing
* who the original creator of the room key was. This naturally means that
* forwarded room keys are always untrusted and should only be accepted in
* some cases.
*
* @param event - An `m.forwarded_room_key` event.
*
* @internal
*
*/
private async onForwardedRoomKey(event: MatrixEvent): Promise<void> {
const roomKey = this.forwardedRoomKeyFromEvent(event);
if (!roomKey) {
return;
}
if (await this.shouldAcceptForwardedKey(event, roomKey)) {
await this.addRoomKey(roomKey);
} else if (this.shouldParkForwardedKey(roomKey)) {
await this.parkForwardedKey(event, roomKey);
}
}
public async onRoomKeyEvent(event: MatrixEvent): Promise<void> {
if (event.getType() == "m.forwarded_room_key") {
await this.onForwardedRoomKey(event);
} else {
const roomKey = this.roomKeyFromEvent(event);
if (!roomKey) {
return;
}
await this.addRoomKey(roomKey);
}
}
/**
* @param event - key event
*/
+1 -1
View File
@@ -70,7 +70,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
this.opts.idBaseUrl = url;
}
public idServerRequest<T extends Record<string, unknown>>(
public idServerRequest<T extends {} = Record<string, unknown>>(
method: Method,
path: string,
params: Record<string, string | string[]> | undefined,
+3 -17
View File
@@ -576,13 +576,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
}
public get replyEventId(): string | undefined {
// We're prefer ev.getContent() over ev.getWireContent() to make sure
// we grab the latest edit with potentially new relations. But we also
// can't just rely on ev.getContent() by itself because historically we
// still show the reply from the original message even though the edit
// event does not include the relation reply.
const mRelatesTo = this.getContent()["m.relates_to"] || this.getWireContent()["m.relates_to"];
return mRelatesTo?.["m.in_reply_to"]?.event_id;
return this.getWireContent()["m.relates_to"]?.["m.in_reply_to"]?.event_id;
}
public get relationEventId(): string | undefined {
@@ -799,22 +793,14 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* @returns array of recipients
*/
public getKeyRequestRecipients(userId: string): IKeyRequestRecipient[] {
// send the request to all of our own devices, and the
// original sending device if it wasn't us.
const wireContent = this.getWireContent();
// send the request to all of our own devices
const recipients = [
{
userId,
deviceId: "*",
},
];
const sender = this.getSender();
if (sender !== userId) {
recipients.push({
userId: sender!,
deviceId: wireContent.device_id,
});
}
return recipients;
}
+7 -2
View File
@@ -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,
@@ -2467,6 +2470,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 +2552,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) {
+178 -9
View File
@@ -25,6 +25,7 @@ import {
ICallStartedPrefixCondition,
IContainsDisplayNameCondition,
IEventMatchCondition,
IEventPropertyIsCondition,
IPushRule,
IPushRules,
IRoomMemberCountCondition,
@@ -123,6 +124,12 @@ export class PushProcessor {
*/
public constructor(private readonly client: MatrixClient) {}
/**
* Maps the original key from the push rules to a list of property names
* after unescaping.
*/
private readonly parsedKeys = new Map<string, string[]>();
/**
* Convert a list of actions into a object with the actions as keys and their values
* @example
@@ -162,7 +169,7 @@ export class PushProcessor {
if (!newRules) newRules = {} as IPushRules;
if (!newRules.global) newRules.global = {} as PushRuleSet;
if (!newRules.global.override) newRules.global.override = [];
if (!newRules.global.override) newRules.global.underride = [];
if (!newRules.global.underride) newRules.global.underride = [];
// Merge the client-level defaults with the ones from the server
const globalOverrides = newRules.global.override;
@@ -202,6 +209,53 @@ export class PushProcessor {
return newRules;
}
/**
* Pre-caches the parsed keys for push rules and cleans out any obsolete cache
* entries. Should be called after push rules are updated.
* @param newRules - The new push rules.
*/
public updateCachedPushRuleKeys(newRules: IPushRules): void {
// These lines are mostly to make the tests happy. We shouldn't run into these
// properties missing in practice.
if (!newRules) newRules = {} as IPushRules;
if (!newRules.global) newRules.global = {} as PushRuleSet;
if (!newRules.global.override) newRules.global.override = [];
if (!newRules.global.room) newRules.global.room = [];
if (!newRules.global.sender) newRules.global.sender = [];
if (!newRules.global.underride) newRules.global.underride = [];
// Process the 'key' property on event_match conditions pre-cache the
// values and clean-out any unused values.
const toRemoveKeys = new Set(this.parsedKeys.keys());
for (const ruleset of [
newRules.global.override,
newRules.global.room,
newRules.global.sender,
newRules.global.underride,
]) {
for (const rule of ruleset) {
if (!rule.conditions) {
continue;
}
for (const condition of rule.conditions) {
if (condition.kind !== ConditionKind.EventMatch) {
continue;
}
// Ensure we keep this key.
toRemoveKeys.delete(condition.key);
// Pre-process the key.
this.parsedKeys.set(condition.key, PushProcessor.partsForDottedKey(condition.key));
}
}
}
// Any keys that were previously cached, but are no longer needed should
// be removed.
toRemoveKeys.forEach((k) => this.parsedKeys.delete(k));
}
private static cachedGlobToRegex: Record<string, RegExp> = {}; // $glob: RegExp
private matchingRuleFromKindSet(ev: MatrixEvent, kindset: PushRuleSet): IAnnotatedPushRule | null {
@@ -284,6 +338,8 @@ export class PushProcessor {
switch (cond.kind) {
case ConditionKind.EventMatch:
return this.eventFulfillsEventMatchCondition(cond, ev);
case ConditionKind.EventPropertyIs:
return this.eventFulfillsEventPropertyIsCondition(cond, ev);
case ConditionKind.ContainsDisplayName:
return this.eventFulfillsDisplayNameCondition(cond, ev);
case ConditionKind.RoomMemberCount:
@@ -382,6 +438,13 @@ export class PushProcessor {
return content.body.search(pat) > -1;
}
/**
* Check whether the given event matches the push rule condition by fetching
* the property from the event and comparing against the condition's glob-based
* pattern.
* @param cond - The push rule condition to check for a match.
* @param ev - The event to check for a match.
*/
private eventFulfillsEventMatchCondition(cond: IEventMatchCondition, ev: MatrixEvent): boolean {
if (!cond.key) {
return false;
@@ -392,6 +455,9 @@ export class PushProcessor {
return false;
}
// XXX This does not match in a case-insensitive manner.
//
// See https://spec.matrix.org/v1.5/client-server-api/#conditions-1
if (cond.value) {
return cond.value === val;
}
@@ -408,6 +474,20 @@ export class PushProcessor {
return !!val.match(regex);
}
/**
* Check whether the given event matches the push rule condition by fetching
* the property from the event and comparing exactly against the condition's
* value.
* @param cond - The push rule condition to check for a match.
* @param ev - The event to check for a match.
*/
private eventFulfillsEventPropertyIsCondition(cond: IEventPropertyIsCondition, ev: MatrixEvent): boolean {
if (!cond.key || cond.value === undefined) {
return false;
}
return cond.value === this.valueForDottedKey(cond.key, ev);
}
private eventFulfillsCallStartedCondition(
_cond: ICallStartedCondition | ICallStartedPrefixCondition,
ev: MatrixEvent,
@@ -433,28 +513,105 @@ export class PushProcessor {
return PushProcessor.cachedGlobToRegex[glob];
}
/**
* Parse the key into the separate fields to search by splitting on
* unescaped ".", and then removing any escape characters.
*
* @param str - The key of the push rule condition: a dotted field.
* @returns The unescaped parts to fetch.
* @internal
*/
public static partsForDottedKey(str: string): string[] {
const result = [];
// The current field and whether the previous character was the escape
// character (a backslash).
let part = "";
let escaped = false;
// Iterate over each character, and decide whether to append to the current
// part (following the escape rules) or to start a new part (based on the
// field separator).
for (const c of str) {
// If the previous character was the escape character (a backslash)
// then decide what to append to the current part.
if (escaped) {
if (c === "\\" || c === ".") {
// An escaped backslash or dot just gets added.
part += c;
} else {
// A character that shouldn't be escaped gets the backslash prepended.
part += "\\" + c;
}
// This always resets being escaped.
escaped = false;
continue;
}
if (c == ".") {
// The field separator creates a new part.
result.push(part);
part = "";
} else if (c == "\\") {
// A backslash adds no characters, but starts an escape sequence.
escaped = true;
} else {
// Otherwise, just add the current character.
part += c;
}
}
// Ensure the final part is included. If there's an open escape sequence
// it should be included.
if (escaped) {
part += "\\";
}
result.push(part);
return result;
}
/**
* For a dotted field and event, fetch the value at that position, if one
* exists.
*
* @param key - The key of the push rule condition: a dotted field to fetch.
* @param ev - The matrix event to fetch the field from.
* @returns The value at the dotted path given by key.
*/
private valueForDottedKey(key: string, ev: MatrixEvent): any {
const parts = key.split(".");
// The key should already have been parsed via updateCachedPushRuleKeys,
// but if it hasn't (maybe via an old consumer of the SDK which hasn't
// been updated?) then lazily calculate it here.
let parts = this.parsedKeys.get(key);
if (parts === undefined) {
parts = PushProcessor.partsForDottedKey(key);
this.parsedKeys.set(key, parts);
}
let val: any;
// special-case the first component to deal with encrypted messages
const firstPart = parts[0];
let currentIndex = 0;
if (firstPart === "content") {
val = ev.getContent();
parts.shift();
++currentIndex;
} else if (firstPart === "type") {
val = ev.getType();
parts.shift();
++currentIndex;
} else {
// use the raw event for any other fields
val = ev.event;
}
while (parts.length > 0) {
const thisPart = parts.shift()!;
if (isNullOrUndefined(val[thisPart])) {
return null;
for (; currentIndex < parts.length; ++currentIndex) {
// The previous iteration resulted in null or undefined, bail (and
// avoid the type error of attempting to retrieve a property).
if (isNullOrUndefined(val)) {
return undefined;
}
const thisPart = parts[currentIndex];
val = val[thisPart];
}
return val;
@@ -507,6 +664,18 @@ export class PushProcessor {
* @returns The push rule, or null if no such rule was found
*/
public getPushRuleById(ruleId: string): IPushRule | null {
const result = this.getPushRuleAndKindById(ruleId);
return result?.rule ?? null;
}
/**
* Get one of the users push rules by its ID
*
* @param ruleId - The ID of the rule to search for
* @returns rule The push rule, or null if no such rule was found
* @returns kind - The PushRuleKind of the rule to search for
*/
public getPushRuleAndKindById(ruleId: string): { rule: IPushRule; kind: PushRuleKind } | null {
for (const scope of ["global"] as const) {
if (this.client.pushRules?.[scope] === undefined) continue;
@@ -514,7 +683,7 @@ export class PushProcessor {
if (this.client.pushRules[scope][kind] === undefined) continue;
for (const rule of this.client.pushRules[scope][kind]!) {
if (rule.rule_id === ruleId) return rule;
if (rule.rule_id === ruleId) return { rule, kind };
}
}
}
@@ -1,5 +1,5 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -25,20 +25,20 @@ import {
RendezvousTransport,
RendezvousFailureReason,
} from "..";
import { encodeBase64, decodeBase64 } from "../../crypto/olmlib";
import { encodeUnpaddedBase64, decodeBase64 } from "../../crypto/olmlib";
import { crypto, subtleCrypto, TextEncoder } from "../../crypto/crypto";
import { generateDecimalSas } from "../../crypto/verification/SASDecimal";
import { UnstableValue } from "../../NamespacedValue";
const ECDH_V1 = new UnstableValue(
"m.rendezvous.v1.curve25519-aes-sha256",
"org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256",
const ECDH_V2 = new UnstableValue(
"m.rendezvous.v2.curve25519-aes-sha256",
"org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256",
);
export interface ECDHv1RendezvousCode extends RendezvousCode {
export interface ECDHv2RendezvousCode extends RendezvousCode {
rendezvous: {
transport: RendezvousTransportDetails;
algorithm: typeof ECDH_V1.name | typeof ECDH_V1.altName;
algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName;
key: string;
};
}
@@ -46,7 +46,7 @@ export interface ECDHv1RendezvousCode extends RendezvousCode {
export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload;
export interface PlainTextPayload {
algorithm: typeof ECDH_V1.name | typeof ECDH_V1.altName;
algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName;
key?: string;
}
@@ -70,7 +70,7 @@ async function importKey(key: Uint8Array): Promise<CryptoKey> {
* X25519/ECDH key agreement based secure rendezvous channel.
* Note that this is UNSTABLE and may have breaking changes without notice.
*/
export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
export class MSC3903ECDHv2RendezvousChannel<T> implements RendezvousChannel<T> {
private olmSAS?: SAS;
private ourPublicKey: Uint8Array;
private aesKey?: CryptoKey;
@@ -85,17 +85,17 @@ export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
this.ourPublicKey = decodeBase64(this.olmSAS.get_pubkey());
}
public async generateCode(intent: RendezvousIntent): Promise<ECDHv1RendezvousCode> {
public async generateCode(intent: RendezvousIntent): Promise<ECDHv2RendezvousCode> {
if (this.transport.ready) {
throw new Error("Code already generated");
}
await this.transport.send({ algorithm: ECDH_V1.name });
await this.transport.send({ algorithm: ECDH_V2.name });
const rendezvous: ECDHv1RendezvousCode = {
const rendezvous: ECDHv2RendezvousCode = {
rendezvous: {
algorithm: ECDH_V1.name,
key: encodeBase64(this.ourPublicKey),
algorithm: ECDH_V2.name,
key: encodeUnpaddedBase64(this.ourPublicKey),
transport: await this.transport.details(),
},
intent,
@@ -123,7 +123,7 @@ export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
}
const res = rawRes as Partial<PlainTextPayload>;
const { key, algorithm } = res;
if (!algorithm || !ECDH_V1.matches(algorithm) || !key) {
if (!algorithm || !ECDH_V2.matches(algorithm) || !key) {
throw new RendezvousError(
"Unsupported algorithm: " + algorithm,
RendezvousFailureReason.UnsupportedAlgorithm,
@@ -134,20 +134,20 @@ export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
} else {
// send our public key unencrypted
await this.transport.send({
algorithm: ECDH_V1.name,
key: encodeBase64(this.ourPublicKey),
algorithm: ECDH_V2.name,
key: encodeUnpaddedBase64(this.ourPublicKey),
});
}
this.connected = true;
this.olmSAS.set_their_key(encodeBase64(this.theirPublicKey!));
this.olmSAS.set_their_key(encodeUnpaddedBase64(this.theirPublicKey!));
const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!;
const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey;
let aesInfo = ECDH_V1.name;
aesInfo += `|${encodeBase64(initiatorKey)}`;
aesInfo += `|${encodeBase64(recipientKey)}`;
let aesInfo = ECDH_V2.name;
aesInfo += `|${encodeUnpaddedBase64(initiatorKey)}`;
aesInfo += `|${encodeUnpaddedBase64(recipientKey)}`;
const aesKeyBytes = this.olmSAS.generate_bytes(aesInfo, 32);
@@ -181,8 +181,8 @@ export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
);
return {
iv: encodeBase64(iv),
ciphertext: encodeBase64(ciphertext),
iv: encodeUnpaddedBase64(iv),
ciphertext: encodeUnpaddedBase64(ciphertext),
};
}
+1 -1
View File
@@ -14,4 +14,4 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export * from "./MSC3903ECDHv1RendezvousChannel";
export * from "./MSC3903ECDHv2RendezvousChannel";
+31
View File
@@ -0,0 +1,31 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* This file replaces rust-crypto/index.ts when the js-sdk is being built for browserify.
*
* It is a stub, so that we do not import the whole of the base64'ed wasm artifact into the browserify bundle.
* It deliberately does nothing except raise an exception.
*/
import { IHttpOpts, MatrixHttpApi } from "../http-api";
export async function initRustCrypto(
_http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
_userId: string,
_deviceId: string,
): Promise<Crypto> {
throw new Error("Rust crypto is not supported under browserify.");
}
+1 -2
View File
@@ -43,7 +43,6 @@ import {
} from "./sliding-sync";
import { EventType } from "./@types/event";
import { IPushRules } from "./@types/PushRules";
import { PushProcessor } from "./pushprocessor";
import { RoomStateEvent } from "./models/room-state";
import { RoomMemberEvent } from "./models/room-member";
@@ -262,7 +261,7 @@ class ExtensionAccountData implements Extension<ExtensionAccountDataRequest, Ext
// (see sync) before syncing over the network.
if (accountDataEvent.getType() === EventType.PushRules) {
const rules = accountDataEvent.getContent<IPushRules>();
this.client.pushRules = PushProcessor.rewriteDefaultRules(rules);
this.client.setPushRules(rules);
}
const prevEvent = prevEventsMap[accountDataEvent.getType()];
this.client.emit(ClientEvent.AccountData, accountDataEvent, prevEvent);
+1 -2
View File
@@ -32,7 +32,6 @@ import * as utils from "./utils";
import { IDeferred } 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";
@@ -1162,7 +1161,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);
+10 -1
View File
@@ -390,6 +390,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// Used to keep the timer for the delay before actually stopping our
// video track after muting (see setLocalVideoMuted)
private stopVideoTrackTimer?: ReturnType<typeof setTimeout>;
// Used to allow connection without Video and Audio. To establish a webrtc connection without media a Data channel is
// needed At the moment this property is true if we allow MatrixClient with isVoipWithNoMediaAllowed = true
private readonly isOnlyDataChannelAllowed: boolean;
/**
* Construct a new Matrix Call.
@@ -420,6 +423,8 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
utils.checkObjectHasKeys(server, ["urls"]);
}
this.callId = genCallID();
// If the Client provides calls without audio and video we need a datachannel for a webrtc connection
this.isOnlyDataChannelAllowed = this.client.isVoipWithNoMediaAllowed;
}
/**
@@ -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!`,
);
+51 -6
View File
@@ -196,6 +196,7 @@ export class GroupCall extends TypedEventEmitter<
public readonly userMediaFeeds: CallFeed[] = [];
public readonly screenshareFeeds: CallFeed[] = [];
public groupCallId: string;
public readonly allowCallWithoutVideoAndAudio: boolean;
private readonly calls = new Map<string, Map<string, MatrixCall>>(); // user_id -> device_id -> MatrixCall
private callHandlers = new Map<string, Map<string, ICallHandlers>>(); // user_id -> device_id -> ICallHandlers
@@ -219,6 +220,7 @@ export class GroupCall extends TypedEventEmitter<
groupCallId?: string,
private dataChannelsEnabled?: boolean,
private dataChannelOptions?: IGroupCallDataChannelOptions,
isCallWithoutVideoAndAudio?: boolean,
) {
super();
this.reEmitter = new ReEmitter(this);
@@ -231,6 +233,7 @@ export class GroupCall extends TypedEventEmitter<
this.on(GroupCallEvent.ParticipantsChanged, this.onParticipantsChanged);
this.on(GroupCallEvent.GroupCallStateChanged, this.onStateChanged);
this.on(GroupCallEvent.LocalScreenshareStateChanged, this.onLocalFeedsChanged);
this.allowCallWithoutVideoAndAudio = !!isCallWithoutVideoAndAudio;
}
public async create(): Promise<GroupCall> {
@@ -374,8 +377,15 @@ export class GroupCall extends TypedEventEmitter<
try {
stream = await this.client.getMediaHandler().getUserMediaStream(true, this.type === GroupCallType.Video);
} catch (error) {
this.state = GroupCallState.LocalCallFeedUninitialized;
throw error;
// If is allowed to join a call without a media stream, then we
// don't throw an error here. But we need an empty Local Feed to establish
// a connection later.
if (this.allowCallWithoutVideoAndAudio) {
stream = new MediaStream();
} else {
this.state = GroupCallState.LocalCallFeedUninitialized;
throw error;
}
}
// The call could've been disposed while we were waiting, and could
@@ -584,6 +594,31 @@ export class GroupCall extends TypedEventEmitter<
logger.log(
`GroupCall ${this.groupCallId} setMicrophoneMuted() (streamId=${this.localCallFeed.stream.id}, muted=${muted})`,
);
// We needed this here to avoid an error in case user join a call without a device.
// I can not use .then .catch functions because linter :-(
try {
if (!muted) {
const stream = await this.client
.getMediaHandler()
.getUserMediaStream(true, !this.localCallFeed.isVideoMuted());
if (stream === null) {
// if case permission denied to get a stream stop this here
/* istanbul ignore next */
logger.log(
`GroupCall ${this.groupCallId} setMicrophoneMuted() no device to receive local stream, muted=${muted}`,
);
return false;
}
}
} catch (e) {
/* istanbul ignore next */
logger.log(
`GroupCall ${this.groupCallId} setMicrophoneMuted() no device or permission to receive local stream, muted=${muted}`,
);
return false;
}
this.localCallFeed.setAudioVideoMuted(muted, null);
// I don't believe its actually necessary to enable these tracks: they
// are the one on the GroupCall's own CallFeed and are cloned before being
@@ -617,14 +652,24 @@ export class GroupCall extends TypedEventEmitter<
}
if (this.localCallFeed) {
/* istanbul ignore next */
logger.log(
`GroupCall ${this.groupCallId} setLocalVideoMuted() (stream=${this.localCallFeed.stream.id}, muted=${muted})`,
);
const stream = await this.client.getMediaHandler().getUserMediaStream(true, !muted);
await this.updateLocalUsermediaStream(stream);
this.localCallFeed.setAudioVideoMuted(null, muted);
setTracksEnabled(this.localCallFeed.stream.getVideoTracks(), !muted);
try {
const stream = await this.client.getMediaHandler().getUserMediaStream(true, !muted);
await this.updateLocalUsermediaStream(stream);
this.localCallFeed.setAudioVideoMuted(null, muted);
setTracksEnabled(this.localCallFeed.stream.getVideoTracks(), !muted);
} catch (_) {
// No permission to video device
/* istanbul ignore next */
logger.log(
`GroupCall ${this.groupCallId} setLocalVideoMuted() no device or permission to receive local stream, muted=${muted}`,
);
return false;
}
} else {
logger.log(`GroupCall ${this.groupCallId} setLocalVideoMuted() no stream muted (muted=${muted})`);
this.initWithVideoMuted = muted;
+4 -1
View File
@@ -184,8 +184,11 @@ export class GroupCallEventHandler {
isPtt,
callIntent,
groupCallId,
content?.dataChannelsEnabled,
// Because without Media section a WebRTC connection is not possible, so need a RTCDataChannel to set up a
// no media WebRTC connection anyway.
content?.dataChannelsEnabled || this.client.isVoipWithNoMediaAllowed,
dataChannelOptions,
this.client.isVoipWithNoMediaAllowed,
);
this.groupCalls.set(room.roomId, groupCall);
+14 -4
View File
@@ -185,13 +185,23 @@ export class MediaHandler extends TypedEventEmitter<
}
public async hasAudioDevice(): Promise<boolean> {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter((device) => device.kind === "audioinput").length > 0;
try {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter((device) => device.kind === "audioinput").length > 0;
} catch (err) {
logger.log(`MediaHandler hasAudioDevice() calling navigator.mediaDevices.enumerateDevices with error`, err);
return false;
}
}
public async hasVideoDevice(): Promise<boolean> {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter((device) => device.kind === "videoinput").length > 0;
try {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter((device) => device.kind === "videoinput").length > 0;
} catch (err) {
logger.log(`MediaHandler hasVideoDevice() calling navigator.mediaDevices.enumerateDevices with error`, err);
return false;
}
}
/**
+721 -494
View File
File diff suppressed because it is too large Load Diff