Compare commits
27 Commits
v23.4.0-rc.2
...
v23.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 26663e67fd | |||
| 0cfc67c679 | |||
| 13d95c8219 | |||
| e119dc4e89 | |||
| b4cdc5a923 | |||
| a82e22b5de | |||
| c894d09d8c | |||
| 585ce07260 | |||
| 8cbbdaa239 | |||
| cd526a254d | |||
| e782a2afa3 | |||
| 565339b1fd | |||
| 493203050a | |||
| 41782c4593 | |||
| 86256a4e74 | |||
| 933a0c9909 | |||
| c8a4d9b88a | |||
| 437128d11b | |||
| c18d09fd22 | |||
| 2a5e5e6a59 | |||
| 68e354752b | |||
| d80b7499fd | |||
| 9c8093eb3e | |||
| aec1c11037 | |||
| e2e9986059 | |||
| d70ffdbc02 | |||
| 21cc9c3d8a |
@@ -14,7 +14,7 @@ jobs:
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@bd10f381a96414ce2b13a11bfa89902ba7cea07f # v2.24.3
|
||||
uses: dawidd6/action-download-artifact@5e780fc7bbd0cac69fc73271ed86edf5dcb72d67 # v2.26.0
|
||||
with:
|
||||
workflow: static_analysis.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -19,3 +19,4 @@ out
|
||||
|
||||
.vscode
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
+15
-7
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 () {
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
import { Crypto } from "../../../src/crypto";
|
||||
import { Crypto, IEventDecryptionResult } from "../../../src/crypto";
|
||||
|
||||
describe("MatrixEvent", () => {
|
||||
it("should create copies of itself", () => {
|
||||
@@ -182,4 +182,38 @@ describe("MatrixEvent", () => {
|
||||
expect(encryptedEvent.getType()).toEqual("m.room.message");
|
||||
});
|
||||
});
|
||||
|
||||
describe("replyEventId", () => {
|
||||
it("should ignore 'm.relates_to' from encrypted content even if cleartext lacks one", async () => {
|
||||
const eventId = "test_encrypted_event";
|
||||
const encryptedEvent = new MatrixEvent({
|
||||
event_id: eventId,
|
||||
type: "m.room.encrypted",
|
||||
content: {
|
||||
ciphertext: "secrets",
|
||||
},
|
||||
});
|
||||
|
||||
const crypto = {
|
||||
decryptEvent: jest.fn().mockImplementationOnce(() => {
|
||||
return Promise.resolve<IEventDecryptionResult>({
|
||||
clearEvent: {
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "!anotherEvent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
} as unknown as Crypto;
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
expect(encryptedEvent.getType()).toEqual("m.room.message");
|
||||
expect(encryptedEvent.replyEventId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+204
-17
@@ -1,6 +1,6 @@
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { IActionsObject, PushProcessor } from "../../src/pushprocessor";
|
||||
import { EventType, IContent, MatrixClient, MatrixEvent } from "../../src";
|
||||
import { ConditionKind, EventType, IContent, MatrixClient, MatrixEvent, PushRuleActionName } from "../../src";
|
||||
|
||||
describe("NotificationService", function () {
|
||||
const testUserId = "@ali:matrix.org";
|
||||
@@ -11,6 +11,23 @@ describe("NotificationService", function () {
|
||||
|
||||
let pushProcessor: PushProcessor;
|
||||
|
||||
const msc3914RoomCallRule = {
|
||||
rule_id: ".org.matrix.msc3914.rule.room.call",
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: [
|
||||
{
|
||||
kind: "event_match",
|
||||
key: "type",
|
||||
pattern: "org.matrix.msc3401.call",
|
||||
},
|
||||
{
|
||||
kind: "call_started",
|
||||
},
|
||||
],
|
||||
actions: ["notify", { set_tweak: "sound", value: "default" }],
|
||||
};
|
||||
|
||||
// These would be better if individual rules were configured in the tests themselves.
|
||||
const matrixClient = {
|
||||
getRoom: function () {
|
||||
@@ -163,26 +180,11 @@ describe("NotificationService", function () {
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.room_one_to_one",
|
||||
},
|
||||
{
|
||||
rule_id: ".org.matrix.msc3914.rule.room.call",
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: [
|
||||
{
|
||||
kind: "event_match",
|
||||
key: "type",
|
||||
pattern: "org.matrix.msc3401.call",
|
||||
},
|
||||
{
|
||||
kind: "call_started",
|
||||
},
|
||||
],
|
||||
actions: ["notify", { set_tweak: "sound", value: "default" }],
|
||||
},
|
||||
],
|
||||
room: [],
|
||||
sender: [],
|
||||
underride: [
|
||||
msc3914RoomCallRule,
|
||||
{
|
||||
actions: ["dont-notify"],
|
||||
conditions: [
|
||||
@@ -287,6 +289,13 @@ describe("NotificationService", function () {
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
// TODO: This is not spec compliant behaviour.
|
||||
//
|
||||
// See https://spec.matrix.org/v1.5/client-server-api/#conditions-1 which
|
||||
// describes pattern should glob:
|
||||
//
|
||||
// 1. * matches 0 or more characters;
|
||||
// 2. ? matches exactly one character
|
||||
it("should bing on character group ([abc]) bing words.", function () {
|
||||
testEvent.event.content!.body = "Ping!";
|
||||
let actions = pushProcessor.actionsForEvent(testEvent);
|
||||
@@ -296,12 +305,14 @@ describe("NotificationService", function () {
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
// TODO: This is not spec compliant behaviour. (See above.)
|
||||
it("should bing on character range ([a-z]) bing words.", function () {
|
||||
testEvent.event.content!.body = "I ate 6 pies";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
// TODO: This is not spec compliant behaviour. (See above.)
|
||||
it("should bing on character negation ([!a]) bing words.", function () {
|
||||
testEvent.event.content!.body = "boke";
|
||||
let actions = pushProcessor.actionsForEvent(testEvent);
|
||||
@@ -330,6 +341,8 @@ describe("NotificationService", function () {
|
||||
// invalid
|
||||
|
||||
it("should gracefully handle bad input.", function () {
|
||||
// The following body is an object (not a string) and thus is invalid
|
||||
// for matching against.
|
||||
testEvent.event.content!.body = { foo: "bar" };
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(false);
|
||||
@@ -493,4 +506,178 @@ describe("NotificationService", function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test exact event matching", () => {
|
||||
it.each([
|
||||
// Simple string matching.
|
||||
{ value: "bar", eventValue: "bar", expected: true },
|
||||
// Matches are case-sensitive.
|
||||
{ value: "bar", eventValue: "BAR", expected: false },
|
||||
// Matches must match the full string.
|
||||
{ value: "bar", eventValue: "barbar", expected: false },
|
||||
// Values should not be type-coerced.
|
||||
{ value: "bar", eventValue: true, expected: false },
|
||||
{ value: "bar", eventValue: 1, expected: false },
|
||||
{ value: "bar", eventValue: false, expected: false },
|
||||
// Boolean matching.
|
||||
{ value: true, eventValue: true, expected: true },
|
||||
{ value: false, eventValue: false, expected: true },
|
||||
// Types should not be coerced.
|
||||
{ value: true, eventValue: "true", expected: false },
|
||||
{ value: true, eventValue: 1, expected: false },
|
||||
{ value: false, eventValue: null, expected: false },
|
||||
// Null matching.
|
||||
{ value: null, eventValue: null, expected: true },
|
||||
// Types should not be coerced
|
||||
{ value: null, eventValue: false, expected: false },
|
||||
{ value: null, eventValue: 0, expected: false },
|
||||
{ value: null, eventValue: "", expected: false },
|
||||
{ value: null, eventValue: undefined, expected: false },
|
||||
// Compound values should never be matched.
|
||||
{ value: "bar", eventValue: ["bar"], expected: false },
|
||||
{ value: "bar", eventValue: { bar: true }, expected: false },
|
||||
{ value: true, eventValue: [true], expected: false },
|
||||
{ value: true, eventValue: { true: true }, expected: false },
|
||||
{ value: null, eventValue: [], expected: false },
|
||||
{ value: null, eventValue: {}, expected: false },
|
||||
])("test $value against $eventValue", ({ value, eventValue, expected }) => {
|
||||
matrixClient.pushRules! = {
|
||||
global: {
|
||||
override: [
|
||||
{
|
||||
actions: [PushRuleActionName.Notify],
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.EventPropertyIs,
|
||||
key: "content.foo",
|
||||
value: value,
|
||||
},
|
||||
],
|
||||
default: true,
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.test",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
testEvent = utils.mkEvent({
|
||||
type: "m.room.message",
|
||||
room: testRoomId,
|
||||
user: "@alfred:localhost",
|
||||
event: true,
|
||||
content: {
|
||||
foo: eventValue,
|
||||
},
|
||||
});
|
||||
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
if (expected) {
|
||||
expect(actions?.notify).toBeTruthy();
|
||||
} else {
|
||||
expect(actions?.notify).toBeFalsy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
// The properly escaped key works.
|
||||
{ key: "content.m\\.test.foo", pattern: "bar", expected: true },
|
||||
// An unescaped version does not match.
|
||||
{ key: "content.m.test.foo", pattern: "bar", expected: false },
|
||||
// Over escaping does not match.
|
||||
{ key: "content.m\\.test\\.foo", pattern: "bar", expected: false },
|
||||
// Escaping backslashes should match.
|
||||
{ key: "content.m\\\\example", pattern: "baz", expected: true },
|
||||
// An unnecessary escape sequence leaves the backslash and still matches.
|
||||
{ key: "content.m\\example", pattern: "baz", expected: true },
|
||||
])("test against escaped dotted paths '$key'", ({ key, pattern, expected }) => {
|
||||
testEvent = utils.mkEvent({
|
||||
type: "m.room.message",
|
||||
room: testRoomId,
|
||||
user: "@alfred:localhost",
|
||||
event: true,
|
||||
content: {
|
||||
// A dot in the field name.
|
||||
"m.test": { foo: "bar" },
|
||||
// A backslash in a field name.
|
||||
"m\\example": "baz",
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
pushProcessor.ruleMatchesEvent(
|
||||
{
|
||||
rule_id: "rule1",
|
||||
actions: [],
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: key,
|
||||
pattern: pattern,
|
||||
},
|
||||
],
|
||||
default: false,
|
||||
enabled: true,
|
||||
},
|
||||
testEvent,
|
||||
),
|
||||
).toBe(expected);
|
||||
});
|
||||
|
||||
describe("getPushRuleById()", () => {
|
||||
it("returns null when rule id is not in rule set", () => {
|
||||
expect(pushProcessor.getPushRuleById("non-existant-rule")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns push rule when it is found in rule set", () => {
|
||||
expect(pushProcessor.getPushRuleById(".org.matrix.msc3914.rule.room.call")).toEqual(msc3914RoomCallRule);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPushRuleAndKindById()", () => {
|
||||
it("returns null when rule id is not in rule set", () => {
|
||||
expect(pushProcessor.getPushRuleAndKindById("non-existant-rule")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns push rule when it is found in rule set", () => {
|
||||
expect(pushProcessor.getPushRuleAndKindById(".org.matrix.msc3914.rule.room.call")).toEqual({
|
||||
kind: "underride",
|
||||
rule: msc3914RoomCallRule,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test PushProcessor.partsForDottedKey", function () {
|
||||
it.each([
|
||||
// A field with no dots.
|
||||
["m", ["m"]],
|
||||
// Simple dotted fields.
|
||||
["m.foo", ["m", "foo"]],
|
||||
["m.foo.bar", ["m", "foo", "bar"]],
|
||||
// Backslash is used as an escape character.
|
||||
["m\\.foo", ["m.foo"]],
|
||||
["m\\\\.foo", ["m\\", "foo"]],
|
||||
["m\\\\\\.foo", ["m\\.foo"]],
|
||||
["m\\\\\\\\.foo", ["m\\\\", "foo"]],
|
||||
["m\\foo", ["m\\foo"]],
|
||||
["m\\\\foo", ["m\\foo"]],
|
||||
["m\\\\\\foo", ["m\\\\foo"]],
|
||||
["m\\\\\\\\foo", ["m\\\\foo"]],
|
||||
// Ensure that escapes at the end don't cause issues.
|
||||
["m.foo\\", ["m", "foo\\"]],
|
||||
["m.foo\\\\", ["m", "foo\\"]],
|
||||
["m.foo\\.", ["m", "foo."]],
|
||||
["m.foo\\\\.", ["m", "foo\\", ""]],
|
||||
["m.foo\\\\\\.", ["m", "foo\\."]],
|
||||
// Empty parts (corresponding to properties which are an empty string) are allowed.
|
||||
[".m", ["", "m"]],
|
||||
["..m", ["", "", "m"]],
|
||||
["m.", ["m", ""]],
|
||||
["m..", ["m", "", ""]],
|
||||
["m..foo", ["m", "", "foo"]],
|
||||
])("partsFotDottedKey for %s", (path: string, expected: string[]) => {
|
||||
expect(PushProcessor.partsForDottedKey(path)).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import "../../olm-loader";
|
||||
import { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
|
||||
import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from "../../../src/rendezvous/channels";
|
||||
import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "../../../src/rendezvous/channels";
|
||||
import { decodeBase64 } from "../../../src/crypto/olmlib";
|
||||
import { DummyTransport } from "./DummyTransport";
|
||||
|
||||
@@ -24,7 +24,7 @@ function makeTransport(name: string) {
|
||||
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: "dummy" });
|
||||
}
|
||||
|
||||
describe("ECDHv1", function () {
|
||||
describe("ECDHv2", function () {
|
||||
beforeAll(async function () {
|
||||
await global.Olm.init();
|
||||
});
|
||||
@@ -37,9 +37,9 @@ describe("ECDHv1", function () {
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
@@ -62,9 +62,9 @@ describe("ECDHv1", function () {
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
@@ -87,9 +87,9 @@ describe("ECDHv1", function () {
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
@@ -109,9 +109,9 @@ describe("ECDHv1", function () {
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
@@ -135,9 +135,9 @@ describe("ECDHv1", function () {
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
@@ -159,7 +159,7 @@ describe("ECDHv1", function () {
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
|
||||
await bobTransport.send({ iv: "dummy", ciphertext: "dummy" });
|
||||
@@ -19,9 +19,9 @@ import MockHttpBackend from "matrix-mock-request";
|
||||
import "../../olm-loader";
|
||||
import { MSC3906Rendezvous, RendezvousCode, RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
|
||||
import {
|
||||
ECDHv1RendezvousCode,
|
||||
ECDHv2RendezvousCode as ECDHRendezvousCode,
|
||||
MSC3903ECDHPayload,
|
||||
MSC3903ECDHv1RendezvousChannel,
|
||||
MSC3903ECDHv2RendezvousChannel as MSC3903ECDHRendezvousChannel,
|
||||
} from "../../../src/rendezvous/channels";
|
||||
import { MatrixClient } from "../../../src";
|
||||
import {
|
||||
@@ -126,7 +126,7 @@ describe("Rendezvous", function () {
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
|
||||
expect(aliceRz.code).toBeUndefined();
|
||||
@@ -143,7 +143,7 @@ describe("Rendezvous", function () {
|
||||
const code = JSON.parse(aliceRz.code!) as RendezvousCode;
|
||||
|
||||
expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE);
|
||||
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256");
|
||||
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256");
|
||||
expect(code.rendezvous?.transport.type).toEqual("org.matrix.msc3886.http.v1");
|
||||
expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri).toEqual(
|
||||
"https://fallbackserver/rz/123",
|
||||
@@ -181,11 +181,11 @@ describe("Rendezvous", function () {
|
||||
msc3882Enabled: false,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
@@ -193,7 +193,7 @@ describe("Rendezvous", function () {
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
@@ -235,11 +235,11 @@ describe("Rendezvous", function () {
|
||||
msc3882Enabled: true,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
@@ -247,7 +247,7 @@ describe("Rendezvous", function () {
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
@@ -293,11 +293,11 @@ describe("Rendezvous", function () {
|
||||
msc3882Enabled: true,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
@@ -305,7 +305,7 @@ describe("Rendezvous", function () {
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
@@ -351,11 +351,11 @@ describe("Rendezvous", function () {
|
||||
msc3882Enabled: true,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
@@ -363,7 +363,7 @@ describe("Rendezvous", function () {
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
@@ -411,11 +411,11 @@ describe("Rendezvous", function () {
|
||||
msc3882Enabled: true,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
@@ -423,7 +423,7 @@ describe("Rendezvous", function () {
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
@@ -485,11 +485,11 @@ describe("Rendezvous", function () {
|
||||
master: "mmmmm",
|
||||
},
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
@@ -497,7 +497,7 @@ describe("Rendezvous", function () {
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
|
||||
@@ -882,11 +882,27 @@ describe("Group Call", function () {
|
||||
expect(await groupCall.setMicrophoneMuted(false)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when no permission for audio stream", async () => {
|
||||
const groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValueOnce(
|
||||
new Error("No Permission"),
|
||||
);
|
||||
expect(await groupCall.setMicrophoneMuted(false)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when unmuting video with no video device", async () => {
|
||||
const groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
jest.spyOn(mockClient.getMediaHandler(), "hasVideoDevice").mockResolvedValue(false);
|
||||
expect(await groupCall.setLocalVideoMuted(false)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when no permission for video stream", async () => {
|
||||
const groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValueOnce(
|
||||
new Error("No Permission"),
|
||||
);
|
||||
expect(await groupCall.setLocalVideoMuted(false)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("remote muting", () => {
|
||||
|
||||
@@ -242,6 +242,11 @@ describe("Media Handler", function () {
|
||||
);
|
||||
expect(await mediaHandler.hasAudioDevice()).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns false if the system not permitting access audio inputs", async () => {
|
||||
mockMediaDevices.enumerateDevices.mockRejectedValueOnce(new Error("No Permission"));
|
||||
expect(await mediaHandler.hasAudioDevice()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasVideoDevice", () => {
|
||||
@@ -255,6 +260,11 @@ describe("Media Handler", function () {
|
||||
);
|
||||
expect(await mediaHandler.hasVideoDevice()).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns false if the system not permitting access video inputs", async () => {
|
||||
mockMediaDevices.enumerateDevices.mockRejectedValueOnce(new Error("No Permission"));
|
||||
expect(await mediaHandler.hasVideoDevice()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserMediaStream", () => {
|
||||
|
||||
@@ -20,5 +20,5 @@ export interface IIdentityServerProvider {
|
||||
* for the associated client.
|
||||
* @returns Promise which resolves to the access token.
|
||||
*/
|
||||
getAccessToken(): Promise<string>;
|
||||
getAccessToken(): Promise<string | null>;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ export function isDmMemberCountCondition(condition: AnyMemberCountCondition): bo
|
||||
|
||||
export enum ConditionKind {
|
||||
EventMatch = "event_match",
|
||||
EventPropertyIs = "event_property_is",
|
||||
ContainsDisplayName = "contains_display_name",
|
||||
RoomMemberCount = "room_member_count",
|
||||
SenderNotificationPermission = "sender_notification_permission",
|
||||
@@ -77,9 +78,16 @@ export interface IPushRuleCondition<N extends ConditionKind | string> {
|
||||
export interface IEventMatchCondition extends IPushRuleCondition<ConditionKind.EventMatch> {
|
||||
key: string;
|
||||
pattern?: string;
|
||||
// Note that value property is an optimization for patterns which do not do
|
||||
// any globbing and when the key is not "content.body".
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export interface IEventPropertyIsCondition extends IPushRuleCondition<ConditionKind.EventPropertyIs> {
|
||||
key: string;
|
||||
value: string | boolean | null | number;
|
||||
}
|
||||
|
||||
export interface IContainsDisplayNameCondition extends IPushRuleCondition<ConditionKind.ContainsDisplayName> {
|
||||
// no additional fields
|
||||
}
|
||||
@@ -105,6 +113,7 @@ export interface ICallStartedPrefixCondition extends IPushRuleCondition<Conditio
|
||||
// IPushRuleCondition<Exclude<string, ConditionKind>> unfortunately does not resolve this at the time of writing.
|
||||
export type PushRuleCondition =
|
||||
| IEventMatchCondition
|
||||
| IEventPropertyIsCondition
|
||||
| IContainsDisplayNameCondition
|
||||
| IRoomMemberCountCondition
|
||||
| ISenderNotificationPermissionCondition
|
||||
@@ -133,6 +142,14 @@ export enum RuleId {
|
||||
IncomingCall = ".m.rule.call",
|
||||
SuppressNotices = ".m.rule.suppress_notices",
|
||||
Tombstone = ".m.rule.tombstone",
|
||||
PollStart = ".m.rule.poll_start",
|
||||
PollStartUnstable = ".org.matrix.msc3930.rule.poll_start",
|
||||
PollEnd = ".m.rule.poll_end",
|
||||
PollEndUnstable = ".org.matrix.msc3930.rule.poll_end",
|
||||
PollStartOneToOne = ".m.rule.poll_start_one_to_one",
|
||||
PollStartOneToOneUnstable = ".org.matrix.msc3930.rule.poll_start_one_to_one",
|
||||
PollEndOneToOne = ".m.rule.poll_end_one_to_one",
|
||||
PollEndOneToOneUnstable = ".org.matrix.msc3930.rule.poll_end_one_to_one",
|
||||
}
|
||||
|
||||
export type PushRuleSet = {
|
||||
|
||||
+49
-20
@@ -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
@@ -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
|
||||
*/
|
||||
|
||||
@@ -70,7 +70,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
this.opts.idBaseUrl = url;
|
||||
}
|
||||
|
||||
public idServerRequest<T extends Record<string, unknown>>(
|
||||
public idServerRequest<T extends {} = Record<string, unknown>>(
|
||||
method: Method,
|
||||
path: string,
|
||||
params: Record<string, string | string[]> | undefined,
|
||||
|
||||
+3
-17
@@ -576,13 +576,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
}
|
||||
|
||||
public get replyEventId(): string | undefined {
|
||||
// We're prefer ev.getContent() over ev.getWireContent() to make sure
|
||||
// we grab the latest edit with potentially new relations. But we also
|
||||
// can't just rely on ev.getContent() by itself because historically we
|
||||
// still show the reply from the original message even though the edit
|
||||
// event does not include the relation reply.
|
||||
const mRelatesTo = this.getContent()["m.relates_to"] || this.getWireContent()["m.relates_to"];
|
||||
return mRelatesTo?.["m.in_reply_to"]?.event_id;
|
||||
return this.getWireContent()["m.relates_to"]?.["m.in_reply_to"]?.event_id;
|
||||
}
|
||||
|
||||
public get relationEventId(): string | undefined {
|
||||
@@ -799,22 +793,14 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* @returns array of recipients
|
||||
*/
|
||||
public getKeyRequestRecipients(userId: string): IKeyRequestRecipient[] {
|
||||
// send the request to all of our own devices, and the
|
||||
// original sending device if it wasn't us.
|
||||
const wireContent = this.getWireContent();
|
||||
// send the request to all of our own devices
|
||||
const recipients = [
|
||||
{
|
||||
userId,
|
||||
deviceId: "*",
|
||||
},
|
||||
];
|
||||
const sender = this.getSender();
|
||||
if (sender !== userId) {
|
||||
recipients.push({
|
||||
userId: sender!,
|
||||
deviceId: wireContent.device_id,
|
||||
});
|
||||
}
|
||||
|
||||
return recipients;
|
||||
}
|
||||
|
||||
|
||||
+7
-2
@@ -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
@@ -25,6 +25,7 @@ import {
|
||||
ICallStartedPrefixCondition,
|
||||
IContainsDisplayNameCondition,
|
||||
IEventMatchCondition,
|
||||
IEventPropertyIsCondition,
|
||||
IPushRule,
|
||||
IPushRules,
|
||||
IRoomMemberCountCondition,
|
||||
@@ -123,6 +124,12 @@ export class PushProcessor {
|
||||
*/
|
||||
public constructor(private readonly client: MatrixClient) {}
|
||||
|
||||
/**
|
||||
* Maps the original key from the push rules to a list of property names
|
||||
* after unescaping.
|
||||
*/
|
||||
private readonly parsedKeys = new Map<string, string[]>();
|
||||
|
||||
/**
|
||||
* Convert a list of actions into a object with the actions as keys and their values
|
||||
* @example
|
||||
@@ -162,7 +169,7 @@ export class PushProcessor {
|
||||
if (!newRules) newRules = {} as IPushRules;
|
||||
if (!newRules.global) newRules.global = {} as PushRuleSet;
|
||||
if (!newRules.global.override) newRules.global.override = [];
|
||||
if (!newRules.global.override) newRules.global.underride = [];
|
||||
if (!newRules.global.underride) newRules.global.underride = [];
|
||||
|
||||
// Merge the client-level defaults with the ones from the server
|
||||
const globalOverrides = newRules.global.override;
|
||||
@@ -202,6 +209,53 @@ export class PushProcessor {
|
||||
return newRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-caches the parsed keys for push rules and cleans out any obsolete cache
|
||||
* entries. Should be called after push rules are updated.
|
||||
* @param newRules - The new push rules.
|
||||
*/
|
||||
public updateCachedPushRuleKeys(newRules: IPushRules): void {
|
||||
// These lines are mostly to make the tests happy. We shouldn't run into these
|
||||
// properties missing in practice.
|
||||
if (!newRules) newRules = {} as IPushRules;
|
||||
if (!newRules.global) newRules.global = {} as PushRuleSet;
|
||||
if (!newRules.global.override) newRules.global.override = [];
|
||||
if (!newRules.global.room) newRules.global.room = [];
|
||||
if (!newRules.global.sender) newRules.global.sender = [];
|
||||
if (!newRules.global.underride) newRules.global.underride = [];
|
||||
|
||||
// Process the 'key' property on event_match conditions pre-cache the
|
||||
// values and clean-out any unused values.
|
||||
const toRemoveKeys = new Set(this.parsedKeys.keys());
|
||||
for (const ruleset of [
|
||||
newRules.global.override,
|
||||
newRules.global.room,
|
||||
newRules.global.sender,
|
||||
newRules.global.underride,
|
||||
]) {
|
||||
for (const rule of ruleset) {
|
||||
if (!rule.conditions) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const condition of rule.conditions) {
|
||||
if (condition.kind !== ConditionKind.EventMatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure we keep this key.
|
||||
toRemoveKeys.delete(condition.key);
|
||||
|
||||
// Pre-process the key.
|
||||
this.parsedKeys.set(condition.key, PushProcessor.partsForDottedKey(condition.key));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Any keys that were previously cached, but are no longer needed should
|
||||
// be removed.
|
||||
toRemoveKeys.forEach((k) => this.parsedKeys.delete(k));
|
||||
}
|
||||
|
||||
private static cachedGlobToRegex: Record<string, RegExp> = {}; // $glob: RegExp
|
||||
|
||||
private matchingRuleFromKindSet(ev: MatrixEvent, kindset: PushRuleSet): IAnnotatedPushRule | null {
|
||||
@@ -284,6 +338,8 @@ export class PushProcessor {
|
||||
switch (cond.kind) {
|
||||
case ConditionKind.EventMatch:
|
||||
return this.eventFulfillsEventMatchCondition(cond, ev);
|
||||
case ConditionKind.EventPropertyIs:
|
||||
return this.eventFulfillsEventPropertyIsCondition(cond, ev);
|
||||
case ConditionKind.ContainsDisplayName:
|
||||
return this.eventFulfillsDisplayNameCondition(cond, ev);
|
||||
case ConditionKind.RoomMemberCount:
|
||||
@@ -382,6 +438,13 @@ export class PushProcessor {
|
||||
return content.body.search(pat) > -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given event matches the push rule condition by fetching
|
||||
* the property from the event and comparing against the condition's glob-based
|
||||
* pattern.
|
||||
* @param cond - The push rule condition to check for a match.
|
||||
* @param ev - The event to check for a match.
|
||||
*/
|
||||
private eventFulfillsEventMatchCondition(cond: IEventMatchCondition, ev: MatrixEvent): boolean {
|
||||
if (!cond.key) {
|
||||
return false;
|
||||
@@ -392,6 +455,9 @@ export class PushProcessor {
|
||||
return false;
|
||||
}
|
||||
|
||||
// XXX This does not match in a case-insensitive manner.
|
||||
//
|
||||
// See https://spec.matrix.org/v1.5/client-server-api/#conditions-1
|
||||
if (cond.value) {
|
||||
return cond.value === val;
|
||||
}
|
||||
@@ -408,6 +474,20 @@ export class PushProcessor {
|
||||
return !!val.match(regex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given event matches the push rule condition by fetching
|
||||
* the property from the event and comparing exactly against the condition's
|
||||
* value.
|
||||
* @param cond - The push rule condition to check for a match.
|
||||
* @param ev - The event to check for a match.
|
||||
*/
|
||||
private eventFulfillsEventPropertyIsCondition(cond: IEventPropertyIsCondition, ev: MatrixEvent): boolean {
|
||||
if (!cond.key || cond.value === undefined) {
|
||||
return false;
|
||||
}
|
||||
return cond.value === this.valueForDottedKey(cond.key, ev);
|
||||
}
|
||||
|
||||
private eventFulfillsCallStartedCondition(
|
||||
_cond: ICallStartedCondition | ICallStartedPrefixCondition,
|
||||
ev: MatrixEvent,
|
||||
@@ -433,28 +513,105 @@ export class PushProcessor {
|
||||
return PushProcessor.cachedGlobToRegex[glob];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the key into the separate fields to search by splitting on
|
||||
* unescaped ".", and then removing any escape characters.
|
||||
*
|
||||
* @param str - The key of the push rule condition: a dotted field.
|
||||
* @returns The unescaped parts to fetch.
|
||||
* @internal
|
||||
*/
|
||||
public static partsForDottedKey(str: string): string[] {
|
||||
const result = [];
|
||||
|
||||
// The current field and whether the previous character was the escape
|
||||
// character (a backslash).
|
||||
let part = "";
|
||||
let escaped = false;
|
||||
|
||||
// Iterate over each character, and decide whether to append to the current
|
||||
// part (following the escape rules) or to start a new part (based on the
|
||||
// field separator).
|
||||
for (const c of str) {
|
||||
// If the previous character was the escape character (a backslash)
|
||||
// then decide what to append to the current part.
|
||||
if (escaped) {
|
||||
if (c === "\\" || c === ".") {
|
||||
// An escaped backslash or dot just gets added.
|
||||
part += c;
|
||||
} else {
|
||||
// A character that shouldn't be escaped gets the backslash prepended.
|
||||
part += "\\" + c;
|
||||
}
|
||||
// This always resets being escaped.
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == ".") {
|
||||
// The field separator creates a new part.
|
||||
result.push(part);
|
||||
part = "";
|
||||
} else if (c == "\\") {
|
||||
// A backslash adds no characters, but starts an escape sequence.
|
||||
escaped = true;
|
||||
} else {
|
||||
// Otherwise, just add the current character.
|
||||
part += c;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the final part is included. If there's an open escape sequence
|
||||
// it should be included.
|
||||
if (escaped) {
|
||||
part += "\\";
|
||||
}
|
||||
result.push(part);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a dotted field and event, fetch the value at that position, if one
|
||||
* exists.
|
||||
*
|
||||
* @param key - The key of the push rule condition: a dotted field to fetch.
|
||||
* @param ev - The matrix event to fetch the field from.
|
||||
* @returns The value at the dotted path given by key.
|
||||
*/
|
||||
private valueForDottedKey(key: string, ev: MatrixEvent): any {
|
||||
const parts = key.split(".");
|
||||
// The key should already have been parsed via updateCachedPushRuleKeys,
|
||||
// but if it hasn't (maybe via an old consumer of the SDK which hasn't
|
||||
// been updated?) then lazily calculate it here.
|
||||
let parts = this.parsedKeys.get(key);
|
||||
if (parts === undefined) {
|
||||
parts = PushProcessor.partsForDottedKey(key);
|
||||
this.parsedKeys.set(key, parts);
|
||||
}
|
||||
let val: any;
|
||||
|
||||
// special-case the first component to deal with encrypted messages
|
||||
const firstPart = parts[0];
|
||||
let currentIndex = 0;
|
||||
if (firstPart === "content") {
|
||||
val = ev.getContent();
|
||||
parts.shift();
|
||||
++currentIndex;
|
||||
} else if (firstPart === "type") {
|
||||
val = ev.getType();
|
||||
parts.shift();
|
||||
++currentIndex;
|
||||
} else {
|
||||
// use the raw event for any other fields
|
||||
val = ev.event;
|
||||
}
|
||||
|
||||
while (parts.length > 0) {
|
||||
const thisPart = parts.shift()!;
|
||||
if (isNullOrUndefined(val[thisPart])) {
|
||||
return null;
|
||||
for (; currentIndex < parts.length; ++currentIndex) {
|
||||
// The previous iteration resulted in null or undefined, bail (and
|
||||
// avoid the type error of attempting to retrieve a property).
|
||||
if (isNullOrUndefined(val)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const thisPart = parts[currentIndex];
|
||||
val = val[thisPart];
|
||||
}
|
||||
return val;
|
||||
@@ -507,6 +664,18 @@ export class PushProcessor {
|
||||
* @returns The push rule, or null if no such rule was found
|
||||
*/
|
||||
public getPushRuleById(ruleId: string): IPushRule | null {
|
||||
const result = this.getPushRuleAndKindById(ruleId);
|
||||
return result?.rule ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get one of the users push rules by its ID
|
||||
*
|
||||
* @param ruleId - The ID of the rule to search for
|
||||
* @returns rule The push rule, or null if no such rule was found
|
||||
* @returns kind - The PushRuleKind of the rule to search for
|
||||
*/
|
||||
public getPushRuleAndKindById(ruleId: string): { rule: IPushRule; kind: PushRuleKind } | null {
|
||||
for (const scope of ["global"] as const) {
|
||||
if (this.client.pushRules?.[scope] === undefined) continue;
|
||||
|
||||
@@ -514,7 +683,7 @@ export class PushProcessor {
|
||||
if (this.client.pushRules[scope][kind] === undefined) continue;
|
||||
|
||||
for (const rule of this.client.pushRules[scope][kind]!) {
|
||||
if (rule.rule_id === ruleId) return rule;
|
||||
if (rule.rule_id === ruleId) return { rule, kind };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-23
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -25,20 +25,20 @@ import {
|
||||
RendezvousTransport,
|
||||
RendezvousFailureReason,
|
||||
} from "..";
|
||||
import { encodeBase64, decodeBase64 } from "../../crypto/olmlib";
|
||||
import { encodeUnpaddedBase64, decodeBase64 } from "../../crypto/olmlib";
|
||||
import { crypto, subtleCrypto, TextEncoder } from "../../crypto/crypto";
|
||||
import { generateDecimalSas } from "../../crypto/verification/SASDecimal";
|
||||
import { UnstableValue } from "../../NamespacedValue";
|
||||
|
||||
const ECDH_V1 = new UnstableValue(
|
||||
"m.rendezvous.v1.curve25519-aes-sha256",
|
||||
"org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256",
|
||||
const ECDH_V2 = new UnstableValue(
|
||||
"m.rendezvous.v2.curve25519-aes-sha256",
|
||||
"org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256",
|
||||
);
|
||||
|
||||
export interface ECDHv1RendezvousCode extends RendezvousCode {
|
||||
export interface ECDHv2RendezvousCode extends RendezvousCode {
|
||||
rendezvous: {
|
||||
transport: RendezvousTransportDetails;
|
||||
algorithm: typeof ECDH_V1.name | typeof ECDH_V1.altName;
|
||||
algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName;
|
||||
key: string;
|
||||
};
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export interface ECDHv1RendezvousCode extends RendezvousCode {
|
||||
export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload;
|
||||
|
||||
export interface PlainTextPayload {
|
||||
algorithm: typeof ECDH_V1.name | typeof ECDH_V1.altName;
|
||||
algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ async function importKey(key: Uint8Array): Promise<CryptoKey> {
|
||||
* X25519/ECDH key agreement based secure rendezvous channel.
|
||||
* Note that this is UNSTABLE and may have breaking changes without notice.
|
||||
*/
|
||||
export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
|
||||
export class MSC3903ECDHv2RendezvousChannel<T> implements RendezvousChannel<T> {
|
||||
private olmSAS?: SAS;
|
||||
private ourPublicKey: Uint8Array;
|
||||
private aesKey?: CryptoKey;
|
||||
@@ -85,17 +85,17 @@ export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
|
||||
this.ourPublicKey = decodeBase64(this.olmSAS.get_pubkey());
|
||||
}
|
||||
|
||||
public async generateCode(intent: RendezvousIntent): Promise<ECDHv1RendezvousCode> {
|
||||
public async generateCode(intent: RendezvousIntent): Promise<ECDHv2RendezvousCode> {
|
||||
if (this.transport.ready) {
|
||||
throw new Error("Code already generated");
|
||||
}
|
||||
|
||||
await this.transport.send({ algorithm: ECDH_V1.name });
|
||||
await this.transport.send({ algorithm: ECDH_V2.name });
|
||||
|
||||
const rendezvous: ECDHv1RendezvousCode = {
|
||||
const rendezvous: ECDHv2RendezvousCode = {
|
||||
rendezvous: {
|
||||
algorithm: ECDH_V1.name,
|
||||
key: encodeBase64(this.ourPublicKey),
|
||||
algorithm: ECDH_V2.name,
|
||||
key: encodeUnpaddedBase64(this.ourPublicKey),
|
||||
transport: await this.transport.details(),
|
||||
},
|
||||
intent,
|
||||
@@ -123,7 +123,7 @@ export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
|
||||
}
|
||||
const res = rawRes as Partial<PlainTextPayload>;
|
||||
const { key, algorithm } = res;
|
||||
if (!algorithm || !ECDH_V1.matches(algorithm) || !key) {
|
||||
if (!algorithm || !ECDH_V2.matches(algorithm) || !key) {
|
||||
throw new RendezvousError(
|
||||
"Unsupported algorithm: " + algorithm,
|
||||
RendezvousFailureReason.UnsupportedAlgorithm,
|
||||
@@ -134,20 +134,20 @@ export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
|
||||
} else {
|
||||
// send our public key unencrypted
|
||||
await this.transport.send({
|
||||
algorithm: ECDH_V1.name,
|
||||
key: encodeBase64(this.ourPublicKey),
|
||||
algorithm: ECDH_V2.name,
|
||||
key: encodeUnpaddedBase64(this.ourPublicKey),
|
||||
});
|
||||
}
|
||||
|
||||
this.connected = true;
|
||||
|
||||
this.olmSAS.set_their_key(encodeBase64(this.theirPublicKey!));
|
||||
this.olmSAS.set_their_key(encodeUnpaddedBase64(this.theirPublicKey!));
|
||||
|
||||
const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!;
|
||||
const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey;
|
||||
let aesInfo = ECDH_V1.name;
|
||||
aesInfo += `|${encodeBase64(initiatorKey)}`;
|
||||
aesInfo += `|${encodeBase64(recipientKey)}`;
|
||||
let aesInfo = ECDH_V2.name;
|
||||
aesInfo += `|${encodeUnpaddedBase64(initiatorKey)}`;
|
||||
aesInfo += `|${encodeUnpaddedBase64(recipientKey)}`;
|
||||
|
||||
const aesKeyBytes = this.olmSAS.generate_bytes(aesInfo, 32);
|
||||
|
||||
@@ -181,8 +181,8 @@ export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
|
||||
);
|
||||
|
||||
return {
|
||||
iv: encodeBase64(iv),
|
||||
ciphertext: encodeBase64(ciphertext),
|
||||
iv: encodeUnpaddedBase64(iv),
|
||||
ciphertext: encodeUnpaddedBase64(ciphertext),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,4 +14,4 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export * from "./MSC3903ECDHv1RendezvousChannel";
|
||||
export * from "./MSC3903ECDHv2RendezvousChannel";
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* This file replaces rust-crypto/index.ts when the js-sdk is being built for browserify.
|
||||
*
|
||||
* It is a stub, so that we do not import the whole of the base64'ed wasm artifact into the browserify bundle.
|
||||
* It deliberately does nothing except raise an exception.
|
||||
*/
|
||||
|
||||
import { IHttpOpts, MatrixHttpApi } from "../http-api";
|
||||
|
||||
export async function initRustCrypto(
|
||||
_http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
_userId: string,
|
||||
_deviceId: string,
|
||||
): Promise<Crypto> {
|
||||
throw new Error("Rust crypto is not supported under browserify.");
|
||||
}
|
||||
@@ -43,7 +43,6 @@ import {
|
||||
} from "./sliding-sync";
|
||||
import { EventType } from "./@types/event";
|
||||
import { IPushRules } from "./@types/PushRules";
|
||||
import { PushProcessor } from "./pushprocessor";
|
||||
import { RoomStateEvent } from "./models/room-state";
|
||||
import { RoomMemberEvent } from "./models/room-member";
|
||||
|
||||
@@ -262,7 +261,7 @@ class ExtensionAccountData implements Extension<ExtensionAccountDataRequest, Ext
|
||||
// (see sync) before syncing over the network.
|
||||
if (accountDataEvent.getType() === EventType.PushRules) {
|
||||
const rules = accountDataEvent.getContent<IPushRules>();
|
||||
this.client.pushRules = PushProcessor.rewriteDefaultRules(rules);
|
||||
this.client.setPushRules(rules);
|
||||
}
|
||||
const prevEvent = prevEventsMap[accountDataEvent.getType()];
|
||||
this.client.emit(ClientEvent.AccountData, accountDataEvent, prevEvent);
|
||||
|
||||
+1
-2
@@ -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
@@ -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
@@ -196,6 +196,7 @@ export class GroupCall extends TypedEventEmitter<
|
||||
public readonly userMediaFeeds: CallFeed[] = [];
|
||||
public readonly screenshareFeeds: CallFeed[] = [];
|
||||
public groupCallId: string;
|
||||
public readonly allowCallWithoutVideoAndAudio: boolean;
|
||||
|
||||
private readonly calls = new Map<string, Map<string, MatrixCall>>(); // user_id -> device_id -> MatrixCall
|
||||
private callHandlers = new Map<string, Map<string, ICallHandlers>>(); // user_id -> device_id -> ICallHandlers
|
||||
@@ -219,6 +220,7 @@ export class GroupCall extends TypedEventEmitter<
|
||||
groupCallId?: string,
|
||||
private dataChannelsEnabled?: boolean,
|
||||
private dataChannelOptions?: IGroupCallDataChannelOptions,
|
||||
isCallWithoutVideoAndAudio?: boolean,
|
||||
) {
|
||||
super();
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
@@ -231,6 +233,7 @@ export class GroupCall extends TypedEventEmitter<
|
||||
this.on(GroupCallEvent.ParticipantsChanged, this.onParticipantsChanged);
|
||||
this.on(GroupCallEvent.GroupCallStateChanged, this.onStateChanged);
|
||||
this.on(GroupCallEvent.LocalScreenshareStateChanged, this.onLocalFeedsChanged);
|
||||
this.allowCallWithoutVideoAndAudio = !!isCallWithoutVideoAndAudio;
|
||||
}
|
||||
|
||||
public async create(): Promise<GroupCall> {
|
||||
@@ -374,8 +377,15 @@ export class GroupCall extends TypedEventEmitter<
|
||||
try {
|
||||
stream = await this.client.getMediaHandler().getUserMediaStream(true, this.type === GroupCallType.Video);
|
||||
} catch (error) {
|
||||
this.state = GroupCallState.LocalCallFeedUninitialized;
|
||||
throw error;
|
||||
// If is allowed to join a call without a media stream, then we
|
||||
// don't throw an error here. But we need an empty Local Feed to establish
|
||||
// a connection later.
|
||||
if (this.allowCallWithoutVideoAndAudio) {
|
||||
stream = new MediaStream();
|
||||
} else {
|
||||
this.state = GroupCallState.LocalCallFeedUninitialized;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// The call could've been disposed while we were waiting, and could
|
||||
@@ -584,6 +594,31 @@ export class GroupCall extends TypedEventEmitter<
|
||||
logger.log(
|
||||
`GroupCall ${this.groupCallId} setMicrophoneMuted() (streamId=${this.localCallFeed.stream.id}, muted=${muted})`,
|
||||
);
|
||||
|
||||
// We needed this here to avoid an error in case user join a call without a device.
|
||||
// I can not use .then .catch functions because linter :-(
|
||||
try {
|
||||
if (!muted) {
|
||||
const stream = await this.client
|
||||
.getMediaHandler()
|
||||
.getUserMediaStream(true, !this.localCallFeed.isVideoMuted());
|
||||
if (stream === null) {
|
||||
// if case permission denied to get a stream stop this here
|
||||
/* istanbul ignore next */
|
||||
logger.log(
|
||||
`GroupCall ${this.groupCallId} setMicrophoneMuted() no device to receive local stream, muted=${muted}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
/* istanbul ignore next */
|
||||
logger.log(
|
||||
`GroupCall ${this.groupCallId} setMicrophoneMuted() no device or permission to receive local stream, muted=${muted}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.localCallFeed.setAudioVideoMuted(muted, null);
|
||||
// I don't believe its actually necessary to enable these tracks: they
|
||||
// are the one on the GroupCall's own CallFeed and are cloned before being
|
||||
@@ -617,14 +652,24 @@ export class GroupCall extends TypedEventEmitter<
|
||||
}
|
||||
|
||||
if (this.localCallFeed) {
|
||||
/* istanbul ignore next */
|
||||
logger.log(
|
||||
`GroupCall ${this.groupCallId} setLocalVideoMuted() (stream=${this.localCallFeed.stream.id}, muted=${muted})`,
|
||||
);
|
||||
|
||||
const stream = await this.client.getMediaHandler().getUserMediaStream(true, !muted);
|
||||
await this.updateLocalUsermediaStream(stream);
|
||||
this.localCallFeed.setAudioVideoMuted(null, muted);
|
||||
setTracksEnabled(this.localCallFeed.stream.getVideoTracks(), !muted);
|
||||
try {
|
||||
const stream = await this.client.getMediaHandler().getUserMediaStream(true, !muted);
|
||||
await this.updateLocalUsermediaStream(stream);
|
||||
this.localCallFeed.setAudioVideoMuted(null, muted);
|
||||
setTracksEnabled(this.localCallFeed.stream.getVideoTracks(), !muted);
|
||||
} catch (_) {
|
||||
// No permission to video device
|
||||
/* istanbul ignore next */
|
||||
logger.log(
|
||||
`GroupCall ${this.groupCallId} setLocalVideoMuted() no device or permission to receive local stream, muted=${muted}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
logger.log(`GroupCall ${this.groupCallId} setLocalVideoMuted() no stream muted (muted=${muted})`);
|
||||
this.initWithVideoMuted = muted;
|
||||
|
||||
@@ -184,8 +184,11 @@ export class GroupCallEventHandler {
|
||||
isPtt,
|
||||
callIntent,
|
||||
groupCallId,
|
||||
content?.dataChannelsEnabled,
|
||||
// Because without Media section a WebRTC connection is not possible, so need a RTCDataChannel to set up a
|
||||
// no media WebRTC connection anyway.
|
||||
content?.dataChannelsEnabled || this.client.isVoipWithNoMediaAllowed,
|
||||
dataChannelOptions,
|
||||
this.client.isVoipWithNoMediaAllowed,
|
||||
);
|
||||
|
||||
this.groupCalls.set(room.roomId, groupCall);
|
||||
|
||||
@@ -185,13 +185,23 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
}
|
||||
|
||||
public async hasAudioDevice(): Promise<boolean> {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.filter((device) => device.kind === "audioinput").length > 0;
|
||||
try {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.filter((device) => device.kind === "audioinput").length > 0;
|
||||
} catch (err) {
|
||||
logger.log(`MediaHandler hasAudioDevice() calling navigator.mediaDevices.enumerateDevices with error`, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async hasVideoDevice(): Promise<boolean> {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.filter((device) => device.kind === "videoinput").length > 0;
|
||||
try {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.filter((device) => device.kind === "videoinput").length > 0;
|
||||
} catch (err) {
|
||||
logger.log(`MediaHandler hasVideoDevice() calling navigator.mediaDevices.enumerateDevices with error`, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user