Compare commits

..

26 Commits

Author SHA1 Message Date
RiotRobot c9b502fb0e v23.4.0-rc.1 2023-02-21 11:56:15 +00:00
RiotRobot 330fbaccfc Prepare changelog for v23.4.0-rc.1 2023-02-21 11:56:12 +00:00
Michael Telatynski 937f370655 Run matrix-react-sdk tests on merge queue (#3161)
* First attempt at merge queues for downstream testing

* Debug

* delint

* Fix typo

* Test

* Iterate

* Fix checkout

* rerun

* experiment

* Iterate

* Iterate

* iterate

* Iterate

* Finalise

* Disable coverage for downstream tests

* Update tests.yml

* Apply merge_queue trigger to other CI

* delint

* delint

* Add exception
2023-02-21 11:09:23 +00:00
Kerry a8ad3ed26d Polls: expose end event id on poll model (#3160) 2023-02-20 10:30:19 +00:00
Michael Telatynski decac58a18 Better type guard parseTopicContent (#3165) 2023-02-20 10:29:33 +00:00
Kerry 1a91ba59a6 Polls: count undecryptable poll relations (#3163) 2023-02-20 10:10:38 +00:00
Michael Telatynski 89df43a975 Fix predecessor types, nowhere does the spec say it can be null (#3159)
* Fix predecessor types, nowhere does the spec say it can be `null`

* Iterate

* Update comment

* update test
2023-02-16 09:38:36 +00:00
Michael Telatynski ad98706db4 Iterate types (#3156) 2023-02-15 13:38:41 +00:00
Will Hunt 195d1730bd Fix notification counts for encrypted rooms with ignored event rules (#3130)
* Validate vars early

* Split out unread  counts for total and highlight to different logic blocks

* Add tests for ignoring non notifying events

* Fix possibly incorrect tests?

* lint fix

* Refactor currentTotalCount

* Track Total locally too

* Lots of total count assumptions and comments

* Adjust for threading too

* Fixup tests

* a word

* lint fix
2023-02-15 11:25:13 +00:00
Richard van der Hoff db4bd907f8 Switch crypto.spec.ts away from TestClient and matrix-mock-request. (#3142)
I became sufficiently annoyed with matrix-mock-request that I decided to replace it with fetch-mock, which is what we use in matrix-react-sdk and is generally more powerful, easier to use, and actually maintained.

Unfortunately, we have a TestClient utility which is widely used and quite tightly integrated with matrix-mock-request. It wasn't going to be possible to change TestClient without changing all the tests that use it.

I also don't find TestClient particularly easy to use - it does a lot of stuff which I'm not convinced ought to be done for every single test.

So... I've introduced a couple of new classes (SyncResponder, E2EKeyReceiver) which do some of the useful bits of TestClient, but in a more granular way, and have switched crypto.spec.ts over so that rather than instantiating a TestClient for each test, it creates a MatrixClient directly and intercepts the endpoints necessary.
2023-02-15 10:39:24 +00:00
Kerry cdd7dbbb2b decrypt poll relations before processing (#3148) 2023-02-14 21:49:52 +00:00
RiotRobot 108f157324 Resetting package fields for development 2023-02-14 10:23:07 +00:00
RiotRobot 8da39ec8f4 Merge branch 'master' into develop 2023-02-14 10:23:02 +00:00
Andy Balaam 5e17626fe0 Include 'browser' in list of adjusted properties in release.sh (#3149) 2023-02-14 09:50:55 +00:00
David Baker abc9c9dcb0 Merge pull request #3147 from matrix-org/dbkr/stop_ice_timer_on_terminate
Stop the ICE disconnected timer on call terminate
2023-02-13 17:35:45 +01:00
David Baker f346fcb056 Stop the ICE disconnected timer on call terminate
It just wasn't getting stopped, so if the call ended while ICE was
disconnected, we'd get confusing error messages after the call ended.
2023-02-13 16:15:57 +00:00
Eric Eastwood c67325ba07 Add matrix-org/jest linting (#2973) 2023-02-10 12:05:40 +01:00
renovate[bot] a063ae8ce7 Update jest monorepo to v29.4.1 (#3133)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-09 15:44:23 +00:00
renovate[bot] f9e5535492 Update typescript-eslint monorepo to v5.50.0 (#3136)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-09 16:29:03 +01:00
renovate[bot] 015d9c5c4f Update dependency @types/node to v18.11.19 (#3137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-09 16:28:22 +01:00
Germain b6d40078d9 Clear notifications when we can infer read status from receipts (#3139) 2023-02-09 10:18:18 +00:00
David Baker b8a8f4850a Merge pull request #3123 from matrix-org/SimonBrandner/task/logging
Improve WebRTC logging
2023-02-08 17:10:40 +00:00
Šimon Brandner 1cc23d789c Add new tests to groupCall
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-02-08 17:24:15 +01:00
Janne Mareike Koschinski 5cf0bb46a4 Messages sent out of order after one message fails (#3131)
* Instead of skipping, bail out by clearing queue
* Allow additional status transition for events from QUEUED to NOT_SENT
2023-02-08 13:23:30 +01:00
renovate[bot] 16672b3d0c Update all non-major dependencies (#3132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-07 17:36:38 +00:00
Šimon Brandner 31459a5d63 Improve logging
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-02-03 11:40:48 +01:00
63 changed files with 2895 additions and 1782 deletions
+1 -1
View File
@@ -23,4 +23,4 @@ indent_size = 4
trim_trailing_whitespace = true
[*.{yml,yaml}]
indent_size = 2
indent_size = 4
+18 -1
View File
@@ -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: [
{
+3 -6
View File
@@ -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
+2
View File
@@ -1,6 +1,8 @@
name: Static Analysis
on:
pull_request: {}
merge_group:
types: [checks_requested]
push:
branches: [develop, master]
concurrency:
+30 -8
View File
@@ -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
+14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
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 () {
+14 -28
View File
@@ -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);
});
});
+37 -31
View File
@@ -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");
});
});
});
+6 -6
View File
@@ -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 () {});
});
});
+105 -102
View File
@@ -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;
});
});
+16 -22
View File
@@ -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]: {},
},
};
}
});
+1 -1
View File
@@ -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";
+4 -4
View File
@@ -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();
});
});
+159
View File
@@ -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;
}
}
+131
View File
@@ -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`);
}
}
}
+4
View File
@@ -450,6 +450,10 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
]
>();
public isInitialSyncComplete(): boolean {
return false;
}
public getMediaHandler(): MediaHandler {
return this.mediaHandler.typed();
}
+16 -6
View File
@@ -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;
});
});
+2 -2
View File
@@ -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);
});
+5 -1
View File
@@ -148,6 +148,10 @@ describe("DeviceList", function () {
dl.invalidateUserDeviceList("@test1:sw1v.org");
dl.refreshOutdatedDeviceLists();
// TODO: Fix this test so we actually await the call and assertions and remove
// the eslint disable, https://github.com/matrix-org/matrix-js-sdk/issues/2977
//
// eslint-disable-next-line jest/valid-expect-in-promise
dl.saveIfDirty()
.then(() => {
// the first request completes
@@ -196,7 +200,7 @@ describe("DeviceList", function () {
downloadSpy.mockReturnValueOnce(queryDefer2.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toBeCalledTimes(2);
expect(downloadSpy).toHaveBeenCalledTimes(2);
expect(downloadSpy).toHaveBeenNthCalledWith(1, ["@test1:sw1v.org"], {});
expect(downloadSpy).toHaveBeenNthCalledWith(2, ["@test2:sw1v.org"], {});
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
+1 -1
View File
@@ -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];
+1 -1
View File
@@ -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");
});
});
+2 -2
View File
@@ -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,
+15 -44
View File
@@ -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
View File
@@ -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);
+50 -86
View File
@@ -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);
+2
View File
@@ -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),
+29 -3
View File
@@ -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);
+2 -2
View File
@@ -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);
});
+5 -5
View File
@@ -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);
}
});
});
+9 -4
View File
@@ -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
View File
@@ -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 () {
+2
View File
@@ -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);
},
};
+1 -1
View File
@@ -1579,7 +1579,7 @@ describe("Call", function () {
hasAdvancedBy += advanceBy;
expect(lengthChangedListener).toHaveBeenCalledTimes(hasAdvancedBy);
expect(lengthChangedListener).toBeCalledWith(hasAdvancedBy);
expect(lengthChangedListener).toHaveBeenCalledWith(hasAdvancedBy);
}
});
+1 -1
View File
@@ -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);
+54 -2
View File
@@ -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", () => {
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
}
+3
View File
@@ -202,6 +202,9 @@ export type TopicState = {
export const parseTopicContent = (content: MRoomTopicEventContent): TopicState => {
const mtopic = M_TOPIC.findIn<MTopicContent>(content);
if (!Array.isArray(mtopic)) {
return { text: content.topic };
}
const text = mtopic?.find((r) => !isProvided(r.mimetype) || r.mimetype === "text/plain")?.body ?? content.topic;
const html = mtopic?.find((r) => r.mimetype === "text/html")?.body;
return { text, html };
+1 -15
View File
@@ -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
View File
@@ -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>;
}
+1 -1
View File
@@ -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
View File
@@ -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;
+24
View File
@@ -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
View File
@@ -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
View File
@@ -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]: [],
+5 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
}
+23 -17
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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,
),
);
}
};
}
+14 -9
View File
@@ -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
View File
@@ -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,
+580 -467
View File
File diff suppressed because it is too large Load Diff