Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9b502fb0e | |||
| 330fbaccfc | |||
| 937f370655 | |||
| a8ad3ed26d | |||
| decac58a18 | |||
| 1a91ba59a6 | |||
| 89df43a975 | |||
| ad98706db4 | |||
| 195d1730bd | |||
| db4bd907f8 | |||
| cdd7dbbb2b | |||
| 108f157324 | |||
| 8da39ec8f4 | |||
| 5e17626fe0 | |||
| abc9c9dcb0 | |||
| f346fcb056 | |||
| c67325ba07 | |||
| a063ae8ce7 | |||
| f9e5535492 | |||
| 015d9c5c4f | |||
| b6d40078d9 | |||
| b8a8f4850a | |||
| 1cc23d789c | |||
| 5cf0bb46a4 | |||
| 16672b3d0c | |||
| 31459a5d63 |
+1
-1
@@ -23,4 +23,4 @@ indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
indent_size = 4
|
||||
|
||||
+18
-1
@@ -1,6 +1,6 @@
|
||||
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"],
|
||||
},
|
||||
@@ -63,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
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
Changes in [23.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.4.0-rc.1) (2023-02-21)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Polls: expose end event id on poll model ([\#3160](https://github.com/matrix-org/matrix-js-sdk/pull/3160)). Contributed by @kerryarchibald.
|
||||
* Polls: count undecryptable poll relations ([\#3163](https://github.com/matrix-org/matrix-js-sdk/pull/3163)). Contributed by @kerryarchibald.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* 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)
|
||||
==================================================================================================
|
||||
|
||||
|
||||
+6
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "23.3.0",
|
||||
"version": "23.4.0-rc.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
@@ -84,6 +84,7 @@
|
||||
"@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",
|
||||
@@ -96,15 +97,17 @@
|
||||
"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.32.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.10.0",
|
||||
"eslint-plugin-matrix-org": "^1.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.17",
|
||||
"eslint-plugin-unicorn": "^45.0.0",
|
||||
"exorcist": "^2.0.0",
|
||||
|
||||
+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`);
|
||||
|
||||
+516
-460
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 () {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -450,6 +450,10 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
]
|
||||
>();
|
||||
|
||||
public isInitialSyncComplete(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public getMediaHandler(): MediaHandler {
|
||||
return this.mediaHandler.typed();
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -211,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];
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
@@ -399,7 +399,7 @@ describe("EventTimelineSet", () => {
|
||||
|
||||
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 () => {
|
||||
|
||||
+105
-99
@@ -893,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!
|
||||
@@ -919,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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -933,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({
|
||||
@@ -944,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",
|
||||
@@ -972,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",
|
||||
@@ -998,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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1035,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);
|
||||
@@ -1052,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 = [];
|
||||
@@ -1089,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({
|
||||
@@ -1119,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",
|
||||
@@ -1135,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);
|
||||
@@ -1147,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({
|
||||
@@ -1175,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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1214,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 () {
|
||||
@@ -1334,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`,
|
||||
@@ -1464,7 +1495,7 @@ describe("MatrixClient", function () {
|
||||
{ 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 }) => {
|
||||
])("enabled thread support for the SDK instance", async ({ startOpts, hasThreadSupport }) => {
|
||||
await client.startClient(startOpts);
|
||||
expect(client.supportsThreads()).toBe(hasThreadSupport);
|
||||
});
|
||||
@@ -2410,31 +2441,6 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRoomUpgradeHistory", () => {
|
||||
@@ -2619,7 +2625,7 @@ describe("MatrixClient", function () {
|
||||
expect(history.map((room) => room.roomId)).toEqual([room1.roomId]);
|
||||
});
|
||||
|
||||
it("Without verify links, includes predecessors that don't point forwards", () => {
|
||||
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);
|
||||
|
||||
@@ -20,6 +20,7 @@ import { M_POLL_END, M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE } from "../../../src
|
||||
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();
|
||||
|
||||
@@ -27,6 +28,7 @@ describe("Poll", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(true),
|
||||
relations: jest.fn(),
|
||||
});
|
||||
const roomId = "!room:server";
|
||||
@@ -74,13 +76,14 @@ describe("Poll", () => {
|
||||
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)).toThrowError("Invalid poll start event.");
|
||||
expect(() => new Poll(pollStartEvent, mockClient, room)).toThrow("Invalid poll start event.");
|
||||
});
|
||||
|
||||
it("throws when poll start has no event id", () => {
|
||||
@@ -88,7 +91,7 @@ describe("Poll", () => {
|
||||
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
||||
room_id: roomId,
|
||||
});
|
||||
expect(() => new Poll(pollStartEvent, mockClient, room)).toThrowError("Invalid poll start event.");
|
||||
expect(() => new Poll(pollStartEvent, mockClient, room)).toThrow("Invalid poll start event.");
|
||||
});
|
||||
|
||||
describe("fetching responses", () => {
|
||||
@@ -130,7 +133,7 @@ describe("Poll", () => {
|
||||
});
|
||||
|
||||
it("filters relations for relevent response events", async () => {
|
||||
const replyEvent = new MatrixEvent({ type: "m.room.message" });
|
||||
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
|
||||
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
|
||||
const unstableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
||||
|
||||
@@ -172,6 +175,8 @@ describe("Poll", () => {
|
||||
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" }],
|
||||
@@ -184,6 +189,47 @@ describe("Poll", () => {
|
||||
});
|
||||
});
|
||||
|
||||
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" });
|
||||
@@ -204,6 +250,7 @@ describe("Poll", () => {
|
||||
|
||||
expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@bob@server.org");
|
||||
expect(poll.isEnded).toBe(true);
|
||||
expect(poll.endEventId).toBe(stablePollEndEvent.getId()!);
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
});
|
||||
|
||||
@@ -292,33 +339,6 @@ describe("Poll", () => {
|
||||
expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@charlie:server.org");
|
||||
});
|
||||
|
||||
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("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" },
|
||||
@@ -356,25 +376,6 @@ describe("Poll", () => {
|
||||
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
|
||||
});
|
||||
|
||||
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("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" },
|
||||
@@ -402,43 +403,6 @@ describe("Poll", () => {
|
||||
expect(poll.isEnded).toBeTruthy();
|
||||
});
|
||||
|
||||
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("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);
|
||||
|
||||
@@ -82,6 +82,7 @@ describe("Thread", () => {
|
||||
beforeEach(() => {
|
||||
client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||
getRoom: jest.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
@@ -193,6 +194,7 @@ describe("Thread", () => {
|
||||
beforeEach(() => {
|
||||
client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||
getRoom: jest.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
|
||||
@@ -53,6 +53,7 @@ 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),
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -788,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 () {
|
||||
@@ -3251,7 +3255,7 @@ describe("Room", function () {
|
||||
return event;
|
||||
};
|
||||
|
||||
it("adds poll models to room state for a poll start event ", async () => {
|
||||
it("adds poll models to room state for a poll start event", async () => {
|
||||
const pollStartEvent = makePollStart("1");
|
||||
const events = [pollStartEvent];
|
||||
|
||||
@@ -3307,6 +3311,7 @@ describe("Room", function () {
|
||||
beforeEach(() => {
|
||||
client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
});
|
||||
@@ -3387,7 +3392,7 @@ describe("Room", function () {
|
||||
const useMsc3946 = true;
|
||||
expect(room.findPredecessor(useMsc3946)).toEqual({
|
||||
roomId: "otherreplacedroomid",
|
||||
eventId: null, // m.predecessor did not include an event_id
|
||||
eventId: undefined, // m.predecessor did not include an event_id
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+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 () {
|
||||
|
||||
@@ -145,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);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1579,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 () => {
|
||||
|
||||
+24
-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;
|
||||
@@ -70,3 +71,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;
|
||||
}
|
||||
|
||||
+83
-49
@@ -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,
|
||||
@@ -1307,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;
|
||||
@@ -4135,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());
|
||||
@@ -6817,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.
|
||||
@@ -9483,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
+3
-3
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
+35
-1
@@ -28,6 +28,7 @@ export enum PollEvent {
|
||||
Update = "Poll.update",
|
||||
Responses = "Poll.Responses",
|
||||
Destroy = "Poll.Destroy",
|
||||
UndecryptableRelations = "Poll.UndecryptableRelations",
|
||||
}
|
||||
|
||||
export type PollEventHandlerMap = {
|
||||
@@ -35,6 +36,7 @@ export type PollEventHandlerMap = {
|
||||
[PollEvent.Destroy]: (pollIdentifier: string) => void;
|
||||
[PollEvent.End]: () => void;
|
||||
[PollEvent.Responses]: (responses: Relations) => void;
|
||||
[PollEvent.UndecryptableRelations]: (count: number) => void;
|
||||
};
|
||||
|
||||
const filterResponseRelations = (
|
||||
@@ -45,7 +47,6 @@ const filterResponseRelations = (
|
||||
} => {
|
||||
const responseEvents = relationEvents.filter((event) => {
|
||||
if (event.isDecryptionFailure()) {
|
||||
// @TODO(kerrya) PSG-1023 track and return these
|
||||
return;
|
||||
}
|
||||
return (
|
||||
@@ -66,6 +67,11 @@ export class Poll extends TypedEventEmitter<Exclude<PollEvent, PollEvent.New>, P
|
||||
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();
|
||||
@@ -80,6 +86,10 @@ export class Poll extends TypedEventEmitter<Exclude<PollEvent, PollEvent.New>, P
|
||||
return this.rootEvent.getId()!;
|
||||
}
|
||||
|
||||
public get endEventId(): string | undefined {
|
||||
return this.endEvent?.getId();
|
||||
}
|
||||
|
||||
public get isEnded(): boolean {
|
||||
return !!this.endEvent;
|
||||
}
|
||||
@@ -88,6 +98,10 @@ export class Poll extends TypedEventEmitter<Exclude<PollEvent, PollEvent.New>, P
|
||||
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
|
||||
@@ -124,10 +138,13 @@ export class Poll extends TypedEventEmitter<Exclude<PollEvent, PollEvent.New>, P
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -149,11 +166,14 @@ export class Poll extends TypedEventEmitter<Exclude<PollEvent, PollEvent.New>, P
|
||||
},
|
||||
);
|
||||
|
||||
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();
|
||||
@@ -170,6 +190,7 @@ export class Poll extends TypedEventEmitter<Exclude<PollEvent, PollEvent.New>, P
|
||||
|
||||
this.relationsNextBatch = allRelations.nextBatch ?? undefined;
|
||||
this.responses = responses;
|
||||
this.countUndecryptableEvents(allRelations.events);
|
||||
|
||||
// while there are more pages of relations
|
||||
// fetch them
|
||||
@@ -206,6 +227,19 @@ export class Poll extends TypedEventEmitter<Exclude<PollEvent, PollEvent.New>, P
|
||||
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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
+17
-10
@@ -973,22 +973,24 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
* 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 is null since those
|
||||
* events do not include an event_id property.
|
||||
* 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 } | null {
|
||||
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 roomId = predecessorEvent.getContent()["predecessor_room_id"];
|
||||
let eventId = predecessorEvent.getContent()["last_known_event_id"];
|
||||
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 = null;
|
||||
eventId = undefined;
|
||||
}
|
||||
if (typeof roomId === "string") {
|
||||
return { roomId, eventId };
|
||||
@@ -998,13 +1000,18 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
|
||||
const createEvent = this.getStateEvents(EventType.RoomCreate, "");
|
||||
if (createEvent) {
|
||||
const predecessor = createEvent.getContent()["predecessor"];
|
||||
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 = null;
|
||||
eventId = undefined;
|
||||
}
|
||||
return { roomId, eventId };
|
||||
}
|
||||
|
||||
+46
-6
@@ -1412,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"];
|
||||
@@ -2762,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
|
||||
@@ -3033,12 +3052,10 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* 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 is null since those
|
||||
* events do not include an event_id property.
|
||||
* 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 } | null {
|
||||
public findPredecessor(msc3946ProcessDynamicPredecessor = false): { roomId: string; eventId?: string } | null {
|
||||
const currentState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
if (!currentState) {
|
||||
return null;
|
||||
@@ -3374,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";
|
||||
@@ -638,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(
|
||||
|
||||
+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)) {
|
||||
|
||||
+27
-14
@@ -1299,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,
|
||||
@@ -1327,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;
|
||||
|
||||
+178
-133
@@ -644,13 +644,15 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
|
||||
if (!purpose) {
|
||||
logger.warn(
|
||||
`Call ${this.callId} Ignoring stream with id ${stream.id} because we didn't get any metadata about it`,
|
||||
`Call ${this.callId} pushRemoteFeed() ignoring stream because we didn't get any metadata about it (streamId=${stream.id})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.getFeedByStreamId(stream.id)) {
|
||||
logger.warn(`Ignoring stream with id ${stream.id} because we already have a feed for it`);
|
||||
logger.warn(
|
||||
`Call ${this.callId} pushRemoteFeed() ignoring stream because we already have a feed for it (streamId=${stream.id})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -671,8 +673,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
this.emit(CallEvent.FeedsChanged, this.feeds);
|
||||
|
||||
logger.info(
|
||||
`Call ${this.callId} pushed remote stream (id="${stream.id}", ` +
|
||||
`active="${stream.active}", purpose=${purpose})`,
|
||||
`Call ${this.callId} pushRemoteFeed() pushed stream (streamId=${stream.id}, active=${stream.active}, purpose=${purpose})`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -691,13 +692,15 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
// If we already have a stream, check this stream has the same id
|
||||
if (oldRemoteStream && stream.id !== oldRemoteStream.id) {
|
||||
logger.warn(
|
||||
`Call ${this.callId} Ignoring new stream ID ${stream.id}: we already have stream ID ${oldRemoteStream.id}`,
|
||||
`Call ${this.callId} pushRemoteFeedWithoutMetadata() ignoring new stream because we already have stream (streamId=${stream.id})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.getFeedByStreamId(stream.id)) {
|
||||
logger.warn(`Ignoring stream with id ${stream.id} because we already have a feed for it`);
|
||||
logger.warn(
|
||||
`Call ${this.callId} pushRemoteFeedWithoutMetadata() ignoring stream because we already have a feed for it (streamId=${stream.id})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -717,7 +720,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
|
||||
this.emit(CallEvent.FeedsChanged, this.feeds);
|
||||
|
||||
logger.info(`Call ${this.callId} pushed remote stream (id="${stream.id}", active="${stream.active}")`);
|
||||
logger.info(
|
||||
`Call ${this.callId} pushRemoteFeedWithoutMetadata() pushed stream (streamId=${stream.id}, active=${stream.active})`,
|
||||
);
|
||||
}
|
||||
|
||||
private pushNewLocalFeed(stream: MediaStream, purpose: SDPStreamMetadataPurpose, addToPeerConnection = true): void {
|
||||
@@ -730,7 +735,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
setTracksEnabled(stream.getVideoTracks(), true);
|
||||
|
||||
if (this.getFeedByStreamId(stream.id)) {
|
||||
logger.warn(`Ignoring stream with id ${stream.id} because we already have a feed for it`);
|
||||
logger.warn(
|
||||
`Call ${this.callId} pushNewLocalFeed() ignoring stream because we already have a feed for it (streamId=${stream.id})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -756,7 +763,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
*/
|
||||
public pushLocalFeed(callFeed: CallFeed, addToPeerConnection = true): void {
|
||||
if (this.feeds.some((feed) => callFeed.stream.id === feed.stream.id)) {
|
||||
logger.info(`Ignoring duplicate local stream ${callFeed.stream.id} in call ${this.callId}`);
|
||||
logger.info(
|
||||
`Call ${this.callId} pushLocalFeed() ignoring duplicate local stream (streamId=${callFeed.stream.id})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -765,14 +774,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
if (addToPeerConnection) {
|
||||
for (const track of callFeed.stream.getTracks()) {
|
||||
logger.info(
|
||||
`Call ${this.callId} ` +
|
||||
`Adding track (` +
|
||||
`id="${track.id}", ` +
|
||||
`kind="${track.kind}", ` +
|
||||
`streamId="${callFeed.stream.id}", ` +
|
||||
`streamPurpose="${callFeed.purpose}", ` +
|
||||
`enabled=${track.enabled}` +
|
||||
`) to peer connection`,
|
||||
`Call ${this.callId} pushLocalFeed() adding track to peer connection (id=${track.id}, kind=${track.kind}, streamId=${callFeed.stream.id}, streamPurpose=${callFeed.purpose}, enabled=${track.enabled})`,
|
||||
);
|
||||
|
||||
const tKey = getTransceiverKey(callFeed.purpose, track.kind);
|
||||
@@ -804,18 +806,16 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
if (newTransceiver) {
|
||||
this.transceivers.set(tKey, newTransceiver);
|
||||
} else {
|
||||
logger.warn("Didn't find a matching transceiver after adding track!");
|
||||
logger.warn(
|
||||
`Call ${this.callId} pushLocalFeed() didn't find a matching transceiver after adding track!`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Call ${this.callId} ` +
|
||||
`Pushed local stream ` +
|
||||
`(id="${callFeed.stream.id}", ` +
|
||||
`active="${callFeed.stream.active}", ` +
|
||||
`purpose="${callFeed.purpose}")`,
|
||||
`Call ${this.callId} pushLocalFeed() pushed stream (id=${callFeed.stream.id}, active=${callFeed.stream.active}, purpose=${callFeed.purpose})`,
|
||||
);
|
||||
|
||||
this.emit(CallEvent.FeedsChanged, this.feeds);
|
||||
@@ -861,7 +861,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
private deleteFeedByStream(stream: MediaStream): void {
|
||||
const feed = this.getFeedByStreamId(stream.id);
|
||||
if (!feed) {
|
||||
logger.warn(`Call ${this.callId} Didn't find the feed with stream id ${stream.id} to delete`);
|
||||
logger.warn(
|
||||
`Call ${this.callId} deleteFeedByStream() didn't find the feed to delete (streamId=${stream.id})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.deleteFeed(feed);
|
||||
@@ -908,7 +910,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
// poll and keep the credentials valid so this should be instant.
|
||||
const haveTurnCreds = await this.client.checkTurnServers();
|
||||
if (!haveTurnCreds) {
|
||||
logger.warn(`Call ${this.callId} Failed to get TURN credentials! Proceeding with call anyway...`);
|
||||
logger.warn(
|
||||
`Call ${this.callId} initWithInvite() failed to get TURN credentials! Proceeding with call anyway...`,
|
||||
);
|
||||
}
|
||||
|
||||
const sdpStreamMetadata = invite[SDPStreamMetadataKey];
|
||||
@@ -916,7 +920,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
|
||||
} else {
|
||||
logger.debug(
|
||||
`Call ${this.callId} did not get any SDPStreamMetadata! Can not send/receive multiple streams`,
|
||||
`Call ${this.callId} initWithInvite() did not get any SDPStreamMetadata! Can not send/receive multiple streams`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -930,7 +934,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
await this.peerConn.setRemoteDescription(invite.offer);
|
||||
await this.addBufferedIceCandidates();
|
||||
} catch (e) {
|
||||
logger.debug(`Call ${this.callId} failed to set remote description`, e);
|
||||
logger.debug(`Call ${this.callId} initWithInvite() failed to set remote description`, e);
|
||||
this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
|
||||
return;
|
||||
}
|
||||
@@ -941,7 +945,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
// add streams until media started arriving on them. Testing latest firefox
|
||||
// (81 at time of writing), this is no longer a problem, so let's do it the correct way.
|
||||
if (!remoteStream || remoteStream.getTracks().length === 0) {
|
||||
logger.error(`Call ${this.callId} no remote stream or no tracks after setting remote description!`);
|
||||
logger.error(
|
||||
`Call ${this.callId} initWithInvite() no remote stream or no tracks after setting remote description!`,
|
||||
);
|
||||
this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
|
||||
return;
|
||||
}
|
||||
@@ -952,7 +958,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
// Time out the call if it's ringing for too long
|
||||
const ringingTimer = setTimeout(() => {
|
||||
if (this.state == CallState.Ringing) {
|
||||
logger.debug(`Call ${this.callId} invite has expired. Hanging up.`);
|
||||
logger.debug(`Call ${this.callId} initWithInvite() invite has expired. Hanging up.`);
|
||||
this.hangupParty = CallParty.Remote; // effectively
|
||||
this.state = CallState.Ended;
|
||||
this.stopAllMedia();
|
||||
@@ -992,7 +998,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
if (wantedValue && !valueOfTheOtherSide) {
|
||||
// TODO: Figure out how to do this
|
||||
logger.warn(
|
||||
`Call ${this.callId} Unable to answer with ${type} because the other side isn't sending it either.`,
|
||||
`Call ${this.callId} shouldAnswerWithMediaType() unable to answer with ${type} because the other side isn't sending it either.`,
|
||||
);
|
||||
return false;
|
||||
} else if (
|
||||
@@ -1001,7 +1007,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
!this.opponentSupportsSDPStreamMetadata()
|
||||
) {
|
||||
logger.warn(
|
||||
`Call ${this.callId} Unable to answer with ${type}=${wantedValue} because the other side doesn't support it. Answering with ${type}=${valueOfTheOtherSide}.`,
|
||||
`Call ${this.callId} shouldAnswerWithMediaType() unable to answer with ${type}=${wantedValue} because the other side doesn't support it. Answering with ${type}=${valueOfTheOtherSide}.`,
|
||||
);
|
||||
return valueOfTheOtherSide!;
|
||||
}
|
||||
@@ -1048,7 +1054,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
} catch (e) {
|
||||
if (answerWithVideo) {
|
||||
// Try to answer without video
|
||||
logger.warn(`Call ${this.callId} Failed to getUserMedia(), trying to getUserMedia() without video`);
|
||||
logger.warn(
|
||||
`Call ${this.callId} answer() failed to getUserMedia(), trying to getUserMedia() without video`,
|
||||
);
|
||||
this.state = prevState;
|
||||
this.waitForLocalAVStream = false;
|
||||
await this.answer(answerWithAudio, false);
|
||||
@@ -1074,15 +1082,19 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
* @param newCall - The new call.
|
||||
*/
|
||||
public replacedBy(newCall: MatrixCall): void {
|
||||
logger.debug(`Call ${this.callId} replaced by ${newCall.callId}`);
|
||||
logger.debug(`Call ${this.callId} replacedBy() running (newCallId=${newCall.callId})`);
|
||||
if (this.state === CallState.WaitLocalMedia) {
|
||||
logger.debug(`Call ${this.callId} telling new call ${newCall.callId} to wait for local media`);
|
||||
logger.debug(
|
||||
`Call ${this.callId} replacedBy() telling new call to wait for local media (newCallId=${newCall.callId})`,
|
||||
);
|
||||
newCall.waitForLocalAVStream = true;
|
||||
} else if ([CallState.CreateOffer, CallState.InviteSent].includes(this.state)) {
|
||||
if (newCall.direction === CallDirection.Outbound) {
|
||||
newCall.queueGotCallFeedsForAnswer([]);
|
||||
} else {
|
||||
logger.debug(`Call ${this.callId} handing local stream to new call ${newCall.callId}`);
|
||||
logger.debug(
|
||||
`Call ${this.callId} replacedBy() handing local stream to new call(newCallId=${newCall.callId})`,
|
||||
);
|
||||
newCall.queueGotCallFeedsForAnswer(this.getLocalFeeds().map((feed) => feed.clone()));
|
||||
}
|
||||
}
|
||||
@@ -1099,7 +1111,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
public hangup(reason: CallErrorCode, suppressEvent: boolean): void {
|
||||
if (this.callHasEnded()) return;
|
||||
|
||||
logger.debug(`Ending call ${this.callId} with reason ${reason}`);
|
||||
logger.debug(`Call ${this.callId} hangup() ending call (reason=${reason})`);
|
||||
this.terminate(CallParty.Local, reason, !suppressEvent);
|
||||
// We don't want to send hangup here if we didn't even get to sending an invite
|
||||
if ([CallState.Fledgling, CallState.WaitLocalMedia].includes(this.state)) return;
|
||||
@@ -1123,7 +1135,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
|
||||
if (this.opponentVersion === 0) {
|
||||
logger.info(
|
||||
`Call ${this.callId} Opponent version is less than 1 (${this.opponentVersion}): sending hangup instead of reject`,
|
||||
`Call ${this.callId} reject() opponent version is less than 1: sending hangup instead of reject (opponentVersion=${this.opponentVersion})`,
|
||||
);
|
||||
this.hangup(CallErrorCode.UserHangup, true);
|
||||
return;
|
||||
@@ -1145,7 +1157,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
if (!this.opponentSupportsSDPStreamMetadata()) return;
|
||||
|
||||
try {
|
||||
logger.debug(`Upgrading call ${this.callId}: audio?=${audio} video?=${video}`);
|
||||
logger.debug(`Call ${this.callId} upgradeCall() upgrading call (audio=${audio}, video=${video})`);
|
||||
const getAudio = audio || this.hasLocalUserMediaAudioTrack;
|
||||
const getVideo = video || this.hasLocalUserMediaVideoTrack;
|
||||
|
||||
@@ -1154,7 +1166,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
const stream = await this.client.getMediaHandler().getUserMediaStream(getAudio, getVideo, false);
|
||||
await this.updateLocalUsermediaStream(stream, audio, video);
|
||||
} catch (error) {
|
||||
logger.error(`Call ${this.callId} Failed to upgrade the call`, error);
|
||||
logger.error(`Call ${this.callId} upgradeCall() failed to upgrade the call`, error);
|
||||
this.emit(
|
||||
CallEvent.Error,
|
||||
new CallError(CallErrorCode.NoUserMedia, "Failed to get camera access: ", <Error>error),
|
||||
@@ -1187,10 +1199,14 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
public async setScreensharingEnabled(enabled: boolean, opts?: IScreensharingOpts): Promise<boolean> {
|
||||
// Skip if there is nothing to do
|
||||
if (enabled && this.isScreensharing()) {
|
||||
logger.warn(`Call ${this.callId} There is already a screensharing stream - there is nothing to do!`);
|
||||
logger.warn(
|
||||
`Call ${this.callId} setScreensharingEnabled() there is already a screensharing stream - there is nothing to do!`,
|
||||
);
|
||||
return true;
|
||||
} else if (!enabled && !this.isScreensharing()) {
|
||||
logger.warn(`Call ${this.callId} There already isn't a screensharing stream - there is nothing to do!`);
|
||||
logger.warn(
|
||||
`Call ${this.callId} setScreensharingEnabled() there already isn't a screensharing stream - there is nothing to do!`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1199,7 +1215,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
return this.setScreensharingEnabledWithoutMetadataSupport(enabled, opts);
|
||||
}
|
||||
|
||||
logger.debug(`Call ${this.callId} set screensharing enabled? ${enabled}`);
|
||||
logger.debug(`Call ${this.callId} setScreensharingEnabled() running (enabled=${enabled})`);
|
||||
if (enabled) {
|
||||
try {
|
||||
const stream = await this.client.getMediaHandler().getScreensharingStream(opts);
|
||||
@@ -1207,7 +1223,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
this.pushNewLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare);
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.error(`Call ${this.callId} Failed to get screen-sharing stream:`, err);
|
||||
logger.error(`Call ${this.callId} setScreensharingEnabled() failed to get screen-sharing stream:`, err);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
@@ -1241,7 +1257,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
enabled: boolean,
|
||||
opts?: IScreensharingOpts,
|
||||
): Promise<boolean> {
|
||||
logger.debug(`Call ${this.callId} Set screensharing enabled? ${enabled} using replaceTrack()`);
|
||||
logger.debug(
|
||||
`Call ${this.callId} setScreensharingEnabledWithoutMetadataSupport() running (enabled=${enabled})`,
|
||||
);
|
||||
if (enabled) {
|
||||
try {
|
||||
const stream = await this.client.getMediaHandler().getScreensharingStream(opts);
|
||||
@@ -1259,7 +1277,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.error(`Call ${this.callId} Failed to get screen-sharing stream:`, err);
|
||||
logger.error(
|
||||
`Call ${this.callId} setScreensharingEnabledWithoutMetadataSupport() failed to get screen-sharing stream:`,
|
||||
err,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
@@ -1289,7 +1310,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
const audioEnabled = forceAudio || (!callFeed.isAudioMuted() && !this.remoteOnHold);
|
||||
const videoEnabled = forceVideo || (!callFeed.isVideoMuted() && !this.remoteOnHold);
|
||||
logger.log(
|
||||
`call ${this.callId} updateLocalUsermediaStream stream ${stream.id} audioEnabled ${audioEnabled} videoEnabled ${videoEnabled}`,
|
||||
`Call ${this.callId} updateLocalUsermediaStream() running (streamId=${stream.id}, audio=${audioEnabled}, video=${videoEnabled})`,
|
||||
);
|
||||
setTracksEnabled(stream.getAudioTracks(), audioEnabled);
|
||||
setTracksEnabled(stream.getVideoTracks(), videoEnabled);
|
||||
@@ -1316,13 +1337,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
if (oldSender) {
|
||||
try {
|
||||
logger.info(
|
||||
`Call ${this.callId} ` +
|
||||
`Replacing track (` +
|
||||
`id="${track.id}", ` +
|
||||
`kind="${track.kind}", ` +
|
||||
`streamId="${stream.id}", ` +
|
||||
`streamPurpose="${callFeed.purpose}"` +
|
||||
`) to peer connection`,
|
||||
`Call ${this.callId} updateLocalUsermediaStream() replacing track (id=${track.id}, kind=${track.kind}, streamId=${stream.id}, streamPurpose=${callFeed.purpose})`,
|
||||
);
|
||||
await oldSender.replaceTrack(track);
|
||||
// Set the direction to indicate we're going to be sending.
|
||||
@@ -1331,19 +1346,16 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
transceiver.direction = transceiver.direction === "inactive" ? "sendonly" : "sendrecv";
|
||||
added = true;
|
||||
} catch (error) {
|
||||
logger.warn(`replaceTrack failed: adding new transceiver instead`, error);
|
||||
logger.warn(
|
||||
`Call ${this.callId} updateLocalUsermediaStream() replaceTrack failed: adding new transceiver instead`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!added) {
|
||||
logger.info(
|
||||
`Call ${this.callId} ` +
|
||||
`Adding track (` +
|
||||
`id="${track.id}", ` +
|
||||
`kind="${track.kind}", ` +
|
||||
`streamId="${stream.id}", ` +
|
||||
`streamPurpose="${callFeed.purpose}"` +
|
||||
`) to peer connection`,
|
||||
`Call ${this.callId} updateLocalUsermediaStream() adding track to peer connection (id=${track.id}, kind=${track.kind}, streamId=${stream.id}, streamPurpose=${callFeed.purpose})`,
|
||||
);
|
||||
|
||||
const newSender = this.peerConn!.addTrack(track, this.localUsermediaStream!);
|
||||
@@ -1351,7 +1363,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
if (newTransceiver) {
|
||||
this.transceivers.set(tKey, newTransceiver);
|
||||
} else {
|
||||
logger.warn("Couldn't find matching transceiver for newly added track!");
|
||||
logger.warn(
|
||||
`Call ${this.callId} updateLocalUsermediaStream() couldn't find matching transceiver for newly added track!`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1363,7 +1377,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
* @returns the new mute state
|
||||
*/
|
||||
public async setLocalVideoMuted(muted: boolean): Promise<boolean> {
|
||||
logger.log(`call ${this.callId} setLocalVideoMuted ${muted}`);
|
||||
logger.log(`Call ${this.callId} setLocalVideoMuted() running ${muted}`);
|
||||
|
||||
// if we were still thinking about stopping and removing the video
|
||||
// track: don't, because we want it back.
|
||||
@@ -1431,7 +1445,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
* @returns the new mute state
|
||||
*/
|
||||
public async setMicrophoneMuted(muted: boolean): Promise<boolean> {
|
||||
logger.log(`call ${this.callId} setMicrophoneMuted ${muted}`);
|
||||
logger.log(`Call ${this.callId} setMicrophoneMuted() running ${muted}`);
|
||||
if (!(await this.client.getMediaHandler().hasAudioDevice())) {
|
||||
return this.isMicrophoneMuted();
|
||||
}
|
||||
@@ -1524,7 +1538,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
const vidShouldBeMuted = this.isLocalVideoMuted() || this.remoteOnHold;
|
||||
|
||||
logger.log(
|
||||
`call ${this.callId} updateMuteStatus stream ${
|
||||
`Call ${this.callId} updateMuteStatus stream ${
|
||||
this.localUsermediaStream!.id
|
||||
} micShouldBeMuted ${micShouldBeMuted} vidShouldBeMuted ${vidShouldBeMuted}`,
|
||||
);
|
||||
@@ -1561,7 +1575,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
|
||||
this.state = CallState.CreateOffer;
|
||||
|
||||
logger.debug(`Call ${this.callId} gotUserMediaForInvite`);
|
||||
logger.debug(`Call ${this.callId} gotUserMediaForInvite() run`);
|
||||
// Now we wait for the negotiationneeded event
|
||||
}
|
||||
|
||||
@@ -1585,7 +1599,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
// contain all the local candidates added so far, so we can discard any candidates
|
||||
// we had queued up because they'll be in the answer.
|
||||
const discardCount = this.discardDuplicateCandidates();
|
||||
logger.info(`Call ${this.callId} Discarding ${discardCount} candidates that will be sent in answer`);
|
||||
logger.info(
|
||||
`Call ${this.callId} sendAnswer() discarding ${discardCount} candidates that will be sent in answer`,
|
||||
);
|
||||
|
||||
try {
|
||||
await this.sendVoipEvent(EventType.CallAnswer, answerContent);
|
||||
@@ -1639,7 +1655,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
if (mod.mediaType !== media.type) continue;
|
||||
|
||||
if (!codecToPayloadTypeMap.has(mod.codec)) {
|
||||
logger.info(`Ignoring SDP modifications for ${mod.codec} as it's not present.`);
|
||||
logger.info(
|
||||
`Call ${this.callId} mungeSdp() ignoring SDP modifications for ${mod.codec} as it's not present.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1697,7 +1715,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
this.getRidOfRTXCodecs();
|
||||
answer = await this.createAnswer();
|
||||
} catch (err) {
|
||||
logger.debug(`Call ${this.callId} Failed to create answer: `, err);
|
||||
logger.debug(`Call ${this.callId} gotCallFeedsForAnswer() failed to create answer: `, err);
|
||||
this.terminate(CallParty.Local, CallErrorCode.CreateAnswer, true);
|
||||
return;
|
||||
}
|
||||
@@ -1720,7 +1738,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
|
||||
this.sendAnswer();
|
||||
} catch (err) {
|
||||
logger.debug(`Call ${this.callId} Error setting local description!`, err);
|
||||
logger.debug(`Call ${this.callId} gotCallFeedsForAnswer() error setting local description!`, err);
|
||||
this.terminate(CallParty.Local, CallErrorCode.SetLocalDescription, true);
|
||||
return;
|
||||
}
|
||||
@@ -1732,18 +1750,13 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
private gotLocalIceCandidate = (event: RTCPeerConnectionIceEvent): void => {
|
||||
if (event.candidate) {
|
||||
if (this.candidatesEnded) {
|
||||
logger.warn("Got candidate after candidates have ended - ignoring!");
|
||||
logger.warn(
|
||||
`Call ${this.callId} gotLocalIceCandidate() got candidate after candidates have ended - ignoring!`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
"Call " +
|
||||
this.callId +
|
||||
" got local ICE " +
|
||||
event.candidate.sdpMid +
|
||||
" candidate: " +
|
||||
event.candidate.candidate,
|
||||
);
|
||||
logger.debug(`Call ${this.callId} got local ICE ${event.candidate.sdpMid} ${event.candidate.candidate}`);
|
||||
|
||||
if (this.callHasEnded()) return;
|
||||
|
||||
@@ -1758,7 +1771,11 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
};
|
||||
|
||||
private onIceGatheringStateChange = (event: Event): void => {
|
||||
logger.debug(`Call ${this.callId} ice gathering state changed to ${this.peerConn!.iceGatheringState}`);
|
||||
logger.debug(
|
||||
`Call ${this.callId} onIceGatheringStateChange() ice gathering state changed to ${
|
||||
this.peerConn!.iceGatheringState
|
||||
}`,
|
||||
);
|
||||
if (this.peerConn?.iceGatheringState === "complete") {
|
||||
this.queueCandidate(null);
|
||||
}
|
||||
@@ -1773,7 +1790,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
const content = ev.getContent<MCallCandidates>();
|
||||
const candidates = content.candidates;
|
||||
if (!candidates) {
|
||||
logger.info(`Call ${this.callId} Ignoring candidates event with no candidates!`);
|
||||
logger.info(
|
||||
`Call ${this.callId} onRemoteIceCandidatesReceived() ignoring candidates event with no candidates!`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1782,7 +1801,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
if (this.opponentPartyId === undefined) {
|
||||
// we haven't picked an opponent yet so save the candidates
|
||||
if (fromPartyId) {
|
||||
logger.info(`Call ${this.callId} Buffering ${candidates.length} candidates until we pick an opponent`);
|
||||
logger.info(
|
||||
`Call ${this.callId} onRemoteIceCandidatesReceived() buffering ${candidates.length} candidates until we pick an opponent`,
|
||||
);
|
||||
const bufferedCandidates = this.remoteCandidateBuffer.get(fromPartyId) || [];
|
||||
bufferedCandidates.push(...candidates);
|
||||
this.remoteCandidateBuffer.set(fromPartyId, bufferedCandidates);
|
||||
@@ -1792,9 +1813,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
|
||||
if (!this.partyIdMatches(content)) {
|
||||
logger.info(
|
||||
`Call ${this.callId} ` +
|
||||
`Ignoring candidates from party ID ${content.party_id}: ` +
|
||||
`we have chosen party ID ${this.opponentPartyId}`,
|
||||
`Call ${this.callId} onRemoteIceCandidatesReceived() ignoring candidates from party ID ${content.party_id}: we have chosen party ID ${this.opponentPartyId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
@@ -1808,18 +1827,16 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
*/
|
||||
public async onAnswerReceived(event: MatrixEvent): Promise<void> {
|
||||
const content = event.getContent<MCallAnswer>();
|
||||
logger.debug(`Got answer for call ID ${this.callId} from party ID ${content.party_id}`);
|
||||
logger.debug(`Call ${this.callId} onAnswerReceived() running (hangupParty=${content.party_id})`);
|
||||
|
||||
if (this.callHasEnded()) {
|
||||
logger.debug(`Ignoring answer because call ID ${this.callId} has ended`);
|
||||
logger.debug(`Call ${this.callId} onAnswerReceived() ignoring answer because call has ended`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.opponentPartyId !== undefined) {
|
||||
logger.info(
|
||||
`Call ${this.callId} ` +
|
||||
`Ignoring answer from party ID ${content.party_id}: ` +
|
||||
`we already have an answer/reject from ${this.opponentPartyId}`,
|
||||
`Call ${this.callId} onAnswerReceived() ignoring answer from party ID ${content.party_id}: we already have an answer/reject from ${this.opponentPartyId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -1833,13 +1850,15 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
if (sdpStreamMetadata) {
|
||||
this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
|
||||
} else {
|
||||
logger.warn(`Call ${this.callId} Did not get any SDPStreamMetadata! Can not send/receive multiple streams`);
|
||||
logger.warn(
|
||||
`Call ${this.callId} onAnswerReceived() did not get any SDPStreamMetadata! Can not send/receive multiple streams`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.peerConn!.setRemoteDescription(content.answer);
|
||||
} catch (e) {
|
||||
logger.debug(`Call ${this.callId} Failed to set remote description`, e);
|
||||
logger.debug(`Call ${this.callId} onAnswerReceived() failed to set remote description`, e);
|
||||
this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
|
||||
return;
|
||||
}
|
||||
@@ -1855,14 +1874,16 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
} catch (err) {
|
||||
// This isn't fatal, and will just mean that if another party has raced to answer
|
||||
// the call, they won't know they got rejected, so we carry on & don't retry.
|
||||
logger.warn(`Call ${this.callId} Failed to send select_answer event`, err);
|
||||
logger.warn(`Call ${this.callId} onAnswerReceived() failed to send select_answer event`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async onSelectAnswerReceived(event: MatrixEvent): Promise<void> {
|
||||
if (this.direction !== CallDirection.Inbound) {
|
||||
logger.warn(`Call ${this.callId} Got select_answer for an outbound call: ignoring`);
|
||||
logger.warn(
|
||||
`Call ${this.callId} onSelectAnswerReceived() got select_answer for an outbound call: ignoring`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1870,14 +1891,14 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
|
||||
if (selectedPartyId === undefined || selectedPartyId === null) {
|
||||
logger.warn(
|
||||
`Call ${this.callId} Got nonsensical select_answer with null/undefined selected_party_id: ignoring`,
|
||||
`Call ${this.callId} onSelectAnswerReceived() got nonsensical select_answer with null/undefined selected_party_id: ignoring`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedPartyId !== this.ourPartyId) {
|
||||
logger.info(
|
||||
`Call ${this.callId} Got select_answer for party ID ${selectedPartyId}: we are party ID ${this.ourPartyId}.`,
|
||||
`Call ${this.callId} onSelectAnswerReceived() got select_answer for party ID ${selectedPartyId}: we are party ID ${this.ourPartyId}.`,
|
||||
);
|
||||
// The other party has picked somebody else's answer
|
||||
await this.terminate(CallParty.Remote, CallErrorCode.AnsweredElsewhere, true);
|
||||
@@ -1888,7 +1909,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
const content = event.getContent<MCallInviteNegotiate>();
|
||||
const description = content.description;
|
||||
if (!description || !description.sdp || !description.type) {
|
||||
logger.info(`Call ${this.callId} Ignoring invalid m.call.negotiate event`);
|
||||
logger.info(`Call ${this.callId} onNegotiateReceived() ignoring invalid m.call.negotiate event`);
|
||||
return;
|
||||
}
|
||||
// Politeness always follows the direction of the call: in a glare situation,
|
||||
@@ -1903,7 +1924,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
|
||||
this.ignoreOffer = !polite && offerCollision;
|
||||
if (this.ignoreOffer) {
|
||||
logger.info(`Call ${this.callId} Ignoring colliding negotiate event because we're impolite`);
|
||||
logger.info(
|
||||
`Call ${this.callId} onNegotiateReceived() ignoring colliding negotiate event because we're impolite`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1913,7 +1936,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
if (sdpStreamMetadata) {
|
||||
this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
|
||||
} else {
|
||||
logger.warn(`Call ${this.callId} Received negotiation event without SDPStreamMetadata!`);
|
||||
logger.warn(
|
||||
`Call ${this.callId} onNegotiateReceived() received negotiation event without SDPStreamMetadata!`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1925,7 +1950,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
this.getRidOfRTXCodecs();
|
||||
answer = await this.createAnswer();
|
||||
} catch (err) {
|
||||
logger.debug(`Call ${this.callId} Failed to create answer: `, err);
|
||||
logger.debug(`Call ${this.callId} onNegotiateReceived() failed to create answer: `, err);
|
||||
this.terminate(CallParty.Local, CallErrorCode.CreateAnswer, true);
|
||||
return;
|
||||
}
|
||||
@@ -1938,7 +1963,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn(`Call ${this.callId} Failed to complete negotiation`, err);
|
||||
logger.warn(`Call ${this.callId} onNegotiateReceived() failed to complete negotiation`, err);
|
||||
}
|
||||
|
||||
const newLocalOnHold = this.isLocalOnHold();
|
||||
@@ -2006,10 +2031,12 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
}
|
||||
|
||||
private async gotLocalOffer(): Promise<void> {
|
||||
logger.debug(`Call ${this.callId} Setting local description`);
|
||||
logger.debug(`Call ${this.callId} gotLocalOffer() running`);
|
||||
|
||||
if (this.callHasEnded()) {
|
||||
logger.debug("Ignoring newly created offer on call ID " + this.callId + " because the call has ended");
|
||||
logger.debug(
|
||||
`Call ${this.callId} gotLocalOffer() ignoring newly created offer because the call has ended"`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2018,7 +2045,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
this.getRidOfRTXCodecs();
|
||||
offer = await this.createOffer();
|
||||
} catch (err) {
|
||||
logger.debug(`Call ${this.callId} Failed to create offer: `, err);
|
||||
logger.debug(`Call ${this.callId} gotLocalOffer() failed to create offer: `, err);
|
||||
this.terminate(CallParty.Local, CallErrorCode.CreateOffer, true);
|
||||
return;
|
||||
}
|
||||
@@ -2026,7 +2053,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
try {
|
||||
await this.peerConn!.setLocalDescription(offer);
|
||||
} catch (err) {
|
||||
logger.debug(`Call ${this.callId} Error setting local description!`, err);
|
||||
logger.debug(`Call ${this.callId} gotLocalOffer() error setting local description!`, err);
|
||||
this.terminate(CallParty.Local, CallErrorCode.SetLocalDescription, true);
|
||||
return;
|
||||
}
|
||||
@@ -2067,12 +2094,14 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
// Get rid of any candidates waiting to be sent: they'll be included in the local
|
||||
// description we just got and will send in the offer.
|
||||
const discardCount = this.discardDuplicateCandidates();
|
||||
logger.info(`Call ${this.callId} Discarding ${discardCount} candidates that will be sent in offer`);
|
||||
logger.info(
|
||||
`Call ${this.callId} gotLocalOffer() discarding ${discardCount} candidates that will be sent in offer`,
|
||||
);
|
||||
|
||||
try {
|
||||
await this.sendVoipEvent(eventType, content);
|
||||
} catch (error) {
|
||||
logger.error(`Call ${this.callId} Failed to send invite`, error);
|
||||
logger.error(`Call ${this.callId} gotLocalOffer() failed to send invite`, error);
|
||||
if (error instanceof MatrixError && error.event) this.client.cancelPendingEvent(error.event);
|
||||
|
||||
let code = CallErrorCode.SignallingFailed;
|
||||
@@ -2108,7 +2137,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
}
|
||||
|
||||
private getLocalOfferFailed = (err: Error): void => {
|
||||
logger.error(`Call ${this.callId} Failed to get local offer`, err);
|
||||
logger.error(`Call ${this.callId} getLocalOfferFailed() running`, err);
|
||||
|
||||
this.emit(CallEvent.Error, new CallError(CallErrorCode.LocalOfferFailed, "Failed to get local offer!", err));
|
||||
this.terminate(CallParty.Local, CallErrorCode.LocalOfferFailed, false);
|
||||
@@ -2120,7 +2149,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn(`Failed to get user media - ending call ${this.callId}`, err);
|
||||
logger.warn(`Call ${this.callId} getUserMediaFailed() failed to get user media - ending call`, err);
|
||||
|
||||
this.emit(
|
||||
CallEvent.Error,
|
||||
@@ -2138,13 +2167,14 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
return; // because ICE can still complete as we're ending the call
|
||||
}
|
||||
logger.debug(
|
||||
"Call ID " + this.callId + ": ICE connection state changed to: " + this.peerConn?.iceConnectionState,
|
||||
`Call ${this.callId} onIceConnectionStateChanged() running (state=${this.peerConn?.iceConnectionState})`,
|
||||
);
|
||||
|
||||
// ideally we'd consider the call to be connected when we get media but
|
||||
// chrome doesn't implement any of the 'onstarted' events yet
|
||||
if (["connected", "completed"].includes(this.peerConn?.iceConnectionState ?? "")) {
|
||||
clearTimeout(this.iceDisconnectedTimeout);
|
||||
this.iceDisconnectedTimeout = undefined;
|
||||
this.state = CallState.Connected;
|
||||
|
||||
if (!this.callLengthInterval && !this.callStartTime) {
|
||||
@@ -2162,12 +2192,16 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
this.candidatesEnded = false;
|
||||
this.peerConn!.restartIce();
|
||||
} else {
|
||||
logger.info(`Hanging up call ${this.callId} (ICE failed and no ICE restart method)`);
|
||||
logger.info(
|
||||
`Call ${this.callId} onIceConnectionStateChanged() hanging up call (ICE failed and no ICE restart method)`,
|
||||
);
|
||||
this.hangup(CallErrorCode.IceFailed, false);
|
||||
}
|
||||
} else if (this.peerConn?.iceConnectionState == "disconnected") {
|
||||
this.iceDisconnectedTimeout = setTimeout(() => {
|
||||
logger.info(`Hanging up call ${this.callId} (ICE disconnected for too long)`);
|
||||
logger.info(
|
||||
`Call ${this.callId} onIceConnectionStateChanged() hanging up call (ICE disconnected for too long)`,
|
||||
);
|
||||
this.hangup(CallErrorCode.IceFailed, false);
|
||||
}, ICE_DISCONNECTED_TIMEOUT);
|
||||
this.state = CallState.Connecting;
|
||||
@@ -2185,12 +2219,14 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
};
|
||||
|
||||
private onSignallingStateChanged = (): void => {
|
||||
logger.debug(`call ${this.callId}: Signalling state changed to: ${this.peerConn?.signalingState}`);
|
||||
logger.debug(`Call ${this.callId} onSignallingStateChanged() running (state=${this.peerConn?.signalingState})`);
|
||||
};
|
||||
|
||||
private onTrack = (ev: RTCTrackEvent): void => {
|
||||
if (ev.streams.length === 0) {
|
||||
logger.warn(`Call ${this.callId} Streamless ${ev.track.kind} found: ignoring.`);
|
||||
logger.warn(
|
||||
`Call ${this.callId} onTrack() called with streamless track streamless (kind=${ev.track.kind})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2200,7 +2236,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
if (!this.removeTrackListeners.has(stream)) {
|
||||
const onRemoveTrack = (): void => {
|
||||
if (stream.getTracks().length === 0) {
|
||||
logger.info(`Call ${this.callId} removing track streamId: ${stream.id}`);
|
||||
logger.info(`Call ${this.callId} onTrack() removing track (streamId=${stream.id})`);
|
||||
this.deleteFeedByStream(stream);
|
||||
stream.removeEventListener("removetrack", onRemoveTrack);
|
||||
this.removeTrackListeners.delete(stream);
|
||||
@@ -2250,11 +2286,11 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
}
|
||||
|
||||
private onNegotiationNeeded = async (): Promise<void> => {
|
||||
logger.info(`Call ${this.callId} Negotiation is needed!`);
|
||||
logger.info(`Call ${this.callId} onNegotiationNeeded() negotiation is needed!`);
|
||||
|
||||
if (this.state !== CallState.CreateOffer && this.opponentVersion === 0) {
|
||||
logger.info(
|
||||
`Call ${this.callId} Opponent does not support renegotiation: ignoring negotiationneeded event`,
|
||||
`Call ${this.callId} onNegotiationNeeded() opponent does not support renegotiation: ignoring negotiationneeded event`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -2263,7 +2299,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
};
|
||||
|
||||
public onHangupReceived = (msg: MCallHangupReject): void => {
|
||||
logger.debug("Hangup received for call ID " + this.callId);
|
||||
logger.debug(`Call ${this.callId} onHangupReceived() running`);
|
||||
|
||||
// party ID must match (our chosen partner hanging up the call) or be undefined (we haven't chosen
|
||||
// a partner yet but we're treating the hangup as a reject as per VoIP v0)
|
||||
@@ -2272,13 +2308,13 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
this.terminate(CallParty.Remote, msg.reason || CallErrorCode.UserHangup, true);
|
||||
} else {
|
||||
logger.info(
|
||||
`Call ${this.callId} Ignoring message from party ID ${msg.party_id}: our partner is ${this.opponentPartyId}`,
|
||||
`Call ${this.callId} onHangupReceived() ignoring message from party ID ${msg.party_id}: our partner is ${this.opponentPartyId}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
public onRejectReceived = (msg: MCallHangupReject): void => {
|
||||
logger.debug("Reject received for call ID " + this.callId);
|
||||
logger.debug(`Call ${this.callId} onRejectReceived() running`);
|
||||
|
||||
// No need to check party_id for reject because if we'd received either
|
||||
// an answer or reject, we wouldn't be in state InviteSent
|
||||
@@ -2294,12 +2330,12 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
if (shouldTerminate) {
|
||||
this.terminate(CallParty.Remote, msg.reason || CallErrorCode.UserHangup, true);
|
||||
} else {
|
||||
logger.debug(`Call ${this.callId} is in state: ${this.state}: ignoring reject`);
|
||||
logger.debug(`Call ${this.callId} onRejectReceived() called in wrong state (state=${this.state})`);
|
||||
}
|
||||
};
|
||||
|
||||
public onAnsweredElsewhere = (msg: MCallAnswer): void => {
|
||||
logger.debug("Call " + this.callId + " answered elsewhere");
|
||||
logger.debug(`Call ${this.callId} onAnsweredElsewhere() running`);
|
||||
this.terminate(CallParty.Remote, CallErrorCode.AnsweredElsewhere, true);
|
||||
};
|
||||
|
||||
@@ -2511,6 +2547,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
clearTimeout(this.inviteTimeout);
|
||||
this.inviteTimeout = undefined;
|
||||
}
|
||||
if (this.iceDisconnectedTimeout !== undefined) {
|
||||
clearTimeout(this.iceDisconnectedTimeout);
|
||||
this.iceDisconnectedTimeout = undefined;
|
||||
}
|
||||
if (this.callLengthInterval) {
|
||||
clearInterval(this.callLengthInterval);
|
||||
this.callLengthInterval = undefined;
|
||||
@@ -2542,7 +2582,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
}
|
||||
|
||||
private stopAllMedia(): void {
|
||||
logger.debug(`Call ${this.callId} stopping all media`);
|
||||
logger.debug(`Call ${this.callId} stopAllMedia() running`);
|
||||
|
||||
for (const feed of this.feeds) {
|
||||
// Slightly awkward as local feed need to go via the correct method on
|
||||
@@ -2556,7 +2596,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
} else if (feed.isLocal() && feed.purpose === SDPStreamMetadataPurpose.Screenshare) {
|
||||
this.client.getMediaHandler().stopScreensharingStream(feed.stream);
|
||||
} else if (!feed.isLocal()) {
|
||||
logger.debug("Stopping remote stream", feed.stream.id);
|
||||
logger.debug(`Call ${this.callId} stopAllMedia() stopping stream (streamId=${feed.stream.id})`);
|
||||
for (const track of feed.stream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
@@ -2585,7 +2625,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
candidate: "",
|
||||
});
|
||||
}
|
||||
logger.debug(`Call ${this.callId} attempting to send ${candidates.length} candidates`);
|
||||
logger.debug(`Call ${this.callId} sendCandidateQueue() attempting to send ${candidates.length} candidates`);
|
||||
try {
|
||||
await this.sendVoipEvent(EventType.CallCandidates, content);
|
||||
// reset our retry count if we have successfully sent our candidates
|
||||
@@ -2604,7 +2644,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
|
||||
if (this.candidateSendTries > 5) {
|
||||
logger.debug(
|
||||
`Call ${this.callId} failed to send candidates on attempt ${this.candidateSendTries}. Giving up on this call.`,
|
||||
`Call ${this.callId} sendCandidateQueue() failed to send candidates on attempt ${this.candidateSendTries}. Giving up on this call.`,
|
||||
error,
|
||||
);
|
||||
|
||||
@@ -2619,7 +2659,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
|
||||
const delayMs = 500 * Math.pow(2, this.candidateSendTries);
|
||||
++this.candidateSendTries;
|
||||
logger.debug(`Call ${this.callId} failed to send candidates. Retrying in ${delayMs}ms`, error);
|
||||
logger.debug(
|
||||
`Call ${this.callId} sendCandidateQueue() failed to send candidates. Retrying in ${delayMs}ms`,
|
||||
error,
|
||||
);
|
||||
setTimeout(() => {
|
||||
this.sendCandidateQueue();
|
||||
}, delayMs);
|
||||
@@ -2681,7 +2724,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
// poll and keep the credentials valid so this should be instant.
|
||||
const haveTurnCreds = await this.client.checkTurnServers();
|
||||
if (!haveTurnCreds) {
|
||||
logger.warn(`Call ${this.callId} Failed to get TURN credentials! Proceeding with call anyway...`);
|
||||
logger.warn(
|
||||
`Call ${this.callId} placeCallWithCallFeeds() failed to get TURN credentials! Proceeding with call anyway...`,
|
||||
);
|
||||
}
|
||||
|
||||
// create the peer connection now so it can be gathering candidates while we get user
|
||||
@@ -2725,7 +2770,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
// I choo-choo-choose you
|
||||
const msg = ev.getContent<MCallInviteNegotiate | MCallAnswer>();
|
||||
|
||||
logger.debug(`Call ${this.callId} choosing opponent party ID ${msg.party_id}`);
|
||||
logger.debug(`Call ${this.callId} chooseOpponent() running (partyId=${msg.party_id})`);
|
||||
|
||||
this.opponentVersion = msg.version;
|
||||
if (this.opponentVersion === 0) {
|
||||
@@ -2746,7 +2791,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
const bufferedCandidates = this.remoteCandidateBuffer.get(this.opponentPartyId!);
|
||||
if (bufferedCandidates) {
|
||||
logger.info(
|
||||
`Call ${this.callId} Adding ${bufferedCandidates.length} buffered candidates for opponent ${this.opponentPartyId}`,
|
||||
`Call ${this.callId} addBufferedIceCandidates() adding ${bufferedCandidates.length} buffered candidates for opponent ${this.opponentPartyId}`,
|
||||
);
|
||||
await this.addIceCandidates(bufferedCandidates);
|
||||
}
|
||||
@@ -2759,10 +2804,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
(candidate.sdpMid === null || candidate.sdpMid === undefined) &&
|
||||
(candidate.sdpMLineIndex === null || candidate.sdpMLineIndex === undefined)
|
||||
) {
|
||||
logger.debug(`Call ${this.callId} got remote ICE end-of-candidates`);
|
||||
logger.debug(`Call ${this.callId} addIceCandidates() got remote ICE end-of-candidates`);
|
||||
} else {
|
||||
logger.debug(
|
||||
`Call ${this.callId} got remote ICE ${candidate.sdpMid} candidate: ${candidate.candidate}`,
|
||||
`Call ${this.callId} addIceCandidates() got remote ICE candidate (sdpMid=${candidate.sdpMid}, candidate=${candidate.candidate})`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2770,7 +2815,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
await this.peerConn!.addIceCandidate(candidate);
|
||||
} catch (err) {
|
||||
if (!this.ignoreOffer) {
|
||||
logger.info(`Call ${this.callId} failed to add remote ICE candidate`, err);
|
||||
logger.info(`Call ${this.callId} addIceCandidates() failed to add remote ICE candidate`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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