Compare commits
84 Commits
v23.2.0-rc.1
...
v23.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a5e5e6a59 | |||
| 68e354752b | |||
| 0f1f1db3d2 | |||
| 77b91a45cb | |||
| fe6add9396 | |||
| 2d59c4647d | |||
| 1f0c6a6dc9 | |||
| c9b502fb0e | |||
| 330fbaccfc | |||
| 937f370655 | |||
| a8ad3ed26d | |||
| decac58a18 | |||
| 1a91ba59a6 | |||
| 89df43a975 | |||
| ad98706db4 | |||
| 195d1730bd | |||
| db4bd907f8 | |||
| cdd7dbbb2b | |||
| 108f157324 | |||
| 8da39ec8f4 | |||
| 182534288c | |||
| f81b7e5e6f | |||
| 6dda9e532d | |||
| 5e17626fe0 | |||
| abc9c9dcb0 | |||
| f346fcb056 | |||
| c67325ba07 | |||
| a063ae8ce7 | |||
| f9e5535492 | |||
| 015d9c5c4f | |||
| b6d40078d9 | |||
| b8a8f4850a | |||
| 1cc23d789c | |||
| 5cf0bb46a4 | |||
| 16672b3d0c | |||
| f61db81961 | |||
| e2a694115f | |||
| 71cf812d24 | |||
| 8a3d7d5671 | |||
| 2a363598dd | |||
| 05bf6428bc | |||
| e492a44dde | |||
| 44d2e47f96 | |||
| 31459a5d63 | |||
| 8f5db463e7 | |||
| 4e8affafcc | |||
| 2800681bb1 | |||
| b2a9e6f12f | |||
| fbd2c97f87 | |||
| 6c6304a620 | |||
| 0c1d5f6b25 | |||
| 1c26dc0233 | |||
| a163a202e7 | |||
| e15cf9976f | |||
| bdc3926417 | |||
| 4f918f684e | |||
| c142232f4d | |||
| c9bc20aa4d | |||
| 5c0cb3a536 | |||
| 415576d0a0 | |||
| 4f9fad66e4 | |||
| ebd9854980 | |||
| cb2fab64d8 | |||
| f446b49e49 | |||
| 22f5e41058 | |||
| fee5a006f1 | |||
| ef51ee28fd | |||
| cb61345780 | |||
| a18d4e226e | |||
| b09b33eb4c | |||
| 5fedc06d7c | |||
| d8c9f6db33 | |||
| 0af5fa0328 | |||
| b328b72cd5 | |||
| ce2a9d7036 | |||
| 66ae985af5 | |||
| 1828e2849c | |||
| b4f8b0fe4f | |||
| 70656e954f | |||
| 40a4c8d954 | |||
| 7ed787b86a | |||
| 1ee487a2ff | |||
| b76e7ca782 | |||
| ddce1bcd28 |
+1
-1
@@ -23,4 +23,4 @@ indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
indent_size = 4
|
||||
|
||||
+21
-1
@@ -1,6 +1,9 @@
|
||||
module.exports = {
|
||||
plugins: ["matrix-org", "import", "jsdoc"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:import/typescript"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
@@ -60,6 +63,23 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
// Disabled tests are a reality for now but as soon as all of the xits are
|
||||
// eliminated, we should enforce this.
|
||||
"jest/no-disabled-tests": "off",
|
||||
// TODO: There are many tests with invalid expects that should be fixed,
|
||||
// https://github.com/matrix-org/matrix-js-sdk/issues/2976
|
||||
"jest/valid-expect": "off",
|
||||
// TODO: There are many cases to refactor away,
|
||||
// https://github.com/matrix-org/matrix-js-sdk/issues/2978
|
||||
"jest/no-conditional-expect": "off",
|
||||
// Also treat "oldBackendOnly" as a test function.
|
||||
// Used in some crypto tests.
|
||||
"jest/no-standalone-expect": [
|
||||
"error",
|
||||
{
|
||||
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"],
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
||||
@@ -2,13 +2,9 @@ name: Pull Request
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
workflow_call:
|
||||
inputs:
|
||||
labels:
|
||||
type: string
|
||||
default: "T-Defect,T-Deprecation,T-Enhancement,T-Task"
|
||||
required: false
|
||||
description: "No longer used, uses allchange logic now, will be removed at a later date"
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
@@ -19,6 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: matrix-org/allchange@main
|
||||
if: github.event_name != 'merge_group'
|
||||
with:
|
||||
ghToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
requireLabel: true
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Static Analysis
|
||||
on:
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: [develop, master]
|
||||
concurrency:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Tests
|
||||
on:
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: [develop, master]
|
||||
concurrency:
|
||||
@@ -36,18 +38,19 @@ jobs:
|
||||
id: cpu-cores
|
||||
uses: SimenB/github-actions-cpu-cores@v1
|
||||
|
||||
- name: Run tests with coverage and metrics
|
||||
- name: Load metrics reporter
|
||||
id: metrics
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
run: |
|
||||
yarn coverage --ci --reporters github-actions '--reporters=<rootDir>/spec/slowReporter.js' --max-workers ${{ steps.cpu-cores.outputs.count }} ./spec/${{ matrix.specs }}
|
||||
mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
|
||||
env:
|
||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
||||
echo "extra-reporter='--reporters=<rootDir>/spec/slowReporter.js'" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run tests with coverage
|
||||
if: github.ref != 'refs/heads/develop'
|
||||
- name: Run tests
|
||||
run: |
|
||||
yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }} ./spec/${{ matrix.specs }}
|
||||
yarn coverage \
|
||||
--ci \
|
||||
--reporters github-actions ${{ steps.metrics.outputs.extra-reporter }} \
|
||||
--max-workers ${{ steps.cpu-cores.outputs.count }} \
|
||||
./spec/${{ matrix.specs }}
|
||||
mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
|
||||
env:
|
||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
||||
@@ -59,3 +62,22 @@ jobs:
|
||||
path: |
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
|
||||
matrix-react-sdk:
|
||||
name: Downstream test matrix-react-sdk
|
||||
if: github.event_name == 'merge_group'
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/tests.yml@develop
|
||||
with:
|
||||
disable_coverage: true
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
|
||||
# Hook for branch protection to work outside merge queues
|
||||
downstream:
|
||||
name: Downstream tests
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
needs:
|
||||
- matrix-react-sdk
|
||||
steps:
|
||||
- if: needs.matrix-react-sdk.result != 'skipped' && needs.matrix-react-sdk.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
+44
-2
@@ -1,5 +1,47 @@
|
||||
Changes in [23.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.2.0-rc.1) (2023-01-24)
|
||||
============================================================================================================
|
||||
Changes in [23.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.4.0) (2023-02-28)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Add easy way to determine if the decryption failure is due to "DecryptionError: The sender has disabled encrypting to unverified devices." ([\#3167](https://github.com/matrix-org/matrix-js-sdk/pull/3167)). Contributed by @florianduros.
|
||||
* Polls: expose end event id on poll model ([\#3160](https://github.com/matrix-org/matrix-js-sdk/pull/3160)). Contributed by @kerryarchibald.
|
||||
* Polls: count undecryptable poll relations ([\#3163](https://github.com/matrix-org/matrix-js-sdk/pull/3163)). Contributed by @kerryarchibald.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix spec compliance issue around encrypted `m.relates_to` ([\#3178](https://github.com/matrix-org/matrix-js-sdk/pull/3178)).
|
||||
* Fix reactions in threads sometimes causing stuck notifications ([\#3146](https://github.com/matrix-org/matrix-js-sdk/pull/3146)). Fixes vector-im/element-web#24000. Contributed by @justjanne.
|
||||
* Better type guard parseTopicContent ([\#3165](https://github.com/matrix-org/matrix-js-sdk/pull/3165)). Fixes matrix-org/element-web-rageshakes#20177 and matrix-org/element-web-rageshakes#20178.
|
||||
* Fix a bug where events in encrypted rooms would sometimes erroneously increment the total unread counter after being processed locally. ([\#3130](https://github.com/matrix-org/matrix-js-sdk/pull/3130)). Fixes vector-im/element-web#24448. Contributed by @Half-Shot.
|
||||
* Stop the ICE disconnected timer on call terminate ([\#3147](https://github.com/matrix-org/matrix-js-sdk/pull/3147)).
|
||||
* Clear notifications when we can infer read status from receipts ([\#3139](https://github.com/matrix-org/matrix-js-sdk/pull/3139)). Fixes vector-im/element-web#23991.
|
||||
* Messages sent out of order after one message fails ([\#3131](https://github.com/matrix-org/matrix-js-sdk/pull/3131)). Fixes vector-im/element-web#22885 and vector-im/element-web#18942. Contributed by @justjanne.
|
||||
|
||||
Changes in [23.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.3.0) (2023-02-14)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Element-R: implement encryption of outgoing events ([\#3122](https://github.com/matrix-org/matrix-js-sdk/pull/3122)).
|
||||
* Poll model - page /relations results ([\#3073](https://github.com/matrix-org/matrix-js-sdk/pull/3073)). Contributed by @kerryarchibald.
|
||||
* Poll model - validate end events ([\#3072](https://github.com/matrix-org/matrix-js-sdk/pull/3072)). Contributed by @kerryarchibald.
|
||||
* Handle optional last_known_event_id property in m.predecessor ([\#3119](https://github.com/matrix-org/matrix-js-sdk/pull/3119)). Contributed by @andybalaam.
|
||||
* Add support for stable identifier for fixed MAC in SAS verification ([\#3101](https://github.com/matrix-org/matrix-js-sdk/pull/3101)).
|
||||
* Provide eventId as well as roomId from Room.findPredecessor ([\#3095](https://github.com/matrix-org/matrix-js-sdk/pull/3095)). Contributed by @andybalaam.
|
||||
* MSC3946 Dynamic room predecessors ([\#3042](https://github.com/matrix-org/matrix-js-sdk/pull/3042)). Contributed by @andybalaam.
|
||||
* Poll model ([\#3036](https://github.com/matrix-org/matrix-js-sdk/pull/3036)). Contributed by @kerryarchibald.
|
||||
* Remove video tracks on video mute without renegotiating ([\#3091](https://github.com/matrix-org/matrix-js-sdk/pull/3091)).
|
||||
* Introduces a backwards-compatible API change. `MegolmEncrypter#prepareToEncrypt`'s return type has changed from `void` to `() => void`. ([\#3035](https://github.com/matrix-org/matrix-js-sdk/pull/3035)). Contributed by @clarkf.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Stop the ICE disconnected timer on call terminate ([\#3147](https://github.com/matrix-org/matrix-js-sdk/pull/3147)).
|
||||
* Clear notifications when we can infer read status from receipts ([\#3139](https://github.com/matrix-org/matrix-js-sdk/pull/3139)). Fixes vector-im/element-web#23991.
|
||||
* Messages sent out of order after one message fails ([\#3131](https://github.com/matrix-org/matrix-js-sdk/pull/3131)). Fixes vector-im/element-web#22885 and vector-im/element-web#18942. Contributed by @justjanne.
|
||||
* Element-R: fix a bug which prevented encryption working after a reload ([\#3126](https://github.com/matrix-org/matrix-js-sdk/pull/3126)).
|
||||
* Element-R: Fix invite processing ([\#3121](https://github.com/matrix-org/matrix-js-sdk/pull/3121)).
|
||||
* Don't throw with no `opponentDeviceInfo` ([\#3107](https://github.com/matrix-org/matrix-js-sdk/pull/3107)).
|
||||
* Remove flaky megolm test ([\#3098](https://github.com/matrix-org/matrix-js-sdk/pull/3098)). Contributed by @clarkf.
|
||||
* Fix "verifyLinks" functionality of getRoomUpgradeHistory ([\#3089](https://github.com/matrix-org/matrix-js-sdk/pull/3089)). Contributed by @andybalaam.
|
||||
|
||||
Changes in [23.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.2.0) (2023-01-31)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Implement decryption via the rust sdk ([\#3074](https://github.com/matrix-org/matrix-js-sdk/pull/3074)).
|
||||
|
||||
+12
-8
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "23.2.0-rc.1",
|
||||
"version": "23.4.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
@@ -33,7 +33,7 @@
|
||||
"matrix-org"
|
||||
],
|
||||
"main": "./lib/index.js",
|
||||
"browser": "./src/browser-index.ts",
|
||||
"browser": "./lib/browser-index.js",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_src_browser": "./src/browser-index.ts",
|
||||
"matrix_lib_main": "./lib/index.js",
|
||||
@@ -55,7 +55,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.2",
|
||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.3",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
@@ -84,11 +84,12 @@
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/domexception": "^4.0.0",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "18",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@types/uuid": "7",
|
||||
"@types/uuid": "9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"allchange": "^1.0.6",
|
||||
@@ -96,26 +97,29 @@
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"debug": "^4.3.4",
|
||||
"docdash": "^2.0.0",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.31.0",
|
||||
"eslint": "8.33.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "^27.1.6",
|
||||
"eslint-plugin-jsdoc": "^39.6.4",
|
||||
"eslint-plugin-matrix-org": "^0.9.0",
|
||||
"eslint-plugin-matrix-org": "^1.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.17",
|
||||
"eslint-plugin-unicorn": "^45.0.0",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^29.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^29.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"prettier": "2.8.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"prettier": "2.8.3",
|
||||
"rimraf": "^4.0.0",
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typedoc": "^0.23.20",
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
||||
|
||||
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
|
||||
# When merging to develop, we need revert the `main` and `typings` fields if we adjusted them previously.
|
||||
for i in main typings
|
||||
for i in main typings browser
|
||||
do
|
||||
# If a `lib` prefixed value is present, it means we adjusted the field
|
||||
# earlier at publish time, so we should revert it now.
|
||||
|
||||
+1
-1
@@ -180,7 +180,7 @@ yarn version --no-git-tag-version --new-version "$release"
|
||||
# they exist). This small bit of gymnastics allows us to use the TypeScript
|
||||
# source directly for development without needing to build before linting or
|
||||
# testing.
|
||||
for i in main typings
|
||||
for i in main typings browser
|
||||
do
|
||||
lib_value=$(jq -r ".matrix_lib_$i" package.json)
|
||||
if [ "$lib_value" != "null" ]; then
|
||||
|
||||
+25
-2
@@ -16,11 +16,16 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// `expect` is allowed in helper functions which are called within `test`/`it` blocks
|
||||
/* eslint-disable jest/no-standalone-expect */
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import "./olm-loader";
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../src/@types/crypto";
|
||||
import type { IE2EKeyReceiver } from "./test-utils/E2EKeyReceiver";
|
||||
import { LocalStorageCryptoStore } from "../src/crypto/store/localStorage-crypto-store";
|
||||
import { logger } from "../src/logger";
|
||||
import { syncPromise } from "./test-utils/test-utils";
|
||||
@@ -28,14 +33,18 @@ import { createClient, IStartClientOpts } from "../src/matrix";
|
||||
import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client";
|
||||
import { MockStorageApi } from "./MockStorageApi";
|
||||
import { encodeUri } from "../src/utils";
|
||||
import { IDeviceKeys, IOneTimeKey } from "../src/crypto/dehydration";
|
||||
import { IKeyBackupSession } from "../src/crypto/keybackup";
|
||||
import { IKeysUploadResponse, IUploadKeysRequest } from "../src/client";
|
||||
import { ISyncResponder } from "./test-utils/SyncResponder";
|
||||
|
||||
/**
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
*
|
||||
* @deprecated Avoid using this; it is tied too tightly to matrix-mock-request and is generally inconvenient to use.
|
||||
* Instead, construct a MatrixClient manually, use fetch-mock-jest to intercept the HTTP requests, and
|
||||
* use things like {@link E2EKeyReceiver} and {@link SyncResponder} to manage the requests.
|
||||
*/
|
||||
export class TestClient {
|
||||
export class TestClient implements IE2EKeyReceiver, ISyncResponder {
|
||||
public readonly httpBackend: MockHttpBackend;
|
||||
public readonly client: MatrixClient;
|
||||
public deviceKeys?: IDeviceKeys | null;
|
||||
@@ -240,8 +249,22 @@ export class TestClient {
|
||||
return this.deviceKeys!.keys[keyId];
|
||||
}
|
||||
|
||||
/** Next time we see a sync request (or immediately, if there is one waiting), send the given response
|
||||
*
|
||||
* Calling this will register a response for `/sync`, and then, in the background, flush a single `/sync` request.
|
||||
* Try calling {@link syncPromise} to wait for the sync to complete.
|
||||
*
|
||||
* @param response - response to /sync request
|
||||
*/
|
||||
public sendOrQueueSyncResponse(syncResponse: object): void {
|
||||
this.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
this.httpBackend.flush("/sync", 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* flush a single /sync request, and wait for the syncing event
|
||||
*
|
||||
* @deprecated: prefer to use {@link #sendOrQueueSyncResponse} followed by {@link syncPromise}.
|
||||
*/
|
||||
public flushSync(): Promise<void> {
|
||||
logger.log(`${this}: flushSync`);
|
||||
|
||||
+534
-449
File diff suppressed because it is too large
Load Diff
@@ -175,7 +175,7 @@ describe("MatrixClient events", function () {
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit User events", function (done) {
|
||||
it("should emit User events", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
let fired = false;
|
||||
@@ -192,10 +192,8 @@ describe("MatrixClient events", function () {
|
||||
});
|
||||
client!.startClient();
|
||||
|
||||
httpBackend!.flushAllExpected().then(function () {
|
||||
expect(fired).toBe(true);
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(fired).toBe(true);
|
||||
});
|
||||
|
||||
it("should emit Room events", function () {
|
||||
|
||||
@@ -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.
|
||||
@@ -590,7 +590,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
await client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
@@ -647,7 +647,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("should return relevant timeline from non-thread timelineSet when asking for the thread root", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
@@ -680,7 +680,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("should return undefined when event is not in the thread that the given timelineSet is representing", () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
@@ -709,7 +709,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("should return undefined when event is within a thread but timelineSet is not", () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
@@ -1127,7 +1127,7 @@ describe("MatrixClient event timelines", function () {
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
@@ -1263,7 +1263,7 @@ describe("MatrixClient event timelines", function () {
|
||||
describe("with server compatibility", function () {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
@@ -1421,7 +1421,7 @@ describe("MatrixClient event timelines", function () {
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
@@ -1473,7 +1473,7 @@ describe("MatrixClient event timelines", function () {
|
||||
describe("without server compatibility", function () {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.None);
|
||||
});
|
||||
@@ -1539,7 +1539,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("should add lazy loading filter", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
// @ts-ignore
|
||||
@@ -1567,7 +1567,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("should correctly pass pagination token", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
|
||||
@@ -1892,7 +1892,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("in stable mode", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
@@ -1902,7 +1902,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("in backwards compatible unstable mode", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Experimental);
|
||||
@@ -1912,7 +1912,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("in backwards compatible mode", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.None);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.None);
|
||||
|
||||
@@ -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.
|
||||
@@ -35,7 +35,7 @@ describe("MatrixClient", function () {
|
||||
let store: MemoryStore | undefined;
|
||||
|
||||
const defaultClientOpts: IStoredClientOpts = {
|
||||
experimentalThreadSupport: false,
|
||||
threadSupport: false,
|
||||
};
|
||||
const setupTests = (): [MatrixClient, HttpBackend, MemoryStore] => {
|
||||
const store = new MemoryStore();
|
||||
@@ -205,19 +205,17 @@ describe("MatrixClient", function () {
|
||||
describe("getFilter", function () {
|
||||
const filterId = "f1lt3r1d";
|
||||
|
||||
it("should return a filter from the store if allowCached", function (done) {
|
||||
it("should return a filter from the store if allowCached", async () => {
|
||||
const filter = Filter.fromJson(userId, filterId, {
|
||||
event_format: "client",
|
||||
});
|
||||
store!.storeFilter(filter);
|
||||
client!.getFilter(userId, filterId, true).then(function (gotFilter) {
|
||||
expect(gotFilter).toEqual(filter);
|
||||
done();
|
||||
});
|
||||
const gotFilter = await client!.getFilter(userId, filterId, true);
|
||||
expect(gotFilter).toEqual(filter);
|
||||
httpBackend!.verifyNoOutstandingRequests();
|
||||
});
|
||||
|
||||
it("should do an HTTP request if !allowCached even if one exists", function (done) {
|
||||
it("should do an HTTP request if !allowCached even if one exists", async () => {
|
||||
const httpFilterDefinition = {
|
||||
event_format: "federation",
|
||||
};
|
||||
@@ -230,15 +228,11 @@ describe("MatrixClient", function () {
|
||||
event_format: "client",
|
||||
});
|
||||
store!.storeFilter(storeFilter);
|
||||
client!.getFilter(userId, filterId, false).then(function (gotFilter) {
|
||||
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
||||
done();
|
||||
});
|
||||
|
||||
httpBackend!.flush("");
|
||||
const [gotFilter] = await Promise.all([client!.getFilter(userId, filterId, false), httpBackend!.flush("")]);
|
||||
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
||||
});
|
||||
|
||||
it("should do an HTTP request if nothing is in the cache and then store it", function (done) {
|
||||
it("should do an HTTP request if nothing is in the cache and then store it", async () => {
|
||||
const httpFilterDefinition = {
|
||||
event_format: "federation",
|
||||
};
|
||||
@@ -247,20 +241,16 @@ describe("MatrixClient", function () {
|
||||
httpBackend!
|
||||
.when("GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId)
|
||||
.respond(200, httpFilterDefinition);
|
||||
client!.getFilter(userId, filterId, true).then(function (gotFilter) {
|
||||
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
||||
expect(store!.getFilter(userId, filterId)).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
|
||||
httpBackend!.flush("");
|
||||
const [gotFilter] = await Promise.all([client!.getFilter(userId, filterId, true), httpBackend!.flush("")]);
|
||||
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
||||
expect(store!.getFilter(userId, filterId)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFilter", function () {
|
||||
const filterId = "f1llllllerid";
|
||||
|
||||
it("should do an HTTP request and then store the filter", function (done) {
|
||||
it("should do an HTTP request and then store the filter", async () => {
|
||||
expect(store!.getFilter(userId, filterId)).toBe(null);
|
||||
|
||||
const filterDefinition = {
|
||||
@@ -276,13 +266,9 @@ describe("MatrixClient", function () {
|
||||
filter_id: filterId,
|
||||
});
|
||||
|
||||
client!.createFilter(filterDefinition).then(function (gotFilter) {
|
||||
expect(gotFilter.getDefinition()).toEqual(filterDefinition);
|
||||
expect(store!.getFilter(userId, filterId)).toEqual(gotFilter);
|
||||
done();
|
||||
});
|
||||
|
||||
httpBackend!.flush("");
|
||||
const [gotFilter] = await Promise.all([client!.createFilter(filterDefinition), httpBackend!.flush("")]);
|
||||
expect(gotFilter.getDefinition()).toEqual(filterDefinition);
|
||||
expect(store!.getFilter(userId, filterId)).toEqual(gotFilter);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -671,7 +657,7 @@ describe("MatrixClient", function () {
|
||||
// @ts-ignore setting private property
|
||||
client!.clientOpts = {
|
||||
...defaultClientOpts,
|
||||
experimentalThreadSupport: true,
|
||||
threadSupport: true,
|
||||
};
|
||||
|
||||
const eventPollResponseReference = buildEventPollResponseReference();
|
||||
@@ -702,7 +688,7 @@ describe("MatrixClient", function () {
|
||||
// @ts-ignore setting private property
|
||||
client!.clientOpts = {
|
||||
...defaultClientOpts,
|
||||
experimentalThreadSupport: true,
|
||||
threadSupport: true,
|
||||
};
|
||||
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
@@ -726,7 +712,7 @@ describe("MatrixClient", function () {
|
||||
// @ts-ignore setting private property
|
||||
client!.clientOpts = {
|
||||
...defaultClientOpts,
|
||||
experimentalThreadSupport: true,
|
||||
threadSupport: true,
|
||||
};
|
||||
|
||||
const eventPollResponseReference = buildEventPollResponseReference();
|
||||
@@ -750,7 +736,7 @@ describe("MatrixClient", function () {
|
||||
// @ts-ignore setting private property
|
||||
client!.clientOpts = {
|
||||
...defaultClientOpts,
|
||||
experimentalThreadSupport: true,
|
||||
threadSupport: true,
|
||||
};
|
||||
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
@@ -774,7 +760,7 @@ describe("MatrixClient", function () {
|
||||
// @ts-ignore setting private property
|
||||
client!.clientOpts = {
|
||||
...defaultClientOpts,
|
||||
experimentalThreadSupport: true,
|
||||
threadSupport: true,
|
||||
};
|
||||
// This is based on recording the events in a real room:
|
||||
|
||||
@@ -831,7 +817,7 @@ describe("MatrixClient", function () {
|
||||
// @ts-ignore setting private property
|
||||
client!.clientOpts = {
|
||||
...defaultClientOpts,
|
||||
experimentalThreadSupport: true,
|
||||
threadSupport: true,
|
||||
};
|
||||
|
||||
const threadRootEvent = buildEventPollStartThreadRoot();
|
||||
@@ -857,7 +843,7 @@ describe("MatrixClient", function () {
|
||||
// @ts-ignore setting private property
|
||||
client!.clientOpts = {
|
||||
...defaultClientOpts,
|
||||
experimentalThreadSupport: true,
|
||||
threadSupport: true,
|
||||
};
|
||||
|
||||
const threadRootEvent = buildEventPollStartThreadRoot();
|
||||
@@ -878,7 +864,7 @@ describe("MatrixClient", function () {
|
||||
// @ts-ignore setting private property
|
||||
client!.clientOpts = {
|
||||
...defaultClientOpts,
|
||||
experimentalThreadSupport: true,
|
||||
threadSupport: true,
|
||||
};
|
||||
|
||||
const threadRootEvent = buildEventPollStartThreadRoot();
|
||||
|
||||
@@ -94,16 +94,16 @@ describe("MatrixClient opts", function () {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("should be able to send messages", function (done) {
|
||||
it("should be able to send messages", async () => {
|
||||
const eventId = "$flibble:wibble";
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
});
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
|
||||
expect(res.event_id).toEqual(eventId);
|
||||
done();
|
||||
});
|
||||
httpBackend.flush("/txn1", 1);
|
||||
const [res] = await Promise.all([
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1"),
|
||||
httpBackend.flush("/txn1", 1),
|
||||
]);
|
||||
expect(res.event_id).toEqual(eventId);
|
||||
});
|
||||
|
||||
it("should be able to sync / get new events", async function () {
|
||||
@@ -149,7 +149,7 @@ describe("MatrixClient opts", function () {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("shouldn't retry sending events", function (done) {
|
||||
it("shouldn't retry sending events", async () => {
|
||||
httpBackend.when("PUT", "/txn1").respond(
|
||||
500,
|
||||
new MatrixError({
|
||||
@@ -157,19 +157,17 @@ describe("MatrixClient opts", function () {
|
||||
error: "Ruh roh",
|
||||
}),
|
||||
);
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(
|
||||
function (res) {
|
||||
expect(false).toBe(true);
|
||||
},
|
||||
function (err) {
|
||||
expect(err.errcode).toEqual("M_SOMETHING");
|
||||
done();
|
||||
},
|
||||
);
|
||||
httpBackend.flush("/txn1", 1);
|
||||
try {
|
||||
await Promise.all([
|
||||
expect(client.sendTextMessage("!foo:bar", "a body", "txn1")).rejects.toThrow(),
|
||||
httpBackend.flush("/txn1", 1),
|
||||
]);
|
||||
} catch (err) {
|
||||
expect((<MatrixError>err).errcode).toEqual("M_SOMETHING");
|
||||
}
|
||||
});
|
||||
|
||||
it("shouldn't queue events", function (done) {
|
||||
it("shouldn't queue events", async () => {
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: "AAA",
|
||||
});
|
||||
@@ -178,30 +176,38 @@ describe("MatrixClient opts", function () {
|
||||
});
|
||||
let sentA = false;
|
||||
let sentB = false;
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
|
||||
const messageASendPromise = client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
|
||||
sentA = true;
|
||||
// We expect messageB to be sent before messageA to ensure as we're
|
||||
// testing that there is no queueing that blocks each other
|
||||
expect(sentB).toBe(true);
|
||||
});
|
||||
client.sendTextMessage("!foo:bar", "b body", "txn2").then(function (res) {
|
||||
const messageBSendPromise = client.sendTextMessage("!foo:bar", "b body", "txn2").then(function (res) {
|
||||
sentB = true;
|
||||
// We expect messageB to be sent before messageA to ensure as we're
|
||||
// testing that there is no queueing that blocks each other
|
||||
expect(sentA).toBe(false);
|
||||
});
|
||||
httpBackend.flush("/txn2", 1).then(function () {
|
||||
httpBackend.flush("/txn1", 1).then(function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
// Allow messageB to succeed first
|
||||
await httpBackend.flush("/txn2", 1);
|
||||
// Then allow messageA to succeed
|
||||
await httpBackend.flush("/txn1", 1);
|
||||
|
||||
// Now await the message send promises to
|
||||
await messageBSendPromise;
|
||||
await messageASendPromise;
|
||||
});
|
||||
|
||||
it("should be able to send messages", function (done) {
|
||||
it("should be able to send messages", async () => {
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: "foo",
|
||||
});
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
|
||||
expect(res.event_id).toEqual("foo");
|
||||
done();
|
||||
});
|
||||
httpBackend.flush("/txn1", 1);
|
||||
const [res] = await Promise.all([
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1"),
|
||||
httpBackend.flush("/txn1", 1),
|
||||
]);
|
||||
|
||||
expect(res.event_id).toEqual("foo");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,13 +48,13 @@ describe("MatrixClient retrying", function () {
|
||||
return httpBackend!.stop();
|
||||
});
|
||||
|
||||
xit("should retry according to MatrixScheduler.retryFn", function () {});
|
||||
it.skip("should retry according to MatrixScheduler.retryFn", function () {});
|
||||
|
||||
xit("should queue according to MatrixScheduler.queueFn", function () {});
|
||||
it.skip("should queue according to MatrixScheduler.queueFn", function () {});
|
||||
|
||||
xit("should mark events as EventStatus.NOT_SENT when giving up", function () {});
|
||||
it.skip("should mark events as EventStatus.NOT_SENT when giving up", function () {});
|
||||
|
||||
xit("should mark events as EventStatus.QUEUED when queued", function () {});
|
||||
it.skip("should mark events as EventStatus.QUEUED when queued", function () {});
|
||||
|
||||
it("should mark events as EventStatus.CANCELLED when cancelled", function () {
|
||||
// send a couple of events; the second will be queued
|
||||
@@ -130,7 +130,7 @@ describe("MatrixClient retrying", function () {
|
||||
});
|
||||
|
||||
describe("resending", function () {
|
||||
xit("should be able to resend a NOT_SENT event", function () {});
|
||||
xit("should be able to resend a sent event", function () {});
|
||||
it.skip("should be able to resend a NOT_SENT event", function () {});
|
||||
it.skip("should be able to resend a sent event", function () {});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -163,36 +163,38 @@ describe("MatrixClient room timelines", function () {
|
||||
it(
|
||||
"should be added immediately after calling MatrixClient.sendEvent " +
|
||||
"with EventStatus.SENDING and the right event.sender",
|
||||
function (done) {
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
async () => {
|
||||
const wasMessageAddedPromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, async (state) => {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
// check it was added
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
// check status
|
||||
expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
|
||||
// check member
|
||||
const member = room.timeline[1].sender;
|
||||
expect(member?.userId).toEqual(userId);
|
||||
expect(member?.name).toEqual(userName);
|
||||
client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
// check it was added
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
// check status
|
||||
expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
|
||||
// check member
|
||||
const member = room.timeline[1].sender;
|
||||
expect(member?.userId).toEqual(userId);
|
||||
expect(member?.name).toEqual(userName);
|
||||
|
||||
httpBackend!.flush("/sync", 1).then(function () {
|
||||
done();
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await wasMessageAddedPromise;
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"should be updated correctly when the send request finishes " +
|
||||
"BEFORE the event comes down the event stream",
|
||||
function (done) {
|
||||
async () => {
|
||||
const eventId = "$foo:bar";
|
||||
httpBackend!.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
@@ -207,28 +209,30 @@ describe("MatrixClient room timelines", function () {
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
client!.sendTextMessage(roomId, "I am a fish", "txn1").then(function () {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
httpBackend!.flush("/sync", 1).then(function () {
|
||||
const wasMessageAddedPromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
client!.sendTextMessage(roomId, "I am a fish", "txn1").then(async () => {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
done();
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
resolve(null);
|
||||
});
|
||||
httpBackend!.flush("/txn1", 1);
|
||||
});
|
||||
httpBackend!.flush("/txn1", 1);
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await wasMessageAddedPromise;
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"should be updated correctly when the send request finishes " +
|
||||
"AFTER the event comes down the event stream",
|
||||
function (done) {
|
||||
async () => {
|
||||
const eventId = "$foo:bar";
|
||||
httpBackend!.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
@@ -243,23 +247,24 @@ describe("MatrixClient room timelines", function () {
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
const promise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
httpBackend!.flush("/sync", 1).then(function () {
|
||||
const wasMessageAddedPromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, async (state) => {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
const messageSendPromise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
httpBackend!.flush("/txn1", 1);
|
||||
promise.then(function () {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
done();
|
||||
});
|
||||
await messageSendPromise;
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await wasMessageAddedPromise;
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -279,30 +284,29 @@ describe("MatrixClient room timelines", function () {
|
||||
});
|
||||
});
|
||||
|
||||
it("should set Room.oldState.paginationToken to null at the start" + " of the timeline.", function (done) {
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
it("should set Room.oldState.paginationToken to null at the start of the timeline.", async () => {
|
||||
const didPaginatePromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, async (state) => {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client!.scrollback(room).then(function () {
|
||||
await Promise.all([client!.scrollback(room), httpBackend!.flush("/messages", 1)]);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
expect(room.oldState.paginationToken).toBe(null);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
httpBackend!.flush("/messages", 1);
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await didPaginatePromise;
|
||||
});
|
||||
|
||||
it("should set the right event.sender values", function (done) {
|
||||
it("should set the right event.sender values", async () => {
|
||||
// We're aiming for an eventual timeline of:
|
||||
//
|
||||
// 'Old Alice' joined the room
|
||||
@@ -353,15 +357,17 @@ describe("MatrixClient room timelines", function () {
|
||||
joinMshipEvent,
|
||||
];
|
||||
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
// sync response
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
const didPaginatePromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, async (state) => {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
// sync response
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
await Promise.all([client!.scrollback(room), httpBackend!.flush("/messages", 1)]);
|
||||
|
||||
client!.scrollback(room).then(function () {
|
||||
expect(room.timeline.length).toEqual(5);
|
||||
const joinMsg = room.timeline[0];
|
||||
expect(joinMsg.sender?.name).toEqual("Old Alice");
|
||||
@@ -371,17 +377,15 @@ describe("MatrixClient room timelines", function () {
|
||||
expect(newMsg.sender?.name).toEqual(userName);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
httpBackend!.flush("/messages", 1);
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await didPaginatePromise;
|
||||
});
|
||||
|
||||
it("should add it them to the right place in the timeline", function (done) {
|
||||
it("should add it them to the right place in the timeline", async () => {
|
||||
// set the list of events to return on scrollback
|
||||
sbEvents = [
|
||||
utils.mkMessage({
|
||||
@@ -396,30 +400,30 @@ describe("MatrixClient room timelines", function () {
|
||||
}),
|
||||
];
|
||||
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
const didPaginatePromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, async (state) => {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
await Promise.all([client!.scrollback(room), httpBackend!.flush("/messages", 1)]);
|
||||
|
||||
client!.scrollback(room).then(function () {
|
||||
expect(room.timeline.length).toEqual(3);
|
||||
expect(room.timeline[0].event).toEqual(sbEvents[1]);
|
||||
expect(room.timeline[1].event).toEqual(sbEvents[0]);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
httpBackend!.flush("/messages", 1);
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await didPaginatePromise;
|
||||
});
|
||||
|
||||
it("should use 'end' as the next pagination token", function (done) {
|
||||
it("should use 'end' as the next pagination token", async () => {
|
||||
// set the list of events to return on scrollback
|
||||
sbEvents = [
|
||||
utils.mkMessage({
|
||||
@@ -429,25 +433,24 @@ describe("MatrixClient room timelines", function () {
|
||||
}),
|
||||
];
|
||||
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.oldState.paginationToken).toBeTruthy();
|
||||
const didPaginatePromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, async (state) => {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.oldState.paginationToken).toBeTruthy();
|
||||
|
||||
client!.scrollback(room, 1).then(function () {
|
||||
await Promise.all([client!.scrollback(room, 1), httpBackend!.flush("/messages", 1)]);
|
||||
expect(room.oldState.paginationToken).toEqual(sbEndTok);
|
||||
});
|
||||
|
||||
httpBackend!.flush("/messages", 1).then(function () {
|
||||
// still have a sync to flush
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await didPaginatePromise;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -81,17 +81,15 @@ describe("MatrixClient syncing", () => {
|
||||
presence: {},
|
||||
};
|
||||
|
||||
it("should /sync after /pushrules and /filter.", (done) => {
|
||||
it("should /sync after /pushrules and /filter.", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client!.startClient();
|
||||
|
||||
httpBackend!.flushAllExpected().then(() => {
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
});
|
||||
|
||||
it("should pass the 'next_batch' token from /sync to the since= param of the next /sync", (done) => {
|
||||
it("should pass the 'next_batch' token from /sync to the since= param of the next /sync", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend!
|
||||
.when("GET", "/sync")
|
||||
@@ -102,9 +100,7 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
client!.startClient();
|
||||
|
||||
httpBackend!.flushAllExpected().then(() => {
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
});
|
||||
|
||||
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
|
||||
@@ -724,7 +720,7 @@ describe("MatrixClient syncing", () => {
|
||||
// events that arrive in the incremental sync as if they preceeded the
|
||||
// timeline events, however this breaks peeking, so it's disabled
|
||||
// (see sync.js)
|
||||
xit("should correctly interpret state in incremental sync.", () => {
|
||||
it.skip("should correctly interpret state in incremental sync.", () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend!.when("GET", "/sync").respond(200, nextSyncData);
|
||||
|
||||
@@ -741,9 +737,9 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
xit("should update power levels for users in a room", () => {});
|
||||
it.skip("should update power levels for users in a room", () => {});
|
||||
|
||||
xit("should update the room topic", () => {});
|
||||
it.skip("should update the room topic", () => {});
|
||||
|
||||
describe("onMarkerStateEvent", () => {
|
||||
const normalMessageEvent = utils.mkMessage({
|
||||
@@ -840,6 +836,7 @@ describe("MatrixClient syncing", () => {
|
||||
roomVersion: "org.matrix.msc2716v3",
|
||||
},
|
||||
].forEach((testMeta) => {
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
describe(testMeta.label, () => {
|
||||
const roomCreateEvent = utils.mkEvent({
|
||||
type: "m.room.create",
|
||||
@@ -1592,27 +1589,24 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
describe("of a room", () => {
|
||||
xit(
|
||||
it.skip(
|
||||
"should sync when a join event (which changes state) for the user" +
|
||||
" arrives down the event stream (e.g. join from another device)",
|
||||
() => {},
|
||||
);
|
||||
|
||||
xit("should sync when the user explicitly calls joinRoom", () => {});
|
||||
it.skip("should sync when the user explicitly calls joinRoom", () => {});
|
||||
});
|
||||
|
||||
describe("syncLeftRooms", () => {
|
||||
beforeEach((done) => {
|
||||
beforeEach(async () => {
|
||||
client!.startClient();
|
||||
|
||||
httpBackend!.flushAllExpected().then(() => {
|
||||
// the /sync call from syncLeftRooms ends up in the request
|
||||
// queue behind the call from the running client; add a response
|
||||
// to flush the client's one out.
|
||||
httpBackend!.when("GET", "/sync").respond(200, {});
|
||||
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
// the /sync call from syncLeftRooms ends up in the request
|
||||
// queue behind the call from the running client; add a response
|
||||
// to flush the client's one out.
|
||||
await httpBackend!.when("GET", "/sync").respond(200, {});
|
||||
});
|
||||
|
||||
it("should create and use an appropriate filter", () => {
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
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 "fake-indexeddb/auto";
|
||||
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import { Category, ISyncResponse, MatrixClient, NotificationCountType, Room } from "../../src";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient syncing", () => {
|
||||
const userA = "@alice:localhost";
|
||||
const userB = "@bob:localhost";
|
||||
|
||||
const selfUserId = userA;
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
|
||||
const setupTestClient = (): [MatrixClient, HttpBackend] => {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const client = testClient.client;
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
return [client, httpBackend];
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
[client, httpBackend] = setupTestClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
client!.stopClient();
|
||||
return httpBackend!.stop();
|
||||
});
|
||||
|
||||
describe("Stuck unread notifications integration tests", () => {
|
||||
const ROOM_ID = "!room:localhost";
|
||||
|
||||
const syncData = getSampleStuckNotificationSyncResponse(ROOM_ID);
|
||||
|
||||
it("resets notifications if the last event originates from the logged in user", async () => {
|
||||
httpBackend!
|
||||
.when("GET", "/sync")
|
||||
.check((req) => {
|
||||
expect(req.queryParams!.filter).toEqual("a filter id");
|
||||
})
|
||||
.respond(200, syncData);
|
||||
|
||||
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
|
||||
client!.startClient({ initialSyncLimit: 1 });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
const room = client?.getRoom(ROOM_ID);
|
||||
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
expect(room?.getUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
function getSampleStuckNotificationSyncResponse(roomId: string): Partial<ISyncResponse> {
|
||||
return {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
[Category.Join]: {
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
{
|
||||
content: {
|
||||
creator: userB,
|
||||
room_version: "9",
|
||||
},
|
||||
origin_server_ts: 1,
|
||||
sender: userB,
|
||||
state_key: "",
|
||||
type: "m.room.create",
|
||||
event_id: "$event1",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
avatar_url: "",
|
||||
displayname: userB,
|
||||
membership: "join",
|
||||
},
|
||||
origin_server_ts: 2,
|
||||
sender: userB,
|
||||
state_key: userB,
|
||||
type: "m.room.member",
|
||||
event_id: "$event2",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
ban: 50,
|
||||
events: {
|
||||
"m.room.avatar": 50,
|
||||
"m.room.canonical_alias": 50,
|
||||
"m.room.encryption": 100,
|
||||
"m.room.history_visibility": 100,
|
||||
"m.room.name": 50,
|
||||
"m.room.power_levels": 100,
|
||||
"m.room.server_acl": 100,
|
||||
"m.room.tombstone": 100,
|
||||
},
|
||||
events_default: 0,
|
||||
historical: 100,
|
||||
invite: 0,
|
||||
kick: 50,
|
||||
redact: 50,
|
||||
state_default: 50,
|
||||
users: {
|
||||
[userA]: 100,
|
||||
[userB]: 100,
|
||||
},
|
||||
users_default: 0,
|
||||
},
|
||||
origin_server_ts: 3,
|
||||
sender: userB,
|
||||
state_key: "",
|
||||
type: "m.room.power_levels",
|
||||
event_id: "$event3",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
join_rule: "invite",
|
||||
},
|
||||
origin_server_ts: 4,
|
||||
sender: userB,
|
||||
state_key: "",
|
||||
type: "m.room.join_rules",
|
||||
event_id: "$event4",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
history_visibility: "shared",
|
||||
},
|
||||
origin_server_ts: 5,
|
||||
sender: userB,
|
||||
state_key: "",
|
||||
type: "m.room.history_visibility",
|
||||
event_id: "$event5",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
guest_access: "can_join",
|
||||
},
|
||||
origin_server_ts: 6,
|
||||
sender: userB,
|
||||
state_key: "",
|
||||
type: "m.room.guest_access",
|
||||
unsigned: {
|
||||
age: 1651569,
|
||||
},
|
||||
event_id: "$event6",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
origin_server_ts: 7,
|
||||
sender: userB,
|
||||
state_key: "",
|
||||
type: "m.room.encryption",
|
||||
event_id: "$event7",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
avatar_url: "",
|
||||
displayname: userA,
|
||||
is_direct: true,
|
||||
membership: "invite",
|
||||
},
|
||||
origin_server_ts: 8,
|
||||
sender: userB,
|
||||
state_key: userA,
|
||||
type: "m.room.member",
|
||||
event_id: "$event8",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "hello",
|
||||
},
|
||||
origin_server_ts: 9,
|
||||
sender: userB,
|
||||
type: "m.room.message",
|
||||
event_id: "$event9",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
avatar_url: "",
|
||||
displayname: userA,
|
||||
membership: "join",
|
||||
},
|
||||
origin_server_ts: 10,
|
||||
sender: userA,
|
||||
state_key: userA,
|
||||
type: "m.room.member",
|
||||
event_id: "$event10",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "world",
|
||||
},
|
||||
origin_server_ts: 11,
|
||||
sender: userA,
|
||||
type: "m.room.message",
|
||||
event_id: "$event11",
|
||||
},
|
||||
],
|
||||
prev_batch: "123",
|
||||
limited: false,
|
||||
},
|
||||
state: {
|
||||
events: [],
|
||||
},
|
||||
account_data: {
|
||||
events: [
|
||||
{
|
||||
type: "m.fully_read",
|
||||
content: {
|
||||
event_id: "$dER5V1RCMxzAhHXQJoMjqyuoxpPtK2X6hCb9T8Jg2wU",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
ephemeral: {
|
||||
events: [
|
||||
{
|
||||
type: "m.receipt",
|
||||
content: {
|
||||
$event9: {
|
||||
"m.read": {
|
||||
[userA]: {
|
||||
ts: 100,
|
||||
},
|
||||
},
|
||||
"m.read.private": {
|
||||
[userA]: {
|
||||
ts: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
dER5V1RCMxzAhHXQJoMjqyuoxpPtK2X6hCb9T8Jg2wU: {
|
||||
"m.read": {
|
||||
[userB]: {
|
||||
ts: 666,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
unread_notifications: {
|
||||
notification_count: 1,
|
||||
highlight_count: 0,
|
||||
},
|
||||
summary: {
|
||||
"m.joined_member_count": 2,
|
||||
"m.invited_member_count": 0,
|
||||
"m.heroes": [userB],
|
||||
},
|
||||
},
|
||||
},
|
||||
[Category.Leave]: {},
|
||||
[Category.Invite]: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -29,13 +29,13 @@ limitations under the License.
|
||||
import "../olm-loader";
|
||||
|
||||
import type { Session } from "@matrix-org/olm";
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
|
||||
import { logger } from "../../src/logger";
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../src/client";
|
||||
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
import { IDeviceKeys, IOneTimeKey } from "../../src/crypto/dehydration";
|
||||
|
||||
let aliTestClient: TestClient;
|
||||
const roomId = "!room:localhost";
|
||||
|
||||
@@ -153,11 +153,11 @@ describe("SlidingSyncSdk", () => {
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
expect(mockSlidingSync!.start).toBeCalled();
|
||||
expect(mockSlidingSync!.start).toHaveBeenCalled();
|
||||
});
|
||||
it("can stop()", async () => {
|
||||
sdk!.stop();
|
||||
expect(mockSlidingSync!.stop).toBeCalled();
|
||||
expect(mockSlidingSync!.stop).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -584,7 +584,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("emits SyncState.Error immediately when receiving M_UNKNOWN_TOKEN and stops syncing", async () => {
|
||||
expect(mockSlidingSync!.stop).not.toBeCalled();
|
||||
expect(mockSlidingSync!.stop).not.toHaveBeenCalled();
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle,
|
||||
SlidingSyncState.RequestFinished,
|
||||
@@ -595,7 +595,7 @@ describe("SlidingSyncSdk", () => {
|
||||
}),
|
||||
);
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
|
||||
expect(mockSlidingSync!.stop).toBeCalled();
|
||||
expect(mockSlidingSync!.stop).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
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 debugFunc from "debug";
|
||||
import { Debugger } from "debug";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
|
||||
|
||||
/** Interface implemented by classes that intercept `/keys/upload` requests from test clients to catch the uploaded keys
|
||||
*
|
||||
* Common interface implemented by {@link TestClient} and {@link E2EKeyReceiver}
|
||||
*/
|
||||
export interface IE2EKeyReceiver {
|
||||
/**
|
||||
* get the uploaded ed25519 device key
|
||||
*
|
||||
* @returns base64 device key
|
||||
*/
|
||||
getSigningKey(): string;
|
||||
|
||||
/**
|
||||
* get the uploaded curve25519 device key
|
||||
*
|
||||
* @returns base64 device key
|
||||
*/
|
||||
getDeviceKey(): string;
|
||||
|
||||
/**
|
||||
* Wait for one-time-keys to be uploaded, then return them.
|
||||
*
|
||||
* @returns Promise for the one-time keys
|
||||
*/
|
||||
awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>>;
|
||||
}
|
||||
|
||||
/** E2EKeyReceiver: An object which intercepts `/keys/uploads` fetches via fetch-mock.
|
||||
*
|
||||
* It stashes the uploaded keys for use elsewhere in the tests.
|
||||
*/
|
||||
export class E2EKeyReceiver implements IE2EKeyReceiver {
|
||||
private readonly debug: Debugger;
|
||||
|
||||
private deviceKeys: IDeviceKeys | null = null;
|
||||
private oneTimeKeys: Record<string, IOneTimeKey> = {};
|
||||
private readonly oneTimeKeysPromise: Promise<void>;
|
||||
|
||||
/**
|
||||
* Construct a new E2EKeyReceiver.
|
||||
*
|
||||
* It will immediately register an intercept of `/keys/uploads` requests for the given homeserverUrl.
|
||||
* Only /upload requests made to this server will be intercepted: this allows a single test to use more than one
|
||||
* client and have the keys collected separately.
|
||||
*
|
||||
* @param homeserverUrl - the Homeserver Url of the client under test.
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
this.debug = debugFunc(`e2e-key-receiver:[${homeserverUrl}]`);
|
||||
|
||||
// set up a listener for /keys/upload.
|
||||
this.oneTimeKeysPromise = new Promise((resolveOneTimeKeys) => {
|
||||
const listener = (url: string, options: RequestInit) =>
|
||||
this.onKeyUploadRequest(resolveOneTimeKeys, options);
|
||||
|
||||
// catch both r0 and v3 variants
|
||||
fetchMock.post(new URL("/_matrix/client/r0/keys/upload", homeserverUrl).toString(), listener);
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(), listener);
|
||||
});
|
||||
}
|
||||
|
||||
private async onKeyUploadRequest(onOnTimeKeysUploaded: () => void, options: RequestInit): Promise<object> {
|
||||
const content = JSON.parse(options.body as string);
|
||||
|
||||
// device keys may only be uploaded once
|
||||
if (content.device_keys && Object.keys(content.device_keys).length > 0) {
|
||||
if (this.deviceKeys) {
|
||||
throw new Error("Application attempted to upload E2E device keys multiple times");
|
||||
}
|
||||
this.debug(`received device keys`);
|
||||
this.deviceKeys = content.device_keys;
|
||||
}
|
||||
|
||||
if (content.one_time_keys && Object.keys(content.one_time_keys).length > 0) {
|
||||
// this is a one-time-key upload
|
||||
|
||||
// if we already have a batch of one-time keys, then slow-roll the response,
|
||||
// otherwise the client ends up tight-looping one-time-key-uploads and filling the logs with junk.
|
||||
if (Object.keys(this.oneTimeKeys).length > 0) {
|
||||
this.debug(`received second batch of one-time keys: blocking response`);
|
||||
await new Promise(() => {});
|
||||
}
|
||||
|
||||
this.debug(`received ${Object.keys(content.one_time_keys).length} one-time keys`);
|
||||
Object.assign(this.oneTimeKeys, content.one_time_keys);
|
||||
onOnTimeKeysUploaded();
|
||||
}
|
||||
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys).length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Get the uploaded Ed25519 key
|
||||
*
|
||||
* If device keys have not yet been uploaded, throws an error
|
||||
*/
|
||||
public getSigningKey(): string {
|
||||
if (!this.deviceKeys) {
|
||||
throw new Error("Device keys not yet uploaded");
|
||||
}
|
||||
const keyIds = Object.keys(this.deviceKeys.keys).filter((v) => v.startsWith("ed25519:"));
|
||||
if (keyIds.length != 1) {
|
||||
throw new Error(`Expected exactly 1 ed25519 key uploaded, got ${keyIds}`);
|
||||
}
|
||||
return this.deviceKeys.keys[keyIds[0]];
|
||||
}
|
||||
|
||||
/** Get the uploaded Curve25519 key
|
||||
*
|
||||
* If device keys have not yet been uploaded, throws an error
|
||||
*/
|
||||
public getDeviceKey(): string {
|
||||
if (!this.deviceKeys) {
|
||||
throw new Error("Device keys not yet uploaded");
|
||||
}
|
||||
const keyIds = Object.keys(this.deviceKeys.keys).filter((v) => v.startsWith("curve25519:"));
|
||||
if (keyIds.length != 1) {
|
||||
throw new Error(`Expected exactly 1 curve25519 key uploaded, got ${keyIds}`);
|
||||
}
|
||||
return this.deviceKeys.keys[keyIds[0]];
|
||||
}
|
||||
|
||||
/**
|
||||
* If one-time keys have already been uploaded, return them. Otherwise,
|
||||
* set up an expectation that the keys will be uploaded, and wait for
|
||||
* that to happen.
|
||||
*
|
||||
* @returns Promise for the one-time keys
|
||||
*/
|
||||
public async awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
|
||||
await this.oneTimeKeysPromise;
|
||||
return this.oneTimeKeys;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
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 debugFunc from "debug";
|
||||
import { Debugger } from "debug";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { MockResponse } from "fetch-mock";
|
||||
|
||||
/** Interface implemented by classes that intercept `/sync` requests from test clients
|
||||
*
|
||||
* Common interface implemented by {@link TestClient} and {@link SyncResponder}
|
||||
*/
|
||||
export interface ISyncResponder {
|
||||
/** Next time we see a sync request (or immediately, if there is one waiting), send the given response
|
||||
*
|
||||
* @param response - response to /sync request
|
||||
*/
|
||||
sendOrQueueSyncResponse(response: object): void;
|
||||
}
|
||||
|
||||
enum SyncResponderState {
|
||||
IDLE,
|
||||
WAITING_FOR_REQUEST,
|
||||
WAITING_FOR_RESPONSE,
|
||||
}
|
||||
|
||||
/** SyncResponder: An object which intercepts `/sync` fetches via fetch-mock.
|
||||
*
|
||||
* Two modes are possible:
|
||||
* * A response can be queued up; the next call to `/sync` will return it.
|
||||
* * If a call to `/sync` arrives before a response is queued, it will block until a call to {@link #sendOrQueueSyncResponse}.
|
||||
*/
|
||||
export class SyncResponder implements ISyncResponder {
|
||||
private readonly debug: Debugger;
|
||||
private state: SyncResponderState = SyncResponderState.IDLE;
|
||||
|
||||
/*
|
||||
* properties that are only valid in WAITING_FOR_REQUEST
|
||||
*/
|
||||
|
||||
/** the response to be sent when the request is made */
|
||||
private pendingResponse: object | null = null;
|
||||
|
||||
/*
|
||||
* properties that are only valid in WAITING_FOR_RESPONSE
|
||||
*/
|
||||
|
||||
/** a callback to be called with a response once one is registered.
|
||||
*
|
||||
* It will release the /sync request and update the state.
|
||||
*/
|
||||
private onResponseReceived: ((response: object) => void) | null = null;
|
||||
|
||||
/**
|
||||
* Construct a new SyncResponder.
|
||||
*
|
||||
* It will immediately register an intercept of `/sync` requests for the given homeserverUrl.
|
||||
* Only /sync requests made to this server will be intercepted: this allows a single test to use more than one
|
||||
* client and have overlapping /sync requests.
|
||||
*
|
||||
* @param homeserverUrl - the Homeserver Url of the client under test.
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
this.debug = debugFunc(`sync-responder:[${homeserverUrl}]`);
|
||||
fetchMock.get("begin:" + new URL("/_matrix/client/r0/sync?", homeserverUrl).toString(), (_url, _options) =>
|
||||
this.onSyncRequest(),
|
||||
);
|
||||
}
|
||||
|
||||
private async onSyncRequest(): Promise<MockResponse> {
|
||||
switch (this.state) {
|
||||
case SyncResponderState.IDLE: {
|
||||
this.debug("Got /sync request: waiting for response to be ready");
|
||||
const res = await new Promise<object>((resolve) => {
|
||||
this.onResponseReceived = resolve;
|
||||
this.state = SyncResponderState.WAITING_FOR_RESPONSE;
|
||||
});
|
||||
this.debug("Responding to /sync");
|
||||
this.state = SyncResponderState.IDLE;
|
||||
this.onResponseReceived = null;
|
||||
return res;
|
||||
}
|
||||
|
||||
case SyncResponderState.WAITING_FOR_REQUEST: {
|
||||
this.debug("Got /sync request: responding immediately with queued response");
|
||||
const res = this.pendingResponse!;
|
||||
this.state = SyncResponderState.IDLE;
|
||||
this.pendingResponse = null;
|
||||
return res;
|
||||
}
|
||||
|
||||
default:
|
||||
// we must already be in WAITING_FOR_RESPONSE, ie we already have a /sync request in progress
|
||||
throw new Error(`Got unexpected /sync request in state ${this.state}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Next time we see a sync request (or immediately, if there is one waiting), send the given response
|
||||
*
|
||||
* @param response - response to /sync request
|
||||
*/
|
||||
public sendOrQueueSyncResponse(response: object): void {
|
||||
switch (this.state) {
|
||||
case SyncResponderState.IDLE:
|
||||
this.pendingResponse = response;
|
||||
this.state = SyncResponderState.WAITING_FOR_REQUEST;
|
||||
break;
|
||||
|
||||
case SyncResponderState.WAITING_FOR_RESPONSE:
|
||||
this.onResponseReceived!(response);
|
||||
break;
|
||||
|
||||
default:
|
||||
// we already have a response queued
|
||||
throw new Error(`Cannot queue more than one /sync response`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ export const getMockClientWithEventEmitter = (
|
||||
*/
|
||||
export const mockClientMethodsUser = (userId = "@alice:domain") => ({
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
getSafeUserId: jest.fn().mockReturnValue(userId),
|
||||
getUser: jest.fn().mockReturnValue(new User(userId)),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
|
||||
@@ -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.
|
||||
@@ -450,6 +450,10 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
]
|
||||
>();
|
||||
|
||||
public isInitialSyncComplete(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public getMediaHandler(): MediaHandler {
|
||||
return this.mediaHandler.typed();
|
||||
}
|
||||
@@ -476,7 +480,7 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
|
||||
public getRoom = jest.fn();
|
||||
|
||||
public supportsExperimentalThreads(): boolean {
|
||||
public supportsThreads(): boolean {
|
||||
return true;
|
||||
}
|
||||
public async decryptEventIfNeeded(): Promise<void> {}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getMockClientWithEventEmitter } from "../test-utils/client";
|
||||
import { StubStore } from "../../src/store/stub";
|
||||
import { IndexedToDeviceBatch } from "../../src/models/ToDeviceMessage";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { defer } from "../../src/utils";
|
||||
|
||||
describe("onResumedSync", () => {
|
||||
let batch: IndexedToDeviceBatch | null;
|
||||
@@ -58,7 +59,9 @@ describe("onResumedSync", () => {
|
||||
queue = new ToDeviceMessageQueue(mockClient);
|
||||
});
|
||||
|
||||
it("resends queue after connectivity restored", (done) => {
|
||||
it("resends queue after connectivity restored", async () => {
|
||||
const deferred = defer();
|
||||
|
||||
onSendToDeviceFailure = () => {
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
|
||||
@@ -70,26 +73,32 @@ describe("onResumedSync", () => {
|
||||
onSendToDeviceSuccess = () => {
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(3);
|
||||
expect(store.removeToDeviceBatch).toHaveBeenCalled();
|
||||
done();
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
queue.start();
|
||||
return deferred.promise;
|
||||
});
|
||||
|
||||
it("does not resend queue if client sync still catching up", (done) => {
|
||||
it("does not resend queue if client sync still catching up", async () => {
|
||||
const deferred = defer();
|
||||
|
||||
onSendToDeviceFailure = () => {
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
|
||||
|
||||
resumeSync(SyncState.Catchup, SyncState.Catchup);
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
done();
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
queue.start();
|
||||
return deferred.promise;
|
||||
});
|
||||
|
||||
it("does not resend queue if connectivity restored after queue stopped", (done) => {
|
||||
it("does not resend queue if connectivity restored after queue stopped", async () => {
|
||||
const deferred = defer();
|
||||
|
||||
onSendToDeviceFailure = () => {
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
|
||||
@@ -98,9 +107,10 @@ describe("onResumedSync", () => {
|
||||
|
||||
resumeSync(SyncState.Syncing, SyncState.Catchup);
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
done();
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
queue.start();
|
||||
return deferred.promise;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -550,7 +550,7 @@ describe("Crypto", function () {
|
||||
aliceClient.crypto!.outgoingRoomKeyRequestManager.sendQueuedRequests();
|
||||
jest.runAllTimers();
|
||||
await Promise.resolve();
|
||||
expect(aliceSendToDevice).toBeCalledTimes(1);
|
||||
expect(aliceSendToDevice).toHaveBeenCalledTimes(1);
|
||||
const txnId = aliceSendToDevice.mock.calls[0][2];
|
||||
|
||||
// give the room key request manager time to update the state
|
||||
@@ -564,7 +564,7 @@ describe("Crypto", function () {
|
||||
// cancelAndResend will call sendToDevice twice:
|
||||
// the first call to sendToDevice will be the cancellation
|
||||
// the second call to sendToDevice will be the key request
|
||||
expect(aliceSendToDevice).toBeCalledTimes(3);
|
||||
expect(aliceSendToDevice).toHaveBeenCalledTimes(3);
|
||||
expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId);
|
||||
});
|
||||
|
||||
|
||||
@@ -148,6 +148,10 @@ describe("DeviceList", function () {
|
||||
dl.invalidateUserDeviceList("@test1:sw1v.org");
|
||||
dl.refreshOutdatedDeviceLists();
|
||||
|
||||
// TODO: Fix this test so we actually await the call and assertions and remove
|
||||
// the eslint disable, https://github.com/matrix-org/matrix-js-sdk/issues/2977
|
||||
//
|
||||
// eslint-disable-next-line jest/valid-expect-in-promise
|
||||
dl.saveIfDirty()
|
||||
.then(() => {
|
||||
// the first request completes
|
||||
@@ -196,7 +200,7 @@ describe("DeviceList", function () {
|
||||
downloadSpy.mockReturnValueOnce(queryDefer2.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toBeCalledTimes(2);
|
||||
expect(downloadSpy).toHaveBeenCalledTimes(2);
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(1, ["@test1:sw1v.org"], {});
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(2, ["@test2:sw1v.org"], {});
|
||||
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
|
||||
|
||||
@@ -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.
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
|
||||
import type { DeviceInfoMap } from "../../../../src/crypto/DeviceList";
|
||||
import "../../../olm-loader";
|
||||
import type { OutboundGroupSession } from "@matrix-org/olm";
|
||||
import * as algorithms from "../../../../src/crypto/algorithms";
|
||||
@@ -33,6 +34,7 @@ import { ClientEvent, MatrixClient, RoomMember } from "../../../../src";
|
||||
import { DeviceInfo, IDevice } from "../../../../src/crypto/deviceinfo";
|
||||
import { DeviceTrustLevel } from "../../../../src/crypto/CrossSigning";
|
||||
import { MegolmEncryption as MegolmEncryptionClass } from "../../../../src/crypto/algorithms/megolm";
|
||||
import { sleep } from "../../../../src/utils";
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
|
||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
|
||||
@@ -58,6 +60,12 @@ describe("MegolmDecryption", function () {
|
||||
|
||||
beforeEach(async function () {
|
||||
mockCrypto = testUtils.mock(Crypto, "Crypto") as MockedObject<Crypto>;
|
||||
|
||||
// @ts-ignore assigning to readonly prop
|
||||
mockCrypto.backupManager = {
|
||||
backupGroupSession: () => {},
|
||||
};
|
||||
|
||||
mockBaseApis = {
|
||||
claimOneTimeKeys: jest.fn(),
|
||||
sendToDevice: jest.fn(),
|
||||
@@ -203,7 +211,7 @@ describe("MegolmDecryption", function () {
|
||||
.then(() => {
|
||||
// check that it called encryptMessageForDevice with
|
||||
// appropriate args.
|
||||
expect(mockOlmLib.encryptMessageForDevice).toBeCalledTimes(1);
|
||||
expect(mockOlmLib.encryptMessageForDevice).toHaveBeenCalledTimes(1);
|
||||
|
||||
const call = mockOlmLib.encryptMessageForDevice.mock.calls[0];
|
||||
const payload = call[6];
|
||||
@@ -314,10 +322,6 @@ describe("MegolmDecryption", function () {
|
||||
let olmDevice: OlmDevice;
|
||||
|
||||
beforeEach(async () => {
|
||||
// @ts-ignore assigning to readonly prop
|
||||
mockCrypto.backupManager = {
|
||||
backupGroupSession: () => {},
|
||||
};
|
||||
const cryptoStore = new MemoryCryptoStore();
|
||||
|
||||
olmDevice = new OlmDevice(cryptoStore);
|
||||
@@ -515,6 +519,78 @@ describe("MegolmDecryption", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("prepareToEncrypt", () => {
|
||||
let megolm: MegolmEncryptionClass;
|
||||
let room: jest.Mocked<Room>;
|
||||
|
||||
const deviceMap: DeviceInfoMap = {
|
||||
"user-a": {
|
||||
"device-a": new DeviceInfo("device-a"),
|
||||
"device-b": new DeviceInfo("device-b"),
|
||||
"device-c": new DeviceInfo("device-c"),
|
||||
},
|
||||
"user-b": {
|
||||
"device-d": new DeviceInfo("device-d"),
|
||||
"device-e": new DeviceInfo("device-e"),
|
||||
"device-f": new DeviceInfo("device-f"),
|
||||
},
|
||||
"user-c": {
|
||||
"device-g": new DeviceInfo("device-g"),
|
||||
"device-h": new DeviceInfo("device-h"),
|
||||
"device-i": new DeviceInfo("device-i"),
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
room = testUtils.mock(Room, "Room") as jest.Mocked<Room>;
|
||||
room.getEncryptionTargetMembers.mockImplementation(async () => [
|
||||
new RoomMember(room.roomId, "@user:example.org"),
|
||||
]);
|
||||
room.getBlacklistUnverifiedDevices.mockReturnValue(false);
|
||||
|
||||
mockCrypto.downloadKeys.mockImplementation(async () => deviceMap);
|
||||
|
||||
mockCrypto.checkDeviceTrust.mockImplementation(() => new DeviceTrustLevel(true, true, true, true));
|
||||
|
||||
const olmDevice = new OlmDevice(new MemoryCryptoStore());
|
||||
megolm = new MegolmEncryptionClass({
|
||||
userId: "@user:id",
|
||||
deviceId: "12345",
|
||||
crypto: mockCrypto,
|
||||
olmDevice,
|
||||
baseApis: mockBaseApis,
|
||||
roomId: room.roomId,
|
||||
config: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
rotation_period_ms: 9_999_999,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("checks each device", async () => {
|
||||
megolm.prepareToEncrypt(room);
|
||||
//@ts-ignore private member access, gross
|
||||
await megolm.encryptionPreparation?.promise;
|
||||
|
||||
for (const userId in deviceMap) {
|
||||
for (const deviceId in deviceMap[userId]) {
|
||||
expect(mockCrypto.checkDeviceTrust).toHaveBeenCalledWith(userId, deviceId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("is cancellable", async () => {
|
||||
const stop = megolm.prepareToEncrypt(room);
|
||||
|
||||
const before = mockCrypto.checkDeviceTrust.mock.calls.length;
|
||||
stop();
|
||||
|
||||
// Ensure that no more devices were checked after cancellation.
|
||||
await sleep(10);
|
||||
expect(mockCrypto.checkDeviceTrust).toHaveBeenCalledTimes(before);
|
||||
});
|
||||
});
|
||||
|
||||
it("notifies devices that have been blocked", async function () {
|
||||
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
|
||||
const bobClient1 = new TestClient("@bob:example.com", "bobdevice1").client;
|
||||
|
||||
@@ -1148,6 +1148,6 @@ describe("userHasCrossSigningKeys", function () {
|
||||
|
||||
it("throws an error if crypto is disabled", () => {
|
||||
aliceClient["cryptoBackend"] = undefined;
|
||||
expect(() => aliceClient.userHasCrossSigningKeys()).toThrowError("encryption disabled");
|
||||
expect(() => aliceClient.userHasCrossSigningKeys()).toThrow("encryption disabled");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -215,7 +215,7 @@ describe("SAS verification", function () {
|
||||
]);
|
||||
|
||||
// make sure that it uses the preferred method
|
||||
expect(macMethod).toBe("org.matrix.msc3783.hkdf-hmac-sha256");
|
||||
expect(macMethod).toBe("hkdf-hmac-sha256.v2");
|
||||
expect(keyAgreement).toBe("curve25519-hkdf-sha256");
|
||||
|
||||
// make sure Alice and Bob verified each other
|
||||
|
||||
@@ -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.
|
||||
@@ -147,7 +147,7 @@ describe("EventTimelineSet", () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true);
|
||||
(client.supportsThreads as jest.Mock).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
@@ -179,7 +179,7 @@ describe("EventTimelineSet", () => {
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline2, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
}).toThrowError();
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("should not add a threaded reply to the main room timeline", () => {
|
||||
@@ -206,7 +206,7 @@ describe("EventTimelineSet", () => {
|
||||
});
|
||||
|
||||
it("should allow edits to be added to thread timeline", async () => {
|
||||
jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true);
|
||||
jest.spyOn(client, "supportsThreads").mockReturnValue(true);
|
||||
jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
|
||||
Thread.hasServerSideSupport = FeatureSupport.Stable;
|
||||
|
||||
@@ -393,13 +393,13 @@ describe("EventTimelineSet", () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true);
|
||||
(client.supportsThreads as jest.Mock).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
it("should throw if timeline set has no room", () => {
|
||||
const eventTimelineSet = new EventTimelineSet(undefined, {}, client);
|
||||
expect(() => eventTimelineSet.canContain(messageEvent)).toThrowError();
|
||||
expect(() => eventTimelineSet.canContain(messageEvent)).toThrow();
|
||||
});
|
||||
|
||||
it("should return false if timeline set is for thread but event is not threaded", () => {
|
||||
|
||||
@@ -150,26 +150,6 @@ describe("PollResponseEvent", () => {
|
||||
expect(response.spoiled).toBe(true);
|
||||
});
|
||||
|
||||
it("should spoil the vote when answers are empty", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(true);
|
||||
|
||||
response.validateAgainst(SAMPLE_POLL);
|
||||
expect(response.spoiled).toBe(true);
|
||||
});
|
||||
|
||||
it("should spoil the vote when answers are not strings", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
|
||||
@@ -78,11 +78,11 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
expect(doRequest).toHaveBeenCalledTimes(1);
|
||||
expect(stateUpdated).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should handle auth errcode presence ", async () => {
|
||||
it("should handle auth errcode presence", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
|
||||
@@ -128,8 +128,8 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
expect(doRequest).toHaveBeenCalledTimes(1);
|
||||
expect(stateUpdated).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should handle set emailSid for email flow", async () => {
|
||||
@@ -180,9 +180,9 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
expect(requestEmailToken).toBeCalledTimes(0);
|
||||
expect(doRequest).toHaveBeenCalledTimes(1);
|
||||
expect(stateUpdated).toHaveBeenCalledTimes(1);
|
||||
expect(requestEmailToken).toHaveBeenCalledTimes(0);
|
||||
expect(ia.getEmailSid()).toBe("myEmailSid");
|
||||
});
|
||||
|
||||
@@ -244,8 +244,8 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(2);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
expect(doRequest).toHaveBeenCalledTimes(2);
|
||||
expect(stateUpdated).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should make a request if authdata is null", async () => {
|
||||
@@ -306,8 +306,8 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(2);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
expect(doRequest).toHaveBeenCalledTimes(2);
|
||||
expect(stateUpdated).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should start an auth stage and reject if no auth flow", async () => {
|
||||
@@ -430,8 +430,8 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(0);
|
||||
expect(doRequest).toHaveBeenCalledTimes(1);
|
||||
expect(stateUpdated).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
describe("requestEmailToken", () => {
|
||||
@@ -464,35 +464,6 @@ describe("InteractiveAuth", () => {
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined);
|
||||
});
|
||||
|
||||
it("increases auth attempts", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest,
|
||||
stateUpdated,
|
||||
requestEmailToken,
|
||||
});
|
||||
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 1, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 2, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 3, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 4, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined);
|
||||
});
|
||||
|
||||
it("passes errors through", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
@@ -508,7 +479,7 @@ describe("InteractiveAuth", () => {
|
||||
requestEmailToken,
|
||||
});
|
||||
|
||||
await expect(ia.requestEmailToken.bind(ia)).rejects.toThrowError("unspecific network error");
|
||||
await expect(ia.requestEmailToken.bind(ia)).rejects.toThrow("unspecific network error");
|
||||
});
|
||||
|
||||
it("only starts one request at a time", async () => {
|
||||
|
||||
+435
-101
@@ -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.
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { logger } from "../../src/logger";
|
||||
import { ClientEvent, ITurnServerResponse, MatrixClient, Store } from "../../src/client";
|
||||
import { ClientEvent, IMatrixClientCreateOpts, ITurnServerResponse, MatrixClient, Store } from "../../src/client";
|
||||
import { Filter } from "../../src/filter";
|
||||
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
|
||||
import {
|
||||
@@ -276,7 +276,7 @@ describe("MatrixClient", function () {
|
||||
);
|
||||
}
|
||||
|
||||
function makeClient() {
|
||||
function makeClient(opts?: Partial<IMatrixClientCreateOpts>) {
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: identityServerUrl,
|
||||
@@ -285,6 +285,7 @@ describe("MatrixClient", function () {
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: userId,
|
||||
...(opts || {}),
|
||||
});
|
||||
// FIXME: We shouldn't be yanking http like this.
|
||||
client.http = (["authedRequest", "getContentUri", "request", "uploadContent"] as const).reduce((r, k) => {
|
||||
@@ -892,7 +893,7 @@ describe("MatrixClient", function () {
|
||||
describe("getOrCreateFilter", function () {
|
||||
it("should POST createFilter if no id is present in localStorage", function () {});
|
||||
it("should use an existing filter if id is present in localStorage", function () {});
|
||||
it("should handle localStorage filterId missing from the server", function (done) {
|
||||
it("should handle localStorage filterId missing from the server", async () => {
|
||||
function getFilterName(userId: string, suffix?: string) {
|
||||
// scope this on the user ID because people may login on many accounts
|
||||
// and they all need to be stored!
|
||||
@@ -918,10 +919,8 @@ describe("MatrixClient", function () {
|
||||
client.store.setFilterIdByName(filterName, invalidFilterId);
|
||||
const filter = new Filter(client.credentials.userId);
|
||||
|
||||
client.getOrCreateFilter(filterName, filter).then(function (filterId) {
|
||||
expect(filterId).toEqual(FILTER_RESPONSE.data?.filter_id);
|
||||
done();
|
||||
});
|
||||
const filterId = await client.getOrCreateFilter(filterName, filter);
|
||||
expect(filterId).toEqual(FILTER_RESPONSE.data?.filter_id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -932,7 +931,7 @@ describe("MatrixClient", function () {
|
||||
expect(client.retryImmediately()).toBe(false);
|
||||
});
|
||||
|
||||
it("should work on /filter", function (done) {
|
||||
it("should work on /filter", async () => {
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push({
|
||||
@@ -943,23 +942,26 @@ describe("MatrixClient", function () {
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(2);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener(ClientEvent.Sync, syncListener);
|
||||
done();
|
||||
} else {
|
||||
// unexpected state transition!
|
||||
expect(state).toEqual(null);
|
||||
}
|
||||
const wasPreparedPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(2);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener(ClientEvent.Sync, syncListener);
|
||||
resolve(null);
|
||||
} else {
|
||||
// unexpected state transition!
|
||||
expect(state).toEqual(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
client.startClient();
|
||||
await client.startClient();
|
||||
await wasPreparedPromise;
|
||||
});
|
||||
|
||||
it("should work on /sync", function (done) {
|
||||
it("should work on /sync", async () => {
|
||||
httpLookups.push({
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
@@ -971,22 +973,25 @@ describe("MatrixClient", function () {
|
||||
data: SYNC_DATA,
|
||||
});
|
||||
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(1);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
|
||||
jest.advanceTimersByTime(10000);
|
||||
} else if (state === "SYNCING" && httpLookups.length === 0) {
|
||||
client.removeListener(ClientEvent.Sync, syncListener);
|
||||
done();
|
||||
}
|
||||
const isSyncingPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(1);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
|
||||
jest.advanceTimersByTime(10000);
|
||||
} else if (state === "SYNCING" && httpLookups.length === 0) {
|
||||
client.removeListener(ClientEvent.Sync, syncListener);
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
client.startClient();
|
||||
await client.startClient();
|
||||
await isSyncingPromise;
|
||||
});
|
||||
|
||||
it("should work on /pushrules", function (done) {
|
||||
it("should work on /pushrules", async () => {
|
||||
httpLookups = [];
|
||||
httpLookups.push({
|
||||
method: "GET",
|
||||
@@ -997,20 +1002,23 @@ describe("MatrixClient", function () {
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(3);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener(ClientEvent.Sync, syncListener);
|
||||
done();
|
||||
} else {
|
||||
// unexpected state transition!
|
||||
expect(state).toEqual(null);
|
||||
}
|
||||
const wasPreparedPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(3);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener(ClientEvent.Sync, syncListener);
|
||||
resolve(null);
|
||||
} else {
|
||||
// unexpected state transition!
|
||||
expect(state).toEqual(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
client.startClient();
|
||||
await client.startClient();
|
||||
await wasPreparedPromise;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1034,14 +1042,17 @@ describe("MatrixClient", function () {
|
||||
};
|
||||
}
|
||||
|
||||
it("should transition null -> PREPARED after the first /sync", function (done) {
|
||||
it("should transition null -> PREPARED after the first /sync", async () => {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
await didSyncPromise;
|
||||
});
|
||||
|
||||
it("should transition null -> ERROR after a failed /filter", function (done) {
|
||||
it("should transition null -> ERROR after a failed /filter", async () => {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
@@ -1051,14 +1062,17 @@ describe("MatrixClient", function () {
|
||||
error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
expectedStates.push(["ERROR", null]);
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
await didSyncPromise;
|
||||
});
|
||||
|
||||
// Disabled because now `startClient` makes a legit call to `/versions`
|
||||
// And those tests are really unhappy about it... Not possible to figure
|
||||
// out what a good resolution would look like
|
||||
xit("should transition ERROR -> CATCHUP after /sync if prev failed", function (done) {
|
||||
it.skip("should transition ERROR -> CATCHUP after /sync if prev failed", async () => {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
acceptKeepalives = false;
|
||||
httpLookups = [];
|
||||
@@ -1088,19 +1102,25 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["CATCHUP", "ERROR"]);
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
await didSyncPromise;
|
||||
});
|
||||
|
||||
it("should transition PREPARED -> SYNCING after /sync", function (done) {
|
||||
it("should transition PREPARED -> SYNCING after /sync", async () => {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
await didSyncPromise;
|
||||
});
|
||||
|
||||
xit("should transition SYNCING -> ERROR after a failed /sync", function (done) {
|
||||
it.skip("should transition SYNCING -> ERROR after a failed /sync", async () => {
|
||||
acceptKeepalives = false;
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups.push({
|
||||
@@ -1118,11 +1138,14 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["RECONNECTING", "SYNCING"]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
await didSyncPromise;
|
||||
});
|
||||
|
||||
xit("should transition ERROR -> SYNCING after /sync if prev failed", function (done) {
|
||||
it.skip("should transition ERROR -> SYNCING after /sync if prev failed", async () => {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups.push({
|
||||
method: "GET",
|
||||
@@ -1134,11 +1157,14 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
await didSyncPromise;
|
||||
});
|
||||
|
||||
it("should transition SYNCING -> SYNCING on subsequent /sync successes", function (done) {
|
||||
it("should transition SYNCING -> SYNCING on subsequent /sync successes", async () => {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
@@ -1146,11 +1172,14 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["SYNCING", "SYNCING"]);
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
await didSyncPromise;
|
||||
});
|
||||
|
||||
xit("should transition ERROR -> ERROR if keepalive keeps failing", function (done) {
|
||||
it.skip("should transition ERROR -> ERROR if keepalive keeps failing", async () => {
|
||||
acceptKeepalives = false;
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups.push({
|
||||
@@ -1174,8 +1203,11 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["RECONNECTING", "SYNCING"]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["ERROR", "ERROR"]);
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
await didSyncPromise;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1213,7 +1245,7 @@ describe("MatrixClient", function () {
|
||||
expect(httpLookups.length).toBe(0);
|
||||
});
|
||||
|
||||
xit("should be able to peek into a room using peekInRoom", function (done) {});
|
||||
it.skip("should be able to peek into a room using peekInRoom", function () {});
|
||||
});
|
||||
|
||||
describe("getPresence", function () {
|
||||
@@ -1333,7 +1365,7 @@ describe("MatrixClient", function () {
|
||||
client.redactEvent(roomId, eventId, txnId, {
|
||||
with_relations: [RelationType.Reference],
|
||||
});
|
||||
}).toThrowError(
|
||||
}).toThrow(
|
||||
new Error(
|
||||
"Server does not support relation based redactions " +
|
||||
`roomId ${roomId} eventId ${eventId} txnId: ${txnId} threadId null`,
|
||||
@@ -1414,7 +1446,7 @@ describe("MatrixClient", function () {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
client.crypto = {
|
||||
client.crypto = client["cryptoBackend"] = {
|
||||
// mock crypto
|
||||
encryptEvent: () => new Promise(() => {}),
|
||||
stop: jest.fn(),
|
||||
@@ -1436,8 +1468,9 @@ describe("MatrixClient", function () {
|
||||
|
||||
it("should cancel an event which is encrypting", async () => {
|
||||
// @ts-ignore protected method access
|
||||
client.encryptAndSendEvent(null, event);
|
||||
client.encryptAndSendEvent(mockRoom, event);
|
||||
await testUtils.emitPromise(event, "Event.status");
|
||||
expect(event.status).toBe(EventStatus.ENCRYPTING);
|
||||
client.cancelPendingEvent(event);
|
||||
assertCancelled();
|
||||
});
|
||||
@@ -1456,9 +1489,20 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
describe("threads", () => {
|
||||
it.each([
|
||||
{ startOpts: {}, hasThreadSupport: false },
|
||||
{ startOpts: { threadSupport: true }, hasThreadSupport: true },
|
||||
{ startOpts: { threadSupport: false }, hasThreadSupport: false },
|
||||
{ startOpts: { experimentalThreadSupport: true }, hasThreadSupport: true },
|
||||
{ startOpts: { experimentalThreadSupport: true, threadSupport: false }, hasThreadSupport: false },
|
||||
])("enabled thread support for the SDK instance", async ({ startOpts, hasThreadSupport }) => {
|
||||
await client.startClient(startOpts);
|
||||
expect(client.supportsThreads()).toBe(hasThreadSupport);
|
||||
});
|
||||
|
||||
it("partitions root events to room timeline and thread timeline", () => {
|
||||
const supportsExperimentalThreads = client.supportsExperimentalThreads;
|
||||
client.supportsExperimentalThreads = () => true;
|
||||
const supportsThreads = client.supportsThreads;
|
||||
client.supportsThreads = () => true;
|
||||
const room = new Room("!room1:matrix.org", client, userId);
|
||||
|
||||
const rootEvent = new MatrixEvent({
|
||||
@@ -1487,7 +1531,7 @@ describe("MatrixClient", function () {
|
||||
expect(threadEvents).toHaveLength(1);
|
||||
|
||||
// Restore method
|
||||
client.supportsExperimentalThreads = supportsExperimentalThreads;
|
||||
client.supportsThreads = supportsThreads;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2205,7 +2249,7 @@ describe("MatrixClient", function () {
|
||||
"creator": "@daryl:alexandria.example.com",
|
||||
"m.federate": true,
|
||||
"predecessor": {
|
||||
event_id: "spec_is_not_clear_what_id_this_is",
|
||||
event_id: "id_of_last_event",
|
||||
room_id: predecessorRoomId,
|
||||
},
|
||||
"room_version": "9",
|
||||
@@ -2234,7 +2278,78 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
}
|
||||
|
||||
function predecessorEvent(newRoomId: string, predecessorRoomId: string): MatrixEvent {
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
predecessor_room_id: predecessorRoomId,
|
||||
},
|
||||
event_id: `predecessor_event_id_pred_${predecessorRoomId}`,
|
||||
origin_server_ts: 1432735824653,
|
||||
room_id: newRoomId,
|
||||
sender: "@daryl:alexandria.example.com",
|
||||
state_key: "",
|
||||
type: "org.matrix.msc3946.room_predecessor",
|
||||
});
|
||||
}
|
||||
|
||||
describe("getVisibleRooms", () => {
|
||||
function setUpReplacedRooms(): {
|
||||
room1: Room;
|
||||
room2: Room;
|
||||
replacedByCreate1: Room;
|
||||
replacedByCreate2: Room;
|
||||
replacedByDynamicPredecessor1: Room;
|
||||
replacedByDynamicPredecessor2: Room;
|
||||
} {
|
||||
const room1 = new Room("room1", client, "@carol:alexandria.example.com");
|
||||
const replacedByCreate1 = new Room("replacedByCreate1", client, "@carol:alexandria.example.com");
|
||||
const replacedByCreate2 = new Room("replacedByCreate2", client, "@carol:alexandria.example.com");
|
||||
const replacedByDynamicPredecessor1 = new Room("dyn1", client, "@carol:alexandria.example.com");
|
||||
const replacedByDynamicPredecessor2 = new Room("dyn2", client, "@carol:alexandria.example.com");
|
||||
const room2 = new Room("room2", client, "@daryl:alexandria.example.com");
|
||||
client.store = new StubStore();
|
||||
client.store.getRooms = () => [
|
||||
room1,
|
||||
replacedByCreate1,
|
||||
replacedByCreate2,
|
||||
replacedByDynamicPredecessor1,
|
||||
replacedByDynamicPredecessor2,
|
||||
room2,
|
||||
];
|
||||
room1.addLiveEvents(
|
||||
[
|
||||
roomCreateEvent(room1.roomId, replacedByCreate1.roomId),
|
||||
predecessorEvent(room1.roomId, replacedByDynamicPredecessor1.roomId),
|
||||
],
|
||||
{},
|
||||
);
|
||||
room2.addLiveEvents(
|
||||
[
|
||||
roomCreateEvent(room2.roomId, replacedByCreate2.roomId),
|
||||
predecessorEvent(room2.roomId, replacedByDynamicPredecessor2.roomId),
|
||||
],
|
||||
{},
|
||||
);
|
||||
replacedByCreate1.addLiveEvents([tombstoneEvent(room1.roomId, replacedByCreate1.roomId)], {});
|
||||
replacedByCreate2.addLiveEvents([tombstoneEvent(room2.roomId, replacedByCreate2.roomId)], {});
|
||||
replacedByDynamicPredecessor1.addLiveEvents(
|
||||
[tombstoneEvent(room1.roomId, replacedByDynamicPredecessor1.roomId)],
|
||||
{},
|
||||
);
|
||||
replacedByDynamicPredecessor2.addLiveEvents(
|
||||
[tombstoneEvent(room2.roomId, replacedByDynamicPredecessor2.roomId)],
|
||||
{},
|
||||
);
|
||||
|
||||
return {
|
||||
room1,
|
||||
room2,
|
||||
replacedByCreate1,
|
||||
replacedByCreate2,
|
||||
replacedByDynamicPredecessor1,
|
||||
replacedByDynamicPredecessor2,
|
||||
};
|
||||
}
|
||||
it("Returns an empty list if there are no rooms", () => {
|
||||
client.store = new StubStore();
|
||||
client.store.getRooms = () => [];
|
||||
@@ -2275,42 +2390,139 @@ describe("MatrixClient", function () {
|
||||
expect(rooms).toContain(room1);
|
||||
expect(rooms).toContain(room2);
|
||||
});
|
||||
|
||||
it("Ignores m.predecessor if we don't ask to use it", () => {
|
||||
// Given 6 rooms, 2 of which have been replaced, and 2 of which WERE
|
||||
// replaced by create events, but are now NOT replaced, because an
|
||||
// m.predecessor event has changed the room's predecessor.
|
||||
const {
|
||||
room1,
|
||||
room2,
|
||||
replacedByCreate1,
|
||||
replacedByCreate2,
|
||||
replacedByDynamicPredecessor1,
|
||||
replacedByDynamicPredecessor2,
|
||||
} = setUpReplacedRooms();
|
||||
|
||||
// When we ask for the visible rooms
|
||||
const rooms = client.getVisibleRooms(); // Don't supply msc3946ProcessDynamicPredecessor
|
||||
|
||||
// Then we only get the ones that have not been replaced
|
||||
expect(rooms).not.toContain(replacedByCreate1);
|
||||
expect(rooms).not.toContain(replacedByCreate2);
|
||||
expect(rooms).toContain(replacedByDynamicPredecessor1);
|
||||
expect(rooms).toContain(replacedByDynamicPredecessor2);
|
||||
expect(rooms).toContain(room1);
|
||||
expect(rooms).toContain(room2);
|
||||
});
|
||||
|
||||
it("Considers rooms replaced with m.predecessor events to be replaced", () => {
|
||||
// Given 6 rooms, 2 of which have been replaced, and 2 of which WERE
|
||||
// replaced by create events, but are now NOT replaced, because an
|
||||
// m.predecessor event has changed the room's predecessor.
|
||||
const {
|
||||
room1,
|
||||
room2,
|
||||
replacedByCreate1,
|
||||
replacedByCreate2,
|
||||
replacedByDynamicPredecessor1,
|
||||
replacedByDynamicPredecessor2,
|
||||
} = setUpReplacedRooms();
|
||||
|
||||
// When we ask for the visible rooms
|
||||
const useMsc3946 = true;
|
||||
const rooms = client.getVisibleRooms(useMsc3946);
|
||||
|
||||
// Then we only get the ones that have not been replaced
|
||||
expect(rooms).not.toContain(replacedByDynamicPredecessor1);
|
||||
expect(rooms).not.toContain(replacedByDynamicPredecessor2);
|
||||
expect(rooms).toContain(replacedByCreate1);
|
||||
expect(rooms).toContain(replacedByCreate2);
|
||||
expect(rooms).toContain(room1);
|
||||
expect(rooms).toContain(room2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRoomUpgradeHistory", () => {
|
||||
function createRoomHistory(): [Room, Room, Room, Room] {
|
||||
/**
|
||||
* Create a chain of room history with create events and tombstones.
|
||||
*
|
||||
* @param creates include create events (default=true)
|
||||
* @param tombstones include tomstone events (default=true)
|
||||
* @returns 4 rooms chained together with tombstones and create
|
||||
* events, in order from oldest to latest.
|
||||
*/
|
||||
function createRoomHistory(creates = true, tombstones = true): [Room, Room, Room, Room] {
|
||||
const room1 = new Room("room1", client, "@carol:alexandria.example.com");
|
||||
const room2 = new Room("room2", client, "@daryl:alexandria.example.com");
|
||||
const room3 = new Room("room3", client, "@rick:helicopter.example.com");
|
||||
const room4 = new Room("room4", client, "@michonne:hawthorne.example.com");
|
||||
|
||||
room1.addLiveEvents([tombstoneEvent(room2.roomId, room1.roomId)], {});
|
||||
room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)]);
|
||||
if (creates) {
|
||||
room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)]);
|
||||
room3.addLiveEvents([roomCreateEvent(room3.roomId, room2.roomId)]);
|
||||
room4.addLiveEvents([roomCreateEvent(room4.roomId, room3.roomId)]);
|
||||
}
|
||||
|
||||
room2.addLiveEvents([tombstoneEvent(room3.roomId, room2.roomId)], {});
|
||||
room3.addLiveEvents([roomCreateEvent(room3.roomId, room2.roomId)]);
|
||||
|
||||
room3.addLiveEvents([tombstoneEvent(room4.roomId, room3.roomId)], {});
|
||||
room4.addLiveEvents([roomCreateEvent(room4.roomId, room3.roomId)]);
|
||||
if (tombstones) {
|
||||
room1.addLiveEvents([tombstoneEvent(room2.roomId, room1.roomId)], {});
|
||||
room2.addLiveEvents([tombstoneEvent(room3.roomId, room2.roomId)], {});
|
||||
room3.addLiveEvents([tombstoneEvent(room4.roomId, room3.roomId)], {});
|
||||
}
|
||||
|
||||
mocked(store.getRoom).mockImplementation((roomId: string) => {
|
||||
switch (roomId) {
|
||||
case "room1":
|
||||
return room1;
|
||||
case "room2":
|
||||
return room2;
|
||||
case "room3":
|
||||
return room3;
|
||||
case "room4":
|
||||
return room4;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return { room1, room2, room3, room4 }[roomId] || null;
|
||||
});
|
||||
|
||||
return [room1, room2, room3, room4];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates 2 alternate chains of room history: one using create
|
||||
* events, and one using MSC2946 predecessor+tombstone events.
|
||||
*
|
||||
* Using create, history looks like:
|
||||
* room1->room2->room3->room4 (but note we do not create tombstones)
|
||||
*
|
||||
* Using predecessor+tombstone, history looks like:
|
||||
* dynRoom1->dynRoom2->room3->dynRoom4->dynRoom4
|
||||
*
|
||||
* @returns [room1, room2, room3, room4, dynRoom1, dynRoom2,
|
||||
* dynRoom4, dynRoom5].
|
||||
*/
|
||||
function createDynamicRoomHistory(): [Room, Room, Room, Room, Room, Room, Room, Room] {
|
||||
// Don't create tombstones for the old versions - we generally
|
||||
// expect only one tombstone in a room, and we are confused by
|
||||
// anything else.
|
||||
const creates = true;
|
||||
const tombstones = false;
|
||||
const [room1, room2, room3, room4] = createRoomHistory(creates, tombstones);
|
||||
const dynRoom1 = new Room("dynRoom1", client, "@rick:grimes.example.com");
|
||||
const dynRoom2 = new Room("dynRoom2", client, "@rick:grimes.example.com");
|
||||
const dynRoom4 = new Room("dynRoom4", client, "@rick:grimes.example.com");
|
||||
const dynRoom5 = new Room("dynRoom5", client, "@rick:grimes.example.com");
|
||||
|
||||
dynRoom1.addLiveEvents([tombstoneEvent(dynRoom2.roomId, dynRoom1.roomId)], {});
|
||||
dynRoom2.addLiveEvents([predecessorEvent(dynRoom2.roomId, dynRoom1.roomId)]);
|
||||
|
||||
dynRoom2.addLiveEvents([tombstoneEvent(room3.roomId, dynRoom2.roomId)], {});
|
||||
room3.addLiveEvents([predecessorEvent(room3.roomId, dynRoom2.roomId)]);
|
||||
|
||||
room3.addLiveEvents([tombstoneEvent(dynRoom4.roomId, room3.roomId)], {});
|
||||
dynRoom4.addLiveEvents([predecessorEvent(dynRoom4.roomId, room3.roomId)]);
|
||||
|
||||
dynRoom4.addLiveEvents([tombstoneEvent(dynRoom5.roomId, dynRoom4.roomId)], {});
|
||||
dynRoom5.addLiveEvents([predecessorEvent(dynRoom5.roomId, dynRoom4.roomId)]);
|
||||
|
||||
mocked(store.getRoom)
|
||||
.mockClear()
|
||||
.mockImplementation((roomId: string) => {
|
||||
return { room1, room2, room3, room4, dynRoom1, dynRoom2, dynRoom4, dynRoom5 }[roomId] || null;
|
||||
});
|
||||
|
||||
return [room1, room2, room3, room4, dynRoom1, dynRoom2, dynRoom4, dynRoom5];
|
||||
}
|
||||
|
||||
it("Returns an empty list if room does not exist", () => {
|
||||
const history = client.getRoomUpgradeHistory("roomthatdoesnotexist");
|
||||
expect(history).toHaveLength(0);
|
||||
@@ -2334,6 +2546,49 @@ describe("MatrixClient", function () {
|
||||
]);
|
||||
});
|
||||
|
||||
it("Returns the predecessors of this room (with verify links)", () => {
|
||||
const [room1, room2, room3, room4] = createRoomHistory();
|
||||
const verifyLinks = true;
|
||||
const history = client.getRoomUpgradeHistory(room4.roomId, verifyLinks);
|
||||
expect(history.map((room) => room.roomId)).toEqual([
|
||||
room1.roomId,
|
||||
room2.roomId,
|
||||
room3.roomId,
|
||||
room4.roomId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("With verify links, rejects predecessors that don't point forwards", () => {
|
||||
// Given successors point back with create events, but
|
||||
// predecessors do not point forwards with tombstones
|
||||
const [, , , room4] = createRoomHistory(true, false);
|
||||
|
||||
// When I ask for history with verifyLinks on
|
||||
const verifyLinks = true;
|
||||
const history = client.getRoomUpgradeHistory(room4.roomId, verifyLinks);
|
||||
|
||||
// Then the predecessors are not included in the history
|
||||
expect(history.map((room) => room.roomId)).toEqual([room4.roomId]);
|
||||
});
|
||||
|
||||
it("Without verify links, includes predecessors that don't point forwards", () => {
|
||||
// Given successors point back with create events, but
|
||||
// predecessors do not point forwards with tombstones
|
||||
const [room1, room2, room3, room4] = createRoomHistory(true, false);
|
||||
|
||||
// When I ask for history with verifyLinks off
|
||||
const verifyLinks = false;
|
||||
const history = client.getRoomUpgradeHistory(room4.roomId, verifyLinks);
|
||||
|
||||
// Then the predecessors are included in the history
|
||||
expect(history.map((room) => room.roomId)).toEqual([
|
||||
room1.roomId,
|
||||
room2.roomId,
|
||||
room3.roomId,
|
||||
room4.roomId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("Returns the subsequent rooms", () => {
|
||||
const [room1, room2, room3, room4] = createRoomHistory();
|
||||
const history = client.getRoomUpgradeHistory(room1.roomId);
|
||||
@@ -2345,6 +2600,49 @@ describe("MatrixClient", function () {
|
||||
]);
|
||||
});
|
||||
|
||||
it("Returns the subsequent rooms (with verify links)", () => {
|
||||
const [room1, room2, room3, room4] = createRoomHistory();
|
||||
const verifyLinks = true;
|
||||
const history = client.getRoomUpgradeHistory(room1.roomId, verifyLinks);
|
||||
expect(history.map((room) => room.roomId)).toEqual([
|
||||
room1.roomId,
|
||||
room2.roomId,
|
||||
room3.roomId,
|
||||
room4.roomId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("With verify links, rejects successors that don't point backwards", () => {
|
||||
// Given predecessors point forwards with tombstones, but
|
||||
// successors do not point back with create events.
|
||||
const [room1, , ,] = createRoomHistory(false, true);
|
||||
|
||||
// When I ask for history with verifyLinks on
|
||||
const verifyLinks = true;
|
||||
const history = client.getRoomUpgradeHistory(room1.roomId, verifyLinks);
|
||||
|
||||
// Then the successors are not included in the history
|
||||
expect(history.map((room) => room.roomId)).toEqual([room1.roomId]);
|
||||
});
|
||||
|
||||
it("Without verify links, includes successors that don't point backwards", () => {
|
||||
// Given predecessors point forwards with tombstones, but
|
||||
// successors do not point back with create events.
|
||||
const [room1, room2, room3, room4] = createRoomHistory(false, true);
|
||||
|
||||
// When I ask for history with verifyLinks off
|
||||
const verifyLinks = false;
|
||||
const history = client.getRoomUpgradeHistory(room1.roomId, verifyLinks);
|
||||
|
||||
// Then the successors are included in the history
|
||||
expect(history.map((room) => room.roomId)).toEqual([
|
||||
room1.roomId,
|
||||
room2.roomId,
|
||||
room3.roomId,
|
||||
room4.roomId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("Returns the predecessors and subsequent rooms", () => {
|
||||
const [room1, room2, room3, room4] = createRoomHistory();
|
||||
const history = client.getRoomUpgradeHistory(room3.roomId);
|
||||
@@ -2355,6 +2653,42 @@ describe("MatrixClient", function () {
|
||||
room4.roomId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("Returns the predecessors and subsequent rooms (with verify links)", () => {
|
||||
const [room1, room2, room3, room4] = createRoomHistory();
|
||||
const verifyLinks = true;
|
||||
const history = client.getRoomUpgradeHistory(room3.roomId, verifyLinks);
|
||||
expect(history.map((room) => room.roomId)).toEqual([
|
||||
room1.roomId,
|
||||
room2.roomId,
|
||||
room3.roomId,
|
||||
room4.roomId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("Returns the predecessors and subsequent rooms using MSC3945 dynamic room predecessors", () => {
|
||||
const [, , room3, , dynRoom1, dynRoom2, dynRoom4, dynRoom5] = createDynamicRoomHistory();
|
||||
const useMsc3946 = true;
|
||||
const verifyLinks = false;
|
||||
const history = client.getRoomUpgradeHistory(room3.roomId, verifyLinks, useMsc3946);
|
||||
expect(history.map((room) => room.roomId)).toEqual([
|
||||
dynRoom1.roomId,
|
||||
dynRoom2.roomId,
|
||||
room3.roomId,
|
||||
dynRoom4.roomId,
|
||||
dynRoom5.roomId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("When not asking for MSC3946, verified history without tombstones is empty", () => {
|
||||
// There no tombstones to match the create events
|
||||
const [, , room3] = createDynamicRoomHistory();
|
||||
const useMsc3946 = false;
|
||||
const verifyLinks = true;
|
||||
const history = client.getRoomUpgradeHistory(room3.roomId, verifyLinks, useMsc3946);
|
||||
// So we get no history back
|
||||
expect(history.map((room) => room.roomId)).toEqual([room3.roomId]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -126,12 +126,31 @@ describe("MatrixEvent", () => {
|
||||
expect(encryptedEvent.isEncrypted()).toBeTruthy();
|
||||
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
|
||||
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
|
||||
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeFalsy();
|
||||
expect(encryptedEvent.getContent()).toEqual({
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: "** Unable to decrypt: Error: test error **",
|
||||
});
|
||||
});
|
||||
|
||||
it(`should report "DecryptionError: The sender has disabled encrypting to unverified devices."`, async () => {
|
||||
const crypto = {
|
||||
decryptEvent: jest
|
||||
.fn()
|
||||
.mockRejectedValue("DecryptionError: The sender has disabled encrypting to unverified devices."),
|
||||
} as unknown as Crypto;
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
expect(encryptedEvent.isEncrypted()).toBeTruthy();
|
||||
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
|
||||
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
|
||||
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeTruthy();
|
||||
expect(encryptedEvent.getContent()).toEqual({
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: "** Unable to decrypt: DecryptionError: The sender has disabled encrypting to unverified devices. **",
|
||||
});
|
||||
});
|
||||
|
||||
it("should retry decryption if a retry is queued", async () => {
|
||||
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, "attemptDecryption");
|
||||
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
/*
|
||||
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 { IEvent, MatrixEvent, PollEvent, Room } from "../../../src";
|
||||
import { REFERENCE_RELATION } from "../../../src/@types/extensible_events";
|
||||
import { M_POLL_END, M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE } from "../../../src/@types/polls";
|
||||
import { PollStartEvent } from "../../../src/extensible_events_v1/PollStartEvent";
|
||||
import { Poll } from "../../../src/models/poll";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils/client";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("Poll", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(true),
|
||||
relations: jest.fn(),
|
||||
});
|
||||
const roomId = "!room:server";
|
||||
const room = new Room(roomId, mockClient, userId);
|
||||
const maySendRedactionForEventSpy = jest.spyOn(room.currentState, "maySendRedactionForEvent");
|
||||
// 14.03.2022 16:15
|
||||
const now = 1647270879403;
|
||||
|
||||
const basePollStartEvent = new MatrixEvent({
|
||||
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
||||
room_id: roomId,
|
||||
sender: userId,
|
||||
});
|
||||
basePollStartEvent.event.event_id = "$12345";
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.setSystemTime(now);
|
||||
|
||||
mockClient.relations.mockReset().mockResolvedValue({ events: [] });
|
||||
|
||||
maySendRedactionForEventSpy.mockClear().mockReturnValue(true);
|
||||
});
|
||||
|
||||
let eventId = 1;
|
||||
const makeRelatedEvent = (eventProps: Partial<IEvent>, timestamp = now): MatrixEvent => {
|
||||
const event = new MatrixEvent({
|
||||
...eventProps,
|
||||
content: {
|
||||
...(eventProps.content || {}),
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: basePollStartEvent.getId(),
|
||||
},
|
||||
},
|
||||
});
|
||||
event.event.origin_server_ts = timestamp;
|
||||
event.event.event_id = `${eventId++}`;
|
||||
return event;
|
||||
};
|
||||
|
||||
it("initialises with root event", () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
expect(poll.roomId).toEqual(roomId);
|
||||
expect(poll.pollId).toEqual(basePollStartEvent.getId());
|
||||
expect(poll.pollEvent).toEqual(basePollStartEvent.unstableExtensibleEvent);
|
||||
expect(poll.isEnded).toBe(false);
|
||||
expect(poll.endEventId).toBe(undefined);
|
||||
});
|
||||
|
||||
it("throws when poll start has no room id", () => {
|
||||
const pollStartEvent = new MatrixEvent(
|
||||
PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
||||
);
|
||||
expect(() => new Poll(pollStartEvent, mockClient, room)).toThrow("Invalid poll start event.");
|
||||
});
|
||||
|
||||
it("throws when poll start has no event id", () => {
|
||||
const pollStartEvent = new MatrixEvent({
|
||||
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
||||
room_id: roomId,
|
||||
});
|
||||
expect(() => new Poll(pollStartEvent, mockClient, room)).toThrow("Invalid poll start event.");
|
||||
});
|
||||
|
||||
describe("fetching responses", () => {
|
||||
it("calls relations api and emits", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const emitSpy = jest.spyOn(poll, "emit");
|
||||
const fetchResponsePromise = poll.getResponses();
|
||||
expect(poll.isFetchingResponses).toBe(true);
|
||||
const responses = await fetchResponsePromise;
|
||||
expect(poll.isFetchingResponses).toBe(false);
|
||||
expect(mockClient.relations).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
basePollStartEvent.getId(),
|
||||
"m.reference",
|
||||
undefined,
|
||||
{ from: undefined },
|
||||
);
|
||||
expect(emitSpy).toHaveBeenCalledWith(PollEvent.Responses, responses);
|
||||
});
|
||||
|
||||
it("returns existing responses object after initial fetch", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const responses = await poll.getResponses();
|
||||
const responses2 = await poll.getResponses();
|
||||
// only fetched relations once
|
||||
expect(mockClient.relations).toHaveBeenCalledTimes(1);
|
||||
// strictly equal
|
||||
expect(responses).toBe(responses2);
|
||||
});
|
||||
|
||||
it("waits for existing relations request to finish when getting responses", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const firstResponsePromise = poll.getResponses();
|
||||
const secondResponsePromise = poll.getResponses();
|
||||
await firstResponsePromise;
|
||||
expect(firstResponsePromise).toEqual(secondResponsePromise);
|
||||
await secondResponsePromise;
|
||||
expect(mockClient.relations).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("filters relations for relevent response events", async () => {
|
||||
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
|
||||
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
|
||||
const unstableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
||||
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [replyEvent, stableResponseEvent, unstableResponseEvent],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const responses = await poll.getResponses();
|
||||
expect(responses.getRelations()).toEqual([stableResponseEvent, unstableResponseEvent]);
|
||||
});
|
||||
|
||||
describe("with multiple pages of relations", () => {
|
||||
const makeResponses = (count = 1, timestamp = now): MatrixEvent[] =>
|
||||
new Array(count)
|
||||
.fill("x")
|
||||
.map((_x, index) =>
|
||||
makeRelatedEvent(
|
||||
{ type: M_POLL_RESPONSE.stable!, sender: "@bob@server.org" },
|
||||
timestamp + index,
|
||||
),
|
||||
);
|
||||
|
||||
it("page relations responses", async () => {
|
||||
const responseEvents = makeResponses(6);
|
||||
mockClient.relations
|
||||
.mockResolvedValueOnce({
|
||||
events: responseEvents.slice(0, 2),
|
||||
nextBatch: "test-next-1",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
events: responseEvents.slice(2, 4),
|
||||
nextBatch: "test-next-2",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
events: responseEvents.slice(4),
|
||||
});
|
||||
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
const responses = await poll.getResponses();
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(mockClient.relations.mock.calls).toEqual([
|
||||
[roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: undefined }],
|
||||
[roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: "test-next-1" }],
|
||||
[roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: "test-next-2" }],
|
||||
]);
|
||||
|
||||
expect(poll.emit).toHaveBeenCalledTimes(3);
|
||||
expect(poll.isFetchingResponses).toBeFalsy();
|
||||
expect(responses.getRelations().length).toEqual(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe("undecryptable relations", () => {
|
||||
it("counts undecryptable relation events when getting responses", async () => {
|
||||
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
|
||||
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
|
||||
const undecryptableEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
||||
jest.spyOn(undecryptableEvent, "isDecryptionFailure").mockReturnValue(true);
|
||||
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [replyEvent, stableResponseEvent, undecryptableEvent],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
expect(poll.undecryptableRelationsCount).toBe(1);
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.UndecryptableRelations, 1);
|
||||
});
|
||||
|
||||
it("adds to undercryptable event count when new relation is undecryptable", async () => {
|
||||
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
|
||||
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
|
||||
const undecryptableEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
||||
const undecryptableEvent2 = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
||||
jest.spyOn(undecryptableEvent, "isDecryptionFailure").mockReturnValue(true);
|
||||
jest.spyOn(undecryptableEvent2, "isDecryptionFailure").mockReturnValue(true);
|
||||
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [replyEvent, stableResponseEvent, undecryptableEvent],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
expect(poll.undecryptableRelationsCount).toBe(1);
|
||||
|
||||
await poll.onNewRelation(undecryptableEvent2);
|
||||
|
||||
expect(poll.undecryptableRelationsCount).toBe(2);
|
||||
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.UndecryptableRelations, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with poll end event", () => {
|
||||
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@bob@server.org" });
|
||||
const unstablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.unstable!, sender: "@bob@server.org" });
|
||||
const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000);
|
||||
const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
|
||||
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd, stablePollEndEvent],
|
||||
});
|
||||
});
|
||||
|
||||
it("sets poll end event with stable event type", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
|
||||
expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@bob@server.org");
|
||||
expect(poll.isEnded).toBe(true);
|
||||
expect(poll.endEventId).toBe(stablePollEndEvent.getId()!);
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
});
|
||||
|
||||
it("sets poll end event when endevent sender also created the poll, but does not have redaction rights", async () => {
|
||||
const pollStartEvent = new MatrixEvent({
|
||||
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
||||
room_id: roomId,
|
||||
sender: "@bob:domain.org",
|
||||
});
|
||||
pollStartEvent.event.event_id = "$6789";
|
||||
const poll = new Poll(pollStartEvent, mockClient, room);
|
||||
const pollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@bob:domain.org" });
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [pollEndEvent],
|
||||
});
|
||||
maySendRedactionForEventSpy.mockReturnValue(false);
|
||||
jest.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
|
||||
expect(maySendRedactionForEventSpy).not.toHaveBeenCalled();
|
||||
expect(poll.isEnded).toBe(true);
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
});
|
||||
|
||||
it("sets poll end event with unstable event type", async () => {
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [unstablePollEndEvent],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
|
||||
expect(poll.isEnded).toBe(true);
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
});
|
||||
|
||||
it("filters out responses that were sent after poll end", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const responses = await poll.getResponses();
|
||||
|
||||
// just response type events
|
||||
// and response with ts after poll end event is excluded
|
||||
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("onNewRelation()", () => {
|
||||
it("discards response if poll responses have not been initialised", () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
const responseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
|
||||
|
||||
poll.onNewRelation(responseEvent);
|
||||
|
||||
// did not add response -> no emit
|
||||
expect(poll.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets poll end event when responses are not initialised", () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: userId });
|
||||
|
||||
poll.onNewRelation(stablePollEndEvent);
|
||||
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
});
|
||||
|
||||
it("does not set poll end event when sent by invalid user", async () => {
|
||||
maySendRedactionForEventSpy.mockReturnValue(false);
|
||||
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@charlie:server.org" });
|
||||
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [responseEventAfterEnd],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
await poll.getResponses();
|
||||
jest.spyOn(poll, "emit");
|
||||
|
||||
poll.onNewRelation(stablePollEndEvent);
|
||||
|
||||
// didn't end, didn't refilter responses
|
||||
expect(poll.emit).not.toHaveBeenCalled();
|
||||
expect(poll.isEnded).toBeFalsy();
|
||||
expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@charlie:server.org");
|
||||
});
|
||||
|
||||
it("replaces poll end event and refilters when an older end event already exists", async () => {
|
||||
const earlierPollEndEvent = makeRelatedEvent(
|
||||
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
||||
now,
|
||||
);
|
||||
const laterPollEndEvent = makeRelatedEvent(
|
||||
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
||||
now + 2000,
|
||||
);
|
||||
const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000);
|
||||
const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
|
||||
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd, laterPollEndEvent],
|
||||
});
|
||||
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const responses = await poll.getResponses();
|
||||
|
||||
// all responses have a timestamp < laterPollEndEvent
|
||||
expect(responses.getRelations().length).toEqual(3);
|
||||
// first end event set correctly
|
||||
expect(poll.isEnded).toBeTruthy();
|
||||
|
||||
// reset spy count
|
||||
jest.spyOn(poll, "emit").mockClear();
|
||||
|
||||
// add a valid end event with earlier timestamp
|
||||
poll.onNewRelation(earlierPollEndEvent);
|
||||
|
||||
// emitted new end event
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
// filtered responses and emitted
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses);
|
||||
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
|
||||
});
|
||||
|
||||
it("does not set poll end event when an earlier end event already exists", async () => {
|
||||
const earlierPollEndEvent = makeRelatedEvent(
|
||||
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
||||
now,
|
||||
);
|
||||
const laterPollEndEvent = makeRelatedEvent(
|
||||
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
||||
now + 2000,
|
||||
);
|
||||
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
await poll.getResponses();
|
||||
|
||||
poll.onNewRelation(earlierPollEndEvent);
|
||||
|
||||
// first end event set correctly
|
||||
expect(poll.isEnded).toBeTruthy();
|
||||
|
||||
// reset spy count
|
||||
jest.spyOn(poll, "emit").mockClear();
|
||||
|
||||
poll.onNewRelation(laterPollEndEvent);
|
||||
// didn't set new end event, didn't refilter responses
|
||||
expect(poll.emit).not.toHaveBeenCalled();
|
||||
expect(poll.isEnded).toBeTruthy();
|
||||
});
|
||||
|
||||
it("sets poll end event and refilters responses based on timestamp", async () => {
|
||||
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: userId });
|
||||
const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000);
|
||||
const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
|
||||
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const responses = await poll.getResponses();
|
||||
jest.spyOn(poll, "emit");
|
||||
|
||||
expect(responses.getRelations().length).toEqual(3);
|
||||
poll.onNewRelation(stablePollEndEvent);
|
||||
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses);
|
||||
expect(responses.getRelations().length).toEqual(2);
|
||||
// after end timestamp event is removed
|
||||
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
|
||||
});
|
||||
|
||||
it("filters out irrelevant relations", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
// init responses
|
||||
const responses = await poll.getResponses();
|
||||
jest.spyOn(poll, "emit");
|
||||
const replyEvent = new MatrixEvent({ type: "m.room.message" });
|
||||
|
||||
poll.onNewRelation(replyEvent);
|
||||
|
||||
// did not add response -> no emit
|
||||
expect(poll.emit).not.toHaveBeenCalled();
|
||||
expect(responses.getRelations().length).toEqual(0);
|
||||
});
|
||||
|
||||
it("adds poll response relations to responses", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
// init responses
|
||||
const responses = await poll.getResponses();
|
||||
jest.spyOn(poll, "emit");
|
||||
const responseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
|
||||
|
||||
poll.onNewRelation(responseEvent);
|
||||
|
||||
// did not add response -> no emit
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses);
|
||||
expect(responses.getRelations()).toEqual([responseEvent]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -82,9 +82,10 @@ describe("Thread", () => {
|
||||
beforeEach(() => {
|
||||
client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||
getRoom: jest.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
|
||||
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
client.reEmitter = mock(ReEmitter, "ReEmitter");
|
||||
client.canSupport = new Map();
|
||||
@@ -193,9 +194,10 @@ describe("Thread", () => {
|
||||
beforeEach(() => {
|
||||
client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||
getRoom: jest.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
|
||||
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
client.reEmitter = mock(ReEmitter, "ReEmitter");
|
||||
client.canSupport = new Map();
|
||||
|
||||
@@ -53,10 +53,11 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
beforeEach(() => {
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||
getPushActionsForEvent: jest.fn().mockReturnValue(mkPushAction(true, true)),
|
||||
getRoom: jest.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
|
||||
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
mockClient.reEmitter = mock(ReEmitter, "ReEmitter");
|
||||
mockClient.canSupport = new Map();
|
||||
@@ -134,7 +135,7 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, event);
|
||||
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(2);
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(3);
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
|
||||
});
|
||||
|
||||
@@ -154,11 +155,11 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, threadEvent);
|
||||
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(2);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(1);
|
||||
});
|
||||
|
||||
it("does not change the room count when there's no unread count", () => {
|
||||
it("does not change the thread count when there's no unread count", () => {
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||
|
||||
@@ -192,6 +193,31 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
|
||||
});
|
||||
|
||||
it("does not change the total room count when an event is marked as non-notifying", () => {
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 0);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
|
||||
|
||||
event.getPushActions = jest.fn().mockReturnValue(mkPushAction(true, false));
|
||||
mockClient.getPushActionsForEvent = jest.fn().mockReturnValue(mkPushAction(false, false));
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, event);
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0);
|
||||
});
|
||||
|
||||
it("does not change the total room count when a threaded event is marked as non-notifying", () => {
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||
|
||||
threadEvent.getPushActions = jest.fn().mockReturnValue(mkPushAction(true, false));
|
||||
mockClient.getPushActionsForEvent = jest.fn().mockReturnValue(mkPushAction(false, false));
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, event);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
|
||||
});
|
||||
|
||||
it("emits events", () => {
|
||||
const cb = jest.fn();
|
||||
room.on(RoomEvent.UnreadNotifications, cb);
|
||||
|
||||
@@ -18,7 +18,6 @@ import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import { MAIN_ROOM_TIMELINE, ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { Feature, ServerSupport } from "../../src/feature";
|
||||
import { EventType } from "../../src/matrix";
|
||||
import { synthesizeReceipt } from "../../src/models/read-receipt";
|
||||
import { encodeUri } from "../../src/utils";
|
||||
@@ -70,10 +69,6 @@ const roomEvent = utils.mkEvent({
|
||||
},
|
||||
});
|
||||
|
||||
function mockServerSideSupport(client: MatrixClient, serverSideSupport: ServerSupport) {
|
||||
client.canSupport.set(Feature.ThreadUnreadNotifications, serverSideSupport);
|
||||
}
|
||||
|
||||
describe("Read receipt", () => {
|
||||
beforeEach(() => {
|
||||
httpBackend = new MockHttpBackend();
|
||||
@@ -101,7 +96,6 @@ describe("Read receipt", () => {
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, ServerSupport.Stable);
|
||||
client.sendReceipt(threadEvent, ReceiptType.Read, {});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
@@ -123,7 +117,6 @@ describe("Read receipt", () => {
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, ServerSupport.Stable);
|
||||
client.sendReadReceipt(threadEvent, ReceiptType.Read, true);
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
@@ -145,56 +138,11 @@ describe("Read receipt", () => {
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, ServerSupport.Stable);
|
||||
client.sendReceipt(roomEvent, ReceiptType.Read, {});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("sends a room read receipt when there's no server support", async () => {
|
||||
httpBackend
|
||||
.when(
|
||||
"POST",
|
||||
encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: threadEvent.getId()!,
|
||||
}),
|
||||
)
|
||||
.check((request) => {
|
||||
expect(request.data.thread_id).toBeUndefined();
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, ServerSupport.Unsupported);
|
||||
client.sendReceipt(threadEvent, ReceiptType.Read, {});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("sends a valid room read receipt even when body omitted", async () => {
|
||||
httpBackend
|
||||
.when(
|
||||
"POST",
|
||||
encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: threadEvent.getId()!,
|
||||
}),
|
||||
)
|
||||
.check((request) => {
|
||||
expect(request.data).toEqual({});
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, ServerSupport.Unsupported);
|
||||
client.sendReceipt(threadEvent, ReceiptType.Read, undefined);
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
});
|
||||
|
||||
describe("synthesizeReceipt", () => {
|
||||
|
||||
@@ -146,7 +146,7 @@ describe("ECDHv1", function () {
|
||||
|
||||
// send a message without encryption
|
||||
await aliceTransport.send({ iv: "dummy", ciphertext: "dummy" });
|
||||
expect(bob.receive()).rejects.toThrowError();
|
||||
expect(bob.receive()).rejects.toThrow();
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
@@ -164,7 +164,7 @@ describe("ECDHv1", function () {
|
||||
|
||||
await bobTransport.send({ iv: "dummy", ciphertext: "dummy" });
|
||||
|
||||
expect(alice.receive()).rejects.toThrowError();
|
||||
expect(alice.receive()).rejects.toThrow();
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
@@ -220,7 +220,7 @@ describe("Rendezvous", function () {
|
||||
await bobStartPromise;
|
||||
});
|
||||
|
||||
it("new device declines protocol", async function () {
|
||||
it("new device declines protocol with outcome unsupported", async function () {
|
||||
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
|
||||
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
@@ -278,7 +278,7 @@ describe("Rendezvous", function () {
|
||||
expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm);
|
||||
});
|
||||
|
||||
it("new device declines protocol", async function () {
|
||||
it("new device requests an invalid protocol", async function () {
|
||||
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
|
||||
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
@@ -570,7 +570,7 @@ describe("Rendezvous", function () {
|
||||
|
||||
it("device not online within timeout", async function () {
|
||||
const { aliceRz } = await completeLogin({});
|
||||
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError();
|
||||
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("device appears online within timeout", async function () {
|
||||
@@ -594,7 +594,7 @@ describe("Rendezvous", function () {
|
||||
getFingerprint: () => "bbbb",
|
||||
};
|
||||
}, 1500);
|
||||
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError();
|
||||
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("mismatched device key", async function () {
|
||||
@@ -603,6 +603,6 @@ describe("Rendezvous", function () {
|
||||
getFingerprint: () => "XXXX",
|
||||
},
|
||||
});
|
||||
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError(/different key/);
|
||||
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow(/different key/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("SimpleHttpRendezvousTransport", function () {
|
||||
it("should throw an error when no server available", function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fetchFn });
|
||||
expect(simpleHttpTransport.send({})).rejects.toThrowError("Invalid rendezvous URI");
|
||||
expect(simpleHttpTransport.send({})).rejects.toThrow("Invalid rendezvous URI");
|
||||
});
|
||||
|
||||
it("POST to fallback server", async function () {
|
||||
@@ -130,7 +130,7 @@ describe("SimpleHttpRendezvousTransport", function () {
|
||||
fetchFn,
|
||||
});
|
||||
const prom = simpleHttpTransport.send({});
|
||||
expect(prom).rejects.toThrowError();
|
||||
expect(prom).rejects.toThrow();
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
@@ -163,15 +163,6 @@ describe("SimpleHttpRendezvousTransport", function () {
|
||||
);
|
||||
});
|
||||
|
||||
it("POST with relative path response including parent", async function () {
|
||||
await postAndCheckLocation(
|
||||
false,
|
||||
"https://fallbackserver/rz/abc",
|
||||
"../xyz/123",
|
||||
"https://fallbackserver/rz/xyz/123",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST to follow 307 to other server", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
@@ -373,7 +364,7 @@ describe("SimpleHttpRendezvousTransport", function () {
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
expect(simpleHttpTransport.details()).rejects.toThrowError();
|
||||
expect(simpleHttpTransport.details()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("send after cancelled", async function () {
|
||||
@@ -394,7 +385,7 @@ describe("SimpleHttpRendezvousTransport", function () {
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
expect(simpleHttpTransport.receive()).rejects.toThrowError();
|
||||
expect(simpleHttpTransport.receive()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("404 failure callback", async function () {
|
||||
@@ -416,7 +407,7 @@ describe("SimpleHttpRendezvousTransport", function () {
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("", 1);
|
||||
expect(onFailure).toBeCalledWith(RendezvousFailureReason.Unknown);
|
||||
expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("404 failure callback mapped to expired", async function () {
|
||||
@@ -456,7 +447,7 @@ describe("SimpleHttpRendezvousTransport", function () {
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
expect(onFailure).toBeCalledWith(RendezvousFailureReason.Expired);
|
||||
expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Expired);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+162
-14
@@ -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.
|
||||
@@ -19,6 +19,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, PollStartEvent } from "matrix-events-sdk";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
PendingEventOrdering,
|
||||
PollEvent,
|
||||
RelationType,
|
||||
RoomEvent,
|
||||
RoomMember,
|
||||
@@ -786,8 +788,12 @@ describe("Room", function () {
|
||||
});
|
||||
};
|
||||
|
||||
describe("resetLiveTimeline with timeline support enabled", resetTimelineTests.bind(null, true));
|
||||
describe("resetLiveTimeline with timeline support disabled", resetTimelineTests.bind(null, false));
|
||||
describe("resetLiveTimeline with timeline support enabled", () => {
|
||||
resetTimelineTests.bind(null, true);
|
||||
});
|
||||
describe("resetLiveTimeline with timeline support disabled", () => {
|
||||
resetTimelineTests.bind(null, false);
|
||||
});
|
||||
|
||||
describe("compareEventOrdering", function () {
|
||||
beforeEach(function () {
|
||||
@@ -1622,7 +1628,7 @@ describe("Room", function () {
|
||||
describe("addPendingEvent", function () {
|
||||
it("should add pending events to the pendingEventList if " + "pendingEventOrdering == 'detached'", function () {
|
||||
const client = new TestClient("@alice:example.com", "alicedevice").client;
|
||||
client.supportsExperimentalThreads = () => true;
|
||||
client.supportsThreads = () => true;
|
||||
const room = new Room(roomId, client, userA, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
@@ -2468,7 +2474,7 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
it("Edits update the lastReply event", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
room.client.supportsThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
|
||||
const randomMessage = mkMessage();
|
||||
@@ -2539,7 +2545,7 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
it("Redactions to thread responses decrement the length", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
room.client.supportsThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
|
||||
const threadRoot = mkMessage();
|
||||
@@ -2606,7 +2612,7 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
it("Redactions to reactions in threads do not decrement the length", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
room.client.supportsThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
|
||||
const threadRoot = mkMessage();
|
||||
@@ -2646,7 +2652,7 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
it("should not decrement the length when the thread root is redacted", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
room.client.supportsThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
|
||||
const threadRoot = mkMessage();
|
||||
@@ -2687,7 +2693,7 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
it("Redacting the lastEvent finds a new lastEvent", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
room.client.supportsThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
|
||||
@@ -2794,7 +2800,7 @@ describe("Room", function () {
|
||||
|
||||
describe("eventShouldLiveIn", () => {
|
||||
const client = new TestClient(userA).client;
|
||||
client.supportsExperimentalThreads = () => true;
|
||||
client.supportsThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
const room = new Room(roomId, client, userA);
|
||||
|
||||
@@ -3228,12 +3234,85 @@ describe("Room", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("processPollEvents()", () => {
|
||||
let room: Room;
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
client = getMockClientWithEventEmitter({
|
||||
decryptEventIfNeeded: jest.fn(),
|
||||
});
|
||||
room = new Room(roomId, client, userA);
|
||||
jest.spyOn(room, "emit").mockClear();
|
||||
});
|
||||
|
||||
const makePollStart = (id: string): MatrixEvent => {
|
||||
const event = new MatrixEvent({
|
||||
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
||||
room_id: roomId,
|
||||
});
|
||||
event.event.event_id = id;
|
||||
return event;
|
||||
};
|
||||
|
||||
it("adds poll models to room state for a poll start event", async () => {
|
||||
const pollStartEvent = makePollStart("1");
|
||||
const events = [pollStartEvent];
|
||||
|
||||
await room.processPollEvents(events);
|
||||
expect(client.decryptEventIfNeeded).toHaveBeenCalledWith(pollStartEvent);
|
||||
const pollInstance = room.polls.get(pollStartEvent.getId()!);
|
||||
expect(pollInstance).toBeTruthy();
|
||||
|
||||
expect(room.emit).toHaveBeenCalledWith(PollEvent.New, pollInstance);
|
||||
});
|
||||
|
||||
it("adds related events to poll models", async () => {
|
||||
const pollStartEvent = makePollStart("1");
|
||||
const pollStartEvent2 = makePollStart("2");
|
||||
const events = [pollStartEvent, pollStartEvent2];
|
||||
const pollResponseEvent = new MatrixEvent({
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Reference,
|
||||
event_id: pollStartEvent.getId(),
|
||||
},
|
||||
},
|
||||
});
|
||||
const messageEvent = new MatrixEvent({
|
||||
type: "m.room.messsage",
|
||||
content: {
|
||||
text: "hello",
|
||||
},
|
||||
});
|
||||
|
||||
// init poll
|
||||
await room.processPollEvents(events);
|
||||
|
||||
const poll = room.polls.get(pollStartEvent.getId()!)!;
|
||||
const poll2 = room.polls.get(pollStartEvent2.getId()!)!;
|
||||
jest.spyOn(poll, "onNewRelation");
|
||||
jest.spyOn(poll2, "onNewRelation");
|
||||
|
||||
await room.processPollEvents([pollResponseEvent, messageEvent]);
|
||||
|
||||
// only called for relevant event
|
||||
expect(poll.onNewRelation).toHaveBeenCalledTimes(1);
|
||||
expect(poll.onNewRelation).toHaveBeenCalledWith(pollResponseEvent);
|
||||
|
||||
// only called on poll with relation
|
||||
expect(poll2.onNewRelation).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("findPredecessorRoomId", () => {
|
||||
let client: MatrixClient | null = null;
|
||||
beforeEach(() => {
|
||||
client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3266,21 +3345,90 @@ describe("Room", function () {
|
||||
});
|
||||
}
|
||||
|
||||
function predecessorEvent(
|
||||
newRoomId: string,
|
||||
predecessorRoomId: string,
|
||||
tombstoneEventId: string | null = null,
|
||||
): MatrixEvent {
|
||||
const content =
|
||||
tombstoneEventId === null
|
||||
? { predecessor_room_id: predecessorRoomId }
|
||||
: { predecessor_room_id: predecessorRoomId, last_known_event_id: tombstoneEventId };
|
||||
|
||||
return new MatrixEvent({
|
||||
content,
|
||||
event_id: `predecessor_event_id_pred_${predecessorRoomId}`,
|
||||
origin_server_ts: 1432735824653,
|
||||
room_id: newRoomId,
|
||||
sender: "@daryl:alexandria.example.com",
|
||||
state_key: "",
|
||||
type: "org.matrix.msc3946.room_predecessor",
|
||||
});
|
||||
}
|
||||
|
||||
it("Returns null if there is no create event", () => {
|
||||
const room = new Room("roomid", client!, "@u:example.com");
|
||||
expect(room.findPredecessorRoomId()).toBeNull();
|
||||
expect(room.findPredecessor()).toBeNull();
|
||||
});
|
||||
|
||||
it("Returns null if the create event has no predecessor", () => {
|
||||
const room = new Room("roomid", client!, "@u:example.com");
|
||||
room.addLiveEvents([roomCreateEvent("roomid", null)]);
|
||||
expect(room.findPredecessorRoomId()).toBeNull();
|
||||
expect(room.findPredecessor()).toBeNull();
|
||||
});
|
||||
|
||||
it("Returns the predecessor ID if one is provided via create event", () => {
|
||||
const room = new Room("roomid", client!, "@u:example.com");
|
||||
room.addLiveEvents([roomCreateEvent("roomid", "replacedroomid")]);
|
||||
expect(room.findPredecessorRoomId()).toBe("replacedroomid");
|
||||
expect(room.findPredecessor()).toEqual({ roomId: "replacedroomid", eventId: "id_of_last_known_event" });
|
||||
});
|
||||
|
||||
it("Prefers the m.predecessor event if one exists", () => {
|
||||
const room = new Room("roomid", client!, "@u:example.com");
|
||||
room.addLiveEvents([
|
||||
roomCreateEvent("roomid", "replacedroomid"),
|
||||
predecessorEvent("roomid", "otherreplacedroomid"),
|
||||
]);
|
||||
const useMsc3946 = true;
|
||||
expect(room.findPredecessor(useMsc3946)).toEqual({
|
||||
roomId: "otherreplacedroomid",
|
||||
eventId: undefined, // m.predecessor did not include an event_id
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the m.predecessor event ID if provided", () => {
|
||||
const room = new Room("roomid", client!, "@u:example.com");
|
||||
room.addLiveEvents([
|
||||
roomCreateEvent("roomid", "replacedroomid"),
|
||||
predecessorEvent("roomid", "otherreplacedroomid", "lstevtid"),
|
||||
]);
|
||||
const useMsc3946 = true;
|
||||
expect(room.findPredecessor(useMsc3946)).toEqual({
|
||||
roomId: "otherreplacedroomid",
|
||||
eventId: "lstevtid",
|
||||
});
|
||||
});
|
||||
|
||||
it("Ignores the m.predecessor event if we don't ask to use it", () => {
|
||||
const room = new Room("roomid", client!, "@u:example.com");
|
||||
room.addLiveEvents([
|
||||
roomCreateEvent("roomid", "replacedroomid"),
|
||||
predecessorEvent("roomid", "otherreplacedroomid"),
|
||||
]);
|
||||
// Don't provide an argument for msc3946ProcessDynamicPredecessor -
|
||||
// we should ignore the predecessor event.
|
||||
expect(room.findPredecessor()).toEqual({ roomId: "replacedroomid", eventId: "id_of_last_known_event" });
|
||||
});
|
||||
|
||||
it("Ignores the m.predecessor event and returns null if we don't ask to use it", () => {
|
||||
const room = new Room("roomid", client!, "@u:example.com");
|
||||
room.addLiveEvents([
|
||||
roomCreateEvent("roomid", null), // Create event has no predecessor
|
||||
predecessorEvent("roomid", "otherreplacedroomid", "lastevtid"),
|
||||
]);
|
||||
// Don't provide an argument for msc3946ProcessDynamicPredecessor -
|
||||
// we should ignore the predecessor event.
|
||||
expect(room.findPredecessor()).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
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 fetchMock from "fetch-mock-jest";
|
||||
import { Mocked } from "jest-mock";
|
||||
import { KeysClaimRequest, UserId } from "@matrix-org/matrix-sdk-crypto-js";
|
||||
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
import { KeyClaimManager } from "../../../src/rust-crypto/KeyClaimManager";
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi } from "../../../src";
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
describe("KeyClaimManager", () => {
|
||||
/* for these tests, we connect a KeyClaimManager to a mock OlmMachine, and a real OutgoingRequestProcessor
|
||||
* (which is connected to a mock fetch implementation)
|
||||
*/
|
||||
|
||||
/** the KeyClaimManager implementation under test */
|
||||
let keyClaimManager: KeyClaimManager;
|
||||
|
||||
/** a mocked-up OlmMachine which the OutgoingRequestProcessor and KeyClaimManager are connected to */
|
||||
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
|
||||
baseUrl: "https://example.com",
|
||||
prefix: "/_matrix",
|
||||
onlyData: true,
|
||||
});
|
||||
|
||||
olmMachine = {
|
||||
getMissingSessions: jest.fn(),
|
||||
markRequestAsSent: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
const outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, httpApi);
|
||||
|
||||
keyClaimManager = new KeyClaimManager(olmMachine, outgoingRequestProcessor);
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a promise which resolve once olmMachine.markRequestAsSent is called.
|
||||
*
|
||||
* The call itself will block initially.
|
||||
*
|
||||
* The promise returned by this function yields a callback function, which should be called to unblock the
|
||||
* markRequestAsSent call.
|
||||
*/
|
||||
function awaitCallToMarkRequestAsSent(): Promise<() => void> {
|
||||
return new Promise<() => void>((resolveCalledPromise, _reject) => {
|
||||
olmMachine.markRequestAsSent.mockImplementationOnce(async () => {
|
||||
// the mock implementation returns a promise...
|
||||
const completePromise = new Promise<void>((resolveCompletePromise, _reject) => {
|
||||
// ... and we now resolve the original promise with the resolver for that second promise.
|
||||
resolveCalledPromise(resolveCompletePromise);
|
||||
});
|
||||
return completePromise;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it("should claim missing keys", async () => {
|
||||
const u1 = new UserId("@alice:example.com");
|
||||
const u2 = new UserId("@bob:example.com");
|
||||
|
||||
// stub out olmMachine.getMissingSessions(), with a result indicating that it needs a keyclaim
|
||||
const keysClaimRequest = new KeysClaimRequest("1234", '{ "k1": "v1" }');
|
||||
olmMachine.getMissingSessions.mockResolvedValueOnce(keysClaimRequest);
|
||||
|
||||
// have the claim request return a 200
|
||||
fetchMock.postOnce("https://example.com/_matrix/client/v3/keys/claim", '{ "k": "v" }');
|
||||
|
||||
// also stub out olmMachine.markRequestAsSent
|
||||
olmMachine.markRequestAsSent.mockResolvedValueOnce(undefined);
|
||||
|
||||
// fire off the request
|
||||
await keyClaimManager.ensureSessionsForUsers([u1, u2]);
|
||||
|
||||
// check that all the calls were made
|
||||
expect(olmMachine.getMissingSessions).toHaveBeenCalledWith([u1, u2]);
|
||||
expect(fetchMock).toHaveFetched("https://example.com/_matrix/client/v3/keys/claim", {
|
||||
method: "POST",
|
||||
body: { k1: "v1" },
|
||||
});
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", keysClaimRequest.type, '{ "k": "v" }');
|
||||
});
|
||||
|
||||
it("should wait for previous claims to complete before making another", async () => {
|
||||
const u1 = new UserId("@alice:example.com");
|
||||
const u2 = new UserId("@bob:example.com");
|
||||
|
||||
// stub out olmMachine.getMissingSessions(), with a result indicating that it needs a keyclaim
|
||||
const keysClaimRequest = new KeysClaimRequest("1234", '{ "k1": "v1" }');
|
||||
olmMachine.getMissingSessions.mockResolvedValue(keysClaimRequest);
|
||||
|
||||
// have the claim request return a 200
|
||||
fetchMock.post("https://example.com/_matrix/client/v3/keys/claim", '{ "k": "v" }');
|
||||
|
||||
// stub out olmMachine.markRequestAsSent, and have it block
|
||||
let markRequestAsSentPromise = awaitCallToMarkRequestAsSent();
|
||||
|
||||
// fire off two requests, and keep track of whether their promises resolve
|
||||
let req1Resolved = false;
|
||||
keyClaimManager.ensureSessionsForUsers([u1]).then(() => {
|
||||
req1Resolved = true;
|
||||
});
|
||||
let req2Resolved = false;
|
||||
const req2 = keyClaimManager.ensureSessionsForUsers([u2]).then(() => {
|
||||
req2Resolved = true;
|
||||
});
|
||||
|
||||
// now: wait for the (first) call to OlmMachine.markRequestAsSent
|
||||
let resolveMarkRequestAsSentCallback = await markRequestAsSentPromise;
|
||||
|
||||
// at this point, there should have been a single call to getMissingSessions, and a single fetch; and neither
|
||||
// call to ensureSessionsAsUsers should have completed
|
||||
expect(olmMachine.getMissingSessions).toHaveBeenCalledWith([u1]);
|
||||
expect(olmMachine.getMissingSessions).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(req1Resolved).toBe(false);
|
||||
expect(req2Resolved).toBe(false);
|
||||
|
||||
// await the next call to markRequestAsSent, and release the first one
|
||||
markRequestAsSentPromise = awaitCallToMarkRequestAsSent();
|
||||
resolveMarkRequestAsSentCallback();
|
||||
resolveMarkRequestAsSentCallback = await markRequestAsSentPromise;
|
||||
|
||||
// the first request should now have completed, and we should have more calls and fetches
|
||||
expect(olmMachine.getMissingSessions).toHaveBeenCalledWith([u2]);
|
||||
expect(olmMachine.getMissingSessions).toHaveBeenCalledTimes(2);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
expect(req1Resolved).toBe(true);
|
||||
expect(req2Resolved).toBe(false);
|
||||
|
||||
// finally, release the second call to markRequestAsSent and check that the second request completes
|
||||
resolveMarkRequestAsSentCallback();
|
||||
await req2;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
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 MockHttpBackend from "matrix-mock-request";
|
||||
import { Mocked } from "jest-mock";
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import {
|
||||
KeysBackupRequest,
|
||||
KeysClaimRequest,
|
||||
KeysQueryRequest,
|
||||
KeysUploadRequest,
|
||||
RoomMessageRequest,
|
||||
SignatureUploadRequest,
|
||||
ToDeviceRequest,
|
||||
} from "@matrix-org/matrix-sdk-crypto-js";
|
||||
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi } from "../../../src";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
|
||||
describe("OutgoingRequestProcessor", () => {
|
||||
/** the OutgoingRequestProcessor implementation under test */
|
||||
let processor: OutgoingRequestProcessor;
|
||||
|
||||
/** A mock http backend which processor is connected to */
|
||||
let httpBackend: MockHttpBackend;
|
||||
|
||||
/** a mocked-up OlmMachine which processor is connected to */
|
||||
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
/** wait for a call to olmMachine.markRequestAsSent */
|
||||
function awaitCallToMarkAsSent(): Promise<void> {
|
||||
return new Promise((resolve, _reject) => {
|
||||
olmMachine.markRequestAsSent.mockImplementationOnce(async () => {
|
||||
resolve(undefined);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
httpBackend = new MockHttpBackend();
|
||||
|
||||
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
|
||||
baseUrl: "https://example.com",
|
||||
prefix: "/_matrix",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
onlyData: true,
|
||||
});
|
||||
|
||||
olmMachine = {
|
||||
markRequestAsSent: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
processor = new OutgoingRequestProcessor(olmMachine, httpApi);
|
||||
});
|
||||
|
||||
/* simple requests that map directly to the request body */
|
||||
const tests: Array<[string, any, "POST" | "PUT", string]> = [
|
||||
["KeysUploadRequest", KeysUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/upload"],
|
||||
["KeysQueryRequest", KeysQueryRequest, "POST", "https://example.com/_matrix/client/v3/keys/query"],
|
||||
["KeysClaimRequest", KeysClaimRequest, "POST", "https://example.com/_matrix/client/v3/keys/claim"],
|
||||
[
|
||||
"SignatureUploadRequest",
|
||||
SignatureUploadRequest,
|
||||
"POST",
|
||||
"https://example.com/_matrix/client/v3/keys/signatures/upload",
|
||||
],
|
||||
["KeysBackupRequest", KeysBackupRequest, "PUT", "https://example.com/_matrix/client/v3/room_keys/keys"],
|
||||
];
|
||||
|
||||
test.each(tests)(`should handle %ss`, async (_, RequestClass, expectedMethod, expectedPath) => {
|
||||
// first, mock up a request as we might expect to receive it from the Rust layer ...
|
||||
const testBody = '{ "foo": "bar" }';
|
||||
const outgoingRequest = new RequestClass("1234", testBody);
|
||||
|
||||
// ... then poke it into the OutgoingRequestProcessor under test.
|
||||
const reqProm = processor.makeOutgoingRequest(outgoingRequest);
|
||||
|
||||
// Now: check that it makes a matching HTTP request ...
|
||||
const testResponse = '{ "result": 1 }';
|
||||
httpBackend
|
||||
.when(expectedMethod, "/_matrix")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual(expectedPath);
|
||||
expect(req.rawData).toEqual(testBody);
|
||||
expect(req.headers["Accept"]).toEqual("application/json");
|
||||
expect(req.headers["Content-Type"]).toEqual("application/json");
|
||||
})
|
||||
.respond(200, testResponse, true);
|
||||
|
||||
// ... and that it calls OlmMachine.markAsSent.
|
||||
const markSentCallPromise = awaitCallToMarkAsSent();
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
await Promise.all([reqProm, markSentCallPromise]);
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
|
||||
it("should handle ToDeviceRequests", async () => {
|
||||
// first, mock up the ToDeviceRequest as we might expect to receive it from the Rust layer ...
|
||||
const testBody = '{ "foo": "bar" }';
|
||||
const outgoingRequest = new ToDeviceRequest("1234", "test/type", "test/txnid", testBody);
|
||||
|
||||
// ... then poke it into the OutgoingRequestProcessor under test.
|
||||
const reqProm = processor.makeOutgoingRequest(outgoingRequest);
|
||||
|
||||
// Now: check that it makes a matching HTTP request ...
|
||||
const testResponse = '{ "result": 1 }';
|
||||
httpBackend
|
||||
.when("PUT", "/_matrix")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://example.com/_matrix/client/v3/sendToDevice/test%2Ftype/test%2Ftxnid");
|
||||
expect(req.rawData).toEqual(testBody);
|
||||
expect(req.headers["Accept"]).toEqual("application/json");
|
||||
expect(req.headers["Content-Type"]).toEqual("application/json");
|
||||
})
|
||||
.respond(200, testResponse, true);
|
||||
|
||||
// ... and that it calls OlmMachine.markAsSent.
|
||||
const markSentCallPromise = awaitCallToMarkAsSent();
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
await Promise.all([reqProm, markSentCallPromise]);
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
|
||||
it("should handle RoomMessageRequests", async () => {
|
||||
// first, mock up the RoomMessageRequest as we might expect to receive it from the Rust layer ...
|
||||
const testBody = '{ "foo": "bar" }';
|
||||
const outgoingRequest = new RoomMessageRequest("1234", "test/room", "test/txnid", "test/type", testBody);
|
||||
|
||||
// ... then poke it into the OutgoingRequestProcessor under test.
|
||||
const reqProm = processor.makeOutgoingRequest(outgoingRequest);
|
||||
|
||||
// Now: check that it makes a matching HTTP request ...
|
||||
const testResponse = '{ "result": 1 }';
|
||||
httpBackend
|
||||
.when("PUT", "/_matrix")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual(
|
||||
"https://example.com/_matrix/client/v3/room/test%2Froom/send/test%2Ftype/test%2Ftxnid",
|
||||
);
|
||||
expect(req.rawData).toEqual(testBody);
|
||||
expect(req.headers["Accept"]).toEqual("application/json");
|
||||
expect(req.headers["Content-Type"]).toEqual("application/json");
|
||||
})
|
||||
.respond(200, testResponse, true);
|
||||
|
||||
// ... and that it calls OlmMachine.markAsSent.
|
||||
const markSentCallPromise = awaitCallToMarkAsSent();
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
await Promise.all([reqProm, markSentCallPromise]);
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
|
||||
it("does not explode with unknown requests", async () => {
|
||||
const outgoingRequest = { id: "5678", type: 987 };
|
||||
const markSentCallPromise = awaitCallToMarkAsSent();
|
||||
await Promise.all([processor.makeOutgoingRequest(outgoingRequest), markSentCallPromise]);
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("5678", 987, "");
|
||||
});
|
||||
});
|
||||
@@ -17,24 +17,16 @@ limitations under the License.
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import {
|
||||
KeysBackupRequest,
|
||||
KeysClaimRequest,
|
||||
KeysQueryRequest,
|
||||
KeysUploadRequest,
|
||||
OlmMachine,
|
||||
SignatureUploadRequest,
|
||||
} from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import { KeysQueryRequest, OlmMachine } from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import { Mocked } from "jest-mock";
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import { RustCrypto } from "../../src/rust-crypto/rust-crypto";
|
||||
import { initRustCrypto } from "../../src/rust-crypto";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../src";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
import { mkEvent } from "../test-utils/test-utils";
|
||||
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
|
||||
import { IEventDecryptionResult } from "../../src/@types/crypto";
|
||||
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
|
||||
import { initRustCrypto } from "../../../src/rust-crypto";
|
||||
import { IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../../src";
|
||||
import { mkEvent } from "../../test-utils/test-utils";
|
||||
import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
|
||||
import { IEventDecryptionResult } from "../../../src/@types/crypto";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@@ -106,8 +98,8 @@ describe("RustCrypto", () => {
|
||||
/** the RustCrypto implementation under test */
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
/** A mock http backend which rustCrypto is connected to */
|
||||
let httpBackend: MockHttpBackend;
|
||||
/** A mock OutgoingRequestProcessor which rustCrypto is connected to */
|
||||
let outgoingRequestProcessor: Mocked<OutgoingRequestProcessor>;
|
||||
|
||||
/** a mocked-up OlmMachine which rustCrypto is connected to */
|
||||
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
@@ -116,28 +108,25 @@ describe("RustCrypto", () => {
|
||||
* the front of the queue, until it is empty. */
|
||||
let outgoingRequestQueue: Array<Array<any>>;
|
||||
|
||||
/** wait for a call to olmMachine.markRequestAsSent */
|
||||
function awaitCallToMarkAsSent(): Promise<void> {
|
||||
return new Promise((resolve, _reject) => {
|
||||
olmMachine.markRequestAsSent.mockImplementationOnce(async () => {
|
||||
resolve(undefined);
|
||||
/** wait for a call to outgoingRequestProcessor.makeOutgoingRequest.
|
||||
*
|
||||
* The promise resolves to a callback: the makeOutgoingRequest call will not complete until the returned
|
||||
* callback is called.
|
||||
*/
|
||||
function awaitCallToMakeOutgoingRequest(): Promise<() => void> {
|
||||
return new Promise<() => void>((resolveCalledPromise, _reject) => {
|
||||
outgoingRequestProcessor.makeOutgoingRequest.mockImplementationOnce(async () => {
|
||||
const completePromise = new Promise<void>((resolveCompletePromise, _reject) => {
|
||||
resolveCalledPromise(resolveCompletePromise);
|
||||
});
|
||||
return completePromise;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
httpBackend = new MockHttpBackend();
|
||||
|
||||
await RustSdkCryptoJs.initAsync();
|
||||
|
||||
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
|
||||
baseUrl: "https://example.com",
|
||||
prefix: "/_matrix",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
onlyData: true,
|
||||
});
|
||||
|
||||
// for these tests we use a mock OlmMachine, with an implementation of outgoingRequests that
|
||||
// returns objects from outgoingRequestQueue
|
||||
outgoingRequestQueue = [];
|
||||
@@ -145,91 +134,55 @@ describe("RustCrypto", () => {
|
||||
outgoingRequests: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve(outgoingRequestQueue.shift() ?? []);
|
||||
}),
|
||||
markRequestAsSent: jest.fn(),
|
||||
close: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
rustCrypto = new RustCrypto(olmMachine, httpApi, TEST_USER, TEST_DEVICE_ID);
|
||||
outgoingRequestProcessor = {
|
||||
makeOutgoingRequest: jest.fn(),
|
||||
} as unknown as Mocked<OutgoingRequestProcessor>;
|
||||
|
||||
rustCrypto = new RustCrypto(olmMachine, {} as MatrixHttpApi<any>, TEST_USER, TEST_DEVICE_ID);
|
||||
rustCrypto["outgoingRequestProcessor"] = outgoingRequestProcessor;
|
||||
});
|
||||
|
||||
it("should poll for outgoing messages", () => {
|
||||
it("should poll for outgoing messages and send them", async () => {
|
||||
const testReq = new KeysQueryRequest("1234", "{}");
|
||||
outgoingRequestQueue.push([testReq]);
|
||||
|
||||
const makeRequestPromise = awaitCallToMakeOutgoingRequest();
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
await makeRequestPromise;
|
||||
expect(olmMachine.outgoingRequests).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
/* simple requests that map directly to the request body */
|
||||
const tests: Array<[any, "POST" | "PUT", string]> = [
|
||||
[KeysUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/upload"],
|
||||
[KeysQueryRequest, "POST", "https://example.com/_matrix/client/v3/keys/query"],
|
||||
[KeysClaimRequest, "POST", "https://example.com/_matrix/client/v3/keys/claim"],
|
||||
[SignatureUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/signatures/upload"],
|
||||
[KeysBackupRequest, "PUT", "https://example.com/_matrix/client/v3/room_keys/keys"],
|
||||
];
|
||||
|
||||
for (const [RequestClass, expectedMethod, expectedPath] of tests) {
|
||||
it(`should handle ${RequestClass.name}s`, async () => {
|
||||
const testBody = '{ "foo": "bar" }';
|
||||
const outgoingRequest = new RequestClass("1234", testBody);
|
||||
outgoingRequestQueue.push([outgoingRequest]);
|
||||
|
||||
const testResponse = '{ "result": 1 }';
|
||||
httpBackend
|
||||
.when(expectedMethod, "/_matrix")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual(expectedPath);
|
||||
expect(req.rawData).toEqual(testBody);
|
||||
expect(req.headers["Accept"]).toEqual("application/json");
|
||||
expect(req.headers["Content-Type"]).toEqual("application/json");
|
||||
})
|
||||
.respond(200, testResponse, true);
|
||||
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(1);
|
||||
|
||||
const markSentCallPromise = awaitCallToMarkAsSent();
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
await markSentCallPromise;
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
}
|
||||
|
||||
it("does not explode with unknown requests", async () => {
|
||||
const outgoingRequest = { id: "5678", type: 987 };
|
||||
outgoingRequestQueue.push([outgoingRequest]);
|
||||
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
await awaitCallToMarkAsSent();
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("5678", 987, "");
|
||||
expect(outgoingRequestProcessor.makeOutgoingRequest).toHaveBeenCalledWith(testReq);
|
||||
});
|
||||
|
||||
it("stops looping when stop() is called", async () => {
|
||||
const testResponse = '{ "result": 1 }';
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
outgoingRequestQueue.push([new KeysQueryRequest("1234", "{}")]);
|
||||
httpBackend.when("POST", "/_matrix").respond(200, testResponse, true);
|
||||
}
|
||||
|
||||
let makeRequestPromise = awaitCallToMakeOutgoingRequest();
|
||||
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
expect(rustCrypto["outgoingRequestLoopRunning"]).toBeTruthy();
|
||||
|
||||
// go a couple of times round the loop
|
||||
await httpBackend.flush("/_matrix", 1);
|
||||
await awaitCallToMarkAsSent();
|
||||
let resolveMakeRequest = await makeRequestPromise;
|
||||
makeRequestPromise = awaitCallToMakeOutgoingRequest();
|
||||
resolveMakeRequest();
|
||||
|
||||
await httpBackend.flush("/_matrix", 1);
|
||||
await awaitCallToMarkAsSent();
|
||||
resolveMakeRequest = await makeRequestPromise;
|
||||
makeRequestPromise = awaitCallToMakeOutgoingRequest();
|
||||
resolveMakeRequest();
|
||||
|
||||
// a second sync while this is going on shouldn't make any difference
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
await httpBackend.flush("/_matrix", 1);
|
||||
await awaitCallToMarkAsSent();
|
||||
resolveMakeRequest = await makeRequestPromise;
|
||||
outgoingRequestProcessor.makeOutgoingRequest.mockReset();
|
||||
resolveMakeRequest();
|
||||
|
||||
// now stop...
|
||||
rustCrypto.stop();
|
||||
@@ -241,7 +194,7 @@ describe("RustCrypto", () => {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
expect(rustCrypto["outgoingRequestLoopRunning"]).toBeFalsy();
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled();
|
||||
expect(olmMachine.outgoingRequests).not.toHaveBeenCalled();
|
||||
|
||||
// we sent three, so there should be 2 left
|
||||
+21
-23
@@ -112,7 +112,7 @@ describe("MatrixScheduler", function () {
|
||||
expect(procCount).toEqual(2);
|
||||
});
|
||||
|
||||
it("should give up if the retryFn on failure returns -1 and try the next event", async function () {
|
||||
it("should give up if the retryFn on failure returns -1", async function () {
|
||||
// Queue A & B.
|
||||
// Reject A and return -1 on retry.
|
||||
// Expect B to be tried next and the promise for A to be rejected.
|
||||
@@ -139,22 +139,18 @@ describe("MatrixScheduler", function () {
|
||||
return new Promise<Record<string, boolean>>(() => {});
|
||||
});
|
||||
|
||||
const globalA = scheduler.queueEvent(eventA);
|
||||
scheduler.queueEvent(eventB);
|
||||
const queuedA = scheduler.queueEvent(eventA);
|
||||
const queuedB = scheduler.queueEvent(eventB);
|
||||
await Promise.resolve();
|
||||
deferA.reject(new Error("Testerror"));
|
||||
// as queueing doesn't start processing synchronously anymore (see commit bbdb5ac)
|
||||
// wait just long enough before it does
|
||||
await Promise.resolve();
|
||||
await expect(queuedA).rejects.toThrow("Testerror");
|
||||
await expect(queuedB).rejects.toThrow("Testerror");
|
||||
expect(procCount).toEqual(1);
|
||||
deferA.reject({});
|
||||
try {
|
||||
await globalA;
|
||||
} catch (err) {
|
||||
await Promise.resolve();
|
||||
expect(procCount).toEqual(2);
|
||||
}
|
||||
});
|
||||
|
||||
it("should treat each queue separately", function (done) {
|
||||
it("should treat each queue separately", async () => {
|
||||
// Queue messages A B C D.
|
||||
// Bucket A&D into queue_A
|
||||
// Bucket B&C into queue_B
|
||||
@@ -179,13 +175,15 @@ describe("MatrixScheduler", function () {
|
||||
|
||||
const expectOrder = [eventA.getId(), eventB.getId(), eventD.getId()];
|
||||
const deferA = defer<Record<string, boolean>>();
|
||||
scheduler.setProcessFunction(function (event) {
|
||||
const id = expectOrder.shift();
|
||||
expect(id).toEqual(event.getId());
|
||||
if (expectOrder.length === 0) {
|
||||
done();
|
||||
}
|
||||
return id === eventA.getId() ? deferA.promise : deferred.promise;
|
||||
const allExpectedEventsSeenInOrderPromise = new Promise((resolve) => {
|
||||
scheduler.setProcessFunction(function (event) {
|
||||
const id = expectOrder.shift();
|
||||
expect(id).toEqual(event.getId());
|
||||
if (expectOrder.length === 0) {
|
||||
resolve(null);
|
||||
}
|
||||
return id === eventA.getId() ? deferA.promise : deferred.promise;
|
||||
});
|
||||
});
|
||||
scheduler.queueEvent(eventA);
|
||||
scheduler.queueEvent(eventB);
|
||||
@@ -197,6 +195,7 @@ describe("MatrixScheduler", function () {
|
||||
deferA.resolve({});
|
||||
}, 1000);
|
||||
jest.advanceTimersByTime(1000);
|
||||
await allExpectedEventsSeenInOrderPromise;
|
||||
});
|
||||
|
||||
describe("queueEvent", function () {
|
||||
@@ -294,7 +293,7 @@ describe("MatrixScheduler", function () {
|
||||
});
|
||||
|
||||
describe("setProcessFunction", function () {
|
||||
it("should call the processFn if there are queued events", function () {
|
||||
it("should call the processFn if there are queued events", async () => {
|
||||
queueFn = function () {
|
||||
return "yep";
|
||||
};
|
||||
@@ -307,9 +306,8 @@ describe("MatrixScheduler", function () {
|
||||
});
|
||||
// as queueing doesn't start processing synchronously anymore (see commit bbdb5ac)
|
||||
// wait just long enough before it does
|
||||
Promise.resolve().then(() => {
|
||||
expect(procCount).toEqual(1);
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(procCount).toEqual(1);
|
||||
});
|
||||
|
||||
it("should not call the processFn if there are no queued events", function () {
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
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 utils from "../../src/utils";
|
||||
import {
|
||||
alphabetPad,
|
||||
@@ -129,9 +145,11 @@ describe("utils", function () {
|
||||
describe("deepCompare", function () {
|
||||
const assert = {
|
||||
isTrue: function (x: any) {
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
expect(x).toBe(true);
|
||||
},
|
||||
isFalse: function (x: any) {
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
expect(x).toBe(false);
|
||||
},
|
||||
};
|
||||
@@ -587,4 +605,22 @@ describe("utils", function () {
|
||||
expect(utils.isSupportedReceiptType("this is a receipt type")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sleep", () => {
|
||||
it("resolves", async () => {
|
||||
await utils.sleep(0);
|
||||
});
|
||||
|
||||
it("resolves with the provided value", async () => {
|
||||
const expected = Symbol("hi");
|
||||
const result = await utils.sleep(0, expected);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("immediate", () => {
|
||||
it("resolves", async () => {
|
||||
await utils.immediate();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,7 +40,9 @@ import {
|
||||
MockMediaStreamTrack,
|
||||
installWebRTCMocks,
|
||||
MockRTCPeerConnection,
|
||||
MockRTCRtpTransceiver,
|
||||
SCREENSHARE_STREAM_ID,
|
||||
MockRTCRtpSender,
|
||||
} from "../../test-utils/webrtc";
|
||||
import { CallFeed } from "../../../src/webrtc/callFeed";
|
||||
import { EventType, IContent, ISendEventResponse, MatrixEvent, Room } from "../../../src";
|
||||
@@ -536,8 +538,15 @@ describe("Call", function () {
|
||||
it("if local video", async () => {
|
||||
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
|
||||
|
||||
// since this is testing for the presence of a local sender, we need to add a transciever
|
||||
// rather than just a source track
|
||||
const mockTrack = new MockMediaStreamTrack("track_id", "video");
|
||||
const mockTransceiver = new MockRTCRtpTransceiver(call.peerConn as unknown as MockRTCPeerConnection);
|
||||
mockTransceiver.sender = new MockRTCRtpSender(mockTrack) as unknown as RTCRtpSender;
|
||||
(call as any).transceivers.set("m.usermedia:video", mockTransceiver);
|
||||
|
||||
(call as any).pushNewLocalFeed(
|
||||
new MockMediaStream("remote_stream1", [new MockMediaStreamTrack("track_id", "video")]),
|
||||
new MockMediaStream("remote_stream1", [mockTrack]),
|
||||
SDPStreamMetadataPurpose.Usermedia,
|
||||
false,
|
||||
);
|
||||
@@ -829,6 +838,55 @@ describe("Call", function () {
|
||||
await startVideoCall(client, call);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("should not remove video sender on video mute", async () => {
|
||||
await call.setLocalVideoMuted(true);
|
||||
expect((call as any).hasUserMediaVideoSender).toBe(true);
|
||||
});
|
||||
|
||||
it("should release camera after short delay on video mute", async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
await call.setLocalVideoMuted(true);
|
||||
|
||||
jest.advanceTimersByTime(500);
|
||||
|
||||
expect(call.hasLocalUserMediaVideoTrack).toBe(false);
|
||||
});
|
||||
|
||||
it("should re-request video feed on video unmute if it doesn't have one", async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const mockGetUserMediaStream = jest
|
||||
.fn()
|
||||
.mockReturnValue(client.client.getMediaHandler().getUserMediaStream(true, true));
|
||||
|
||||
client.client.getMediaHandler().getUserMediaStream = mockGetUserMediaStream;
|
||||
|
||||
await call.setLocalVideoMuted(true);
|
||||
|
||||
jest.advanceTimersByTime(500);
|
||||
|
||||
await call.setLocalVideoMuted(false);
|
||||
|
||||
expect(mockGetUserMediaStream).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not release camera on fast mute and unmute", async () => {
|
||||
const mockGetUserMediaStream = jest.fn();
|
||||
|
||||
client.client.getMediaHandler().getUserMediaStream = mockGetUserMediaStream;
|
||||
|
||||
await call.setLocalVideoMuted(true);
|
||||
await call.setLocalVideoMuted(false);
|
||||
|
||||
expect(mockGetUserMediaStream).not.toHaveBeenCalled();
|
||||
expect(call.hasLocalUserMediaVideoTrack).toBe(true);
|
||||
});
|
||||
|
||||
describe("sending sdp_stream_metadata_changed events", () => {
|
||||
it("should send sdp_stream_metadata_changed when muting audio", async () => {
|
||||
await call.setMicrophoneMuted(true);
|
||||
@@ -1521,7 +1579,7 @@ describe("Call", function () {
|
||||
hasAdvancedBy += advanceBy;
|
||||
|
||||
expect(lengthChangedListener).toHaveBeenCalledTimes(hasAdvancedBy);
|
||||
expect(lengthChangedListener).toBeCalledWith(hasAdvancedBy);
|
||||
expect(lengthChangedListener).toHaveBeenCalledWith(hasAdvancedBy);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ describe("CallFeed", () => {
|
||||
});
|
||||
|
||||
describe("muting after calling setAudioVideoMuted()", () => {
|
||||
it("should mute audio by default ", () => {
|
||||
it("should mute audio by default", () => {
|
||||
// @ts-ignore Mock
|
||||
feed.stream.addTrack(new MockMediaStreamTrack("track", "audio", true));
|
||||
feed.setAudioVideoMuted(true, false);
|
||||
|
||||
@@ -147,10 +147,17 @@ describe("Group Call", function () {
|
||||
async (state: GroupCallState) => {
|
||||
// @ts-ignore
|
||||
groupCall.state = state;
|
||||
await expect(groupCall.initLocalCallFeed()).rejects.toThrowError();
|
||||
await expect(groupCall.initLocalCallFeed()).rejects.toThrow();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([0, 3, 5, 10, 5000])("sets correct creation timestamp when creating a call", async (time: number) => {
|
||||
jest.spyOn(Date, "now").mockReturnValue(time);
|
||||
await groupCall.create();
|
||||
|
||||
expect(groupCall.creationTs).toBe(time);
|
||||
});
|
||||
|
||||
it("does not initialize local call feed, if it already is", async () => {
|
||||
await groupCall.initLocalCallFeed();
|
||||
jest.spyOn(groupCall, "initLocalCallFeed");
|
||||
@@ -161,6 +168,25 @@ describe("Group Call", function () {
|
||||
groupCall.leave();
|
||||
});
|
||||
|
||||
it("does not start initializing local call feed twice", () => {
|
||||
const promise1 = groupCall.initLocalCallFeed();
|
||||
// @ts-ignore Mock
|
||||
groupCall.state = GroupCallState.LocalCallFeedUninitialized;
|
||||
const promise2 = groupCall.initLocalCallFeed();
|
||||
|
||||
expect(promise1).toEqual(promise2);
|
||||
});
|
||||
|
||||
it("sets state to local call feed uninitialized when getUserMedia() fails", async () => {
|
||||
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValue("Error");
|
||||
|
||||
try {
|
||||
await groupCall.initLocalCallFeed();
|
||||
} catch (e) {}
|
||||
|
||||
expect(groupCall.state).toBe(GroupCallState.LocalCallFeedUninitialized);
|
||||
});
|
||||
|
||||
it("stops initializing local call feed when leaving", async () => {
|
||||
const initPromise = groupCall.initLocalCallFeed();
|
||||
groupCall.leave();
|
||||
@@ -317,6 +343,20 @@ describe("Group Call", function () {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not throw when calling updateLocalUsermediaStream() without local usermedia stream", () => {
|
||||
expect(async () => await groupCall.updateLocalUsermediaStream({} as MediaStream)).not.toThrow();
|
||||
});
|
||||
|
||||
it.each([GroupCallState.Ended, GroupCallState.Entered, GroupCallState.InitializingLocalCallFeed])(
|
||||
"throws when entering call in the wrong state",
|
||||
async (state: GroupCallState) => {
|
||||
// @ts-ignore Mock
|
||||
groupCall.state = state;
|
||||
|
||||
await expect(groupCall.enter()).rejects.toThrow();
|
||||
},
|
||||
);
|
||||
|
||||
describe("hasLocalParticipant()", () => {
|
||||
it("should return false, if we don't have a local participant", () => {
|
||||
expect(groupCall.hasLocalParticipant()).toBeFalsy();
|
||||
@@ -349,7 +389,7 @@ describe("Group Call", function () {
|
||||
jest.spyOn(call, "getOpponentMember").mockReturnValue({ userId: undefined });
|
||||
|
||||
// @ts-ignore Mock
|
||||
expect(() => groupCall.onCallFeedsChanged(call)).toThrowError();
|
||||
expect(() => groupCall.onCallFeedsChanged(call)).toThrow();
|
||||
});
|
||||
|
||||
describe("usermedia feeds", () => {
|
||||
@@ -835,6 +875,18 @@ describe("Group Call", function () {
|
||||
|
||||
groupCall.terminate();
|
||||
});
|
||||
|
||||
it("returns false when unmuting audio with no audio device", async () => {
|
||||
const groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
jest.spyOn(mockClient.getMediaHandler(), "hasAudioDevice").mockResolvedValue(false);
|
||||
expect(await groupCall.setMicrophoneMuted(false)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when unmuting video with no video device", async () => {
|
||||
const groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
jest.spyOn(mockClient.getMediaHandler(), "hasVideoDevice").mockResolvedValue(false);
|
||||
expect(await groupCall.setLocalVideoMuted(false)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("remote muting", () => {
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("Media Handler", function () {
|
||||
} as unknown as MatrixClient);
|
||||
});
|
||||
|
||||
it("does not trigger update after restore media settings ", () => {
|
||||
it("does not trigger update after restore media settings", () => {
|
||||
mediaHandler.restoreMediaSettings(FAKE_AUDIO_INPUT_ID, FAKE_VIDEO_INPUT_ID);
|
||||
|
||||
expect(mockMediaDevices.getUserMedia).not.toHaveBeenCalled();
|
||||
@@ -401,7 +401,7 @@ describe("Media Handler", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("stopUserMediaStream", () => {
|
||||
describe("stopScreensharingStream", () => {
|
||||
let stream: MediaStream;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
+28
-1
@@ -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.
|
||||
@@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IClearEvent } from "../models/event";
|
||||
import type { ISignatures } from "./signed";
|
||||
|
||||
export type OlmGroupSessionExtraData = {
|
||||
untrusted?: boolean;
|
||||
@@ -43,6 +44,10 @@ export interface IEventDecryptionResult {
|
||||
*/
|
||||
claimedEd25519Key?: string;
|
||||
untrusted?: boolean;
|
||||
/**
|
||||
* The sender doesn't authorize the unverified devices to decrypt his messages
|
||||
*/
|
||||
encryptedDisabledForUnverifiedDevices?: boolean;
|
||||
}
|
||||
|
||||
interface Extensible {
|
||||
@@ -70,3 +75,25 @@ export interface IMegolmSessionData extends Extensible {
|
||||
}
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
/** the type of the `device_keys` parameter on `/_matrix/client/v3/keys/upload`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.5/client-server-api/#post_matrixclientv3keysupload
|
||||
*/
|
||||
export interface IDeviceKeys {
|
||||
algorithms: Array<string>;
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
user_id: string; // eslint-disable-line camelcase
|
||||
keys: Record<string, string>;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
/** the type of the `one_time_keys` and `fallback_keys` parameters on `/_matrix/client/v3/keys/upload`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.5/client-server-api/#post_matrixclientv3keysupload
|
||||
*/
|
||||
export interface IOneTimeKey {
|
||||
key: string;
|
||||
fallback?: boolean;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export enum EventType {
|
||||
RoomGuestAccess = "m.room.guest_access",
|
||||
RoomServerAcl = "m.room.server_acl",
|
||||
RoomTombstone = "m.room.tombstone",
|
||||
RoomPredecessor = "org.matrix.msc3946.room_predecessor",
|
||||
|
||||
SpaceChild = "m.space.child",
|
||||
SpaceParent = "m.space.parent",
|
||||
|
||||
@@ -47,7 +47,7 @@ interface WellKnownConfig extends Omit<IWellKnownConfig, "error"> {
|
||||
error?: IWellKnownConfig["error"] | null;
|
||||
}
|
||||
|
||||
interface ClientConfig extends Omit<IClientWellKnown, "m.homeserver" | "m.identity_server"> {
|
||||
export interface ClientConfig extends Omit<IClientWellKnown, "m.homeserver" | "m.identity_server"> {
|
||||
"m.homeserver": WellKnownConfig;
|
||||
"m.identity_server": WellKnownConfig;
|
||||
}
|
||||
|
||||
+219
-124
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2015-2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015-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.
|
||||
@@ -20,7 +20,7 @@ limitations under the License.
|
||||
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
|
||||
import type { IMegolmSessionData } from "./@types/crypto";
|
||||
import type { IDeviceKeys, IMegolmSessionData, IOneTimeKey } from "./@types/crypto";
|
||||
import { ISyncStateData, SyncApi, SyncApiOptions, SyncState } from "./sync";
|
||||
import {
|
||||
EventStatus,
|
||||
@@ -85,13 +85,7 @@ import { keyFromAuthData } from "./crypto/key_passphrase";
|
||||
import { User, UserEvent, UserEventHandlerMap } from "./models/user";
|
||||
import { getHttpUriForMxc } from "./content-repo";
|
||||
import { SearchResult } from "./models/search-result";
|
||||
import {
|
||||
DEHYDRATION_ALGORITHM,
|
||||
IDehydratedDevice,
|
||||
IDehydratedDeviceKeyInfo,
|
||||
IDeviceKeys,
|
||||
IOneTimeKey,
|
||||
} from "./crypto/dehydration";
|
||||
import { DEHYDRATION_ALGORITHM, IDehydratedDevice, IDehydratedDeviceKeyInfo } from "./crypto/dehydration";
|
||||
import {
|
||||
IKeyBackupInfo,
|
||||
IKeyBackupPrepareOpts,
|
||||
@@ -209,7 +203,6 @@ import { ToDeviceBatch } from "./models/ToDeviceMessage";
|
||||
import { IgnoredInvites } from "./models/invites-ignorer";
|
||||
import { UIARequest, UIAResponse } from "./@types/uia";
|
||||
import { LocalNotificationSettings } from "./@types/local_notifications";
|
||||
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync";
|
||||
import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature";
|
||||
import { CryptoBackend } from "./common-crypto/CryptoBackend";
|
||||
import { RUST_SDK_STORE_PREFIX } from "./rust-crypto/constants";
|
||||
@@ -446,10 +439,16 @@ export interface IStartClientOpts {
|
||||
clientWellKnownPollPeriod?: number;
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* @deprecated use `threadSupport` instead
|
||||
*/
|
||||
experimentalThreadSupport?: boolean;
|
||||
|
||||
/**
|
||||
* Will organises events in threaded conversations when
|
||||
* a thread relation is encountered
|
||||
*/
|
||||
threadSupport?: boolean;
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
@@ -568,6 +567,8 @@ export interface IWellKnownConfig {
|
||||
error?: Error | string;
|
||||
// eslint-disable-next-line
|
||||
base_url?: string | null;
|
||||
// XXX: this is undocumented
|
||||
server_name?: string;
|
||||
}
|
||||
|
||||
export interface IDelegatedAuthConfig {
|
||||
@@ -1300,6 +1301,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.on(ClientEvent.Sync, this.startCallEventHandler);
|
||||
}
|
||||
|
||||
this.on(ClientEvent.Sync, this.fixupRoomNotifications);
|
||||
|
||||
this.timelineSupport = Boolean(opts.timelineSupport);
|
||||
|
||||
this.cryptoStore = opts.cryptoStore;
|
||||
@@ -1448,6 +1451,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.syncApi = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
|
||||
}
|
||||
|
||||
if (this.clientOpts.hasOwnProperty("experimentalThreadSupport")) {
|
||||
logger.warn("`experimentalThreadSupport` has been deprecated, use `threadSupport` instead");
|
||||
}
|
||||
|
||||
// If `threadSupport` is omitted and the deprecated `experimentalThreadSupport` has been passed
|
||||
// We should fallback to that value for backwards compatibility purposes
|
||||
if (
|
||||
!this.clientOpts.hasOwnProperty("threadSupport") &&
|
||||
this.clientOpts.hasOwnProperty("experimentalThreadSupport")
|
||||
) {
|
||||
this.clientOpts.threadSupport = this.clientOpts.experimentalThreadSupport;
|
||||
}
|
||||
|
||||
this.syncApi.sync();
|
||||
|
||||
if (this.clientOpts.clientWellKnownPollPeriod !== undefined) {
|
||||
@@ -2165,7 +2181,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// importing rust-crypto will download the webassembly, so we delay it until we know it will be
|
||||
// needed.
|
||||
const RustCrypto = await import("./rust-crypto");
|
||||
this.cryptoBackend = await RustCrypto.initRustCrypto(this.http, userId, deviceId);
|
||||
const rustCrypto = await RustCrypto.initRustCrypto(this.http, userId, deviceId);
|
||||
this.cryptoBackend = rustCrypto;
|
||||
|
||||
// attach the event listeners needed by RustCrypto
|
||||
this.on(RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2588,10 +2608,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param room - the room the event is in
|
||||
*/
|
||||
public prepareToEncrypt(room: Room): void {
|
||||
if (!this.crypto) {
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
this.crypto.prepareToEncrypt(room);
|
||||
this.cryptoBackend.prepareToEncrypt(room);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3780,14 +3800,18 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* This is essentially getRooms() with some rooms filtered out, eg. old versions
|
||||
* of rooms that have been replaced or (in future) other rooms that have been
|
||||
* marked at the protocol level as not to be displayed to the user.
|
||||
*
|
||||
* @param msc3946ProcessDynamicPredecessor - if true, look for an
|
||||
* m.room.predecessor state event and
|
||||
* use it if found (MSC3946).
|
||||
* @returns A list of rooms, or an empty list if there is no data store.
|
||||
*/
|
||||
public getVisibleRooms(): Room[] {
|
||||
public getVisibleRooms(msc3946ProcessDynamicPredecessor = false): Room[] {
|
||||
const allRooms = this.store.getRooms();
|
||||
|
||||
const replacedRooms = new Set();
|
||||
for (const r of allRooms) {
|
||||
const predecessor = r.findPredecessorRoomId();
|
||||
const predecessor = r.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
|
||||
if (predecessor) {
|
||||
replacedRooms.add(predecessor);
|
||||
}
|
||||
@@ -4107,12 +4131,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
roomId: string,
|
||||
userId: string | string[],
|
||||
powerLevel: number,
|
||||
event: MatrixEvent,
|
||||
event: MatrixEvent | null,
|
||||
): Promise<ISendEventResponse> {
|
||||
let content = {
|
||||
users: {} as Record<string, number>,
|
||||
};
|
||||
if (event.getType() === EventType.RoomPowerLevels) {
|
||||
if (event?.getType() === EventType.RoomPowerLevels) {
|
||||
// take a copy of the content to ensure we don't corrupt
|
||||
// existing client state with a failed power level change
|
||||
content = utils.deepCopy(event.getContent());
|
||||
@@ -4368,11 +4392,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.isRoomEncrypted(event.getRoomId()!)) {
|
||||
if (!room || !this.isRoomEncrypted(event.getRoomId()!)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.crypto && this.usingExternalCrypto) {
|
||||
if (!this.cryptoBackend && this.usingExternalCrypto) {
|
||||
// The client has opted to allow sending messages to encrypted
|
||||
// rooms even if the room is encrypted, and we haven't setup
|
||||
// crypto. This is useful for users of matrix-org/pantalaimon
|
||||
@@ -4393,13 +4417,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.crypto) {
|
||||
throw new Error(
|
||||
"This room is configured to use encryption, but your client does " + "not support encryption.",
|
||||
);
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("This room is configured to use encryption, but your client does not support encryption.");
|
||||
}
|
||||
|
||||
return this.crypto.encryptEvent(event, room);
|
||||
return this.cryptoBackend.encryptEvent(event, room);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4817,8 +4839,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
$eventId: event.getId()!,
|
||||
});
|
||||
|
||||
const supportsThreadRR = this.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported;
|
||||
if (supportsThreadRR && !unthreaded) {
|
||||
if (!unthreaded) {
|
||||
const isThread = !!event.threadRootId;
|
||||
body = {
|
||||
...body,
|
||||
@@ -4986,70 +5007,83 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* which can be proven to be linked. For example, rooms which have a create
|
||||
* event pointing to an old room which the client is not aware of or doesn't
|
||||
* have a matching tombstone would not be returned.
|
||||
* @param msc3946ProcessDynamicPredecessor - if true, look for
|
||||
* m.room.predecessor state events as well as create events, and prefer
|
||||
* predecessor events where they exist (MSC3946).
|
||||
* @returns An array of rooms representing the upgrade
|
||||
* history.
|
||||
*/
|
||||
public getRoomUpgradeHistory(roomId: string, verifyLinks = false): Room[] {
|
||||
let currentRoom = this.getRoom(roomId);
|
||||
public getRoomUpgradeHistory(
|
||||
roomId: string,
|
||||
verifyLinks = false,
|
||||
msc3946ProcessDynamicPredecessor = false,
|
||||
): Room[] {
|
||||
const currentRoom = this.getRoom(roomId);
|
||||
if (!currentRoom) return [];
|
||||
|
||||
const upgradeHistory = [currentRoom];
|
||||
const before = this.findPredecessorRooms(currentRoom, verifyLinks, msc3946ProcessDynamicPredecessor);
|
||||
const after = this.findSuccessorRooms(currentRoom, verifyLinks, msc3946ProcessDynamicPredecessor);
|
||||
|
||||
// Work backwards first, looking at create events.
|
||||
let createEvent = currentRoom.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
while (createEvent) {
|
||||
const predecessor = createEvent.getContent()["predecessor"];
|
||||
if (predecessor && predecessor["room_id"]) {
|
||||
const refRoom = this.getRoom(predecessor["room_id"]);
|
||||
if (!refRoom) break; // end of the chain
|
||||
return [...before, currentRoom, ...after];
|
||||
}
|
||||
|
||||
if (verifyLinks) {
|
||||
const tombstone = refRoom.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
private findPredecessorRooms(room: Room, verifyLinks: boolean, msc3946ProcessDynamicPredecessor: boolean): Room[] {
|
||||
const ret: Room[] = [];
|
||||
|
||||
if (!tombstone || tombstone.getContent()["replacement_room"] !== refRoom.roomId) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert at the front because we're working backwards from the currentRoom
|
||||
upgradeHistory.splice(0, 0, refRoom);
|
||||
createEvent = refRoom.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
} else {
|
||||
// No further create events to look at
|
||||
// Work backwards from newer to older rooms
|
||||
let predecessorRoomId = room.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
|
||||
while (predecessorRoomId !== null) {
|
||||
const predecessorRoom = this.getRoom(predecessorRoomId);
|
||||
if (predecessorRoom === null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (verifyLinks) {
|
||||
const tombstone = predecessorRoom.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
if (!tombstone || tombstone.getContent()["replacement_room"] !== room.roomId) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Work forwards next, looking at tombstone events
|
||||
let tombstoneEvent = currentRoom.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
// Insert at the front because we're working backwards from the currentRoom
|
||||
ret.splice(0, 0, predecessorRoom);
|
||||
|
||||
room = predecessorRoom;
|
||||
predecessorRoomId = room.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private findSuccessorRooms(room: Room, verifyLinks: boolean, msc3946ProcessDynamicPredecessor: boolean): Room[] {
|
||||
const ret: Room[] = [];
|
||||
|
||||
// Work forwards, looking at tombstone events
|
||||
let tombstoneEvent = room.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
while (tombstoneEvent) {
|
||||
const refRoom = this.getRoom(tombstoneEvent.getContent()["replacement_room"]);
|
||||
if (!refRoom) break; // end of the chain
|
||||
if (refRoom.roomId === currentRoom.roomId) break; // Tombstone is referencing it's own room
|
||||
const successorRoom = this.getRoom(tombstoneEvent.getContent()["replacement_room"]);
|
||||
if (!successorRoom) break; // end of the chain
|
||||
if (successorRoom.roomId === room.roomId) break; // Tombstone is referencing its own room
|
||||
|
||||
if (verifyLinks) {
|
||||
createEvent = refRoom.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
if (!createEvent || !createEvent.getContent()["predecessor"]) break;
|
||||
|
||||
const predecessor = createEvent.getContent()["predecessor"];
|
||||
if (predecessor["room_id"] !== currentRoom.roomId) break;
|
||||
const predecessorRoomId = successorRoom.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
|
||||
if (!predecessorRoomId || predecessorRoomId !== room.roomId) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Push to the end because we're looking forwards
|
||||
upgradeHistory.push(refRoom);
|
||||
const roomIds = new Set(upgradeHistory.map((ref) => ref.roomId));
|
||||
if (roomIds.size < upgradeHistory.length) {
|
||||
ret.push(successorRoom);
|
||||
const roomIds = new Set(ret.map((ref) => ref.roomId));
|
||||
if (roomIds.size < ret.length) {
|
||||
// The last room added to the list introduced a previous roomId
|
||||
// To avoid recursion, return the last rooms - 1
|
||||
return upgradeHistory.slice(0, upgradeHistory.length - 1);
|
||||
return ret.slice(0, ret.length - 1);
|
||||
}
|
||||
|
||||
// Set the current room to the reference room so we know where we're at
|
||||
currentRoom = refRoom;
|
||||
tombstoneEvent = currentRoom.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
room = successorRoom;
|
||||
tombstoneEvent = room.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
}
|
||||
|
||||
return upgradeHistory;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5420,7 +5454,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents);
|
||||
|
||||
this.processBeaconEvents(room, timelineEvents);
|
||||
this.processAggregatedTimelineEvents(room, timelineEvents);
|
||||
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
|
||||
this.processThreadEvents(room, threadedEvents, true);
|
||||
|
||||
@@ -5481,7 +5515,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return timelineSet.getTimelineForEvent(eventId);
|
||||
}
|
||||
|
||||
if (timelineSet.thread && this.supportsExperimentalThreads()) {
|
||||
if (timelineSet.thread && this.supportsThreads()) {
|
||||
return this.getThreadTimeline(timelineSet, eventId);
|
||||
}
|
||||
|
||||
@@ -5535,7 +5569,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
|
||||
// The target event is not in a thread but process the contextual events, so we can show any threads around it.
|
||||
this.processThreadEvents(timelineSet.room, threadedEvents, true);
|
||||
this.processBeaconEvents(timelineSet.room, timelineEvents);
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, timelineEvents);
|
||||
|
||||
// There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring
|
||||
// timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up
|
||||
@@ -5548,7 +5582,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
public async getThreadTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<EventTimeline | undefined> {
|
||||
if (!this.supportsExperimentalThreads()) {
|
||||
if (!this.supportsThreads()) {
|
||||
throw new Error("could not get thread timeline: no client support");
|
||||
}
|
||||
|
||||
@@ -5630,7 +5664,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
|
||||
timeline.setPaginationToken(resNewer.next_batch ?? null, Direction.Forward);
|
||||
this.processBeaconEvents(timelineSet.room, events);
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, events);
|
||||
|
||||
// There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring
|
||||
// timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up
|
||||
@@ -5687,7 +5721,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
|
||||
timeline.setPaginationToken(null, Direction.Forward);
|
||||
this.processBeaconEvents(timelineSet.room, events);
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, events);
|
||||
|
||||
return timeline;
|
||||
}
|
||||
@@ -5940,7 +5974,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// in the notification timeline set
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
|
||||
this.processBeaconEvents(timelineSet.room, matrixEvents);
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
|
||||
|
||||
// if we've hit the end of the timeline, we need to stop trying to
|
||||
// paginate. We need to keep the 'forwards' token though, to make sure
|
||||
@@ -5982,7 +6016,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
|
||||
this.processBeaconEvents(room, matrixEvents);
|
||||
this.processAggregatedTimelineEvents(room, matrixEvents);
|
||||
this.processThreadRoots(room, matrixEvents, backwards);
|
||||
|
||||
// if we've hit the end of the timeline, we need to stop trying to
|
||||
@@ -6029,7 +6063,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const originalEvent = await this.fetchRoomEvent(eventTimeline.getRoomId() ?? "", thread.id);
|
||||
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, eventTimeline, null);
|
||||
}
|
||||
this.processBeaconEvents(timelineSet.room, matrixEvents);
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
|
||||
|
||||
// if we've hit the end of the timeline, we need to stop trying to
|
||||
// paginate. We need to keep the 'forwards' token though, to make sure
|
||||
@@ -6067,7 +6101,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
const [timelineEvents] = room.partitionThreadedEvents(matrixEvents);
|
||||
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
||||
this.processBeaconEvents(room, timelineEvents);
|
||||
this.processAggregatedTimelineEvents(room, timelineEvents);
|
||||
this.processThreadRoots(
|
||||
room,
|
||||
timelineEvents.filter((it) => it.getServerAggregatedRelation(THREAD_RELATION_TYPE.name)),
|
||||
@@ -6779,6 +6813,31 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Once the client has been initialised, we want to clear notifications we
|
||||
* know for a fact should be here.
|
||||
* This issue should also be addressed on synapse's side and is tracked as part
|
||||
* of https://github.com/matrix-org/synapse/issues/14837
|
||||
*
|
||||
* We consider a room or a thread as fully read if the current user has sent
|
||||
* the last event in the live timeline of that context and if the read receipt
|
||||
* we have on record matches.
|
||||
*/
|
||||
private fixupRoomNotifications = (): void => {
|
||||
if (this.isInitialSyncComplete()) {
|
||||
const unreadRooms = (this.getRooms() ?? []).filter((room) => {
|
||||
return room.getUnreadNotificationCount(NotificationCountType.Total) > 0;
|
||||
});
|
||||
|
||||
for (const room of unreadRooms) {
|
||||
const currentUserId = this.getSafeUserId();
|
||||
room.fixupNotifications(currentUserId);
|
||||
}
|
||||
|
||||
this.off(ClientEvent.Sync, this.fixupRoomNotifications);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns Promise which resolves: ITurnServerResponse object
|
||||
* @returns Rejects: with an error response.
|
||||
@@ -7009,10 +7068,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const serverVersions = await this.serverVersionsPromise;
|
||||
this.canSupport = await buildFeatureSupportMap(serverVersions);
|
||||
|
||||
// We can set flag values to use their stable or unstable version
|
||||
const support = this.canSupport.get(Feature.ThreadUnreadNotifications);
|
||||
UNREAD_THREAD_NOTIFICATIONS.setPreferUnstable(support === ServerSupport.Unstable);
|
||||
|
||||
return this.serverVersionsPromise;
|
||||
}
|
||||
|
||||
@@ -9320,12 +9375,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* @deprecated use supportsThreads() instead
|
||||
*/
|
||||
public supportsExperimentalThreads(): boolean {
|
||||
logger.warn(`supportsExperimentalThreads() is deprecated, use supportThreads() instead`);
|
||||
return this.clientOpts?.experimentalThreadSupport || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper to determine thread support
|
||||
* @returns a boolean to determine if threads are enabled
|
||||
*/
|
||||
public supportsThreads(): boolean {
|
||||
return this.clientOpts?.threadSupport || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the summary of a room as defined by an initial version of MSC3266 and implemented in Synapse
|
||||
* Proposed at https://github.com/matrix-org/matrix-doc/pull/3266
|
||||
@@ -9340,24 +9404,42 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Processes a list of threaded events and adds them to their respective timelines
|
||||
* @param room - the room the adds the threaded events
|
||||
* @param threadedEvents - an array of the threaded events
|
||||
* @param toStartOfTimeline - the direction in which we want to add the events
|
||||
*/
|
||||
public processThreadEvents(room: Room, threadedEvents: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
room.processThreadedEvents(threadedEvents, toStartOfTimeline);
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Processes a list of thread roots and creates a thread model
|
||||
* @param room - the room to create the threads in
|
||||
* @param threadedEvents - an array of thread roots
|
||||
* @param toStartOfTimeline - the direction
|
||||
*/
|
||||
public processThreadRoots(room: Room, threadedEvents: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
room.processThreadRoots(threadedEvents, toStartOfTimeline);
|
||||
}
|
||||
|
||||
public processBeaconEvents(room?: Room, events?: MatrixEvent[]): void {
|
||||
this.processAggregatedTimelineEvents(room, events);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls aggregation functions for event types that are aggregated
|
||||
* Polls and location beacons
|
||||
* @param room - room the events belong to
|
||||
* @param events - timeline events to be processed
|
||||
* @returns
|
||||
*/
|
||||
public processAggregatedTimelineEvents(room?: Room, events?: MatrixEvent[]): void {
|
||||
if (!events?.length) return;
|
||||
if (!room) return;
|
||||
|
||||
room.currentState.processBeaconEvents(events, this);
|
||||
room.processPollEvents(events);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9422,64 +9504,77 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* accurate notification_count
|
||||
*/
|
||||
export function fixNotificationCountOnDecryption(cli: MatrixClient, event: MatrixEvent): void {
|
||||
const ourUserId = cli.getUserId();
|
||||
const eventId = event.getId();
|
||||
|
||||
const room = cli.getRoom(event.getRoomId());
|
||||
if (!room || !ourUserId || !eventId) return;
|
||||
|
||||
const oldActions = event.getPushActions();
|
||||
const actions = cli.getPushActionsForEvent(event, true);
|
||||
|
||||
const room = cli.getRoom(event.getRoomId());
|
||||
if (!room || !cli.getUserId()) return;
|
||||
|
||||
const isThreadEvent = !!event.threadRootId && !event.isThreadRoot;
|
||||
|
||||
const currentCount = room.getUnreadCountForEventContext(NotificationCountType.Highlight, event);
|
||||
const currentHighlightCount = room.getUnreadCountForEventContext(NotificationCountType.Highlight, event);
|
||||
|
||||
// Ensure the unread counts are kept up to date if the event is encrypted
|
||||
// We also want to make sure that the notification count goes up if we already
|
||||
// have encrypted events to avoid other code from resetting 'highlight' to zero.
|
||||
const oldHighlight = !!oldActions?.tweaks?.highlight;
|
||||
const newHighlight = !!actions?.tweaks?.highlight;
|
||||
if (oldHighlight !== newHighlight || currentCount > 0) {
|
||||
|
||||
let hasReadEvent;
|
||||
if (isThreadEvent) {
|
||||
const thread = room.getThread(event.threadRootId);
|
||||
hasReadEvent = thread
|
||||
? thread.hasUserReadEvent(ourUserId, eventId)
|
||||
: // If the thread object does not exist in the room yet, we don't
|
||||
// want to calculate notification for this event yet. We have not
|
||||
// restored the read receipts yet and can't accurately calculate
|
||||
// notifications at this stage.
|
||||
//
|
||||
// This issue can likely go away when MSC3874 is implemented
|
||||
true;
|
||||
} else {
|
||||
hasReadEvent = room.hasUserReadEvent(ourUserId, eventId);
|
||||
}
|
||||
|
||||
if (hasReadEvent) {
|
||||
// If the event has been read, ignore it.
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldHighlight !== newHighlight || currentHighlightCount > 0) {
|
||||
// TODO: Handle mentions received while the client is offline
|
||||
// See also https://github.com/vector-im/element-web/issues/9069
|
||||
let hasReadEvent;
|
||||
let newCount = currentHighlightCount;
|
||||
if (newHighlight && !oldHighlight) newCount++;
|
||||
if (!newHighlight && oldHighlight) newCount--;
|
||||
|
||||
if (isThreadEvent) {
|
||||
const thread = room.getThread(event.threadRootId);
|
||||
hasReadEvent = thread
|
||||
? thread.hasUserReadEvent(cli.getUserId()!, event.getId()!)
|
||||
: // If the thread object does not exist in the room yet, we don't
|
||||
// want to calculate notification for this event yet. We have not
|
||||
// restored the read receipts yet and can't accurately calculate
|
||||
// highlight notifications at this stage.
|
||||
//
|
||||
// This issue can likely go away when MSC3874 is implemented
|
||||
true;
|
||||
room.setThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Highlight, newCount);
|
||||
} else {
|
||||
hasReadEvent = room.hasUserReadEvent(cli.getUserId()!, event.getId()!);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, newCount);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasReadEvent) {
|
||||
let newCount = currentCount;
|
||||
if (newHighlight && !oldHighlight) newCount++;
|
||||
if (!newHighlight && oldHighlight) newCount--;
|
||||
// Total count is used to typically increment a room notification counter, but not loudly highlight it.
|
||||
const currentTotalCount = room.getUnreadCountForEventContext(NotificationCountType.Total, event);
|
||||
|
||||
if (isThreadEvent) {
|
||||
room.setThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Highlight, newCount);
|
||||
} else {
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, newCount);
|
||||
}
|
||||
// `notify` is used in practice for incrementing the total count
|
||||
const newNotify = !!actions?.notify;
|
||||
|
||||
// Fix 'Mentions Only' rooms from not having the right badge count
|
||||
const totalCount =
|
||||
(isThreadEvent
|
||||
? room.getThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Total)
|
||||
: room.getRoomUnreadNotificationCount(NotificationCountType.Total)) ?? 0;
|
||||
|
||||
if (totalCount < newCount) {
|
||||
if (isThreadEvent) {
|
||||
room.setThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Total, newCount);
|
||||
} else {
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, newCount);
|
||||
}
|
||||
}
|
||||
// The room total count is NEVER incremented by the server for encrypted rooms. We basically ignore
|
||||
// the server here as it's always going to tell us to increment for encrypted events.
|
||||
if (newNotify) {
|
||||
if (isThreadEvent) {
|
||||
room.setThreadUnreadNotificationCount(
|
||||
event.threadRootId,
|
||||
NotificationCountType.Total,
|
||||
currentTotalCount + 1,
|
||||
);
|
||||
} else {
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, currentTotalCount + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypt
|
||||
import type { IToDeviceEvent } from "../sync-accumulator";
|
||||
import type { DeviceTrustLevel, UserTrustLevel } from "../crypto/CrossSigning";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { Room } from "../models/room";
|
||||
import { IEncryptedEventInfo } from "../crypto/api";
|
||||
|
||||
/**
|
||||
@@ -75,6 +76,26 @@ export interface CryptoBackend extends SyncCryptoCallbacks {
|
||||
*/
|
||||
checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel;
|
||||
|
||||
/**
|
||||
* Perform any background tasks that can be done before a message is ready to
|
||||
* send, in order to speed up sending of the message.
|
||||
*
|
||||
* @param room - the room the event is in
|
||||
*/
|
||||
prepareToEncrypt(room: Room): void;
|
||||
|
||||
/**
|
||||
* Encrypt an event according to the configuration of the room.
|
||||
*
|
||||
* @param event - event to be sent
|
||||
*
|
||||
* @param room - destination room.
|
||||
*
|
||||
* @returns Promise which resolves when the event has been
|
||||
* encrypted, or null if nothing was needed
|
||||
*/
|
||||
encryptEvent(event: MatrixEvent, room: Room): Promise<void>;
|
||||
|
||||
/**
|
||||
* Decrypt a received event
|
||||
*
|
||||
@@ -117,6 +138,20 @@ export interface SyncCryptoCallbacks {
|
||||
*/
|
||||
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]>;
|
||||
|
||||
/**
|
||||
* Called by the /sync loop whenever an m.room.encryption event is received.
|
||||
*
|
||||
* This is called before RoomStateEvents are emitted for any of the events in the /sync
|
||||
* response (even if the other events technically happened first). This works around a problem
|
||||
* if the client uses a RoomStateEvent (typically a membership event) as a trigger to send a message
|
||||
* in a new room (or one where encryption has been newly enabled): that would otherwise leave the
|
||||
* crypto layer confused because it expects crypto to be set up, but it has not yet been.
|
||||
*
|
||||
* @param room - in which the event was received
|
||||
* @param event - encryption event to be processed
|
||||
*/
|
||||
onCryptoEvent(room: Room, event: MatrixEvent): Promise<void>;
|
||||
|
||||
/**
|
||||
* Called by the /sync loop after each /sync response is processed.
|
||||
*
|
||||
|
||||
@@ -202,6 +202,9 @@ export type TopicState = {
|
||||
|
||||
export const parseTopicContent = (content: MRoomTopicEventContent): TopicState => {
|
||||
const mtopic = M_TOPIC.findIn<MTopicContent>(content);
|
||||
if (!Array.isArray(mtopic)) {
|
||||
return { text: content.topic };
|
||||
}
|
||||
const text = mtopic?.find((r) => !isProvided(r.mimetype) || r.mimetype === "text/plain")?.body ?? content.topic;
|
||||
const html = mtopic?.find((r) => r.mimetype === "text/html")?.body;
|
||||
return { text, html };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015 - 2021, 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.
|
||||
@@ -43,6 +43,7 @@ import { IMegolmEncryptedContent, IncomingRoomKeyRequest, IEncryptedContent } fr
|
||||
import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager";
|
||||
import { OlmGroupSessionExtraData } from "../../@types/crypto";
|
||||
import { MatrixError } from "../../http-api";
|
||||
import { immediate } from "../../utils";
|
||||
|
||||
// determine whether the key can be shared with invitees
|
||||
export function isRoomSharedHistory(room: Room): boolean {
|
||||
@@ -73,7 +74,6 @@ export interface IOlmDevice<T = DeviceInfo> {
|
||||
deviceInfo: T;
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface IOutboundGroupSessionKey {
|
||||
chain_index: number;
|
||||
key: string;
|
||||
@@ -106,7 +106,6 @@ interface IPayload extends Partial<IMessage> {
|
||||
algorithm?: string;
|
||||
sender_key?: string;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
interface SharedWithData {
|
||||
// The identity key of the device we shared with
|
||||
@@ -223,6 +222,7 @@ export class MegolmEncryption extends EncryptionAlgorithm {
|
||||
private encryptionPreparation?: {
|
||||
promise: Promise<void>;
|
||||
startTime: number;
|
||||
cancel: () => void;
|
||||
};
|
||||
|
||||
protected readonly roomId: string;
|
||||
@@ -974,30 +974,36 @@ export class MegolmEncryption extends EncryptionAlgorithm {
|
||||
* send, in order to speed up sending of the message.
|
||||
*
|
||||
* @param room - the room the event is in
|
||||
* @returns A function that, when called, will stop the preparation
|
||||
*/
|
||||
public prepareToEncrypt(room: Room): void {
|
||||
public prepareToEncrypt(room: Room): () => void {
|
||||
if (room.roomId !== this.roomId) {
|
||||
throw new Error("MegolmEncryption.prepareToEncrypt called on unexpected room");
|
||||
}
|
||||
|
||||
if (this.encryptionPreparation != null) {
|
||||
// We're already preparing something, so don't do anything else.
|
||||
// FIXME: check if we need to restart
|
||||
// (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
|
||||
const elapsedTime = Date.now() - this.encryptionPreparation.startTime;
|
||||
this.prefixedLogger.debug(
|
||||
`Already started preparing to encrypt for this room ${elapsedTime}ms ago, skipping`,
|
||||
);
|
||||
return;
|
||||
return this.encryptionPreparation.cancel;
|
||||
}
|
||||
|
||||
this.prefixedLogger.debug("Preparing to encrypt events");
|
||||
|
||||
let cancelled = false;
|
||||
const isCancelled = (): boolean => cancelled;
|
||||
|
||||
this.encryptionPreparation = {
|
||||
startTime: Date.now(),
|
||||
promise: (async (): Promise<void> => {
|
||||
try {
|
||||
const [devicesInRoom, blocked] = await this.getDevicesInRoom(room);
|
||||
// Attempt to enumerate the devices in room, and gracefully
|
||||
// handle cancellation if it occurs.
|
||||
const getDevicesResult = await this.getDevicesInRoom(room, false, isCancelled);
|
||||
if (getDevicesResult === null) return;
|
||||
const [devicesInRoom, blocked] = getDevicesResult;
|
||||
|
||||
if (this.crypto.globalErrorOnUnknownDevices) {
|
||||
// Drop unknown devices for now. When the message gets sent, we'll
|
||||
@@ -1016,7 +1022,16 @@ export class MegolmEncryption extends EncryptionAlgorithm {
|
||||
delete this.encryptionPreparation;
|
||||
}
|
||||
})(),
|
||||
|
||||
cancel: (): void => {
|
||||
// The caller has indicated that the process should be cancelled,
|
||||
// so tell the promise that we'd like to halt, and reset the preparation state.
|
||||
cancelled = true;
|
||||
delete this.encryptionPreparation;
|
||||
},
|
||||
};
|
||||
|
||||
return this.encryptionPreparation.cancel;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1165,17 +1180,32 @@ export class MegolmEncryption extends EncryptionAlgorithm {
|
||||
*
|
||||
* @param forceDistributeToUnverified - if set to true will include the unverified devices
|
||||
* even if setting is set to block them (useful for verification)
|
||||
* @param isCancelled - will cause the procedure to abort early if and when it starts
|
||||
* returning `true`. If omitted, cancellation won't happen.
|
||||
*
|
||||
* @returns Promise which resolves to an array whose
|
||||
* first element is a map from userId to deviceId to deviceInfo indicating
|
||||
* @returns Promise which resolves to `null`, or an array whose
|
||||
* first element is a {@link DeviceInfoMap} indicating
|
||||
* the devices that messages should be encrypted to, and whose second
|
||||
* element is a map from userId to deviceId to data indicating the devices
|
||||
* that are in the room but that have been blocked
|
||||
* that are in the room but that have been blocked.
|
||||
* If `isCancelled` is provided and returns `true` while processing, `null`
|
||||
* will be returned.
|
||||
* If `isCancelled` is not provided, the Promise will never resolve to `null`.
|
||||
*/
|
||||
private async getDevicesInRoom(
|
||||
room: Room,
|
||||
forceDistributeToUnverified?: boolean,
|
||||
): Promise<[DeviceInfoMap, IBlockedMap]>;
|
||||
private async getDevicesInRoom(
|
||||
room: Room,
|
||||
forceDistributeToUnverified?: boolean,
|
||||
isCancelled?: () => boolean,
|
||||
): Promise<null | [DeviceInfoMap, IBlockedMap]>;
|
||||
private async getDevicesInRoom(
|
||||
room: Room,
|
||||
forceDistributeToUnverified = false,
|
||||
): Promise<[DeviceInfoMap, IBlockedMap]> {
|
||||
isCancelled?: () => boolean,
|
||||
): Promise<null | [DeviceInfoMap, IBlockedMap]> {
|
||||
const members = await room.getEncryptionTargetMembers();
|
||||
this.prefixedLogger.debug(
|
||||
`Encrypting for users (shouldEncryptForInvitedMembers: ${room.shouldEncryptForInvitedMembers()}):`,
|
||||
@@ -1201,6 +1231,11 @@ export class MegolmEncryption extends EncryptionAlgorithm {
|
||||
// See https://github.com/vector-im/element-web/issues/2305 for details.
|
||||
const devices = await this.crypto.downloadKeys(roomMembers, false);
|
||||
const blocked: IBlockedMap = {};
|
||||
|
||||
if (isCancelled?.() === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// remove any blocked devices
|
||||
for (const userId in devices) {
|
||||
if (!devices.hasOwnProperty(userId)) {
|
||||
@@ -1213,6 +1248,11 @@ export class MegolmEncryption extends EncryptionAlgorithm {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Yield prior to checking each device so that we don't block
|
||||
// updating/rendering for too long.
|
||||
// See https://github.com/vector-im/element-web/issues/21612
|
||||
if (isCancelled !== undefined) await immediate();
|
||||
if (isCancelled?.() === true) return null;
|
||||
const deviceTrust = this.crypto.checkDeviceTrust(userId, deviceId);
|
||||
|
||||
if (
|
||||
@@ -1304,7 +1344,7 @@ export class MegolmDecryption extends DecryptionAlgorithm {
|
||||
errorCode = "OLM_UNKNOWN_MESSAGE_INDEX";
|
||||
}
|
||||
|
||||
throw new DecryptionError(errorCode, e ? e.toString() : "Unknown Error: Error is undefined", {
|
||||
throw new DecryptionError(errorCode, e instanceof Error ? e.message : "Unknown Error: Error is undefined", {
|
||||
session: content.sender_key + "|" + content.session_id,
|
||||
});
|
||||
}
|
||||
@@ -1327,7 +1367,8 @@ export class MegolmDecryption extends DecryptionAlgorithm {
|
||||
if (problem) {
|
||||
this.prefixedLogger.info(
|
||||
`When handling UISI from ${event.getSender()} (sender key ${content.sender_key}): ` +
|
||||
`recent session problem with that sender: ${problem}`,
|
||||
`recent session problem with that sender:`,
|
||||
problem,
|
||||
);
|
||||
let problemDescription = PROBLEM_DESCRIPTIONS[problem.type as "no_olm"] || PROBLEM_DESCRIPTIONS.unknown;
|
||||
if (problem.fixed) {
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import anotherjson from "another-json";
|
||||
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../@types/crypto";
|
||||
import { decodeBase64, encodeBase64 } from "./olmlib";
|
||||
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store";
|
||||
import { decryptAES, encryptAES } from "./aes";
|
||||
@@ -23,7 +24,6 @@ import { logger } from "../logger";
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
import { Crypto } from "./index";
|
||||
import { Method } from "../http-api";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
|
||||
export interface IDehydratedDevice {
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
@@ -38,20 +38,6 @@ export interface IDehydratedDeviceKeyInfo {
|
||||
passphrase?: string;
|
||||
}
|
||||
|
||||
export interface IDeviceKeys {
|
||||
algorithms: Array<string>;
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
user_id: string; // eslint-disable-line camelcase
|
||||
keys: Record<string, string>;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
export interface IOneTimeKey {
|
||||
key: string;
|
||||
fallback?: boolean;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
export const DEHYDRATION_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle";
|
||||
|
||||
const oneweek = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
+5
-8
@@ -20,7 +20,7 @@ limitations under the License.
|
||||
import anotherjson from "another-json";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
||||
import type { IDeviceKeys, IEventDecryptionResult, IMegolmSessionData, IOneTimeKey } from "../@types/crypto";
|
||||
import type { PkDecryption, PkSigning } from "@matrix-org/olm";
|
||||
import { EventType, ToDeviceMessageId } from "../@types/event";
|
||||
import { TypedReEmitter } from "../ReEmitter";
|
||||
@@ -63,7 +63,7 @@ import { ToDeviceChannel, ToDeviceRequests, Request } from "./verification/reque
|
||||
import { IllegalMethod } from "./verification/IllegalMethod";
|
||||
import { KeySignatureUploadError } from "../errors";
|
||||
import { calculateKeyCheck, decryptAES, encryptAES } from "./aes";
|
||||
import { DehydrationManager, IDeviceKeys, IOneTimeKey } from "./dehydration";
|
||||
import { DehydrationManager } from "./dehydration";
|
||||
import { BackupManager } from "./backup";
|
||||
import { IStore } from "../store";
|
||||
import { Room, RoomEvent } from "../models/room";
|
||||
@@ -151,7 +151,7 @@ export interface ICryptoCallbacks {
|
||||
requestId: string,
|
||||
secretName: string,
|
||||
deviceTrust: DeviceTrustLevel,
|
||||
) => Promise<string>;
|
||||
) => Promise<string | undefined>;
|
||||
getDehydrationKey?: (keyInfo: ISecretStorageKeyInfo, checkFunc: (key: Uint8Array) => void) => Promise<Uint8Array>;
|
||||
getBackupKey?: () => Promise<Uint8Array>;
|
||||
}
|
||||
@@ -1202,6 +1202,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
*/
|
||||
public async storeSessionBackupPrivateKey(key: ArrayLike<number>): Promise<void> {
|
||||
if (!(key instanceof Uint8Array)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||
throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`);
|
||||
}
|
||||
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
|
||||
@@ -2808,11 +2809,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
* @returns Promise which resolves when the event has been
|
||||
* encrypted, or null if nothing was needed
|
||||
*/
|
||||
public async encryptEvent(event: MatrixEvent, room?: Room): Promise<void> {
|
||||
if (!room) {
|
||||
throw new Error("Cannot send encrypted messages in unknown rooms");
|
||||
}
|
||||
|
||||
public async encryptEvent(event: MatrixEvent, room: Room): Promise<void> {
|
||||
const roomId = event.getRoomId()!;
|
||||
|
||||
const alg = this.roomEncryptors.get(roomId);
|
||||
|
||||
@@ -21,10 +21,10 @@ limitations under the License.
|
||||
import anotherjson from "another-json";
|
||||
|
||||
import type { PkSigning } from "@matrix-org/olm";
|
||||
import type { IOneTimeKey } from "../@types/crypto";
|
||||
import { OlmDevice } from "./OlmDevice";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { logger } from "../logger";
|
||||
import { IOneTimeKey } from "./dehydration";
|
||||
import { IClaimOTKsResult, MatrixClient } from "../client";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
|
||||
@@ -159,6 +159,7 @@ function generateSas(sasBytes: Uint8Array, methods: string[]): IGeneratedSas {
|
||||
const macMethods = {
|
||||
"hkdf-hmac-sha256": "calculate_mac",
|
||||
"org.matrix.msc3783.hkdf-hmac-sha256": "calculate_mac_fixed_base64",
|
||||
"hkdf-hmac-sha256.v2": "calculate_mac_fixed_base64",
|
||||
"hmac-sha256": "calculate_mac_long_kdf",
|
||||
} as const;
|
||||
|
||||
@@ -202,7 +203,12 @@ type KeyAgreement = keyof typeof calculateKeyAgreement;
|
||||
*/
|
||||
const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"];
|
||||
const HASHES_LIST = ["sha256"];
|
||||
const MAC_LIST: MacMethod[] = ["org.matrix.msc3783.hkdf-hmac-sha256", "hkdf-hmac-sha256", "hmac-sha256"];
|
||||
const MAC_LIST: MacMethod[] = [
|
||||
"hkdf-hmac-sha256.v2",
|
||||
"org.matrix.msc3783.hkdf-hmac-sha256",
|
||||
"hkdf-hmac-sha256",
|
||||
"hmac-sha256",
|
||||
];
|
||||
const SAS_LIST = Object.keys(sasGenerators);
|
||||
|
||||
const KEY_AGREEMENT_SET = new Set(KEY_AGREEMENT_LIST);
|
||||
|
||||
@@ -24,7 +24,7 @@ import { MatrixError } from "./http-api";
|
||||
const EMAIL_STAGE_TYPE = "m.login.email.identity";
|
||||
const MSISDN_STAGE_TYPE = "m.login.msisdn";
|
||||
|
||||
interface IFlow {
|
||||
export interface UIAFlow {
|
||||
stages: AuthType[];
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ export interface IAuthData {
|
||||
session?: string;
|
||||
type?: string;
|
||||
completed?: string[];
|
||||
flows?: IFlow[];
|
||||
available_flows?: IFlow[];
|
||||
flows?: UIAFlow[];
|
||||
available_flows?: UIAFlow[];
|
||||
stages?: string[];
|
||||
required_stages?: AuthType[];
|
||||
params?: Record<string, Record<string, any>>;
|
||||
@@ -101,7 +101,7 @@ class NoAuthFlowFoundError extends Error {
|
||||
public name = "NoAuthFlowFoundError";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||
public constructor(m: string, public readonly required_stages: string[], public readonly flows: IFlow[]) {
|
||||
public constructor(m: string, public readonly required_stages: string[], public readonly flows: UIAFlow[]) {
|
||||
super(m);
|
||||
}
|
||||
}
|
||||
@@ -198,7 +198,7 @@ export class InteractiveAuth {
|
||||
private emailSid?: string;
|
||||
private requestingEmailToken = false;
|
||||
private attemptAuthDeferred: IDeferred<IAuthData> | null = null;
|
||||
private chosenFlow: IFlow | null = null;
|
||||
private chosenFlow: UIAFlow | null = null;
|
||||
private currentStage: string | null = null;
|
||||
|
||||
private emailAttempt = 1;
|
||||
@@ -320,7 +320,7 @@ export class InteractiveAuth {
|
||||
return this.data.params?.[loginType];
|
||||
}
|
||||
|
||||
public getChosenFlow(): IFlow | null {
|
||||
public getChosenFlow(): UIAFlow | null {
|
||||
return this.chosenFlow;
|
||||
}
|
||||
|
||||
@@ -573,7 +573,7 @@ export class InteractiveAuth {
|
||||
* @returns flow
|
||||
* @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found
|
||||
*/
|
||||
private chooseFlow(): IFlow {
|
||||
private chooseFlow(): UIAFlow {
|
||||
const flows = this.data.flows || [];
|
||||
|
||||
// we've been given an email or we've already done an email part
|
||||
@@ -610,7 +610,7 @@ export class InteractiveAuth {
|
||||
* @internal
|
||||
* @returns login type
|
||||
*/
|
||||
private firstUncompletedStage(flow: IFlow): AuthType | undefined {
|
||||
private firstUncompletedStage(flow: UIAFlow): AuthType | undefined {
|
||||
const completed = this.data.completed || [];
|
||||
return flow.stages.find((stageType) => !completed.includes(stageType));
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export * from "./models/event";
|
||||
export * from "./models/room";
|
||||
export * from "./models/event-timeline";
|
||||
export * from "./models/event-timeline-set";
|
||||
export * from "./models/poll";
|
||||
export * from "./models/room-member";
|
||||
export * from "./models/room-state";
|
||||
export * from "./models/user";
|
||||
|
||||
+22
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2015 - 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015 - 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.
|
||||
@@ -36,6 +36,7 @@ import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
import { EventStatus } from "./event-status";
|
||||
import { DecryptionError } from "../crypto/algorithms";
|
||||
import { CryptoBackend } from "../common-crypto/CryptoBackend";
|
||||
import { WITHHELD_MESSAGES } from "../crypto/OlmDevice";
|
||||
|
||||
export { EventStatus } from "./event-status";
|
||||
|
||||
@@ -267,12 +268,17 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
private txnId?: string;
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* A reference to the thread this event belongs to
|
||||
*/
|
||||
private thread?: Thread;
|
||||
private threadId?: string;
|
||||
|
||||
/*
|
||||
* True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and
|
||||
* the sender has disabled encrypting to unverified devices.
|
||||
*/
|
||||
private encryptedDisabledForUnverifiedDevices = false;
|
||||
|
||||
/* Set an approximate timestamp for the event relative the local clock.
|
||||
* This will inherently be approximate because it doesn't take into account
|
||||
* the time between the server putting the 'age' field on the event as it sent
|
||||
@@ -546,7 +552,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Get the event ID of the thread head
|
||||
*/
|
||||
public get threadRootId(): string | undefined {
|
||||
@@ -559,7 +564,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* A helper to check if an event is a thread's head or not
|
||||
*/
|
||||
public get isThreadRoot(): boolean {
|
||||
const threadDetails = this.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
|
||||
@@ -706,6 +711,14 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
return this.clearEvent?.content?.msgtype === "m.bad.encrypted";
|
||||
}
|
||||
|
||||
/*
|
||||
* True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and
|
||||
* the sender has disabled encrypting to unverified devices.
|
||||
*/
|
||||
public get isEncryptedDisabledForUnverifiedDevices(): boolean {
|
||||
return this.isDecryptionFailure() && this.encryptedDisabledForUnverifiedDevices;
|
||||
}
|
||||
|
||||
public shouldAttemptDecryption(): boolean {
|
||||
if (this.isRedacted()) return false;
|
||||
if (this.isBeingDecrypted()) return false;
|
||||
@@ -901,6 +914,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
body: "** Unable to decrypt: " + reason + " **",
|
||||
},
|
||||
},
|
||||
encryptedDisabledForUnverifiedDevices: reason === `DecryptionError: ${WITHHELD_MESSAGES["m.unverified"]}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -922,6 +936,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
this.claimedEd25519Key = decryptionResult.claimedEd25519Key ?? null;
|
||||
this.forwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain || [];
|
||||
this.untrusted = decryptionResult.untrusted || false;
|
||||
this.encryptedDisabledForUnverifiedDevices = decryptionResult.encryptedDisabledForUnverifiedDevices || false;
|
||||
this.invalidateExtensibleEvent();
|
||||
}
|
||||
|
||||
@@ -1557,7 +1572,8 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Set the instance of a thread associated with the current event
|
||||
* @param thread - the thread
|
||||
*/
|
||||
public setThread(thread?: Thread): void {
|
||||
if (this.thread) {
|
||||
@@ -1571,7 +1587,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Get the instance of the thread associated with the current event
|
||||
*/
|
||||
public getThread(): Thread | undefined {
|
||||
return this.thread;
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
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 { M_POLL_END, M_POLL_RESPONSE } from "../@types/polls";
|
||||
import { MatrixClient } from "../client";
|
||||
import { PollStartEvent } from "../extensible_events_v1/PollStartEvent";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { Relations } from "./relations";
|
||||
import { Room } from "./room";
|
||||
import { TypedEventEmitter } from "./typed-event-emitter";
|
||||
|
||||
export enum PollEvent {
|
||||
New = "Poll.new",
|
||||
End = "Poll.end",
|
||||
Update = "Poll.update",
|
||||
Responses = "Poll.Responses",
|
||||
Destroy = "Poll.Destroy",
|
||||
UndecryptableRelations = "Poll.UndecryptableRelations",
|
||||
}
|
||||
|
||||
export type PollEventHandlerMap = {
|
||||
[PollEvent.Update]: (event: MatrixEvent, poll: Poll) => void;
|
||||
[PollEvent.Destroy]: (pollIdentifier: string) => void;
|
||||
[PollEvent.End]: () => void;
|
||||
[PollEvent.Responses]: (responses: Relations) => void;
|
||||
[PollEvent.UndecryptableRelations]: (count: number) => void;
|
||||
};
|
||||
|
||||
const filterResponseRelations = (
|
||||
relationEvents: MatrixEvent[],
|
||||
pollEndTimestamp: number,
|
||||
): {
|
||||
responseEvents: MatrixEvent[];
|
||||
} => {
|
||||
const responseEvents = relationEvents.filter((event) => {
|
||||
if (event.isDecryptionFailure()) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
M_POLL_RESPONSE.matches(event.getType()) &&
|
||||
// From MSC3381:
|
||||
// "Votes sent on or before the end event's timestamp are valid votes"
|
||||
event.getTs() <= pollEndTimestamp
|
||||
);
|
||||
});
|
||||
|
||||
return { responseEvents };
|
||||
};
|
||||
|
||||
export class Poll extends TypedEventEmitter<Exclude<PollEvent, PollEvent.New>, PollEventHandlerMap> {
|
||||
public readonly roomId: string;
|
||||
public readonly pollEvent: PollStartEvent;
|
||||
private _isFetchingResponses = false;
|
||||
private relationsNextBatch: string | undefined;
|
||||
private responses: null | Relations = null;
|
||||
private endEvent: MatrixEvent | undefined;
|
||||
/**
|
||||
* Keep track of undecryptable relations
|
||||
* As incomplete result sets affect poll results
|
||||
*/
|
||||
private undecryptableRelationEventIds = new Set<string>();
|
||||
|
||||
public constructor(public readonly rootEvent: MatrixEvent, private matrixClient: MatrixClient, private room: Room) {
|
||||
super();
|
||||
if (!this.rootEvent.getRoomId() || !this.rootEvent.getId()) {
|
||||
throw new Error("Invalid poll start event.");
|
||||
}
|
||||
this.roomId = this.rootEvent.getRoomId()!;
|
||||
this.pollEvent = this.rootEvent.unstableExtensibleEvent as unknown as PollStartEvent;
|
||||
}
|
||||
|
||||
public get pollId(): string {
|
||||
return this.rootEvent.getId()!;
|
||||
}
|
||||
|
||||
public get endEventId(): string | undefined {
|
||||
return this.endEvent?.getId();
|
||||
}
|
||||
|
||||
public get isEnded(): boolean {
|
||||
return !!this.endEvent;
|
||||
}
|
||||
|
||||
public get isFetchingResponses(): boolean {
|
||||
return this._isFetchingResponses;
|
||||
}
|
||||
|
||||
public get undecryptableRelationsCount(): number {
|
||||
return this.undecryptableRelationEventIds.size;
|
||||
}
|
||||
|
||||
public async getResponses(): Promise<Relations> {
|
||||
// if we have already fetched some responses
|
||||
// just return them
|
||||
if (this.responses) {
|
||||
return this.responses;
|
||||
}
|
||||
|
||||
// if there is no fetching in progress
|
||||
// start fetching
|
||||
if (!this.isFetchingResponses) {
|
||||
await this.fetchResponses();
|
||||
}
|
||||
// return whatever responses we got from the first page
|
||||
return this.responses!;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param event - event with a relation to the rootEvent
|
||||
* @returns void
|
||||
*/
|
||||
public onNewRelation(event: MatrixEvent): void {
|
||||
if (M_POLL_END.matches(event.getType()) && this.validateEndEvent(event)) {
|
||||
this.endEvent = event;
|
||||
this.refilterResponsesOnEnd();
|
||||
this.emit(PollEvent.End);
|
||||
}
|
||||
|
||||
// wait for poll responses to be initialised
|
||||
if (!this.responses) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pollEndTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER;
|
||||
const { responseEvents } = filterResponseRelations([event], pollEndTimestamp);
|
||||
|
||||
this.countUndecryptableEvents([event]);
|
||||
|
||||
if (responseEvents.length) {
|
||||
responseEvents.forEach((event) => {
|
||||
this.responses!.addEvent(event);
|
||||
});
|
||||
|
||||
this.emit(PollEvent.Responses, this.responses);
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchResponses(): Promise<void> {
|
||||
this._isFetchingResponses = true;
|
||||
|
||||
// we want:
|
||||
// - stable and unstable M_POLL_RESPONSE
|
||||
// - stable and unstable M_POLL_END
|
||||
// so make one api call and filter by event type client side
|
||||
const allRelations = await this.matrixClient.relations(
|
||||
this.roomId,
|
||||
this.rootEvent.getId()!,
|
||||
"m.reference",
|
||||
undefined,
|
||||
{
|
||||
from: this.relationsNextBatch || undefined,
|
||||
},
|
||||
);
|
||||
|
||||
await Promise.all(allRelations.events.map((event) => this.matrixClient.decryptEventIfNeeded(event)));
|
||||
|
||||
const responses =
|
||||
this.responses ||
|
||||
new Relations("m.reference", M_POLL_RESPONSE.name, this.matrixClient, [M_POLL_RESPONSE.altName!]);
|
||||
|
||||
const pollEndEvent = allRelations.events.find((event) => M_POLL_END.matches(event.getType()));
|
||||
|
||||
if (this.validateEndEvent(pollEndEvent)) {
|
||||
this.endEvent = pollEndEvent;
|
||||
this.refilterResponsesOnEnd();
|
||||
this.emit(PollEvent.End);
|
||||
}
|
||||
|
||||
const pollCloseTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER;
|
||||
|
||||
const { responseEvents } = filterResponseRelations(allRelations.events, pollCloseTimestamp);
|
||||
|
||||
responseEvents.forEach((event) => {
|
||||
responses.addEvent(event);
|
||||
});
|
||||
|
||||
this.relationsNextBatch = allRelations.nextBatch ?? undefined;
|
||||
this.responses = responses;
|
||||
this.countUndecryptableEvents(allRelations.events);
|
||||
|
||||
// while there are more pages of relations
|
||||
// fetch them
|
||||
if (this.relationsNextBatch) {
|
||||
// don't await
|
||||
// we want to return the first page as soon as possible
|
||||
this.fetchResponses();
|
||||
} else {
|
||||
// no more pages
|
||||
this._isFetchingResponses = false;
|
||||
}
|
||||
|
||||
// emit after updating _isFetchingResponses state
|
||||
this.emit(PollEvent.Responses, this.responses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only responses made before the poll ended are valid
|
||||
* Refilter after an end event is recieved
|
||||
* To ensure responses are valid
|
||||
*/
|
||||
private refilterResponsesOnEnd(): void {
|
||||
if (!this.responses) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pollEndTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER;
|
||||
this.responses.getRelations().forEach((event) => {
|
||||
if (event.getTs() > pollEndTimestamp) {
|
||||
this.responses?.removeEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
this.emit(PollEvent.Responses, this.responses);
|
||||
}
|
||||
|
||||
private countUndecryptableEvents = (events: MatrixEvent[]): void => {
|
||||
const undecryptableEventIds = events
|
||||
.filter((event) => event.isDecryptionFailure())
|
||||
.map((event) => event.getId()!);
|
||||
|
||||
const previousCount = this.undecryptableRelationsCount;
|
||||
this.undecryptableRelationEventIds = new Set([...this.undecryptableRelationEventIds, ...undecryptableEventIds]);
|
||||
|
||||
if (this.undecryptableRelationsCount !== previousCount) {
|
||||
this.emit(PollEvent.UndecryptableRelations, this.undecryptableRelationsCount);
|
||||
}
|
||||
};
|
||||
|
||||
private validateEndEvent(endEvent?: MatrixEvent): boolean {
|
||||
if (!endEvent) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Repeated end events are ignored -
|
||||
* only the first (valid) closure event by origin_server_ts is counted.
|
||||
*/
|
||||
if (this.endEvent && this.endEvent.getTs() < endEvent.getTs()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* MSC3381
|
||||
* If a m.poll.end event is received from someone other than the poll creator or user with permission to redact
|
||||
* others' messages in the room, the event must be ignored by clients due to being invalid.
|
||||
*/
|
||||
const roomCurrentState = this.room.currentState;
|
||||
const endEventSender = endEvent.getSender();
|
||||
return (
|
||||
!!endEventSender &&
|
||||
(endEventSender === this.rootEvent.getSender() ||
|
||||
roomCurrentState.maySendRedactionForEvent(this.rootEvent, endEventSender))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import * as utils from "../utils";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { EventType } from "../@types/event";
|
||||
import { EventTimelineSet } from "./event-timeline-set";
|
||||
import { NotificationCountType } from "./room";
|
||||
|
||||
export function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent {
|
||||
return new MatrixEvent({
|
||||
@@ -219,6 +220,29 @@ export abstract class ReadReceipt<
|
||||
|
||||
public abstract addReceipt(event: MatrixEvent, synthetic: boolean): void;
|
||||
|
||||
public abstract setUnread(type: NotificationCountType, count: number): void;
|
||||
|
||||
/**
|
||||
* This issue should also be addressed on synapse's side and is tracked as part
|
||||
* of https://github.com/matrix-org/synapse/issues/14837
|
||||
*
|
||||
* Retrieves the read receipt for the logged in user and checks if it matches
|
||||
* the last event in the room and whether that event originated from the logged
|
||||
* in user.
|
||||
* Under those conditions we can consider the context as read. This is useful
|
||||
* because we never send read receipts against our own events
|
||||
* @param userId - the logged in user
|
||||
*/
|
||||
public fixupNotifications(userId: string): void {
|
||||
const receipt = this.getReadReceiptForUserId(userId, false);
|
||||
|
||||
const lastEvent = this.timeline[this.timeline.length - 1];
|
||||
if (lastEvent && receipt?.eventId === lastEvent.getId() && userId === lastEvent.getSender()) {
|
||||
this.setUnread(NotificationCountType.Total, 0);
|
||||
this.setUnread(NotificationCountType.Highlight, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a temporary local-echo receipt to the room to reflect in the
|
||||
* client the fact that we've sent one.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2021, 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.
|
||||
@@ -122,7 +122,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
||||
*
|
||||
* @param event - The relation event to remove.
|
||||
*/
|
||||
private async removeEvent(event: MatrixEvent): Promise<void> {
|
||||
public async removeEvent(event: MatrixEvent): Promise<void> {
|
||||
if (!this.relations.has(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -964,6 +964,62 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
return guestAccessContent["guest_access"] || GuestAccess.Forbidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the predecessor room based on this room state.
|
||||
*
|
||||
* @param msc3946ProcessDynamicPredecessor - if true, look for an
|
||||
* m.room.predecessor state event and use it if found (MSC3946).
|
||||
* @returns null if this room has no predecessor. Otherwise, returns
|
||||
* the roomId and last eventId of the predecessor room.
|
||||
* If msc3946ProcessDynamicPredecessor is true, use m.predecessor events
|
||||
* as well as m.room.create events to find predecessors.
|
||||
* Note: if an m.predecessor event is used, eventId may be undefined
|
||||
* since last_known_event_id is optional.
|
||||
*/
|
||||
public findPredecessor(msc3946ProcessDynamicPredecessor = false): { roomId: string; eventId?: string } | null {
|
||||
// Note: the tests for this function are against Room.findPredecessor,
|
||||
// which just calls through to here.
|
||||
|
||||
if (msc3946ProcessDynamicPredecessor) {
|
||||
const predecessorEvent = this.getStateEvents(EventType.RoomPredecessor, "");
|
||||
if (predecessorEvent) {
|
||||
const content = predecessorEvent.getContent<{
|
||||
predecessor_room_id: string;
|
||||
last_known_event_id?: string;
|
||||
}>();
|
||||
const roomId = content.predecessor_room_id;
|
||||
let eventId = content.last_known_event_id;
|
||||
if (typeof eventId !== "string") {
|
||||
eventId = undefined;
|
||||
}
|
||||
if (typeof roomId === "string") {
|
||||
return { roomId, eventId };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createEvent = this.getStateEvents(EventType.RoomCreate, "");
|
||||
if (createEvent) {
|
||||
const predecessor = createEvent.getContent<{
|
||||
predecessor?: Partial<{
|
||||
room_id: string;
|
||||
event_id: string;
|
||||
}>;
|
||||
}>()["predecessor"];
|
||||
if (predecessor) {
|
||||
const roomId = predecessor["room_id"];
|
||||
if (typeof roomId === "string") {
|
||||
let eventId = predecessor["event_id"];
|
||||
if (typeof eventId !== "string" || eventId === "") {
|
||||
eventId = undefined;
|
||||
}
|
||||
return { roomId, eventId };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private updateThirdPartyTokenCache(memberEvent: MatrixEvent): void {
|
||||
if (!memberEvent.getContent().third_party_invite) {
|
||||
return;
|
||||
|
||||
+109
-36
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
import { M_POLL_START, Optional } from "matrix-events-sdk";
|
||||
|
||||
import {
|
||||
EventTimelineSet,
|
||||
@@ -64,7 +64,7 @@ import {
|
||||
import { IStateEventWithRoomId } from "../@types/search";
|
||||
import { RelationsContainer } from "./relations-container";
|
||||
import { ReadReceipt, synthesizeReceipt } from "./read-receipt";
|
||||
import { Feature, ServerSupport } from "../feature";
|
||||
import { Poll, PollEvent } from "./poll";
|
||||
|
||||
// These constants are used as sane defaults when the homeserver doesn't support
|
||||
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
||||
@@ -162,7 +162,8 @@ export type RoomEmittedEvents =
|
||||
| BeaconEvent.New
|
||||
| BeaconEvent.Update
|
||||
| BeaconEvent.Destroy
|
||||
| BeaconEvent.LivenessChange;
|
||||
| BeaconEvent.LivenessChange
|
||||
| PollEvent.New;
|
||||
|
||||
export type RoomEventHandlerMap = {
|
||||
/**
|
||||
@@ -289,6 +290,11 @@ export type RoomEventHandlerMap = {
|
||||
[RoomEvent.UnreadNotifications]: (unreadNotifications?: NotificationCount, threadId?: string) => void;
|
||||
[RoomEvent.TimelineRefresh]: (room: Room, eventTimelineSet: EventTimelineSet) => void;
|
||||
[ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void;
|
||||
/**
|
||||
* Fires when a new poll instance is added to the room state
|
||||
* @param poll - the new poll
|
||||
*/
|
||||
[PollEvent.New]: (poll: Poll) => void;
|
||||
} & Pick<ThreadHandlerMap, ThreadEvent.Update | ThreadEvent.NewReply | ThreadEvent.Delete> &
|
||||
EventTimelineSetHandlerMap &
|
||||
Pick<MatrixEventHandlerMap, MatrixEventEvent.BeforeRedaction> &
|
||||
@@ -317,6 +323,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
*/
|
||||
private unthreadedReceipts = new Map<string, Receipt>();
|
||||
private readonly timelineSets: EventTimelineSet[];
|
||||
public readonly polls: Map<string, Poll> = new Map<string, Poll>();
|
||||
public readonly threadsTimelineSets: EventTimelineSet[] = [];
|
||||
// any filtered timeline sets we're maintaining for this room
|
||||
private readonly filteredTimelineSets: Record<string, EventTimelineSet> = {}; // filter_id: timelineSet
|
||||
@@ -375,7 +382,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
public readonly relations = new RelationsContainer(this.client, this);
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* A collection of events known by the client
|
||||
* This is not a comprehensive list of the threads that exist in this room
|
||||
*/
|
||||
private threads = new Map<string, Thread>();
|
||||
public lastThread?: Thread;
|
||||
@@ -475,7 +483,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
return this.threadTimelineSetsPromise;
|
||||
}
|
||||
|
||||
if (this.client?.supportsExperimentalThreads()) {
|
||||
if (this.client?.supportsThreads()) {
|
||||
try {
|
||||
this.threadTimelineSetsPromise = Promise.all([
|
||||
this.createThreadTimelineSet(),
|
||||
@@ -1290,10 +1298,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
*/
|
||||
public getUnreadNotificationCount(type = NotificationCountType.Total): number {
|
||||
let count = this.getRoomUnreadNotificationCount(type);
|
||||
if (this.client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported) {
|
||||
for (const threadNotification of this.threadNotifications.values()) {
|
||||
count += threadNotification[type] ?? 0;
|
||||
}
|
||||
for (const threadNotification of this.threadNotifications.values()) {
|
||||
count += threadNotification[type] ?? 0;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
@@ -1322,7 +1328,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Get one of the notification counts for a thread
|
||||
* @param threadId - the root event ID
|
||||
* @param type - The type of notification count to get. default: 'total'
|
||||
@@ -1334,7 +1339,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Checks if the current room has unread thread notifications
|
||||
* @returns
|
||||
*/
|
||||
@@ -1348,7 +1352,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Swet one of the notification count for a thread
|
||||
* @param threadId - the root event ID
|
||||
* @param type - The type of notification count to get. default: 'total'
|
||||
@@ -1369,7 +1372,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* @returns the notification count type for all the threads in the room
|
||||
*/
|
||||
public get threadsAggregateNotificationType(): NotificationCountType | null {
|
||||
@@ -1385,7 +1387,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Resets the thread notifications for this room
|
||||
*/
|
||||
public resetThreadUnreadNotificationCount(notificationsToKeep?: string[]): void {
|
||||
@@ -1411,6 +1412,10 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
this.emit(RoomEvent.UnreadNotifications, this.notificationCounts);
|
||||
}
|
||||
|
||||
public setUnread(type: NotificationCountType, count: number): void {
|
||||
return this.setUnreadNotificationCount(type, count);
|
||||
}
|
||||
|
||||
public setSummary(summary: IRoomSummary): void {
|
||||
const heroes = summary["m.heroes"];
|
||||
const joinedCount = summary["m.joined_member_count"];
|
||||
@@ -1545,14 +1550,16 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Get the instance of the thread associated with the current event
|
||||
* @param eventId - the ID of the current event
|
||||
* @returns a thread instance if known
|
||||
*/
|
||||
public getThread(eventId: string): Thread | null {
|
||||
return this.threads.get(eventId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Get all the known threads in the room
|
||||
*/
|
||||
public getThreads(): Thread[] {
|
||||
return Array.from(this.threads.values());
|
||||
@@ -1819,7 +1826,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* Without server support that means fetching as much at once as the server allows us to.
|
||||
*/
|
||||
public async fetchRoomThreads(): Promise<void> {
|
||||
if (this.threadsReady || !this.client.supportsExperimentalThreads()) {
|
||||
if (this.threadsReady || !this.client.supportsThreads()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1890,6 +1897,38 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
this.threadsReady = true;
|
||||
}
|
||||
|
||||
public async processPollEvents(events: MatrixEvent[]): Promise<void> {
|
||||
const processPollStartEvent = (event: MatrixEvent): void => {
|
||||
if (!M_POLL_START.matches(event.getType())) return;
|
||||
try {
|
||||
const poll = new Poll(event, this.client, this);
|
||||
this.polls.set(event.getId()!, poll);
|
||||
this.emit(PollEvent.New, poll);
|
||||
} catch {}
|
||||
// poll creation can fail for malformed poll start events
|
||||
};
|
||||
|
||||
const processPollRelationEvent = (event: MatrixEvent): void => {
|
||||
const relationEventId = event.relationEventId;
|
||||
if (relationEventId && this.polls.has(relationEventId)) {
|
||||
const poll = this.polls.get(relationEventId);
|
||||
poll?.onNewRelation(event);
|
||||
}
|
||||
};
|
||||
|
||||
const processPollEvent = (event: MatrixEvent): void => {
|
||||
processPollStartEvent(event);
|
||||
processPollRelationEvent(event);
|
||||
};
|
||||
|
||||
for (const event of events) {
|
||||
try {
|
||||
await this.client.decryptEventIfNeeded(event);
|
||||
processPollEvent(event);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single page of threadlist messages for the specific thread filter
|
||||
* @internal
|
||||
@@ -1964,7 +2003,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
shouldLiveInThread: boolean;
|
||||
threadId?: string;
|
||||
} {
|
||||
if (!this.client?.supportsExperimentalThreads()) {
|
||||
if (!this.client?.supportsThreads()) {
|
||||
return {
|
||||
shouldLiveInRoom: true,
|
||||
shouldLiveInThread: false,
|
||||
@@ -2033,7 +2072,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
|
||||
/**
|
||||
* Adds events to a thread's timeline. Will fire "Thread.update"
|
||||
* @experimental
|
||||
*/
|
||||
public processThreadedEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
events.forEach(this.applyRedaction);
|
||||
@@ -2662,7 +2700,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
// Indices to the events array, for readability
|
||||
const ROOM = 0;
|
||||
const THREAD = 1;
|
||||
if (this.client.supportsExperimentalThreads()) {
|
||||
if (this.client.supportsThreads()) {
|
||||
const threadRoots = this.findThreadRoots(events);
|
||||
return events.reduce(
|
||||
(memo, event: MatrixEvent) => {
|
||||
@@ -2728,6 +2766,21 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
receipt,
|
||||
synthetic,
|
||||
);
|
||||
|
||||
// If the read receipt sent for the logged in user matches
|
||||
// the last event of the live timeline, then we know for a fact
|
||||
// that the user has read that message.
|
||||
// We can mark the room as read and not wait for the local echo
|
||||
// from synapse
|
||||
// This needs to be done after the initial sync as we do not want this
|
||||
// logic to run whilst the room is being initialised
|
||||
if (this.client.isInitialSyncComplete() && userId === this.client.getUserId()) {
|
||||
const lastEvent = receiptDestination.timeline[receiptDestination.timeline.length - 1];
|
||||
if (lastEvent && eventId === lastEvent.getId() && userId === lastEvent.getSender()) {
|
||||
receiptDestination.setUnread(NotificationCountType.Total, 0);
|
||||
receiptDestination.setUnread(NotificationCountType.Highlight, 0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// The thread does not exist locally, keep the read receipt
|
||||
// in a cache locally, and re-apply the `addReceipt` logic
|
||||
@@ -2991,26 +3044,23 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the ID of the room that was this room's predecessor, or null if
|
||||
* this room has no predecessor.
|
||||
* Find the predecessor of this room.
|
||||
*
|
||||
* @param msc3946ProcessDynamicPredecessor - if true, look for an
|
||||
* m.room.predecessor state event and use it if found (MSC3946).
|
||||
* @returns null if this room has no predecessor. Otherwise, returns
|
||||
* the roomId and last eventId of the predecessor room.
|
||||
* If msc3946ProcessDynamicPredecessor is true, use m.predecessor events
|
||||
* as well as m.room.create events to find predecessors.
|
||||
* Note: if an m.predecessor event is used, eventId may be undefined
|
||||
* since last_known_event_id is optional.
|
||||
*/
|
||||
public findPredecessorRoomId(): string | null {
|
||||
public findPredecessor(msc3946ProcessDynamicPredecessor = false): { roomId: string; eventId?: string } | null {
|
||||
const currentState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
if (!currentState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const createEvent = currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
if (createEvent) {
|
||||
const predecessor = createEvent.getContent()["predecessor"];
|
||||
if (predecessor) {
|
||||
const roomId = predecessor["room_id"];
|
||||
if (roomId) {
|
||||
return roomId;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return currentState.findPredecessor(msc3946ProcessDynamicPredecessor);
|
||||
}
|
||||
|
||||
private roomNameGenerator(state: RoomNameState): string {
|
||||
@@ -3341,13 +3391,36 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
public getLastUnthreadedReceiptFor(userId: string): Receipt | undefined {
|
||||
return this.unthreadedReceipts.get(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* This issue should also be addressed on synapse's side and is tracked as part
|
||||
* of https://github.com/matrix-org/synapse/issues/14837
|
||||
*
|
||||
*
|
||||
* We consider a room fully read if the current user has sent
|
||||
* the last event in the live timeline of that context and if the read receipt
|
||||
* we have on record matches.
|
||||
* This also detects all unread threads and applies the same logic to those
|
||||
* contexts
|
||||
*/
|
||||
public fixupNotifications(userId: string): void {
|
||||
super.fixupNotifications(userId);
|
||||
|
||||
const unreadThreads = this.getThreads().filter(
|
||||
(thread) => this.getThreadUnreadNotificationCount(thread.id, NotificationCountType.Total) > 0,
|
||||
);
|
||||
|
||||
for (const thread of unreadThreads) {
|
||||
thread.fixupNotifications(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a map from current event status to a list of allowed next statuses
|
||||
const ALLOWED_TRANSITIONS: Record<EventStatus, EventStatus[]> = {
|
||||
[EventStatus.ENCRYPTING]: [EventStatus.SENDING, EventStatus.NOT_SENT, EventStatus.CANCELLED],
|
||||
[EventStatus.SENDING]: [EventStatus.ENCRYPTING, EventStatus.QUEUED, EventStatus.NOT_SENT, EventStatus.SENT],
|
||||
[EventStatus.QUEUED]: [EventStatus.SENDING, EventStatus.CANCELLED],
|
||||
[EventStatus.QUEUED]: [EventStatus.SENDING, EventStatus.NOT_SENT, EventStatus.CANCELLED],
|
||||
[EventStatus.SENT]: [],
|
||||
[EventStatus.NOT_SENT]: [EventStatus.SENDING, EventStatus.QUEUED, EventStatus.CANCELLED],
|
||||
[EventStatus.CANCELLED]: [],
|
||||
|
||||
@@ -22,7 +22,7 @@ import { RelationType } from "../@types/event";
|
||||
import { IThreadBundledRelationship, MatrixEvent, MatrixEventEvent } from "./event";
|
||||
import { Direction, EventTimeline } from "./event-timeline";
|
||||
import { EventTimelineSet, EventTimelineSetHandlerMap } from "./event-timeline-set";
|
||||
import { Room, RoomEvent } from "./room";
|
||||
import { NotificationCountType, Room, RoomEvent } from "./room";
|
||||
import { RoomState } from "./room-state";
|
||||
import { ServerControlledNamespacedValue } from "../NamespacedValue";
|
||||
import { logger } from "../logger";
|
||||
@@ -69,9 +69,6 @@ export function determineFeatureSupport(stable: boolean, unstable: boolean): Fea
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
public static hasServerSideSupport = FeatureSupport.None;
|
||||
public static hasServerSideListSupport = FeatureSupport.None;
|
||||
@@ -641,6 +638,10 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
|
||||
return super.hasUserReadEvent(userId, eventId);
|
||||
}
|
||||
|
||||
public setUnread(type: NotificationCountType, count: number): void {
|
||||
return this.room.setThreadUnreadNotificationCount(this.id, type, count);
|
||||
}
|
||||
}
|
||||
|
||||
export const FILTER_RELATED_BY_SENDERS = new ServerControlledNamespacedValue(
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
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 { OlmMachine, UserId } from "@matrix-org/matrix-sdk-crypto-js";
|
||||
|
||||
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
|
||||
|
||||
/**
|
||||
* KeyClaimManager: linearises calls to OlmMachine.getMissingSessions to avoid races
|
||||
*
|
||||
* We have one of these per `RustCrypto` (and hence per `MatrixClient`).
|
||||
*/
|
||||
export class KeyClaimManager {
|
||||
private currentClaimPromise: Promise<void>;
|
||||
private stopped = false;
|
||||
|
||||
public constructor(
|
||||
private readonly olmMachine: OlmMachine,
|
||||
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
|
||||
) {
|
||||
this.currentClaimPromise = Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the KeyClaimManager to immediately stop processing requests.
|
||||
*
|
||||
* Any further calls, and any still in the queue, will fail with an error.
|
||||
*/
|
||||
public stop(): void {
|
||||
this.stopped = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of users, attempt to ensure that we have Olm Sessions active with each of their devices
|
||||
*
|
||||
* If we don't have an active olm session, we will claim a one-time key and start one.
|
||||
*
|
||||
* @param userList - list of userIDs to claim
|
||||
*/
|
||||
public ensureSessionsForUsers(userList: Array<UserId>): Promise<void> {
|
||||
// The Rust-SDK requires that we only have one getMissingSessions process in flight at once. This little dance
|
||||
// ensures that, by only having one call to ensureSessionsForUsersInner active at once (and making them
|
||||
// queue up in order).
|
||||
const prom = this.currentClaimPromise.finally(() => this.ensureSessionsForUsersInner(userList));
|
||||
this.currentClaimPromise = prom;
|
||||
return prom;
|
||||
}
|
||||
|
||||
private async ensureSessionsForUsersInner(userList: Array<UserId>): Promise<void> {
|
||||
// bail out quickly if we've been stopped.
|
||||
if (this.stopped) {
|
||||
throw new Error(`Cannot ensure Olm sessions: shutting down`);
|
||||
}
|
||||
const claimRequest = await this.olmMachine.getMissingSessions(userList);
|
||||
if (claimRequest) {
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(claimRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
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 {
|
||||
OlmMachine,
|
||||
KeysBackupRequest,
|
||||
KeysClaimRequest,
|
||||
KeysQueryRequest,
|
||||
KeysUploadRequest,
|
||||
RoomMessageRequest,
|
||||
SignatureUploadRequest,
|
||||
ToDeviceRequest,
|
||||
} from "@matrix-org/matrix-sdk-crypto-js";
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { IHttpOpts, MatrixHttpApi, Method } from "../http-api";
|
||||
import { QueryDict } from "../utils";
|
||||
|
||||
/**
|
||||
* Common interface for all the request types returned by `OlmMachine.outgoingRequests`.
|
||||
*/
|
||||
export interface OutgoingRequest {
|
||||
readonly id: string | undefined;
|
||||
readonly type: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* OutgoingRequestManager: turns `OutgoingRequest`s from the rust sdk into HTTP requests
|
||||
*
|
||||
* We have one of these per `RustCrypto` (and hence per `MatrixClient`), not that it does anything terribly complicated.
|
||||
* It's responsible for:
|
||||
*
|
||||
* * holding the reference to the `MatrixHttpApi`
|
||||
* * turning `OutgoingRequest`s from the rust backend into HTTP requests, and sending them
|
||||
* * sending the results of such requests back to the rust backend.
|
||||
*/
|
||||
export class OutgoingRequestProcessor {
|
||||
public constructor(
|
||||
private readonly olmMachine: OlmMachine,
|
||||
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
) {}
|
||||
|
||||
public async makeOutgoingRequest(msg: OutgoingRequest): Promise<void> {
|
||||
let resp: string;
|
||||
|
||||
/* refer https://docs.rs/matrix-sdk-crypto/0.6.0/matrix_sdk_crypto/requests/enum.OutgoingRequests.html
|
||||
* for the complete list of request types
|
||||
*/
|
||||
if (msg instanceof KeysUploadRequest) {
|
||||
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/upload", {}, msg.body);
|
||||
} else if (msg instanceof KeysQueryRequest) {
|
||||
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/query", {}, msg.body);
|
||||
} else if (msg instanceof KeysClaimRequest) {
|
||||
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/claim", {}, msg.body);
|
||||
} else if (msg instanceof SignatureUploadRequest) {
|
||||
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/signatures/upload", {}, msg.body);
|
||||
} else if (msg instanceof KeysBackupRequest) {
|
||||
resp = await this.rawJsonRequest(Method.Put, "/_matrix/client/v3/room_keys/keys", {}, msg.body);
|
||||
} else if (msg instanceof ToDeviceRequest) {
|
||||
const path =
|
||||
`/_matrix/client/v3/sendToDevice/${encodeURIComponent(msg.event_type)}/` +
|
||||
encodeURIComponent(msg.txn_id);
|
||||
resp = await this.rawJsonRequest(Method.Put, path, {}, msg.body);
|
||||
} else if (msg instanceof RoomMessageRequest) {
|
||||
const path =
|
||||
`/_matrix/client/v3/room/${encodeURIComponent(msg.room_id)}/send/` +
|
||||
`${encodeURIComponent(msg.event_type)}/${encodeURIComponent(msg.txn_id)}`;
|
||||
resp = await this.rawJsonRequest(Method.Put, path, {}, msg.body);
|
||||
} else {
|
||||
logger.warn("Unsupported outgoing message", Object.getPrototypeOf(msg));
|
||||
resp = "";
|
||||
}
|
||||
|
||||
if (msg.id) {
|
||||
await this.olmMachine.markRequestAsSent(msg.id, msg.type, resp);
|
||||
}
|
||||
}
|
||||
|
||||
private async rawJsonRequest(method: Method, path: string, queryParams: QueryDict, body: string): Promise<string> {
|
||||
const opts = {
|
||||
// inhibit the JSON stringification and parsing within HttpApi.
|
||||
json: false,
|
||||
|
||||
// nevertheless, we are sending, and accept, JSON.
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
|
||||
// we use the full prefix
|
||||
prefix: "",
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this.http.authedRequest<string>(method, path, queryParams, body, opts);
|
||||
logger.info(`rust-crypto: successfully made HTTP request: ${method} ${path}`);
|
||||
return response;
|
||||
} catch (e) {
|
||||
logger.warn(`rust-crypto: error making HTTP request: ${method} ${path}: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
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 { EncryptionSettings, OlmMachine, RoomId, UserId } from "@matrix-org/matrix-sdk-crypto-js";
|
||||
|
||||
import { EventType } from "../@types/event";
|
||||
import { IContent, MatrixEvent } from "../models/event";
|
||||
import { Room } from "../models/room";
|
||||
import { logger, PrefixedLogger } from "../logger";
|
||||
import { KeyClaimManager } from "./KeyClaimManager";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
|
||||
/**
|
||||
* RoomEncryptor: responsible for encrypting messages to a given room
|
||||
*/
|
||||
export class RoomEncryptor {
|
||||
private readonly prefixedLogger: PrefixedLogger;
|
||||
|
||||
/**
|
||||
* @param olmMachine - The rust-sdk's OlmMachine
|
||||
* @param keyClaimManager - Our KeyClaimManager, which manages the queue of one-time-key claim requests
|
||||
* @param room - The room we want to encrypt for
|
||||
* @param encryptionSettings - body of the m.room.encryption event currently in force in this room
|
||||
*/
|
||||
public constructor(
|
||||
private readonly olmMachine: OlmMachine,
|
||||
private readonly keyClaimManager: KeyClaimManager,
|
||||
private readonly room: Room,
|
||||
private encryptionSettings: IContent,
|
||||
) {
|
||||
this.prefixedLogger = logger.withPrefix(`[${room.roomId} encryption]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a new `m.room.encryption` event in this room
|
||||
*
|
||||
* @param config - The content of the encryption event
|
||||
*/
|
||||
public onCryptoEvent(config: IContent): void {
|
||||
if (JSON.stringify(this.encryptionSettings) != JSON.stringify(config)) {
|
||||
this.prefixedLogger.error(`Ignoring m.room.encryption event which requests a change of config`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a new `m.room.member` event in this room
|
||||
*
|
||||
* @param member - new membership state
|
||||
*/
|
||||
public onRoomMembership(member: RoomMember): void {
|
||||
this.prefixedLogger.debug(`${member.membership} event for ${member.userId}`);
|
||||
|
||||
if (
|
||||
member.membership == "join" ||
|
||||
(member.membership == "invite" && this.room.shouldEncryptForInvitedMembers())
|
||||
) {
|
||||
// make sure we are tracking the deviceList for this user
|
||||
this.prefixedLogger.debug(`starting to track devices for: ${member.userId}`);
|
||||
this.olmMachine.updateTrackedUsers([new UserId(member.userId)]);
|
||||
}
|
||||
|
||||
// TODO: handle leaves (including our own)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare to encrypt events in this room.
|
||||
*
|
||||
* This ensures that we have a megolm session ready to use and that we have shared its key with all the devices
|
||||
* in the room.
|
||||
*/
|
||||
public async ensureEncryptionSession(): Promise<void> {
|
||||
if (this.encryptionSettings.algorithm !== "m.megolm.v1.aes-sha2") {
|
||||
throw new Error(
|
||||
`Cannot encrypt in ${this.room.roomId} for unsupported algorithm '${this.encryptionSettings.algorithm}'`,
|
||||
);
|
||||
}
|
||||
|
||||
const members = await this.room.getEncryptionTargetMembers();
|
||||
this.prefixedLogger.debug(
|
||||
`Encrypting for users (shouldEncryptForInvitedMembers: ${this.room.shouldEncryptForInvitedMembers()}):`,
|
||||
members.map((u) => `${u.userId} (${u.membership})`),
|
||||
);
|
||||
|
||||
const userList = members.map((u) => new UserId(u.userId));
|
||||
await this.keyClaimManager.ensureSessionsForUsers(userList);
|
||||
|
||||
const rustEncryptionSettings = new EncryptionSettings();
|
||||
/* FIXME historyVisibility, rotation, etc */
|
||||
|
||||
await this.olmMachine.shareRoomKey(new RoomId(this.room.roomId), userList, rustEncryptionSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt an event for this room
|
||||
*
|
||||
* This will ensure that we have a megolm session for this room, share it with the devices in the room, and
|
||||
* then encrypt the event using the session.
|
||||
*
|
||||
* @param event - Event to be encrypted.
|
||||
*/
|
||||
public async encryptEvent(event: MatrixEvent): Promise<void> {
|
||||
await this.ensureEncryptionSession();
|
||||
|
||||
const encryptedContent = await this.olmMachine.encryptRoomEvent(
|
||||
new RoomId(this.room.roomId),
|
||||
event.getType(),
|
||||
JSON.stringify(event.getContent()),
|
||||
);
|
||||
|
||||
event.makeEncrypted(
|
||||
EventType.RoomMessageEncrypted,
|
||||
JSON.parse(encryptedContent),
|
||||
this.olmMachine.identityKeys.curve25519.toBase64(),
|
||||
this.olmMachine.identityKeys.ed25519.toBase64(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
|
||||
import { RustCrypto } from "./rust-crypto";
|
||||
import { logger } from "../logger";
|
||||
import { CryptoBackend } from "../common-crypto/CryptoBackend";
|
||||
import { RUST_SDK_STORE_PREFIX } from "./constants";
|
||||
import { IHttpOpts, MatrixHttpApi } from "../http-api";
|
||||
|
||||
@@ -26,7 +25,7 @@ export async function initRustCrypto(
|
||||
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
): Promise<CryptoBackend> {
|
||||
): Promise<RustCrypto> {
|
||||
// initialise the rust matrix-sdk-crypto-js, if it hasn't already been done
|
||||
await RustSdkCryptoJs.initAsync();
|
||||
|
||||
|
||||
+100
-67
@@ -15,32 +15,20 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import {
|
||||
DecryptedRoomEvent,
|
||||
KeysBackupRequest,
|
||||
KeysClaimRequest,
|
||||
KeysQueryRequest,
|
||||
KeysUploadRequest,
|
||||
SignatureUploadRequest,
|
||||
} from "@matrix-org/matrix-sdk-crypto-js";
|
||||
|
||||
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
||||
import type { IToDeviceEvent } from "../sync-accumulator";
|
||||
import type { IEncryptedEventInfo } from "../crypto/api";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
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, Method } from "../http-api";
|
||||
import { QueryDict } from "../utils";
|
||||
import { IHttpOpts, MatrixHttpApi } from "../http-api";
|
||||
import { DeviceTrustLevel, UserTrustLevel } from "../crypto/CrossSigning";
|
||||
|
||||
/**
|
||||
* Common interface for all the request types returned by `OlmMachine.outgoingRequests`.
|
||||
*/
|
||||
interface OutgoingRequest {
|
||||
readonly id: string | undefined;
|
||||
readonly type: number;
|
||||
}
|
||||
import { RoomEncryptor } from "./RoomEncryptor";
|
||||
import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
|
||||
import { KeyClaimManager } from "./KeyClaimManager";
|
||||
|
||||
/**
|
||||
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
|
||||
@@ -55,12 +43,27 @@ export class RustCrypto implements CryptoBackend {
|
||||
/** whether {@link outgoingRequestLoop} is currently running */
|
||||
private outgoingRequestLoopRunning = false;
|
||||
|
||||
/** mapping of roomId → encryptor class */
|
||||
private roomEncryptors: Record<string, RoomEncryptor> = {};
|
||||
|
||||
private keyClaimManager: KeyClaimManager;
|
||||
private outgoingRequestProcessor: OutgoingRequestProcessor;
|
||||
|
||||
public constructor(
|
||||
private readonly olmMachine: RustSdkCryptoJs.OlmMachine,
|
||||
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
_userId: string,
|
||||
_deviceId: string,
|
||||
) {}
|
||||
) {
|
||||
this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http);
|
||||
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// CryptoBackend implementation
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public stop(): void {
|
||||
// stop() may be called multiple times, but attempting to close() the OlmMachine twice
|
||||
@@ -70,13 +73,43 @@ export class RustCrypto implements CryptoBackend {
|
||||
}
|
||||
this.stopped = true;
|
||||
|
||||
this.keyClaimManager.stop();
|
||||
|
||||
// make sure we close() the OlmMachine; doing so means that all the Rust objects will be
|
||||
// cleaned up; in particular, the indexeddb connections will be closed, which means they
|
||||
// can then be deleted.
|
||||
this.olmMachine.close();
|
||||
}
|
||||
|
||||
public prepareToEncrypt(room: Room): void {
|
||||
const encryptor = this.roomEncryptors[room.roomId];
|
||||
|
||||
if (encryptor) {
|
||||
encryptor.ensureEncryptionSession();
|
||||
}
|
||||
}
|
||||
|
||||
public async encryptEvent(event: MatrixEvent, _room: Room): Promise<void> {
|
||||
const roomId = event.getRoomId()!;
|
||||
const encryptor = this.roomEncryptors[roomId];
|
||||
|
||||
if (!encryptor) {
|
||||
throw new Error(`Cannot encrypt event in unconfigured room ${roomId}`);
|
||||
}
|
||||
|
||||
await encryptor.encryptEvent(event);
|
||||
}
|
||||
|
||||
public async decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult> {
|
||||
const roomId = event.getRoomId();
|
||||
if (!roomId) {
|
||||
// presumably, a to-device message. These are normally decrypted in preprocessToDeviceMessages
|
||||
// so the fact it has come back here suggests that decryption failed.
|
||||
//
|
||||
// once we drop support for the libolm crypto implementation, we can stop passing to-device messages
|
||||
// through decryptEvent and hence get rid of this case.
|
||||
throw new Error("to-device event was not decrypted in preprocessToDeviceMessages");
|
||||
}
|
||||
const res = (await this.olmMachine.decryptRoomEvent(
|
||||
JSON.stringify({
|
||||
event_id: event.getId(),
|
||||
@@ -87,7 +120,7 @@ export class RustCrypto implements CryptoBackend {
|
||||
origin_server_ts: event.getTs(),
|
||||
}),
|
||||
new RustSdkCryptoJs.RoomId(event.getRoomId()!),
|
||||
)) as DecryptedRoomEvent;
|
||||
)) as RustSdkCryptoJs.DecryptedRoomEvent;
|
||||
return {
|
||||
clearEvent: JSON.parse(res.event),
|
||||
claimedEd25519Key: res.senderClaimedEd25519Key,
|
||||
@@ -159,6 +192,30 @@ export class RustCrypto implements CryptoBackend {
|
||||
return JSON.parse(result);
|
||||
}
|
||||
|
||||
/** called by the sync loop on m.room.encrypted events
|
||||
*
|
||||
* @param room - in which the event was received
|
||||
* @param event - encryption event to be processed
|
||||
*/
|
||||
public async onCryptoEvent(room: Room, event: MatrixEvent): Promise<void> {
|
||||
const config = event.getContent();
|
||||
|
||||
const existingEncryptor = this.roomEncryptors[room.roomId];
|
||||
if (existingEncryptor) {
|
||||
existingEncryptor.onCryptoEvent(config);
|
||||
} else {
|
||||
this.roomEncryptors[room.roomId] = new RoomEncryptor(this.olmMachine, this.keyClaimManager, room, config);
|
||||
}
|
||||
|
||||
// start tracking devices for any users already known to be in this room.
|
||||
const members = await room.getEncryptionTargetMembers();
|
||||
logger.debug(
|
||||
`[${room.roomId} encryption] starting to track devices for: `,
|
||||
members.map((u) => `${u.userId} (${u.membership})`),
|
||||
);
|
||||
await this.olmMachine.updateTrackedUsers(members.map((u) => new RustSdkCryptoJs.UserId(u.userId)));
|
||||
}
|
||||
|
||||
/** called by the sync loop after processing each sync.
|
||||
*
|
||||
* TODO: figure out something equivalent for sliding sync.
|
||||
@@ -171,6 +228,27 @@ export class RustCrypto implements CryptoBackend {
|
||||
this.outgoingRequestLoop();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Other public functions
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/** called by the MatrixClient on a room membership event
|
||||
*
|
||||
* @param event - The matrix event which caused this event to fire.
|
||||
* @param member - The member whose RoomMember.membership changed.
|
||||
* @param oldMembership - The previous membership state. Null if it's a new member.
|
||||
*/
|
||||
public onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void {
|
||||
const enc = this.roomEncryptors[event.getRoomId()!];
|
||||
if (!enc) {
|
||||
// not encrypting in this room
|
||||
return;
|
||||
}
|
||||
enc.onRoomMembership(member);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Outgoing requests
|
||||
@@ -190,7 +268,7 @@ export class RustCrypto implements CryptoBackend {
|
||||
return;
|
||||
}
|
||||
for (const msg of outgoingRequests) {
|
||||
await this.doOutgoingRequest(msg as OutgoingRequest);
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(msg as OutgoingRequest);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -199,49 +277,4 @@ export class RustCrypto implements CryptoBackend {
|
||||
this.outgoingRequestLoopRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async doOutgoingRequest(msg: OutgoingRequest): Promise<void> {
|
||||
let resp: string;
|
||||
|
||||
/* refer https://docs.rs/matrix-sdk-crypto/0.6.0/matrix_sdk_crypto/requests/enum.OutgoingRequests.html
|
||||
* for the complete list of request types
|
||||
*/
|
||||
if (msg instanceof KeysUploadRequest) {
|
||||
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/upload", {}, msg.body);
|
||||
} else if (msg instanceof KeysQueryRequest) {
|
||||
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/query", {}, msg.body);
|
||||
} else if (msg instanceof KeysClaimRequest) {
|
||||
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/claim", {}, msg.body);
|
||||
} else if (msg instanceof SignatureUploadRequest) {
|
||||
resp = await this.rawJsonRequest(Method.Post, "/_matrix/client/v3/keys/signatures/upload", {}, msg.body);
|
||||
} else if (msg instanceof KeysBackupRequest) {
|
||||
resp = await this.rawJsonRequest(Method.Put, "/_matrix/client/v3/room_keys/keys", {}, msg.body);
|
||||
} else {
|
||||
// TODO: ToDeviceRequest, RoomMessageRequest
|
||||
logger.warn("Unsupported outgoing message", Object.getPrototypeOf(msg));
|
||||
resp = "";
|
||||
}
|
||||
|
||||
if (msg.id) {
|
||||
await this.olmMachine.markRequestAsSent(msg.id, msg.type, resp);
|
||||
}
|
||||
}
|
||||
|
||||
private async rawJsonRequest(method: Method, path: string, queryParams: QueryDict, body: string): Promise<string> {
|
||||
const opts = {
|
||||
// inhibit the JSON stringification and parsing within HttpApi.
|
||||
json: false,
|
||||
|
||||
// nevertheless, we are sending, and accept, JSON.
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
|
||||
// we use the full prefix
|
||||
prefix: "",
|
||||
};
|
||||
|
||||
return await this.http.authedRequest<string>(method, path, queryParams, body, opts);
|
||||
}
|
||||
}
|
||||
|
||||
+20
-10
@@ -245,12 +245,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
|
||||
// get head of queue
|
||||
const obj = this.peekNextEvent(queueName);
|
||||
if (!obj) {
|
||||
// queue is empty. Mark as inactive and stop recursing.
|
||||
const index = this.activeQueues.indexOf(queueName);
|
||||
if (index >= 0) {
|
||||
this.activeQueues.splice(index, 1);
|
||||
}
|
||||
debuglog("Stopping queue '%s' as it is now empty", queueName);
|
||||
this.disableQueue(queueName);
|
||||
return;
|
||||
}
|
||||
debuglog("Queue '%s' has %s pending events", queueName, this.queues[queueName].length);
|
||||
@@ -289,10 +284,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
|
||||
// give up (you quitter!)
|
||||
debuglog("Queue '%s' giving up on event %s", queueName, obj.event.getId());
|
||||
// remove this from the queue
|
||||
this.removeNextEvent(queueName);
|
||||
obj.defer.reject(err);
|
||||
// process next event
|
||||
this.processQueue(queueName);
|
||||
this.clearQueue(queueName, err);
|
||||
} else {
|
||||
setTimeout(this.processQueue, waitTimeMs, queueName);
|
||||
}
|
||||
@@ -300,6 +292,24 @@ export class MatrixScheduler<T = ISendEventResponse> {
|
||||
);
|
||||
};
|
||||
|
||||
private disableQueue(queueName: string): void {
|
||||
// queue is empty. Mark as inactive and stop recursing.
|
||||
const index = this.activeQueues.indexOf(queueName);
|
||||
if (index >= 0) {
|
||||
this.activeQueues.splice(index, 1);
|
||||
}
|
||||
debuglog("Stopping queue '%s' as it is now empty", queueName);
|
||||
}
|
||||
|
||||
private clearQueue(queueName: string, err: unknown): void {
|
||||
debuglog("clearing queue '%s'", queueName);
|
||||
let obj: IQueueEntry<T> | undefined;
|
||||
while ((obj = this.removeNextEvent(queueName))) {
|
||||
obj.defer.reject(err);
|
||||
}
|
||||
this.disableQueue(queueName);
|
||||
}
|
||||
|
||||
private peekNextEvent(queueName: string): IQueueEntry<T> | undefined {
|
||||
const queue = this.queues[queueName];
|
||||
if (!Array.isArray(queue)) {
|
||||
|
||||
@@ -736,8 +736,8 @@ export class SlidingSyncSdk {
|
||||
|
||||
const processRoomEvent = async (e: MatrixEvent): Promise<void> => {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
if (e.isState() && e.getType() == EventType.RoomEncryption && this.syncOpts.crypto) {
|
||||
await this.syncOpts.crypto.onCryptoEvent(room, e);
|
||||
if (e.isState() && e.getType() == EventType.RoomEncryption && this.syncOpts.cryptoCallbacks) {
|
||||
await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+35
-21
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2015 - 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015 - 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.
|
||||
@@ -198,7 +198,7 @@ export function defaultClientOpts(opts?: IStoredClientOpts): IStoredClientOpts {
|
||||
resolveInvitesToProfiles: false,
|
||||
pollTimeout: 30 * 1000,
|
||||
pendingEventOrdering: PendingEventOrdering.Chronological,
|
||||
experimentalThreadSupport: false,
|
||||
threadSupport: false,
|
||||
...opts,
|
||||
};
|
||||
}
|
||||
@@ -1254,11 +1254,12 @@ export class SyncApi {
|
||||
|
||||
const inviter = room.currentState.getStateEvents(EventType.RoomMember, client.getUserId()!)?.getSender();
|
||||
|
||||
if (client.isCryptoEnabled()) {
|
||||
const parkedHistory = await client.crypto!.cryptoStore.takeParkedSharedHistory(room.roomId);
|
||||
const crypto = client.crypto;
|
||||
if (crypto) {
|
||||
const parkedHistory = await crypto.cryptoStore.takeParkedSharedHistory(room.roomId);
|
||||
for (const parked of parkedHistory) {
|
||||
if (parked.senderId === inviter) {
|
||||
await client.crypto!.olmDevice.addInboundGroupSession(
|
||||
await crypto.olmDevice.addInboundGroupSession(
|
||||
room.roomId,
|
||||
parked.senderKey,
|
||||
parked.forwardingCurve25519KeyChain,
|
||||
@@ -1298,18 +1299,29 @@ export class SyncApi {
|
||||
const accountDataEvents = this.mapSyncEventsFormat(joinObj.account_data);
|
||||
|
||||
const encrypted = client.isRoomEncrypted(room.roomId);
|
||||
// we do this first so it's correct when any of the events fire
|
||||
// We store the server-provided value first so it's correct when any of the events fire.
|
||||
if (joinObj.unread_notifications) {
|
||||
room.setUnreadNotificationCount(
|
||||
NotificationCountType.Total,
|
||||
joinObj.unread_notifications.notification_count ?? 0,
|
||||
);
|
||||
/**
|
||||
* We track unread notifications ourselves in encrypted rooms, so don't
|
||||
* bother setting it here. We trust our calculations better than the
|
||||
* server's for this case, and therefore will assume that our non-zero
|
||||
* count is accurate.
|
||||
*
|
||||
* @see import("./client").fixNotificationCountOnDecryption
|
||||
*/
|
||||
if (!encrypted || joinObj.unread_notifications.notification_count === 0) {
|
||||
// In an encrypted room, if the room has notifications enabled then it's typical for
|
||||
// the server to flag all new messages as notifying. However, some push rules calculate
|
||||
// events as ignored based on their event contents (e.g. ignoring msgtype=m.notice messages)
|
||||
// so we want to calculate this figure on the client in all cases.
|
||||
room.setUnreadNotificationCount(
|
||||
NotificationCountType.Total,
|
||||
joinObj.unread_notifications.notification_count ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
// We track unread notifications ourselves in encrypted rooms, so don't
|
||||
// bother setting it here. We trust our calculations better than the
|
||||
// server's for this case, and therefore will assume that our non-zero
|
||||
// count is accurate.
|
||||
if (!encrypted || room.getUnreadNotificationCount(NotificationCountType.Highlight) <= 0) {
|
||||
// If the locally stored highlight count is zero, use the server provided value.
|
||||
room.setUnreadNotificationCount(
|
||||
NotificationCountType.Highlight,
|
||||
joinObj.unread_notifications.highlight_count ?? 0,
|
||||
@@ -1326,11 +1338,13 @@ export class SyncApi {
|
||||
// decryption
|
||||
room.resetThreadUnreadNotificationCount(Object.keys(unreadThreadNotifications));
|
||||
for (const [threadId, unreadNotification] of Object.entries(unreadThreadNotifications)) {
|
||||
room.setThreadUnreadNotificationCount(
|
||||
threadId,
|
||||
NotificationCountType.Total,
|
||||
unreadNotification.notification_count ?? 0,
|
||||
);
|
||||
if (!encrypted || unreadNotification.notification_count === 0) {
|
||||
room.setThreadUnreadNotificationCount(
|
||||
threadId,
|
||||
NotificationCountType.Total,
|
||||
unreadNotification.notification_count ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
const hasNoNotifications =
|
||||
room.getThreadUnreadNotificationCount(threadId, NotificationCountType.Highlight) <= 0;
|
||||
@@ -1408,10 +1422,10 @@ export class SyncApi {
|
||||
// avoids a race condition if the application tries to send a message after the
|
||||
// state event is processed, but before crypto is enabled, which then causes the
|
||||
// crypto layer to complain.
|
||||
if (this.syncOpts.crypto) {
|
||||
if (this.syncOpts.cryptoCallbacks) {
|
||||
for (const e of stateEvents.concat(events)) {
|
||||
if (e.isState() && e.getType() === EventType.RoomEncryption && e.getStateKey() === "") {
|
||||
await this.syncOpts.crypto.onCryptoEvent(room, e);
|
||||
await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-3
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015, 2016, 2019, 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.
|
||||
@@ -392,13 +391,22 @@ export function ensureNoTrailingSlash(url?: string): string | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a promise which resolves with a given value after the given number of ms
|
||||
/**
|
||||
* Returns a promise which resolves with a given value after the given number of ms
|
||||
*/
|
||||
export function sleep<T>(ms: number, value?: T): Promise<T> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms, value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise/async version of {@link setImmediate}.
|
||||
*/
|
||||
export function immediate(): Promise<void> {
|
||||
return new Promise(setImmediate);
|
||||
}
|
||||
|
||||
export function isNullOrUndefined(val: any): boolean {
|
||||
return val === null || val === undefined;
|
||||
}
|
||||
|
||||
+240
-153
File diff suppressed because it is too large
Load Diff
@@ -131,7 +131,7 @@ export class CallEventHandler {
|
||||
try {
|
||||
await this.handleCallEvent(event);
|
||||
} catch (e) {
|
||||
logger.error("Caught exception handling call event", e);
|
||||
logger.error("CallEventHandler evaluateEventBuffer() caught exception handling call event", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,20 +207,26 @@ export class CallEventHandler {
|
||||
groupCall = this.client.groupCallEventHandler!.getGroupCallById(groupCallId);
|
||||
|
||||
if (!groupCall) {
|
||||
logger.warn(`Cannot find a group call ${groupCallId} for event ${type}. Ignoring event.`);
|
||||
logger.warn(
|
||||
`CallEventHandler handleCallEvent() could not find a group call - ignoring event (groupCallId=${groupCallId}, type=${type})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
opponentDeviceId = content.device_id;
|
||||
|
||||
if (!opponentDeviceId) {
|
||||
logger.warn(`Cannot find a device id for ${senderId}. Ignoring event.`);
|
||||
logger.warn(
|
||||
`CallEventHandler handleCallEvent() could not find a device id - ignoring event (senderId=${senderId})`,
|
||||
);
|
||||
groupCall.emit(GroupCallEvent.Error, new GroupCallUnknownDeviceError(senderId));
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.dest_session_id !== this.client.getSessionId()) {
|
||||
logger.warn("Call event does not match current session id, ignoring.");
|
||||
logger.warn(
|
||||
"CallEventHandler handleCallEvent() call event does not match current session id - ignoring",
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -240,8 +246,8 @@ export class CallEventHandler {
|
||||
if (call && call.state === CallState.Ended) return;
|
||||
|
||||
if (call) {
|
||||
logger.log(
|
||||
`WARN: Already have a MatrixCall with id ${content.call_id} but got an ` + `invite. Clobbering.`,
|
||||
logger.warn(
|
||||
`CallEventHandler handleCallEvent() already has a call but got an invite - clobbering (callId=${content.call_id})`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -250,7 +256,9 @@ export class CallEventHandler {
|
||||
}
|
||||
|
||||
const timeUntilTurnCresExpire = (this.client.getTurnServersExpiry() ?? 0) - Date.now();
|
||||
logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
|
||||
logger.info(
|
||||
"CallEventHandler handleCallEvent() current turn creds expire in " + timeUntilTurnCresExpire + " ms",
|
||||
);
|
||||
call =
|
||||
createNewMatrixCall(this.client, callRoomId, {
|
||||
forceTURN: this.client.forceTURN,
|
||||
@@ -259,7 +267,9 @@ export class CallEventHandler {
|
||||
opponentSessionId: content.sender_session_id,
|
||||
}) ?? undefined;
|
||||
if (!call) {
|
||||
logger.log("Incoming call ID " + content.call_id + " but this client " + "doesn't support WebRTC");
|
||||
logger.log(
|
||||
`CallEventHandler handleCallEvent() this client does not support WebRTC (callId=${content.call_id})`,
|
||||
);
|
||||
// don't hang up the call: there could be other clients
|
||||
// connected that do support WebRTC and declining the
|
||||
// the call on their behalf would be really annoying.
|
||||
@@ -308,18 +318,12 @@ export class CallEventHandler {
|
||||
if (existingCall) {
|
||||
if (existingCall.callId > call.callId) {
|
||||
logger.log(
|
||||
"Glare detected: answering incoming call " +
|
||||
call.callId +
|
||||
" and canceling outgoing call " +
|
||||
existingCall.callId,
|
||||
`CallEventHandler handleCallEvent() detected glare - answering incoming call and canceling outgoing call (incomingId=${call.callId}, outgoingId=${existingCall.callId})`,
|
||||
);
|
||||
existingCall.replacedBy(call);
|
||||
} else {
|
||||
logger.log(
|
||||
"Glare detected: rejecting incoming call " +
|
||||
call.callId +
|
||||
" and keeping outgoing call " +
|
||||
existingCall.callId,
|
||||
`CallEventHandler handleCallEvent() detected glare - hanging up incoming call (incomingId=${call.callId}, outgoingId=${existingCall.callId})`,
|
||||
);
|
||||
call.hangup(CallErrorCode.Replaced, true);
|
||||
}
|
||||
@@ -376,7 +380,9 @@ export class CallEventHandler {
|
||||
|
||||
// The following events need a call and a peer connection
|
||||
if (!call || !call.hasPeerConnection) {
|
||||
logger.info(`Discarding possible call event ${event.getId()} as we don't have a call/peerConn`, type);
|
||||
logger.info(
|
||||
`CallEventHandler handleCallEvent() discarding possible call event as we don't have a call (type=${type})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Ignore remote echo
|
||||
|
||||
@@ -309,7 +309,7 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
|
||||
public clone(): CallFeed {
|
||||
const mediaHandler = this.client.getMediaHandler();
|
||||
const stream = this.stream.clone();
|
||||
logger.log(`callFeed cloning stream ${this.stream.id} newStream ${stream.id}`);
|
||||
logger.log(`CallFeed clone() cloning stream (originalStreamId=${this.stream.id}, newStreamId${stream.id})`);
|
||||
|
||||
if (this.purpose === SDPStreamMetadataPurpose.Usermedia) {
|
||||
mediaHandler.userMediaStreams.push(stream);
|
||||
|
||||
+64
-24
@@ -367,7 +367,7 @@ export class GroupCall extends TypedEventEmitter<
|
||||
}
|
||||
|
||||
private async initLocalCallFeedInternal(): Promise<void> {
|
||||
logger.log(`groupCall ${this.groupCallId} initLocalCallFeed`);
|
||||
logger.log(`GroupCall ${this.groupCallId} initLocalCallFeedInternal() running`);
|
||||
|
||||
let stream: MediaStream;
|
||||
|
||||
@@ -413,7 +413,7 @@ export class GroupCall extends TypedEventEmitter<
|
||||
const micShouldBeMuted = this.localCallFeed.isAudioMuted();
|
||||
const vidShouldBeMuted = this.localCallFeed.isVideoMuted();
|
||||
logger.log(
|
||||
`groupCall ${this.groupCallId} updateLocalUsermediaStream oldStream ${oldStream.id} newStream ${stream.id} micShouldBeMuted ${micShouldBeMuted} vidShouldBeMuted ${vidShouldBeMuted}`,
|
||||
`GroupCall ${this.groupCallId} updateLocalUsermediaStream() (oldStreamId=${oldStream.id}, newStreamId=${stream.id}, micShouldBeMuted=${micShouldBeMuted}, vidShouldBeMuted=${vidShouldBeMuted})`,
|
||||
);
|
||||
setTracksEnabled(stream.getAudioTracks(), !micShouldBeMuted);
|
||||
setTracksEnabled(stream.getVideoTracks(), !vidShouldBeMuted);
|
||||
@@ -428,7 +428,7 @@ export class GroupCall extends TypedEventEmitter<
|
||||
throw new Error(`Cannot enter call in the "${this.state}" state`);
|
||||
}
|
||||
|
||||
logger.log(`Entered group call ${this.groupCallId}`);
|
||||
logger.log(`GroupCall ${this.groupCallId} enter() running`);
|
||||
this.state = GroupCallState.Entered;
|
||||
|
||||
this.client.on(CallEventHandlerEvent.Incoming, this.onIncomingCall);
|
||||
@@ -570,14 +570,19 @@ export class GroupCall extends TypedEventEmitter<
|
||||
const updates: Promise<void>[] = [];
|
||||
this.forEachCall((call) => updates.push(call.sendMetadataUpdate()));
|
||||
|
||||
await Promise.all(updates).catch((e) => logger.info("Failed to send some metadata updates", e));
|
||||
await Promise.all(updates).catch((e) =>
|
||||
logger.info(
|
||||
`GroupCall ${this.groupCallId} setMicrophoneMuted() failed to send some metadata updates`,
|
||||
e,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
if (sendUpdatesBefore) await sendUpdates();
|
||||
|
||||
if (this.localCallFeed) {
|
||||
logger.log(
|
||||
`groupCall ${this.groupCallId} setMicrophoneMuted stream ${this.localCallFeed.stream.id} muted ${muted}`,
|
||||
`GroupCall ${this.groupCallId} setMicrophoneMuted() (streamId=${this.localCallFeed.stream.id}, muted=${muted})`,
|
||||
);
|
||||
this.localCallFeed.setAudioVideoMuted(muted, null);
|
||||
// I don't believe its actually necessary to enable these tracks: they
|
||||
@@ -586,7 +591,7 @@ export class GroupCall extends TypedEventEmitter<
|
||||
// anywhere. Let's do it anyway to avoid confusion.
|
||||
setTracksEnabled(this.localCallFeed.stream.getAudioTracks(), !muted);
|
||||
} else {
|
||||
logger.log(`groupCall ${this.groupCallId} setMicrophoneMuted no stream muted ${muted}`);
|
||||
logger.log(`GroupCall ${this.groupCallId} setMicrophoneMuted() no stream muted (muted=${muted})`);
|
||||
this.initWithAudioMuted = muted;
|
||||
}
|
||||
|
||||
@@ -613,7 +618,7 @@ export class GroupCall extends TypedEventEmitter<
|
||||
|
||||
if (this.localCallFeed) {
|
||||
logger.log(
|
||||
`groupCall ${this.groupCallId} setLocalVideoMuted stream ${this.localCallFeed.stream.id} muted ${muted}`,
|
||||
`GroupCall ${this.groupCallId} setLocalVideoMuted() (stream=${this.localCallFeed.stream.id}, muted=${muted})`,
|
||||
);
|
||||
|
||||
const stream = await this.client.getMediaHandler().getUserMediaStream(true, !muted);
|
||||
@@ -621,7 +626,7 @@ export class GroupCall extends TypedEventEmitter<
|
||||
this.localCallFeed.setAudioVideoMuted(null, muted);
|
||||
setTracksEnabled(this.localCallFeed.stream.getVideoTracks(), !muted);
|
||||
} else {
|
||||
logger.log(`groupCall ${this.groupCallId} setLocalVideoMuted no stream muted ${muted}`);
|
||||
logger.log(`GroupCall ${this.groupCallId} setLocalVideoMuted() no stream muted (muted=${muted})`);
|
||||
this.initWithVideoMuted = muted;
|
||||
}
|
||||
|
||||
@@ -641,7 +646,9 @@ export class GroupCall extends TypedEventEmitter<
|
||||
|
||||
if (enabled) {
|
||||
try {
|
||||
logger.log("Asking for screensharing permissions...");
|
||||
logger.log(
|
||||
`GroupCall ${this.groupCallId} setScreensharingEnabled() is asking for screensharing permissions`,
|
||||
);
|
||||
const stream = await this.client.getMediaHandler().getScreensharingStream(opts);
|
||||
|
||||
for (const track of stream.getTracks()) {
|
||||
@@ -653,7 +660,9 @@ export class GroupCall extends TypedEventEmitter<
|
||||
track.addEventListener("ended", onTrackEnded);
|
||||
}
|
||||
|
||||
logger.log("Screensharing permissions granted. Setting screensharing enabled on all calls");
|
||||
logger.log(
|
||||
`GroupCall ${this.groupCallId} setScreensharingEnabled() granted screensharing permissions. Setting screensharing enabled on all calls`,
|
||||
);
|
||||
|
||||
this.localDesktopCapturerSourceId = opts.desktopCapturerSourceId;
|
||||
this.localScreenshareFeed = new CallFeed({
|
||||
@@ -681,7 +690,10 @@ export class GroupCall extends TypedEventEmitter<
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (opts.throwOnFail) throw error;
|
||||
logger.error("Enabling screensharing error", error);
|
||||
logger.error(
|
||||
`GroupCall ${this.groupCallId} setScreensharingEnabled() enabling screensharing error`,
|
||||
error,
|
||||
);
|
||||
this.emit(
|
||||
GroupCallEvent.Error,
|
||||
new GroupCallError(
|
||||
@@ -725,13 +737,15 @@ export class GroupCall extends TypedEventEmitter<
|
||||
}
|
||||
|
||||
if (newCall.state !== CallState.Ringing) {
|
||||
logger.warn("Incoming call no longer in ringing state. Ignoring.");
|
||||
logger.warn(
|
||||
`GroupCall ${this.groupCallId} onIncomingCall() incoming call no longer in ringing state - ignoring`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newCall.groupCallId || newCall.groupCallId !== this.groupCallId) {
|
||||
logger.log(
|
||||
`Incoming call with groupCallId ${newCall.groupCallId} ignored because it doesn't match the current group call`,
|
||||
`GroupCall ${this.groupCallId} onIncomingCall() ignored because it doesn't match the current group call`,
|
||||
);
|
||||
newCall.reject();
|
||||
return;
|
||||
@@ -739,7 +753,7 @@ export class GroupCall extends TypedEventEmitter<
|
||||
|
||||
const opponentUserId = newCall.getOpponentMember()?.userId;
|
||||
if (opponentUserId === undefined) {
|
||||
logger.warn("Incoming call with no member. Ignoring.");
|
||||
logger.warn(`GroupCall ${this.groupCallId} onIncomingCall() incoming call with no member - ignoring`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -748,7 +762,9 @@ export class GroupCall extends TypedEventEmitter<
|
||||
|
||||
if (prevCall?.callId === newCall.callId) return;
|
||||
|
||||
logger.log(`GroupCall: incoming call from ${opponentUserId} with ID ${newCall.callId}`);
|
||||
logger.log(
|
||||
`GroupCall ${this.groupCallId} onIncomingCall() incoming call (userId=${opponentUserId}, callId=${newCall.callId})`,
|
||||
);
|
||||
|
||||
if (prevCall) this.disposeCall(prevCall, CallErrorCode.Replaced);
|
||||
|
||||
@@ -797,7 +813,9 @@ export class GroupCall extends TypedEventEmitter<
|
||||
callsChanged = true;
|
||||
|
||||
if (prevCall !== undefined) {
|
||||
logger.debug(`Replacing call ${prevCall.callId} to ${userId} ${deviceId}`);
|
||||
logger.debug(
|
||||
`GroupCall ${this.groupCallId} placeOutgoingCalls() replacing call (userId=${userId}, deviceId=${deviceId}, callId=${prevCall.callId})`,
|
||||
);
|
||||
this.disposeCall(prevCall, CallErrorCode.NewSession);
|
||||
}
|
||||
|
||||
@@ -809,13 +827,17 @@ export class GroupCall extends TypedEventEmitter<
|
||||
});
|
||||
|
||||
if (newCall === null) {
|
||||
logger.error(`Failed to create call with ${userId} ${deviceId}`);
|
||||
logger.error(
|
||||
`GroupCall ${this.groupCallId} placeOutgoingCalls() failed to create call (userId=${userId}, device=${deviceId})`,
|
||||
);
|
||||
callMap.delete(deviceId);
|
||||
} else {
|
||||
this.initCall(newCall);
|
||||
callMap.set(deviceId, newCall);
|
||||
|
||||
logger.debug(`Placing call to ${userId} ${deviceId} (session ${participant.sessionId})`);
|
||||
logger.debug(
|
||||
`GroupCall ${this.groupCallId} placeOutgoingCalls() placing call (userId=${userId}, deviceId=${deviceId}, sessionId=${participant.sessionId})`,
|
||||
);
|
||||
|
||||
newCall
|
||||
.placeCallWithCallFeeds(
|
||||
@@ -828,7 +850,10 @@ export class GroupCall extends TypedEventEmitter<
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.warn(`Failed to place call to ${userId}`, e);
|
||||
logger.warn(
|
||||
`GroupCall ${this.groupCallId} placeOutgoingCalls() failed to place call (userId=${userId})`,
|
||||
e,
|
||||
);
|
||||
|
||||
if (e instanceof CallError && e.code === GroupCallErrorCode.UnknownDevice) {
|
||||
this.emit(GroupCallEvent.Error, e);
|
||||
@@ -1192,7 +1217,9 @@ export class GroupCall extends TypedEventEmitter<
|
||||
if (!localMember) {
|
||||
// The client hasn't fetched enough of the room state to get our own member
|
||||
// event. This probably shouldn't happen, but sanity check & exit for now.
|
||||
logger.warn("Tried to update participants before local room member is available");
|
||||
logger.warn(
|
||||
`GroupCall ${this.groupCallId} updateParticipants() tried to update participants before local room member is available`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1355,11 +1382,14 @@ export class GroupCall extends TypedEventEmitter<
|
||||
|
||||
// Resend the state event every so often so it doesn't become stale
|
||||
this.resendMemberStateTimer = setInterval(async () => {
|
||||
logger.log("Resending call member state");
|
||||
logger.log(`GroupCall ${this.groupCallId} updateMemberState() resending call member state"`);
|
||||
try {
|
||||
await this.addDeviceToMemberState();
|
||||
} catch (e) {
|
||||
logger.error("Failed to resend call member state", e);
|
||||
logger.error(
|
||||
`GroupCall ${this.groupCallId} updateMemberState() failed to resend call member state`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}, (DEVICE_TIMEOUT * 3) / 4);
|
||||
} else {
|
||||
@@ -1412,13 +1442,23 @@ export class GroupCall extends TypedEventEmitter<
|
||||
) {
|
||||
// We either entered, left, or ended the call
|
||||
this.updateParticipants();
|
||||
this.updateMemberState().catch((e) => logger.error("Failed to update member state devices", e));
|
||||
this.updateMemberState().catch((e) =>
|
||||
logger.error(
|
||||
`GroupCall ${this.groupCallId} onStateChanged() failed to update member state devices"`,
|
||||
e,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private onLocalFeedsChanged = (): void => {
|
||||
if (this.state === GroupCallState.Entered) {
|
||||
this.updateMemberState().catch((e) => logger.error("Failed to update member state feeds", e));
|
||||
this.updateMemberState().catch((e) =>
|
||||
logger.error(
|
||||
`GroupCall ${this.groupCallId} onLocalFeedsChanged() failed to update member state feeds`,
|
||||
e,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export class GroupCallEventHandler {
|
||||
// we create a group call for the room so we can be fairly sure that
|
||||
// the group call we create is really the latest one.
|
||||
if (this.client.getSyncState() !== SyncState.Syncing) {
|
||||
logger.debug("Waiting for client to start syncing...");
|
||||
logger.debug("GroupCallEventHandler start() waiting for client to start syncing");
|
||||
await new Promise<void>((resolve) => {
|
||||
const onSync = (): void => {
|
||||
if (this.client.getSyncState() === SyncState.Syncing) {
|
||||
@@ -123,15 +123,16 @@ export class GroupCallEventHandler {
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Choosing group call ${callEvent.getStateKey()} with TS ` +
|
||||
`${callEvent.getTs()} for room ${room.roomId} from ${callEvents.length} possible calls.`,
|
||||
`GroupCallEventHandler createGroupCallForRoom() choosing group call from possible calls (stateKey=${callEvent.getStateKey()}, ts=${callEvent.getTs()}, roomId=${
|
||||
room.roomId
|
||||
}, numOfPossibleCalls=${callEvents.length})`,
|
||||
);
|
||||
|
||||
this.createGroupCallFromRoomStateEvent(callEvent);
|
||||
break;
|
||||
}
|
||||
|
||||
logger.info("Group call event handler processed room", room.roomId);
|
||||
logger.info(`GroupCallEventHandler createGroupCallForRoom() processed room (roomId=${room.roomId})`);
|
||||
this.getRoomDeferred(room.roomId).resolve!();
|
||||
}
|
||||
|
||||
@@ -142,7 +143,9 @@ export class GroupCallEventHandler {
|
||||
const room = this.client.getRoom(roomId);
|
||||
|
||||
if (!room) {
|
||||
logger.warn(`Couldn't find room ${roomId} for GroupCall`);
|
||||
logger.warn(
|
||||
`GroupCallEventHandler createGroupCallFromRoomStateEvent() couldn't find room for call (roomId=${roomId})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -151,14 +154,16 @@ export class GroupCallEventHandler {
|
||||
const callType = content["m.type"];
|
||||
|
||||
if (!Object.values(GroupCallType).includes(callType)) {
|
||||
logger.warn(`Received invalid group call type ${callType} for room ${roomId}.`);
|
||||
logger.warn(
|
||||
`GroupCallEventHandler createGroupCallFromRoomStateEvent() received invalid call type (type=${callType}, roomId=${roomId})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const callIntent = content["m.intent"];
|
||||
|
||||
if (!Object.values(GroupCallIntent).includes(callIntent)) {
|
||||
logger.warn(`Received invalid group call intent ${callType} for room ${roomId}.`);
|
||||
logger.warn(`Received invalid group call intent (type=${callType}, roomId=${roomId})`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -210,13 +215,13 @@ export class GroupCallEventHandler {
|
||||
} else if (content["m.type"] !== currentGroupCall.type) {
|
||||
// TODO: Handle the callType changing when the room state changes
|
||||
logger.warn(
|
||||
`The group call type changed for room: ${state.roomId}. Changing the group call type is currently unsupported.`,
|
||||
`GroupCallEventHandler onRoomStateChanged() currently does not support changing type (roomId=${state.roomId})`,
|
||||
);
|
||||
}
|
||||
} else if (currentGroupCall && currentGroupCall.groupCallId !== groupCallId) {
|
||||
// TODO: Handle new group calls and multiple group calls
|
||||
logger.warn(
|
||||
`Multiple group calls detected for room: ${state.roomId}. Multiple group calls are currently unsupported.`,
|
||||
`GroupCallEventHandler onRoomStateChanged() currently does not support multiple calls (roomId=${state.roomId})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+30
-20
@@ -75,7 +75,7 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
* undefined treated as unset
|
||||
*/
|
||||
public async setAudioInput(deviceId: string): Promise<void> {
|
||||
logger.info("Setting audio input to", deviceId);
|
||||
logger.info(`MediaHandler setAudioInput() running (deviceId=${deviceId})`);
|
||||
|
||||
if (this.audioInput === deviceId) return;
|
||||
|
||||
@@ -88,7 +88,7 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
* @param opts - audio options to set
|
||||
*/
|
||||
public async setAudioSettings(opts: AudioSettings): Promise<void> {
|
||||
logger.info("Setting audio settings to", opts);
|
||||
logger.info(`MediaHandler setAudioSettings() running (opts=${JSON.stringify(opts)})`);
|
||||
|
||||
this.audioSettings = Object.assign({}, opts) as AudioSettings;
|
||||
await this.updateLocalUsermediaStreams();
|
||||
@@ -100,7 +100,7 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
* undefined treated as unset
|
||||
*/
|
||||
public async setVideoInput(deviceId: string): Promise<void> {
|
||||
logger.info("Setting video input to", deviceId);
|
||||
logger.info(`MediaHandler setVideoInput() running (deviceId=${deviceId})`);
|
||||
|
||||
if (this.videoInput === deviceId) return;
|
||||
|
||||
@@ -115,7 +115,7 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
* undefined treated as unset
|
||||
*/
|
||||
public async setMediaInputs(audioInput: string, videoInput: string): Promise<void> {
|
||||
logger.log(`mediaHandler setMediaInputs audioInput: ${audioInput} videoInput: ${videoInput}`);
|
||||
logger.log(`MediaHandler setMediaInputs() running (audioInput: ${audioInput} videoInput: ${videoInput})`);
|
||||
this.audioInput = audioInput;
|
||||
this.videoInput = videoInput;
|
||||
await this.updateLocalUsermediaStreams();
|
||||
@@ -136,7 +136,7 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
}
|
||||
|
||||
for (const stream of this.userMediaStreams) {
|
||||
logger.log(`mediaHandler stopping all tracks for stream ${stream.id}`);
|
||||
logger.log(`MediaHandler updateLocalUsermediaStreams() stopping all tracks (streamId=${stream.id})`);
|
||||
for (const track of stream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
@@ -152,7 +152,9 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
|
||||
const { audio, video } = callMediaStreamParams.get(call.callId)!;
|
||||
|
||||
logger.log(`mediaHandler updateLocalUsermediaStreams getUserMediaStream call ${call.callId}`);
|
||||
logger.log(
|
||||
`MediaHandler updateLocalUsermediaStreams() calling getUserMediaStream() (callId=${call.callId})`,
|
||||
);
|
||||
const stream = await this.getUserMediaStream(audio, video);
|
||||
|
||||
if (call.callHasEnded()) {
|
||||
@@ -168,7 +170,7 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
}
|
||||
|
||||
logger.log(
|
||||
`mediaHandler updateLocalUsermediaStreams getUserMediaStream groupCall ${groupCall.groupCallId}`,
|
||||
`MediaHandler updateLocalUsermediaStreams() calling getUserMediaStream() (groupCallId=${groupCall.groupCallId})`,
|
||||
);
|
||||
const stream = await this.getUserMediaStream(true, groupCall.type === GroupCallType.Video);
|
||||
|
||||
@@ -252,8 +254,11 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
const constraints = this.getUserMediaContraints(shouldRequestAudio, shouldRequestVideo);
|
||||
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
logger.log(
|
||||
`mediaHandler getUserMediaStream streamId ${stream.id} shouldRequestAudio ${shouldRequestAudio} shouldRequestVideo ${shouldRequestVideo}`,
|
||||
constraints,
|
||||
`MediaHandler getUserMediaStreamInternal() calling getUserMediaStream (streamId=${
|
||||
stream.id
|
||||
}, shouldRequestAudio=${shouldRequestAudio}, shouldRequestVideo=${shouldRequestVideo}, constraints=${JSON.stringify(
|
||||
constraints,
|
||||
)})`,
|
||||
);
|
||||
|
||||
for (const track of stream.getTracks()) {
|
||||
@@ -272,7 +277,7 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
} else {
|
||||
stream = this.localUserMediaStream!.clone();
|
||||
logger.log(
|
||||
`mediaHandler clone userMediaStream ${this.localUserMediaStream?.id} new stream ${stream.id} shouldRequestAudio ${shouldRequestAudio} shouldRequestVideo ${shouldRequestVideo}`,
|
||||
`MediaHandler getUserMediaStreamInternal() cloning (oldStreamId=${this.localUserMediaStream?.id} newStreamId=${stream.id} shouldRequestAudio=${shouldRequestAudio} shouldRequestVideo=${shouldRequestVideo})`,
|
||||
);
|
||||
|
||||
if (!shouldRequestAudio) {
|
||||
@@ -301,7 +306,7 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
* Stops all tracks on the provided usermedia stream
|
||||
*/
|
||||
public stopUserMediaStream(mediaStream: MediaStream): void {
|
||||
logger.log(`mediaHandler stopUserMediaStream stopping stream ${mediaStream.id}`);
|
||||
logger.log(`MediaHandler stopUserMediaStream() stopping (streamId=${mediaStream.id})`);
|
||||
for (const track of mediaStream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
@@ -309,7 +314,10 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
const index = this.userMediaStreams.indexOf(mediaStream);
|
||||
|
||||
if (index !== -1) {
|
||||
logger.debug("Splicing usermedia stream out stream array", mediaStream.id);
|
||||
logger.debug(
|
||||
`MediaHandler stopUserMediaStream() splicing usermedia stream out stream array (streamId=${mediaStream.id})`,
|
||||
mediaStream.id,
|
||||
);
|
||||
this.userMediaStreams.splice(index, 1);
|
||||
}
|
||||
|
||||
@@ -333,16 +341,20 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
|
||||
if (opts.desktopCapturerSourceId) {
|
||||
// We are using Electron
|
||||
logger.debug("Getting screensharing stream using getUserMedia()", opts);
|
||||
logger.debug(
|
||||
`MediaHandler getScreensharingStream() calling getUserMedia() (opts=${JSON.stringify(opts)})`,
|
||||
);
|
||||
stream = await navigator.mediaDevices.getUserMedia(screenshareConstraints);
|
||||
} else {
|
||||
// We are not using Electron
|
||||
logger.debug("Getting screensharing stream using getDisplayMedia()", opts);
|
||||
logger.debug(
|
||||
`MediaHandler getScreensharingStream() calling getDisplayMedia() (opts=${JSON.stringify(opts)})`,
|
||||
);
|
||||
stream = await navigator.mediaDevices.getDisplayMedia(screenshareConstraints);
|
||||
}
|
||||
} else {
|
||||
const matchingStream = this.screensharingStreams[this.screensharingStreams.length - 1];
|
||||
logger.log("Cloning screensharing stream", matchingStream.id);
|
||||
logger.log(`MediaHandler getScreensharingStream() cloning (streamId=${matchingStream.id})`);
|
||||
stream = matchingStream.clone();
|
||||
}
|
||||
|
||||
@@ -359,7 +371,7 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
* Stops all tracks on the provided screensharing stream
|
||||
*/
|
||||
public stopScreensharingStream(mediaStream: MediaStream): void {
|
||||
logger.debug("Stopping screensharing stream", mediaStream.id);
|
||||
logger.debug(`MediaHandler stopScreensharingStream() stopping stream (streamId=${mediaStream.id})`);
|
||||
for (const track of mediaStream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
@@ -367,7 +379,7 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
const index = this.screensharingStreams.indexOf(mediaStream);
|
||||
|
||||
if (index !== -1) {
|
||||
logger.debug("Splicing screensharing stream out stream array", mediaStream.id);
|
||||
logger.debug(`MediaHandler stopScreensharingStream() splicing stream out (streamId=${mediaStream.id})`);
|
||||
this.screensharingStreams.splice(index, 1);
|
||||
}
|
||||
|
||||
@@ -379,7 +391,7 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
*/
|
||||
public stopAllStreams(): void {
|
||||
for (const stream of this.userMediaStreams) {
|
||||
logger.log(`mediaHandler stopAllStreams stopping stream ${stream.id}`);
|
||||
logger.log(`MediaHandler stopAllStreams() stopping (streamId=${stream.id})`);
|
||||
for (const track of stream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
@@ -428,7 +440,6 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
private getScreenshareContraints(opts: IScreensharingOpts): DesktopCapturerConstraints {
|
||||
const { desktopCapturerSourceId, audio } = opts;
|
||||
if (desktopCapturerSourceId) {
|
||||
logger.debug("Using desktop capturer source", desktopCapturerSourceId);
|
||||
return {
|
||||
audio: audio ?? false,
|
||||
video: {
|
||||
@@ -439,7 +450,6 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
},
|
||||
};
|
||||
} else {
|
||||
logger.debug("Not using desktop capturer source");
|
||||
return {
|
||||
audio: audio ?? false,
|
||||
video: true,
|
||||
|
||||
Reference in New Issue
Block a user