Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ec1fa6605 | |||
| f15d682938 | |||
| 21a10a2d14 | |||
| fc02e550bd | |||
| 78637a0689 | |||
| 4ca882fcd4 | |||
| 13ee0eb7f5 | |||
| cb018dfc80 | |||
| 7574dacdb3 | |||
| 0c417b7c32 | |||
| daf845d7bd | |||
| 52792ec89b | |||
| 6dc4a62e8c | |||
| 1cd8ea61ea | |||
| 0c5eb277e4 | |||
| d459a91af3 | |||
| 18722d0031 | |||
| 5119934268 | |||
| 4316009401 | |||
| 72f3c360b6 | |||
| fcbc195fbe | |||
| af38021d28 | |||
| 5e8cb9fa18 | |||
| 6ef9f6c55e | |||
| e6a3b0ebc0 | |||
| aaae55736f | |||
| 9e586ab634 | |||
| 7ff44d4a50 | |||
| 63abd00ca7 | |||
| 40f2579158 | |||
| ceb2a57feb | |||
| 90e8336797 | |||
| 73ca9c9ed2 | |||
| cc065c2772 | |||
| 028be0fee2 | |||
| 49696cecbd | |||
| 83cb52c89c | |||
| e82bae2c4d | |||
| ee2b0204aa | |||
| 1ec7670f6a | |||
| 8ab2e10471 | |||
| 8ff8685ae5 | |||
| f3772cdf82 | |||
| ee2f1cdfd4 | |||
| 0de73a0b3e | |||
| be742e811c | |||
| ca9263fa64 | |||
| 9f619be08d |
@@ -25,15 +25,25 @@ jobs:
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: 📖 Generate JSDoc
|
||||
run: "yarn gendoc"
|
||||
- name: 🔨 Install symlinks
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y symlinks
|
||||
|
||||
- name: 📖 Generate docs
|
||||
run: |
|
||||
yarn tpv purge --yes --out _docs --stale --major 10
|
||||
yarn gendoc
|
||||
symlinks -rc _docs
|
||||
|
||||
- name: 🚀 Deploy
|
||||
uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 # v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
keep_files: true
|
||||
publish_dir: _docs
|
||||
run: |
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
git add . --all
|
||||
git commit -m "Update docs"
|
||||
git push
|
||||
working-directory: _docs
|
||||
|
||||
npm:
|
||||
name: Publish
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5 # v5
|
||||
uses: peter-evans/create-pull-request@284f54f989303d2699d373481a0cfa13ad5a6666 # v5
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/upgrade-deps
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
Changes in [25.2.0-rc.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.2.0-rc.4) (2023-05-16)
|
||||
============================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Deprecate device methods in MatrixClient ([\#3357](https://github.com/matrix-org/matrix-js-sdk/pull/3357)).
|
||||
|
||||
## ✨ Features
|
||||
* Total summary count ([\#3351](https://github.com/matrix-org/matrix-js-sdk/pull/3351)). Contributed by @toger5.
|
||||
* Audio concealment ([\#3349](https://github.com/matrix-org/matrix-js-sdk/pull/3349)). Contributed by @toger5.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Correctly accumulate sync summaries. ([\#3366](https://github.com/matrix-org/matrix-js-sdk/pull/3366)). Fixes vector-im/element-web#23345.
|
||||
* Keep measuring a call feed's volume after a stream replacement ([\#3361](https://github.com/matrix-org/matrix-js-sdk/pull/3361)). Fixes vector-im/element-call#1051.
|
||||
* Element-R: Avoid uploading a new fallback key at every `/sync` ([\#3338](https://github.com/matrix-org/matrix-js-sdk/pull/3338)). Fixes vector-im/element-web#25215.
|
||||
* Accumulate receipts for the main thread and unthreaded separately ([\#3339](https://github.com/matrix-org/matrix-js-sdk/pull/3339)). Fixes vector-im/element-web#24629.
|
||||
|
||||
Changes in [25.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.1.1) (2023-05-16)
|
||||
==================================================================================================
|
||||
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "25.1.1",
|
||||
"version": "25.2.0-rc.4",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
@@ -55,7 +55,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.7",
|
||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.9",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
@@ -120,7 +120,7 @@
|
||||
"jest-mock": "^29.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"prettier": "2.8.8",
|
||||
"rimraf": "^4.0.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"terser": "^5.5.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsify": "^5.0.2",
|
||||
@@ -128,6 +128,7 @@
|
||||
"typedoc-plugin-mdn-links": "^3.0.3",
|
||||
"typedoc-plugin-missing-exports": "^2.0.0",
|
||||
"typedoc-plugin-versions": "^0.2.3",
|
||||
"typedoc-plugin-versions-cli": "^0.1.12",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
|
||||
@@ -15,4 +15,4 @@ for line in sys.stdin:
|
||||
break
|
||||
found_first_header = True
|
||||
elif not re.match(r"^=+$", line) and len(line) > 0:
|
||||
print line
|
||||
print(line)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
|
||||
import { CRYPTO_BACKENDS, InitCrypto } from "../test-utils/test-utils";
|
||||
import { createClient, MatrixClient, UIAuthCallback } from "../../src";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
const TEST_USER_ID = "@alice:localhost";
|
||||
const TEST_DEVICE_ID = "xzcvb";
|
||||
|
||||
/**
|
||||
* Integration tests for cross-signing functionality.
|
||||
*
|
||||
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
|
||||
* to provide the most effective integration tests possible.
|
||||
*/
|
||||
describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: string, initCrypto: InitCrypto) => {
|
||||
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
|
||||
// Rust backend. Once we have full support in the rust sdk, it will go away.
|
||||
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
|
||||
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: TEST_USER_ID,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: TEST_DEVICE_ID,
|
||||
});
|
||||
|
||||
await initCrypto(aliceClient);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await aliceClient.stopClient();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
describe("bootstrapCrossSigning (before initialsync completes)", () => {
|
||||
oldBackendOnly("publishes keys if none were yet published", async () => {
|
||||
// have account_data requests return an empty object
|
||||
fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {});
|
||||
|
||||
// we expect a request to upload signatures for our device ...
|
||||
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
|
||||
|
||||
// ... and one to upload the cross-signing keys (with UIA)
|
||||
fetchMock.post(
|
||||
{ url: "path:/_matrix/client/unstable/keys/device_signing/upload", name: "upload-keys" },
|
||||
{},
|
||||
);
|
||||
|
||||
// provide a UIA callback, so that the cross-signing keys are uploaded
|
||||
const authDict = { type: "test" };
|
||||
const uiaCallback: UIAuthCallback<void> = async (makeRequest) => {
|
||||
await makeRequest(authDict);
|
||||
};
|
||||
|
||||
// now bootstrap cross signing, and check it resolves successfully
|
||||
await aliceClient.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: uiaCallback,
|
||||
});
|
||||
|
||||
// check the cross-signing keys upload
|
||||
expect(fetchMock.called("upload-keys")).toBeTruthy();
|
||||
const [, keysOpts] = fetchMock.lastCall("upload-keys")!;
|
||||
const keysBody = JSON.parse(keysOpts!.body as string);
|
||||
expect(keysBody.auth).toEqual(authDict); // check uia dict was passed
|
||||
// there should be a key of each type
|
||||
// master key is signed by the device
|
||||
expect(keysBody).toHaveProperty(`master_key.signatures.[${TEST_USER_ID}].[ed25519:${TEST_DEVICE_ID}]`);
|
||||
const masterKeyId = Object.keys(keysBody.master_key.keys)[0];
|
||||
// ssk and usk are signed by the master key
|
||||
expect(keysBody).toHaveProperty(`self_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
|
||||
expect(keysBody).toHaveProperty(`user_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
|
||||
const sskId = Object.keys(keysBody.self_signing_key.keys)[0];
|
||||
|
||||
// check the publish call
|
||||
expect(fetchMock.called("upload-sigs")).toBeTruthy();
|
||||
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
|
||||
const body = JSON.parse(sigsOpts!.body as string);
|
||||
// there should be a signature for our device, by our self-signing key.
|
||||
expect(body).toHaveProperty(
|
||||
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[${sskId}]`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1698,7 +1698,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
function timeout(delayMs: number, reason: string): { promise: Promise<never>; cancel: () => void } {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
return {
|
||||
promise: new Promise((resolve, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
|
||||
@@ -239,6 +239,8 @@ export class MockRTCPeerConnection {
|
||||
public triggerIncomingDataChannel(): void {
|
||||
this.onDataChannelListener?.({ channel: {} } as RTCDataChannelEvent);
|
||||
}
|
||||
|
||||
public restartIce(): void {}
|
||||
}
|
||||
|
||||
export class MockRTCRtpSender {
|
||||
|
||||
@@ -24,10 +24,11 @@ import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import { MatrixError } from "../../../src/http-api";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { ICrossSigningKey, ICreateClientOpts, ISignedKey, MatrixClient } from "../../../src/client";
|
||||
import { CryptoEvent, IBootstrapCrossSigningOpts } from "../../../src/crypto";
|
||||
import { CryptoEvent } from "../../../src/crypto";
|
||||
import { IDevice } from "../../../src/crypto/deviceinfo";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { BootstrapCrossSigningOpts } from "../../../src/crypto-api";
|
||||
|
||||
const PUSH_RULES_RESPONSE: Response = {
|
||||
method: "GET",
|
||||
@@ -146,7 +147,7 @@ describe("Cross Signing", function () {
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
alice.setAccountData = async () => ({});
|
||||
alice.getAccountDataFromServer = async <T extends { [k: string]: any }>(): Promise<T | null> => ({} as T);
|
||||
const authUploadDeviceSigningKeys: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"] = async (func) => {
|
||||
const authUploadDeviceSigningKeys: BootstrapCrossSigningOpts["authUploadDeviceSigningKeys"] = async (func) => {
|
||||
await func({});
|
||||
};
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ import { SyncState } from "../../src/sync";
|
||||
import * as featureUtils from "../../src/feature";
|
||||
import { StubStore } from "../../src/store/stub";
|
||||
import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorageImpl } from "../../src/secret-storage";
|
||||
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -2750,6 +2751,60 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
});
|
||||
|
||||
// these wrappers are deprecated, but we need coverage of them to pass the quality gate
|
||||
describe("Crypto wrappers", () => {
|
||||
describe("exception if no crypto", () => {
|
||||
it("isCrossSigningReady", () => {
|
||||
expect(() => client.isCrossSigningReady()).toThrow("End-to-end encryption disabled");
|
||||
});
|
||||
|
||||
it("bootstrapCrossSigning", () => {
|
||||
expect(() => client.bootstrapCrossSigning({})).toThrow("End-to-end encryption disabled");
|
||||
});
|
||||
|
||||
it("isSecretStorageReady", () => {
|
||||
expect(() => client.isSecretStorageReady()).toThrow("End-to-end encryption disabled");
|
||||
});
|
||||
});
|
||||
|
||||
describe("defer to crypto backend", () => {
|
||||
let mockCryptoBackend: Mocked<CryptoBackend>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCryptoBackend = {
|
||||
isCrossSigningReady: jest.fn(),
|
||||
bootstrapCrossSigning: jest.fn(),
|
||||
isSecretStorageReady: jest.fn(),
|
||||
stop: jest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as Mocked<CryptoBackend>;
|
||||
client["cryptoBackend"] = mockCryptoBackend;
|
||||
});
|
||||
|
||||
it("isCrossSigningReady", async () => {
|
||||
const testResult = "test";
|
||||
mockCryptoBackend.isCrossSigningReady.mockResolvedValue(testResult as unknown as boolean);
|
||||
expect(await client.isCrossSigningReady()).toBe(testResult);
|
||||
expect(mockCryptoBackend.isCrossSigningReady).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("bootstrapCrossSigning", async () => {
|
||||
const testOpts = {};
|
||||
mockCryptoBackend.bootstrapCrossSigning.mockResolvedValue(undefined);
|
||||
await client.bootstrapCrossSigning(testOpts);
|
||||
expect(mockCryptoBackend.bootstrapCrossSigning).toHaveBeenCalledTimes(1);
|
||||
expect(mockCryptoBackend.bootstrapCrossSigning).toHaveBeenCalledWith(testOpts);
|
||||
});
|
||||
|
||||
it("isSecretStorageReady", async () => {
|
||||
client["cryptoBackend"] = mockCryptoBackend;
|
||||
const testResult = "test";
|
||||
mockCryptoBackend.isSecretStorageReady.mockResolvedValue(testResult as unknown as boolean);
|
||||
expect(await client.isSecretStorageReady()).toBe(testResult);
|
||||
expect(mockCryptoBackend.isSecretStorageReady).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("paginateEventTimeline()", () => {
|
||||
describe("notifications timeline", () => {
|
||||
const unsafeNotification = {
|
||||
|
||||
@@ -129,6 +129,50 @@ describe("ReceiptAccumulator", function () {
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("Keeps main thread receipts even when an unthreaded receipt came later", () => {
|
||||
const acc = new ReceiptAccumulator();
|
||||
|
||||
// Given receipts for the special thread "main" and also unthreaded
|
||||
// receipts (which have no thread id).
|
||||
const receipt1 = newReceipt("$event1", ReceiptType.Read, "@alice:localhost", 1, "main");
|
||||
const receipt2 = newReceipt("$event2", ReceiptType.Read, "@alice:localhost", 2);
|
||||
|
||||
// When we collect them
|
||||
acc.consumeEphemeralEvents([receipt1, receipt2]);
|
||||
const newEvent = acc.buildAccumulatedReceiptEvent(roomId);
|
||||
|
||||
// We preserve both: thread:main and unthreaded receipts are different
|
||||
// things, with different meanings.
|
||||
expect(newEvent).toEqual(
|
||||
newMultiReceipt([
|
||||
["$event1", ReceiptType.Read, "@alice:localhost", 1, "main"],
|
||||
["$event2", ReceiptType.Read, "@alice:localhost", 2, undefined],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("Keeps unthreaded receipts even when a main thread receipt came later", () => {
|
||||
const acc = new ReceiptAccumulator();
|
||||
|
||||
// Given receipts for the special thread "main" and also unthreaded
|
||||
// receipts (which have no thread id).
|
||||
const receipt1 = newReceipt("$event1", ReceiptType.Read, "@alice:localhost", 1);
|
||||
const receipt2 = newReceipt("$event2", ReceiptType.Read, "@alice:localhost", 2, "main");
|
||||
|
||||
// When we collect them
|
||||
acc.consumeEphemeralEvents([receipt1, receipt2]);
|
||||
const newEvent = acc.buildAccumulatedReceiptEvent(roomId);
|
||||
|
||||
// We preserve both: thread:main and unthreaded receipts are different
|
||||
// things, with different meanings.
|
||||
expect(newEvent).toEqual(
|
||||
newMultiReceipt([
|
||||
["$event1", ReceiptType.Read, "@alice:localhost", 1, undefined],
|
||||
["$event2", ReceiptType.Read, "@alice:localhost", 2, "main"],
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const newReceipt = (
|
||||
|
||||
@@ -24,11 +24,12 @@ import {
|
||||
KeysUploadRequest,
|
||||
RoomMessageRequest,
|
||||
SignatureUploadRequest,
|
||||
SigningKeysUploadRequest,
|
||||
ToDeviceRequest,
|
||||
} from "@matrix-org/matrix-sdk-crypto-js";
|
||||
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi } from "../../../src";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi, UIAuthCallback } from "../../../src";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
|
||||
describe("OutgoingRequestProcessor", () => {
|
||||
@@ -80,6 +81,12 @@ describe("OutgoingRequestProcessor", () => {
|
||||
"https://example.com/_matrix/client/v3/keys/signatures/upload",
|
||||
],
|
||||
["KeysBackupRequest", KeysBackupRequest, "PUT", "https://example.com/_matrix/client/v3/room_keys/keys"],
|
||||
[
|
||||
"SigningKeysUploadRequest",
|
||||
SigningKeysUploadRequest,
|
||||
"POST",
|
||||
"https://example.com/_matrix/client/v3/keys/device_signing/upload",
|
||||
],
|
||||
];
|
||||
|
||||
test.each(tests)(`should handle %ss`, async (_, RequestClass, expectedMethod, expectedPath) => {
|
||||
@@ -171,6 +178,40 @@ describe("OutgoingRequestProcessor", () => {
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
|
||||
it("should handle SigningKeysUploadRequests with UIA", async () => {
|
||||
// first, mock up a request as we might expect to receive it from the Rust layer ...
|
||||
const testReq = { foo: "bar" };
|
||||
const outgoingRequest = new SigningKeysUploadRequest("1234", JSON.stringify(testReq));
|
||||
|
||||
// also create a UIA callback
|
||||
const authCallback: UIAuthCallback<Object> = async (makeRequest) => {
|
||||
return await makeRequest({ type: "test" });
|
||||
};
|
||||
|
||||
// ... then poke the request into the OutgoingRequestProcessor under test
|
||||
const reqProm = processor.makeOutgoingRequest(outgoingRequest, authCallback);
|
||||
|
||||
// Now: check that it makes a matching HTTP request ...
|
||||
const testResponse = '{"result":1}';
|
||||
httpBackend
|
||||
.when("POST", "/_matrix")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://example.com/_matrix/client/v3/keys/device_signing/upload");
|
||||
expect(JSON.parse(req.rawData)).toEqual({ foo: "bar", auth: { type: "test" } });
|
||||
expect(req.headers["Accept"]).toEqual("application/json");
|
||||
expect(req.headers["Content-Type"]).toEqual("application/json");
|
||||
})
|
||||
.respond(200, testResponse, true);
|
||||
|
||||
// ... and that it calls OlmMachine.markAsSent.
|
||||
const markSentCallPromise = awaitCallToMarkAsSent();
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
await Promise.all([reqProm, markSentCallPromise]);
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
|
||||
it("does not explode with unknown requests", async () => {
|
||||
const outgoingRequest = { id: "5678", type: 987 };
|
||||
const markSentCallPromise = awaitCallToMarkAsSent();
|
||||
|
||||
@@ -22,11 +22,12 @@ import { Mocked } from "jest-mock";
|
||||
|
||||
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
|
||||
import { initRustCrypto } from "../../../src/rust-crypto";
|
||||
import { IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../../src";
|
||||
import { IHttpOpts, IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../../src";
|
||||
import { mkEvent } from "../../test-utils/test-utils";
|
||||
import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
|
||||
import { IEventDecryptionResult } from "../../../src/@types/crypto";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
import { ServerSideSecretStorage } from "../../../src/secret-storage";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@@ -35,16 +36,15 @@ afterEach(() => {
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
describe("RustCrypto", () => {
|
||||
const TEST_USER = "@alice:example.com";
|
||||
const TEST_DEVICE_ID = "TEST_DEVICE";
|
||||
const TEST_USER = "@alice:example.com";
|
||||
const TEST_DEVICE_ID = "TEST_DEVICE";
|
||||
|
||||
describe("RustCrypto", () => {
|
||||
describe(".exportRoomKeys", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockHttpApi = {} as MatrixClient["http"];
|
||||
rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto;
|
||||
rustCrypto = await makeTestRustCrypto();
|
||||
});
|
||||
|
||||
it("should return a list", async () => {
|
||||
@@ -57,8 +57,7 @@ describe("RustCrypto", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockHttpApi = {} as MatrixClient["http"];
|
||||
rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto;
|
||||
rustCrypto = await makeTestRustCrypto();
|
||||
});
|
||||
|
||||
it("should pass through unencrypted to-device messages", async () => {
|
||||
@@ -94,6 +93,26 @@ describe("RustCrypto", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("isCrossSigningReady", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
await expect(rustCrypto.isCrossSigningReady()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("getCrossSigningKeyId", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
await expect(rustCrypto.getCrossSigningKeyId()).resolves.toBe(null);
|
||||
});
|
||||
|
||||
it("bootstrapCrossSigning", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
await rustCrypto.bootstrapCrossSigning({});
|
||||
});
|
||||
|
||||
it("isSecretStorageReady", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
await expect(rustCrypto.isSecretStorageReady()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
describe("outgoing requests", () => {
|
||||
/** the RustCrypto implementation under test */
|
||||
let rustCrypto: RustCrypto;
|
||||
@@ -141,7 +160,13 @@ describe("RustCrypto", () => {
|
||||
makeOutgoingRequest: jest.fn(),
|
||||
} as unknown as Mocked<OutgoingRequestProcessor>;
|
||||
|
||||
rustCrypto = new RustCrypto(olmMachine, {} as MatrixHttpApi<any>, TEST_USER, TEST_DEVICE_ID);
|
||||
rustCrypto = new RustCrypto(
|
||||
olmMachine,
|
||||
{} as MatrixHttpApi<any>,
|
||||
TEST_USER,
|
||||
TEST_DEVICE_ID,
|
||||
{} as ServerSideSecretStorage,
|
||||
);
|
||||
rustCrypto["outgoingRequestProcessor"] = outgoingRequestProcessor;
|
||||
});
|
||||
|
||||
@@ -206,8 +231,7 @@ describe("RustCrypto", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockHttpApi = {} as MatrixClient["http"];
|
||||
rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto;
|
||||
rustCrypto = await makeTestRustCrypto();
|
||||
});
|
||||
|
||||
it("should handle unencrypted events", () => {
|
||||
@@ -235,7 +259,7 @@ describe("RustCrypto", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
beforeEach(async () => {
|
||||
rustCrypto = await initRustCrypto({} as MatrixClient["http"], TEST_USER, TEST_DEVICE_ID);
|
||||
rustCrypto = await makeTestRustCrypto();
|
||||
});
|
||||
|
||||
it("should be true by default", () => {
|
||||
@@ -258,7 +282,13 @@ describe("RustCrypto", () => {
|
||||
olmMachine = {
|
||||
getDevice: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
rustCrypto = new RustCrypto(olmMachine, {} as MatrixClient["http"], TEST_USER, TEST_DEVICE_ID);
|
||||
rustCrypto = new RustCrypto(
|
||||
olmMachine,
|
||||
{} as MatrixClient["http"],
|
||||
TEST_USER,
|
||||
TEST_DEVICE_ID,
|
||||
{} as ServerSideSecretStorage,
|
||||
);
|
||||
});
|
||||
|
||||
it("should call getDevice", async () => {
|
||||
@@ -282,3 +312,16 @@ describe("RustCrypto", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/** build a basic RustCrypto instance for testing
|
||||
*
|
||||
* just provides default arguments for initRustCrypto()
|
||||
*/
|
||||
async function makeTestRustCrypto(
|
||||
http: MatrixHttpApi<IHttpOpts & { onlyData: true }> = {} as MatrixClient["http"],
|
||||
userId: string = TEST_USER,
|
||||
deviceId: string = TEST_DEVICE_ID,
|
||||
secretStorage: ServerSideSecretStorage = {} as ServerSideSecretStorage,
|
||||
): Promise<RustCrypto> {
|
||||
return await initRustCrypto(http, userId, deviceId, secretStorage);
|
||||
}
|
||||
|
||||
@@ -545,6 +545,25 @@ describe("SyncAccumulator", function () {
|
||||
expect(summary["m.heroes"]).toEqual(["@bob:bar"]);
|
||||
});
|
||||
|
||||
it("should correctly update summary properties to zero", function () {
|
||||
// When we receive updates of a summary property, the last of which is 0
|
||||
sa.accumulate(
|
||||
createSyncResponseWithSummary({
|
||||
"m.heroes": ["@alice:bar"],
|
||||
"m.invited_member_count": 2,
|
||||
}),
|
||||
);
|
||||
sa.accumulate(
|
||||
createSyncResponseWithSummary({
|
||||
"m.heroes": ["@alice:bar"],
|
||||
"m.invited_member_count": 0,
|
||||
}),
|
||||
);
|
||||
const summary = sa.getJSON().roomsData.join["!foo:bar"].summary;
|
||||
// Then we give an answer of 0
|
||||
expect(summary["m.invited_member_count"]).toEqual(0);
|
||||
});
|
||||
|
||||
it("should return correctly adjusted age attributes", () => {
|
||||
const delta = 1000;
|
||||
const startingTs = 1000;
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
CallType,
|
||||
CallState,
|
||||
CallParty,
|
||||
CallDirection,
|
||||
} from "../../../src/webrtc/call";
|
||||
import {
|
||||
MCallAnswer,
|
||||
@@ -1652,12 +1653,18 @@ describe("Call", function () {
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
jest.spyOn(call, "hangup");
|
||||
|
||||
await fakeIncomingCall(client, call, "1");
|
||||
|
||||
mockPeerConn = call.peerConn as unknown as MockRTCPeerConnection;
|
||||
|
||||
mockPeerConn.iceConnectionState = "disconnected";
|
||||
mockPeerConn.iceConnectionStateChangeListener!();
|
||||
jest.spyOn(mockPeerConn, "restartIce");
|
||||
});
|
||||
|
||||
it("should restart ICE gathering after being disconnected for 2 seconds", () => {
|
||||
jest.advanceTimersByTime(3 * 1000);
|
||||
expect(mockPeerConn.restartIce).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should hang up after being disconnected for 30 seconds", () => {
|
||||
@@ -1665,6 +1672,20 @@ describe("Call", function () {
|
||||
expect(call.hangup).toHaveBeenCalledWith(CallErrorCode.IceFailed, false);
|
||||
});
|
||||
|
||||
it("should restart ICE gathering once again after ICE being failed", () => {
|
||||
mockPeerConn.iceConnectionState = "failed";
|
||||
mockPeerConn.iceConnectionStateChangeListener!();
|
||||
expect(mockPeerConn.restartIce).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call hangup after ICE being failed and if there not exists a restartIce method", () => {
|
||||
// @ts-ignore
|
||||
mockPeerConn.restartIce = null;
|
||||
mockPeerConn.iceConnectionState = "failed";
|
||||
mockPeerConn.iceConnectionStateChangeListener!();
|
||||
expect(call.hangup).toHaveBeenCalledWith(CallErrorCode.IceFailed, false);
|
||||
});
|
||||
|
||||
it("should not hangup if we've managed to re-connect", () => {
|
||||
mockPeerConn.iceConnectionState = "connected";
|
||||
mockPeerConn.iceConnectionStateChangeListener!();
|
||||
@@ -1692,4 +1713,110 @@ describe("Call", function () {
|
||||
expect(onReplace).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe("should handle glare in negotiation process", () => {
|
||||
beforeEach(async () => {
|
||||
// cut methods not want to test
|
||||
call.hangup = () => null;
|
||||
call.isLocalOnHold = () => true;
|
||||
// @ts-ignore
|
||||
call.updateRemoteSDPStreamMetadata = jest.fn();
|
||||
// @ts-ignore
|
||||
call.getRidOfRTXCodecs = jest.fn();
|
||||
// @ts-ignore
|
||||
call.createAnswer = jest.fn().mockResolvedValue({});
|
||||
// @ts-ignore
|
||||
call.sendVoipEvent = jest.fn();
|
||||
});
|
||||
|
||||
it("and reject remote offer if not polite and have pending local offer", async () => {
|
||||
// not polite user == CallDirection.Outbound
|
||||
call.direction = CallDirection.Outbound;
|
||||
// have already a local offer
|
||||
// @ts-ignore
|
||||
call.makingOffer = true;
|
||||
const offerEvent = makeMockEvent("@test:foo", {
|
||||
description: {
|
||||
type: "offer",
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
});
|
||||
// @ts-ignore
|
||||
call.peerConn = {
|
||||
signalingState: "have-local-offer",
|
||||
setRemoteDescription: jest.fn(),
|
||||
};
|
||||
await call.onNegotiateReceived(offerEvent);
|
||||
expect(call.peerConn?.setRemoteDescription).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("and not reject remote offer if not polite and do have pending answer", async () => {
|
||||
// not polite user == CallDirection.Outbound
|
||||
call.direction = CallDirection.Outbound;
|
||||
// have not a local offer
|
||||
// @ts-ignore
|
||||
call.makingOffer = false;
|
||||
|
||||
// If we have a setRemoteDescription() answer operation pending, then
|
||||
// we will be "stable" by the time the next setRemoteDescription() is
|
||||
// executed, so we count this being readyForOffer when deciding whether to
|
||||
// ignore the offer.
|
||||
// @ts-ignore
|
||||
call.isSettingRemoteAnswerPending = true;
|
||||
const offerEvent = makeMockEvent("@test:foo", {
|
||||
description: {
|
||||
type: "offer",
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
});
|
||||
// @ts-ignore
|
||||
call.peerConn = {
|
||||
signalingState: "have-local-offer",
|
||||
setRemoteDescription: jest.fn(),
|
||||
};
|
||||
await call.onNegotiateReceived(offerEvent);
|
||||
expect(call.peerConn?.setRemoteDescription).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("and not reject remote offer if not polite and do not have pending local offer", async () => {
|
||||
// not polite user == CallDirection.Outbound
|
||||
call.direction = CallDirection.Outbound;
|
||||
// have no local offer
|
||||
// @ts-ignore
|
||||
call.makingOffer = false;
|
||||
const offerEvent = makeMockEvent("@test:foo", {
|
||||
description: {
|
||||
type: "offer",
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
});
|
||||
// @ts-ignore
|
||||
call.peerConn = {
|
||||
signalingState: "stable",
|
||||
setRemoteDescription: jest.fn(),
|
||||
};
|
||||
await call.onNegotiateReceived(offerEvent);
|
||||
expect(call.peerConn?.setRemoteDescription).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("and if polite do rollback pending local offer", async () => {
|
||||
// polite user == CallDirection.Inbound
|
||||
call.direction = CallDirection.Inbound;
|
||||
// have already a local offer
|
||||
// @ts-ignore
|
||||
call.makingOffer = true;
|
||||
const offerEvent = makeMockEvent("@test:foo", {
|
||||
description: {
|
||||
type: "offer",
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
});
|
||||
// @ts-ignore
|
||||
call.peerConn = {
|
||||
signalingState: "have-local-offer",
|
||||
setRemoteDescription: jest.fn(),
|
||||
};
|
||||
await call.onNegotiateReceived(offerEvent);
|
||||
expect(call.peerConn?.setRemoteDescription).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -517,8 +517,7 @@ describe("Group Call", function () {
|
||||
await groupCall.setMicrophoneMuted(false);
|
||||
expect(groupCall.isMicrophoneMuted()).toEqual(false);
|
||||
|
||||
jest.advanceTimersByTime(groupCall.pttMaxTransmitTime + 100);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(groupCall.pttMaxTransmitTime + 100);
|
||||
expect(groupCall.isMicrophoneMuted()).toEqual(true);
|
||||
});
|
||||
|
||||
@@ -585,7 +584,15 @@ describe("Group Call", function () {
|
||||
});
|
||||
mockCall.sendMetadataUpdate = jest.fn().mockReturnValue(metadataUpdatePromise);
|
||||
|
||||
const getUserMediaStreamFlush = Promise.resolve("stream");
|
||||
// @ts-ignore
|
||||
mockCall.cleint = {
|
||||
getMediaHandler: {
|
||||
getUserMediaStream: jest.fn().mockReturnValue(getUserMediaStreamFlush),
|
||||
},
|
||||
};
|
||||
const mutePromise = groupCall.setMicrophoneMuted(true);
|
||||
await getUserMediaStreamFlush;
|
||||
// we should be muted at this point, before the metadata update has been sent
|
||||
expect(groupCall.isMicrophoneMuted()).toEqual(true);
|
||||
expect(mockCall.localUsermediaFeed.setAudioVideoMuted).toHaveBeenCalled();
|
||||
@@ -892,14 +899,34 @@ describe("Group Call", function () {
|
||||
expect(await groupCall.setMicrophoneMuted(false)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when no permission for audio stream", async () => {
|
||||
it("returns false when no permission for audio stream and localCallFeed do not have an audio track", async () => {
|
||||
const groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
// @ts-ignore
|
||||
jest.spyOn(groupCall.localCallFeed, "hasAudioTrack", "get").mockReturnValue(false);
|
||||
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValueOnce(
|
||||
new Error("No Permission"),
|
||||
);
|
||||
expect(await groupCall.setMicrophoneMuted(false)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when user media stream null", async () => {
|
||||
const groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
// @ts-ignore
|
||||
jest.spyOn(groupCall.localCallFeed, "hasAudioTrack", "get").mockReturnValue(false);
|
||||
// @ts-ignore
|
||||
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockResolvedValue({} as MediaStream);
|
||||
expect(await groupCall.setMicrophoneMuted(false)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when no permission for audio stream but localCallFeed has a audio track already", async () => {
|
||||
const groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
// @ts-ignore
|
||||
jest.spyOn(groupCall.localCallFeed, "hasAudioTrack", "get").mockReturnValue(true);
|
||||
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream");
|
||||
expect(mockClient.getMediaHandler().getUserMediaStream).not.toHaveBeenCalled();
|
||||
expect(await groupCall.setMicrophoneMuted(false)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when unmuting video with no video device", async () => {
|
||||
const groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
jest.spyOn(mockClient.getMediaHandler(), "hasVideoDevice").mockResolvedValue(false);
|
||||
|
||||
@@ -91,6 +91,13 @@ describe("StatsReportBuilder", () => {
|
||||
["REMOTE_AUDIO_TRACK_ID", 0.1],
|
||||
["REMOTE_VIDEO_TRACK_ID", 50],
|
||||
]),
|
||||
audioConcealment: new Map([
|
||||
["REMOTE_AUDIO_TRACK_ID", { concealedAudio: 3000, totalAudioDuration: 3000 * 20 }],
|
||||
]),
|
||||
totalAudioConcealment: {
|
||||
concealedAudio: 3000,
|
||||
totalAudioDuration: (1 / 0.05) * 3000,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -104,6 +111,7 @@ describe("StatsReportBuilder", () => {
|
||||
remoteAudioTrack.setLoss({ packetsTotal: 20, packetsLost: 0, isDownloadStream: true });
|
||||
remoteAudioTrack.setBitrate({ download: 4000, upload: 0 });
|
||||
remoteAudioTrack.setJitter(0.1);
|
||||
remoteAudioTrack.setAudioConcealment(3000, 3000 * 20);
|
||||
|
||||
localVideoTrack.setCodec("v8");
|
||||
localVideoTrack.setLoss({ packetsTotal: 30, packetsLost: 6, isDownloadStream: false });
|
||||
|
||||
@@ -43,8 +43,22 @@ describe("StatsReportGatherer", () => {
|
||||
receivedMedia: 0,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
audioTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
});
|
||||
expect(collector.getActive()).toBeTruthy();
|
||||
});
|
||||
@@ -74,8 +88,22 @@ describe("StatsReportGatherer", () => {
|
||||
receivedMedia: 0,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
audioTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
});
|
||||
expect(getStats).toHaveBeenCalled();
|
||||
expect(collector.getActive()).toBeFalsy();
|
||||
|
||||
@@ -37,29 +37,85 @@ describe("SummaryStatsReporter", () => {
|
||||
receivedMedia: 10,
|
||||
receivedAudioMedia: 4,
|
||||
receivedVideoMedia: 6,
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 100,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
receivedMedia: 13,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 13,
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 5,
|
||||
totalAudio: 100,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
receivedMedia: 0,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 10,
|
||||
totalAudio: 100,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
receivedMedia: 15,
|
||||
receivedAudioMedia: 6,
|
||||
receivedVideoMedia: 9,
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 100,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
@@ -69,6 +125,8 @@ describe("SummaryStatsReporter", () => {
|
||||
percentageReceivedVideoMedia: 0.75,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
peerConnections: 4,
|
||||
percentageConcealedAudio: 0.0375,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,8 +136,22 @@ describe("SummaryStatsReporter", () => {
|
||||
receivedMedia: 10,
|
||||
receivedAudioMedia: 10,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 1, maxJitter: 0, maxPacketLoss: 0 },
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
@@ -89,6 +161,8 @@ describe("SummaryStatsReporter", () => {
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
peerConnections: 1,
|
||||
percentageConcealedAudio: 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,8 +172,22 @@ describe("SummaryStatsReporter", () => {
|
||||
receivedMedia: 10,
|
||||
receivedAudioMedia: 10,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 2, muted: 1, maxJitter: 0, maxPacketLoss: 0 },
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 2,
|
||||
muted: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
@@ -109,6 +197,8 @@ describe("SummaryStatsReporter", () => {
|
||||
percentageReceivedVideoMedia: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
peerConnections: 1,
|
||||
percentageConcealedAudio: 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,8 +208,22 @@ describe("SummaryStatsReporter", () => {
|
||||
receivedMedia: 100,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 100,
|
||||
audioTrackSummary: { count: 1, muted: 1, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
@@ -129,6 +233,8 @@ describe("SummaryStatsReporter", () => {
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
peerConnections: 1,
|
||||
percentageConcealedAudio: 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -138,29 +244,85 @@ describe("SummaryStatsReporter", () => {
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 1,
|
||||
receivedVideoMedia: 1,
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 1,
|
||||
receivedVideoMedia: 1,
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 20, maxPacketLoss: 5 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 20,
|
||||
maxPacketLoss: 5,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 1,
|
||||
receivedVideoMedia: 1,
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 2, maxPacketLoss: 5 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 2, maxPacketLoss: 5 },
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 2,
|
||||
maxPacketLoss: 5,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 2,
|
||||
maxPacketLoss: 5,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 1,
|
||||
receivedVideoMedia: 1,
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 2, maxPacketLoss: 5 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 40 },
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 2,
|
||||
maxPacketLoss: 5,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 40,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
@@ -170,6 +332,8 @@ describe("SummaryStatsReporter", () => {
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 20,
|
||||
maxPacketLoss: 40,
|
||||
peerConnections: 4,
|
||||
percentageConcealedAudio: 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -179,8 +343,22 @@ describe("SummaryStatsReporter", () => {
|
||||
receivedMedia: 10,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 10,
|
||||
audioTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
audioTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
@@ -190,6 +368,8 @@ describe("SummaryStatsReporter", () => {
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
peerConnections: 1,
|
||||
percentageConcealedAudio: 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -199,8 +379,22 @@ describe("SummaryStatsReporter", () => {
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 22,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
@@ -210,6 +404,8 @@ describe("SummaryStatsReporter", () => {
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
peerConnections: 1,
|
||||
percentageConcealedAudio: 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -219,8 +415,22 @@ describe("SummaryStatsReporter", () => {
|
||||
receivedMedia: 0,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
audioTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
@@ -230,6 +440,8 @@ describe("SummaryStatsReporter", () => {
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
peerConnections: 1,
|
||||
percentageConcealedAudio: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -226,12 +226,16 @@ describe("TrackStatsReporter", () => {
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -245,12 +249,16 @@ describe("TrackStatsReporter", () => {
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 3,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -266,12 +274,16 @@ describe("TrackStatsReporter", () => {
|
||||
muted: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 3,
|
||||
muted: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -287,17 +299,21 @@ describe("TrackStatsReporter", () => {
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 3,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("and returns summary and build max jitter and packet loss", async () => {
|
||||
it("and returns summary and build max jitter, packet loss and audio conealment", async () => {
|
||||
const trackStatsList = buildMockTrackStatsList();
|
||||
// video remote
|
||||
trackStatsList[1].setJitter(12);
|
||||
@@ -311,6 +327,8 @@ describe("TrackStatsReporter", () => {
|
||||
trackStatsList[5].setJitter(15);
|
||||
trackStatsList[2].setLoss({ packetsLost: 5, packetsTotal: 0, isDownloadStream: true });
|
||||
trackStatsList[5].setLoss({ packetsLost: 0, packetsTotal: 0, isDownloadStream: true });
|
||||
trackStatsList[2].setAudioConcealment(220, 2000);
|
||||
trackStatsList[5].setAudioConcealment(180, 2000);
|
||||
|
||||
const summary = TrackStatsReporter.buildTrackSummary(trackStatsList);
|
||||
expect(summary).toEqual({
|
||||
@@ -319,12 +337,16 @@ describe("TrackStatsReporter", () => {
|
||||
muted: 0,
|
||||
maxJitter: 15,
|
||||
maxPacketLoss: 5,
|
||||
concealedAudio: 400,
|
||||
totalAudio: 4000,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 3,
|
||||
muted: 0,
|
||||
maxJitter: 66,
|
||||
maxPacketLoss: 55,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
+3
-1
@@ -25,7 +25,7 @@ export class ReEmitter {
|
||||
public constructor(private readonly target: EventEmitter) {}
|
||||
|
||||
// Map from emitter to event name to re-emitter
|
||||
private reEmitters = new Map<EventEmitter, Map<string, (...args: any[]) => void>>();
|
||||
private reEmitters = new WeakMap<EventEmitter, Map<string, (...args: any[]) => void>>();
|
||||
|
||||
public reEmit(source: EventEmitter, eventNames: string[]): void {
|
||||
let reEmittersByEvent = this.reEmitters.get(source);
|
||||
@@ -35,6 +35,8 @@ export class ReEmitter {
|
||||
}
|
||||
|
||||
for (const eventName of eventNames) {
|
||||
if (reEmittersByEvent.has(eventName)) continue;
|
||||
|
||||
// We include the source as the last argument for event handlers which may need it,
|
||||
// such as read receipt listeners on the client class which won't have the context
|
||||
// of the room.
|
||||
|
||||
+21
-19
@@ -74,7 +74,6 @@ import {
|
||||
CryptoEventHandlerMap,
|
||||
fixBackupKey,
|
||||
ICryptoCallbacks,
|
||||
IBootstrapCrossSigningOpts,
|
||||
ICheckOwnCrossSigningTrustOpts,
|
||||
isCryptoAvailable,
|
||||
VerificationMethod,
|
||||
@@ -205,7 +204,7 @@ import { LocalNotificationSettings } from "./@types/local_notifications";
|
||||
import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature";
|
||||
import { CryptoBackend } from "./common-crypto/CryptoBackend";
|
||||
import { RUST_SDK_STORE_PREFIX } from "./rust-crypto/constants";
|
||||
import { CryptoApi } from "./crypto-api";
|
||||
import { BootstrapCrossSigningOpts, CryptoApi } from "./crypto-api";
|
||||
import { DeviceInfoMap } from "./crypto/DeviceList";
|
||||
import {
|
||||
AddSecretStorageKeyOpts,
|
||||
@@ -635,7 +634,7 @@ interface IJoinRequestBody {
|
||||
|
||||
interface ITagMetadata {
|
||||
[key: string]: any;
|
||||
order: number;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
interface IMessagesResponse {
|
||||
@@ -2228,7 +2227,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// importing rust-crypto will download the webassembly, so we delay it until we know it will be
|
||||
// needed.
|
||||
const RustCrypto = await import("./rust-crypto");
|
||||
const rustCrypto = await RustCrypto.initRustCrypto(this.http, userId, deviceId);
|
||||
const rustCrypto = await RustCrypto.initRustCrypto(this.http, userId, deviceId, this.secretStorage);
|
||||
this.cryptoBackend = rustCrypto;
|
||||
|
||||
// attach the event listeners needed by RustCrypto
|
||||
@@ -2295,6 +2294,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param forceDownload - Always download the keys even if cached.
|
||||
*
|
||||
* @returns A promise which resolves to a map userId-\>deviceId-\>`DeviceInfo`
|
||||
*
|
||||
* @deprecated Prefer {@link CryptoApi.getUserDeviceInfo}
|
||||
*/
|
||||
public downloadKeys(userIds: string[], forceDownload?: boolean): Promise<DeviceInfoMap> {
|
||||
if (!this.crypto) {
|
||||
@@ -2309,6 +2310,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param userId - the user to list keys for.
|
||||
*
|
||||
* @returns list of devices
|
||||
* @deprecated Prefer {@link CryptoApi.getUserDeviceInfo}
|
||||
*/
|
||||
public getStoredDevicesForUser(userId: string): DeviceInfo[] {
|
||||
if (!this.crypto) {
|
||||
@@ -2324,6 +2326,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param deviceId - unique identifier for the device
|
||||
*
|
||||
* @returns device or null
|
||||
* @deprecated Prefer {@link CryptoApi.getUserDeviceInfo}
|
||||
*/
|
||||
public getStoredDevice(userId: string, deviceId: string): DeviceInfo | null {
|
||||
if (!this.crypto) {
|
||||
@@ -2568,14 +2571,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's cross-signing key ID.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
* Get the ID of one of the user's cross-signing keys
|
||||
*
|
||||
* @param type - The type of key to get the ID of. One of
|
||||
* "master", "self_signing", or "user_signing". Defaults to "master".
|
||||
*
|
||||
* @returns the key ID
|
||||
* @deprecated prefer {@link CryptoApi#getCrossSigningKeyId}
|
||||
*/
|
||||
public getCrossSigningId(type: CrossSigningKey | string = CrossSigningKey.Master): string | null {
|
||||
if (!this.crypto) {
|
||||
@@ -2732,12 +2734,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* bootstrapCrossSigning() completes successfully, this function should
|
||||
* return true.
|
||||
* @returns True if cross-signing is ready to be used on this device
|
||||
* @deprecated Prefer {@link CryptoApi.isCrossSigningReady | `CryptoApi.isCrossSigningReady`}:
|
||||
*/
|
||||
public isCrossSigningReady(): Promise<boolean> {
|
||||
if (!this.crypto) {
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this.crypto.isCrossSigningReady();
|
||||
return this.cryptoBackend.isCrossSigningReady();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2747,15 +2750,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* This function:
|
||||
* - creates new cross-signing keys if they are not found locally cached nor in
|
||||
* secret storage (if it has been setup)
|
||||
* secret storage (if it has been set up)
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
* @deprecated Prefer {@link CryptoApi.bootstrapCrossSigning | `CryptoApi.bootstrapCrossSigning`}.
|
||||
*/
|
||||
public bootstrapCrossSigning(opts: IBootstrapCrossSigningOpts): Promise<void> {
|
||||
if (!this.crypto) {
|
||||
public bootstrapCrossSigning(opts: BootstrapCrossSigningOpts): Promise<void> {
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this.crypto.bootstrapCrossSigning(opts);
|
||||
return this.cryptoBackend.bootstrapCrossSigning(opts);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2844,15 +2847,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* bootstrapSecretStorage() completes successfully, this function should
|
||||
* return true.
|
||||
*
|
||||
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @returns True if secret storage is ready to be used on this device
|
||||
* @deprecated Prefer {@link CryptoApi.isSecretStorageReady | `CryptoApi.isSecretStorageReady`}:
|
||||
*/
|
||||
public isSecretStorageReady(): Promise<boolean> {
|
||||
if (!this.crypto) {
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this.crypto.isSecretStorageReady();
|
||||
return this.cryptoBackend.isSecretStorageReady();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4185,7 +4187,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves: to an empty object
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public setRoomTag(roomId: string, tagName: string, metadata: ITagMetadata): Promise<{}> {
|
||||
public setRoomTag(roomId: string, tagName: string, metadata: ITagMetadata = {}): Promise<{}> {
|
||||
const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", {
|
||||
$userId: this.credentials.userId!,
|
||||
$roomId: roomId,
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as utils from "./utils";
|
||||
import { encodeParams } from "./utils";
|
||||
|
||||
/**
|
||||
* Get the HTTP URL for an MXC URI.
|
||||
@@ -74,6 +74,6 @@ export function getHttpUriForMxc(
|
||||
serverAndMediaId = serverAndMediaId.slice(0, fragmentOffset);
|
||||
}
|
||||
|
||||
const urlParams = Object.keys(params).length === 0 ? "" : "?" + utils.encodeParams(params);
|
||||
const urlParams = Object.keys(params).length === 0 ? "" : "?" + encodeParams(params);
|
||||
return baseUrl + prefix + serverAndMediaId + urlParams + fragment;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,14 @@ limitations under the License.
|
||||
import type { IMegolmSessionData } from "./@types/crypto";
|
||||
import { Room } from "./models/room";
|
||||
import { DeviceMap } from "./models/device";
|
||||
import { UIAuthCallback } from "./interactive-auth";
|
||||
|
||||
/** Types of cross-signing key */
|
||||
export enum CrossSigningKey {
|
||||
Master = "master",
|
||||
SelfSigning = "self_signing",
|
||||
UserSigning = "user_signing",
|
||||
}
|
||||
|
||||
/**
|
||||
* Public interface to the cryptography parts of the js-sdk
|
||||
@@ -121,6 +129,75 @@ export interface CryptoApi {
|
||||
* @returns Verification status of the device, or `null` if the device is not known
|
||||
*/
|
||||
getDeviceVerificationStatus(userId: string, deviceId: string): Promise<DeviceVerificationStatus | null>;
|
||||
|
||||
/**
|
||||
* Checks whether cross signing:
|
||||
* - is enabled on this account and trusted by this device
|
||||
* - has private keys either cached locally or stored in secret storage
|
||||
*
|
||||
* If this function returns false, bootstrapCrossSigning() can be used
|
||||
* to fix things such that it returns true. That is to say, after
|
||||
* bootstrapCrossSigning() completes successfully, this function should
|
||||
* return true.
|
||||
*
|
||||
* @returns True if cross-signing is ready to be used on this device
|
||||
*/
|
||||
isCrossSigningReady(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get the ID of one of the user's cross-signing keys.
|
||||
*
|
||||
* @param type - The type of key to get the ID of. One of `CrossSigningKey.Master`, `CrossSigngingKey.SelfSigning`,
|
||||
* or `CrossSigningKey.UserSigning`. Defaults to `CrossSigningKey.Master`.
|
||||
*
|
||||
* @returns If cross-signing has been initialised on this device, the ID of the given key. Otherwise, null
|
||||
*/
|
||||
getCrossSigningKeyId(type?: CrossSigningKey): Promise<string | null>;
|
||||
|
||||
/**
|
||||
* Bootstrap cross-signing by creating keys if needed.
|
||||
*
|
||||
* If everything is already set up, then no changes are made, so this is safe to run to ensure
|
||||
* cross-signing is ready for use.
|
||||
*
|
||||
* This function:
|
||||
* - creates new cross-signing keys if they are not found locally cached nor in
|
||||
* secret storage (if it has been set up)
|
||||
* - publishes the public keys to the server if they are not already published
|
||||
* - stores the private keys in secret storage if secret storage is set up.
|
||||
*
|
||||
* @param opts - options object
|
||||
*/
|
||||
bootstrapCrossSigning(opts: BootstrapCrossSigningOpts): Promise<void>;
|
||||
|
||||
/**
|
||||
* Checks whether secret storage:
|
||||
* - is enabled on this account
|
||||
* - is storing cross-signing private keys
|
||||
* - is storing session backup key (if enabled)
|
||||
*
|
||||
* If this function returns false, bootstrapSecretStorage() can be used
|
||||
* to fix things such that it returns true. That is to say, after
|
||||
* bootstrapSecretStorage() completes successfully, this function should
|
||||
* return true.
|
||||
*
|
||||
* @returns True if secret storage is ready to be used on this device
|
||||
*/
|
||||
isSecretStorageReady(): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options object for `CryptoApi.bootstrapCrossSigning`.
|
||||
*/
|
||||
export interface BootstrapCrossSigningOpts {
|
||||
/** Optional. Reset the cross-signing keys even if keys already exist. */
|
||||
setupNewCrossSigning?: boolean;
|
||||
|
||||
/**
|
||||
* An application callback to collect the authentication data for uploading the keys. If not given, the keys
|
||||
* will not be uploaded to the server (which seems like a bad thing?).
|
||||
*/
|
||||
authUploadDeviceSigningKeys?: UIAuthCallback<void>;
|
||||
}
|
||||
|
||||
export class DeviceVerificationStatus {
|
||||
|
||||
@@ -19,7 +19,7 @@ import { MatrixEvent } from "../models/event";
|
||||
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
|
||||
import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
|
||||
import { Method, ClientPrefix } from "../http-api";
|
||||
import { Crypto, ICryptoCallbacks, IBootstrapCrossSigningOpts } from "./index";
|
||||
import { Crypto, ICryptoCallbacks } from "./index";
|
||||
import {
|
||||
ClientEvent,
|
||||
ClientEventHandlerMap,
|
||||
@@ -31,9 +31,10 @@ import {
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { AccountDataClient, SecretStorageKeyDescription } from "../secret-storage";
|
||||
import { BootstrapCrossSigningOpts } from "../crypto-api";
|
||||
|
||||
interface ICrossSigningKeys {
|
||||
authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
|
||||
authUpload: BootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
|
||||
keys: Record<"master" | "self_signing" | "user_signing", ICrossSigningKey>;
|
||||
}
|
||||
|
||||
|
||||
+1
-6
@@ -19,6 +19,7 @@ import { IKeyBackupInfo } from "./keybackup";
|
||||
import type { AddSecretStorageKeyOpts } from "../secret-storage";
|
||||
|
||||
/* re-exports for backwards compatibility. */
|
||||
export { CrossSigningKey } from "../crypto-api";
|
||||
export type {
|
||||
AddSecretStorageKeyOpts as IAddSecretStorageKeyOpts,
|
||||
PassphraseInfo as IPassphraseInfo,
|
||||
@@ -27,12 +28,6 @@ export type {
|
||||
|
||||
// TODO: Merge this with crypto.js once converted
|
||||
|
||||
export enum CrossSigningKey {
|
||||
Master = "master",
|
||||
SelfSigning = "self_signing",
|
||||
UserSigning = "user_signing",
|
||||
}
|
||||
|
||||
export interface IEncryptedEventInfo {
|
||||
/**
|
||||
* whether the event is encrypted (if not encrypted, some of the other properties may not be set)
|
||||
|
||||
+21
-18
@@ -35,7 +35,13 @@ import * as algorithms from "./algorithms";
|
||||
import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from "./CrossSigning";
|
||||
import { EncryptionSetupBuilder } from "./EncryptionSetup";
|
||||
import { SecretStorage as LegacySecretStorage } from "./SecretStorage";
|
||||
import { ICreateSecretStorageOpts, IEncryptedEventInfo, IImportRoomKeysOpts, IRecoveryKey } from "./api";
|
||||
import {
|
||||
CrossSigningKey,
|
||||
ICreateSecretStorageOpts,
|
||||
IEncryptedEventInfo,
|
||||
IImportRoomKeysOpts,
|
||||
IRecoveryKey,
|
||||
} from "./api";
|
||||
import { OutgoingRoomKeyRequestManager } from "./OutgoingRoomKeyRequestManager";
|
||||
import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
|
||||
import { VerificationBase } from "./verification/Base";
|
||||
@@ -45,7 +51,7 @@ import { keyFromPassphrase } from "./key_passphrase";
|
||||
import { decodeRecoveryKey, encodeRecoveryKey } from "./recoverykey";
|
||||
import { VerificationRequest } from "./verification/request/VerificationRequest";
|
||||
import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChannel";
|
||||
import { ToDeviceChannel, ToDeviceRequests, Request } from "./verification/request/ToDeviceChannel";
|
||||
import { Request, ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel";
|
||||
import { IllegalMethod } from "./verification/IllegalMethod";
|
||||
import { KeySignatureUploadError } from "../errors";
|
||||
import { calculateKeyCheck, decryptAES, encryptAES } from "./aes";
|
||||
@@ -54,7 +60,7 @@ import { BackupManager } from "./backup";
|
||||
import { IStore } from "../store";
|
||||
import { Room, RoomEvent } from "../models/room";
|
||||
import { RoomMember, RoomMemberEvent } from "../models/room-member";
|
||||
import { EventStatus, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event";
|
||||
import { EventStatus, IContent, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event";
|
||||
import { ToDeviceBatch } from "../models/ToDeviceMessage";
|
||||
import {
|
||||
ClientEvent,
|
||||
@@ -70,7 +76,6 @@ import { ISyncStateData } from "../sync";
|
||||
import { CryptoStore } from "./store/base";
|
||||
import { IVerificationChannel } from "./verification/request/Channel";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { IContent } from "../models/event";
|
||||
import { IDeviceLists, ISyncResponse, IToDeviceEvent } from "../sync-accumulator";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
import { IMessage } from "./algorithms/olm";
|
||||
@@ -80,18 +85,21 @@ import { MapWithDefault, recursiveMapToObject } from "../utils";
|
||||
import {
|
||||
AccountDataClient,
|
||||
AddSecretStorageKeyOpts,
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
SecretStorageCallbacks,
|
||||
SecretStorageKeyDescription,
|
||||
SecretStorageKeyObject,
|
||||
SecretStorageKeyTuple,
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
SecretStorageCallbacks,
|
||||
ServerSideSecretStorageImpl,
|
||||
} from "../secret-storage";
|
||||
import { ISecretRequest } from "./SecretSharing";
|
||||
import { DeviceVerificationStatus } from "../crypto-api";
|
||||
import { BootstrapCrossSigningOpts, DeviceVerificationStatus } from "../crypto-api";
|
||||
import { Device, DeviceMap } from "../models/device";
|
||||
import { deviceInfoToDevice } from "./device-converter";
|
||||
|
||||
/* re-exports for backwards compatibility */
|
||||
export type { BootstrapCrossSigningOpts as IBootstrapCrossSigningOpts } from "../crypto-api";
|
||||
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
const defaultVerificationMethods = {
|
||||
@@ -127,16 +135,6 @@ interface IInitOpts {
|
||||
pickleKey?: string;
|
||||
}
|
||||
|
||||
export interface IBootstrapCrossSigningOpts {
|
||||
/** Optional. Reset even if keys already exist. */
|
||||
setupNewCrossSigning?: boolean;
|
||||
/**
|
||||
* A function that makes the request requiring auth. Receives the auth data as an object.
|
||||
* Can be called multiple times, first with an empty authDict, to obtain the flows.
|
||||
*/
|
||||
authUploadDeviceSigningKeys?(makeRequest: (authData: any) => Promise<{}>): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ICryptoCallbacks extends SecretStorageCallbacks {
|
||||
getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array | null>;
|
||||
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
|
||||
@@ -769,7 +767,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
public async bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys,
|
||||
setupNewCrossSigning,
|
||||
}: IBootstrapCrossSigningOpts = {}): Promise<void> {
|
||||
}: BootstrapCrossSigningOpts = {}): Promise<void> {
|
||||
logger.log("Bootstrapping cross-signing");
|
||||
|
||||
const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks;
|
||||
@@ -1422,6 +1420,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
*
|
||||
* @returns the key ID
|
||||
*/
|
||||
public getCrossSigningKeyId(type: CrossSigningKey = CrossSigningKey.Master): Promise<string | null> {
|
||||
return Promise.resolve(this.getCrossSigningId(type));
|
||||
}
|
||||
|
||||
// old name, for backwards compatibility
|
||||
public getCrossSigningId(type: string): string | null {
|
||||
return this.crossSigningInfo.getId(type);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger, PrefixedLogger } from "../../logger";
|
||||
import * as utils from "../../utils";
|
||||
import { deepCompare } from "../../utils";
|
||||
import {
|
||||
CryptoStore,
|
||||
IDeviceData,
|
||||
@@ -158,7 +158,7 @@ export class Backend implements CryptoStore {
|
||||
|
||||
const existing = cursor.value;
|
||||
|
||||
if (utils.deepCompare(existing.requestBody, requestBody)) {
|
||||
if (deepCompare(existing.requestBody, requestBody)) {
|
||||
// got a match
|
||||
callback(existing);
|
||||
return;
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "../../logger";
|
||||
import * as utils from "../../utils";
|
||||
import { deepCompare, promiseTry } from "../../utils";
|
||||
import {
|
||||
CryptoStore,
|
||||
IDeviceData,
|
||||
@@ -90,7 +90,7 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise<OutgoingRoomKeyRequest> {
|
||||
const requestBody = request.requestBody;
|
||||
|
||||
return utils.promiseTry(() => {
|
||||
return promiseTry(() => {
|
||||
// first see if we already have an entry for this request.
|
||||
const existing = this._getOutgoingRoomKeyRequest(requestBody);
|
||||
|
||||
@@ -138,7 +138,7 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private _getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): OutgoingRoomKeyRequest | null {
|
||||
for (const existing of this.outgoingRoomKeyRequests) {
|
||||
if (utils.deepCompare(existing.requestBody, requestBody)) {
|
||||
if (deepCompare(existing.requestBody, requestBody)) {
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ limitations under the License.
|
||||
* This is an internal module. See {@link MatrixHttpApi} for the public class.
|
||||
*/
|
||||
|
||||
import * as utils from "../utils";
|
||||
import { checkObjectHasKeys, encodeParams } from "../utils";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { Method } from "./method";
|
||||
import { ConnectionError, MatrixError } from "./errors";
|
||||
@@ -45,7 +45,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
private eventEmitter: TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>,
|
||||
public readonly opts: O,
|
||||
) {
|
||||
utils.checkObjectHasKeys(opts, ["baseUrl", "prefix"]);
|
||||
checkObjectHasKeys(opts, ["baseUrl", "prefix"]);
|
||||
opts.onlyData = !!opts.onlyData;
|
||||
opts.useAuthorizationHeader = opts.useAuthorizationHeader ?? true;
|
||||
}
|
||||
@@ -304,7 +304,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
public getUrl(path: string, queryParams?: QueryDict, prefix?: string, baseUrl?: string): URL {
|
||||
const url = new URL((baseUrl ?? this.opts.baseUrl) + (prefix ?? this.opts.prefix) + path);
|
||||
if (queryParams) {
|
||||
utils.encodeParams(queryParams, url.searchParams);
|
||||
encodeParams(queryParams, url.searchParams);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
+13
-13
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import { FetchHttpApi } from "./fetch";
|
||||
import { FileType, IContentUri, IHttpOpts, Upload, UploadOpts, UploadResponse } from "./interface";
|
||||
import { MediaPrefix } from "./prefix";
|
||||
import * as utils from "../utils";
|
||||
import { defer, QueryDict, removeElement } from "../utils";
|
||||
import * as callbacks from "../realtime-callbacks";
|
||||
import { Method } from "./method";
|
||||
import { ConnectionError } from "./errors";
|
||||
@@ -58,14 +58,14 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
total: 0,
|
||||
abortController,
|
||||
} as Upload;
|
||||
const defer = utils.defer<UploadResponse>();
|
||||
const deferred = defer<UploadResponse>();
|
||||
|
||||
if (global.XMLHttpRequest) {
|
||||
const xhr = new global.XMLHttpRequest();
|
||||
|
||||
const timeoutFn = function (): void {
|
||||
xhr.abort();
|
||||
defer.reject(new Error("Timeout"));
|
||||
deferred.reject(new Error("Timeout"));
|
||||
};
|
||||
|
||||
// set an initial timeout of 30s; we'll advance it each time we get a progress notification
|
||||
@@ -84,16 +84,16 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
}
|
||||
|
||||
if (xhr.status >= 400) {
|
||||
defer.reject(parseErrorResponse(xhr, xhr.responseText));
|
||||
deferred.reject(parseErrorResponse(xhr, xhr.responseText));
|
||||
} else {
|
||||
defer.resolve(JSON.parse(xhr.responseText));
|
||||
deferred.resolve(JSON.parse(xhr.responseText));
|
||||
}
|
||||
} catch (err) {
|
||||
if ((<Error>err).name === "AbortError") {
|
||||
defer.reject(err);
|
||||
deferred.reject(err);
|
||||
return;
|
||||
}
|
||||
defer.reject(new ConnectionError("request failed", <Error>err));
|
||||
deferred.reject(new ConnectionError("request failed", <Error>err));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -131,7 +131,7 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
xhr.abort();
|
||||
});
|
||||
} else {
|
||||
const queryParams: utils.QueryDict = {};
|
||||
const queryParams: QueryDict = {};
|
||||
if (includeFilename && fileName) {
|
||||
queryParams.filename = fileName;
|
||||
}
|
||||
@@ -146,16 +146,16 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
.then((response) => {
|
||||
return this.opts.onlyData ? <UploadResponse>response : response.json();
|
||||
})
|
||||
.then(defer.resolve, defer.reject);
|
||||
.then(deferred.resolve, deferred.reject);
|
||||
}
|
||||
|
||||
// remove the upload from the list on completion
|
||||
upload.promise = defer.promise.finally(() => {
|
||||
utils.removeElement(this.uploads, (elem) => elem === upload);
|
||||
upload.promise = deferred.promise.finally(() => {
|
||||
removeElement(this.uploads, (elem) => elem === upload);
|
||||
});
|
||||
abortController.signal.addEventListener("abort", () => {
|
||||
utils.removeElement(this.uploads, (elem) => elem === upload);
|
||||
defer.reject(new DOMException("Aborted", "AbortError"));
|
||||
removeElement(this.uploads, (elem) => elem === upload);
|
||||
deferred.reject(new DOMException("Aborted", "AbortError"));
|
||||
});
|
||||
this.uploads.push(upload);
|
||||
return upload.promise;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { logger } from "./logger";
|
||||
import { MatrixClient } from "./client";
|
||||
import { defer, IDeferred } from "./utils";
|
||||
import { MatrixError } from "./http-api";
|
||||
import { UIAResponse } from "./@types/uia";
|
||||
|
||||
const EMAIL_STAGE_TYPE = "m.login.email.identity";
|
||||
const MSISDN_STAGE_TYPE = "m.login.msisdn";
|
||||
@@ -118,6 +119,16 @@ export class NoAuthFlowFoundError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of an application callback to perform the user-interactive bit of UIA.
|
||||
*
|
||||
* It is called with a single parameter, `makeRequest`, which is a function which takes the UIA parameters and
|
||||
* makes the HTTP request.
|
||||
*
|
||||
* The generic parameter `T` is the type of the response of the endpoint, once it is eventually successful.
|
||||
*/
|
||||
export type UIAuthCallback<T> = (makeRequest: (authData: IAuthDict) => Promise<UIAResponse<T>>) => Promise<T>;
|
||||
|
||||
interface IOpts {
|
||||
/**
|
||||
* A matrix client to use for the auth process
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
WrappedReceipt,
|
||||
} from "../@types/read_receipts";
|
||||
import { ListenerMap, TypedEventEmitter } from "./typed-event-emitter";
|
||||
import * as utils from "../utils";
|
||||
import { isSupportedReceiptType } from "../utils";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { EventType } from "../@types/event";
|
||||
import { EventTimelineSet } from "./event-timeline-set";
|
||||
@@ -267,7 +267,7 @@ export abstract class ReadReceipt<
|
||||
public getUsersReadUpTo(event: MatrixEvent): string[] {
|
||||
return this.getReceiptsForEvent(event)
|
||||
.filter(function (receipt) {
|
||||
return utils.isSupportedReceiptType(receipt.type);
|
||||
return isSupportedReceiptType(receipt.type);
|
||||
})
|
||||
.map(function (receipt) {
|
||||
return receipt.userId;
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { getHttpUriForMxc } from "../content-repo";
|
||||
import * as utils from "../utils";
|
||||
import { removeDirectionOverrideChars, removeHiddenChars } from "../utils";
|
||||
import { User } from "./user";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { RoomState } from "./room-state";
|
||||
@@ -206,8 +206,8 @@ export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEve
|
||||
|
||||
// not quite raw: we strip direction override chars so it can safely be inserted into
|
||||
// blocks of text without breaking the text direction
|
||||
this.rawDisplayName = utils.removeDirectionOverrideChars(event.getDirectionalContent().displayname ?? "");
|
||||
if (!this.rawDisplayName || !utils.removeHiddenChars(this.rawDisplayName)) {
|
||||
this.rawDisplayName = removeDirectionOverrideChars(event.getDirectionalContent().displayname ?? "");
|
||||
if (!this.rawDisplayName || !removeHiddenChars(this.rawDisplayName)) {
|
||||
this.rawDisplayName = this.userId;
|
||||
}
|
||||
|
||||
@@ -407,7 +407,7 @@ function shouldDisambiguate(selfUserId: string, displayName?: string, roomState?
|
||||
|
||||
// First check if the displayname is something we consider truthy
|
||||
// after stripping it of zero width characters and padding spaces
|
||||
if (!utils.removeHiddenChars(displayName)) return false;
|
||||
if (!removeHiddenChars(displayName)) return false;
|
||||
|
||||
if (!roomState) return false;
|
||||
|
||||
@@ -432,11 +432,11 @@ function shouldDisambiguate(selfUserId: string, displayName?: string, roomState?
|
||||
function calculateDisplayName(selfUserId: string, displayName: string | undefined, disambiguate: boolean): string {
|
||||
if (!displayName || displayName === selfUserId) return selfUserId;
|
||||
|
||||
if (disambiguate) return utils.removeDirectionOverrideChars(displayName) + " (" + selfUserId + ")";
|
||||
if (disambiguate) return removeDirectionOverrideChars(displayName) + " (" + selfUserId + ")";
|
||||
|
||||
// First check if the displayname is something we consider truthy
|
||||
// after stripping it of zero width characters and padding spaces
|
||||
if (!utils.removeHiddenChars(displayName)) return selfUserId;
|
||||
if (!removeHiddenChars(displayName)) return selfUserId;
|
||||
|
||||
// We always strip the direction override characters (LRO and RLO).
|
||||
// These override the text direction for all subsequent characters
|
||||
@@ -449,5 +449,5 @@ function calculateDisplayName(selfUserId: string, displayName: string | undefine
|
||||
// names should flip into the correct direction automatically based on
|
||||
// the characters, and you can still embed rtl in ltr or vice versa
|
||||
// with the embed chars or marker chars.
|
||||
return utils.removeDirectionOverrideChars(displayName);
|
||||
return removeDirectionOverrideChars(displayName);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import { RoomMember } from "./room-member";
|
||||
import { logger } from "../logger";
|
||||
import * as utils from "../utils";
|
||||
import { isNumber, removeHiddenChars } from "../utils";
|
||||
import { EventType, UNSTABLE_MSC2716_MARKER } from "../@types/event";
|
||||
import { IEvent, MatrixEvent, MatrixEventEvent } from "./event";
|
||||
import { MatrixClient } from "../client";
|
||||
@@ -759,7 +759,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
* @returns An array of user IDs or an empty array.
|
||||
*/
|
||||
public getUserIdsWithDisplayName(displayName: string): string[] {
|
||||
return this.displayNameToUserIds.get(utils.removeHiddenChars(displayName)) ?? [];
|
||||
return this.displayNameToUserIds.get(removeHiddenChars(displayName)) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -798,7 +798,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
}
|
||||
|
||||
let requiredLevel = 50;
|
||||
if (utils.isNumber(powerLevels[action])) {
|
||||
if (isNumber(powerLevels[action])) {
|
||||
requiredLevel = powerLevels[action]!;
|
||||
}
|
||||
|
||||
@@ -928,7 +928,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
powerLevelsEvent &&
|
||||
powerLevelsEvent.getContent() &&
|
||||
powerLevelsEvent.getContent().notifications &&
|
||||
utils.isNumber(powerLevelsEvent.getContent().notifications[notifLevelKey])
|
||||
isNumber(powerLevelsEvent.getContent().notifications[notifLevelKey])
|
||||
) {
|
||||
notifLevel = powerLevelsEvent.getContent().notifications[notifLevelKey];
|
||||
}
|
||||
@@ -1058,7 +1058,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
// We clobber the user_id > name lookup but the name -> [user_id] lookup
|
||||
// means we need to remove that user ID from that array rather than nuking
|
||||
// the lot.
|
||||
const strippedOldName = utils.removeHiddenChars(oldName);
|
||||
const strippedOldName = removeHiddenChars(oldName);
|
||||
|
||||
const existingUserIds = this.displayNameToUserIds.get(strippedOldName);
|
||||
if (existingUserIds) {
|
||||
@@ -1070,7 +1070,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
|
||||
this.userIdsToDisplayNames[userId] = displayName;
|
||||
|
||||
const strippedDisplayname = displayName && utils.removeHiddenChars(displayName);
|
||||
const strippedDisplayname = displayName && removeHiddenChars(displayName);
|
||||
// an empty stripped displayname (undefined/'') will be set to MXID in room-member.js
|
||||
if (strippedDisplayname) {
|
||||
const arr = this.displayNameToUserIds.get(strippedDisplayname) ?? [];
|
||||
|
||||
+3
-3
@@ -24,7 +24,7 @@ import {
|
||||
} from "./event-timeline-set";
|
||||
import { Direction, EventTimeline } from "./event-timeline";
|
||||
import { getHttpUriForMxc } from "../content-repo";
|
||||
import * as utils from "../utils";
|
||||
import { compare, removeElement } from "../utils";
|
||||
import { normalize, noUnsafeEventProps } from "../utils";
|
||||
import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event";
|
||||
import { EventStatus } from "./event-status";
|
||||
@@ -733,7 +733,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
);
|
||||
}
|
||||
|
||||
const removed = utils.removeElement(
|
||||
const removed = removeElement(
|
||||
this.pendingEventList,
|
||||
function (ev) {
|
||||
return ev.getId() == eventId;
|
||||
@@ -3267,7 +3267,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
return true;
|
||||
});
|
||||
// make sure members have stable order
|
||||
otherMembers.sort((a, b) => utils.compare(a.userId, b.userId));
|
||||
otherMembers.sort((a, b) => compare(a.userId, b.userId));
|
||||
// only 5 first members, immitate summaryHeroes
|
||||
otherMembers = otherMembers.slice(0, 5);
|
||||
otherNames = otherMembers.map((m) => m.name);
|
||||
|
||||
@@ -18,7 +18,7 @@ import { IMinimalEvent } from "./sync-accumulator";
|
||||
import { EventType } from "./@types/event";
|
||||
import { isSupportedReceiptType, MapWithDefault, recursiveMapToObject } from "./utils";
|
||||
import { IContent } from "./models/event";
|
||||
import { MAIN_ROOM_TIMELINE, ReceiptContent, ReceiptType } from "./@types/read_receipts";
|
||||
import { ReceiptContent, ReceiptType } from "./@types/read_receipts";
|
||||
|
||||
interface AccumulatedReceipt {
|
||||
data: IMinimalEvent;
|
||||
@@ -118,7 +118,7 @@ export class ReceiptAccumulator {
|
||||
eventId,
|
||||
};
|
||||
|
||||
if (!data.thread_id || data.thread_id === MAIN_ROOM_TIMELINE) {
|
||||
if (!data.thread_id) {
|
||||
this.setUnthreaded(userId, receipt);
|
||||
} else {
|
||||
this.setThreaded(data.thread_id, userId, receipt);
|
||||
|
||||
@@ -23,11 +23,14 @@ import {
|
||||
RoomMessageRequest,
|
||||
SignatureUploadRequest,
|
||||
ToDeviceRequest,
|
||||
SigningKeysUploadRequest,
|
||||
} from "@matrix-org/matrix-sdk-crypto-js";
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { IHttpOpts, MatrixHttpApi, Method } from "../http-api";
|
||||
import { QueryDict } from "../utils";
|
||||
import { IAuthDict, UIAuthCallback } from "../interactive-auth";
|
||||
import { UIAResponse } from "../@types/uia";
|
||||
|
||||
/**
|
||||
* Common interface for all the request types returned by `OlmMachine.outgoingRequests`.
|
||||
@@ -53,7 +56,7 @@ export class OutgoingRequestProcessor {
|
||||
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
) {}
|
||||
|
||||
public async makeOutgoingRequest(msg: OutgoingRequest): Promise<void> {
|
||||
public async makeOutgoingRequest<T>(msg: OutgoingRequest, uiaCallback?: UIAuthCallback<T>): Promise<void> {
|
||||
let resp: string;
|
||||
|
||||
/* refer https://docs.rs/matrix-sdk-crypto/0.6.0/matrix_sdk_crypto/requests/enum.OutgoingRequests.html
|
||||
@@ -79,6 +82,14 @@ export class OutgoingRequestProcessor {
|
||||
`/_matrix/client/v3/room/${encodeURIComponent(msg.room_id)}/send/` +
|
||||
`${encodeURIComponent(msg.event_type)}/${encodeURIComponent(msg.txn_id)}`;
|
||||
resp = await this.rawJsonRequest(Method.Put, path, {}, msg.body);
|
||||
} else if (msg instanceof SigningKeysUploadRequest) {
|
||||
resp = await this.makeRequestWithUIA(
|
||||
Method.Post,
|
||||
"/_matrix/client/v3/keys/device_signing/upload",
|
||||
{},
|
||||
msg.body,
|
||||
uiaCallback,
|
||||
);
|
||||
} else {
|
||||
logger.warn("Unsupported outgoing message", Object.getPrototypeOf(msg));
|
||||
resp = "";
|
||||
@@ -89,6 +100,31 @@ export class OutgoingRequestProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
private async makeRequestWithUIA<T>(
|
||||
method: Method,
|
||||
path: string,
|
||||
queryParams: QueryDict,
|
||||
body: string,
|
||||
uiaCallback: UIAuthCallback<T> | undefined,
|
||||
): Promise<string> {
|
||||
if (!uiaCallback) {
|
||||
return await this.rawJsonRequest(method, path, queryParams, body);
|
||||
}
|
||||
|
||||
const parsedBody = JSON.parse(body);
|
||||
const makeRequest = async (auth: IAuthDict): Promise<UIAResponse<T>> => {
|
||||
const newBody = {
|
||||
...parsedBody,
|
||||
auth,
|
||||
};
|
||||
const resp = await this.rawJsonRequest(method, path, queryParams, JSON.stringify(newBody));
|
||||
return JSON.parse(resp) as T;
|
||||
};
|
||||
|
||||
const resp = await uiaCallback(makeRequest);
|
||||
return JSON.stringify(resp);
|
||||
}
|
||||
|
||||
private async rawJsonRequest(method: Method, path: string, queryParams: QueryDict, body: string): Promise<string> {
|
||||
const opts = {
|
||||
// inhibit the JSON stringification and parsing within HttpApi.
|
||||
|
||||
@@ -20,11 +20,22 @@ import { RustCrypto } from "./rust-crypto";
|
||||
import { logger } from "../logger";
|
||||
import { RUST_SDK_STORE_PREFIX } from "./constants";
|
||||
import { IHttpOpts, MatrixHttpApi } from "../http-api";
|
||||
import { ServerSideSecretStorage } from "../secret-storage";
|
||||
|
||||
/**
|
||||
* Create a new `RustCrypto` implementation
|
||||
*
|
||||
* @param http - Low-level HTTP interface: used to make outgoing requests required by the rust SDK.
|
||||
* We expect it to set the access token, etc.
|
||||
* @param userId - The local user's User ID.
|
||||
* @param deviceId - The local user's Device ID.
|
||||
* @param secretStorage - Interface to server-side secret storage.
|
||||
*/
|
||||
export async function initRustCrypto(
|
||||
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
secretStorage: ServerSideSecretStorage,
|
||||
): Promise<RustCrypto> {
|
||||
// initialise the rust matrix-sdk-crypto-js, if it hasn't already been done
|
||||
await RustSdkCryptoJs.initAsync();
|
||||
@@ -38,7 +49,7 @@ export async function initRustCrypto(
|
||||
|
||||
// TODO: use the pickle key for the passphrase
|
||||
const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(u, d, RUST_SDK_STORE_PREFIX, "test pass");
|
||||
const rustCrypto = new RustCrypto(olmMachine, http, userId, deviceId);
|
||||
const rustCrypto = new RustCrypto(olmMachine, http, userId, deviceId, secretStorage);
|
||||
await olmMachine.registerRoomKeyUpdatedCallback((sessions: RustSdkCryptoJs.RoomKeyInfo[]) =>
|
||||
rustCrypto.onRoomKeysUpdated(sessions),
|
||||
);
|
||||
|
||||
@@ -30,10 +30,12 @@ import { RoomEncryptor } from "./RoomEncryptor";
|
||||
import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
|
||||
import { KeyClaimManager } from "./KeyClaimManager";
|
||||
import { MapWithDefault } from "../utils";
|
||||
import { DeviceVerificationStatus } from "../crypto-api";
|
||||
import { BootstrapCrossSigningOpts, DeviceVerificationStatus } from "../crypto-api";
|
||||
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter";
|
||||
import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
|
||||
import { Device, DeviceMap } from "../models/device";
|
||||
import { ServerSideSecretStorage } from "../secret-storage";
|
||||
import { CrossSigningKey } from "../crypto/api";
|
||||
|
||||
/**
|
||||
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
|
||||
@@ -56,10 +58,24 @@ export class RustCrypto implements CryptoBackend {
|
||||
private outgoingRequestProcessor: OutgoingRequestProcessor;
|
||||
|
||||
public constructor(
|
||||
/** The `OlmMachine` from the underlying rust crypto sdk. */
|
||||
private readonly olmMachine: RustSdkCryptoJs.OlmMachine,
|
||||
|
||||
/**
|
||||
* Low-level HTTP interface: used to make outgoing requests required by the rust SDK.
|
||||
*
|
||||
* We expect it to set the access token, etc.
|
||||
*/
|
||||
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
|
||||
/** The local user's User ID. */
|
||||
_userId: string,
|
||||
|
||||
/** The local user's Device ID. */
|
||||
_deviceId: string,
|
||||
|
||||
/** Interface to server-side secret storage */
|
||||
_secretStorage: ServerSideSecretStorage,
|
||||
) {
|
||||
this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http);
|
||||
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
|
||||
@@ -302,6 +318,35 @@ export class RustCrypto implements CryptoBackend {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#isCrossSigningReady}
|
||||
*/
|
||||
public async isCrossSigningReady(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#getCrossSigningKeyId}
|
||||
*/
|
||||
public async getCrossSigningKeyId(type: CrossSigningKey = CrossSigningKey.Master): Promise<string | null> {
|
||||
// TODO
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#boostrapCrossSigning}
|
||||
*/
|
||||
public async bootstrapCrossSigning(opts: BootstrapCrossSigningOpts): Promise<void> {
|
||||
logger.log("Cross-signing ready");
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#isSecretStorageReady}
|
||||
*/
|
||||
public async isSecretStorageReady(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// SyncCryptoCallbacks implementation
|
||||
@@ -319,7 +364,7 @@ export class RustCrypto implements CryptoBackend {
|
||||
private async receiveSyncChanges({
|
||||
events,
|
||||
oneTimeKeysCounts = new Map<string, number>(),
|
||||
unusedFallbackKeys = new Set<string>(),
|
||||
unusedFallbackKeys,
|
||||
devices = new RustSdkCryptoJs.DeviceLists(),
|
||||
}: {
|
||||
events?: IToDeviceEvent[];
|
||||
|
||||
+5
-6
@@ -18,11 +18,10 @@ limitations under the License.
|
||||
* This is an internal module which manages queuing, scheduling and retrying
|
||||
* of requests.
|
||||
*/
|
||||
import * as utils from "./utils";
|
||||
import { logger } from "./logger";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
import { EventType } from "./@types/event";
|
||||
import { IDeferred } from "./utils";
|
||||
import { defer, IDeferred, removeElement } from "./utils";
|
||||
import { ConnectionError, MatrixError } from "./http-api";
|
||||
import { ISendEventResponse } from "./@types/requests";
|
||||
|
||||
@@ -175,7 +174,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
|
||||
return false;
|
||||
}
|
||||
let removed = false;
|
||||
utils.removeElement(this.queues[name], (element) => {
|
||||
removeElement(this.queues[name], (element) => {
|
||||
if (element.event.getId() === event.getId()) {
|
||||
// XXX we should probably reject the promise?
|
||||
// https://github.com/matrix-org/matrix-js-sdk/issues/496
|
||||
@@ -214,15 +213,15 @@ export class MatrixScheduler<T = ISendEventResponse> {
|
||||
if (!this.queues[queueName]) {
|
||||
this.queues[queueName] = [];
|
||||
}
|
||||
const defer = utils.defer<T>();
|
||||
const deferred = defer<T>();
|
||||
this.queues[queueName].push({
|
||||
event: event,
|
||||
defer: defer,
|
||||
defer: deferred,
|
||||
attempts: 0,
|
||||
});
|
||||
debuglog("Queue algorithm dumped event %s into queue '%s'", event.getId(), queueName);
|
||||
this.startProcessingQueues();
|
||||
return defer.promise;
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private startProcessingQueues(): void {
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend";
|
||||
import { NotificationCountType, Room, RoomEvent } from "./models/room";
|
||||
import { logger } from "./logger";
|
||||
import * as utils from "./utils";
|
||||
import { promiseMapSeries } from "./utils";
|
||||
import { EventTimeline } from "./models/event-timeline";
|
||||
import { ClientEvent, IStoredClientOpts, MatrixClient } from "./client";
|
||||
import {
|
||||
@@ -726,8 +726,8 @@ export class SlidingSyncSdk {
|
||||
}
|
||||
};
|
||||
|
||||
await utils.promiseMapSeries(stateEvents, processRoomEvent);
|
||||
await utils.promiseMapSeries(timelineEvents, processRoomEvent);
|
||||
await promiseMapSeries(stateEvents, processRoomEvent);
|
||||
await promiseMapSeries(timelineEvents, processRoomEvent);
|
||||
ephemeralEvents.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
|
||||
@@ -15,8 +15,8 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { IMinimalEvent, ISyncData, ISyncResponse, SyncAccumulator } from "../sync-accumulator";
|
||||
import * as utils from "../utils";
|
||||
import * as IndexedDBHelpers from "../indexeddb-helpers";
|
||||
import { deepCopy, promiseTry } from "../utils";
|
||||
import { exists as idbExists } from "../indexeddb-helpers";
|
||||
import { logger } from "../logger";
|
||||
import { IStateEventWithRoomId, IStoredClientOpts } from "../matrix";
|
||||
import { ISavedSync } from "./index";
|
||||
@@ -122,7 +122,7 @@ function reqAsCursorPromise<T>(req: IDBRequest<T>): Promise<T> {
|
||||
export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
public static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
|
||||
dbName = "matrix-js-sdk:" + (dbName || "default");
|
||||
return IndexedDBHelpers.exists(indexedDB, dbName);
|
||||
return idbExists(indexedDB, dbName);
|
||||
}
|
||||
|
||||
private readonly dbName: string;
|
||||
@@ -380,7 +380,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
if (copy) {
|
||||
// We must deep copy the stored data so that the /sync processing code doesn't
|
||||
// corrupt the internal state of the sync accumulator (it adds non-clonable keys)
|
||||
return Promise.resolve(utils.deepCopy(data));
|
||||
return Promise.resolve(deepCopy(data));
|
||||
} else {
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
@@ -435,7 +435,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
*/
|
||||
private persistSyncData(nextBatch: string, roomsData: ISyncResponse["rooms"]): Promise<void> {
|
||||
logger.log("Persisting sync data up to", nextBatch);
|
||||
return utils.promiseTry<void>(() => {
|
||||
return promiseTry<void>(() => {
|
||||
const txn = this.db!.transaction(["sync"], "readwrite");
|
||||
const store = txn.objectStore("sync");
|
||||
store.put({
|
||||
@@ -456,7 +456,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
* @returns Promise which resolves if the events were persisted.
|
||||
*/
|
||||
private persistAccountData(accountData: IMinimalEvent[]): Promise<void> {
|
||||
return utils.promiseTry<void>(() => {
|
||||
return promiseTry<void>(() => {
|
||||
const txn = this.db!.transaction(["accountData"], "readwrite");
|
||||
const store = txn.objectStore("accountData");
|
||||
for (const event of accountData) {
|
||||
@@ -475,7 +475,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
* @returns Promise which resolves if the users were persisted.
|
||||
*/
|
||||
private persistUserPresenceEvents(tuples: UserTuple[]): Promise<void> {
|
||||
return utils.promiseTry<void>(() => {
|
||||
return promiseTry<void>(() => {
|
||||
const txn = this.db!.transaction(["users"], "readwrite");
|
||||
const store = txn.objectStore("users");
|
||||
for (const tuple of tuples) {
|
||||
@@ -495,7 +495,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
* @returns A list of presence events in their raw form.
|
||||
*/
|
||||
public getUserPresenceEvents(): Promise<UserTuple[]> {
|
||||
return utils.promiseTry<UserTuple[]>(() => {
|
||||
return promiseTry<UserTuple[]>(() => {
|
||||
const txn = this.db!.transaction(["users"], "readonly");
|
||||
const store = txn.objectStore("users");
|
||||
return selectQuery(store, undefined, (cursor) => {
|
||||
@@ -510,7 +510,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
*/
|
||||
private loadAccountData(): Promise<IMinimalEvent[]> {
|
||||
logger.log(`LocalIndexedDBStoreBackend: loading account data...`);
|
||||
return utils.promiseTry<IMinimalEvent[]>(() => {
|
||||
return promiseTry<IMinimalEvent[]>(() => {
|
||||
const txn = this.db!.transaction(["accountData"], "readonly");
|
||||
const store = txn.objectStore("accountData");
|
||||
return selectQuery(store, undefined, (cursor) => {
|
||||
@@ -528,7 +528,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
*/
|
||||
private loadSyncData(): Promise<ISyncData> {
|
||||
logger.log(`LocalIndexedDBStoreBackend: loading sync data...`);
|
||||
return utils.promiseTry<ISyncData>(() => {
|
||||
return promiseTry<ISyncData>(() => {
|
||||
const txn = this.db!.transaction(["sync"], "readonly");
|
||||
const store = txn.objectStore("sync");
|
||||
return selectQuery(store, undefined, (cursor) => {
|
||||
|
||||
@@ -399,9 +399,9 @@ export class SyncAccumulator {
|
||||
|
||||
const acc = currentData._summary;
|
||||
const sum = data.summary;
|
||||
acc[HEROES_KEY] = sum[HEROES_KEY] || acc[HEROES_KEY];
|
||||
acc[JOINED_COUNT_KEY] = sum[JOINED_COUNT_KEY] || acc[JOINED_COUNT_KEY];
|
||||
acc[INVITED_COUNT_KEY] = sum[INVITED_COUNT_KEY] || acc[INVITED_COUNT_KEY];
|
||||
acc[HEROES_KEY] = sum[HEROES_KEY] ?? acc[HEROES_KEY];
|
||||
acc[JOINED_COUNT_KEY] = sum[JOINED_COUNT_KEY] ?? acc[JOINED_COUNT_KEY];
|
||||
acc[INVITED_COUNT_KEY] = sum[INVITED_COUNT_KEY] ?? acc[INVITED_COUNT_KEY];
|
||||
}
|
||||
|
||||
// We purposefully do not persist m.typing events.
|
||||
|
||||
+6
-6
@@ -28,7 +28,7 @@ import { Optional } from "matrix-events-sdk";
|
||||
import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend";
|
||||
import { User, UserEvent } from "./models/user";
|
||||
import { NotificationCountType, Room, RoomEvent } from "./models/room";
|
||||
import * as utils from "./utils";
|
||||
import { promiseMapSeries, defer, deepCopy } from "./utils";
|
||||
import { IDeferred, noUnsafeEventProps, unsafeProp } from "./utils";
|
||||
import { Filter } from "./filter";
|
||||
import { EventTimeline } from "./models/event-timeline";
|
||||
@@ -414,7 +414,7 @@ export class SyncApi {
|
||||
|
||||
// FIXME: Mostly duplicated from injectRoomEvents but not entirely
|
||||
// because "state" in this API is at the BEGINNING of the chunk
|
||||
const oldStateEvents = utils.deepCopy(response.state).map(client.getEventMapper());
|
||||
const oldStateEvents = deepCopy(response.state).map(client.getEventMapper());
|
||||
const stateEvents = response.state.map(client.getEventMapper());
|
||||
const messages = response.messages.chunk.map(client.getEventMapper());
|
||||
|
||||
@@ -1247,7 +1247,7 @@ export class SyncApi {
|
||||
this.notifEvents = [];
|
||||
|
||||
// Handle invites
|
||||
await utils.promiseMapSeries(inviteRooms, async (inviteObj) => {
|
||||
await promiseMapSeries(inviteRooms, async (inviteObj) => {
|
||||
const room = inviteObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room);
|
||||
|
||||
@@ -1288,7 +1288,7 @@ export class SyncApi {
|
||||
});
|
||||
|
||||
// Handle joins
|
||||
await utils.promiseMapSeries(joinRooms, async (joinObj) => {
|
||||
await promiseMapSeries(joinRooms, async (joinObj) => {
|
||||
const room = joinObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(joinObj.state, room);
|
||||
// Prevent events from being decrypted ahead of time
|
||||
@@ -1471,7 +1471,7 @@ export class SyncApi {
|
||||
});
|
||||
|
||||
// Handle leaves (e.g. kicked rooms)
|
||||
await utils.promiseMapSeries(leaveRooms, async (leaveObj) => {
|
||||
await promiseMapSeries(leaveRooms, async (leaveObj) => {
|
||||
const room = leaveObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
|
||||
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
|
||||
@@ -1552,7 +1552,7 @@ export class SyncApi {
|
||||
this.pokeKeepAlive();
|
||||
}
|
||||
if (!this.connectionReturnedDefer) {
|
||||
this.connectionReturnedDefer = utils.defer();
|
||||
this.connectionReturnedDefer = defer();
|
||||
}
|
||||
return this.connectionReturnedDefer.promise;
|
||||
}
|
||||
|
||||
+43
-12
@@ -25,7 +25,7 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import { parse as parseSdp, write as writeSdp } from "sdp-transform";
|
||||
|
||||
import { logger } from "../logger";
|
||||
import * as utils from "../utils";
|
||||
import { checkObjectHasKeys, isNullOrUndefined, recursivelyAssign } from "../utils";
|
||||
import { IContent, MatrixEvent } from "../models/event";
|
||||
import { EventType, ToDeviceMessageId } from "../@types/event";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
@@ -263,7 +263,8 @@ const CALL_TIMEOUT_MS = 60 * 1000; // ms
|
||||
const CALL_LENGTH_INTERVAL = 1000; // ms
|
||||
/** The time after which we end the call, if ICE got disconnected */
|
||||
const ICE_DISCONNECTED_TIMEOUT = 30 * 1000; // ms
|
||||
|
||||
/** The time after which we try a ICE restart, if ICE got disconnected */
|
||||
const ICE_RECONNECTING_TIMEOUT = 2 * 1000; // ms
|
||||
export class CallError extends Error {
|
||||
public readonly code: string;
|
||||
|
||||
@@ -382,6 +383,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
private opponentPartyId: string | null | undefined;
|
||||
private opponentCaps?: CallCapabilities;
|
||||
private iceDisconnectedTimeout?: ReturnType<typeof setTimeout>;
|
||||
private iceReconnectionTimeOut?: ReturnType<typeof setTimeout> | undefined;
|
||||
private inviteTimeout?: ReturnType<typeof setTimeout>;
|
||||
private readonly removeTrackListeners = new Map<MediaStream, () => void>();
|
||||
|
||||
@@ -451,7 +453,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
});
|
||||
}
|
||||
for (const server of this.turnServers) {
|
||||
utils.checkObjectHasKeys(server, ["urls"]);
|
||||
checkObjectHasKeys(server, ["urls"]);
|
||||
}
|
||||
this.callId = genCallID();
|
||||
// If the Client provides calls without audio and video we need a datachannel for a webrtc connection
|
||||
@@ -965,6 +967,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
await this.initOpponentCrypto();
|
||||
try {
|
||||
await this.peerConn.setRemoteDescription(invite.offer);
|
||||
logger.debug(`Call ${this.callId} initWithInvite() set remote description: ${invite.offer.type}`);
|
||||
await this.addBufferedIceCandidates();
|
||||
} catch (e) {
|
||||
logger.debug(`Call ${this.callId} initWithInvite() failed to set remote description`, e);
|
||||
@@ -1040,7 +1043,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
);
|
||||
return false;
|
||||
} else if (
|
||||
!utils.isNullOrUndefined(wantedValue) &&
|
||||
!isNullOrUndefined(wantedValue) &&
|
||||
wantedValue !== valueOfTheOtherSide &&
|
||||
!this.opponentSupportsSDPStreamMetadata()
|
||||
) {
|
||||
@@ -1790,10 +1793,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
private gotLocalIceCandidate = (event: RTCPeerConnectionIceEvent): void => {
|
||||
if (event.candidate) {
|
||||
if (this.candidatesEnded) {
|
||||
logger.warn(
|
||||
`Call ${this.callId} gotLocalIceCandidate() got candidate after candidates have ended - ignoring!`,
|
||||
);
|
||||
return;
|
||||
logger.warn(`Call ${this.callId} gotLocalIceCandidate() got candidate after candidates have ended!`);
|
||||
}
|
||||
|
||||
logger.debug(`Call ${this.callId} got local ICE ${event.candidate.sdpMid} ${event.candidate.candidate}`);
|
||||
@@ -1817,7 +1817,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
}`,
|
||||
);
|
||||
if (this.peerConn?.iceGatheringState === "complete") {
|
||||
this.queueCandidate(null);
|
||||
this.queueCandidate(null); // We should leave it to WebRTC to announce the end
|
||||
logger.debug(
|
||||
`Call ${this.callId} onIceGatheringStateChange() ice gathering state complete, set candidates have ended`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1899,6 +1902,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
this.isSettingRemoteAnswerPending = true;
|
||||
await this.peerConn!.setRemoteDescription(content.answer);
|
||||
this.isSettingRemoteAnswerPending = false;
|
||||
logger.debug(`Call ${this.callId} onAnswerReceived() set remote description: ${content.answer.type}`);
|
||||
} catch (e) {
|
||||
this.isSettingRemoteAnswerPending = false;
|
||||
logger.debug(`Call ${this.callId} onAnswerReceived() failed to set remote description`, e);
|
||||
@@ -1991,6 +1995,8 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
await this.peerConn!.setRemoteDescription(description); // SRD rolls back as needed
|
||||
this.isSettingRemoteAnswerPending = false;
|
||||
|
||||
logger.debug(`Call ${this.callId} onNegotiateReceived() set remote description: ${description.type}`);
|
||||
|
||||
if (description.type === "offer") {
|
||||
let answer: RTCSessionDescriptionInit;
|
||||
try {
|
||||
@@ -2003,6 +2009,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
}
|
||||
|
||||
await this.peerConn!.setLocalDescription(answer);
|
||||
logger.debug(`Call ${this.callId} onNegotiateReceived() create an answer`);
|
||||
|
||||
this.sendVoipEvent(EventType.CallNegotiate, {
|
||||
description: this.peerConn!.localDescription?.toJSON(),
|
||||
@@ -2023,7 +2030,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
}
|
||||
|
||||
private updateRemoteSDPStreamMetadata(metadata: SDPStreamMetadata): void {
|
||||
this.remoteSDPStreamMetadata = utils.recursivelyAssign(this.remoteSDPStreamMetadata || {}, metadata, true);
|
||||
this.remoteSDPStreamMetadata = recursivelyAssign(this.remoteSDPStreamMetadata || {}, metadata, true);
|
||||
for (const feed of this.getRemoteFeeds()) {
|
||||
const streamId = feed.stream.id;
|
||||
const metadata = this.remoteSDPStreamMetadata![streamId];
|
||||
@@ -2226,7 +2233,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
return; // because ICE can still complete as we're ending the call
|
||||
}
|
||||
logger.debug(
|
||||
`Call ${this.callId} onIceConnectionStateChanged() running (state=${this.peerConn?.iceConnectionState})`,
|
||||
`Call ${this.callId} onIceConnectionStateChanged() running (state=${this.peerConn?.iceConnectionState}, conn=${this.peerConn?.connectionState})`,
|
||||
);
|
||||
|
||||
// ideally we'd consider the call to be connected when we get media but
|
||||
@@ -2234,6 +2241,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
if (["connected", "completed"].includes(this.peerConn?.iceConnectionState ?? "")) {
|
||||
clearTimeout(this.iceDisconnectedTimeout);
|
||||
this.iceDisconnectedTimeout = undefined;
|
||||
if (this.iceReconnectionTimeOut) {
|
||||
clearTimeout(this.iceReconnectionTimeOut);
|
||||
}
|
||||
this.state = CallState.Connected;
|
||||
|
||||
if (!this.callLengthInterval && !this.callStartTime) {
|
||||
@@ -2244,11 +2254,15 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
}, CALL_LENGTH_INTERVAL);
|
||||
}
|
||||
} else if (this.peerConn?.iceConnectionState == "failed") {
|
||||
this.candidatesEnded = false;
|
||||
// Firefox for Android does not yet have support for restartIce()
|
||||
// (the types say it's always defined though, so we have to cast
|
||||
// to prevent typescript from warning).
|
||||
if (this.peerConn?.restartIce as (() => void) | null) {
|
||||
this.candidatesEnded = false;
|
||||
logger.debug(
|
||||
`Call ${this.callId} onIceConnectionStateChanged() ice restart (state=${this.peerConn?.iceConnectionState})`,
|
||||
);
|
||||
this.peerConn!.restartIce();
|
||||
} else {
|
||||
logger.info(
|
||||
@@ -2257,7 +2271,19 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
this.hangup(CallErrorCode.IceFailed, false);
|
||||
}
|
||||
} else if (this.peerConn?.iceConnectionState == "disconnected") {
|
||||
this.iceDisconnectedTimeout = setTimeout(() => {
|
||||
this.candidatesEnded = false;
|
||||
this.iceReconnectionTimeOut = setTimeout((): void => {
|
||||
logger.info(
|
||||
`Call ${this.callId} onIceConnectionStateChanged() ICE restarting because of ICE disconnected, (state=${this.peerConn?.iceConnectionState}, conn=${this.peerConn?.connectionState})`,
|
||||
);
|
||||
if (this.peerConn?.restartIce as (() => void) | null) {
|
||||
this.candidatesEnded = false;
|
||||
this.peerConn!.restartIce();
|
||||
}
|
||||
this.iceReconnectionTimeOut = undefined;
|
||||
}, ICE_RECONNECTING_TIMEOUT);
|
||||
|
||||
this.iceDisconnectedTimeout = setTimeout((): void => {
|
||||
logger.info(
|
||||
`Call ${this.callId} onIceConnectionStateChanged() hanging up call (ICE disconnected for too long)`,
|
||||
);
|
||||
@@ -2887,6 +2913,11 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
} catch (err) {
|
||||
if (!this.ignoreOffer) {
|
||||
logger.info(`Call ${this.callId} addIceCandidates() failed to add remote ICE candidate`, err);
|
||||
} else {
|
||||
logger.debug(
|
||||
`Call ${this.callId} addIceCandidates() failed to add remote ICE candidate because ignoring offer`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,13 +128,15 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
|
||||
this.emit(CallFeedEvent.ConnectedChanged, this.connected);
|
||||
}
|
||||
|
||||
private get hasAudioTrack(): boolean {
|
||||
public get hasAudioTrack(): boolean {
|
||||
return this.stream.getAudioTracks().length > 0;
|
||||
}
|
||||
|
||||
private updateStream(oldStream: MediaStream | null, newStream: MediaStream): void {
|
||||
if (newStream === oldStream) return;
|
||||
|
||||
const wasMeasuringVolumeActivity = this.measuringVolumeActivity;
|
||||
|
||||
if (oldStream) {
|
||||
oldStream.removeEventListener("addtrack", this.onAddTrack);
|
||||
this.measureVolumeActivity(false);
|
||||
@@ -145,6 +147,7 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
|
||||
|
||||
if (this.hasAudioTrack) {
|
||||
this.initVolumeMeasuring();
|
||||
if (wasMeasuringVolumeActivity) this.measureVolumeActivity(true);
|
||||
} else {
|
||||
this.measureVolumeActivity(false);
|
||||
}
|
||||
|
||||
+39
-21
@@ -655,27 +655,9 @@ export class GroupCall extends TypedEventEmitter<
|
||||
`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}`,
|
||||
);
|
||||
const hasPermission = await this.checkAudioPermissionIfNecessary(muted);
|
||||
|
||||
if (!hasPermission) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -700,6 +682,42 @@ export class GroupCall extends TypedEventEmitter<
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If we allow entering a call without a camera and without video, it can happen that the access rights to the
|
||||
* devices have not yet been queried. If a stream does not yet have an audio track, we assume that the rights have
|
||||
* not yet been checked.
|
||||
*
|
||||
* `this.client.getMediaHandler().getUserMediaStream` clones the current stream, so it only wanted to be called when
|
||||
* not Audio Track exists.
|
||||
* As such, this is a compromise, because, the access rights should always be queried before the call.
|
||||
*/
|
||||
private async checkAudioPermissionIfNecessary(muted: boolean): Promise<boolean> {
|
||||
// We needed this here to avoid an error in case user join a call without a device.
|
||||
try {
|
||||
if (!muted && this.localCallFeed && !this.localCallFeed.hasAudioTrack) {
|
||||
const stream = await this.client
|
||||
.getMediaHandler()
|
||||
.getUserMediaStream(true, !this.localCallFeed.isVideoMuted());
|
||||
if (stream?.getTracks().length === 0) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mute state of the local participants's video.
|
||||
* @param muted - Whether to mute the video
|
||||
|
||||
@@ -33,7 +33,7 @@ export interface ConnectionStatsBitrate extends Bitrate {
|
||||
video?: Bitrate;
|
||||
}
|
||||
|
||||
export interface PacketLoos {
|
||||
export interface PacketLoss {
|
||||
total: number;
|
||||
download: number;
|
||||
upload: number;
|
||||
@@ -42,6 +42,6 @@ export interface PacketLoos {
|
||||
export class ConnectionStats {
|
||||
public bandwidth: ConnectionStatsBitrate = {} as ConnectionStatsBitrate;
|
||||
public bitrate: ConnectionStatsBitrate = {} as ConnectionStatsBitrate;
|
||||
public packetLoss: PacketLoos = {} as PacketLoos;
|
||||
public packetLoss: PacketLoss = {} as PacketLoss;
|
||||
public transport: TransportStats[] = [];
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { AudioConcealment } from "../statsReport";
|
||||
import { TrackId } from "./mediaTrackHandler";
|
||||
|
||||
export interface PacketLoss {
|
||||
@@ -32,7 +33,14 @@ export interface Bitrate {
|
||||
*/
|
||||
upload: number;
|
||||
}
|
||||
export interface ConcealedAudio {
|
||||
/**
|
||||
* duration in ms
|
||||
*/
|
||||
duration: number;
|
||||
|
||||
ratio: number;
|
||||
}
|
||||
export interface Resolution {
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -44,6 +52,7 @@ export class MediaTrackStats {
|
||||
private loss: PacketLoss = { packetsTotal: 0, packetsLost: 0, isDownloadStream: false };
|
||||
private bitrate: Bitrate = { download: 0, upload: 0 };
|
||||
private resolution: Resolution = { width: -1, height: -1 };
|
||||
private audioConcealment: AudioConcealment = { concealedAudio: 0, totalAudioDuration: 0 };
|
||||
private framerate = 0;
|
||||
private jitter = 0;
|
||||
private codec = "";
|
||||
@@ -61,8 +70,8 @@ export class MediaTrackStats {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public setLoss(loos: PacketLoss): void {
|
||||
this.loss = loos;
|
||||
public setLoss(loss: PacketLoss): void {
|
||||
this.loss = loss;
|
||||
}
|
||||
|
||||
public getLoss(): PacketLoss {
|
||||
@@ -152,4 +161,16 @@ export class MediaTrackStats {
|
||||
public getJitter(): number {
|
||||
return this.jitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio concealment ration (conceled duration / total duration)
|
||||
*/
|
||||
public setAudioConcealment(concealedAudioDuration: number, totalAudioDuration: number): void {
|
||||
this.audioConcealment.concealedAudio = concealedAudioDuration;
|
||||
this.audioConcealment.totalAudioDuration = totalAudioDuration;
|
||||
}
|
||||
|
||||
public getAudioConcealment(): AudioConcealment {
|
||||
return this.audioConcealment;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConnectionStatsBandwidth, ConnectionStatsBitrate, PacketLoos } from "./connectionStats";
|
||||
import { ConnectionStatsBandwidth, ConnectionStatsBitrate, PacketLoss } from "./connectionStats";
|
||||
import { TransportStats } from "./transportStats";
|
||||
import { Resolution } from "./media/mediaTrackStats";
|
||||
|
||||
@@ -34,7 +34,9 @@ export interface ByteSentStatsReport extends Map<TrackID, ByteSend> {
|
||||
export interface ConnectionStatsReport {
|
||||
bandwidth: ConnectionStatsBandwidth;
|
||||
bitrate: ConnectionStatsBitrate;
|
||||
packetLoss: PacketLoos;
|
||||
packetLoss: PacketLoss;
|
||||
audioConcealment: Map<TrackID, AudioConcealment>;
|
||||
totalAudioConcealment: AudioConcealment;
|
||||
resolution: ResolutionMap;
|
||||
framerate: FramerateMap;
|
||||
codec: CodecMap;
|
||||
@@ -42,6 +44,11 @@ export interface ConnectionStatsReport {
|
||||
transport: TransportStats[];
|
||||
}
|
||||
|
||||
export interface AudioConcealment {
|
||||
concealedAudio: number;
|
||||
totalAudioDuration: number;
|
||||
}
|
||||
|
||||
export interface ResolutionMap {
|
||||
local: Map<TrackID, Resolution>;
|
||||
remote: Map<TrackID, Resolution>;
|
||||
@@ -70,4 +77,6 @@ export interface SummaryStatsReport {
|
||||
percentageReceivedVideoMedia: number;
|
||||
maxJitter: number;
|
||||
maxPacketLoss: number;
|
||||
percentageConcealedAudio: number;
|
||||
peerConnections: number;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ 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.
|
||||
*/
|
||||
import { CodecMap, ConnectionStatsReport, FramerateMap, ResolutionMap, TrackID } from "./statsReport";
|
||||
import { AudioConcealment, CodecMap, ConnectionStatsReport, FramerateMap, ResolutionMap, TrackID } from "./statsReport";
|
||||
import { MediaTrackStats, Resolution } from "./media/mediaTrackStats";
|
||||
|
||||
export class StatsReportBuilder {
|
||||
@@ -38,12 +38,16 @@ export class StatsReportBuilder {
|
||||
const framerates: FramerateMap = { local: new Map<TrackID, number>(), remote: new Map<TrackID, number>() };
|
||||
const codecs: CodecMap = { local: new Map<TrackID, string>(), remote: new Map<TrackID, string>() };
|
||||
const jitter = new Map<TrackID, number>();
|
||||
const audioConcealment = new Map<TrackID, AudioConcealment>();
|
||||
|
||||
let audioBitrateDownload = 0;
|
||||
let audioBitrateUpload = 0;
|
||||
let videoBitrateDownload = 0;
|
||||
let videoBitrateUpload = 0;
|
||||
|
||||
let totalConcealedAudio = 0;
|
||||
let totalAudioDuration = 0;
|
||||
|
||||
for (const [trackId, trackStats] of stats) {
|
||||
// process packet loss stats
|
||||
const loss = trackStats.getLoss();
|
||||
@@ -58,6 +62,11 @@ export class StatsReportBuilder {
|
||||
|
||||
// collect resolutions and framerates
|
||||
if (trackStats.kind === "audio") {
|
||||
// process audio quality stats
|
||||
const audioConcealmentForTrack = trackStats.getAudioConcealment();
|
||||
totalConcealedAudio += audioConcealmentForTrack.concealedAudio;
|
||||
totalAudioDuration += audioConcealmentForTrack.totalAudioDuration;
|
||||
|
||||
audioBitrateDownload += trackStats.getBitrate().download;
|
||||
audioBitrateUpload += trackStats.getBitrate().upload;
|
||||
} else {
|
||||
@@ -70,6 +79,9 @@ export class StatsReportBuilder {
|
||||
codecs[trackStats.getType()].set(trackId, trackStats.getCodec());
|
||||
if (trackStats.getType() === "remote") {
|
||||
jitter.set(trackId, trackStats.getJitter());
|
||||
if (trackStats.kind === "audio") {
|
||||
audioConcealment.set(trackId, trackStats.getAudioConcealment());
|
||||
}
|
||||
}
|
||||
|
||||
trackStats.resetBitrate();
|
||||
@@ -98,6 +110,12 @@ export class StatsReportBuilder {
|
||||
download: StatsReportBuilder.calculatePacketLoss(lostPackets.download, totalPackets.download),
|
||||
upload: StatsReportBuilder.calculatePacketLoss(lostPackets.upload, totalPackets.upload),
|
||||
};
|
||||
report.audioConcealment = audioConcealment;
|
||||
report.totalAudioConcealment = {
|
||||
concealedAudio: totalConcealedAudio,
|
||||
totalAudioDuration,
|
||||
};
|
||||
|
||||
report.framerate = framerates;
|
||||
report.resolution = resolutions;
|
||||
report.codec = codecs;
|
||||
|
||||
@@ -53,8 +53,8 @@ export class StatsReportGatherer {
|
||||
receivedMedia: 0,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: { count: 0, muted: 0, maxPacketLoss: 0, maxJitter: 0 },
|
||||
videoTrackSummary: { count: 0, muted: 0, maxPacketLoss: 0, maxJitter: 0 },
|
||||
audioTrackSummary: { count: 0, muted: 0, maxPacketLoss: 0, maxJitter: 0, concealedAudio: 0, totalAudio: 0 },
|
||||
videoTrackSummary: { count: 0, muted: 0, maxPacketLoss: 0, maxJitter: 0, concealedAudio: 0, totalAudio: 0 },
|
||||
} as SummaryStats;
|
||||
if (this.isActive) {
|
||||
const statsPromise = this.pc.getStats();
|
||||
@@ -138,6 +138,7 @@ export class StatsReportGatherer {
|
||||
const ts = this.trackStats.findTransceiverByTrackId(trackStats.trackId);
|
||||
TrackStatsReporter.setTrackStatsState(trackStats, ts);
|
||||
TrackStatsReporter.buildJitter(trackStats, now);
|
||||
TrackStatsReporter.buildAudioConcealment(trackStats, now);
|
||||
} else if (before) {
|
||||
byteSentStats.set(trackStats.trackId, StatsValueFormatter.getNonNegativeValue(now.bytesSent));
|
||||
TrackStatsReporter.buildBitrateSend(trackStats, now, before);
|
||||
|
||||
@@ -23,4 +23,6 @@ export interface TrackSummary {
|
||||
muted: number;
|
||||
maxJitter: number;
|
||||
maxPacketLoss: number;
|
||||
concealedAudio: number;
|
||||
totalAudio: number;
|
||||
}
|
||||
|
||||
@@ -14,59 +14,77 @@ import { StatsReportEmitter } from "./statsReportEmitter";
|
||||
import { SummaryStats } from "./summaryStats";
|
||||
import { SummaryStatsReport } from "./statsReport";
|
||||
|
||||
interface ReceivedMedia {
|
||||
audio: number;
|
||||
video: number;
|
||||
media: number;
|
||||
interface SummaryCounter {
|
||||
receivedAudio: number;
|
||||
receivedVideo: number;
|
||||
receivedMedia: number;
|
||||
concealedAudio: number;
|
||||
totalAudio: number;
|
||||
}
|
||||
|
||||
export class SummaryStatsReporter {
|
||||
public constructor(private emitter: StatsReportEmitter) {}
|
||||
|
||||
public build(summary: SummaryStats[]): void {
|
||||
const entiretyTracksCount = summary.length;
|
||||
if (entiretyTracksCount === 0) {
|
||||
const summaryTotalCount = summary.length;
|
||||
if (summaryTotalCount === 0) {
|
||||
return;
|
||||
}
|
||||
const receivedCounter: ReceivedMedia = { audio: 0, video: 0, media: 0 };
|
||||
const summaryCounter: SummaryCounter = {
|
||||
receivedAudio: 0,
|
||||
receivedVideo: 0,
|
||||
receivedMedia: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
};
|
||||
let maxJitter = 0;
|
||||
let maxPacketLoss = 0;
|
||||
|
||||
summary.forEach((stats) => {
|
||||
this.countTrackListReceivedMedia(receivedCounter, stats);
|
||||
this.countTrackListReceivedMedia(summaryCounter, stats);
|
||||
this.countConcealedAudio(summaryCounter, stats);
|
||||
maxJitter = this.buildMaxJitter(maxJitter, stats);
|
||||
maxPacketLoss = this.buildMaxPacketLoss(maxPacketLoss, stats);
|
||||
});
|
||||
|
||||
const decimalPlaces = 5;
|
||||
const report = {
|
||||
percentageReceivedMedia: Math.round((receivedCounter.media / entiretyTracksCount) * 100) / 100,
|
||||
percentageReceivedVideoMedia: Math.round((receivedCounter.video / entiretyTracksCount) * 100) / 100,
|
||||
percentageReceivedAudioMedia: Math.round((receivedCounter.audio / entiretyTracksCount) * 100) / 100,
|
||||
percentageReceivedMedia: Number((summaryCounter.receivedMedia / summaryTotalCount).toFixed(decimalPlaces)),
|
||||
percentageReceivedVideoMedia: Number(
|
||||
(summaryCounter.receivedVideo / summaryTotalCount).toFixed(decimalPlaces),
|
||||
),
|
||||
percentageReceivedAudioMedia: Number(
|
||||
(summaryCounter.receivedAudio / summaryTotalCount).toFixed(decimalPlaces),
|
||||
),
|
||||
maxJitter,
|
||||
maxPacketLoss,
|
||||
percentageConcealedAudio: Number(
|
||||
summaryCounter.totalAudio > 0
|
||||
? (summaryCounter.concealedAudio / summaryCounter.totalAudio).toFixed(decimalPlaces)
|
||||
: 0,
|
||||
),
|
||||
peerConnections: summaryTotalCount,
|
||||
} as SummaryStatsReport;
|
||||
this.emitter.emitSummaryStatsReport(report);
|
||||
}
|
||||
|
||||
private countTrackListReceivedMedia(counter: ReceivedMedia, stats: SummaryStats): void {
|
||||
private countTrackListReceivedMedia(counter: SummaryCounter, stats: SummaryStats): void {
|
||||
let hasReceivedAudio = false;
|
||||
let hasReceivedVideo = false;
|
||||
if (stats.receivedAudioMedia > 0 || stats.audioTrackSummary.count === 0) {
|
||||
counter.audio++;
|
||||
counter.receivedAudio++;
|
||||
hasReceivedAudio = true;
|
||||
}
|
||||
if (stats.receivedVideoMedia > 0 || stats.videoTrackSummary.count === 0) {
|
||||
counter.video++;
|
||||
counter.receivedVideo++;
|
||||
hasReceivedVideo = true;
|
||||
} else {
|
||||
if (stats.videoTrackSummary.muted > 0 && stats.videoTrackSummary.muted === stats.videoTrackSummary.count) {
|
||||
counter.video++;
|
||||
counter.receivedVideo++;
|
||||
hasReceivedVideo = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasReceivedVideo && hasReceivedAudio) {
|
||||
counter.media++;
|
||||
counter.receivedMedia++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,4 +109,9 @@ export class SummaryStatsReporter {
|
||||
}
|
||||
return maxPacketLoss;
|
||||
}
|
||||
|
||||
private countConcealedAudio(summaryCounter: SummaryCounter, stats: SummaryStats): void {
|
||||
summaryCounter.concealedAudio += stats.audioTrackSummary.concealedAudio;
|
||||
summaryCounter.totalAudio += stats.audioTrackSummary.totalAudio;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,23 +140,44 @@ export class TrackStatsReporter {
|
||||
audioTrackSummary: TrackSummary;
|
||||
videoTrackSummary: TrackSummary;
|
||||
} {
|
||||
const audioTrackSummary = { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 };
|
||||
const videoTrackSummary = { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 };
|
||||
trackStatsList
|
||||
.filter((t) => t.getType() === "remote")
|
||||
.forEach((stats) => {
|
||||
const trackSummary = stats.kind === "video" ? videoTrackSummary : audioTrackSummary;
|
||||
trackSummary.count++;
|
||||
if (stats.alive && stats.muted) {
|
||||
trackSummary.muted++;
|
||||
}
|
||||
if (trackSummary.maxJitter < stats.getJitter()) {
|
||||
trackSummary.maxJitter = stats.getJitter();
|
||||
}
|
||||
if (trackSummary.maxPacketLoss < stats.getLoss().packetsLost) {
|
||||
trackSummary.maxPacketLoss = stats.getLoss().packetsLost;
|
||||
}
|
||||
});
|
||||
const videoTrackSummary: TrackSummary = {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
};
|
||||
const audioTrackSummary: TrackSummary = {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
};
|
||||
|
||||
const remoteTrackList = trackStatsList.filter((t) => t.getType() === "remote");
|
||||
const audioTrackList = remoteTrackList.filter((t) => t.kind === "audio");
|
||||
|
||||
remoteTrackList.forEach((stats) => {
|
||||
const trackSummary = stats.kind === "video" ? videoTrackSummary : audioTrackSummary;
|
||||
trackSummary.count++;
|
||||
if (stats.alive && stats.muted) {
|
||||
trackSummary.muted++;
|
||||
}
|
||||
if (trackSummary.maxJitter < stats.getJitter()) {
|
||||
trackSummary.maxJitter = stats.getJitter();
|
||||
}
|
||||
if (trackSummary.maxPacketLoss < stats.getLoss().packetsLost) {
|
||||
trackSummary.maxPacketLoss = stats.getLoss().packetsLost;
|
||||
}
|
||||
if (audioTrackList.length > 0) {
|
||||
trackSummary.concealedAudio += stats.getAudioConcealment()?.concealedAudio;
|
||||
trackSummary.totalAudio += stats.getAudioConcealment()?.totalAudioDuration;
|
||||
}
|
||||
});
|
||||
|
||||
return { audioTrackSummary, videoTrackSummary };
|
||||
}
|
||||
|
||||
@@ -173,4 +194,14 @@ export class TrackStatsReporter {
|
||||
trackStats.setJitter(-1);
|
||||
}
|
||||
}
|
||||
|
||||
public static buildAudioConcealment(trackStats: MediaTrackStats, statsReport: any): void {
|
||||
if (statsReport.type !== "inbound-rtp") {
|
||||
return;
|
||||
}
|
||||
const msPerSample = (1000 * statsReport?.totalSamplesDuration) / statsReport?.totalSamplesReceived;
|
||||
const concealedAudioDuration = msPerSample * statsReport?.concealedSamples;
|
||||
const totalAudioDuration = 1000 * statsReport?.totalSamplesDuration;
|
||||
trackStats.setAudioConcealment(concealedAudioDuration, totalAudioDuration);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"noEmit": false,
|
||||
|
||||
Reference in New Issue
Block a user