Compare commits

..

48 Commits

Author SHA1 Message Date
ElementRobot 2ec1fa6605 v25.2.0-rc.4 2023-05-16 14:22:04 +01:00
ElementRobot f15d682938 Prepare changelog for v25.2.0-rc.4 2023-05-16 14:22:02 +01:00
ElementRobot 21a10a2d14 v25.2.0-rc.3 2023-05-16 14:17:04 +01:00
ElementRobot fc02e550bd Prepare changelog for v25.2.0-rc.3 2023-05-16 14:17:02 +01:00
Michael Telatynski 78637a0689 Fix docs deployment 2023-05-16 14:15:27 +01:00
ElementRobot 4ca882fcd4 v25.2.0-rc.2 2023-05-16 13:58:35 +01:00
ElementRobot 13ee0eb7f5 Prepare changelog for v25.2.0-rc.2 2023-05-16 13:58:33 +01:00
ElementRobot cb018dfc80 Fix tsconfig-build.json 2023-05-16 13:58:05 +01:00
ElementRobot 7574dacdb3 v25.2.0-rc.1 2023-05-16 13:37:19 +01:00
ElementRobot 0c417b7c32 Prepare changelog for v25.2.0-rc.1 2023-05-16 13:37:17 +01:00
ElementRobot daf845d7bd Fix changelog_head.py script to be Python 3 compatible 2023-05-16 13:36:40 +01:00
Michael Telatynski 52792ec89b Fix CI failure on develop due to force merged PR and prettier failure (#3369) 2023-05-16 11:52:58 +00:00
Matthew Hodgson 6dc4a62e8c Merge pull request #3367 from matrix-org/matthew/fix-accumulated-sync-summaries
fix accumulated sync summaries - andy review
2023-05-16 12:34:54 +01:00
Matthew Hodgson 1cd8ea61ea merge 2023-05-16 12:08:02 +01:00
Matthew Hodgson 0c5eb277e4 incorporate andy review 2023-05-16 12:05:56 +01:00
Matthew Hodgson d459a91af3 correctly accumulate sync summaries. (#3366)
if a sync summary for (say) invited_member_count goes from 1 to 0, it should be
accumluated as 0, rather than 1.

Should fix https://github.com/vector-im/element-web/issues/23345
2023-05-16 10:41:35 +00:00
Matthew Hodgson 18722d0031 correctly accumulate sync summaries.
if a sync summary for (say) invited_member_count goes from 1 to 0, it should be
accumluated as 0, rather than 1.

Should fix https://github.com/vector-im/element-web/issues/23345
2023-05-16 11:34:06 +01:00
RiotRobot 5119934268 Merge branch 'master' into develop 2023-05-16 09:12:28 +01:00
Richard van der Hoff 4316009401 Element-R: support for SigningKeysUploadRequest (#3365)
* OutgoingRequestProcessor: support for SigningKeysUploadRequest

* Tests

* Bump matrix-org/matrix-sdk-crypto-js

... to pick up bug fixes for outgoing requests
2023-05-15 19:14:05 +00:00
Richard van der Hoff 72f3c360b6 Add CryptoApi.getCrossSigningKeyId (#3360) 2023-05-15 18:46:33 +01:00
Enrico Schwendig fcbc195fbe Check permission on mute mic, only if no audio track exists. (#3359)
* check permission only if no audio track

* fix linter issues

* add missing tests for perfect negotiation pattern

* add null case in unit tests for audio muting

* fix issue with type MediaStream

* force right type of mock methode

* format code
2023-05-15 14:38:43 +00:00
Richard van der Hoff af38021d28 Deprecate device methods in MatrixClient (#3357) 2023-05-15 10:37:35 +00:00
Michael Telatynski 5e8cb9fa18 Fix typedoc release documentation deployment (#3358)
* Prune typedoc docs before generating new ones

* Only maintain 10 major versions

* Switch to deploy mechanism which doesn't mangle symlinks

* Convert absolute symlinks to relative
2023-05-15 09:14:35 +00:00
Michael Telatynski 6ef9f6c55e Enable better tree shaking (#3356) 2023-05-15 08:10:44 +00:00
Timo e6a3b0ebc0 Total summary count (#3351)
* add audio concealment to stats report

* audio concealment to summary

* make ts linter happy

* format and rename

* fix and add tests

* make it prettier!

* we can make it even prettier ?!

* review

* fix tests

* pretty

* one empty line to ...

* remove ratio in audio concealment (ratio is now done in the summary)

* remove comment

* fix test

* add peer connections to summary report

* tests
2023-05-14 16:58:38 +00:00
Robin aaae55736f Keep measuring a call feed's volume after a stream replacement (#3361) 2023-05-13 17:54:29 +00:00
Timo 9e586ab634 Audio concealment (#3349)
* add audio concealment to stats report

* audio concealment to summary

* make ts linter happy

* format and rename

* fix and add tests

* make it prettier!

* we can make it even prettier ?!

* review

* fix tests

* pretty

* one empty line to ...

* remove ratio in audio concealment (ratio is now done in the summary)

* remove comment

* fix test
2023-05-12 16:24:27 +00:00
Richard van der Hoff 7ff44d4a50 Integration test for bootstrapCrossSigning (#3355)
* Stub implementation of bootstrapCrossSigning

* Integration test for `bootstrapCrossSigning`
2023-05-12 16:19:18 +00:00
Richard van der Hoff 63abd00ca7 Element-R: Stub out isCrossSigningReady and isSecretStorageReady (#3354)
* Stub implementation of `isCrossSigningReady`

* Stub implementation of `isSecretStorageReady`

* add tests to meet quality gate

* factor out common

* Remove accidentally-added file
2023-05-12 12:21:52 +00:00
Richard van der Hoff 40f2579158 Pass SecretStorage into RustCrypto (#3353)
* Pass SecretStorage into RustCrypto

* Update src/rust-crypto/rust-crypto.ts
2023-05-12 09:38:33 +00:00
Richard van der Hoff ceb2a57feb Rename and move crypto.IBootstrapCrossSigningOpts (#3352)
* Define `UIAuthCallback` type and use in `IBootstrapCrossSigningOpts`

* Move `IBootstrapCrossSigningOpts` to `crypto-api` and rename

* Replace uses of `IBootstrapCrossSigningOpts`

... with `BootstrapCrossSigningOpts`

* Update src/crypto-api.ts
2023-05-11 18:41:58 +00:00
Enrico Schwendig 90e8336797 Do an ICE Restart if WebRTC become disconnected (#3341)
* Do an ice restart if ICE disconnected

  - Waite two seconds after disconnected
  - Remove check for finish ICE gathering and try to add each local candidate. Avoid race in multible ICE gathering

* Add tests for failed iceConnectionState

* suppress type check in unit test

* fix pr issues
2023-05-11 14:00:26 +00:00
Andy Balaam 73ca9c9ed2 Revert "Update JS-DevTools/npm-publish action to v2 (#3336)" (#3350)
Attempting to fix failure to publish to NPM due to an "invalid token"
error https://github.com/matrix-org/matrix-js-sdk/actions/runs/4926808655/jobs/8819822367 .

This reverts commit ca9263fa64.
2023-05-10 10:32:22 +00:00
RiotRobot cc065c2772 Resetting package fields for development 2023-05-09 15:14:29 +01:00
RiotRobot 028be0fee2 Merge branch 'master' into develop 2023-05-09 15:14:24 +01:00
Michael Telatynski 49696cecbd Use WeakMap in ReEmitter to help enable garbage collection (#3344) 2023-05-09 13:24:34 +00:00
renovate[bot] 83cb52c89c Lock file maintenance (#3312)
* Lock file maintenance

* Fix types

* Iterate

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-05-09 13:11:30 +00:00
renovate[bot] e82bae2c4d Update dependency @types/node to v18.16.6 (#3347)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-09 10:52:45 +00:00
renovate[bot] ee2b0204aa Update dependency @types/node to v18.16.4 (#3335)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-09 04:58:36 +00:00
renovate[bot] 1ec7670f6a Update babel monorepo (#3332)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-08 02:47:30 +00:00
Michael Telatynski 8ab2e10471 Update types to match spec (#3330) 2023-05-05 08:13:07 +00:00
renovate[bot] 8ff8685ae5 Update dependency rimraf to v5 (#3298)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-04 21:43:38 +00:00
Florian Duros f3772cdf82 Avoid upload a new fallback key at every /sync (#3338) 2023-05-03 13:10:02 +00:00
Andy Balaam ee2f1cdfd4 Accumulate receipts for the main thread and unthreaded separately. (#3339)
Fixes matrix-org/element-web#24629
2023-05-02 15:57:53 +00:00
Florian Duros 0de73a0b3e Update @matrix-org/matrix-sdk-crypto-js to ^0.1.0-alpha.8 (#3337) 2023-05-02 15:53:36 +00:00
renovate[bot] be742e811c Update dependency @types/jest to v29.5.1 (#3333)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-02 13:58:33 +00:00
renovate[bot] ca9263fa64 Update JS-DevTools/npm-publish action to v2 (#3336)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-02 13:56:42 +00:00
renovate[bot] 9f619be08d Update peter-evans/create-pull-request digest to 284f54f (#3331)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-02 13:40:31 +00:00
58 changed files with 1985 additions and 722 deletions
+17 -7
View File
@@ -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
+1 -1
View File
@@ -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
+16
View File
@@ -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
View File
@@ -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": {
+1 -1
View File
@@ -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)
+117
View File
@@ -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}]`,
);
});
});
});
+1 -1
View File
@@ -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(() => {
+2
View File
@@ -239,6 +239,8 @@ export class MockRTCPeerConnection {
public triggerIncomingDataChannel(): void {
this.onDataChannelListener?.({ channel: {} } as RTCDataChannelEvent);
}
public restartIce(): void {}
}
export class MockRTCRtpSender {
+3 -2
View File
@@ -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({});
};
+55
View File
@@ -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 = {
+44
View File
@@ -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();
+56 -13
View File
@@ -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);
}
+19
View File
@@ -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;
+128 -1
View File
@@ -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();
});
});
});
+30 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+77
View File
@@ -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 {
+3 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+3 -3
View File
@@ -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;
}
}
+3 -3
View File
@@ -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
View File
@@ -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;
+11
View File
@@ -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
+2 -2
View File
@@ -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;
+7 -7
View File
@@ -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);
}
+6 -6
View File
@@ -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
View File
@@ -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);
+2 -2
View File
@@ -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);
+37 -1
View File
@@ -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.
+12 -1
View File
@@ -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),
);
+47 -2
View File
@@ -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
View File
@@ -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 {
+3 -3
View File
@@ -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);
});
+10 -10
View File
@@ -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) => {
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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,
);
}
}
}
+4 -1
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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[] = [];
}
+23 -2
View File
@@ -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;
}
}
+11 -2
View File
@@ -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;
}
+19 -1
View File
@@ -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;
+3 -2
View File
@@ -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);
+2
View File
@@ -23,4 +23,6 @@ export interface TrackSummary {
muted: number;
maxJitter: number;
maxPacketLoss: number;
concealedAudio: number;
totalAudio: number;
}
+41 -18
View File
@@ -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;
}
}
+48 -17
View File
@@ -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
View File
@@ -1,6 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"forceConsistentCasingInFileNames": true,
"declarationMap": true,
"sourceMap": true,
"noEmit": false,
+607 -457
View File
File diff suppressed because it is too large Load Diff