Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4500da59a | |||
| b6aef6772e | |||
| 7792254c12 | |||
| fff41f1f27 | |||
| 3e0f9f582e | |||
| 47ba8cfa24 | |||
| 54bb4c8011 | |||
| 71e763263e | |||
| 2ebcda2a55 | |||
| e10db6f7e9 | |||
| 1e041a2957 | |||
| 1631d6f3c0 | |||
| 3d86821258 | |||
| 261bc81554 | |||
| 8f701f43fb | |||
| 4bdb9111dd | |||
| 93e2135223 | |||
| 56cb05aac0 | |||
| 38d5b202c9 | |||
| cfffa9c518 | |||
| 73dbd709d8 | |||
| eef67e2c03 | |||
| e3498f0668 | |||
| f04c147faa | |||
| c2afc357b9 | |||
| 1d3f67f2ce | |||
| 514e4f07f1 | |||
| fbb1c4b2bd | |||
| 63dea599b1 | |||
| 743ba5f050 | |||
| cb180b4195 | |||
| c805b7e29d | |||
| 8f6814450b | |||
| 37e8391cde | |||
| 90234402a7 | |||
| 8ecf603d73 | |||
| af243581ff | |||
| 01afed9ff9 | |||
| 2687bb37fb | |||
| 65b1a10803 | |||
| 9d230ef0d6 | |||
| a03438f2af | |||
| 8c30a3b0df | |||
| c61d53eed0 |
@@ -14,7 +14,7 @@ jobs:
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@5e780fc7bbd0cac69fc73271ed86edf5dcb72d67 # v2
|
||||
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2
|
||||
with:
|
||||
workflow: static_analysis.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -11,6 +11,12 @@ jobs:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: gh-pages
|
||||
path: _docs
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
@@ -22,35 +28,12 @@ jobs:
|
||||
- name: 📖 Generate JSDoc
|
||||
run: "yarn gendoc"
|
||||
|
||||
- name: 📋 Copy to temp
|
||||
run: |
|
||||
cp -a "./_docs" "$RUNNER_TEMP/"
|
||||
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: gh-pages
|
||||
|
||||
- name: 🔪 Prepare
|
||||
env:
|
||||
GITHUB_REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
[ ! -e "$VERSION" ] || rm -r $VERSION
|
||||
cp -r $RUNNER_TEMP/_docs/ $VERSION
|
||||
|
||||
# Add the new directory to the index if it isn't there already
|
||||
if ! grep -q ">Version $VERSION</a>" index.html; then
|
||||
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
|
||||
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' "$VERSION" index.html
|
||||
fi
|
||||
|
||||
- name: 🚀 Deploy
|
||||
uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 # v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
keep_files: true
|
||||
publish_dir: .
|
||||
publish_dir: _docs
|
||||
|
||||
npm:
|
||||
name: Publish
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.3
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.5
|
||||
# workflow_run fails report against the develop commit always, we don't want that for PRs
|
||||
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
|
||||
with:
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@5e780fc7bbd0cac69fc73271ed86edf5dcb72d67 # v2
|
||||
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2
|
||||
with:
|
||||
workflow: tests.yaml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -67,6 +67,13 @@ jobs:
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc --treatWarningsAsErrors"
|
||||
|
||||
# Upload artifact duplicates symlink contents so we do this to save 75% space
|
||||
- name: Flatten symlink and write _redirects
|
||||
run: |
|
||||
find _docs -mindepth 1 -maxdepth 1 ! -type f ! -name stable -printf '/%f/* /stable/:splat\n' > _docs/_redirects
|
||||
find _docs -mindepth 1 -maxdepth 1 -type l -delete
|
||||
find _docs -mindepth 1 -maxdepth 1 -type d -execdir mv {} stable \; -quit
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54 # v4
|
||||
uses: peter-evans/create-pull-request@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5 # v5
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/upgrade-deps
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
Changes in [25.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.1.0) (2023-05-09)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Deprecate MatrixClient::resolveRoomAlias ([\#3316](https://github.com/matrix-org/matrix-js-sdk/pull/3316)).
|
||||
|
||||
## ✨ Features
|
||||
* add client method to remove pusher ([\#3324](https://github.com/matrix-org/matrix-js-sdk/pull/3324)). Contributed by @kerryarchibald.
|
||||
* Implement MSC 3981 ([\#3248](https://github.com/matrix-org/matrix-js-sdk/pull/3248)). Fixes vector-im/element-web#25021. Contributed by @justjanne.
|
||||
* Added `Room.getLastLiveEvent` and `Room.getLastThread`. Deprecated `Room.lastThread` in favour of `Room.getLastThread`. ([\#3321](https://github.com/matrix-org/matrix-js-sdk/pull/3321)).
|
||||
* Element-R: wire up device lists ([\#3272](https://github.com/matrix-org/matrix-js-sdk/pull/3272)). Contributed by @florianduros.
|
||||
* Node 20 support ([\#3302](https://github.com/matrix-org/matrix-js-sdk/pull/3302)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix racing between one-time-keys processing and sync ([\#3327](https://github.com/matrix-org/matrix-js-sdk/pull/3327)). Fixes vector-im/element-web#25214. Contributed by @florianduros.
|
||||
* Fix lack of media when a user reconnects ([\#3318](https://github.com/matrix-org/matrix-js-sdk/pull/3318)).
|
||||
* Fix TimelineWindow getEvents exploding if no neigbouring timeline ([\#3285](https://github.com/matrix-org/matrix-js-sdk/pull/3285)). Fixes vector-im/element-web#25104.
|
||||
|
||||
Changes in [25.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.0.0) (2023-04-25)
|
||||
==================================================================================================
|
||||
|
||||
|
||||
+8
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "25.0.0",
|
||||
"version": "25.1.0",
|
||||
"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.6",
|
||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.7",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
@@ -101,13 +101,13 @@
|
||||
"debug": "^4.3.4",
|
||||
"docdash": "^2.0.0",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.37.0",
|
||||
"eslint": "8.39.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "^27.1.6",
|
||||
"eslint-plugin-jsdoc": "^40.0.0",
|
||||
"eslint-plugin-jsdoc": "^43.0.6",
|
||||
"eslint-plugin-matrix-org": "^1.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.17",
|
||||
"eslint-plugin-unicorn": "^46.0.0",
|
||||
@@ -119,14 +119,15 @@
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^29.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"prettier": "2.8.7",
|
||||
"prettier": "2.8.8",
|
||||
"rimraf": "^4.0.0",
|
||||
"terser": "^5.5.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typedoc": "^0.23.20",
|
||||
"typedoc": "^0.24.0",
|
||||
"typedoc-plugin-mdn-links": "^3.0.3",
|
||||
"typedoc-plugin-missing-exports": "^1.0.0",
|
||||
"typedoc-plugin-missing-exports": "^2.0.0",
|
||||
"typedoc-plugin-versions": "^0.2.3",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
|
||||
@@ -48,6 +48,8 @@ import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
import { E2EKeyReceiver, IE2EKeyReceiver } from "../test-utils/E2EKeyReceiver";
|
||||
import { ISyncResponder, SyncResponder } from "../test-utils/SyncResponder";
|
||||
import { escapeRegExp } from "../../src/utils";
|
||||
import { downloadDeviceToJsDevice } from "../../src/rust-crypto/device-converter";
|
||||
import { flushPromises } from "../test-utils/flushPromises";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -1997,4 +1999,178 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expect(res.fallbackKeysCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserDeviceInfo", () => {
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
// From https://spec.matrix.org/v1.6/client-server-api/#post_matrixclientv3keysquery
|
||||
// Using extracted response from matrix.org, it needs to have real keys etc to pass old crypto verification
|
||||
const queryResponseBody = {
|
||||
device_keys: {
|
||||
"@testing_florian1:matrix.org": {
|
||||
EBMMPAFOPU: {
|
||||
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
device_id: "EBMMPAFOPU",
|
||||
keys: {
|
||||
"curve25519:EBMMPAFOPU": "HyhQD4mXwNViqns0noABW9NxHbCAOkriQ4QKGGndk3w",
|
||||
"ed25519:EBMMPAFOPU": "xSQaxrFOTXH+7Zjo+iwb445hlNPFjnx1O3KaV3Am55k",
|
||||
},
|
||||
signatures: {
|
||||
"@testing_florian1:matrix.org": {
|
||||
"ed25519:EBMMPAFOPU":
|
||||
"XFJVq9HmO5lfJN7l6muaUt887aUHg0/poR3p9XHGXBrLUqzfG7Qllq7jjtUjtcTc5CMD7/mpsXfuC2eV+X1uAw",
|
||||
},
|
||||
},
|
||||
user_id: "@testing_florian1:matrix.org",
|
||||
unsigned: {
|
||||
device_display_name: "display name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
failures: {},
|
||||
master_keys: {
|
||||
"@testing_florian1:matrix.org": {
|
||||
user_id: "@testing_florian1:matrix.org",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
"ed25519:O5s5RoLaz93Bjf/pg55oJeCVeYYoruQhqEd0Mda6lq0":
|
||||
"O5s5RoLaz93Bjf/pg55oJeCVeYYoruQhqEd0Mda6lq0",
|
||||
},
|
||||
signatures: {
|
||||
"@testing_florian1:matrix.org": {
|
||||
"ed25519:UKAQMJSJZC":
|
||||
"q4GuzzuhZfTpwrlqnJ9+AEUtEfEQ0um1PO3puwp/+vidzFicw0xEPjedpJoASYQIJ8XJAAWX8Q235EKeCzEXCA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
self_signing_keys: {
|
||||
"@testing_florian1:matrix.org": {
|
||||
user_id: "@testing_florian1:matrix.org",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
"ed25519:YYWIHBCuKGEy9CXiVrfBVR0N1I60JtiJTNCWjiLAFzo":
|
||||
"YYWIHBCuKGEy9CXiVrfBVR0N1I60JtiJTNCWjiLAFzo",
|
||||
},
|
||||
signatures: {
|
||||
"@testing_florian1:matrix.org": {
|
||||
"ed25519:O5s5RoLaz93Bjf/pg55oJeCVeYYoruQhqEd0Mda6lq0":
|
||||
"yckmxgQ3JA5bb205/RunJipnpZ37ycGNf4OFzDwAad++chd71aGHqAMQ1f6D2GVfl8XdHmiRaohZf4mGnDL0AA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user_signing_keys: {
|
||||
"@testing_florian1:matrix.org": {
|
||||
user_id: "@testing_florian1:matrix.org",
|
||||
usage: ["user_signing"],
|
||||
keys: {
|
||||
"ed25519:Maa77okgZxnABGqaiChEUnV4rVsAI61WXWeL5TSEUhs":
|
||||
"Maa77okgZxnABGqaiChEUnV4rVsAI61WXWeL5TSEUhs",
|
||||
},
|
||||
signatures: {
|
||||
"@testing_florian1:matrix.org": {
|
||||
"ed25519:O5s5RoLaz93Bjf/pg55oJeCVeYYoruQhqEd0Mda6lq0":
|
||||
"WxNNXb13yCrBwXUQzdDWDvWSQ/qWCfwpvssOudlAgbtMzRESMbCTDkeA8sS1awaAtUmu7FrPtDb5LYfK/EE2CQ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function awaitKeyQueryRequest(): Promise<Record<string, []>> {
|
||||
return new Promise((resolve) => {
|
||||
const listener = (url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
// Resolve with request payload
|
||||
resolve(content.device_keys);
|
||||
|
||||
// Return response of `/keys/query`
|
||||
return queryResponseBody;
|
||||
};
|
||||
|
||||
for (const path of ["/_matrix/client/r0/keys/query", "/_matrix/client/v3/keys/query"]) {
|
||||
fetchMock.post(new URL(path, aliceClient.getHomeserverUrl()).toString(), listener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
it("Download uncached keys for known user", async () => {
|
||||
const queryPromise = awaitKeyQueryRequest();
|
||||
|
||||
const user = "@testing_florian1:matrix.org";
|
||||
const devicesInfo = await aliceClient.getCrypto()!.getUserDeviceInfo([user], true);
|
||||
|
||||
// Wait for `/keys/query` to be called
|
||||
const deviceKeysPayload = await queryPromise;
|
||||
|
||||
expect(deviceKeysPayload).toStrictEqual({ [user]: [] });
|
||||
expect(devicesInfo.get(user)?.size).toBe(1);
|
||||
|
||||
// Convert the expected device to IDevice and check
|
||||
expect(devicesInfo.get(user)?.get("EBMMPAFOPU")).toStrictEqual(
|
||||
downloadDeviceToJsDevice(queryResponseBody.device_keys[user]?.EBMMPAFOPU),
|
||||
);
|
||||
});
|
||||
|
||||
it("Download uncached keys for unknown user", async () => {
|
||||
const queryPromise = awaitKeyQueryRequest();
|
||||
|
||||
const user = "@bob:xyz";
|
||||
const devicesInfo = await aliceClient.getCrypto()!.getUserDeviceInfo([user], true);
|
||||
|
||||
// Wait for `/keys/query` to be called
|
||||
const deviceKeysPayload = await queryPromise;
|
||||
|
||||
expect(deviceKeysPayload).toStrictEqual({ [user]: [] });
|
||||
// The old crypto has an empty map for `@bob:xyz`
|
||||
// The new crypto does not have the `@bob:xyz` entry in `devicesInfo`
|
||||
expect(devicesInfo.get(user)?.size).toBeFalsy();
|
||||
});
|
||||
|
||||
it("Get devices from tacked users", async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
const queryPromise = awaitKeyQueryRequest();
|
||||
|
||||
const user = "@testing_florian1:matrix.org";
|
||||
// `user` will be added to the room
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse([user, "@bob:xyz"]));
|
||||
|
||||
// Advance local date to 2 minutes
|
||||
// The old crypto only runs the upload every 60 seconds
|
||||
jest.setSystemTime(Date.now() + 2 * 60 * 1000);
|
||||
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Old crypto: for alice: run over the `sleep(5)` in `doQueuedQueries` of `DeviceList`
|
||||
jest.runAllTimers();
|
||||
// Old crypto: for alice: run the `processQueryResponseForUser` in `doQueuedQueries` of `DeviceList`
|
||||
await flushPromises();
|
||||
|
||||
// Wait for alice to query `user` keys
|
||||
await queryPromise;
|
||||
|
||||
// Old crypto: for `user`: run over the `sleep(5)` in `doQueuedQueries` of `DeviceList`
|
||||
jest.runAllTimers();
|
||||
// Old crypto: for `user`: run the `processQueryResponseForUser` in `doQueuedQueries` of `DeviceList`
|
||||
// It will add `@testing_florian1:matrix.org` devices to the DeviceList
|
||||
await flushPromises();
|
||||
|
||||
const devicesInfo = await aliceClient.getCrypto()!.getUserDeviceInfo([user]);
|
||||
|
||||
// We should only have the `user` in it
|
||||
expect(devicesInfo.size).toBe(1);
|
||||
// We are expecting only the EBMMPAFOPU device
|
||||
expect(devicesInfo.get(user)!.size).toBe(1);
|
||||
expect(devicesInfo.get(user)!.get("EBMMPAFOPU")).toEqual(
|
||||
downloadDeviceToJsDevice(queryResponseBody.device_keys[user]["EBMMPAFOPU"]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,11 +21,13 @@ import {
|
||||
EventStatus,
|
||||
EventTimeline,
|
||||
EventTimelineSet,
|
||||
EventType,
|
||||
Filter,
|
||||
IEvent,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
PendingEventOrdering,
|
||||
RelationType,
|
||||
Room,
|
||||
} from "../../src/matrix";
|
||||
import { logger } from "../../src/logger";
|
||||
@@ -33,6 +35,7 @@ import { encodeParams, encodeUri, QueryDict, replaceParam } from "../../src/util
|
||||
import { TestClient } from "../TestClient";
|
||||
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../src/models/thread";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { Feature, ServerSupport } from "../../src/feature";
|
||||
|
||||
const userId = "@alice:localhost";
|
||||
const userName = "Alice";
|
||||
@@ -1164,6 +1167,117 @@ describe("MatrixClient event timelines", function () {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should ensure thread events don't get reordered with recursive relations", async () => {
|
||||
// Test data for a second reply to the first thread
|
||||
const THREAD_REPLY2 = utils.mkEvent({
|
||||
room: roomId,
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": "thread reply 2",
|
||||
"msgtype": "m.text",
|
||||
"m.relates_to": {
|
||||
// We can't use the const here because we change server support mode for test
|
||||
rel_type: "io.element.thread",
|
||||
event_id: THREAD_ROOT.event_id,
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
THREAD_REPLY2.localTimestamp += 1000;
|
||||
const THREAD_ROOT_REACTION = utils.mkEvent({
|
||||
event: true,
|
||||
type: EventType.Reaction,
|
||||
user: userId,
|
||||
room: roomId,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
event_id: THREAD_ROOT.event_id!,
|
||||
key: Math.random().toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
THREAD_ROOT_REACTION.localTimestamp += 2000;
|
||||
|
||||
// Test data for a second reply to the first thread
|
||||
const THREAD_REPLY3 = utils.mkEvent({
|
||||
room: roomId,
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": "thread reply 3",
|
||||
"msgtype": "m.text",
|
||||
"m.relates_to": {
|
||||
// We can't use the const here because we change server support mode for test
|
||||
rel_type: "io.element.thread",
|
||||
event_id: THREAD_ROOT.event_id,
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
THREAD_REPLY3.localTimestamp += 3000;
|
||||
|
||||
// Test data for the first thread, with the second reply
|
||||
const THREAD_ROOT_UPDATED = {
|
||||
...THREAD_ROOT,
|
||||
unsigned: {
|
||||
...THREAD_ROOT.unsigned,
|
||||
"m.relations": {
|
||||
...THREAD_ROOT.unsigned!["m.relations"],
|
||||
"io.element.thread": {
|
||||
...THREAD_ROOT.unsigned!["m.relations"]!["io.element.thread"],
|
||||
count: 3,
|
||||
latest_event: THREAD_REPLY3.event,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
client.clientOpts.threadSupport = true;
|
||||
client.canSupport.set(Feature.RelationsRecursion, ServerSupport.Stable);
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
|
||||
client.fetchRoomEvent = () => Promise.resolve(THREAD_ROOT_UPDATED);
|
||||
|
||||
await client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
// Assume we're seeing the reply while loading backlog
|
||||
room.addLiveEvents([THREAD_REPLY2]);
|
||||
httpBackend
|
||||
.when(
|
||||
"GET",
|
||||
"/_matrix/client/v1/rooms/!foo%3Abar/relations/" +
|
||||
encodeURIComponent(THREAD_ROOT_UPDATED.event_id!) +
|
||||
"/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
||||
buildRelationPaginationQuery({
|
||||
dir: Direction.Backward,
|
||||
limit: 3,
|
||||
recurse: true,
|
||||
}),
|
||||
)
|
||||
.respond(200, {
|
||||
chunk: [THREAD_REPLY3.event, THREAD_ROOT_REACTION, THREAD_REPLY2.event, THREAD_REPLY],
|
||||
});
|
||||
await flushHttp(prom);
|
||||
// but while loading the metadata, a new reply has arrived
|
||||
room.addLiveEvents([THREAD_REPLY3]);
|
||||
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
|
||||
// then the events should still be all in the right order
|
||||
expect(thread.events.map((it) => it.getId())).toEqual([
|
||||
THREAD_ROOT.event_id,
|
||||
THREAD_REPLY.event_id,
|
||||
THREAD_REPLY2.getId(),
|
||||
THREAD_REPLY3.getId(),
|
||||
]);
|
||||
});
|
||||
|
||||
describe("paginateEventTimeline for thread list timeline", function () {
|
||||
const RANDOM_TOKEN = "7280349c7bee430f91defe2a38a0a08c";
|
||||
|
||||
@@ -1847,7 +1961,10 @@ describe("MatrixClient event timelines", function () {
|
||||
encodeURIComponent(THREAD_ROOT.event_id!) +
|
||||
"/" +
|
||||
encodeURIComponent(THREAD_RELATION_TYPE.name) +
|
||||
buildRelationPaginationQuery({ dir: Direction.Backward, from: "start_token" }),
|
||||
buildRelationPaginationQuery({
|
||||
dir: Direction.Backward,
|
||||
from: "start_token",
|
||||
}),
|
||||
)
|
||||
.respond(200, function () {
|
||||
return {
|
||||
|
||||
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
import { Mocked } from "jest-mock";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { CRYPTO_ENABLED, IStoredClientOpts, MatrixClient } from "../../src/client";
|
||||
@@ -24,6 +25,7 @@ import { THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||
import { IFilterDefinition } from "../../src/filter";
|
||||
import { ISearchResults } from "../../src/@types/search";
|
||||
import { IStore } from "../../src/store";
|
||||
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
|
||||
|
||||
describe("MatrixClient", function () {
|
||||
const userId = "@alice:localhost";
|
||||
@@ -1412,6 +1414,42 @@ describe("MatrixClient", function () {
|
||||
await client!.uploadKeys();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCryptoTrustCrossSignedDevices", () => {
|
||||
it("should throw if e2e is disabled", () => {
|
||||
expect(() => client!.getCryptoTrustCrossSignedDevices()).toThrow("End-to-end encryption disabled");
|
||||
});
|
||||
|
||||
it("should proxy to the crypto backend", async () => {
|
||||
const mockBackend = {
|
||||
getTrustCrossSignedDevices: jest.fn().mockReturnValue(true),
|
||||
} as unknown as Mocked<CryptoBackend>;
|
||||
client!["cryptoBackend"] = mockBackend;
|
||||
|
||||
expect(client!.getCryptoTrustCrossSignedDevices()).toBe(true);
|
||||
mockBackend.getTrustCrossSignedDevices.mockReturnValue(false);
|
||||
expect(client!.getCryptoTrustCrossSignedDevices()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setCryptoTrustCrossSignedDevices", () => {
|
||||
it("should throw if e2e is disabled", () => {
|
||||
expect(() => client!.setCryptoTrustCrossSignedDevices(false)).toThrow("End-to-end encryption disabled");
|
||||
});
|
||||
|
||||
it("should proxy to the crypto backend", async () => {
|
||||
const mockBackend = {
|
||||
setTrustCrossSignedDevices: jest.fn(),
|
||||
} as unknown as Mocked<CryptoBackend>;
|
||||
client!["cryptoBackend"] = mockBackend;
|
||||
|
||||
client!.setCryptoTrustCrossSignedDevices(true);
|
||||
expect(mockBackend.setTrustCrossSignedDevices).toHaveBeenLastCalledWith(true);
|
||||
|
||||
client!.setCryptoTrustCrossSignedDevices(false);
|
||||
expect(mockBackend.setTrustCrossSignedDevices).toHaveBeenLastCalledWith(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
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 { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
import { DeviceVerification } from "../../../src";
|
||||
import { deviceInfoToDevice } from "../../../src/crypto/device-converter";
|
||||
|
||||
describe("device-converter", () => {
|
||||
const userId = "@alice:example.com";
|
||||
const deviceId = "xcvf";
|
||||
|
||||
// All parameters for DeviceInfo initialization
|
||||
const keys = {
|
||||
[`ed25519:${deviceId}`]: "key1",
|
||||
[`curve25519:${deviceId}`]: "key2",
|
||||
};
|
||||
const algorithms = ["algo1", "algo2"];
|
||||
const verified = DeviceVerification.Verified;
|
||||
const signatures = { [userId]: { [deviceId]: "sign1" } };
|
||||
const displayName = "display name";
|
||||
const unsigned = {
|
||||
device_display_name: displayName,
|
||||
};
|
||||
|
||||
describe("deviceInfoToDevice", () => {
|
||||
it("should convert a DeviceInfo to a Device", () => {
|
||||
const deviceInfo = DeviceInfo.fromStorage({ keys, algorithms, verified, signatures, unsigned }, deviceId);
|
||||
const device = deviceInfoToDevice(deviceInfo, userId);
|
||||
|
||||
expect(device.deviceId).toBe(deviceId);
|
||||
expect(device.userId).toBe(userId);
|
||||
expect(device.verified).toBe(verified);
|
||||
expect(device.getIdentityKey()).toBe(keys[`curve25519:${deviceId}`]);
|
||||
expect(device.getFingerprint()).toBe(keys[`ed25519:${deviceId}`]);
|
||||
expect(device.displayName).toBe(displayName);
|
||||
});
|
||||
|
||||
it("should add empty signatures", () => {
|
||||
const deviceInfo = DeviceInfo.fromStorage({ keys, algorithms, verified }, deviceId);
|
||||
const device = deviceInfoToDevice(deviceInfo, userId);
|
||||
|
||||
expect(device.signatures.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -374,6 +374,12 @@ describe("SAS verification", function () {
|
||||
expect(bobDeviceTrust.isLocallyVerified()).toBeTruthy();
|
||||
expect(bobDeviceTrust.isCrossSigningVerified()).toBeFalsy();
|
||||
|
||||
const bobDeviceVerificationStatus = (await alice.client
|
||||
.getCrypto()!
|
||||
.getDeviceVerificationStatus("@bob:example.com", "Dynabook"))!;
|
||||
expect(bobDeviceVerificationStatus.localVerified).toBe(true);
|
||||
expect(bobDeviceVerificationStatus.crossSigningVerified).toBe(false);
|
||||
|
||||
const aliceTrust = bob.client.checkUserTrust("@alice:example.com");
|
||||
expect(aliceTrust.isCrossSigningVerified()).toBeTruthy();
|
||||
expect(aliceTrust.isTofu()).toBeTruthy();
|
||||
@@ -381,6 +387,17 @@ describe("SAS verification", function () {
|
||||
const aliceDeviceTrust = bob.client.checkDeviceTrust("@alice:example.com", "Osborne2");
|
||||
expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy();
|
||||
expect(aliceDeviceTrust.isCrossSigningVerified()).toBeFalsy();
|
||||
|
||||
const aliceDeviceVerificationStatus = (await bob.client
|
||||
.getCrypto()!
|
||||
.getDeviceVerificationStatus("@alice:example.com", "Osborne2"))!;
|
||||
expect(aliceDeviceVerificationStatus.localVerified).toBe(true);
|
||||
expect(aliceDeviceVerificationStatus.crossSigningVerified).toBe(false);
|
||||
|
||||
const unknownDeviceVerificationStatus = await bob.client
|
||||
.getCrypto()!
|
||||
.getDeviceVerificationStatus("@alice:example.com", "xyz");
|
||||
expect(unknownDeviceVerificationStatus).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ describe("EventTimelineSet", () => {
|
||||
beforeEach(() => {
|
||||
client = utils.mock(MatrixClient, "MatrixClient");
|
||||
client.reEmitter = utils.mock(ReEmitter, "ReEmitter");
|
||||
client.canSupport = new Map();
|
||||
room = new Room(roomId, client, userA);
|
||||
eventTimelineSet = new EventTimelineSet(room);
|
||||
eventTimeline = new EventTimeline(eventTimelineSet);
|
||||
|
||||
@@ -2909,4 +2909,43 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("pushers", () => {
|
||||
const pusher = {
|
||||
app_id: "test",
|
||||
app_display_name: "Test App",
|
||||
data: {},
|
||||
device_display_name: "test device",
|
||||
kind: "http",
|
||||
lang: "en-NZ",
|
||||
pushkey: "1234",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
makeClient();
|
||||
const response: HttpLookup = {
|
||||
method: Method.Post,
|
||||
path: "/pushers/set",
|
||||
data: {},
|
||||
};
|
||||
httpLookups = [response];
|
||||
jest.spyOn(client.http, "authedRequest").mockClear();
|
||||
});
|
||||
|
||||
it("should make correct request to set pusher", async () => {
|
||||
const result = await client.setPusher(pusher);
|
||||
expect(client.http.authedRequest).toHaveBeenCalledWith(Method.Post, "/pushers/set", undefined, pusher);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("should make correct request to remove pusher", async () => {
|
||||
const result = await client.removePusher(pusher.pushkey, pusher.app_id);
|
||||
expect(client.http.authedRequest).toHaveBeenCalledWith(Method.Post, "/pushers/set", undefined, {
|
||||
pushkey: pusher.pushkey,
|
||||
app_id: pusher.app_id,
|
||||
kind: null,
|
||||
});
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
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 { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { ReceiptAccumulator } from "../../src/receipt-accumulator";
|
||||
|
||||
const roomId = "!foo:bar";
|
||||
|
||||
describe("ReceiptAccumulator", function () {
|
||||
/*
|
||||
* Note: at the time of writing, the ReceiptAccumulator uses the order of
|
||||
* events from sync as the determinant of which one is most recent. This
|
||||
* is correct, but inconsistent with other areas of the code, since we
|
||||
* don't persist this order, so in other places we are forced to use the
|
||||
* timestamp of events and hope that this matches up.
|
||||
*
|
||||
* The tests in this file provide ts values that are consistent with the
|
||||
* sync order, so they should still pass if we change to using ts to
|
||||
* determine order.
|
||||
*
|
||||
* Ideally, we would keep track of sync order so we can use it everywhere.
|
||||
* This would mean we are consistent with how the homeserver sees receipts
|
||||
* and notifications.
|
||||
*/
|
||||
|
||||
it("Discards previous unthreaded receipts for the same user", () => {
|
||||
const acc = new ReceiptAccumulator();
|
||||
|
||||
const receipt1 = newReceipt("$event1", ReceiptType.Read, "@alice:localhost", 1);
|
||||
const receipt2 = newReceipt("$event2", ReceiptType.Read, "@alice:localhost", 2);
|
||||
|
||||
acc.consumeEphemeralEvents([receipt1, receipt2]);
|
||||
|
||||
const newEvent = acc.buildAccumulatedReceiptEvent(roomId);
|
||||
expect(newEvent).toEqual(newReceipt("$event2", ReceiptType.Read, "@alice:localhost", 2));
|
||||
});
|
||||
|
||||
it("Discards previous threaded receipts for the same user in the same thread", () => {
|
||||
const acc = new ReceiptAccumulator();
|
||||
|
||||
const receipt1 = newReceipt("$event1", ReceiptType.Read, "@alice:localhost", 1, "thread1");
|
||||
const receipt2 = newReceipt("$event2", ReceiptType.Read, "@alice:localhost", 2, "thread1");
|
||||
|
||||
acc.consumeEphemeralEvents([receipt1, receipt2]);
|
||||
|
||||
const newEvent = acc.buildAccumulatedReceiptEvent(roomId);
|
||||
expect(newEvent).toEqual(newReceipt("$event2", ReceiptType.Read, "@alice:localhost", 2, "thread1"));
|
||||
});
|
||||
|
||||
it("Collects multiple receipts for the same user if they are in different threads", () => {
|
||||
const acc = new ReceiptAccumulator();
|
||||
|
||||
const receipt1 = newReceipt("$event1", ReceiptType.Read, "@alice:localhost", 1, "thread1");
|
||||
const receipt2 = newReceipt("$event2", ReceiptType.Read, "@alice:localhost", 2, "thread2");
|
||||
|
||||
acc.consumeEphemeralEvents([receipt1, receipt2]);
|
||||
|
||||
const newEvent = acc.buildAccumulatedReceiptEvent(roomId);
|
||||
expect(newEvent).toEqual(
|
||||
newMultiReceipt([
|
||||
["$event1", ReceiptType.Read, "@alice:localhost", 1, "thread1"],
|
||||
["$event2", ReceiptType.Read, "@alice:localhost", 2, "thread2"],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("Collects multiple receipts for different users", () => {
|
||||
const acc = new ReceiptAccumulator();
|
||||
|
||||
const receipt1 = newReceipt("$event1", ReceiptType.Read, "@alice:localhost", 1, "thread1");
|
||||
const receipt2 = newReceipt("$event2", ReceiptType.Read, "@bobby:localhost", 2, "thread1");
|
||||
|
||||
acc.consumeEphemeralEvents([receipt1, receipt2]);
|
||||
|
||||
const newEvent = acc.buildAccumulatedReceiptEvent(roomId);
|
||||
expect(newEvent).toEqual(
|
||||
newMultiReceipt([
|
||||
["$event1", ReceiptType.Read, "@alice:localhost", 1, "thread1"],
|
||||
["$event2", ReceiptType.Read, "@bobby:localhost", 2, "thread1"],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("Collects last receipt for various users and threads", () => {
|
||||
const acc = new ReceiptAccumulator();
|
||||
|
||||
// Below, if ts=1, this receipt is going to be superceded by another
|
||||
// with ts=2
|
||||
const receipts = [
|
||||
newReceipt("$event1", ReceiptType.Read, "@alice:localhost", 1),
|
||||
newReceipt("$event2", ReceiptType.Read, "@bobby:localhost", 1, "thread1"),
|
||||
newReceipt("$event3", ReceiptType.Read, "@alice:localhost", 1, "thread1"),
|
||||
newReceipt("$event4", ReceiptType.Read, "@bobby:localhost", 1),
|
||||
newReceipt("$event5", ReceiptType.Read, "@bobby:localhost", 2),
|
||||
newReceipt("$event6", ReceiptType.Read, "@alice:localhost", 2),
|
||||
newReceipt("$event7", ReceiptType.Read, "@bobby:localhost", 2, "thread1"),
|
||||
newReceipt("$event8", ReceiptType.Read, "@bobby:localhost", 1, "thread2"),
|
||||
newReceipt("$event9", ReceiptType.Read, "@bobby:localhost", 2, "thread2"),
|
||||
newReceipt("$eventA", ReceiptType.Read, "@alice:localhost", 2, "thread1"),
|
||||
newReceipt("$eventB", ReceiptType.Read, "@alice:localhost", 1, "thread2"),
|
||||
newReceipt("$eventC", ReceiptType.Read, "@alice:localhost", 2, "thread2"),
|
||||
];
|
||||
|
||||
acc.consumeEphemeralEvents(receipts);
|
||||
const newEvent = acc.buildAccumulatedReceiptEvent(roomId);
|
||||
|
||||
// Only the ts=2 receipts make it through
|
||||
expect(newEvent).toEqual(
|
||||
newMultiReceipt([
|
||||
["$event5", ReceiptType.Read, "@bobby:localhost", 2, undefined],
|
||||
["$event6", ReceiptType.Read, "@alice:localhost", 2, undefined],
|
||||
["$event7", ReceiptType.Read, "@bobby:localhost", 2, "thread1"],
|
||||
["$event9", ReceiptType.Read, "@bobby:localhost", 2, "thread2"],
|
||||
["$eventA", ReceiptType.Read, "@alice:localhost", 2, "thread1"],
|
||||
["$eventC", ReceiptType.Read, "@alice:localhost", 2, "thread2"],
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const newReceipt = (
|
||||
eventId: string,
|
||||
receiptType: ReceiptType,
|
||||
userId: string,
|
||||
ts: number,
|
||||
threadId: string | undefined = undefined,
|
||||
) => {
|
||||
return {
|
||||
type: "m.receipt",
|
||||
room_id: roomId,
|
||||
content: {
|
||||
[eventId]: {
|
||||
[receiptType]: {
|
||||
[userId]: { ts, thread_id: threadId },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const newMultiReceipt = (infoArray: Array<[string, ReceiptType, string, number, string | undefined]>) => {
|
||||
return {
|
||||
type: "m.receipt",
|
||||
room_id: roomId,
|
||||
content: Object.fromEntries(
|
||||
infoArray.map(([eventId, receiptType, userId, ts, threadId]) => {
|
||||
return [
|
||||
eventId,
|
||||
{
|
||||
[receiptType]: {
|
||||
[userId]: { ts, thread_id: threadId },
|
||||
},
|
||||
},
|
||||
];
|
||||
}),
|
||||
),
|
||||
};
|
||||
};
|
||||
+129
-3
@@ -19,7 +19,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, M_POLL_START, PollStartEvent } from "matrix-events-sdk";
|
||||
import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, M_POLL_START, Optional, PollStartEvent } from "matrix-events-sdk";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
@@ -54,6 +54,7 @@ import { Crypto } from "../../src/crypto";
|
||||
import { mkThread } from "../test-utils/thread";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../test-utils/client";
|
||||
import { logger } from "../../src/logger";
|
||||
import { IMessageOpts } from "../test-utils/test-utils";
|
||||
|
||||
describe("Room", function () {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -63,9 +64,10 @@ describe("Room", function () {
|
||||
const userD = "@dorothy:bar";
|
||||
let room: Room;
|
||||
|
||||
const mkMessage = () =>
|
||||
const mkMessage = (opts?: Partial<IMessageOpts>) =>
|
||||
utils.mkMessage(
|
||||
{
|
||||
...opts,
|
||||
event: true,
|
||||
user: userA,
|
||||
room: roomId,
|
||||
@@ -113,9 +115,10 @@ describe("Room", function () {
|
||||
room.client,
|
||||
);
|
||||
|
||||
const mkThreadResponse = (root: MatrixEvent) =>
|
||||
const mkThreadResponse = (root: MatrixEvent, opts?: Partial<IMessageOpts>) =>
|
||||
utils.mkEvent(
|
||||
{
|
||||
...opts,
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
user: userA,
|
||||
@@ -165,6 +168,66 @@ describe("Room", function () {
|
||||
room.client,
|
||||
);
|
||||
|
||||
const addRoomMainAndThreadMessages = (
|
||||
room: Room,
|
||||
tsMain?: number,
|
||||
tsThread?: number,
|
||||
): { mainEvent?: MatrixEvent; threadEvent?: MatrixEvent } => {
|
||||
const result: { mainEvent?: MatrixEvent; threadEvent?: MatrixEvent } = {};
|
||||
|
||||
if (tsMain) {
|
||||
result.mainEvent = mkMessage({ ts: tsMain });
|
||||
room.addLiveEvents([result.mainEvent]);
|
||||
}
|
||||
|
||||
if (tsThread) {
|
||||
const { rootEvent, thread } = mkThread({
|
||||
room,
|
||||
client: new TestClient().client,
|
||||
authorId: "@bob:example.org",
|
||||
participantUserIds: ["@bob:example.org"],
|
||||
});
|
||||
result.threadEvent = mkThreadResponse(rootEvent, { ts: tsThread });
|
||||
thread.liveTimeline.addEvent(result.threadEvent, { toStartOfTimeline: true });
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const addRoomThreads = (
|
||||
room: Room,
|
||||
thread1EventTs: Optional<number>,
|
||||
thread2EventTs: Optional<number>,
|
||||
): { thread1?: Thread; thread2?: Thread } => {
|
||||
const result: { thread1?: Thread; thread2?: Thread } = {};
|
||||
|
||||
if (thread1EventTs !== null) {
|
||||
const { rootEvent: thread1RootEvent, thread: thread1 } = mkThread({
|
||||
room,
|
||||
client: new TestClient().client,
|
||||
authorId: "@bob:example.org",
|
||||
participantUserIds: ["@bob:example.org"],
|
||||
});
|
||||
const thread1Event = mkThreadResponse(thread1RootEvent, { ts: thread1EventTs });
|
||||
thread1.liveTimeline.addEvent(thread1Event, { toStartOfTimeline: true });
|
||||
result.thread1 = thread1;
|
||||
}
|
||||
|
||||
if (thread2EventTs !== null) {
|
||||
const { rootEvent: thread2RootEvent, thread: thread2 } = mkThread({
|
||||
room,
|
||||
client: new TestClient().client,
|
||||
authorId: "@bob:example.org",
|
||||
participantUserIds: ["@bob:example.org"],
|
||||
});
|
||||
const thread2Event = mkThreadResponse(thread2RootEvent, { ts: thread2EventTs });
|
||||
thread2.liveTimeline.addEvent(thread2Event, { toStartOfTimeline: true });
|
||||
result.thread2 = thread2;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
room = new Room(roomId, new TestClient(userA, "device").client, userA);
|
||||
// mock RoomStates
|
||||
@@ -3475,4 +3538,67 @@ describe("Room", function () {
|
||||
expect(room.findPredecessor()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLastLiveEvent", () => {
|
||||
let lastEventInMainTimeline: MatrixEvent;
|
||||
let lastEventInThread: MatrixEvent;
|
||||
|
||||
it("when there are no events, it should return undefined", () => {
|
||||
expect(room.getLastLiveEvent()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("when there is only an event in the main timeline and there are no threads, it should return the last event from the main timeline", () => {
|
||||
lastEventInMainTimeline = addRoomMainAndThreadMessages(room, 23).mainEvent!;
|
||||
room.addLiveEvents([lastEventInMainTimeline]);
|
||||
expect(room.getLastLiveEvent()).toBe(lastEventInMainTimeline);
|
||||
});
|
||||
|
||||
it("when there is no event in the room live timeline but in a thread, it should return the last event from the thread", () => {
|
||||
lastEventInThread = addRoomMainAndThreadMessages(room, undefined, 42).threadEvent!;
|
||||
expect(room.getLastLiveEvent()).toBe(lastEventInThread);
|
||||
});
|
||||
|
||||
describe("when there are events in both, the main timeline and threads", () => {
|
||||
it("and the last event is in a thread, it should return the last event from the thread", () => {
|
||||
lastEventInThread = addRoomMainAndThreadMessages(room, 23, 42).threadEvent!;
|
||||
expect(room.getLastLiveEvent()).toBe(lastEventInThread);
|
||||
});
|
||||
|
||||
it("and the last event is in the main timeline, it should return the last event from the main timeline", () => {
|
||||
lastEventInMainTimeline = addRoomMainAndThreadMessages(room, 42, 23).mainEvent!;
|
||||
expect(room.getLastLiveEvent()).toBe(lastEventInMainTimeline);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLastThread", () => {
|
||||
it("when there is no thread, it should return undefined", () => {
|
||||
expect(room.getLastThread()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("when there is only one thread, it should return this one", () => {
|
||||
const { thread1 } = addRoomThreads(room, 23, null);
|
||||
expect(room.getLastThread()).toBe(thread1);
|
||||
});
|
||||
|
||||
it("when there are tho threads, it should return the one with the recent event I", () => {
|
||||
const { thread2 } = addRoomThreads(room, 23, 42);
|
||||
expect(room.getLastThread()).toBe(thread2);
|
||||
});
|
||||
|
||||
it("when there are tho threads, it should return the one with the recent event II", () => {
|
||||
const { thread1 } = addRoomThreads(room, 42, 23);
|
||||
expect(room.getLastThread()).toBe(thread1);
|
||||
});
|
||||
|
||||
it("when there is a thread with the last event ts undefined, it should return the thread with the defined event ts", () => {
|
||||
const { thread2 } = addRoomThreads(room, undefined, 23);
|
||||
expect(room.getLastThread()).toBe(thread2);
|
||||
});
|
||||
|
||||
it("when the last event ts of all threads is undefined, it should return the last added thread", () => {
|
||||
const { thread2 } = addRoomThreads(room, undefined, undefined);
|
||||
expect(room.getLastThread()).toBe(thread2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
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 { DeviceKeys, DeviceVerification } from "../../../src";
|
||||
import { downloadDeviceToJsDevice } from "../../../src/rust-crypto/device-converter";
|
||||
|
||||
describe("device-converter", () => {
|
||||
const userId = "@alice:example.com";
|
||||
const deviceId = "xcvf";
|
||||
|
||||
// All parameters for QueryDevice initialization
|
||||
const keys = {
|
||||
[`ed25519:${deviceId}`]: "key1",
|
||||
[`curve25519:${deviceId}`]: "key2",
|
||||
};
|
||||
const algorithms = ["algo1", "algo2"];
|
||||
const signatures = { [userId]: { [deviceId]: "sign1" } };
|
||||
const displayName = "display name";
|
||||
const unsigned = {
|
||||
device_display_name: displayName,
|
||||
};
|
||||
|
||||
describe("downloadDeviceToJsDevice", () => {
|
||||
it("should convert a QueryDevice to a Device", () => {
|
||||
const queryDevice: DeviceKeys[keyof DeviceKeys] = {
|
||||
keys,
|
||||
algorithms,
|
||||
device_id: deviceId,
|
||||
user_id: userId,
|
||||
signatures,
|
||||
unsigned,
|
||||
};
|
||||
const device = downloadDeviceToJsDevice(queryDevice);
|
||||
|
||||
expect(device.deviceId).toBe(deviceId);
|
||||
expect(device.userId).toBe(userId);
|
||||
expect(device.verified).toBe(DeviceVerification.Unverified);
|
||||
expect(device.getIdentityKey()).toBe(keys[`curve25519:${deviceId}`]);
|
||||
expect(device.getFingerprint()).toBe(keys[`ed25519:${deviceId}`]);
|
||||
expect(device.displayName).toBe(displayName);
|
||||
});
|
||||
|
||||
it("should add empty signatures", () => {
|
||||
const queryDevice: DeviceKeys[keyof DeviceKeys] = {
|
||||
keys,
|
||||
algorithms,
|
||||
device_id: deviceId,
|
||||
user_id: userId,
|
||||
};
|
||||
const device = downloadDeviceToJsDevice(queryDevice);
|
||||
|
||||
expect(device.signatures.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022-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.
|
||||
@@ -230,4 +230,55 @@ describe("RustCrypto", () => {
|
||||
expect(res.encrypted).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("get|setTrustCrossSignedDevices", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
beforeEach(async () => {
|
||||
rustCrypto = await initRustCrypto({} as MatrixClient["http"], TEST_USER, TEST_DEVICE_ID);
|
||||
});
|
||||
|
||||
it("should be true by default", () => {
|
||||
expect(rustCrypto.getTrustCrossSignedDevices()).toBe(true);
|
||||
});
|
||||
|
||||
it("should be easily turn-off-and-on-able", () => {
|
||||
rustCrypto.setTrustCrossSignedDevices(false);
|
||||
expect(rustCrypto.getTrustCrossSignedDevices()).toBe(false);
|
||||
rustCrypto.setTrustCrossSignedDevices(true);
|
||||
expect(rustCrypto.getTrustCrossSignedDevices()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDeviceVerificationStatus", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
beforeEach(() => {
|
||||
olmMachine = {
|
||||
getDevice: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
rustCrypto = new RustCrypto(olmMachine, {} as MatrixClient["http"], TEST_USER, TEST_DEVICE_ID);
|
||||
});
|
||||
|
||||
it("should call getDevice", async () => {
|
||||
olmMachine.getDevice.mockResolvedValue({
|
||||
isCrossSigningTrusted: jest.fn().mockReturnValue(false),
|
||||
isLocallyTrusted: jest.fn().mockReturnValue(false),
|
||||
isCrossSignedByOwner: jest.fn().mockReturnValue(false),
|
||||
} as unknown as RustSdkCryptoJs.Device);
|
||||
const res = await rustCrypto.getDeviceVerificationStatus("@user:domain", "device");
|
||||
expect(olmMachine.getDevice.mock.calls[0][0].toString()).toEqual("@user:domain");
|
||||
expect(olmMachine.getDevice.mock.calls[0][1].toString()).toEqual("device");
|
||||
expect(res?.crossSigningVerified).toBe(false);
|
||||
expect(res?.localVerified).toBe(false);
|
||||
expect(res?.signedByOwner).toBe(false);
|
||||
});
|
||||
|
||||
it("should return null for unknown device", async () => {
|
||||
olmMachine.getDevice.mockResolvedValue(undefined);
|
||||
const res = await rustCrypto.getDeviceVerificationStatus("@user:domain", "device");
|
||||
expect(res).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -356,6 +356,77 @@ describe("SyncAccumulator", function () {
|
||||
});
|
||||
});
|
||||
|
||||
it("can handle large numbers of identical receipts", () => {
|
||||
const testSize = 1000; // Make this big to check performance (e.g. 10 million ~= 10s)
|
||||
|
||||
const newReceipt = (ts: number) => {
|
||||
return {
|
||||
type: "m.receipt",
|
||||
room_id: "!foo:bar",
|
||||
content: {
|
||||
"$event1:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
"@alice:localhost": { ts },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const receipts = [];
|
||||
for (let i = 0; i < testSize; i++) {
|
||||
receipts.push(newReceipt(testSize - i));
|
||||
}
|
||||
|
||||
sa.accumulate(
|
||||
syncSkeleton({
|
||||
ephemeral: {
|
||||
events: receipts,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const events = sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events;
|
||||
expect(events.length).toEqual(1);
|
||||
expect(events[0]).toEqual(newReceipt(1));
|
||||
});
|
||||
|
||||
it("can handle large numbers of receipts for different users and events", () => {
|
||||
const testSize = 100; // Make this big to check performance (e.g. 1 million ~= 10s)
|
||||
|
||||
const newReceipt = (ts: number) => {
|
||||
return {
|
||||
type: "m.receipt",
|
||||
room_id: "!foo:bar",
|
||||
content: {
|
||||
[`$event${ts}:localhost`]: {
|
||||
[ReceiptType.Read]: {
|
||||
[`@alice${ts}:localhost`]: { ts },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const receipts = [];
|
||||
for (let i = 0; i < testSize; i++) {
|
||||
receipts.push(newReceipt(testSize - i));
|
||||
}
|
||||
|
||||
sa.accumulate(
|
||||
syncSkeleton({
|
||||
ephemeral: {
|
||||
events: receipts,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const events = sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events;
|
||||
expect(events.length).toEqual(1);
|
||||
expect(events[0]["content"]["$event1:localhost"]).toEqual({ "m.read": { "@alice1:localhost": { ts: 1 } } });
|
||||
expect(Object.keys(events[0]["content"]).length).toEqual(testSize);
|
||||
});
|
||||
|
||||
it("should accumulate threaded read receipts", () => {
|
||||
const receipt1 = {
|
||||
type: "m.receipt",
|
||||
|
||||
@@ -388,6 +388,10 @@ describe("Group Call", function () {
|
||||
call = new MockMatrixCall(room.roomId, groupCall.groupCallId);
|
||||
|
||||
await groupCall.create();
|
||||
|
||||
const deviceCallMap = new Map<string, MatrixCall>();
|
||||
deviceCallMap.set(FAKE_DEVICE_ID_1, call.typed());
|
||||
(groupCall as any).calls.set(FAKE_USER_ID_1, deviceCallMap);
|
||||
});
|
||||
|
||||
it("ignores changes, if we can't get user id of opponent", async () => {
|
||||
@@ -1625,4 +1629,77 @@ describe("Group Call", function () {
|
||||
expect(room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, FAKE_USER_ID_2)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collection stats", () => {
|
||||
let groupCall: GroupCall;
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
});
|
||||
|
||||
afterAll(() => jest.useRealTimers());
|
||||
|
||||
beforeEach(async () => {
|
||||
const typedMockClient = new MockCallMatrixClient(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, FAKE_SESSION_ID_1);
|
||||
const mockClient = typedMockClient.typed();
|
||||
const room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
|
||||
groupCall = new GroupCall(
|
||||
mockClient,
|
||||
room,
|
||||
GroupCallType.Video,
|
||||
false,
|
||||
GroupCallIntent.Prompt,
|
||||
FAKE_CONF_ID,
|
||||
);
|
||||
});
|
||||
it("should be undefined if not get stats", async () => {
|
||||
// @ts-ignore
|
||||
const stats = groupCall.stats;
|
||||
expect(stats).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should be defined after first access", async () => {
|
||||
groupCall.getGroupCallStats();
|
||||
// @ts-ignore
|
||||
const stats = groupCall.stats;
|
||||
expect(stats).toBeDefined();
|
||||
});
|
||||
|
||||
it("with every number should do nothing if no stats exists.", async () => {
|
||||
groupCall.setGroupCallStatsInterval(0);
|
||||
// @ts-ignore
|
||||
let stats = groupCall.stats;
|
||||
expect(stats).toBeUndefined();
|
||||
|
||||
groupCall.setGroupCallStatsInterval(10000);
|
||||
// @ts-ignore
|
||||
stats = groupCall.stats;
|
||||
expect(stats).toBeUndefined();
|
||||
});
|
||||
|
||||
it("with number should stop existing stats", async () => {
|
||||
const stats = groupCall.getGroupCallStats();
|
||||
// @ts-ignore
|
||||
const stop = jest.spyOn(stats, "stop");
|
||||
// @ts-ignore
|
||||
const start = jest.spyOn(stats, "start");
|
||||
groupCall.setGroupCallStatsInterval(0);
|
||||
|
||||
expect(stop).toHaveBeenCalled();
|
||||
expect(start).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("with number should restart existing stats", async () => {
|
||||
const stats = groupCall.getGroupCallStats();
|
||||
// @ts-ignore
|
||||
const stop = jest.spyOn(stats, "stop");
|
||||
// @ts-ignore
|
||||
const start = jest.spyOn(stats, "start");
|
||||
groupCall.setGroupCallStatsInterval(10000);
|
||||
|
||||
expect(stop).toHaveBeenCalled();
|
||||
expect(start).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ describe("GroupCallStats", () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("starting processing as well without stats collectors", async () => {
|
||||
it("starting processing stats as well without stats collectors", async () => {
|
||||
// @ts-ignore
|
||||
stats.processStats = jest.fn();
|
||||
stats.start();
|
||||
@@ -77,6 +77,16 @@ describe("GroupCallStats", () => {
|
||||
expect(stats.processStats).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("not starting processing stats if interval 0", async () => {
|
||||
const statsDisabled = new GroupCallStats(GROUP_CALL_ID, LOCAL_USER_ID, 0);
|
||||
// @ts-ignore
|
||||
statsDisabled.processStats = jest.fn();
|
||||
statsDisabled.start();
|
||||
jest.advanceTimersByTime(TIME_INTERVAL);
|
||||
// @ts-ignore
|
||||
expect(statsDisabled.processStats).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("starting processing and calling the collectors", async () => {
|
||||
stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection());
|
||||
const collector = stats.getStatsReportGatherer("CALL_ID");
|
||||
|
||||
@@ -43,8 +43,8 @@ describe("StatsReportGatherer", () => {
|
||||
receivedMedia: 0,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: { count: 0, muted: 0 },
|
||||
videoTrackSummary: { count: 0, muted: 0 },
|
||||
audioTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
});
|
||||
expect(collector.getActive()).toBeTruthy();
|
||||
});
|
||||
@@ -74,8 +74,8 @@ describe("StatsReportGatherer", () => {
|
||||
receivedMedia: 0,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: { count: 0, muted: 0 },
|
||||
videoTrackSummary: { count: 0, muted: 0 },
|
||||
audioTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
});
|
||||
expect(getStats).toHaveBeenCalled();
|
||||
expect(collector.getActive()).toBeFalsy();
|
||||
|
||||
@@ -37,29 +37,29 @@ describe("SummaryStatsReporter", () => {
|
||||
receivedMedia: 10,
|
||||
receivedAudioMedia: 4,
|
||||
receivedVideoMedia: 6,
|
||||
audioTrackSummary: { count: 1, muted: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 0 },
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
},
|
||||
{
|
||||
receivedMedia: 13,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 13,
|
||||
audioTrackSummary: { count: 1, muted: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 0 },
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
},
|
||||
{
|
||||
receivedMedia: 0,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: { count: 1, muted: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 0 },
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
},
|
||||
{
|
||||
receivedMedia: 15,
|
||||
receivedAudioMedia: 6,
|
||||
receivedVideoMedia: 9,
|
||||
audioTrackSummary: { count: 1, muted: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 0 },
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
@@ -67,6 +67,8 @@ describe("SummaryStatsReporter", () => {
|
||||
percentageReceivedMedia: 0.5,
|
||||
percentageReceivedAudioMedia: 0.5,
|
||||
percentageReceivedVideoMedia: 0.75,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,8 +78,8 @@ describe("SummaryStatsReporter", () => {
|
||||
receivedMedia: 10,
|
||||
receivedAudioMedia: 10,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: { count: 1, muted: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 1 },
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 1, maxJitter: 0, maxPacketLoss: 0 },
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
@@ -85,6 +87,8 @@ describe("SummaryStatsReporter", () => {
|
||||
percentageReceivedMedia: 1,
|
||||
percentageReceivedAudioMedia: 1,
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,8 +98,8 @@ describe("SummaryStatsReporter", () => {
|
||||
receivedMedia: 10,
|
||||
receivedAudioMedia: 10,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: { count: 1, muted: 0 },
|
||||
videoTrackSummary: { count: 2, muted: 1 },
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 2, muted: 1, maxJitter: 0, maxPacketLoss: 0 },
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
@@ -103,6 +107,8 @@ describe("SummaryStatsReporter", () => {
|
||||
percentageReceivedMedia: 0,
|
||||
percentageReceivedAudioMedia: 1,
|
||||
percentageReceivedVideoMedia: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,8 +118,8 @@ describe("SummaryStatsReporter", () => {
|
||||
receivedMedia: 100,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 100,
|
||||
audioTrackSummary: { count: 1, muted: 1 },
|
||||
videoTrackSummary: { count: 1, muted: 0 },
|
||||
audioTrackSummary: { count: 1, muted: 1, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
@@ -121,6 +127,109 @@ describe("SummaryStatsReporter", () => {
|
||||
percentageReceivedMedia: 0,
|
||||
percentageReceivedAudioMedia: 0,
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("should find max jitter and max packet loss", async () => {
|
||||
const summary = [
|
||||
{
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 1,
|
||||
receivedVideoMedia: 1,
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 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 },
|
||||
},
|
||||
{
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 1,
|
||||
receivedVideoMedia: 1,
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 2, maxPacketLoss: 5 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 2, maxPacketLoss: 5 },
|
||||
},
|
||||
{
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 1,
|
||||
receivedVideoMedia: 1,
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 2, maxPacketLoss: 5 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 40 },
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({
|
||||
percentageReceivedMedia: 1,
|
||||
percentageReceivedAudioMedia: 1,
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 20,
|
||||
maxPacketLoss: 40,
|
||||
});
|
||||
});
|
||||
|
||||
it("as received video Media, if no audio track received should count as received Media", async () => {
|
||||
const summary = [
|
||||
{
|
||||
receivedMedia: 10,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 10,
|
||||
audioTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({
|
||||
percentageReceivedMedia: 1,
|
||||
percentageReceivedAudioMedia: 1,
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("as received audio Media, if no video track received should count as received Media", async () => {
|
||||
const summary = [
|
||||
{
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 22,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: { count: 1, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({
|
||||
percentageReceivedMedia: 1,
|
||||
percentageReceivedAudioMedia: 1,
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("as received no media at all, as received Media", async () => {
|
||||
const summary = [
|
||||
{
|
||||
receivedMedia: 0,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
videoTrackSummary: { count: 0, muted: 0, maxJitter: 0, maxPacketLoss: 0 },
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({
|
||||
percentageReceivedMedia: 1,
|
||||
percentageReceivedAudioMedia: 1,
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -224,24 +224,107 @@ describe("TrackStatsReporter", () => {
|
||||
audioTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 0,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("and returns summary if stats list not empty", async () => {
|
||||
const summary = TrackStatsReporter.buildTrackSummary([]);
|
||||
it("and returns summary if stats list not empty and ignore local summery", async () => {
|
||||
const trackStatsList = buildMockTrackStatsList();
|
||||
const summary = TrackStatsReporter.buildTrackSummary(trackStatsList);
|
||||
expect(summary).toEqual({
|
||||
audioTrackSummary: {
|
||||
count: 0,
|
||||
count: 2,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 0,
|
||||
count: 3,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("and returns summary and count muted if alive", async () => {
|
||||
const trackStatsList = buildMockTrackStatsList();
|
||||
trackStatsList[1].muted = true;
|
||||
trackStatsList[5].muted = true;
|
||||
const summary = TrackStatsReporter.buildTrackSummary(trackStatsList);
|
||||
expect(summary).toEqual({
|
||||
audioTrackSummary: {
|
||||
count: 2,
|
||||
muted: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 3,
|
||||
muted: 1,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("and returns summary and ignore muted if not alive", async () => {
|
||||
const trackStatsList = buildMockTrackStatsList();
|
||||
trackStatsList[1].muted = true;
|
||||
trackStatsList[1].alive = false;
|
||||
const summary = TrackStatsReporter.buildTrackSummary(trackStatsList);
|
||||
expect(summary).toEqual({
|
||||
audioTrackSummary: {
|
||||
count: 2,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 3,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("and returns summary and build max jitter and packet loss", async () => {
|
||||
const trackStatsList = buildMockTrackStatsList();
|
||||
// video remote
|
||||
trackStatsList[1].setJitter(12);
|
||||
trackStatsList[4].setJitter(66);
|
||||
trackStatsList[6].setJitter(1);
|
||||
trackStatsList[1].setLoss({ packetsLost: 55, packetsTotal: 0, isDownloadStream: true });
|
||||
trackStatsList[4].setLoss({ packetsLost: 0, packetsTotal: 0, isDownloadStream: true });
|
||||
trackStatsList[6].setLoss({ packetsLost: 1, packetsTotal: 0, isDownloadStream: true });
|
||||
// audio remote
|
||||
trackStatsList[2].setJitter(1);
|
||||
trackStatsList[5].setJitter(15);
|
||||
trackStatsList[2].setLoss({ packetsLost: 5, packetsTotal: 0, isDownloadStream: true });
|
||||
trackStatsList[5].setLoss({ packetsLost: 0, packetsTotal: 0, isDownloadStream: true });
|
||||
|
||||
const summary = TrackStatsReporter.buildTrackSummary(trackStatsList);
|
||||
expect(summary).toEqual({
|
||||
audioTrackSummary: {
|
||||
count: 2,
|
||||
muted: 0,
|
||||
maxJitter: 15,
|
||||
maxPacketLoss: 5,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 3,
|
||||
muted: 0,
|
||||
maxJitter: 66,
|
||||
maxPacketLoss: 55,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -273,3 +356,28 @@ describe("TrackStatsReporter", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function buildMockTrackStatsList(): MediaTrackStats[] {
|
||||
const trackStats1 = new MediaTrackStats("1", "local", "video");
|
||||
trackStats1.muted = false;
|
||||
trackStats1.alive = true;
|
||||
const trackStats2 = new MediaTrackStats("1", "remote", "video");
|
||||
trackStats2.muted = false;
|
||||
trackStats2.alive = true;
|
||||
const trackStats3 = new MediaTrackStats("1", "remote", "audio");
|
||||
trackStats3.muted = false;
|
||||
trackStats3.alive = true;
|
||||
const trackStats4 = new MediaTrackStats("1", "local", "audio");
|
||||
trackStats4.muted = false;
|
||||
trackStats4.alive = true;
|
||||
const trackStats5 = new MediaTrackStats("1", "remote", "video");
|
||||
trackStats5.muted = false;
|
||||
trackStats5.alive = true;
|
||||
const trackStats6 = new MediaTrackStats("1", "remote", "audio");
|
||||
trackStats6.muted = false;
|
||||
trackStats6.alive = true;
|
||||
const trackStats7 = new MediaTrackStats("1", "remote", "video");
|
||||
trackStats7.muted = false;
|
||||
trackStats7.alive = true;
|
||||
return [trackStats1, trackStats2, trackStats3, trackStats4, trackStats5, trackStats6, trackStats7];
|
||||
}
|
||||
|
||||
@@ -186,6 +186,7 @@ export interface IRelationsRequestOpts {
|
||||
to?: string;
|
||||
limit?: number;
|
||||
dir?: Direction;
|
||||
recurse?: boolean; // MSC3981 Relations Recursion https://github.com/matrix-org/matrix-spec-proposals/pull/3981
|
||||
}
|
||||
|
||||
export interface IRelationsResponse {
|
||||
|
||||
+2
-2
@@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IAuthData } from "../interactive-auth";
|
||||
import { IAuthDict, IAuthData } from "../interactive-auth";
|
||||
|
||||
/**
|
||||
* Helper type to represent HTTP request body for a UIA enabled endpoint
|
||||
*/
|
||||
export type UIARequest<T> = T & {
|
||||
auth?: IAuthData;
|
||||
auth?: IAuthDict;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+53
-26
@@ -715,7 +715,7 @@ interface IJoinedMembersResponse {
|
||||
}
|
||||
|
||||
export interface IRegisterRequestParams {
|
||||
auth?: IAuthData;
|
||||
auth?: IAuthDict;
|
||||
username?: string;
|
||||
password?: string;
|
||||
refresh_token?: boolean;
|
||||
@@ -2434,10 +2434,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns the VerificationRequest that is in progress, if any
|
||||
*/
|
||||
public findVerificationRequestDMInProgress(roomId: string): VerificationRequest | undefined {
|
||||
if (!this.crypto) {
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this.crypto.findVerificationRequestDMInProgress(roomId);
|
||||
return this.cryptoBackend.findVerificationRequestDMInProgress(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2594,10 +2594,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns the cross signing information for the user.
|
||||
*/
|
||||
public getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null {
|
||||
if (!this.crypto) {
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this.crypto.getStoredCrossSigningForUser(userId);
|
||||
return this.cryptoBackend.getStoredCrossSigningForUser(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2621,12 +2621,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @param userId - The ID of the user whose devices is to be checked.
|
||||
* @param deviceId - The ID of the device to check
|
||||
*
|
||||
* @deprecated Use {@link CryptoApi.getDeviceVerificationStatus | `CryptoApi.getDeviceVerificationStatus`}
|
||||
*/
|
||||
public checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel {
|
||||
if (!this.cryptoBackend) {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this.cryptoBackend.checkDeviceTrust(userId, deviceId);
|
||||
return this.crypto.checkDeviceTrust(userId, deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2764,24 +2766,28 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* Default: true
|
||||
*
|
||||
* @returns True if trusting cross-signed devices
|
||||
*
|
||||
* @deprecated Prefer {@link CryptoApi.getTrustCrossSignedDevices | `CryptoApi.getTrustCrossSignedDevices`}.
|
||||
*/
|
||||
public getCryptoTrustCrossSignedDevices(): boolean {
|
||||
if (!this.crypto) {
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this.crypto.getCryptoTrustCrossSignedDevices();
|
||||
return this.cryptoBackend.getTrustCrossSignedDevices();
|
||||
}
|
||||
|
||||
/**
|
||||
* See getCryptoTrustCrossSignedDevices
|
||||
*
|
||||
* @param val - True to trust cross-signed devices
|
||||
*
|
||||
* @deprecated Prefer {@link CryptoApi.setTrustCrossSignedDevices | `CryptoApi.setTrustCrossSignedDevices`}.
|
||||
*/
|
||||
public setCryptoTrustCrossSignedDevices(val: boolean): void {
|
||||
if (!this.crypto) {
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
this.crypto.setCryptoTrustCrossSignedDevices(val);
|
||||
this.cryptoBackend.setTrustCrossSignedDevices(val);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5730,6 +5736,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const recurse = this.canSupport.get(Feature.RelationsRecursion) !== ServerSupport.Unsupported;
|
||||
if (Thread.hasServerSideSupport) {
|
||||
if (Thread.hasServerSideFwdPaginationSupport) {
|
||||
if (!timelineSet.thread) {
|
||||
@@ -5742,14 +5749,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
thread.id,
|
||||
THREAD_RELATION_TYPE.name,
|
||||
null,
|
||||
{ dir: Direction.Backward, from: res.start },
|
||||
{ dir: Direction.Backward, from: res.start, recurse: recurse || undefined },
|
||||
);
|
||||
const resNewer: IRelationsResponse = await this.fetchRelations(
|
||||
timelineSet.room.roomId,
|
||||
thread.id,
|
||||
THREAD_RELATION_TYPE.name,
|
||||
null,
|
||||
{ dir: Direction.Forward, from: res.end },
|
||||
{ dir: Direction.Forward, from: res.end, recurse: recurse || undefined },
|
||||
);
|
||||
const events = [
|
||||
// Order events from most recent to oldest (reverse-chronological).
|
||||
@@ -5797,7 +5804,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
thread.id,
|
||||
THREAD_RELATION_TYPE.name,
|
||||
null,
|
||||
{ dir: Direction.Backward, from: res.start },
|
||||
{ dir: Direction.Backward, from: res.start, recurse: recurse || undefined },
|
||||
);
|
||||
const eventsNewer: IEvent[] = [];
|
||||
let nextBatch: Optional<string> = res.end;
|
||||
@@ -5807,7 +5814,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
thread.id,
|
||||
THREAD_RELATION_TYPE.name,
|
||||
null,
|
||||
{ dir: Direction.Forward, from: nextBatch },
|
||||
{ dir: Direction.Forward, from: nextBatch, recurse: recurse || undefined },
|
||||
);
|
||||
nextBatch = resNewer.next_batch ?? null;
|
||||
eventsNewer.push(...resNewer.chunk);
|
||||
@@ -5878,12 +5885,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
);
|
||||
event = res.chunk?.[0];
|
||||
} else if (timelineSet.thread && Thread.hasServerSideSupport) {
|
||||
const recurse = this.canSupport.get(Feature.RelationsRecursion) !== ServerSupport.Unsupported;
|
||||
const res = await this.fetchRelations(
|
||||
timelineSet.room.roomId,
|
||||
timelineSet.thread.id,
|
||||
THREAD_RELATION_TYPE.name,
|
||||
null,
|
||||
{ dir: Direction.Backward, limit: 1 },
|
||||
{ dir: Direction.Backward, limit: 1, recurse: recurse || undefined },
|
||||
);
|
||||
event = res.chunk?.[0];
|
||||
} else {
|
||||
@@ -6158,10 +6166,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
throw new Error("Unknown room " + eventTimeline.getRoomId());
|
||||
}
|
||||
|
||||
const recurse = this.canSupport.get(Feature.RelationsRecursion) !== ServerSupport.Unsupported;
|
||||
promise = this.fetchRelations(eventTimeline.getRoomId() ?? "", thread.id, THREAD_RELATION_TYPE.name, null, {
|
||||
dir,
|
||||
limit: opts.limit,
|
||||
from: token ?? undefined,
|
||||
recurse: recurse || undefined,
|
||||
})
|
||||
.then(async (res) => {
|
||||
const mapper = this.getEventMapper();
|
||||
@@ -7851,7 +7861,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves: On success, the token response
|
||||
* or UIA auth data.
|
||||
*/
|
||||
public async requestLoginToken(auth?: IAuthData): Promise<UIAResponse<LoginTokenPostResponse>> {
|
||||
public async requestLoginToken(auth?: IAuthDict): Promise<UIAResponse<LoginTokenPostResponse>> {
|
||||
// use capabilities to determine which revision of the MSC is being used
|
||||
const capabilities = await this.getCapabilities();
|
||||
// use r1 endpoint if capability is exposed otherwise use old r0 endpoint
|
||||
@@ -7950,6 +7960,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
if (Thread.hasServerSideFwdPaginationSupport === FeatureSupport.Experimental) {
|
||||
params = replaceParam("dir", "org.matrix.msc3715.dir", params);
|
||||
}
|
||||
if (this.canSupport.get(Feature.RelationsRecursion) === ServerSupport.Unstable) {
|
||||
params = replaceParam("recurse", "org.matrix.msc3981.recurse", params);
|
||||
}
|
||||
const queryString = utils.encodeParams(params);
|
||||
|
||||
let templatedUrl = "/rooms/$roomId/relations/$eventId";
|
||||
@@ -8230,7 +8243,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*/
|
||||
public getRoomIdForAlias(alias: string): Promise<{ room_id: string; servers: string[] }> {
|
||||
// eslint-disable-line camelcase
|
||||
// TODO: deprecate this or resolveRoomAlias
|
||||
const path = utils.encodeUri("/directory/room/$alias", {
|
||||
$alias: alias,
|
||||
});
|
||||
@@ -8240,10 +8252,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
/**
|
||||
* @returns Promise which resolves: Object with room_id and servers.
|
||||
* @returns Rejects: with an error response.
|
||||
* @deprecated use `getRoomIdForAlias` instead
|
||||
*/
|
||||
// eslint-disable-next-line camelcase
|
||||
public resolveRoomAlias(roomAlias: string): Promise<{ room_id: string; servers: string[] }> {
|
||||
// TODO: deprecate this or getRoomIdForAlias
|
||||
const path = utils.encodeUri("/directory/room/$alias", { $alias: roomAlias });
|
||||
return this.http.request(Method.Get, path);
|
||||
}
|
||||
@@ -8615,6 +8627,23 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return this.http.authedRequest(Method.Post, path, undefined, pusher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an existing pusher
|
||||
* @param pushKey - pushkey of pusher to remove
|
||||
* @param appId - app_id of pusher to remove
|
||||
* @returns Promise which resolves: Empty json object on success
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public removePusher(pushKey: string, appId: string): Promise<{}> {
|
||||
const path = "/pushers/set";
|
||||
const body = {
|
||||
pushkey: pushKey,
|
||||
app_id: appId,
|
||||
kind: null, // marks pusher for removal
|
||||
};
|
||||
return this.http.authedRequest(Method.Post, path, undefined, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists local notification settings
|
||||
* @returns Promise which resolves: an empty object
|
||||
@@ -8837,7 +8866,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return this.http.authedRequest(Method.Get, "/keys/changes", qps);
|
||||
}
|
||||
|
||||
public uploadDeviceSigningKeys(auth?: IAuthData, keys?: CrossSigningKeys): Promise<{}> {
|
||||
public uploadDeviceSigningKeys(auth?: IAuthDict, keys?: CrossSigningKeys): Promise<{}> {
|
||||
// API returns empty object
|
||||
const data = Object.assign({}, keys);
|
||||
if (auth) Object.assign(data, { auth });
|
||||
@@ -8986,7 +9015,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param identityAccessToken - The `access_token` field of the Identity
|
||||
* Server `/account/register` response (see {@link registerWithIdentityServer}).
|
||||
*
|
||||
* @returns Promise which resolves: Object, currently with no parameters.
|
||||
* @returns Promise which resolves: Object, containing success boolean.
|
||||
* @returns Rejects: with an error response.
|
||||
* @throws Error if No identity server is set
|
||||
*/
|
||||
@@ -8995,8 +9024,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
clientSecret: string,
|
||||
msisdnToken: string,
|
||||
identityAccessToken: string,
|
||||
): Promise<any> {
|
||||
// TODO: Types
|
||||
): Promise<{ success: boolean }> {
|
||||
const params = {
|
||||
sid: sid,
|
||||
client_secret: clientSecret,
|
||||
@@ -9027,7 +9055,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* This must be the same value submitted in the requestToken call.
|
||||
* @param msisdnToken - The MSISDN token, as enetered by the user.
|
||||
*
|
||||
* @returns Promise which resolves: Object, currently with no parameters.
|
||||
* @returns Promise which resolves: Object, containing success boolean.
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public submitMsisdnTokenOtherUrl(
|
||||
@@ -9035,8 +9063,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
sid: string,
|
||||
clientSecret: string,
|
||||
msisdnToken: string,
|
||||
): Promise<any> {
|
||||
// TODO: Types
|
||||
): Promise<{ success: boolean }> {
|
||||
const params = {
|
||||
sid: sid,
|
||||
client_secret: clientSecret,
|
||||
|
||||
@@ -18,9 +18,10 @@ import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { Room } from "../models/room";
|
||||
import { CryptoApi } from "../crypto-api";
|
||||
import { DeviceTrustLevel, UserTrustLevel } from "../crypto/CrossSigning";
|
||||
import { CrossSigningInfo, UserTrustLevel } from "../crypto/CrossSigning";
|
||||
import { IEncryptedEventInfo } from "../crypto/api";
|
||||
import { IEventDecryptionResult } from "../@types/crypto";
|
||||
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
|
||||
|
||||
/**
|
||||
* Common interface for the crypto implementations
|
||||
@@ -51,16 +52,6 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
|
||||
*/
|
||||
checkUserTrust(userId: string): UserTrustLevel;
|
||||
|
||||
/**
|
||||
* Get the verification level for a given device
|
||||
*
|
||||
* TODO: define this better
|
||||
*
|
||||
* @param userId - user to be checked
|
||||
* @param deviceId - device to be checked
|
||||
*/
|
||||
checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel;
|
||||
|
||||
/**
|
||||
* Encrypt an event according to the configuration of the room.
|
||||
*
|
||||
@@ -87,6 +78,26 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
|
||||
* @param event - event to be checked
|
||||
*/
|
||||
getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo;
|
||||
|
||||
/**
|
||||
* Finds a DM verification request that is already in progress for the given room id
|
||||
*
|
||||
* @param roomId - the room to use for verification
|
||||
*
|
||||
* @returns the VerificationRequest that is in progress, if any
|
||||
*/
|
||||
findVerificationRequestDMInProgress(roomId: string): VerificationRequest | undefined;
|
||||
|
||||
/**
|
||||
* Get the cross signing information for a given user.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @param userId - the user ID to get the cross-signing info for.
|
||||
*
|
||||
* @returns the cross signing information for the user.
|
||||
*/
|
||||
getStoredCrossSigningForUser(userId: string): CrossSigningInfo | null;
|
||||
}
|
||||
|
||||
/** The methods which crypto implementations should expose to the Sync api */
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import type { IMegolmSessionData } from "./@types/crypto";
|
||||
import { Room } from "./models/room";
|
||||
import { DeviceMap } from "./models/device";
|
||||
|
||||
/**
|
||||
* Public interface to the cryptography parts of the js-sdk
|
||||
@@ -72,4 +73,113 @@ export interface CryptoApi {
|
||||
* session export objects
|
||||
*/
|
||||
exportRoomKeys(): Promise<IMegolmSessionData[]>;
|
||||
|
||||
/**
|
||||
* Get the device information for the given list of users.
|
||||
*
|
||||
* For any users whose device lists are cached (due to sharing an encrypted room with the user), the
|
||||
* cached device data is returned.
|
||||
*
|
||||
* If there are uncached users, and the `downloadUncached` parameter is set to `true`,
|
||||
* a `/keys/query` request is made to the server to retrieve these devices.
|
||||
*
|
||||
* @param userIds - The users to fetch.
|
||||
* @param downloadUncached - If true, download the device list for users whose device list we are not
|
||||
* currently tracking. Defaults to false, in which case such users will not appear at all in the result map.
|
||||
*
|
||||
* @returns A map `{@link DeviceMap}`.
|
||||
*/
|
||||
getUserDeviceInfo(userIds: string[], downloadUncached?: boolean): Promise<DeviceMap>;
|
||||
|
||||
/**
|
||||
* Set whether to trust other user's signatures of their devices.
|
||||
*
|
||||
* If false, devices will only be considered 'verified' if we have
|
||||
* verified that device individually (effectively disabling cross-signing).
|
||||
*
|
||||
* `true` by default.
|
||||
*
|
||||
* @param val - the new value
|
||||
*/
|
||||
setTrustCrossSignedDevices(val: boolean): void;
|
||||
|
||||
/**
|
||||
* Return whether we trust other user's signatures of their devices.
|
||||
*
|
||||
* @see {@link CryptoApi#setTrustCrossSignedDevices}
|
||||
*
|
||||
* @returns `true` if we trust cross-signed devices, otherwise `false`.
|
||||
*/
|
||||
getTrustCrossSignedDevices(): boolean;
|
||||
|
||||
/**
|
||||
* Get the verification status of a given device.
|
||||
*
|
||||
* @param userId - The ID of the user whose device is to be checked.
|
||||
* @param deviceId - The ID of the device to check
|
||||
*
|
||||
* @returns Verification status of the device, or `null` if the device is not known
|
||||
*/
|
||||
getDeviceVerificationStatus(userId: string, deviceId: string): Promise<DeviceVerificationStatus | null>;
|
||||
}
|
||||
|
||||
export class DeviceVerificationStatus {
|
||||
/**
|
||||
* True if this device has been signed by its owner (and that signature verified).
|
||||
*
|
||||
* This doesn't necessarily mean that we have verified the device, since we may not have verified the
|
||||
* owner's cross-signing key.
|
||||
*/
|
||||
public readonly signedByOwner: boolean;
|
||||
|
||||
/**
|
||||
* True if this device has been verified via cross signing.
|
||||
*
|
||||
* This does *not* take into account `trustCrossSignedDevices`.
|
||||
*/
|
||||
public readonly crossSigningVerified: boolean;
|
||||
|
||||
/**
|
||||
* TODO: tofu magic wtf does this do?
|
||||
*/
|
||||
public readonly tofu: boolean;
|
||||
|
||||
/**
|
||||
* True if the device has been marked as locally verified.
|
||||
*/
|
||||
public readonly localVerified: boolean;
|
||||
|
||||
/**
|
||||
* True if the client has been configured to trust cross-signed devices via {@link CryptoApi#setTrustCrossSignedDevices}.
|
||||
*/
|
||||
private readonly trustCrossSignedDevices: boolean;
|
||||
|
||||
public constructor(
|
||||
opts: Partial<DeviceVerificationStatus> & {
|
||||
/**
|
||||
* True if cross-signed devices should be considered verified for {@link DeviceVerificationStatus#isVerified}.
|
||||
*/
|
||||
trustCrossSignedDevices?: boolean;
|
||||
},
|
||||
) {
|
||||
this.signedByOwner = opts.signedByOwner ?? false;
|
||||
this.crossSigningVerified = opts.crossSigningVerified ?? false;
|
||||
this.tofu = opts.tofu ?? false;
|
||||
this.localVerified = opts.localVerified ?? false;
|
||||
this.trustCrossSignedDevices = opts.trustCrossSignedDevices ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should consider this device "verified".
|
||||
*
|
||||
* A device is "verified" if either:
|
||||
* * it has been manually marked as such via {@link MatrixClient#setDeviceVerified}.
|
||||
* * it has been cross-signed with a verified signing key, **and** the client has been configured to trust
|
||||
* cross-signed devices via {@link CryptoApi#setTrustCrossSignedDevices}.
|
||||
*
|
||||
* @returns true if this device is verified via any means.
|
||||
*/
|
||||
public isVerified(): boolean {
|
||||
return this.localVerified || (this.trustCrossSignedDevices && this.crossSigningVerified);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-14
@@ -31,6 +31,7 @@ import { ICryptoCallbacks } from ".";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
import { CryptoStore, SecretStorePrivateKeys } from "./store/base";
|
||||
import { ServerSideSecretStorage, SecretStorageKeyDescription } from "../secret-storage";
|
||||
import { DeviceVerificationStatus } from "../crypto-api";
|
||||
|
||||
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
|
||||
|
||||
@@ -628,15 +629,20 @@ export class UserTrustLevel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the ways in which we trust a device
|
||||
* Represents the ways in which we trust a device.
|
||||
*
|
||||
* @deprecated Use {@link DeviceVerificationStatus}.
|
||||
*/
|
||||
export class DeviceTrustLevel {
|
||||
export class DeviceTrustLevel extends DeviceVerificationStatus {
|
||||
public constructor(
|
||||
public readonly crossSigningVerified: boolean,
|
||||
public readonly tofu: boolean,
|
||||
private readonly localVerified: boolean,
|
||||
private readonly trustCrossSignedDevices: boolean,
|
||||
) {}
|
||||
crossSigningVerified: boolean,
|
||||
tofu: boolean,
|
||||
localVerified: boolean,
|
||||
trustCrossSignedDevices: boolean,
|
||||
signedByOwner = false,
|
||||
) {
|
||||
super({ crossSigningVerified, tofu, localVerified, trustCrossSignedDevices, signedByOwner });
|
||||
}
|
||||
|
||||
public static fromUserTrustLevel(
|
||||
userTrustLevel: UserTrustLevel,
|
||||
@@ -648,16 +654,10 @@ export class DeviceTrustLevel {
|
||||
userTrustLevel.isTofu(),
|
||||
localVerified,
|
||||
trustCrossSignedDevices,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if this device is verified via any means
|
||||
*/
|
||||
public isVerified(): boolean {
|
||||
return Boolean(this.isLocallyVerified() || (this.trustCrossSignedDevices && this.isCrossSigningVerified()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if this device is verified via cross signing
|
||||
*/
|
||||
|
||||
@@ -192,12 +192,23 @@ class OlmDecryption extends DecryptionAlgorithm {
|
||||
});
|
||||
}
|
||||
|
||||
// check that the device that encrypted the event belongs to the user
|
||||
// that the event claims it's from. We need to make sure that our
|
||||
// device list is up-to-date. If the device is unknown, we can only
|
||||
// assume that the device logged out. Some event handlers, such as
|
||||
// secret sharing, may be more strict and reject events that come from
|
||||
// unknown devices.
|
||||
// check that the device that encrypted the event belongs to the user that the event claims it's from.
|
||||
//
|
||||
// To do this, we need to make sure that our device list is up-to-date. If the device is unknown, we can only
|
||||
// assume that the device logged out and accept it anyway. Some event handlers, such as secret sharing, may be
|
||||
// more strict and reject events that come from unknown devices.
|
||||
//
|
||||
// This is a defence against the following scenario:
|
||||
//
|
||||
// * Alice has verified Bob and Mallory.
|
||||
// * Mallory gets control of Alice's server, and sends a megolm session to Alice using her (Mallory's)
|
||||
// senderkey, but claiming to be from Bob.
|
||||
// * Mallory sends more events using that session, claiming to be from Bob.
|
||||
// * Alice sees that the senderkey is verified (since she verified Mallory) so marks events those
|
||||
// events as verified even though the sender is forged.
|
||||
//
|
||||
// In practice, it's not clear that the js-sdk would behave that way, so this may be only a defence in depth.
|
||||
|
||||
await this.crypto.deviceList.downloadKeys([event.getSender()!], false);
|
||||
const senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, deviceKey);
|
||||
if (senderKeyUser !== event.getSender() && senderKeyUser != undefined) {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
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 { Device } from "../models/device";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
|
||||
/**
|
||||
* Convert a {@link DeviceInfo} to a {@link Device}.
|
||||
* @param deviceInfo - deviceInfo to convert
|
||||
* @param userId - id of the user that owns the device.
|
||||
*/
|
||||
export function deviceInfoToDevice(deviceInfo: DeviceInfo, userId: string): Device {
|
||||
const keys = new Map<string, string>(Object.entries(deviceInfo.keys));
|
||||
const displayName = deviceInfo.getDisplayName() || undefined;
|
||||
|
||||
const signatures = new Map<string, Map<string, string>>();
|
||||
if (deviceInfo.signatures) {
|
||||
for (const userId in deviceInfo.signatures) {
|
||||
signatures.set(userId, new Map(Object.entries(deviceInfo.signatures[userId])));
|
||||
}
|
||||
}
|
||||
|
||||
return new Device({
|
||||
deviceId: deviceInfo.deviceId,
|
||||
userId: userId,
|
||||
keys,
|
||||
algorithms: deviceInfo.algorithms,
|
||||
verified: deviceInfo.verified,
|
||||
signatures,
|
||||
displayName,
|
||||
});
|
||||
}
|
||||
@@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { ISignatures } from "../@types/signed";
|
||||
import { DeviceVerification } from "../models/device";
|
||||
|
||||
export interface IDevice {
|
||||
keys: Record<string, string>;
|
||||
@@ -25,12 +26,6 @@ export interface IDevice {
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
enum DeviceVerification {
|
||||
Blocked = -1,
|
||||
Unverified = 0,
|
||||
Verified = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about a user's device
|
||||
*/
|
||||
|
||||
+82
-7
@@ -88,6 +88,9 @@ import {
|
||||
ServerSideSecretStorageImpl,
|
||||
} from "../secret-storage";
|
||||
import { ISecretRequest } from "./SecretSharing";
|
||||
import { DeviceVerificationStatus } from "../crypto-api";
|
||||
import { Device, DeviceMap } from "../models/device";
|
||||
import { deviceInfoToDevice } from "./device-converter";
|
||||
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
@@ -605,18 +608,23 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
*
|
||||
* @returns True if trusting cross-signed devices
|
||||
*/
|
||||
public getTrustCrossSignedDevices(): boolean {
|
||||
return this.trustCrossSignedDevices;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link CryptoApi#getTrustCrossSignedDevices}.
|
||||
*/
|
||||
public getCryptoTrustCrossSignedDevices(): boolean {
|
||||
return this.trustCrossSignedDevices;
|
||||
}
|
||||
|
||||
/**
|
||||
* See getCryptoTrustCrossSignedDevices
|
||||
|
||||
* This may be set before initCrypto() is called to ensure no races occur.
|
||||
*
|
||||
* @param val - True to trust cross-signed devices
|
||||
*/
|
||||
public setCryptoTrustCrossSignedDevices(val: boolean): void {
|
||||
public setTrustCrossSignedDevices(val: boolean): void {
|
||||
this.trustCrossSignedDevices = val;
|
||||
|
||||
for (const userId of this.deviceList.getKnownUserIds()) {
|
||||
@@ -634,6 +642,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link CryptoApi#setTrustCrossSignedDevices}.
|
||||
*/
|
||||
public setCryptoTrustCrossSignedDevices(val: boolean): void {
|
||||
this.setTrustCrossSignedDevices(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a recovery key from a user-supplied passphrase.
|
||||
*
|
||||
@@ -1440,10 +1455,22 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
/**
|
||||
* Check whether a given device is trusted.
|
||||
*
|
||||
* @param userId - The ID of the user whose devices is to be checked.
|
||||
* @param userId - The ID of the user whose device is to be checked.
|
||||
* @param deviceId - The ID of the device to check
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
public async getDeviceVerificationStatus(
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
): Promise<DeviceVerificationStatus | null> {
|
||||
const device = this.deviceList.getStoredDevice(userId, deviceId);
|
||||
if (!device) {
|
||||
return null;
|
||||
}
|
||||
return this.checkDeviceInfoTrust(userId, device);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link CryptoApi.getDeviceVerificationStatus}.
|
||||
*/
|
||||
public checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel {
|
||||
const device = this.deviceList.getStoredDevice(userId, deviceId);
|
||||
@@ -1456,7 +1483,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
* @param userId - The ID of the user whose devices is to be checked.
|
||||
* @param device - The device info object to check
|
||||
*
|
||||
* @returns
|
||||
* @deprecated Use {@link CryptoApi.getDeviceVerificationStatus}.
|
||||
*/
|
||||
public checkDeviceInfoTrust(userId: string, device?: DeviceInfo): DeviceTrustLevel {
|
||||
const trustedLocally = !!device?.isVerified();
|
||||
@@ -2038,6 +2065,54 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
return this.deviceList.getStoredDevicesForUser(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the device information for the given list of users.
|
||||
*
|
||||
* @param userIds - The users to fetch.
|
||||
* @param downloadUncached - If true, download the device list for users whose device list we are not
|
||||
* currently tracking. Defaults to false, in which case such users will not appear at all in the result map.
|
||||
*
|
||||
* @returns A map `{@link DeviceMap}`.
|
||||
*/
|
||||
public async getUserDeviceInfo(userIds: string[], downloadUncached = false): Promise<DeviceMap> {
|
||||
const deviceMapByUserId = new Map<string, Map<string, Device>>();
|
||||
// Keep the users without device to download theirs keys
|
||||
const usersWithoutDeviceInfo: string[] = [];
|
||||
|
||||
for (const userId of userIds) {
|
||||
const deviceInfos = await this.getStoredDevicesForUser(userId);
|
||||
// If there are device infos for a userId, we transform it into a map
|
||||
// Else, the keys will be downloaded after
|
||||
if (deviceInfos) {
|
||||
const deviceMap = new Map(
|
||||
// Convert DeviceInfo to Device
|
||||
deviceInfos.map((deviceInfo) => [deviceInfo.deviceId, deviceInfoToDevice(deviceInfo, userId)]),
|
||||
);
|
||||
deviceMapByUserId.set(userId, deviceMap);
|
||||
} else {
|
||||
usersWithoutDeviceInfo.push(userId);
|
||||
}
|
||||
}
|
||||
|
||||
// Download device info for users without device infos
|
||||
if (downloadUncached && usersWithoutDeviceInfo.length > 0) {
|
||||
const newDeviceInfoMap = await this.downloadKeys(usersWithoutDeviceInfo);
|
||||
|
||||
newDeviceInfoMap.forEach((deviceInfoMap, userId) => {
|
||||
const deviceMap = new Map<string, Device>();
|
||||
// Convert DeviceInfo to Device
|
||||
deviceInfoMap.forEach((deviceInfo, deviceId) =>
|
||||
deviceMap.set(deviceId, deviceInfoToDevice(deviceInfo, userId)),
|
||||
);
|
||||
|
||||
// Put the new device infos into the returned map
|
||||
deviceMapByUserId.set(userId, deviceMap);
|
||||
});
|
||||
}
|
||||
|
||||
return deviceMapByUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stored keys for a single device
|
||||
*
|
||||
|
||||
@@ -31,6 +31,7 @@ export enum Feature {
|
||||
LoginTokenRequest = "LoginTokenRequest",
|
||||
RelationBasedRedactions = "RelationBasedRedactions",
|
||||
AccountDataDeletion = "AccountDataDeletion",
|
||||
RelationsRecursion = "RelationsRecursion",
|
||||
}
|
||||
|
||||
type FeatureSupportCondition = {
|
||||
@@ -56,6 +57,9 @@ const featureSupportResolver: Record<string, FeatureSupportCondition> = {
|
||||
[Feature.AccountDataDeletion]: {
|
||||
unstablePrefixes: ["org.matrix.msc3391"],
|
||||
},
|
||||
[Feature.RelationsRecursion]: {
|
||||
unstablePrefixes: ["org.matrix.msc3981"],
|
||||
},
|
||||
};
|
||||
|
||||
export async function buildFeatureSupportMap(versions: IServerVersions): Promise<Map<Feature, ServerSupport>> {
|
||||
|
||||
+14
-2
@@ -44,7 +44,14 @@ export interface IStageStatus {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data returned in the body of a 401 response from a UIA endpoint.
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.6/client-server-api/#user-interactive-api-in-the-rest-api
|
||||
*/
|
||||
export interface IAuthData {
|
||||
// XXX: many of the fields here (`type`, `available_flows`, `required_stages`, etc) look like they
|
||||
// shouldn't be here. They aren't in the spec and it's unclear what they are supposed to do. Be wary of using them.
|
||||
session?: string;
|
||||
type?: string;
|
||||
completed?: string[];
|
||||
@@ -77,6 +84,11 @@ export enum AuthType {
|
||||
UnstableRegistrationToken = "org.matrix.msc3231.login.registration_token",
|
||||
}
|
||||
|
||||
/**
|
||||
* The parameters which are submitted as the `auth` dict in a UIA request
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.6/client-server-api/#authentication-types
|
||||
*/
|
||||
export interface IAuthDict {
|
||||
// [key: string]: any;
|
||||
type?: string;
|
||||
@@ -139,7 +151,7 @@ interface IOpts {
|
||||
* The busyChanged callback should be used instead of the background flag.
|
||||
* Should return a promise which resolves to the successful response or rejects with a MatrixError.
|
||||
*/
|
||||
doRequest(auth: IAuthData | null, background: boolean): Promise<IAuthData>;
|
||||
doRequest(auth: IAuthDict | null, background: boolean): Promise<IAuthData>;
|
||||
/**
|
||||
* Called when the status of the UI auth changes,
|
||||
* ie. when the state of an auth stage changes of when the auth flow moves to a new stage.
|
||||
@@ -441,7 +453,7 @@ export class InteractiveAuth {
|
||||
* This can be set to true for requests that just poll to see if auth has
|
||||
* been completed elsewhere.
|
||||
*/
|
||||
private async doRequest(auth: IAuthData | null, background = false): Promise<void> {
|
||||
private async doRequest(auth: IAuthDict | null, background = false): Promise<void> {
|
||||
try {
|
||||
const result = await this.requestCallback(auth, background);
|
||||
this.attemptAuthDeferred!.resolve(result);
|
||||
|
||||
@@ -38,6 +38,7 @@ export * from "./models/poll";
|
||||
export * from "./models/room-member";
|
||||
export * from "./models/room-state";
|
||||
export * from "./models/user";
|
||||
export * from "./models/device";
|
||||
export * from "./scheduler";
|
||||
export * from "./filter";
|
||||
export * from "./timeline-window";
|
||||
@@ -63,6 +64,7 @@ export type { MatrixCall } from "./webrtc/call";
|
||||
export { GroupCallEvent, GroupCallIntent, GroupCallState, GroupCallType } from "./webrtc/groupCall";
|
||||
export type { GroupCall } from "./webrtc/groupCall";
|
||||
export type { CryptoApi } from "./crypto-api";
|
||||
export { DeviceVerificationStatus } from "./crypto-api";
|
||||
export { CryptoEvent } from "./crypto";
|
||||
|
||||
let cryptoStoreFactory = (): CryptoStore => new MemoryCryptoStore();
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/** State of the verification of the device. */
|
||||
export enum DeviceVerification {
|
||||
Blocked = -1,
|
||||
Unverified = 0,
|
||||
Verified = 1,
|
||||
}
|
||||
|
||||
/** A map from user ID to device ID to Device */
|
||||
export type DeviceMap = Map<string, Map<string, Device>>;
|
||||
|
||||
type DeviceParameters = Pick<Device, "deviceId" | "userId" | "algorithms" | "keys"> & Partial<Device>;
|
||||
|
||||
/**
|
||||
* Information on a user's device, as returned by {@link CryptoApi.getUserDeviceInfo}.
|
||||
*/
|
||||
export class Device {
|
||||
/** id of the device */
|
||||
public readonly deviceId: string;
|
||||
|
||||
/** id of the user that owns the device */
|
||||
public readonly userId: string;
|
||||
|
||||
/** list of algorithms supported by this device */
|
||||
public readonly algorithms: string[];
|
||||
|
||||
/** a map from `<key type>:<id> -> <base64-encoded key>` */
|
||||
public readonly keys: Map<string, string>;
|
||||
|
||||
/** whether the device has been verified/blocked by the user */
|
||||
public readonly verified: DeviceVerification;
|
||||
|
||||
/** a map `<userId, map<algorithm:device_id, signature>>` */
|
||||
public readonly signatures: Map<string, Map<string, string>>;
|
||||
|
||||
/** display name of the device */
|
||||
public readonly displayName?: string;
|
||||
|
||||
public constructor(opts: DeviceParameters) {
|
||||
this.deviceId = opts.deviceId;
|
||||
this.userId = opts.userId;
|
||||
this.algorithms = opts.algorithms;
|
||||
this.keys = opts.keys;
|
||||
this.verified = opts.verified || DeviceVerification.Unverified;
|
||||
this.signatures = opts.signatures || new Map();
|
||||
this.displayName = opts.displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fingerprint for this device (ie, the Ed25519 key)
|
||||
*
|
||||
* @returns base64-encoded fingerprint of this device
|
||||
*/
|
||||
public getFingerprint(): string | undefined {
|
||||
return this.keys.get(`ed25519:${this.deviceId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the identity key for this device (ie, the Curve25519 key)
|
||||
*
|
||||
* @returns base64-encoded identity key of this device
|
||||
*/
|
||||
public getIdentityKey(): string | undefined {
|
||||
return this.keys.get(`curve25519:${this.deviceId}`);
|
||||
}
|
||||
}
|
||||
+64
-8
@@ -368,22 +368,25 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* The room summary.
|
||||
*/
|
||||
public summary: RoomSummary | null = null;
|
||||
// legacy fields
|
||||
/**
|
||||
* The live event timeline for this room, with the oldest event at index 0.
|
||||
* Present for backwards compatibility - prefer getLiveTimeline().getEvents()
|
||||
*
|
||||
* @deprecated Present for backwards compatibility.
|
||||
* Use getLiveTimeline().getEvents() instead
|
||||
*/
|
||||
public timeline!: MatrixEvent[];
|
||||
/**
|
||||
* oldState The state of the room at the time of the oldest
|
||||
* event in the live timeline. Present for backwards compatibility -
|
||||
* prefer getLiveTimeline().getState(EventTimeline.BACKWARDS).
|
||||
* oldState The state of the room at the time of the oldest event in the live timeline.
|
||||
*
|
||||
* @deprecated Present for backwards compatibility.
|
||||
* Use getLiveTimeline().getState(EventTimeline.BACKWARDS) instead
|
||||
*/
|
||||
public oldState!: RoomState;
|
||||
/**
|
||||
* currentState The state of the room at the time of the
|
||||
* newest event in the timeline. Present for backwards compatibility -
|
||||
* prefer getLiveTimeline().getState(EventTimeline.FORWARDS).
|
||||
* currentState The state of the room at the time of the newest event in the timeline.
|
||||
*
|
||||
* @deprecated Present for backwards compatibility.
|
||||
* Use getLiveTimeline().getState(EventTimeline.FORWARDS) instead.
|
||||
*/
|
||||
public currentState!: RoomState;
|
||||
public readonly relations = new RelationsContainer(this.client, this);
|
||||
@@ -393,6 +396,11 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* This is not a comprehensive list of the threads that exist in this room
|
||||
*/
|
||||
private threads = new Map<string, Thread>();
|
||||
|
||||
/**
|
||||
* @deprecated This value is unreliable. It may not contain the last thread.
|
||||
* Use {@link Room.getLastThread} instead.
|
||||
*/
|
||||
public lastThread?: Thread;
|
||||
|
||||
/**
|
||||
@@ -782,6 +790,54 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last live event of this room.
|
||||
* "last" means latest timestamp.
|
||||
* Instead of using timestamps, it would be better to do the comparison based on the order of the homeserver DAG.
|
||||
* Unfortunately, this information is currently not available in the client.
|
||||
* See {@link https://github.com/matrix-org/matrix-js-sdk/issues/3325}.
|
||||
* "live of this room" means from all live timelines: the room and the threads.
|
||||
*
|
||||
* @returns MatrixEvent if there is a last event; else undefined.
|
||||
*/
|
||||
public getLastLiveEvent(): MatrixEvent | undefined {
|
||||
const roomEvents = this.getLiveTimeline().getEvents();
|
||||
const lastRoomEvent = roomEvents[roomEvents.length - 1] as MatrixEvent | undefined;
|
||||
const lastThread = this.getLastThread();
|
||||
|
||||
if (!lastThread) return lastRoomEvent;
|
||||
|
||||
const lastThreadEvent = lastThread.events[lastThread.events.length - 1];
|
||||
|
||||
return (lastRoomEvent?.getTs() ?? 0) > (lastThreadEvent.getTs() ?? 0) ? lastRoomEvent : lastThreadEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last thread of this room.
|
||||
* "last" means latest timestamp of the last thread event.
|
||||
* Instead of using timestamps, it would be better to do the comparison based on the order of the homeserver DAG.
|
||||
* Unfortunately, this information is currently not available in the client.
|
||||
* See {@link https://github.com/matrix-org/matrix-js-sdk/issues/3325}.
|
||||
*
|
||||
* @returns the thread with the most recent event in its live time line. undefined if there is no thread.
|
||||
*/
|
||||
public getLastThread(): Thread | undefined {
|
||||
return this.getThreads().reduce<Thread | undefined>((lastThread: Thread | undefined, thread: Thread) => {
|
||||
if (!lastThread) return thread;
|
||||
|
||||
const threadEvent = thread.events[thread.events.length - 1];
|
||||
const lastThreadEvent = lastThread.events[lastThread.events.length - 1];
|
||||
|
||||
if ((threadEvent?.getTs() ?? 0) >= (lastThreadEvent?.getTs() ?? 0)) {
|
||||
// Last message of current thread is newer → new last thread.
|
||||
// Equal also means newer, because it was added to the thread map later.
|
||||
return thread;
|
||||
}
|
||||
|
||||
return lastThread;
|
||||
}, undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the membership type (join | leave | invite) for the logged in user
|
||||
*/
|
||||
|
||||
+23
-19
@@ -28,6 +28,7 @@ import { ServerControlledNamespacedValue } from "../NamespacedValue";
|
||||
import { logger } from "../logger";
|
||||
import { ReadReceipt } from "./read-receipt";
|
||||
import { CachedReceiptStructure, ReceiptType } from "../@types/read_receipts";
|
||||
import { Feature, ServerSupport } from "../feature";
|
||||
|
||||
export enum ThreadEvent {
|
||||
New = "Thread.new",
|
||||
@@ -458,25 +459,28 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
|
||||
// XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084
|
||||
private async fetchEditsWhereNeeded(...events: MatrixEvent[]): Promise<unknown> {
|
||||
return Promise.all(
|
||||
events
|
||||
.filter((e) => e.isEncrypted())
|
||||
.map((event: MatrixEvent) => {
|
||||
if (event.isRelation()) return; // skip - relations don't get edits
|
||||
return this.client
|
||||
.relations(this.roomId, event.getId()!, RelationType.Replace, event.getType(), {
|
||||
limit: 1,
|
||||
})
|
||||
.then((relations) => {
|
||||
if (relations.events.length) {
|
||||
event.makeReplaced(relations.events[0]);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error("Failed to load edits for encrypted thread event", e);
|
||||
});
|
||||
}),
|
||||
);
|
||||
const recursionSupport = this.client.canSupport.get(Feature.RelationsRecursion) ?? ServerSupport.Unsupported;
|
||||
if (recursionSupport !== ServerSupport.Unsupported) {
|
||||
return Promise.all(
|
||||
events
|
||||
.filter((e) => e.isEncrypted())
|
||||
.map((event: MatrixEvent) => {
|
||||
if (event.isRelation()) return; // skip - relations don't get edits
|
||||
return this.client
|
||||
.relations(this.roomId, event.getId()!, RelationType.Replace, event.getType(), {
|
||||
limit: 1,
|
||||
})
|
||||
.then((relations) => {
|
||||
if (relations.events.length) {
|
||||
event.makeReplaced(relations.events[0]);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error("Failed to load edits for encrypted thread event", e);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public setEventMetadata(event: Optional<MatrixEvent>): void {
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
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 { 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";
|
||||
|
||||
interface AccumulatedReceipt {
|
||||
data: IMinimalEvent;
|
||||
type: ReceiptType;
|
||||
eventId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarises the read receipts within a room. Used by the sync accumulator.
|
||||
*
|
||||
* Given receipts for users, picks the most recently-received one and provides
|
||||
* the results in a new fake receipt event returned from
|
||||
* buildAccumulatedReceiptEvent().
|
||||
*
|
||||
* Handles unthreaded receipts and receipts in each thread separately, so the
|
||||
* returned event contains the most recently received unthreaded receipt, and
|
||||
* the most recently received receipt in each thread.
|
||||
*/
|
||||
export class ReceiptAccumulator {
|
||||
/** user_id -\> most-recently-received unthreaded receipt */
|
||||
private unthreadedReadReceipts: Map<string, AccumulatedReceipt> = new Map();
|
||||
|
||||
/** thread_id -\> user_id -\> most-recently-received receipt for this thread */
|
||||
private threadedReadReceipts: MapWithDefault<string, Map<string, AccumulatedReceipt>> = new MapWithDefault(
|
||||
() => new Map(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Provide an unthreaded receipt for this user. Overwrites any other
|
||||
* unthreaded receipt we have for this user.
|
||||
*/
|
||||
private setUnthreaded(userId: string, receipt: AccumulatedReceipt): void {
|
||||
this.unthreadedReadReceipts.set(userId, receipt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a receipt for this user in this thread. Overwrites any other
|
||||
* receipt we have for this user in this thread.
|
||||
*/
|
||||
private setThreaded(threadId: string, userId: string, receipt: AccumulatedReceipt): void {
|
||||
this.threadedReadReceipts.getOrCreate(threadId).set(userId, receipt);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns an iterator of pairs of [userId, AccumulatedReceipt] - all the
|
||||
* most recently-received unthreaded receipts for each user.
|
||||
*/
|
||||
private allUnthreaded(): IterableIterator<[string, AccumulatedReceipt]> {
|
||||
return this.unthreadedReadReceipts.entries();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns an iterator of pairs of [userId, AccumulatedReceipt] - all the
|
||||
* most recently-received threaded receipts for each user, in all
|
||||
* threads.
|
||||
*/
|
||||
private *allThreaded(): IterableIterator<[string, AccumulatedReceipt]> {
|
||||
for (const receiptsForThread of this.threadedReadReceipts.values()) {
|
||||
for (const e of receiptsForThread.entries()) {
|
||||
yield e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of ephemeral events, find the receipts and store the
|
||||
* relevant ones to be returned later from buildAccumulatedReceiptEvent().
|
||||
*/
|
||||
public consumeEphemeralEvents(events: IMinimalEvent[] | undefined): void {
|
||||
events?.forEach((e) => {
|
||||
if (e.type !== EventType.Receipt || !e.content) {
|
||||
// This means we'll drop unknown ephemeral events but that
|
||||
// seems okay.
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle m.receipt events. They clobber based on:
|
||||
// (user_id, receipt_type)
|
||||
// but they are keyed in the event as:
|
||||
// content:{ $event_id: { $receipt_type: { $user_id: {json} }}}
|
||||
// so store them in the former so we can accumulate receipt deltas
|
||||
// quickly and efficiently (we expect a lot of them). Fold the
|
||||
// receipt type into the key name since we only have 1 at the
|
||||
// moment (m.read) and nested JSON objects are slower and more
|
||||
// of a hassle to work with. We'll inflate this back out when
|
||||
// getJSON() is called.
|
||||
Object.keys(e.content).forEach((eventId) => {
|
||||
Object.entries<ReceiptContent>(e.content[eventId]).forEach(([key, value]) => {
|
||||
if (!isSupportedReceiptType(key)) return;
|
||||
|
||||
for (const userId of Object.keys(value)) {
|
||||
const data = e.content[eventId][key][userId];
|
||||
|
||||
const receipt = {
|
||||
data: e.content[eventId][key][userId],
|
||||
type: key as ReceiptType,
|
||||
eventId,
|
||||
};
|
||||
|
||||
if (!data.thread_id || data.thread_id === MAIN_ROOM_TIMELINE) {
|
||||
this.setUnthreaded(userId, receipt);
|
||||
} else {
|
||||
this.setThreaded(data.thread_id, userId, receipt);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a receipt event that contains all relevant information for this
|
||||
* room, taking the most recently received receipt for each user in an
|
||||
* unthreaded context, and in each thread.
|
||||
*/
|
||||
public buildAccumulatedReceiptEvent(roomId: string): IMinimalEvent | null {
|
||||
const receiptEvent: IMinimalEvent = {
|
||||
type: EventType.Receipt,
|
||||
room_id: roomId,
|
||||
content: {
|
||||
// $event_id: { "m.read": { $user_id: $json } }
|
||||
} as IContent,
|
||||
};
|
||||
|
||||
const receiptEventContent: MapWithDefault<
|
||||
string,
|
||||
MapWithDefault<ReceiptType, Map<string, object>>
|
||||
> = new MapWithDefault(() => new MapWithDefault(() => new Map()));
|
||||
|
||||
for (const [userId, receiptData] of this.allUnthreaded()) {
|
||||
receiptEventContent
|
||||
.getOrCreate(receiptData.eventId)
|
||||
.getOrCreate(receiptData.type)
|
||||
.set(userId, receiptData.data);
|
||||
}
|
||||
|
||||
for (const [userId, receiptData] of this.allThreaded()) {
|
||||
receiptEventContent
|
||||
.getOrCreate(receiptData.eventId)
|
||||
.getOrCreate(receiptData.type)
|
||||
.set(userId, receiptData.data);
|
||||
}
|
||||
|
||||
receiptEvent.content = recursiveMapToObject(receiptEventContent);
|
||||
|
||||
return receiptEventContent.size > 0 ? receiptEvent : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
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 * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
|
||||
import { Device, DeviceVerification } from "../models/device";
|
||||
import { DeviceKeys } from "../client";
|
||||
|
||||
/**
|
||||
* Convert a {@link RustSdkCryptoJs.Device} to a {@link Device}
|
||||
* @param device - Rust Sdk device
|
||||
* @param userId - owner of the device
|
||||
*/
|
||||
export function rustDeviceToJsDevice(device: RustSdkCryptoJs.Device, userId: RustSdkCryptoJs.UserId): Device {
|
||||
// Copy rust device keys to Device.keys
|
||||
const keys = new Map<string, string>();
|
||||
for (const [keyId, key] of device.keys.entries()) {
|
||||
keys.set(keyId.toString(), key.toBase64());
|
||||
}
|
||||
|
||||
// Compute verified from device state
|
||||
let verified: DeviceVerification = DeviceVerification.Unverified;
|
||||
if (device.isBlacklisted()) {
|
||||
verified = DeviceVerification.Blocked;
|
||||
} else if (device.isVerified()) {
|
||||
verified = DeviceVerification.Verified;
|
||||
}
|
||||
|
||||
// Convert rust signatures to Device.signatures
|
||||
const signatures = new Map<string, Map<string, string>>();
|
||||
const mayBeSignatureMap: Map<string, RustSdkCryptoJs.MaybeSignature> | undefined = device.signatures.get(userId);
|
||||
if (mayBeSignatureMap) {
|
||||
const convertedSignatures = new Map<string, string>();
|
||||
// Convert maybeSignatures map to a Map<string, string>
|
||||
for (const [key, value] of mayBeSignatureMap.entries()) {
|
||||
if (value.isValid() && value.signature) {
|
||||
convertedSignatures.set(key, value.signature.toBase64());
|
||||
}
|
||||
}
|
||||
|
||||
signatures.set(userId.toString(), convertedSignatures);
|
||||
}
|
||||
|
||||
// Convert rust algorithms to algorithms
|
||||
const rustAlgorithms: RustSdkCryptoJs.EncryptionAlgorithm[] = device.algorithms;
|
||||
// Use set to ensure that algorithms are not duplicated
|
||||
const algorithms = new Set<string>();
|
||||
rustAlgorithms.forEach((algorithm) => {
|
||||
switch (algorithm) {
|
||||
case RustSdkCryptoJs.EncryptionAlgorithm.MegolmV1AesSha2:
|
||||
algorithms.add("m.megolm.v1.aes-sha2");
|
||||
break;
|
||||
case RustSdkCryptoJs.EncryptionAlgorithm.OlmV1Curve25519AesSha2:
|
||||
default:
|
||||
algorithms.add("m.olm.v1.curve25519-aes-sha2");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return new Device({
|
||||
deviceId: device.deviceId.toString(),
|
||||
userId: userId.toString(),
|
||||
keys,
|
||||
algorithms: Array.from(algorithms),
|
||||
verified,
|
||||
signatures,
|
||||
displayName: device.displayName,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert {@link DeviceKeys} from `/keys/query` request to a `Map<string, Device>`
|
||||
* @param deviceKeys - Device keys object to convert
|
||||
*/
|
||||
export function deviceKeysToDeviceMap(deviceKeys: DeviceKeys): Map<string, Device> {
|
||||
return new Map(
|
||||
Object.entries(deviceKeys).map(([deviceId, device]) => [deviceId, downloadDeviceToJsDevice(device)]),
|
||||
);
|
||||
}
|
||||
|
||||
// Device from `/keys/query` request
|
||||
type QueryDevice = DeviceKeys[keyof DeviceKeys];
|
||||
|
||||
/**
|
||||
* Convert `/keys/query` {@link QueryDevice} device to {@link Device}
|
||||
* @param device - Device from `/keys/query` request
|
||||
*/
|
||||
export function downloadDeviceToJsDevice(device: QueryDevice): Device {
|
||||
const keys = new Map(Object.entries(device.keys));
|
||||
const displayName = device.unsigned?.device_display_name;
|
||||
|
||||
const signatures = new Map<string, Map<string, string>>();
|
||||
if (device.signatures) {
|
||||
for (const userId in device.signatures) {
|
||||
signatures.set(userId, new Map(Object.entries(device.signatures[userId])));
|
||||
}
|
||||
}
|
||||
|
||||
return new Device({
|
||||
deviceId: device.device_id,
|
||||
userId: device.user_id,
|
||||
keys,
|
||||
algorithms: device.algorithms,
|
||||
verified: DeviceVerification.Unverified,
|
||||
signatures,
|
||||
displayName,
|
||||
});
|
||||
}
|
||||
@@ -24,18 +24,23 @@ import { Room } from "../models/room";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
|
||||
import { logger } from "../logger";
|
||||
import { IHttpOpts, MatrixHttpApi } from "../http-api";
|
||||
import { DeviceTrustLevel, UserTrustLevel } from "../crypto/CrossSigning";
|
||||
import { IHttpOpts, MatrixHttpApi, Method } from "../http-api";
|
||||
import { UserTrustLevel } from "../crypto/CrossSigning";
|
||||
import { RoomEncryptor } from "./RoomEncryptor";
|
||||
import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
|
||||
import { KeyClaimManager } from "./KeyClaimManager";
|
||||
import { MapWithDefault } from "../utils";
|
||||
import { DeviceVerificationStatus } from "../crypto-api";
|
||||
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter";
|
||||
import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
|
||||
import { Device, DeviceMap } from "../models/device";
|
||||
|
||||
/**
|
||||
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
|
||||
*/
|
||||
export class RustCrypto implements CryptoBackend {
|
||||
public globalErrorOnUnknownDevices = false;
|
||||
private _trustCrossSignedDevices = true;
|
||||
|
||||
/** whether {@link stop} has been called */
|
||||
private stopped = false;
|
||||
@@ -52,7 +57,7 @@ export class RustCrypto implements CryptoBackend {
|
||||
|
||||
public constructor(
|
||||
private readonly olmMachine: RustSdkCryptoJs.OlmMachine,
|
||||
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
_userId: string,
|
||||
_deviceId: string,
|
||||
) {
|
||||
@@ -130,9 +135,30 @@ export class RustCrypto implements CryptoBackend {
|
||||
return new UserTrustLevel(false, false, false);
|
||||
}
|
||||
|
||||
public checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel {
|
||||
/**
|
||||
* Finds a DM verification request that is already in progress for the given room id
|
||||
*
|
||||
* @param roomId - the room to use for verification
|
||||
*
|
||||
* @returns the VerificationRequest that is in progress, if any
|
||||
*/
|
||||
public findVerificationRequestDMInProgress(roomId: string): undefined {
|
||||
// TODO
|
||||
return new DeviceTrustLevel(false, false, false, false);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cross signing information for a given user.
|
||||
*
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @param userId - the user ID to get the cross-signing info for.
|
||||
*
|
||||
* @returns the cross signing information for the user.
|
||||
*/
|
||||
public getStoredCrossSigningForUser(userId: string): null {
|
||||
// TODO
|
||||
return null;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -165,6 +191,117 @@ export class RustCrypto implements CryptoBackend {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the device information for the given list of users.
|
||||
*
|
||||
* @param userIds - The users to fetch.
|
||||
* @param downloadUncached - If true, download the device list for users whose device list we are not
|
||||
* currently tracking. Defaults to false, in which case such users will not appear at all in the result map.
|
||||
*
|
||||
* @returns A map `{@link DeviceMap}`.
|
||||
*/
|
||||
public async getUserDeviceInfo(userIds: string[], downloadUncached = false): Promise<DeviceMap> {
|
||||
const deviceMapByUserId = new Map<string, Map<string, Device>>();
|
||||
const rustTrackedUsers: Set<RustSdkCryptoJs.UserId> = await this.olmMachine.trackedUsers();
|
||||
|
||||
// Convert RustSdkCryptoJs.UserId to a `Set<string>`
|
||||
const trackedUsers = new Set<string>();
|
||||
rustTrackedUsers.forEach((rustUserId) => trackedUsers.add(rustUserId.toString()));
|
||||
|
||||
// Keep untracked user to download their keys after
|
||||
const untrackedUsers: Set<string> = new Set();
|
||||
|
||||
for (const userId of userIds) {
|
||||
// if this is a tracked user, we can just fetch the device list from the rust-sdk
|
||||
// (NB: this is probably ok even if we race with a leave event such that we stop tracking the user's
|
||||
// devices: the rust-sdk will return the last-known device list, which will be good enough.)
|
||||
if (trackedUsers.has(userId)) {
|
||||
deviceMapByUserId.set(userId, await this.getUserDevices(userId));
|
||||
} else {
|
||||
untrackedUsers.add(userId);
|
||||
}
|
||||
}
|
||||
|
||||
// for any users whose device lists we are not tracking, fall back to downloading the device list
|
||||
// over HTTP.
|
||||
if (downloadUncached && untrackedUsers.size >= 1) {
|
||||
const queryResult = await this.downloadDeviceList(untrackedUsers);
|
||||
Object.entries(queryResult.device_keys).forEach(([userId, deviceKeys]) =>
|
||||
deviceMapByUserId.set(userId, deviceKeysToDeviceMap(deviceKeys)),
|
||||
);
|
||||
}
|
||||
|
||||
return deviceMapByUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the device list for the given user from the olm machine
|
||||
* @param userId - Rust SDK UserId
|
||||
*/
|
||||
private async getUserDevices(userId: string): Promise<Map<string, Device>> {
|
||||
const rustUserId = new RustSdkCryptoJs.UserId(userId);
|
||||
const devices: RustSdkCryptoJs.UserDevices = await this.olmMachine.getUserDevices(rustUserId);
|
||||
return new Map(
|
||||
devices
|
||||
.devices()
|
||||
.map((device: RustSdkCryptoJs.Device) => [
|
||||
device.deviceId.toString(),
|
||||
rustDeviceToJsDevice(device, rustUserId),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the given user keys by calling `/keys/query` request
|
||||
* @param untrackedUsers - download keys of these users
|
||||
*/
|
||||
private async downloadDeviceList(untrackedUsers: Set<string>): Promise<IDownloadKeyResult> {
|
||||
const queryBody: IQueryKeysRequest = { device_keys: {} };
|
||||
untrackedUsers.forEach((user) => (queryBody.device_keys[user] = []));
|
||||
|
||||
return await this.http.authedRequest(Method.Post, "/_matrix/client/v3/keys/query", undefined, queryBody, {
|
||||
prefix: "",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#getTrustCrossSignedDevices}.
|
||||
*/
|
||||
public getTrustCrossSignedDevices(): boolean {
|
||||
return this._trustCrossSignedDevices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#setTrustCrossSignedDevices}.
|
||||
*/
|
||||
public setTrustCrossSignedDevices(val: boolean): void {
|
||||
this._trustCrossSignedDevices = val;
|
||||
// TODO: legacy crypto goes through the list of known devices and emits DeviceVerificationChanged
|
||||
// events. Maybe we need to do the same?
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#getDeviceVerificationStatus}.
|
||||
*/
|
||||
public async getDeviceVerificationStatus(
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
): Promise<DeviceVerificationStatus | null> {
|
||||
const device: RustSdkCryptoJs.Device | undefined = await this.olmMachine.getDevice(
|
||||
new RustSdkCryptoJs.UserId(userId),
|
||||
new RustSdkCryptoJs.DeviceId(deviceId),
|
||||
);
|
||||
|
||||
if (!device) return null;
|
||||
|
||||
return new DeviceVerificationStatus({
|
||||
signedByOwner: device.isCrossSignedByOwner(),
|
||||
crossSigningVerified: device.isCrossSigningTrusted(),
|
||||
localVerified: device.isLocallyTrusted(),
|
||||
trustCrossSignedDevices: this._trustCrossSignedDevices,
|
||||
});
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// SyncCryptoCallbacks implementation
|
||||
|
||||
+3
-3
@@ -282,7 +282,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
|
||||
);
|
||||
if (waitTimeMs === -1) {
|
||||
// give up (you quitter!)
|
||||
debuglog("Queue '%s' giving up on event %s", queueName, obj.event.getId());
|
||||
logger.info("Queue '%s' giving up on event %s", queueName, obj.event.getId());
|
||||
// remove this from the queue
|
||||
this.clearQueue(queueName, err);
|
||||
} else {
|
||||
@@ -298,11 +298,11 @@ export class MatrixScheduler<T = ISendEventResponse> {
|
||||
if (index >= 0) {
|
||||
this.activeQueues.splice(index, 1);
|
||||
}
|
||||
debuglog("Stopping queue '%s' as it is now empty", queueName);
|
||||
logger.info("Stopping queue '%s' as it is now empty", queueName);
|
||||
}
|
||||
|
||||
private clearQueue(queueName: string, err: unknown): void {
|
||||
debuglog("clearing queue '%s'", queueName);
|
||||
logger.info("clearing queue '%s'", queueName);
|
||||
let obj: IQueueEntry<T> | undefined;
|
||||
while ((obj = this.removeNextEvent(queueName))) {
|
||||
obj.defer.reject(err);
|
||||
|
||||
+19
-105
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2017 - 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.
|
||||
@@ -19,12 +19,12 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "./logger";
|
||||
import { deepCopy, isSupportedReceiptType, MapWithDefault, recursiveMapToObject } from "./utils";
|
||||
import { deepCopy } from "./utils";
|
||||
import { IContent, IUnsigned } from "./models/event";
|
||||
import { IRoomSummary } from "./models/room-summary";
|
||||
import { EventType } from "./@types/event";
|
||||
import { MAIN_ROOM_TIMELINE, ReceiptContent, ReceiptType } from "./@types/read_receipts";
|
||||
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync";
|
||||
import { ReceiptAccumulator } from "./receipt-accumulator";
|
||||
|
||||
interface IOpts {
|
||||
/**
|
||||
@@ -40,6 +40,7 @@ interface IOpts {
|
||||
export interface IMinimalEvent {
|
||||
content: IContent;
|
||||
type: EventType | string;
|
||||
room_id?: string;
|
||||
unsigned?: IUnsigned;
|
||||
}
|
||||
|
||||
@@ -167,22 +168,7 @@ interface IRoom {
|
||||
_accountData: { [eventType: string]: IMinimalEvent };
|
||||
_unreadNotifications: Partial<UnreadNotificationCounts>;
|
||||
_unreadThreadNotifications?: Record<string, Partial<UnreadNotificationCounts>>;
|
||||
_readReceipts: {
|
||||
[userId: string]: {
|
||||
data: IMinimalEvent;
|
||||
type: ReceiptType;
|
||||
eventId: string;
|
||||
};
|
||||
};
|
||||
_threadReadReceipts: {
|
||||
[threadId: string]: {
|
||||
[userId: string]: {
|
||||
data: IMinimalEvent;
|
||||
type: ReceiptType;
|
||||
eventId: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
_receipts: ReceiptAccumulator;
|
||||
}
|
||||
|
||||
export interface ISyncData {
|
||||
@@ -387,8 +373,7 @@ export class SyncAccumulator {
|
||||
_unreadNotifications: {},
|
||||
_unreadThreadNotifications: {},
|
||||
_summary: {},
|
||||
_readReceipts: {},
|
||||
_threadReadReceipts: {},
|
||||
_receipts: new ReceiptAccumulator(),
|
||||
};
|
||||
}
|
||||
const currentData = this.joinRooms[roomId];
|
||||
@@ -419,58 +404,17 @@ export class SyncAccumulator {
|
||||
acc[INVITED_COUNT_KEY] = sum[INVITED_COUNT_KEY] || acc[INVITED_COUNT_KEY];
|
||||
}
|
||||
|
||||
data.ephemeral?.events?.forEach((e) => {
|
||||
// We purposefully do not persist m.typing events.
|
||||
// Technically you could refresh a browser before the timer on a
|
||||
// typing event is up, so it'll look like you aren't typing when
|
||||
// you really still are. However, the alternative is worse. If
|
||||
// we do persist typing events, it will look like people are
|
||||
// typing forever until someone really does start typing (which
|
||||
// will prompt Synapse to send down an actual m.typing event to
|
||||
// clobber the one we persisted).
|
||||
if (e.type !== EventType.Receipt || !e.content) {
|
||||
// This means we'll drop unknown ephemeral events but that
|
||||
// seems okay.
|
||||
return;
|
||||
}
|
||||
// Handle m.receipt events. They clobber based on:
|
||||
// (user_id, receipt_type)
|
||||
// but they are keyed in the event as:
|
||||
// content:{ $event_id: { $receipt_type: { $user_id: {json} }}}
|
||||
// so store them in the former so we can accumulate receipt deltas
|
||||
// quickly and efficiently (we expect a lot of them). Fold the
|
||||
// receipt type into the key name since we only have 1 at the
|
||||
// moment (m.read) and nested JSON objects are slower and more
|
||||
// of a hassle to work with. We'll inflate this back out when
|
||||
// getJSON() is called.
|
||||
Object.keys(e.content).forEach((eventId) => {
|
||||
Object.entries<ReceiptContent>(e.content[eventId]).forEach(([key, value]) => {
|
||||
if (!isSupportedReceiptType(key)) return;
|
||||
// We purposefully do not persist m.typing events.
|
||||
// Technically you could refresh a browser before the timer on a
|
||||
// typing event is up, so it'll look like you aren't typing when
|
||||
// you really still are. However, the alternative is worse. If
|
||||
// we do persist typing events, it will look like people are
|
||||
// typing forever until someone really does start typing (which
|
||||
// will prompt Synapse to send down an actual m.typing event to
|
||||
// clobber the one we persisted).
|
||||
|
||||
for (const userId of Object.keys(value)) {
|
||||
const data = e.content[eventId][key][userId];
|
||||
|
||||
const receipt = {
|
||||
data: e.content[eventId][key][userId],
|
||||
type: key as ReceiptType,
|
||||
eventId: eventId,
|
||||
};
|
||||
|
||||
if (!data.thread_id || data.thread_id === MAIN_ROOM_TIMELINE) {
|
||||
currentData._readReceipts[userId] = receipt;
|
||||
} else {
|
||||
currentData._threadReadReceipts = {
|
||||
...currentData._threadReadReceipts,
|
||||
[data.thread_id]: {
|
||||
...(currentData._threadReadReceipts[data.thread_id] ?? {}),
|
||||
[userId]: receipt,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
// Persist the receipts
|
||||
currentData._receipts.consumeEphemeralEvents(data.ephemeral?.events);
|
||||
|
||||
// if we got a limited sync, we need to remove all timeline entries or else
|
||||
// we will have gaps in the timeline.
|
||||
@@ -576,41 +520,11 @@ export class SyncAccumulator {
|
||||
roomJson.account_data.events.push(roomData._accountData[evType]);
|
||||
});
|
||||
|
||||
// Add receipt data
|
||||
const receiptEvent = {
|
||||
type: EventType.Receipt,
|
||||
room_id: roomId,
|
||||
content: {
|
||||
// $event_id: { "m.read": { $user_id: $json } }
|
||||
} as IContent,
|
||||
};
|
||||
|
||||
const receiptEventContent: MapWithDefault<
|
||||
string,
|
||||
MapWithDefault<ReceiptType, Map<string, object>>
|
||||
> = new MapWithDefault(() => new MapWithDefault(() => new Map()));
|
||||
|
||||
for (const [userId, receiptData] of Object.entries(roomData._readReceipts)) {
|
||||
receiptEventContent
|
||||
.getOrCreate(receiptData.eventId)
|
||||
.getOrCreate(receiptData.type)
|
||||
.set(userId, receiptData.data);
|
||||
}
|
||||
|
||||
for (const threadReceipts of Object.values(roomData._threadReadReceipts)) {
|
||||
for (const [userId, receiptData] of Object.entries(threadReceipts)) {
|
||||
receiptEventContent
|
||||
.getOrCreate(receiptData.eventId)
|
||||
.getOrCreate(receiptData.type)
|
||||
.set(userId, receiptData.data);
|
||||
}
|
||||
}
|
||||
|
||||
receiptEvent.content = recursiveMapToObject(receiptEventContent);
|
||||
const receiptEvent = roomData._receipts.buildAccumulatedReceiptEvent(roomId);
|
||||
|
||||
// add only if we have some receipt data
|
||||
if (receiptEventContent.size > 0) {
|
||||
roomJson.ephemeral.events.push(receiptEvent as IMinimalEvent);
|
||||
if (receiptEvent) {
|
||||
roomJson.ephemeral.events.push(receiptEvent);
|
||||
}
|
||||
|
||||
// Add timeline data
|
||||
|
||||
+1
-1
@@ -1525,7 +1525,7 @@ export class SyncApi {
|
||||
}
|
||||
|
||||
// Handle one_time_keys_count and unused fallback keys
|
||||
this.syncOpts.cryptoCallbacks?.processKeyCounts(
|
||||
await this.syncOpts.cryptoCallbacks?.processKeyCounts(
|
||||
data.device_one_time_keys_count,
|
||||
data.device_unused_fallback_key_types ?? data["org.matrix.msc2732.device_unused_fallback_key_types"],
|
||||
);
|
||||
|
||||
@@ -369,9 +369,9 @@ export class TimelineWindow {
|
||||
|
||||
// iterate through each timeline between this.start and this.end
|
||||
// (inclusive).
|
||||
let timeline = this.start.timeline;
|
||||
let timeline: EventTimeline | null = this.start.timeline;
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
while (timeline) {
|
||||
const events = timeline.getEvents();
|
||||
|
||||
// For the first timeline in the chain, we want to start at
|
||||
@@ -399,7 +399,7 @@ export class TimelineWindow {
|
||||
if (timeline === this.end?.timeline) {
|
||||
break;
|
||||
} else {
|
||||
timeline = timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)!;
|
||||
timeline = timeline.getNeighbouringTimeline(EventTimeline.FORWARDS);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -668,7 +668,7 @@ export function recursivelyAssign<T1 extends T2, T2 extends Record<string, any>>
|
||||
continue;
|
||||
}
|
||||
if ((sourceValue !== null && sourceValue !== undefined) || !ignoreNullish) {
|
||||
target[sourceKey as keyof T1] = sourceValue;
|
||||
safeSet(target, sourceKey, sourceValue);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
+13
-4
@@ -397,6 +397,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
// Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example
|
||||
private makingOffer = false;
|
||||
private ignoreOffer = false;
|
||||
private isSettingRemoteAnswerPending = false;
|
||||
|
||||
private responsePromiseChain?: Promise<void>;
|
||||
|
||||
@@ -1895,8 +1896,11 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
}
|
||||
|
||||
try {
|
||||
this.isSettingRemoteAnswerPending = true;
|
||||
await this.peerConn!.setRemoteDescription(content.answer);
|
||||
this.isSettingRemoteAnswerPending = false;
|
||||
} catch (e) {
|
||||
this.isSettingRemoteAnswerPending = false;
|
||||
logger.debug(`Call ${this.callId} onAnswerReceived() failed to set remote description`, e);
|
||||
this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
|
||||
return;
|
||||
@@ -1957,9 +1961,11 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
const polite = this.direction === CallDirection.Inbound;
|
||||
|
||||
// Here we follow the perfect negotiation logic from
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation
|
||||
const offerCollision =
|
||||
description.type === "offer" && (this.makingOffer || this.peerConn!.signalingState !== "stable");
|
||||
// https://w3c.github.io/webrtc-pc/#perfect-negotiation-example
|
||||
const readyForOffer =
|
||||
!this.makingOffer && (this.peerConn!.signalingState === "stable" || this.isSettingRemoteAnswerPending);
|
||||
|
||||
const offerCollision = description.type === "offer" && !readyForOffer;
|
||||
|
||||
this.ignoreOffer = !polite && offerCollision;
|
||||
if (this.ignoreOffer) {
|
||||
@@ -1981,7 +1987,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
}
|
||||
|
||||
try {
|
||||
await this.peerConn!.setRemoteDescription(description);
|
||||
this.isSettingRemoteAnswerPending = description.type == "answer";
|
||||
await this.peerConn!.setRemoteDescription(description); // SRD rolls back as needed
|
||||
this.isSettingRemoteAnswerPending = false;
|
||||
|
||||
if (description.type === "offer") {
|
||||
let answer: RTCSessionDescriptionInit;
|
||||
@@ -2002,6 +2010,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
this.isSettingRemoteAnswerPending = false;
|
||||
logger.warn(`Call ${this.callId} onNegotiateReceived() failed to complete negotiation`, err);
|
||||
}
|
||||
|
||||
|
||||
+39
-11
@@ -236,7 +236,12 @@ export class GroupCall extends TypedEventEmitter<
|
||||
private initWithVideoMuted = false;
|
||||
private initCallFeedPromise?: Promise<void>;
|
||||
|
||||
private readonly stats: GroupCallStats;
|
||||
private stats: GroupCallStats | undefined;
|
||||
/**
|
||||
* Configure default webrtc stats collection interval in ms
|
||||
* Disable collecting webrtc stats by setting interval to 0
|
||||
*/
|
||||
private statsCollectIntervalTime = 0;
|
||||
|
||||
public constructor(
|
||||
private client: MatrixClient,
|
||||
@@ -261,12 +266,6 @@ export class GroupCall extends TypedEventEmitter<
|
||||
this.on(GroupCallEvent.GroupCallStateChanged, this.onStateChanged);
|
||||
this.on(GroupCallEvent.LocalScreenshareStateChanged, this.onLocalFeedsChanged);
|
||||
this.allowCallWithoutVideoAndAudio = !!isCallWithoutVideoAndAudio;
|
||||
|
||||
const userID = this.client.getUserId() || "unknown";
|
||||
this.stats = new GroupCallStats(this.groupCallId, userID);
|
||||
this.stats.reports.on(StatsReport.CONNECTION_STATS, this.onConnectionStats);
|
||||
this.stats.reports.on(StatsReport.BYTE_SENT_STATS, this.onByteSentStats);
|
||||
this.stats.reports.on(StatsReport.SUMMARY_STATS, this.onSummaryStats);
|
||||
}
|
||||
|
||||
private onConnectionStats = (report: ConnectionStatsReport): void => {
|
||||
@@ -553,7 +552,7 @@ export class GroupCall extends TypedEventEmitter<
|
||||
clearInterval(this.retryCallLoopInterval);
|
||||
|
||||
this.client.removeListener(CallEventHandlerEvent.Incoming, this.onIncomingCall);
|
||||
this.stats.stop();
|
||||
this.stats?.stop();
|
||||
}
|
||||
|
||||
public leave(): void {
|
||||
@@ -881,6 +880,11 @@ export class GroupCall extends TypedEventEmitter<
|
||||
);
|
||||
|
||||
if (prevCall) prevCall.hangup(CallErrorCode.Replaced, false);
|
||||
// We must do this before we start initialising / answering the call as we
|
||||
// need to know it is the active call for this user+deviceId and to not ignore
|
||||
// events from it.
|
||||
deviceMap.set(newCall.getOpponentDeviceId()!, newCall);
|
||||
this.calls.set(opponentUserId, deviceMap);
|
||||
|
||||
this.initCall(newCall);
|
||||
|
||||
@@ -895,8 +899,6 @@ export class GroupCall extends TypedEventEmitter<
|
||||
}
|
||||
newCall.answerWithCallFeeds(feeds);
|
||||
|
||||
deviceMap.set(newCall.getOpponentDeviceId()!, newCall);
|
||||
this.calls.set(opponentUserId, deviceMap);
|
||||
this.emit(GroupCallEvent.CallsChanged, this.calls);
|
||||
};
|
||||
|
||||
@@ -1084,7 +1086,7 @@ export class GroupCall extends TypedEventEmitter<
|
||||
|
||||
this.reEmitter.reEmit(call, Object.values(CallEvent));
|
||||
|
||||
call.initStats(this.stats);
|
||||
call.initStats(this.getGroupCallStats());
|
||||
|
||||
onCallFeedsChanged();
|
||||
}
|
||||
@@ -1138,6 +1140,14 @@ export class GroupCall extends TypedEventEmitter<
|
||||
const remoteUsermediaFeed = call.remoteUsermediaFeed;
|
||||
const remoteFeedChanged = remoteUsermediaFeed !== currentUserMediaFeed;
|
||||
|
||||
const deviceMap = this.calls.get(opponentMemberId);
|
||||
const currentCallForUserDevice = deviceMap?.get(opponentDeviceId);
|
||||
if (currentCallForUserDevice?.callId !== call.callId) {
|
||||
// the call in question is not the current call for this user/deviceId
|
||||
// so ignore feed events from it otherwise we'll remove our real feeds
|
||||
return;
|
||||
}
|
||||
|
||||
if (remoteFeedChanged) {
|
||||
if (!currentUserMediaFeed && remoteUsermediaFeed) {
|
||||
this.addUserMediaFeed(remoteUsermediaFeed);
|
||||
@@ -1598,6 +1608,24 @@ export class GroupCall extends TypedEventEmitter<
|
||||
};
|
||||
|
||||
public getGroupCallStats(): GroupCallStats {
|
||||
if (this.stats === undefined) {
|
||||
const userID = this.client.getUserId() || "unknown";
|
||||
this.stats = new GroupCallStats(this.groupCallId, userID, this.statsCollectIntervalTime);
|
||||
this.stats.reports.on(StatsReport.CONNECTION_STATS, this.onConnectionStats);
|
||||
this.stats.reports.on(StatsReport.BYTE_SENT_STATS, this.onByteSentStats);
|
||||
this.stats.reports.on(StatsReport.SUMMARY_STATS, this.onSummaryStats);
|
||||
}
|
||||
return this.stats;
|
||||
}
|
||||
|
||||
public setGroupCallStatsInterval(interval: number): void {
|
||||
this.statsCollectIntervalTime = interval;
|
||||
if (this.stats !== undefined) {
|
||||
this.stats.stop();
|
||||
this.stats.setInterval(interval);
|
||||
if (interval > 0) {
|
||||
this.stats.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export class GroupCallStats {
|
||||
public constructor(private groupCallId: string, private userId: string, private interval: number = 10000) {}
|
||||
|
||||
public start(): void {
|
||||
if (this.timer === undefined) {
|
||||
if (this.timer === undefined && this.interval > 0) {
|
||||
this.timer = setInterval(() => {
|
||||
this.processStats();
|
||||
}, this.interval);
|
||||
@@ -69,4 +69,8 @@ export class GroupCallStats {
|
||||
|
||||
Promise.all(summary).then((s: Awaited<SummaryStats>[]) => this.summaryStatsReporter.build(s));
|
||||
}
|
||||
|
||||
public setInterval(interval: number): void {
|
||||
this.interval = interval;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,4 +68,6 @@ export interface SummaryStatsReport {
|
||||
percentageReceivedMedia: number;
|
||||
percentageReceivedAudioMedia: number;
|
||||
percentageReceivedVideoMedia: number;
|
||||
maxJitter: number;
|
||||
maxPacketLoss: number;
|
||||
}
|
||||
|
||||
@@ -53,8 +53,8 @@ export class StatsReportGatherer {
|
||||
receivedMedia: 0,
|
||||
receivedAudioMedia: 0,
|
||||
receivedVideoMedia: 0,
|
||||
audioTrackSummary: { count: 0, muted: 0 },
|
||||
videoTrackSummary: { count: 0, muted: 0 },
|
||||
audioTrackSummary: { count: 0, muted: 0, maxPacketLoss: 0, maxJitter: 0 },
|
||||
videoTrackSummary: { count: 0, muted: 0, maxPacketLoss: 0, maxJitter: 0 },
|
||||
} as SummaryStats;
|
||||
if (this.isActive) {
|
||||
const statsPromise = this.pc.getStats();
|
||||
|
||||
@@ -21,4 +21,6 @@ export interface SummaryStats {
|
||||
export interface TrackSummary {
|
||||
count: number;
|
||||
muted: number;
|
||||
maxJitter: number;
|
||||
maxPacketLoss: number;
|
||||
}
|
||||
|
||||
@@ -14,48 +14,81 @@ import { StatsReportEmitter } from "./statsReportEmitter";
|
||||
import { SummaryStats } from "./summaryStats";
|
||||
import { SummaryStatsReport } from "./statsReport";
|
||||
|
||||
interface ReceivedMedia {
|
||||
audio: number;
|
||||
video: number;
|
||||
media: number;
|
||||
}
|
||||
|
||||
export class SummaryStatsReporter {
|
||||
public constructor(private emitter: StatsReportEmitter) {}
|
||||
|
||||
public build(summary: SummaryStats[]): void {
|
||||
const entirety = summary.length;
|
||||
if (entirety === 0) {
|
||||
const entiretyTracksCount = summary.length;
|
||||
if (entiretyTracksCount === 0) {
|
||||
return;
|
||||
}
|
||||
let receivedMedia = 0;
|
||||
let receivedVideoMedia = 0;
|
||||
let receivedAudioMedia = 0;
|
||||
const receivedCounter: ReceivedMedia = { audio: 0, video: 0, media: 0 };
|
||||
let maxJitter = 0;
|
||||
let maxPacketLoss = 0;
|
||||
|
||||
summary.forEach((stats) => {
|
||||
let hasReceivedAudio = false;
|
||||
let hasReceivedVideo = false;
|
||||
if (stats.receivedAudioMedia > 0) {
|
||||
receivedAudioMedia++;
|
||||
hasReceivedAudio = true;
|
||||
}
|
||||
if (stats.receivedVideoMedia > 0) {
|
||||
receivedVideoMedia++;
|
||||
hasReceivedVideo = true;
|
||||
} else {
|
||||
if (
|
||||
stats.videoTrackSummary.muted > 0 &&
|
||||
stats.videoTrackSummary.muted === stats.videoTrackSummary.count
|
||||
) {
|
||||
receivedVideoMedia++;
|
||||
hasReceivedVideo = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (stats.receivedMedia > 0 && hasReceivedVideo && hasReceivedAudio) {
|
||||
receivedMedia++;
|
||||
}
|
||||
this.countTrackListReceivedMedia(receivedCounter, stats);
|
||||
maxJitter = this.buildMaxJitter(maxJitter, stats);
|
||||
maxPacketLoss = this.buildMaxPacketLoss(maxPacketLoss, stats);
|
||||
});
|
||||
|
||||
const report = {
|
||||
percentageReceivedMedia: Math.round((receivedMedia / entirety) * 100) / 100,
|
||||
percentageReceivedVideoMedia: Math.round((receivedVideoMedia / entirety) * 100) / 100,
|
||||
percentageReceivedAudioMedia: Math.round((receivedAudioMedia / entirety) * 100) / 100,
|
||||
percentageReceivedMedia: Math.round((receivedCounter.media / entiretyTracksCount) * 100) / 100,
|
||||
percentageReceivedVideoMedia: Math.round((receivedCounter.video / entiretyTracksCount) * 100) / 100,
|
||||
percentageReceivedAudioMedia: Math.round((receivedCounter.audio / entiretyTracksCount) * 100) / 100,
|
||||
maxJitter,
|
||||
maxPacketLoss,
|
||||
} as SummaryStatsReport;
|
||||
this.emitter.emitSummaryStatsReport(report);
|
||||
}
|
||||
|
||||
private countTrackListReceivedMedia(counter: ReceivedMedia, stats: SummaryStats): void {
|
||||
let hasReceivedAudio = false;
|
||||
let hasReceivedVideo = false;
|
||||
if (stats.receivedAudioMedia > 0 || stats.audioTrackSummary.count === 0) {
|
||||
counter.audio++;
|
||||
hasReceivedAudio = true;
|
||||
}
|
||||
if (stats.receivedVideoMedia > 0 || stats.videoTrackSummary.count === 0) {
|
||||
counter.video++;
|
||||
hasReceivedVideo = true;
|
||||
} else {
|
||||
if (stats.videoTrackSummary.muted > 0 && stats.videoTrackSummary.muted === stats.videoTrackSummary.count) {
|
||||
counter.video++;
|
||||
hasReceivedVideo = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasReceivedVideo && hasReceivedAudio) {
|
||||
counter.media++;
|
||||
}
|
||||
}
|
||||
|
||||
private buildMaxJitter(maxJitter: number, stats: SummaryStats): number {
|
||||
if (maxJitter < stats.videoTrackSummary.maxJitter) {
|
||||
maxJitter = stats.videoTrackSummary.maxJitter;
|
||||
}
|
||||
|
||||
if (maxJitter < stats.audioTrackSummary.maxJitter) {
|
||||
maxJitter = stats.audioTrackSummary.maxJitter;
|
||||
}
|
||||
return maxJitter;
|
||||
}
|
||||
|
||||
private buildMaxPacketLoss(maxPacketLoss: number, stats: SummaryStats): number {
|
||||
if (maxPacketLoss < stats.videoTrackSummary.maxPacketLoss) {
|
||||
maxPacketLoss = stats.videoTrackSummary.maxPacketLoss;
|
||||
}
|
||||
|
||||
if (maxPacketLoss < stats.audioTrackSummary.maxPacketLoss) {
|
||||
maxPacketLoss = stats.audioTrackSummary.maxPacketLoss;
|
||||
}
|
||||
return maxPacketLoss;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,8 +140,8 @@ export class TrackStatsReporter {
|
||||
audioTrackSummary: TrackSummary;
|
||||
videoTrackSummary: TrackSummary;
|
||||
} {
|
||||
const audioTrackSummary = { count: 0, muted: 0 };
|
||||
const videoTrackSummary = { count: 0, muted: 0 };
|
||||
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) => {
|
||||
@@ -150,6 +150,12 @@ export class TrackStatsReporter {
|
||||
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;
|
||||
}
|
||||
});
|
||||
return { audioTrackSummary, videoTrackSummary };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"plugin": ["typedoc-plugin-mdn-links", "typedoc-plugin-missing-exports", "typedoc-plugin-versions"]
|
||||
}
|
||||
@@ -1060,13 +1060,13 @@
|
||||
dependencies:
|
||||
"@jridgewell/trace-mapping" "0.3.9"
|
||||
|
||||
"@es-joy/jsdoccomment@~0.37.0":
|
||||
version "0.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.37.0.tgz#aaafb4bb6c88288aa7899aef0f3b1b851c36f908"
|
||||
integrity sha512-hjK0wnsPCYLlF+HHB4R/RbUjOWeLW2SlarB67+Do5WsKILOkmIZvvPJFbtWSmbypxcjpoECLAMzoao0D4Bg5ZQ==
|
||||
"@es-joy/jsdoccomment@~0.37.1":
|
||||
version "0.37.1"
|
||||
resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.37.1.tgz#fa32a41ba12097452693343e09ad4d26d157aedd"
|
||||
integrity sha512-5vxWJ1gEkEF0yRd0O+uK6dHJf7adrxwQSX8PuRiPfFSAbNLnY0ZJfXaZucoz14Jj2N11xn2DnlEPwWRpYpvRjg==
|
||||
dependencies:
|
||||
comment-parser "1.3.1"
|
||||
esquery "^1.4.0"
|
||||
esquery "^1.5.0"
|
||||
jsdoc-type-pratt-parser "~4.0.0"
|
||||
|
||||
"@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0":
|
||||
@@ -1096,10 +1096,10 @@
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@8.37.0":
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.37.0.tgz#cf1b5fa24217fe007f6487a26d765274925efa7d"
|
||||
integrity sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A==
|
||||
"@eslint/js@8.39.0":
|
||||
version "8.39.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.39.0.tgz#58b536bcc843f4cd1e02a7e6171da5c040f4d44b"
|
||||
integrity sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==
|
||||
|
||||
"@humanwhocodes/config-array@^0.11.8":
|
||||
version "0.11.8"
|
||||
@@ -1419,10 +1419,10 @@
|
||||
dependencies:
|
||||
lodash "^4.17.21"
|
||||
|
||||
"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.6":
|
||||
version "0.1.0-alpha.6"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.6.tgz#c0bdb9ab0d30179b8ef744d1b4010b0ad0ab9c3a"
|
||||
integrity sha512-7hMffzw7KijxDyyH/eUyTfrLeCQHuyU3kaPOKGhcl3DZ3vx7bCncqjGMGTnxNPoP23I6gosvKSbO+3wYOT24Xg==
|
||||
"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.7":
|
||||
version "0.1.0-alpha.7"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.7.tgz#136375b84fd8a7e698f70fc969f668e541a61313"
|
||||
integrity sha512-sQEG9cSfNji5NYBf5h7j5IxYVO0dwtAKoetaVyR+LhIXz/Su7zyEE3EwlAWAeJOFdAV/vZ5LTNyh39xADuNlTg==
|
||||
|
||||
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
|
||||
version "3.2.14"
|
||||
@@ -1835,14 +1835,14 @@
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.45.0":
|
||||
version "5.57.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.57.1.tgz#d1ab162a3cd2671b8a1c9ddf6e2db73b14439735"
|
||||
integrity sha512-1MeobQkQ9tztuleT3v72XmY0XuKXVXusAhryoLuU5YZ+mXoYKZP9SQ7Flulh1NX4DTjpGTc2b/eMu4u7M7dhnQ==
|
||||
version "5.59.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.0.tgz#c0e10eeb936debe5d1c3433cf36206a95befefd0"
|
||||
integrity sha512-p0QgrEyrxAWBecR56gyn3wkG15TJdI//eetInP3zYRewDh0XS+DhB3VUAd3QqvziFsfaQIoIuZMxZRB7vXYaYw==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.4.0"
|
||||
"@typescript-eslint/scope-manager" "5.57.1"
|
||||
"@typescript-eslint/type-utils" "5.57.1"
|
||||
"@typescript-eslint/utils" "5.57.1"
|
||||
"@typescript-eslint/scope-manager" "5.59.0"
|
||||
"@typescript-eslint/type-utils" "5.59.0"
|
||||
"@typescript-eslint/utils" "5.59.0"
|
||||
debug "^4.3.4"
|
||||
grapheme-splitter "^1.0.4"
|
||||
ignore "^5.2.0"
|
||||
@@ -1851,13 +1851,13 @@
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/parser@^5.45.0":
|
||||
version "5.57.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.57.1.tgz#af911234bd4401d09668c5faf708a0570a17a748"
|
||||
integrity sha512-hlA0BLeVSA/wBPKdPGxoVr9Pp6GutGoY380FEhbVi0Ph4WNe8kLvqIRx76RSQt1lynZKfrXKs0/XeEk4zZycuA==
|
||||
version "5.59.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.0.tgz#0ad7cd019346cc5d150363f64869eca10ca9977c"
|
||||
integrity sha512-qK9TZ70eJtjojSUMrrEwA9ZDQ4N0e/AuoOIgXuNBorXYcBDk397D2r5MIe1B3cok/oCtdNC5j+lUUpVB+Dpb+w==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.57.1"
|
||||
"@typescript-eslint/types" "5.57.1"
|
||||
"@typescript-eslint/typescript-estree" "5.57.1"
|
||||
"@typescript-eslint/scope-manager" "5.59.0"
|
||||
"@typescript-eslint/types" "5.59.0"
|
||||
"@typescript-eslint/typescript-estree" "5.59.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.57.1":
|
||||
@@ -1868,13 +1868,21 @@
|
||||
"@typescript-eslint/types" "5.57.1"
|
||||
"@typescript-eslint/visitor-keys" "5.57.1"
|
||||
|
||||
"@typescript-eslint/type-utils@5.57.1":
|
||||
version "5.57.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.57.1.tgz#235daba621d3f882b8488040597b33777c74bbe9"
|
||||
integrity sha512-/RIPQyx60Pt6ga86hKXesXkJ2WOS4UemFrmmq/7eOyiYjYv/MUSHPlkhU6k9T9W1ytnTJueqASW+wOmW4KrViw==
|
||||
"@typescript-eslint/scope-manager@5.59.0":
|
||||
version "5.59.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.0.tgz#86501d7a17885710b6716a23be2e93fc54a4fe8c"
|
||||
integrity sha512-tsoldKaMh7izN6BvkK6zRMINj4Z2d6gGhO2UsI8zGZY3XhLq1DndP3Ycjhi1JwdwPRwtLMW4EFPgpuKhbCGOvQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "5.57.1"
|
||||
"@typescript-eslint/utils" "5.57.1"
|
||||
"@typescript-eslint/types" "5.59.0"
|
||||
"@typescript-eslint/visitor-keys" "5.59.0"
|
||||
|
||||
"@typescript-eslint/type-utils@5.59.0":
|
||||
version "5.59.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.0.tgz#8e8d1420fc2265989fa3a0d897bde37f3851e8c9"
|
||||
integrity sha512-d/B6VSWnZwu70kcKQSCqjcXpVH+7ABKH8P1KNn4K7j5PXXuycZTPXF44Nui0TEm6rbWGi8kc78xRgOC4n7xFgA==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "5.59.0"
|
||||
"@typescript-eslint/utils" "5.59.0"
|
||||
debug "^4.3.4"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
@@ -1883,6 +1891,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.57.1.tgz#d9989c7a9025897ea6f0550b7036027f69e8a603"
|
||||
integrity sha512-bSs4LOgyV3bJ08F5RDqO2KXqg3WAdwHCu06zOqcQ6vqbTJizyBhuh1o1ImC69X4bV2g1OJxbH71PJqiO7Y1RuA==
|
||||
|
||||
"@typescript-eslint/types@5.59.0":
|
||||
version "5.59.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.0.tgz#3fcdac7dbf923ec5251545acdd9f1d42d7c4fe32"
|
||||
integrity sha512-yR2h1NotF23xFFYKHZs17QJnB51J/s+ud4PYU4MqdZbzeNxpgUr05+dNeCN/bb6raslHvGdd6BFCkVhpPk/ZeA==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.57.1":
|
||||
version "5.57.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.57.1.tgz#10d9643e503afc1ca4f5553d9bbe672ea4050b71"
|
||||
@@ -1896,7 +1909,34 @@
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/utils@5.57.1", "@typescript-eslint/utils@^5.10.0":
|
||||
"@typescript-eslint/typescript-estree@5.59.0":
|
||||
version "5.59.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.0.tgz#8869156ee1dcfc5a95be3ed0e2809969ea28e965"
|
||||
integrity sha512-sUNnktjmI8DyGzPdZ8dRwW741zopGxltGs/SAPgGL/AAgDpiLsCFLcMNSpbfXfmnNeHmK9h3wGmCkGRGAoUZAg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.59.0"
|
||||
"@typescript-eslint/visitor-keys" "5.59.0"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/utils@5.59.0":
|
||||
version "5.59.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.0.tgz#063d066b3bc4850c18872649ed0da9ee72d833d5"
|
||||
integrity sha512-GGLFd+86drlHSvPgN/el6dRQNYYGOvRSDVydsUaQluwIW3HvbXuxyuD5JETvBt/9qGYe+lOrDk6gRrWOHb/FvA==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@types/json-schema" "^7.0.9"
|
||||
"@types/semver" "^7.3.12"
|
||||
"@typescript-eslint/scope-manager" "5.59.0"
|
||||
"@typescript-eslint/types" "5.59.0"
|
||||
"@typescript-eslint/typescript-estree" "5.59.0"
|
||||
eslint-scope "^5.1.1"
|
||||
semver "^7.3.7"
|
||||
|
||||
"@typescript-eslint/utils@^5.10.0":
|
||||
version "5.57.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.57.1.tgz#0f97b0bbd88c2d5e2036869f26466be5f4c69475"
|
||||
integrity sha512-kN6vzzf9NkEtawECqze6v99LtmDiUJCVpvieTFA1uL7/jDghiJGubGZ5csicYHU1Xoqb3oH/R5cN5df6W41Nfg==
|
||||
@@ -1918,6 +1958,14 @@
|
||||
"@typescript-eslint/types" "5.57.1"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.59.0":
|
||||
version "5.59.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.0.tgz#a59913f2bf0baeb61b5cfcb6135d3926c3854365"
|
||||
integrity sha512-qZ3iXxQhanchCeaExlKPV3gDQFxMUmU35xfd5eCXB6+kUw1TUAbIy2n7QIrwz9s98DQLzNWyHp61fY0da4ZcbA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.59.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
JSONStream@^1.0.3:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
|
||||
@@ -2094,6 +2142,11 @@ anymatch@^3.0.3, anymatch@~3.1.2:
|
||||
normalize-path "^3.0.0"
|
||||
picomatch "^2.0.4"
|
||||
|
||||
are-docs-informative@^0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/are-docs-informative/-/are-docs-informative-0.0.2.tgz#387f0e93f5d45280373d387a59d34c96db321963"
|
||||
integrity sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==
|
||||
|
||||
arg@^4.1.0:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
|
||||
@@ -3478,17 +3531,18 @@ eslint-plugin-jest@^27.1.6:
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "^5.10.0"
|
||||
|
||||
eslint-plugin-jsdoc@^40.0.0:
|
||||
version "40.1.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-40.1.2.tgz#177bc131488157e4aae247df1aa1fdf9f4296842"
|
||||
integrity sha512-U4Kt42OVjF0EXOWPEc8pjanT8O1ULvILwgA5k87CnhrCKG4xaJ8Sjsb6CWgDtaemOywN06u86duKU1yMaBp7IQ==
|
||||
eslint-plugin-jsdoc@^43.0.6:
|
||||
version "43.1.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-43.1.1.tgz#fc72ba21597cc99b1a0dc988aebb9bb57d0ec492"
|
||||
integrity sha512-J2kjjsJ5vBXSyNzqJhceeSGTAgVgZHcPSJKo3vD4tNjUdfky98rR2VfZUDsS1GKL6isyVa8GWvr+Az7Vyg2HXA==
|
||||
dependencies:
|
||||
"@es-joy/jsdoccomment" "~0.37.0"
|
||||
"@es-joy/jsdoccomment" "~0.37.1"
|
||||
are-docs-informative "^0.0.2"
|
||||
comment-parser "1.3.1"
|
||||
debug "^4.3.4"
|
||||
escape-string-regexp "^4.0.0"
|
||||
esquery "^1.5.0"
|
||||
semver "^7.3.8"
|
||||
semver "^7.5.0"
|
||||
spdx-expression-parse "^3.0.1"
|
||||
|
||||
eslint-plugin-matrix-org@^1.0.0:
|
||||
@@ -3539,10 +3593,10 @@ eslint-scope@5.1.1, eslint-scope@^5.1.1:
|
||||
esrecurse "^4.3.0"
|
||||
estraverse "^4.1.1"
|
||||
|
||||
eslint-scope@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642"
|
||||
integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==
|
||||
eslint-scope@^7.2.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b"
|
||||
integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==
|
||||
dependencies:
|
||||
esrecurse "^4.3.0"
|
||||
estraverse "^5.2.0"
|
||||
@@ -3557,15 +3611,15 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.0:
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz#c7f0f956124ce677047ddbc192a68f999454dedc"
|
||||
integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==
|
||||
|
||||
eslint@8.37.0:
|
||||
version "8.37.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.37.0.tgz#1f660ef2ce49a0bfdec0b0d698e0b8b627287412"
|
||||
integrity sha512-NU3Ps9nI05GUoVMxcZx1J8CNR6xOvUT4jAUMH5+z8lpp3aEdPVCImKw6PWG4PY+Vfkpr+jvMpxs/qoE7wq0sPw==
|
||||
eslint@8.39.0:
|
||||
version "8.39.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.39.0.tgz#7fd20a295ef92d43809e914b70c39fd5a23cf3f1"
|
||||
integrity sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@eslint-community/regexpp" "^4.4.0"
|
||||
"@eslint/eslintrc" "^2.0.2"
|
||||
"@eslint/js" "8.37.0"
|
||||
"@eslint/js" "8.39.0"
|
||||
"@humanwhocodes/config-array" "^0.11.8"
|
||||
"@humanwhocodes/module-importer" "^1.0.1"
|
||||
"@nodelib/fs.walk" "^1.2.8"
|
||||
@@ -3575,7 +3629,7 @@ eslint@8.37.0:
|
||||
debug "^4.3.2"
|
||||
doctrine "^3.0.0"
|
||||
escape-string-regexp "^4.0.0"
|
||||
eslint-scope "^7.1.1"
|
||||
eslint-scope "^7.2.0"
|
||||
eslint-visitor-keys "^3.4.0"
|
||||
espree "^9.5.1"
|
||||
esquery "^1.4.2"
|
||||
@@ -3898,6 +3952,15 @@ form-data@^4.0.0:
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
fs-extra@^10.1.0:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
|
||||
integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.0"
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
|
||||
fs-readdir-recursive@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27"
|
||||
@@ -4088,7 +4151,7 @@ gopd@^1.0.1:
|
||||
dependencies:
|
||||
get-intrinsic "^1.1.3"
|
||||
|
||||
graceful-fs@^4.2.4, graceful-fs@^4.2.9:
|
||||
graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9:
|
||||
version "4.2.11"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||
@@ -5165,6 +5228,15 @@ jsonc-parser@^3.2.0:
|
||||
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76"
|
||||
integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==
|
||||
|
||||
jsonfile@^6.0.1:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
|
||||
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
|
||||
dependencies:
|
||||
universalify "^2.0.0"
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
jsonparse@^1.2.0:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
|
||||
@@ -5379,7 +5451,7 @@ makeerror@1.0.12:
|
||||
dependencies:
|
||||
tmpl "1.0.5"
|
||||
|
||||
marked@^4.2.12:
|
||||
marked@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3"
|
||||
integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==
|
||||
@@ -5500,13 +5572,6 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatc
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^7.1.3:
|
||||
version "7.4.6"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.6.tgz#845d6f254d8f4a5e4fd6baf44d5f10c8448365fb"
|
||||
integrity sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^8.0.2:
|
||||
version "8.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229"
|
||||
@@ -5514,6 +5579,13 @@ minimatch@^8.0.2:
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.0.tgz#bfc8e88a1c40ffd40c172ddac3decb8451503b56"
|
||||
integrity sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimist@0.0.5:
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.5.tgz#d7aa327bcecf518f9106ac6b8f003fa3bcea8566"
|
||||
@@ -5960,10 +6032,10 @@ prelude-ls@~1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
||||
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
|
||||
|
||||
prettier@2.8.7:
|
||||
version "2.8.7"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.7.tgz#bb79fc8729308549d28fe3a98fce73d2c0656450"
|
||||
integrity sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==
|
||||
prettier@2.8.8:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
|
||||
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
|
||||
|
||||
pretty-format@^28.1.3:
|
||||
version "28.1.3"
|
||||
@@ -6561,13 +6633,20 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||
|
||||
semver@^7.3.5, semver@^7.3.7, semver@^7.3.8:
|
||||
semver@^7.3.5, semver@^7.3.8:
|
||||
version "7.3.8"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
|
||||
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
semver@^7.3.7, semver@^7.5.0:
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0"
|
||||
integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
sha.js@^2.4.0, sha.js@^2.4.8:
|
||||
version "2.4.11"
|
||||
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
|
||||
@@ -6927,9 +7006,9 @@ tapable@^2.2.0:
|
||||
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
|
||||
|
||||
terser@^5.5.1:
|
||||
version "5.16.8"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.8.tgz#ccde583dabe71df3f4ed02b65eb6532e0fae15d5"
|
||||
integrity sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA==
|
||||
version "5.17.1"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.1.tgz#948f10830454761e2eeedc6debe45c532c83fd69"
|
||||
integrity sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==
|
||||
dependencies:
|
||||
"@jridgewell/source-map" "^0.3.2"
|
||||
acorn "^8.5.0"
|
||||
@@ -7213,19 +7292,27 @@ typedoc-plugin-mdn-links@^3.0.3:
|
||||
resolved "https://registry.yarnpkg.com/typedoc-plugin-mdn-links/-/typedoc-plugin-mdn-links-3.0.3.tgz#da8d1a9750d57333e6c21717b38bfc13d4058de2"
|
||||
integrity sha512-NXhIpwQnsg7BcyMCHVqj3tUK+DL4g3Bt96JbFl4APzTGFkA+iM6GfZ/fn3TAqJ8O0CXG5R9BfWxolw1m1omNuQ==
|
||||
|
||||
typedoc-plugin-missing-exports@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-1.0.0.tgz#7212a2cfaba7b48264df4b4110f3a5684b5c49a1"
|
||||
integrity sha512-7s6znXnuAj1eD9KYPyzVzR1lBF5nwAY8IKccP5sdoO9crG4lpd16RoFpLsh2PccJM+I2NASpr0+/NMka6ThwVA==
|
||||
typedoc-plugin-missing-exports@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-2.0.0.tgz#9bdc4e30b0c7f24e9f1cb8890db4d01f608717c5"
|
||||
integrity sha512-t0QlKCm27/8DaheJkLo/gInSNjzBXgSciGhoLpL6sLyXZibm7SuwJtHvg4qXI2IjJfFBgW9mJvvszpoxMyB0TA==
|
||||
|
||||
typedoc@^0.23.20:
|
||||
version "0.23.28"
|
||||
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.23.28.tgz#3ce9c36ef1c273fa849d2dea18651855100d3ccd"
|
||||
integrity sha512-9x1+hZWTHEQcGoP7qFmlo4unUoVJLB0H/8vfO/7wqTnZxg4kPuji9y3uRzEu0ZKez63OJAUmiGhUrtukC6Uj3w==
|
||||
typedoc-plugin-versions@^0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/typedoc-plugin-versions/-/typedoc-plugin-versions-0.2.3.tgz#2cae4d722e45c3d9ab1a7640349c970dfc880f86"
|
||||
integrity sha512-SDbd+Q1ieHlSa5y3rCxuZH5Eg87Flgotx5qcpO3x2h/LaQ8i25oyiArXALm5wFXYr7CgSit4ODrVVlbA7SH2jA==
|
||||
dependencies:
|
||||
fs-extra "^10.1.0"
|
||||
semver "^7.3.7"
|
||||
|
||||
typedoc@^0.24.0:
|
||||
version "0.24.6"
|
||||
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.24.6.tgz#1f76fac27db9c6626b5b5c79f78f3c813b285827"
|
||||
integrity sha512-c3y3h45xJv3qYwKDAwU6Cl+26CjT0ZvblHzfHJ+SjQDM4p1mZxtgHky4lhmG0+nNarRht8kADfZlbspJWdZarQ==
|
||||
dependencies:
|
||||
lunr "^2.3.9"
|
||||
marked "^4.2.12"
|
||||
minimatch "^7.1.3"
|
||||
marked "^4.3.0"
|
||||
minimatch "^9.0.0"
|
||||
shiki "^0.14.1"
|
||||
|
||||
typescript@^3.2.2:
|
||||
@@ -7341,6 +7428,11 @@ universalify@^0.2.0:
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
|
||||
integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==
|
||||
|
||||
universalify@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
|
||||
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
|
||||
|
||||
update-browserslist-db@^1.0.10:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3"
|
||||
|
||||
Reference in New Issue
Block a user