Compare commits

...

55 Commits

Author SHA1 Message Date
RiotRobot b6369cc2bd v22.0.0 2022-12-06 12:32:58 +00:00
RiotRobot e6e079f487 Prepare changelog for v22.0.0 2022-12-06 12:32:58 +00:00
RiotRobot 83a1e07380 v22.0.0-rc.2 2022-12-02 16:23:53 +00:00
RiotRobot 7f3123ed65 Prepare changelog for v22.0.0-rc.2 2022-12-02 16:23:53 +00:00
ElementRobot 5a88a6c62a [Backport staging] Fix highlight notifications increasing when total notification is zero (#2939)
Co-authored-by: Germain <germains@element.io>
2022-12-02 16:16:41 +00:00
ElementRobot 12cecbdcf1 [Backport staging] Fix synthesizeReceipt (#2934)
(cherry picked from commit 3577aa98b5)

Co-authored-by: Germain <germains@element.io>
2022-12-02 16:09:05 +00:00
Robin c17deb0806 Backport "Make GroupCall work better with widgets" to staging (#2936) 2022-12-02 10:34:41 +00:00
RiotRobot 31c4f6c16b v22.0.0-rc.1 2022-11-29 15:33:50 +00:00
RiotRobot 22271d22f8 Prepare changelog for v22.0.0-rc.1 2022-11-29 15:33:49 +00:00
Robin 9d3ac66cf8 Merge pull request #2902 from robintown/group-call-participants
Refactor GroupCall participant management
2022-11-28 16:33:08 -05:00
Robin Townsend a4ad4ed2cf Merge branch 'develop' into group-call-participants 2022-11-28 16:11:24 -05:00
kegsay 7fd55a61bf Merge pull request #2912 from matrix-org/kegan/ss-receipts
sliding sync: add receipts extension
2022-11-28 18:22:11 +00:00
Kegan Dougal 847766c114 Review comments 2022-11-28 18:13:17 +00:00
Kegan Dougal c8c39052a7 Linting 2022-11-28 11:09:03 +00:00
Kegan Dougal 6592b2c205 sonarcloud 2022-11-28 10:58:38 +00:00
Michael Telatynski fc91153be4 Revert "Process m.room.encryption events before emitting RoomMember events" (#2913)
This reverts commit aaf3702c66.
2022-11-28 10:23:23 +00:00
Robin Townsend 5511a6ef8c Fix tests 2022-11-26 00:28:11 -05:00
Robin Townsend 19e02e894f Add a method for cleaning group call member state 2022-11-25 23:47:01 -05:00
Robin Townsend c54d61e158 Put creation timestamps on group calls 2022-11-25 23:45:45 -05:00
Robin Townsend 44da9040f4 Emit an event for outgoing group calls 2022-11-25 23:44:46 -05:00
Robin Townsend 995f5bf7d7 Merge branch 'develop' into group-call-participants 2022-11-25 11:56:45 -05:00
Travis Ralston ad16b26247 Define a spec support policy for the js-sdk (#2882)
* Define a spec support policy for the js-sdk

* Update timeline per team discussion
2022-11-25 09:07:09 -07:00
Richard van der Hoff aaf3702c66 Process m.room.encryption events before emitting RoomMember events (#2910)
* Update tests

* Call `Store.storeRoom` earlier

We're going to call `onCryptoEvent` earlier in `processSyncResponse`, but we
need to have stored the room before doing so. We therefore need to move the
call to `storeRoom` earlier.

We can actually reduce a bit of duplication by moving the call into
`SyncApi.createRoom`.

`storeRoom` has relatively few side-effects, so as far as I can tell this
should be pretty safe.

* Call onCryptoEvent before processing state events

This fixes the problematic race condition.
2022-11-25 13:47:28 +00:00
Kegan Dougal 74147b9943 Linting 2022-11-25 13:24:12 +00:00
Kegan Dougal 815370c5f9 sliding sync: add receipts extension 2022-11-25 13:21:16 +00:00
Damir Jelić a01d8e3174 Deprecate a function containing a typo (#2904) 2022-11-25 09:47:52 +00:00
Michael Telatynski 007b7dd242 Fix 3pid invite acceptance not working due to mxid being sent in body (#2907) 2022-11-25 09:22:10 +00:00
Florian Duros 77d6def1cc Add jest metrics (#2897)
* Add slow jest reporter
2022-11-24 13:06:19 +00:00
RiotRobot b318a77ece Resetting package fields for development 2022-11-22 11:25:53 +00:00
RiotRobot 1a90259326 Merge branch 'master' into develop 2022-11-22 11:25:49 +00:00
Robin Townsend f46ecf970c Refactor GroupCall participant management
This refactoring brings a number of improvements to GroupCall, which I've unfortunately had to combine into a single commit due to coupling:

- Moves the expiration timestamp field on call membership state to be per-device
- Makes the participants of a group call visible without having to enter the call yourself
- Enables users to join group calls from multiple devices
- Identifies active speakers by their call feed, rather than just their user ID
- Plays nicely with clients that can be in multiple calls in a room at once
- Fixes a memory leak caused by the call retry loop never stopping
- Changes GroupCall to update its state synchronously, and write back to room state asynchronously
  - This was already sort of halfway being done, but now we'd be committing to it
  - Generally improves the robustness of the state machine
  - It means that group call joins will appear instant, in a sense

For many reasons, this is a breaking change.
2022-11-21 12:16:44 -05:00
Richard van der Hoff dd98d7eb2c Further improvements to e2ee logging (#2900)
A followup to #2884. In particular, it is not always the case that the
`sender_key` in a `m.room_key_withheld` message is the sender of that message.
2022-11-21 14:33:30 +00:00
kegsay f3dc1c4ca2 Merge pull request #2893 from matrix-org/kegan/ss-typing
sliding sync: add support for typing extension
2022-11-21 11:45:14 +00:00
Michael Telatynski 305b83f8ea Merge branch 'develop' into kegan/ss-typing 2022-11-21 11:36:07 +00:00
Kegan Dougal acc488da64 typing events don't need to be in an array 2022-11-21 11:31:33 +00:00
renovate[bot] 7217f83db9 Update all (major) (#2890)
* Update all

* Pin p-retry

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-11-21 08:58:04 +00:00
renovate[bot] 37ea905faa Update all (#2899)
* Update all

* Update webrtc.ts

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-11-21 08:57:49 +00:00
Kegan Dougal 78de55b835 Review comments 2022-11-21 08:42:45 +00:00
David Baker cb410f463a Merge pull request #2898 from matrix-org/dbkr/dont_hang_up_calls_that_didnt_start
Don't hang up calls that haven't started yet
2022-11-18 17:33:31 +00:00
David Baker 72f9d5e6f9 Move check so we still do cleanup but just don't send the hangup 2022-11-18 17:08:50 +00:00
Michael Telatynski c389de98f3 Merge branch 'develop' into dbkr/dont_hang_up_calls_that_didnt_start 2022-11-18 16:39:16 +00:00
Michael Telatynski 20745dc9ac Add CI check with tsc --noImplicitAny (#2896) 2022-11-18 16:26:08 +00:00
David Baker 9410902049 Don't hang up calls that haven't started yet 2022-11-18 16:16:19 +00:00
Kegan Dougal a6badbb7fa s/room_ephemeral/typing/ 2022-11-18 14:30:30 +00:00
Kegan Dougal 0b65b199e3 Add ExtensionRoomEphemeral tests 2022-11-18 11:49:02 +00:00
Kegan Dougal c1138bc085 sliding sync ext: add room ephemeral events 2022-11-18 11:41:58 +00:00
Michael Telatynski c0f7df8c3b Update eslint-plugin-matrix-org and improve visibilities & types (#2887) 2022-11-18 09:20:53 +00:00
renovate[bot] e085609572 Update jest monorepo to v29.2.3 (#2888)
* Update jest monorepo to v29.2.3

* Trigger CI

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Weimann <michaelw@matrix.org>
2022-11-18 07:40:07 +00:00
renovate[bot] 0a4f86a79e Update typescript-eslint monorepo to v5.43.0 (#2889)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-18 08:29:30 +01:00
renovate[bot] 5d6ff6c7f9 Update dependency jest-environment-jsdom to v29 (#2891)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-17 18:20:34 -07:00
Richard van der Hoff ffcdfe166e Improve logging on Olm session errors (#2885)
I strongly suspect we are logging "secure channel corruption" errors when no
such thing happened, bit I can't quite figure it out yet. Add a bit more
logging to try to track them down.
2022-11-16 17:22:04 +00:00
Richard van der Hoff e1aa7d335b Improve logging of e2ee messages (#2884)
Attempt to make the way we log megolm session ids more consistent.
2022-11-16 15:45:44 +00:00
kegsay 29643e745c Merge pull request #2883 from matrix-org/kegan/custom-room-subs
Define sliding sync consts
2022-11-16 14:41:22 +00:00
Kegan Dougal 54622ce424 Define msc3575 consts 2022-11-16 13:22:31 +00:00
Germain ca2ae24d46 Read receipt accumulation for threads (#2881) 2022-11-16 10:58:42 +00:00
106 changed files with 2700 additions and 1756 deletions
+9
View File
@@ -85,5 +85,14 @@ module.exports = {
// We use a `logger` intermediary module
"no-console": "error",
},
}, {
files: [
"spec/**/*.ts",
],
rules: {
// We don't need super strict typing in test utilities
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
},
}],
};
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
- name: 📥 Download artifact
uses: dawidd6/action-download-artifact@b12b127cf24433d14b4f93cee62f5465076ba82a # v2.24.1
uses: dawidd6/action-download-artifact@e6e25ac3a2b93187502a8be1ef9e9603afc34925 # v2.24.2
with:
workflow: static_analysis.yml
run_id: ${{ github.event.workflow_run.id }}
+36 -1
View File
@@ -66,9 +66,44 @@ jobs:
run: "yarn run gendoc"
- name: Upload Artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: docs
path: _docs
# We'll only use this in a workflow_run, then we're done with it
retention-days: 1
tsc-strict:
name: Typescript Strict Error Checker
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
pull-requests: read
checks: write
steps:
- uses: actions/checkout@v3
- name: Get diff lines
id: diff
uses: Equip-Collaboration/diff-line-numbers@v1.0.0
with:
include: '["\\.tsx?$"]'
- name: Detecting files changed
id: files
uses: futuratrepadeira/changed-files@v4.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pattern: '^.*\.tsx?$'
- uses: t3chguy/typescript-check-action@main
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
use-check: false
check-fail-mode: added
output-behaviour: annotate
ts-extra-args: '--noImplicitAny'
files-changed: ${{ steps.files.outputs.files_updated }}
files-added: ${{ steps.files.outputs.files_created }}
files-deleted: ${{ steps.files.outputs.files_deleted }}
line-numbers: ${{ steps.diff.outputs.lineNumbers }}
+9
View File
@@ -36,7 +36,16 @@ jobs:
id: cpu-cores
uses: SimenB/github-actions-cpu-cores@v1
- name: Run tests with coverage and 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
- name: Run tests with coverage
if: github.ref != 'refs/heads/develop'
run: |
yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }} ./spec/${{ matrix.specs }}
mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
+25
View File
@@ -1,3 +1,28 @@
Changes in [22.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v22.0.0) (2022-12-06)
==================================================================================================
## 🚨 BREAKING CHANGES
* Enable users to join group calls from multiple devices ([\#2902](https://github.com/matrix-org/matrix-js-sdk/pull/2902)).
## 🦖 Deprecations
* Deprecate a function containing a typo ([\#2904](https://github.com/matrix-org/matrix-js-sdk/pull/2904)).
## ✨ Features
* sliding sync: add receipts extension ([\#2912](https://github.com/matrix-org/matrix-js-sdk/pull/2912)).
* Define a spec support policy for the js-sdk ([\#2882](https://github.com/matrix-org/matrix-js-sdk/pull/2882)).
* Further improvements to e2ee logging ([\#2900](https://github.com/matrix-org/matrix-js-sdk/pull/2900)).
* sliding sync: add support for typing extension ([\#2893](https://github.com/matrix-org/matrix-js-sdk/pull/2893)).
* Improve logging on Olm session errors ([\#2885](https://github.com/matrix-org/matrix-js-sdk/pull/2885)).
* Improve logging of e2ee messages ([\#2884](https://github.com/matrix-org/matrix-js-sdk/pull/2884)).
## 🐛 Bug Fixes
* Fix 3pid invite acceptance not working due to mxid being sent in body ([\#2907](https://github.com/matrix-org/matrix-js-sdk/pull/2907)). Fixes vector-im/element-web#23823.
* Don't hang up calls that haven't started yet ([\#2898](https://github.com/matrix-org/matrix-js-sdk/pull/2898)).
* Read receipt accumulation for threads ([\#2881](https://github.com/matrix-org/matrix-js-sdk/pull/2881)).
* Make GroupCall work better with widgets ([\#2935](https://github.com/matrix-org/matrix-js-sdk/pull/2935)).
* Fix highlight notifications increasing when total notification is zero ([\#2937](https://github.com/matrix-org/matrix-js-sdk/pull/2937)). Fixes vector-im/element-web#23885.
* Fix synthesizeReceipt ([\#2916](https://github.com/matrix-org/matrix-js-sdk/pull/2916)). Fixes vector-im/element-web#23827 vector-im/element-web#23754 and vector-im/element-web#23847.
Changes in [21.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.2.0) (2022-11-22)
==================================================================================================
+8 -2
View File
@@ -9,8 +9,14 @@
Matrix Javascript SDK
=====================
This is the [Matrix](https://matrix.org) Client-Server r0 SDK for
JavaScript. This SDK can be run in a browser or in Node.js.
This is the [Matrix](https://matrix.org) Client-Server SDK for JavaScript and TypeScript. This SDK can be run in a
browser or in Node.js.
The Matrix specification is constantly evolving - while this SDK aims for maximum backwards compatibility, it only
guarantees that a feature will be supported for at least 4 spec releases. For example, if a feature the js-sdk supports
is removed in v1.4 then the feature is *eligible* for removal from the SDK when v1.8 is released. This SDK has no
guarantee on implementing all features of any particular spec release, currently. This can mean that the SDK will call
endpoints from before Matrix 1.1, for example.
Quickstart
==========
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "21.2.0",
"version": "22.0.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=16.0.0"
@@ -85,7 +85,7 @@
"@types/content-type": "^1.1.5",
"@types/domexception": "^4.0.0",
"@types/jest": "^29.0.0",
"@types/node": "16",
"@types/node": "18",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"allchange": "^1.0.6",
@@ -93,18 +93,18 @@
"babelify": "^10.0.0",
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"docdash": "^1.2.0",
"docdash": "^2.0.0",
"domexception": "^4.0.0",
"eslint": "8.26.0",
"eslint": "8.28.0",
"eslint-config-google": "^0.14.0",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-matrix-org": "^0.7.0",
"eslint-plugin-unicorn": "^44.0.2",
"eslint-plugin-matrix-org": "^0.8.0",
"eslint-plugin-unicorn": "^45.0.0",
"exorcist": "^2.0.0",
"fake-indexeddb": "^4.0.0",
"jest": "^29.0.0",
"jest-environment-jsdom": "^28.1.3",
"jest-environment-jsdom": "^29.0.0",
"jest-localstorage-mock": "^2.4.6",
"jest-mock": "^29.0.0",
"matrix-mock-request": "^2.5.0",
+3 -1
View File
@@ -173,7 +173,9 @@ describe("MatrixClient", function() {
signatures: {},
};
httpBackend!.when("POST", inviteSignUrl).respond(200, signature);
httpBackend!.when("POST", inviteSignUrl).check(request => {
expect(request.queryParams?.mxid).toEqual(client!.getUserId());
}).respond(200, signature);
httpBackend!.when("POST", "/join/" + encodeURIComponent(roomId)).check(request => {
expect(request.data.third_party_signed).toEqual(signature);
}).respond(200, { room_id: roomId });
+3 -3
View File
@@ -709,11 +709,11 @@ describe("MatrixClient syncing", () => {
const room = client!.getRoom(roomOne)!;
const stateAtStart = room.getLiveTimeline().getState(EventTimeline.BACKWARDS)!;
const startRoomNameEvent = stateAtStart.getStateEvents('m.room.name', '');
expect(startRoomNameEvent.getContent().name).toEqual('Old room name');
expect(startRoomNameEvent!.getContent().name).toEqual('Old room name');
const stateAtEnd = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const endRoomNameEvent = stateAtEnd.getStateEvents('m.room.name', '');
expect(endRoomNameEvent.getContent().name).toEqual('A new room name');
expect(endRoomNameEvent!.getContent().name).toEqual('A new room name');
});
});
@@ -1599,7 +1599,7 @@ describe("MatrixClient syncing", () => {
expect(room.roomId).toBe(roomOne);
expect(room.getMyMembership()).toBe("leave");
expect(room.name).toBe("Room Name");
expect(room.currentState.getStateEvents("m.room.name", "").getId()).toBe("$eventId");
expect(room.currentState.getStateEvents("m.room.name", "")?.getId()).toBe("$eventId");
expect(room.timeline[0].getContent().body).toBe("Message 1");
expect(room.timeline[1].getContent().body).toBe("Message 2");
client?.stopPeeking();
+205
View File
@@ -542,6 +542,7 @@ describe("SlidingSyncSdk", () => {
describe("ExtensionE2EE", () => {
let ext: Extension;
beforeAll(async () => {
await setupClient({
withCrypto: true,
@@ -551,18 +552,21 @@ describe("SlidingSyncSdk", () => {
await hasSynced;
ext = findExtension("e2ee");
});
afterAll(async () => {
// needed else we do some async operations in the background which can cause Jest to whine:
// "Cannot log after tests are done. Did you forget to wait for something async in your test?"
// Attempted to log "Saving device tracking data null"."
client!.crypto!.stop();
});
it("gets enabled on the initial request only", () => {
expect(ext.onRequest(true)).toEqual({
enabled: true,
});
expect(ext.onRequest(false)).toEqual(undefined);
});
it("can update device lists", () => {
ext.onResponse({
device_lists: {
@@ -572,6 +576,7 @@ describe("SlidingSyncSdk", () => {
});
// TODO: more assertions?
});
it("can update OTK counts", () => {
client!.crypto!.updateOneTimeKeyCount = jest.fn();
ext.onResponse({
@@ -588,6 +593,7 @@ describe("SlidingSyncSdk", () => {
});
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
});
it("can update fallback keys", () => {
ext.onResponse({
device_unused_fallback_key_types: ["signed_curve25519"],
@@ -599,8 +605,10 @@ describe("SlidingSyncSdk", () => {
expect(client!.crypto!.getNeedsNewFallback()).toEqual(true);
});
});
describe("ExtensionAccountData", () => {
let ext: Extension;
beforeAll(async () => {
await setupClient();
const hasSynced = sdk!.sync();
@@ -608,12 +616,14 @@ describe("SlidingSyncSdk", () => {
await hasSynced;
ext = findExtension("account_data");
});
it("gets enabled on the initial request only", () => {
expect(ext.onRequest(true)).toEqual({
enabled: true,
});
expect(ext.onRequest(false)).toEqual(undefined);
});
it("processes global account data", async () => {
const globalType = "global_test";
const globalContent = {
@@ -633,6 +643,7 @@ describe("SlidingSyncSdk", () => {
expect(globalData).toBeDefined();
expect(globalData.getContent()).toEqual(globalContent);
});
it("processes rooms account data", async () => {
const roomId = "!room:id";
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
@@ -667,6 +678,7 @@ describe("SlidingSyncSdk", () => {
expect(event).toBeDefined();
expect(event.getContent()).toEqual(roomContent);
});
it("doesn't crash for unknown room account data", async () => {
const unknownRoomId = "!unknown:id";
const roomType = "tester";
@@ -686,6 +698,7 @@ describe("SlidingSyncSdk", () => {
expect(room).toBeNull();
expect(client!.getAccountData(roomType)).toBeUndefined();
});
it("can update push rules via account data", async () => {
const roomId = "!foo:bar";
const pushRulesContent: IPushRules = {
@@ -718,8 +731,10 @@ describe("SlidingSyncSdk", () => {
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific]![0]);
});
});
describe("ExtensionToDevice", () => {
let ext: Extension;
beforeAll(async () => {
await setupClient();
const hasSynced = sdk!.sync();
@@ -727,12 +742,14 @@ describe("SlidingSyncSdk", () => {
await hasSynced;
ext = findExtension("to_device");
});
it("gets enabled with a limit on the initial request only", () => {
const reqJson: any = ext.onRequest(true);
expect(reqJson.enabled).toEqual(true);
expect(reqJson.limit).toBeGreaterThan(0);
expect(reqJson.since).toBeUndefined();
});
it("updates the since value", async () => {
ext.onResponse({
next_batch: "12345",
@@ -742,12 +759,14 @@ describe("SlidingSyncSdk", () => {
since: "12345",
});
});
it("can handle missing fields", async () => {
ext.onResponse({
next_batch: "23456",
// no events array
});
});
it("emits to-device events on the client", async () => {
const toDeviceType = "custom_test";
const toDeviceContent = {
@@ -770,6 +789,7 @@ describe("SlidingSyncSdk", () => {
});
expect(called).toBe(true);
});
it("can cancel key verification requests", async () => {
const seen: Record<string, boolean> = {};
client!.on(ClientEvent.ToDeviceEvent, (ev) => {
@@ -809,4 +829,189 @@ describe("SlidingSyncSdk", () => {
});
});
});
describe("ExtensionTyping", () => {
let ext: Extension;
beforeAll(async () => {
await setupClient();
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
ext = findExtension("typing");
});
it("gets enabled on the initial request only", () => {
expect(ext.onRequest(true)).toEqual({
enabled: true,
});
expect(ext.onRequest(false)).toEqual(undefined);
});
it("processes typing notifications", async () => {
const roomId = "!room:id";
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with typing",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
initial: true,
});
const room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
expect(room.getMember(selfUserId)?.typing).toEqual(false);
ext.onResponse({
rooms: {
[roomId]: {
type: EventType.Typing,
content: {
user_ids: [selfUserId],
},
},
},
});
expect(room.getMember(selfUserId)?.typing).toEqual(true);
ext.onResponse({
rooms: {
[roomId]: {
type: EventType.Typing,
content: {
user_ids: [],
},
},
},
});
expect(room.getMember(selfUserId)?.typing).toEqual(false);
});
it("gracefully handles missing rooms and members when typing", async () => {
const roomId = "!room:id";
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with typing",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
initial: true,
});
const room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
expect(room.getMember(selfUserId)?.typing).toEqual(false);
ext.onResponse({
rooms: {
[roomId]: {
type: EventType.Typing,
content: {
user_ids: ["@someone:else"],
},
},
},
});
expect(room.getMember(selfUserId)?.typing).toEqual(false);
ext.onResponse({
rooms: {
"!something:else": {
type: EventType.Typing,
content: {
user_ids: [selfUserId],
},
},
},
});
expect(room.getMember(selfUserId)?.typing).toEqual(false);
});
});
describe("ExtensionReceipts", () => {
let ext: Extension;
const generateReceiptResponse = (
userId: string, roomId: string, eventId: string, recType: string, ts: number,
) => {
return {
rooms: {
[roomId]: {
type: EventType.Receipt,
content: {
[eventId]: {
[recType]: {
[userId]: {
ts: ts,
},
},
},
},
},
},
};
};
beforeAll(async () => {
await setupClient();
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
ext = findExtension("receipts");
});
it("gets enabled on the initial request only", () => {
expect(ext.onRequest(true)).toEqual({
enabled: true,
});
expect(ext.onRequest(false)).toEqual(undefined);
});
it("processes receipts", async () => {
const roomId = "!room:id";
const alice = "@alice:alice";
const lastEvent = mkOwnEvent(EventType.RoomMessage, { body: "hello" });
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with receipts",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
{
type: EventType.RoomMember,
state_key: alice,
content: { membership: "join" },
sender: alice,
origin_server_ts: Date.now(),
event_id: "$alice",
},
lastEvent,
],
initial: true,
});
const room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
expect(room.getReadReceiptForUserId(alice, true)).toBeNull();
ext.onResponse(
generateReceiptResponse(alice, roomId, lastEvent.event_id, "m.read", 1234567),
);
const receipt = room.getReadReceiptForUserId(alice);
expect(receipt).toBeDefined();
expect(receipt?.eventId).toEqual(lastEvent.event_id);
expect(receipt?.data.ts).toEqual(1234567);
expect(receipt?.data.thread_id).toBeFalsy();
});
it("gracefully handles missing rooms when receiving receipts", async () => {
const roomId = "!room:id";
const alice = "@alice:alice";
const eventId = "$something";
ext.onResponse(
generateReceiptResponse(alice, roomId, eventId, "m.read", 1234567),
);
// we expect it not to crash
});
});
});
+100
View File
@@ -0,0 +1,100 @@
/*
Copyright 2022 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.
*/
/* eslint-disable no-console */
class JestSlowTestReporter {
constructor(globalConfig, options) {
this._globalConfig = globalConfig;
this._options = options;
this._slowTests = [];
this._slowTestSuites = [];
}
onRunComplete() {
const displayResult = (result, isTestSuite) => {
if (!isTestSuite) console.log();
result.sort((a, b) => b.duration - a.duration);
const rootPathRegex = new RegExp(`^${process.cwd()}`);
const slowestTests = result.slice(0, this._options.numTests || 10);
const slowTestTime = this._slowTestTime(slowestTests);
const allTestTime = this._allTestTime(result);
const percentTime = (slowTestTime / allTestTime) * 100;
if (isTestSuite) {
console.log(
`Top ${slowestTests.length} slowest test suites (${slowTestTime / 1000} seconds,` +
` ${percentTime.toFixed(1)}% of total time):`,
);
} else {
console.log(
`Top ${slowestTests.length} slowest tests (${slowTestTime / 1000} seconds,` +
` ${percentTime.toFixed(1)}% of total time):`,
);
}
for (let i = 0; i < slowestTests.length; i++) {
const duration = slowestTests[i].duration;
const filePath = slowestTests[i].filePath.replace(rootPathRegex, '.');
if (isTestSuite) {
console.log(` ${duration / 1000} seconds ${filePath}`);
} else {
const fullName = slowestTests[i].fullName;
console.log(` ${fullName}`);
console.log(` ${duration / 1000} seconds ${filePath}`);
}
}
console.log();
};
displayResult(this._slowTests);
displayResult(this._slowTestSuites, true);
}
onTestResult(test, testResult) {
this._slowTestSuites.push({
duration: testResult.perfStats.runtime,
filePath: testResult.testFilePath,
});
for (let i = 0; i < testResult.testResults.length; i++) {
this._slowTests.push({
duration: testResult.testResults[i].duration,
fullName: testResult.testResults[i].fullName,
filePath: testResult.testFilePath,
});
}
}
_slowTestTime(slowestTests) {
let slowTestTime = 0;
for (let i = 0; i < slowestTests.length; i++) {
slowTestTime += slowestTests[i].duration;
}
return slowTestTime;
}
_allTestTime(result) {
let allTestTime = 0;
for (let i = 0; i < result.length; i++) {
allTestTime += result[i].duration;
}
return allTestTime;
}
}
module.exports = JestSlowTestReporter;
+5 -1
View File
@@ -360,7 +360,7 @@ export class MockMediaDevices {
Promise.resolve(new MockMediaStream("local_stream").typed()),
);
public getDisplayMedia = jest.fn<Promise<MediaStream>, [DisplayMediaStreamConstraints]>().mockReturnValue(
public getDisplayMedia = jest.fn<Promise<MediaStream>, [MediaStreamConstraints]>().mockReturnValue(
Promise.resolve(new MockMediaStream("local_display_stream").typed()),
);
@@ -416,6 +416,9 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
public getRoom = jest.fn();
public supportsExperimentalThreads(): boolean { return true; }
public async decryptEventIfNeeded(): Promise<void> {}
public typed(): MatrixClient { return this as unknown as MatrixClient; }
public emitRoomState(event: MatrixEvent, state: RoomState): void {
@@ -431,6 +434,7 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
export class MockCallFeed {
constructor(
public userId: string,
public deviceId: string | undefined,
public stream: MockMediaStream,
) {}
+2 -2
View File
@@ -179,7 +179,7 @@ describe("RoomWidgetClient", () => {
// It should've also inserted the event into the room object
const room = client.getRoom("!1:example.org");
expect(room).not.toBeNull();
expect(room!.currentState.getStateEvents("org.example.foo", "bar").getEffectiveEvent()).toEqual(event);
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
});
it("backfills", async () => {
@@ -195,7 +195,7 @@ describe("RoomWidgetClient", () => {
const room = client.getRoom("!1:example.org");
expect(room).not.toBeNull();
expect(room!.currentState.getStateEvents("org.example.foo", "bar").getEffectiveEvent()).toEqual(event);
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
});
});
+22 -2
View File
@@ -37,7 +37,7 @@ let event: MatrixEvent;
let threadEvent: MatrixEvent;
const ROOM_ID = "!roomId:example.org";
let THREAD_ID;
let THREAD_ID: string;
function mkPushAction(notify, highlight): IActionsObject {
return {
@@ -76,7 +76,7 @@ describe("fixNotificationCountOnDecryption", () => {
event: true,
}, mockClient);
THREAD_ID = event.getId();
THREAD_ID = event.getId()!;
threadEvent = mkEvent({
type: EventType.RoomMessage,
content: {
@@ -108,6 +108,16 @@ describe("fixNotificationCountOnDecryption", () => {
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
});
it("does not change the room count when there's no unread count", () => {
room.setUnreadNotificationCount(NotificationCountType.Total, 0);
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
fixNotificationCountOnDecryption(mockClient, event);
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(1);
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
});
it("changes the thread count to highlight on decryption", () => {
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1);
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
@@ -118,6 +128,16 @@ describe("fixNotificationCountOnDecryption", () => {
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(1);
});
it("does not change the room count when there's no unread count", () => {
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
fixNotificationCountOnDecryption(mockClient, event);
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
});
it("emits events", () => {
const cb = jest.fn();
room.on(RoomEvent.UnreadNotifications, cb);
+18 -2
View File
@@ -16,11 +16,11 @@ limitations under the License.
import MockHttpBackend from 'matrix-mock-request';
import { ReceiptType } from '../../src/@types/read_receipts';
import { MAIN_ROOM_TIMELINE, ReceiptType } from '../../src/@types/read_receipts';
import { MatrixClient } from "../../src/client";
import { Feature, ServerSupport } from '../../src/feature';
import { EventType } from '../../src/matrix';
import { MAIN_ROOM_TIMELINE } from '../../src/models/read-receipt';
import { synthesizeReceipt } from '../../src/models/read-receipt';
import { encodeUri } from '../../src/utils';
import * as utils from "../test-utils/test-utils";
@@ -176,4 +176,20 @@ describe("Read receipt", () => {
await flushPromises();
});
});
describe("synthesizeReceipt", () => {
it.each([
{ event: roomEvent, destinationId: MAIN_ROOM_TIMELINE },
{ event: threadEvent, destinationId: threadEvent.threadRootId! },
])("adds the receipt to $destinationId", ({ event, destinationId }) => {
const userId = "@bob:example.org";
const receiptType = ReceiptType.Read;
const fakeReadReceipt = synthesizeReceipt(userId, event, receiptType);
const content = fakeReadReceipt.getContent()[event.getId()!][receiptType][userId];
expect(content.thread_id).toEqual(destinationId);
});
});
});
+1 -1
View File
@@ -152,7 +152,7 @@ describe("RoomState", function() {
it("should return a single MatrixEvent if a state_key was specified",
function() {
const event = state.getStateEvents("m.room.member", userA);
expect(event.getContent()).toMatchObject({
expect(event?.getContent()).toMatchObject({
membership: "join",
});
});
+1 -2
View File
@@ -38,9 +38,8 @@ import { RoomState } from "../../src/models/room-state";
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
import { TestClient } from "../TestClient";
import { emitPromise } from "../test-utils/test-utils";
import { ReceiptType } from "../../src/@types/read_receipts";
import { ReceiptType, WrappedReceipt } from "../../src/@types/read_receipts";
import { FeatureSupport, Thread, ThreadEvent, THREAD_RELATION_TYPE } from "../../src/models/thread";
import { WrappedReceipt } from "../../src/models/read-receipt";
import { Crypto } from "../../src/crypto";
describe("Room", function() {
+57
View File
@@ -364,6 +364,63 @@ describe("SyncAccumulator", function() {
});
});
it("should accumulate threaded read receipts", () => {
const receipt1 = {
type: "m.receipt",
room_id: "!foo:bar",
content: {
"$event1:localhost": {
[ReceiptType.Read]: {
"@alice:localhost": { ts: 1, thread_id: "main" },
},
},
},
};
const receipt2 = {
type: "m.receipt",
room_id: "!foo:bar",
content: {
"$event2:localhost": {
[ReceiptType.Read]: {
"@alice:localhost": { ts: 2, thread_id: "$123" }, // does not clobbers event1 receipt
},
},
},
};
sa.accumulate(syncSkeleton({
ephemeral: {
events: [receipt1],
},
}));
sa.accumulate(syncSkeleton({
ephemeral: {
events: [receipt2],
},
}));
expect(
sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events.length,
).toEqual(1);
expect(
sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events[0],
).toEqual({
type: "m.receipt",
room_id: "!foo:bar",
content: {
"$event1:localhost": {
[ReceiptType.Read]: {
"@alice:localhost": { ts: 1, thread_id: "main" },
},
},
"$event2:localhost": {
[ReceiptType.Read]: {
"@alice:localhost": { ts: 2, thread_id: "$123" },
},
},
},
});
});
describe("summary field", function() {
function createSyncResponseWithSummary(summary) {
return {
+19 -14
View File
@@ -81,10 +81,13 @@ const fakeIncomingCall = async (client: TestClient, call: MatrixCall, version: s
call.getFeeds().push(new CallFeed({
client: client.client,
userId: "remote_user_id",
// @ts-ignore Mock
stream: new MockMediaStream("remote_stream_id", [new MockMediaStreamTrack("remote_tack_id")]),
id: "remote_feed_id",
deviceId: undefined,
stream: new MockMediaStream(
"remote_stream_id", [new MockMediaStreamTrack("remote_tack_id", "audio")],
) as unknown as MediaStream,
purpose: SDPStreamMetadataPurpose.Usermedia,
audioMuted: false,
videoMuted: false,
}));
await callPromise;
};
@@ -447,7 +450,7 @@ describe('Call', function() {
client.client.getRoom = () => {
return {
getMember: (userId) => {
getMember: (userId: string) => {
if (userId === opponentMember.userId) {
return opponentMember;
}
@@ -521,10 +524,12 @@ describe('Call', function() {
it("should correctly generate local SDPStreamMetadata", async () => {
const callPromise = call.placeCallWithCallFeeds([new CallFeed({
client: client.client,
// @ts-ignore Mock
stream: new MockMediaStream("local_stream1", [new MockMediaStreamTrack("track_id", "audio")]),
stream: new MockMediaStream(
"local_stream1", [new MockMediaStreamTrack("track_id", "audio")],
) as unknown as MediaStream,
roomId: call.roomId,
userId: client.getUserId(),
deviceId: undefined,
purpose: SDPStreamMetadataPurpose.Usermedia,
audioMuted: false,
videoMuted: false,
@@ -534,8 +539,10 @@ describe('Call', function() {
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
(call as any).pushNewLocalFeed(
new MockMediaStream("local_stream2", [new MockMediaStreamTrack("track_id", "video")]),
SDPStreamMetadataPurpose.Screenshare, "feed_id2",
new MockMediaStream(
"local_stream2", [new MockMediaStreamTrack("track_id", "video")],
) as unknown as MediaStream,
SDPStreamMetadataPurpose.Screenshare,
);
await call.setMicrophoneMuted(true);
@@ -563,20 +570,18 @@ describe('Call', function() {
new CallFeed({
client: client.client,
userId: client.getUserId(),
// @ts-ignore Mock
stream: localUsermediaStream,
deviceId: undefined,
stream: localUsermediaStream as unknown as MediaStream,
purpose: SDPStreamMetadataPurpose.Usermedia,
id: "local_usermedia_feed_id",
audioMuted: false,
videoMuted: false,
}),
new CallFeed({
client: client.client,
userId: client.getUserId(),
// @ts-ignore Mock
stream: localScreensharingStream,
deviceId: undefined,
stream: localScreensharingStream as unknown as MediaStream,
purpose: SDPStreamMetadataPurpose.Screenshare,
id: "local_screensharing_feed_id",
audioMuted: false,
videoMuted: false,
}),
+303 -111
View File
@@ -23,8 +23,9 @@ import {
Room,
RoomMember,
} from '../../../src';
import { RoomStateEvent } from "../../../src/models/room-state";
import { GroupCall, GroupCallEvent, GroupCallState } from "../../../src/webrtc/groupCall";
import { MatrixClient } from "../../../src/client";
import { IMyDevice, MatrixClient } from "../../../src/client";
import {
installWebRTCMocks,
MockCallFeed,
@@ -53,40 +54,57 @@ const FAKE_USER_ID_3 = "@charlie:test.dummy";
const FAKE_STATE_EVENTS = [
{
getContent: () => ({
["m.expires_ts"]: Date.now() + ONE_HOUR,
"m.calls": [],
}),
getStateKey: () => FAKE_USER_ID_1,
getRoomId: () => FAKE_ROOM_ID,
getTs: () => 0,
},
{
getContent: () => ({
["m.expires_ts"]: Date.now() + ONE_HOUR,
["m.calls"]: [{
["m.call_id"]: FAKE_CONF_ID,
["m.devices"]: [{
"m.calls": [{
"m.call_id": FAKE_CONF_ID,
"m.devices": [{
device_id: FAKE_DEVICE_ID_2,
session_id: FAKE_SESSION_ID_2,
expires_ts: Date.now() + ONE_HOUR,
feeds: [],
}],
}],
}),
getStateKey: () => FAKE_USER_ID_2,
getRoomId: () => FAKE_ROOM_ID,
getTs: () => 0,
}, {
getContent: () => ({
["m.expires_ts"]: Date.now() + ONE_HOUR,
["m.calls"]: [{
["m.call_id"]: FAKE_CONF_ID,
["m.devices"]: [{
"m.expires_ts": Date.now() + ONE_HOUR,
"m.calls": [{
"m.call_id": FAKE_CONF_ID,
"m.devices": [{
device_id: "user3_device",
session_id: "user3_session",
expires_ts: Date.now() + ONE_HOUR,
feeds: [],
}],
}],
}),
getStateKey: () => "user3",
getRoomId: () => FAKE_ROOM_ID,
getTs: () => 0,
},
];
const mockGetStateEvents = (type: EventType, userId?: string): MatrixEvent[] | MatrixEvent | null => {
if (type === EventType.GroupCallMemberPrefix) {
return userId === undefined
? FAKE_STATE_EVENTS as MatrixEvent[]
: FAKE_STATE_EVENTS.find(e => e.getStateKey() === userId) as MatrixEvent;
} else {
const fakeEvent = { getContent: () => ({}), getTs: () => 0 } as MatrixEvent;
return userId === undefined ? [fakeEvent] : fakeEvent;
}
};
const ONE_HOUR = 1000 * 60 * 60;
const createAndEnterGroupCall = async (cli: MatrixClient, room: Room): Promise<GroupCall> => {
@@ -111,6 +129,8 @@ class MockCall {
public state = CallState.Ringing;
public opponentUserId = FAKE_USER_ID_1;
public opponentDeviceId = FAKE_DEVICE_ID_1;
public opponentMember = { userId: this.opponentUserId };
public callId = "1";
public localUsermediaFeed = {
setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(),
@@ -129,9 +149,11 @@ class MockCall {
public removeListener = jest.fn();
public getOpponentMember(): Partial<RoomMember> {
return {
userId: this.opponentUserId,
};
return this.opponentMember;
}
public getOpponentDeviceId(): string {
return this.opponentDeviceId;
}
public typed(): MatrixCall { return this as unknown as MatrixCall; }
@@ -158,13 +180,13 @@ describe('Group Call', function() {
room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
groupCall = new GroupCall(mockClient, room, GroupCallType.Video, false, GroupCallIntent.Prompt);
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
membership: "join",
} as unknown as RoomMember;
});
it("does not initialize local call feed, if it already is", async () => {
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
} as unknown as RoomMember;
await groupCall.initLocalCallFeed();
jest.spyOn(groupCall, "initLocalCallFeed");
await groupCall.enter();
@@ -194,10 +216,6 @@ describe('Group Call', function() {
});
it("sends member state event to room on enter", async () => {
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
} as unknown as RoomMember;
await groupCall.create();
try {
@@ -227,10 +245,6 @@ describe('Group Call', function() {
});
it("sends member state event to room on leave", async () => {
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
} as unknown as RoomMember;
await groupCall.create();
await groupCall.enter();
mockSendState.mockClear();
@@ -245,6 +259,18 @@ describe('Group Call', function() {
);
});
it("includes local device in participants when entered via another session", async () => {
const hasLocalParticipant = () => groupCall.participants.get(
room.getMember(mockClient.getUserId()!)!,
)?.has(mockClient.getDeviceId()!) ?? false;
expect(groupCall.enteredViaAnotherSession).toBe(false);
expect(hasLocalParticipant()).toBe(false);
groupCall.enteredViaAnotherSession = true;
expect(hasLocalParticipant()).toBe(true);
});
it("starts with mic unmuted in regular calls", async () => {
try {
await groupCall.create();
@@ -326,8 +352,8 @@ describe('Group Call', function() {
describe("call feeds changing", () => {
let call: MockCall;
const currentFeed = new MockCallFeed(FAKE_USER_ID_1, new MockMediaStream("current"));
const newFeed = new MockCallFeed(FAKE_USER_ID_1, new MockMediaStream("new"));
const currentFeed = new MockCallFeed(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, new MockMediaStream("current"));
const newFeed = new MockCallFeed(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, new MockMediaStream("new"));
beforeEach(async () => {
jest.spyOn(currentFeed, "dispose");
@@ -358,7 +384,7 @@ describe('Group Call', function() {
});
it("replaces usermedia feed", async () => {
groupCall.userMediaFeeds = [currentFeed.typed()];
groupCall.userMediaFeeds.push(currentFeed.typed());
call.remoteUsermediaFeed = newFeed.typed();
// @ts-ignore Mock
@@ -368,7 +394,7 @@ describe('Group Call', function() {
});
it("removes usermedia feed", async () => {
groupCall.userMediaFeeds = [currentFeed.typed()];
groupCall.userMediaFeeds.push(currentFeed.typed());
// @ts-ignore Mock
groupCall.onCallFeedsChanged(call);
@@ -387,7 +413,7 @@ describe('Group Call', function() {
});
it("replaces screenshare feed", async () => {
groupCall.screenshareFeeds = [currentFeed.typed()];
groupCall.screenshareFeeds.push(currentFeed.typed());
call.remoteScreensharingFeed = newFeed.typed();
// @ts-ignore Mock
@@ -397,7 +423,7 @@ describe('Group Call', function() {
});
it("removes screenshare feed", async () => {
groupCall.screenshareFeeds = [currentFeed.typed()];
groupCall.screenshareFeeds.push(currentFeed.typed());
// @ts-ignore Mock
groupCall.onCallFeedsChanged(call);
@@ -408,7 +434,7 @@ describe('Group Call', function() {
describe("feed replacing", () => {
it("replaces usermedia feed", async () => {
groupCall.userMediaFeeds = [currentFeed.typed()];
groupCall.userMediaFeeds.push(currentFeed.typed());
// @ts-ignore Mock
groupCall.replaceUserMediaFeed(currentFeed, newFeed);
@@ -422,7 +448,7 @@ describe('Group Call', function() {
});
it("replaces screenshare feed", async () => {
groupCall.screenshareFeeds = [currentFeed.typed()];
groupCall.screenshareFeeds.push(currentFeed.typed());
// @ts-ignore Mock
groupCall.replaceScreenshareFeed(currentFeed, newFeed);
@@ -489,7 +515,10 @@ describe('Group Call', function() {
it("sends metadata updates before unmuting in PTT mode", async () => {
const mockCall = new MockCall(FAKE_ROOM_ID, groupCall.groupCallId);
groupCall.calls.push(mockCall as unknown as MatrixCall);
groupCall.calls.set(
mockCall.getOpponentMember() as RoomMember,
new Map([[mockCall.getOpponentDeviceId(), mockCall.typed()]]),
);
let metadataUpdateResolve: () => void;
const metadataUpdatePromise = new Promise<void>(resolve => {
@@ -511,7 +540,10 @@ describe('Group Call', function() {
it("sends metadata updates after muting in PTT mode", async () => {
const mockCall = new MockCall(FAKE_ROOM_ID, groupCall.groupCallId);
groupCall.calls.push(mockCall as unknown as MatrixCall);
groupCall.calls.set(
mockCall.getOpponentMember() as RoomMember,
new Map([[mockCall.getOpponentDeviceId(), mockCall.typed()]]),
);
// the call starts muted, so unmute to get in the right state to test
await groupCall.setMicrophoneMuted(false);
@@ -560,7 +592,7 @@ describe('Group Call', function() {
if (eventType === EventType.GroupCallMemberPrefix) {
const fakeEvent = {
getContent: () => content,
getRoomId: () => FAKE_ROOM_ID,
getRoomId: () => roomId,
getStateKey: () => statekey,
} as unknown as MatrixEvent;
@@ -574,8 +606,8 @@ describe('Group Call', function() {
// just add it once.
subMap.set(statekey, fakeEvent);
groupCall1.onMemberStateChanged(fakeEvent);
groupCall2.onMemberStateChanged(fakeEvent);
client1Room.currentState.emit(RoomStateEvent.Update, client1Room.currentState);
client2Room.currentState.emit(RoomStateEvent.Update, client2Room.currentState);
}
return Promise.resolve({ "event_id": "foo" });
};
@@ -584,9 +616,17 @@ describe('Group Call', function() {
client2.sendStateEvent.mockImplementation(fakeSendStateEvents);
const client1Room = new Room(FAKE_ROOM_ID, client1.typed(), FAKE_USER_ID_1);
const client2Room = new Room(FAKE_ROOM_ID, client2.typed(), FAKE_USER_ID_2);
client1Room.currentState.members[FAKE_USER_ID_1] = client2Room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
membership: "join",
} as unknown as RoomMember;
client1Room.currentState.members[FAKE_USER_ID_2] = client2Room.currentState.members[FAKE_USER_ID_2] = {
userId: FAKE_USER_ID_2,
membership: "join",
} as unknown as RoomMember;
groupCall1 = new GroupCall(
client1.typed(), client1Room, GroupCallType.Video, false, GroupCallIntent.Prompt, FAKE_CONF_ID,
);
@@ -594,20 +634,6 @@ describe('Group Call', function() {
groupCall2 = new GroupCall(
client2.typed(), client2Room, GroupCallType.Video, false, GroupCallIntent.Prompt, FAKE_CONF_ID,
);
client1Room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
} as unknown as RoomMember;
client1Room.currentState.members[FAKE_USER_ID_2] = {
userId: FAKE_USER_ID_2,
} as unknown as RoomMember;
client2Room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
} as unknown as RoomMember;
client2Room.currentState.members[FAKE_USER_ID_2] = {
userId: FAKE_USER_ID_2,
} as unknown as RoomMember;
});
afterEach(function() {
@@ -672,8 +698,10 @@ describe('Group Call', function() {
expect(client1.sendToDevice).toHaveBeenCalled();
const oldCall = groupCall1.getCallByUserId(client2.userId);
oldCall!.emit(CallEvent.Hangup, oldCall!);
const oldCall = groupCall1.calls.get(
groupCall1.room.getMember(client2.userId)!,
)!.get(client2.deviceId)!;
oldCall.emit(CallEvent.Hangup, oldCall!);
client1.sendToDevice.mockClear();
@@ -691,9 +719,11 @@ describe('Group Call', function() {
// to even be created...
let newCall: MatrixCall | undefined;
while (
(newCall = groupCall1.getCallByUserId(client2.userId)) === undefined ||
newCall.peerConn === undefined ||
newCall.callId == oldCall!.callId
(newCall = groupCall1.calls.get(
groupCall1.room.getMember(client2.userId)!,
)?.get(client2.deviceId)) === undefined
|| newCall.peerConn === undefined
|| newCall.callId == oldCall.callId
) {
await flushPromises();
}
@@ -733,7 +763,9 @@ describe('Group Call', function() {
groupCall1.setMicrophoneMuted(false);
groupCall1.setLocalVideoMuted(false);
const call = groupCall1.getCallByUserId(client2.userId)!;
const call = groupCall1.calls.get(
groupCall1.room.getMember(client2.userId)!,
)!.get(client2.deviceId)!;
call.isMicrophoneMuted = jest.fn().mockReturnValue(true);
call.setMicrophoneMuted = jest.fn();
call.isLocalVideoMuted = jest.fn().mockReturnValue(true);
@@ -760,12 +792,15 @@ describe('Group Call', function() {
mockClient = typedMockClient as unknown as MatrixClient;
room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
room.currentState.getStateEvents = jest.fn().mockImplementation((type: EventType, userId: string) => {
return type === EventType.GroupCallMemberPrefix
? FAKE_STATE_EVENTS.find(e => e.getStateKey() === userId) || FAKE_STATE_EVENTS
: { getContent: () => ([]) };
});
room.getMember = jest.fn().mockImplementation((userId) => ({ userId }));
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents);
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
membership: "join",
} as unknown as RoomMember;
room.currentState.members[FAKE_USER_ID_2] = {
userId: FAKE_USER_ID_2,
membership: "join",
} as unknown as RoomMember;
});
describe("local muting", () => {
@@ -773,17 +808,13 @@ describe('Group Call', function() {
const groupCall = await createAndEnterGroupCall(mockClient, room);
groupCall.localCallFeed!.setAudioVideoMuted = jest.fn();
const setAVMutedArray = groupCall.calls.map(call => {
call.localUsermediaFeed!.setAudioVideoMuted = jest.fn();
return call.localUsermediaFeed!.setAudioVideoMuted;
});
const tracksArray = groupCall.calls.reduce((acc: MediaStreamTrack[], call: MatrixCall) => {
acc.push(...call.localUsermediaStream!.getAudioTracks());
return acc;
}, []);
const sendMetadataUpdateArray = groupCall.calls.map(call => {
call.sendMetadataUpdate = jest.fn();
return call.sendMetadataUpdate;
const setAVMutedArray: ((audioMuted: boolean | null, videoMuted: boolean | null) => void)[] = [];
const tracksArray: MediaStreamTrack[] = [];
const sendMetadataUpdateArray: (() => Promise<void>)[] = [];
groupCall.forEachCall(call => {
setAVMutedArray.push(call.localUsermediaFeed!.setAudioVideoMuted = jest.fn());
tracksArray.push(...call.localUsermediaStream!.getAudioTracks());
sendMetadataUpdateArray.push(call.sendMetadataUpdate = jest.fn());
});
await groupCall.setMicrophoneMuted(true);
@@ -801,18 +832,14 @@ describe('Group Call', function() {
const groupCall = await createAndEnterGroupCall(mockClient, room);
groupCall.localCallFeed!.setAudioVideoMuted = jest.fn();
const setAVMutedArray = groupCall.calls.map(call => {
call.localUsermediaFeed!.setAudioVideoMuted = jest.fn();
const setAVMutedArray: ((audioMuted: boolean | null, videoMuted: boolean | null) => void)[] = [];
const tracksArray: MediaStreamTrack[] = [];
const sendMetadataUpdateArray: (() => Promise<void>)[] = [];
groupCall.forEachCall(call => {
call.localUsermediaFeed!.isVideoMuted = jest.fn().mockReturnValue(true);
return call.localUsermediaFeed!.setAudioVideoMuted;
});
const tracksArray = groupCall.calls.reduce((acc: MediaStreamTrack[], call: MatrixCall) => {
acc.push(...call.localUsermediaStream!.getVideoTracks());
return acc;
}, []);
const sendMetadataUpdateArray = groupCall.calls.map(call => {
call.sendMetadataUpdate = jest.fn();
return call.sendMetadataUpdate;
setAVMutedArray.push(call.localUsermediaFeed!.setAudioVideoMuted = jest.fn());
tracksArray.push(...call.localUsermediaStream!.getVideoTracks());
sendMetadataUpdateArray.push(call.sendMetadataUpdate = jest.fn());
});
await groupCall.setLocalVideoMuted(true);
@@ -847,7 +874,7 @@ describe('Group Call', function() {
// It takes a bit of time for the calls to get created
await sleep(10);
const call = groupCall.calls[0];
const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!;
call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember;
// @ts-ignore Mock
call.pushRemoteFeed(new MockMediaStream("stream", [
@@ -856,7 +883,7 @@ describe('Group Call', function() {
]));
call.onSDPStreamMetadataChangedReceived(metadataEvent);
const feed = groupCall.getUserMediaFeedByUserId(call.invitee!);
const feed = groupCall.getUserMediaFeed(call.invitee!, call.getOpponentDeviceId()!);
expect(feed!.isAudioMuted()).toBe(true);
expect(feed!.isVideoMuted()).toBe(false);
@@ -870,7 +897,7 @@ describe('Group Call', function() {
// It takes a bit of time for the calls to get created
await sleep(10);
const call = groupCall.calls[0];
const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!;
call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember;
// @ts-ignore Mock
call.pushRemoteFeed(new MockMediaStream("stream", [
@@ -879,7 +906,7 @@ describe('Group Call', function() {
]));
call.onSDPStreamMetadataChangedReceived(metadataEvent);
const feed = groupCall.getUserMediaFeedByUserId(call.invitee!);
const feed = groupCall.getUserMediaFeed(call.invitee!, call.getOpponentDeviceId()!);
expect(feed!.isAudioMuted()).toBe(false);
expect(feed!.isVideoMuted()).toBe(true);
@@ -945,12 +972,16 @@ describe('Group Call', function() {
expect(mockCall.reject).not.toHaveBeenCalled();
expect(mockCall.answerWithCallFeeds).toHaveBeenCalled();
expect(groupCall.calls).toEqual([mockCall]);
expect(groupCall.calls).toEqual(new Map([[
groupCall.room.getMember(FAKE_USER_ID_1)!,
new Map([[FAKE_DEVICE_ID_1, mockCall]]),
]]));
});
it("replaces calls if it already has one with the same user", async () => {
const oldMockCall = new MockCall(room.roomId, groupCall.groupCallId);
const newMockCall = new MockCall(room.roomId, groupCall.groupCallId);
newMockCall.opponentMember = oldMockCall.opponentMember; // Ensure referential equality
newMockCall.callId = "not " + oldMockCall.callId;
mockClient.emit(CallEventHandlerEvent.Incoming, oldMockCall as unknown as MatrixCall);
@@ -958,7 +989,10 @@ describe('Group Call', function() {
expect(oldMockCall.hangup).toHaveBeenCalled();
expect(newMockCall.answerWithCallFeeds).toHaveBeenCalled();
expect(groupCall.calls).toEqual([newMockCall]);
expect(groupCall.calls).toEqual(new Map([[
groupCall.room.getMember(FAKE_USER_ID_1)!,
new Map([[FAKE_DEVICE_ID_1, newMockCall]]),
]]));
});
it("starts to process incoming calls when we've entered", async () => {
@@ -988,32 +1022,34 @@ describe('Group Call', function() {
mockClient = typedMockClient.typed();
room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
room.getMember = jest.fn().mockImplementation((userId) => ({ userId }));
room.currentState.getStateEvents = jest.fn().mockImplementation((type: EventType, userId: string) => {
return type === EventType.GroupCallMemberPrefix
? FAKE_STATE_EVENTS.find(e => e.getStateKey() === userId) || FAKE_STATE_EVENTS
: { getContent: () => ([]) };
});
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
membership: "join",
} as unknown as RoomMember;
room.currentState.members[FAKE_USER_ID_2] = {
userId: FAKE_USER_ID_2,
membership: "join",
} as unknown as RoomMember;
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents);
groupCall = await createAndEnterGroupCall(mockClient, room);
});
it("sending screensharing stream", async () => {
const onNegotiationNeededArray = groupCall.calls.map(call => {
const onNegotiationNeededArray: (() => Promise<void>)[] = [];
groupCall.forEachCall(call => {
// @ts-ignore Mock
call.gotLocalOffer = jest.fn();
// @ts-ignore Mock
return call.gotLocalOffer;
onNegotiationNeededArray.push(call.gotLocalOffer = jest.fn());
});
let enabledResult;
let enabledResult: boolean;
enabledResult = await groupCall.setScreensharingEnabled(true);
expect(enabledResult).toEqual(true);
expect(typedMockClient.mediaHandler.getScreensharingStream).toHaveBeenCalled();
MockRTCPeerConnection.triggerAllNegotiations();
expect(groupCall.screenshareFeeds).toHaveLength(1);
groupCall.calls.forEach(c => {
groupCall.forEachCall(c => {
expect(c.getLocalFeeds().find(f => f.purpose === SDPStreamMetadataPurpose.Screenshare)).toBeDefined();
});
onNegotiationNeededArray.forEach(f => expect(f).toHaveBeenCalled());
@@ -1036,7 +1072,7 @@ describe('Group Call', function() {
// It takes a bit of time for the calls to get created
await sleep(10);
const call = groupCall.calls[0];
const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!;
call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember;
call.onNegotiateReceived({
getContent: () => ({
@@ -1057,7 +1093,7 @@ describe('Group Call', function() {
]));
expect(groupCall.screenshareFeeds).toHaveLength(1);
expect(groupCall.getScreenshareFeedByUserId(call.invitee!)).toBeDefined();
expect(groupCall.getScreenshareFeed(call.invitee!, call.getOpponentDeviceId()!)).toBeDefined();
groupCall.terminate();
});
@@ -1097,12 +1133,16 @@ describe('Group Call', function() {
);
room = new Room(FAKE_ROOM_ID, mockClient.typed(), FAKE_USER_ID_1);
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
} as unknown as RoomMember;
groupCall = await createAndEnterGroupCall(mockClient.typed(), room);
mediaFeed1 = new CallFeed({
client: mockClient.typed(),
roomId: FAKE_ROOM_ID,
userId: FAKE_USER_ID_2,
deviceId: FAKE_DEVICE_ID_1,
stream: (new MockMediaStream("foo", [])).typed(),
purpose: SDPStreamMetadataPurpose.Usermedia,
audioMuted: false,
@@ -1114,6 +1154,7 @@ describe('Group Call', function() {
client: mockClient.typed(),
roomId: FAKE_ROOM_ID,
userId: FAKE_USER_ID_3,
deviceId: FAKE_DEVICE_ID_1,
stream: (new MockMediaStream("foo", [])).typed(),
purpose: SDPStreamMetadataPurpose.Usermedia,
audioMuted: false,
@@ -1136,15 +1177,15 @@ describe('Group Call', function() {
mediaFeed2.speakingVolumeSamples = [0, 0];
jest.runOnlyPendingTimers();
expect(groupCall.activeSpeaker).toEqual(FAKE_USER_ID_2);
expect(onActiveSpeakerEvent).toHaveBeenCalledWith(FAKE_USER_ID_2);
expect(groupCall.activeSpeaker).toEqual(mediaFeed1);
expect(onActiveSpeakerEvent).toHaveBeenCalledWith(mediaFeed1);
mediaFeed1.speakingVolumeSamples = [0, 0];
mediaFeed2.speakingVolumeSamples = [100, 100];
jest.runOnlyPendingTimers();
expect(groupCall.activeSpeaker).toEqual(FAKE_USER_ID_3);
expect(onActiveSpeakerEvent).toHaveBeenCalledWith(FAKE_USER_ID_3);
expect(groupCall.activeSpeaker).toEqual(mediaFeed2);
expect(onActiveSpeakerEvent).toHaveBeenCalledWith(mediaFeed2);
});
});
@@ -1233,4 +1274,155 @@ describe('Group Call', function() {
});
});
});
describe("cleaning member state", () => {
const bobWeb: IMyDevice = {
device_id: "bobweb",
last_seen_ts: 0,
};
const bobDesktop: IMyDevice = {
device_id: "bobdesktop",
last_seen_ts: 0,
};
const bobDesktopOffline: IMyDevice = {
device_id: "bobdesktopoffline",
last_seen_ts: 1000 * 60 * 60 * -2, // 2 hours ago
};
const bobDesktopNeverOnline: IMyDevice = {
device_id: "bobdesktopneveronline",
};
const mkContent = (devices: IMyDevice[]) => ({
"m.calls": [{
"m.call_id": groupCall.groupCallId,
"m.devices": devices.map(d => ({
device_id: d.device_id, session_id: "1", feeds: [], expires_ts: 1000 * 60 * 10,
})),
}],
});
const expectDevices = (devices: IMyDevice[]) => expect(
room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, FAKE_USER_ID_2)?.getContent(),
).toEqual({
"m.calls": [{
"m.call_id": groupCall.groupCallId,
"m.devices": devices.map(d => ({
device_id: d.device_id, session_id: "1", feeds: [], expires_ts: expect.any(Number),
})),
}],
});
let mockClient: MatrixClient;
let room: Room;
let groupCall: GroupCall;
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
});
afterAll(() => jest.useRealTimers());
beforeEach(async () => {
const typedMockClient = new MockCallMatrixClient(
FAKE_USER_ID_2, bobWeb.device_id, FAKE_SESSION_ID_2,
);
jest.spyOn(typedMockClient, "sendStateEvent").mockImplementation(
async (roomId, eventType, content, stateKey) => {
const eventId = `$${Math.random()}`;
if (roomId === room.roomId) {
room.addLiveEvents([new MatrixEvent({
event_id: eventId,
type: eventType,
room_id: roomId,
sender: FAKE_USER_ID_2,
content,
state_key: stateKey,
})]);
}
return { event_id: eventId };
},
);
mockClient = typedMockClient as unknown as MatrixClient;
room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_2);
room.getMember = jest.fn().mockImplementation((userId) => ({ userId }));
groupCall = new GroupCall(
mockClient,
room,
GroupCallType.Video,
false,
GroupCallIntent.Prompt,
FAKE_CONF_ID,
);
await groupCall.create();
mockClient.getDevices = async () => ({
devices: [
bobWeb,
bobDesktop,
bobDesktopOffline,
bobDesktopNeverOnline,
],
});
});
afterEach(() => groupCall.leave());
it("doesn't clean up valid devices", async () => {
await groupCall.enter();
await mockClient.sendStateEvent(
room.roomId,
EventType.GroupCallMemberPrefix,
mkContent([bobWeb, bobDesktop]),
FAKE_USER_ID_2,
);
await groupCall.cleanMemberState();
expectDevices([bobWeb, bobDesktop]);
});
it("cleans up our own device if we're disconnected", async () => {
await mockClient.sendStateEvent(
room.roomId,
EventType.GroupCallMemberPrefix,
mkContent([bobWeb, bobDesktop]),
FAKE_USER_ID_2,
);
await groupCall.cleanMemberState();
expectDevices([bobDesktop]);
});
it("doesn't clean up the local device if entered via another session", async () => {
groupCall.enteredViaAnotherSession = true;
await mockClient.sendStateEvent(
room.roomId,
EventType.GroupCallMemberPrefix,
mkContent([bobWeb]),
FAKE_USER_ID_2,
);
await groupCall.cleanMemberState();
expectDevices([bobWeb]);
});
it("cleans up devices that have never been online", async () => {
await mockClient.sendStateEvent(
room.roomId,
EventType.GroupCallMemberPrefix,
mkContent([bobDesktop, bobDesktopNeverOnline]),
FAKE_USER_ID_2,
);
await groupCall.cleanMemberState();
expectDevices([bobDesktop]);
});
it("no-ops if there are no state events", async () => {
await groupCall.cleanMemberState();
expect(room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, FAKE_USER_ID_2)).toBe(null);
});
});
});
+27 -38
View File
@@ -16,26 +16,21 @@ limitations under the License.
import { mocked } from "jest-mock";
import { ClientEvent } from "../../../src/client";
import { RoomMember } from "../../../src/models/room-member";
import { SyncState } from "../../../src/sync";
import {
ClientEvent,
GroupCall,
GroupCallIntent,
GroupCallState,
GroupCallType,
IContent,
MatrixEvent,
Room,
RoomState,
} from "../../../src";
import { SyncState } from "../../../src/sync";
import { GroupCallTerminationReason } from "../../../src/webrtc/groupCall";
GroupCallTerminationReason,
} from "../../../src/webrtc/groupCall";
import { IContent, MatrixEvent } from "../../../src/models/event";
import { Room } from "../../../src/models/room";
import { RoomState } from "../../../src/models/room-state";
import { GroupCallEventHandler, GroupCallEventHandlerEvent } from "../../../src/webrtc/groupCallEventHandler";
import { flushPromises } from "../../test-utils/flushPromises";
import {
makeMockGroupCallMemberStateEvent,
makeMockGroupCallStateEvent,
MockCallMatrixClient,
} from "../../test-utils/webrtc";
import { makeMockGroupCallStateEvent, MockCallMatrixClient } from "../../test-utils/webrtc";
const FAKE_USER_ID = "@alice:test.dummy";
const FAKE_DEVICE_ID = "AAAAAAA";
@@ -47,6 +42,7 @@ describe('Group Call Event Handler', function() {
let groupCallEventHandler: GroupCallEventHandler;
let mockClient: MockCallMatrixClient;
let mockRoom: Room;
let mockMember: RoomMember;
beforeEach(() => {
mockClient = new MockCallMatrixClient(
@@ -54,13 +50,27 @@ describe('Group Call Event Handler', function() {
);
groupCallEventHandler = new GroupCallEventHandler(mockClient.typed());
mockMember = {
userId: FAKE_USER_ID,
membership: "join",
} as unknown as RoomMember;
const mockEvent = makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID);
mockRoom = {
on: () => {},
off: () => {},
roomId: FAKE_ROOM_ID,
currentState: {
getStateEvents: jest.fn().mockReturnValue([makeMockGroupCallStateEvent(
FAKE_ROOM_ID, FAKE_GROUP_CALL_ID,
)]),
getStateEvents: jest.fn((type, key) => {
if (type === mockEvent.getType()) {
return key === undefined ? [mockEvent] : mockEvent;
} else {
return key === undefined ? [] : null;
}
}),
},
getMember: (userId: string) => userId === FAKE_USER_ID ? mockMember : null,
} as unknown as Room;
(mockClient as any).getRoom = jest.fn().mockReturnValue(mockRoom);
@@ -211,27 +221,6 @@ describe('Group Call Event Handler', function() {
);
});
it("sends member events to group calls", async () => {
await groupCallEventHandler.start();
const mockGroupCall = {
onMemberStateChanged: jest.fn(),
};
groupCallEventHandler.groupCalls.set(FAKE_ROOM_ID, mockGroupCall as unknown as GroupCall);
const mockStateEvent = makeMockGroupCallMemberStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID);
mockClient.emitRoomState(
mockStateEvent,
{
roomId: FAKE_ROOM_ID,
} as unknown as RoomState,
);
expect(mockGroupCall.onMemberStateChanged).toHaveBeenCalledWith(mockStateEvent);
});
describe("ignoring invalid group call state events", () => {
let mockClientEmit: jest.Func;
+35
View File
@@ -19,3 +19,38 @@ export enum ReceiptType {
FullyRead = "m.fully_read",
ReadPrivate = "m.read.private",
}
export const MAIN_ROOM_TIMELINE = "main";
export interface Receipt {
ts: number;
thread_id?: string;
}
export interface WrappedReceipt {
eventId: string;
data: Receipt;
}
export interface CachedReceipt {
type: ReceiptType;
userId: string;
data: Receipt;
}
export type ReceiptCache = {[eventId: string]: CachedReceipt[]};
export interface ReceiptContent {
[eventId: string]: {
[key in ReceiptType]: {
[userId: string]: Receipt;
};
};
}
// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer.
export type Receipts = {
[receiptType: string]: {
[userId: string]: [WrappedReceipt | null, WrappedReceipt | null]; // Pair<real receipt, synthetic receipt> (both nullable)
};
};
+3 -3
View File
@@ -22,7 +22,7 @@ import { EventEmitter } from "events";
import { ListenerMap, TypedEventEmitter } from "./models/typed-event-emitter";
export class ReEmitter {
constructor(private readonly target: EventEmitter) {}
public constructor(private readonly target: EventEmitter) {}
// Map from emitter to event name to re-emitter
private reEmitters = new Map<EventEmitter, Map<string, (...args: any[]) => void>>();
@@ -38,7 +38,7 @@ export class ReEmitter {
// We include the source as the last argument for event handlers which may need it,
// such as read receipt listeners on the client class which won't have the context
// of the room.
const forSource = (...args: any[]) => {
const forSource = (...args: any[]): void => {
// EventEmitter special cases 'error' to make the emit function throw if no
// handler is attached, which sort of makes sense for making sure that something
// handles an error, but for re-emitting, there could be a listener on the original
@@ -74,7 +74,7 @@ export class TypedReEmitter<
Events extends string,
Arguments extends ListenerMap<Events>,
> extends ReEmitter {
constructor(target: TypedEventEmitter<Events, Arguments>) {
public constructor(target: TypedEventEmitter<Events, Arguments>) {
super(target);
}
+1 -1
View File
@@ -31,7 +31,7 @@ export class ToDeviceMessageQueue {
private retryTimeout: ReturnType<typeof setTimeout> | null = null;
private retryAttempts = 0;
constructor(private client: MatrixClient) {
public constructor(private client: MatrixClient) {
}
public start(): void {
+46 -45
View File
@@ -195,7 +195,7 @@ import { MediaHandler } from "./webrtc/mediaHandler";
import { GroupCallEventHandler } from "./webrtc/groupCallEventHandler";
import { LoginTokenPostResponse, ILoginFlowsResponse, IRefreshTokenResponse, SSOAction } from "./@types/auth";
import { TypedEventEmitter } from "./models/typed-event-emitter";
import { ReceiptType } from "./@types/read_receipts";
import { MAIN_ROOM_TIMELINE, ReceiptType } from "./@types/read_receipts";
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
import { SlidingSyncSdk } from "./sliding-sync-sdk";
import {
@@ -210,7 +210,6 @@ import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
import { UnstableValue } from "./NamespacedValue";
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
import { ToDeviceBatch } from "./models/ToDeviceMessage";
import { MAIN_ROOM_TIMELINE } from "./models/read-receipt";
import { IgnoredInvites } from "./models/invites-ignorer";
import { UIARequest, UIAResponse } from "./@types/uia";
import { LocalNotificationSettings } from "./@types/local_notifications";
@@ -897,6 +896,7 @@ export type EmittedEvents = ClientEvent
| CallEvent // re-emitted by call.ts using Object.values
| CallEventHandlerEvent.Incoming
| GroupCallEventHandlerEvent.Incoming
| GroupCallEventHandlerEvent.Outgoing
| GroupCallEventHandlerEvent.Ended
| GroupCallEventHandlerEvent.Participants
| HttpApiEvent.SessionLoggedOut
@@ -1011,7 +1011,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// A manager for determining which invites should be ignored.
public readonly ignoredInvites: IgnoredInvites;
constructor(opts: IMatrixClientCreateOpts) {
public constructor(opts: IMatrixClientCreateOpts) {
super();
opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl);
@@ -1233,7 +1233,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// shallow-copy the opts dict before modifying and storing it
this.clientOpts = Object.assign({}, opts) as IStoredClientOpts;
this.clientOpts.crypto = this.crypto;
this.clientOpts.canResetEntireTimeline = (roomId) => {
this.clientOpts.canResetEntireTimeline = (roomId): boolean => {
if (!this.canResetTimelineCallback) {
return false;
}
@@ -1260,7 +1260,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* High level helper method to stop the client from polling and allow a
* clean shutdown.
*/
public stopClient() {
public stopClient(): void {
this.crypto?.stop(); // crypto might have been initialised even if the client wasn't fully started
if (!this.clientRunning) return; // already stopped
@@ -1537,7 +1537,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* when creating the client.
* @param {boolean} force True to force use of TURN servers
*/
public setForceTURN(force: boolean) {
public setForceTURN(force: boolean): void {
this.forceTURN = force;
}
@@ -1545,7 +1545,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Set whether to advertise transfer support to other parties on Matrix calls.
* @param {boolean} support True to advertise the 'm.call.transferee' capability
*/
public setSupportsCallTransfer(support: boolean) {
public setSupportsCallTransfer(support: boolean): void {
this.supportsCallTransfer = support;
}
@@ -1679,7 +1679,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* and may change without warning.</b>
* @param {boolean} guest True if this is a guest account.
*/
public setGuest(guest: boolean) {
public setGuest(guest: boolean): void {
// EXPERIMENTAL:
// If the token is a macaroon, it should be encoded in it that it is a 'guest'
// access token, which means that the SDK can determine this entirely without
@@ -1722,7 +1722,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @param {EventTimelineSet} set
*/
public setNotifTimelineSet(set: EventTimelineSet) {
public setNotifTimelineSet(set: EventTimelineSet): void {
this.notifTimelineSet = set;
}
@@ -2280,11 +2280,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* send, in order to speed up sending of the message.
* @param {module:models/room} room the room the event is in
*/
public prepareToEncrypt(room: Room) {
public prepareToEncrypt(room: Room): void {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.prepareToEncrypt(room);
this.crypto.prepareToEncrypt(room);
}
/**
@@ -2325,7 +2325,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* auth data as an object. Can be called multiple times, first with an empty
* authDict, to obtain the flows.
*/
public bootstrapCrossSigning(opts: IBootstrapCrossSigningOpts) {
public bootstrapCrossSigning(opts: IBootstrapCrossSigningOpts): Promise<void> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
@@ -2353,11 +2353,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @param {boolean} val True to trust cross-signed devices
*/
public setCryptoTrustCrossSignedDevices(val: boolean) {
public setCryptoTrustCrossSignedDevices(val: boolean): void {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.setCryptoTrustCrossSignedDevices(val);
this.crypto.setCryptoTrustCrossSignedDevices(val);
}
/**
@@ -2498,7 +2498,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {Array} keys The IDs of the keys to use to encrypt the secret or null/undefined
* to use the default (will throw if no default key is set).
*/
public storeSecret(name: string, secret: string, keys?: string[]) {
public storeSecret(name: string, secret: string, keys?: string[]): Promise<void> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
@@ -2576,7 +2576,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @param {string} keyId The new default key ID
*/
public setDefaultSecretStorageKeyId(keyId: string) {
public setDefaultSecretStorageKeyId(keyId: string): Promise<void> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
@@ -2717,7 +2717,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* This should not normally be necessary.
*/
public forceDiscardSession(roomId: string) {
public forceDiscardSession(roomId: string): void {
if (!this.crypto) {
throw new Error("End-to-End encryption disabled");
}
@@ -2841,7 +2841,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Disable backing up of keys.
*/
public disableKeyBackup() {
public disableKeyBackup(): void {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
@@ -3045,7 +3045,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Marks all group sessions as needing to be backed up and schedules them to
* upload in the background as soon as possible.
*/
public async scheduleAllGroupSessionsForBackup() {
public async scheduleAllGroupSessionsForBackup(): Promise<void> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
@@ -3402,7 +3402,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {array} userIds a list of users to share with. The keys will be sent to
* all of the user's current devices.
*/
public async sendSharedHistoryKeys(roomId: string, userIds: string[]) {
public async sendSharedHistoryKeys(roomId: string, userIds: string[]): Promise<void> {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
@@ -3630,10 +3630,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
let signPromise: Promise<IThirdPartySigned | void> = Promise.resolve();
if (opts.inviteSignUrl) {
signPromise = this.http.requestOtherUrl<IThirdPartySigned>(
Method.Post,
new URL(opts.inviteSignUrl), { mxid: this.credentials.userId },
);
const url = new URL(opts.inviteSignUrl);
url.searchParams.set("mxid", this.credentials.userId!);
signPromise = this.http.requestOtherUrl<IThirdPartySigned>(Method.Post, url);
}
const queryString: Record<string, string | string[]> = {};
@@ -3686,7 +3685,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {MatrixEvent} event Event to cancel
* @throws Error if the event is not in QUEUED, NOT_SENT or ENCRYPTING state
*/
public cancelPendingEvent(event: MatrixEvent) {
public cancelPendingEvent(event: MatrixEvent): void {
if (![EventStatus.QUEUED, EventStatus.NOT_SENT, EventStatus.ENCRYPTING].includes(event.status!)) {
throw new Error("cannot cancel an event with status " + event.status);
}
@@ -3835,7 +3834,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public async unstable_createLiveBeacon(
roomId: Room["roomId"],
beaconInfoContent: MBeaconInfoEventContent,
) {
): Promise<ISendEventResponse> {
return this.unstable_setLiveBeacon(roomId, beaconInfoContent);
}
@@ -3850,7 +3849,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public async unstable_setLiveBeacon(
roomId: string,
beaconInfoContent: MBeaconInfoEventContent,
) {
): Promise<ISendEventResponse> {
return this.sendStateEvent(roomId, M_BEACON_INFO.name, beaconInfoContent, this.getUserId()!);
}
@@ -4122,7 +4121,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.isRoomEncrypted(roomId) ? EventType.RoomMessageEncrypted : eventType;
}
protected updatePendingEventStatus(room: Room | null, event: MatrixEvent, newStatus: EventStatus) {
protected updatePendingEventStatus(room: Room | null, event: MatrixEvent, newStatus: EventStatus): void {
if (room) {
room.updatePendingEvent(event, newStatus);
} else {
@@ -4945,12 +4944,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const populationResults: { [roomId: string]: Error } = {};
const promises: Promise<any>[] = [];
const doLeave = (roomId: string) => {
const doLeave = (roomId: string): Promise<void> => {
return this.leave(roomId).then(() => {
delete populationResults[roomId];
}).catch((err) => {
// suppress error
populationResults[roomId] = err;
return null; // suppress error
});
};
@@ -5831,8 +5830,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
).then(async (res) => {
const mapper = this.getEventMapper();
const matrixEvents = res.chunk.map(mapper);
for (const event of matrixEvents) {
await eventTimeline.getTimelineSet()?.thread?.processEvent(event);
// Process latest events first
for (const event of matrixEvents.slice().reverse()) {
await thread?.processEvent(event);
const sender = event.getSender()!;
if (!backwards || thread?.getEventReadUpTo(sender) === null) {
room.addLocalEchoReceipt(sender, event, ReceiptType.Read);
}
}
const newToken = res.next_batch;
@@ -5905,7 +5910,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Reset the notifTimelineSet entirely, paginating in some historical notifs as
* a starting point for subsequent pagination.
*/
public resetNotifTimelineSet() {
public resetNotifTimelineSet(): void {
if (!this.notifTimelineSet) {
return;
}
@@ -5950,7 +5955,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Stop any ongoing room peeking.
*/
public stopPeeking() {
public stopPeeking(): void {
if (this.peekSync) {
this.peekSync.stopPeeking();
this.peekSync = null;
@@ -6684,7 +6689,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @param {boolean} allow
*/
public setFallbackICEServerAllowed(allow: boolean) {
public setFallbackICEServerAllowed(allow: boolean): void {
this.fallbackICEServerAllowed = allow;
}
@@ -7025,7 +7030,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Default: returns false.
* @param {Function} cb The callback which will be invoked.
*/
public setCanResetTimelineCallback(cb: ResetTimelineCallback) {
public setCanResetTimelineCallback(cb: ResetTimelineCallback): void {
this.canResetTimelineCallback = cb;
}
@@ -7165,7 +7170,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Set the identity server URL of this client
* @param {string} url New identity server URL
*/
public setIdentityServerUrl(url: string) {
public setIdentityServerUrl(url: string): void {
this.idBaseUrl = utils.ensureNoTrailingSlash(url);
this.http.setIdBaseUrl(this.idBaseUrl);
}
@@ -7182,7 +7187,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Set the access token associated with this account.
* @param {string} token The new access token.
*/
public setAccessToken(token: string) {
public setAccessToken(token: string): void {
this.http.opts.accessToken = token;
}
@@ -9339,12 +9344,8 @@ export function fixNotificationCountOnDecryption(cli: MatrixClient, event: Matri
if (!room || !cli.getUserId()) return;
const isThreadEvent = !!event.threadRootId && !event.isThreadRoot;
const currentCount = (isThreadEvent
? room.getThreadUnreadNotificationCount(
event.threadRootId,
NotificationCountType.Highlight,
)
: room.getUnreadNotificationCount(NotificationCountType.Highlight)) ?? 0;
const currentCount = 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
@@ -9376,7 +9377,7 @@ export function fixNotificationCountOnDecryption(cli: MatrixClient, event: Matri
// Fix 'Mentions Only' rooms from not having the right badge count
const totalCount = (isThreadEvent
? room.getThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Total)
: room.getUnreadNotificationCount(NotificationCountType.Total)) ?? 0;
: room.getRoomUnreadNotificationCount(NotificationCountType.Total)) ?? 0;
if (totalCount < newCount) {
if (isThreadEvent) {
+7 -6
View File
@@ -33,6 +33,7 @@ import {
LegacyLocationEventContent,
} from "./@types/location";
import { MRoomTopicEventContent, MTopicContent, M_TOPIC } from "./@types/topic";
import { IContent } from "./models/event";
/**
* Generates the content for a HTML Message event
@@ -40,7 +41,7 @@ import { MRoomTopicEventContent, MTopicContent, M_TOPIC } from "./@types/topic";
* @param {string} htmlBody the HTML representation of the message
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
export function makeHtmlMessage(body: string, htmlBody: string) {
export function makeHtmlMessage(body: string, htmlBody: string): IContent {
return {
msgtype: MsgType.Text,
format: "org.matrix.custom.html",
@@ -55,7 +56,7 @@ export function makeHtmlMessage(body: string, htmlBody: string) {
* @param {string} htmlBody the HTML representation of the notice
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
export function makeHtmlNotice(body: string, htmlBody: string) {
export function makeHtmlNotice(body: string, htmlBody: string): IContent {
return {
msgtype: MsgType.Notice,
format: "org.matrix.custom.html",
@@ -70,7 +71,7 @@ export function makeHtmlNotice(body: string, htmlBody: string) {
* @param {string} htmlBody the HTML representation of the emote
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
export function makeHtmlEmote(body: string, htmlBody: string) {
export function makeHtmlEmote(body: string, htmlBody: string): IContent {
return {
msgtype: MsgType.Emote,
format: "org.matrix.custom.html",
@@ -84,7 +85,7 @@ export function makeHtmlEmote(body: string, htmlBody: string) {
* @param {string} body the plaintext body of the emote
* @returns {{msgtype: string, body: string}}
*/
export function makeTextMessage(body: string) {
export function makeTextMessage(body: string): IContent {
return {
msgtype: MsgType.Text,
body: body,
@@ -96,7 +97,7 @@ export function makeTextMessage(body: string) {
* @param {string} body the plaintext body of the notice
* @returns {{msgtype: string, body: string}}
*/
export function makeNotice(body: string) {
export function makeNotice(body: string): IContent {
return {
msgtype: MsgType.Notice,
body: body,
@@ -108,7 +109,7 @@ export function makeNotice(body: string) {
* @param {string} body the plaintext body of the emote
* @returns {{msgtype: string, body: string}}
*/
export function makeEmoteMessage(body: string) {
export function makeEmoteMessage(body: string): IContent {
return {
msgtype: MsgType.Emote,
body: body,
+15 -8
View File
@@ -21,7 +21,7 @@ limitations under the License.
import { PkSigning } from "@matrix-org/olm";
import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib';
import { decodeBase64, encodeBase64, IObject, pkSign, pkVerify } from './olmlib';
import { logger } from '../logger';
import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
import { decryptAES, encryptAES } from './aes';
@@ -74,7 +74,7 @@ export class CrossSigningInfo {
* Requires getCrossSigningKey and saveCrossSigningKeys
* @param {object} cacheCallbacks Callbacks used to interact with the cache
*/
constructor(
public constructor(
public readonly userId: string,
private callbacks: ICryptoCallbacks = {},
private cacheCallbacks: ICacheCallbacks = {},
@@ -175,7 +175,7 @@ export class CrossSigningInfo {
// check what SSSS keys have encrypted the master key (if any)
const stored = await secretStorage.isStored("m.cross_signing.master") || {};
// then check which of those SSSS keys have also encrypted the SSK and USK
function intersect(s: Record<string, ISecretStorageKeyInfo>) {
function intersect(s: Record<string, ISecretStorageKeyInfo>): void {
for (const k of Object.keys(stored)) {
if (!s[k]) {
delete stored[k];
@@ -586,7 +586,14 @@ export class CrossSigningInfo {
}
}
function deviceToObject(device: DeviceInfo, userId: string) {
interface DeviceObject extends IObject {
algorithms: string[];
keys: Record<string, string>;
device_id: string;
user_id: string;
}
function deviceToObject(device: DeviceInfo, userId: string): DeviceObject {
return {
algorithms: device.algorithms,
keys: device.keys,
@@ -606,7 +613,7 @@ export enum CrossSigningLevel {
* Represents the ways in which we trust a user
*/
export class UserTrustLevel {
constructor(
public constructor(
private readonly crossSigningVerified: boolean,
private readonly crossSigningVerifiedBefore: boolean,
private readonly tofu: boolean,
@@ -646,7 +653,7 @@ export class UserTrustLevel {
* Represents the ways in which we trust a device
*/
export class DeviceTrustLevel {
constructor(
public constructor(
public readonly crossSigningVerified: boolean,
public readonly tofu: boolean,
private readonly localVerified: boolean,
@@ -775,7 +782,7 @@ export async function requestKeysDuringVerification(
// CrossSigningInfo.getCrossSigningKey() to validate/cache
const crossSigning = new CrossSigningInfo(
original.userId,
{ getCrossSigningKey: async (type) => {
{ getCrossSigningKey: async (type): Promise<Uint8Array> => {
logger.debug("Cross-signing: requesting secret", type, deviceId);
const { promise } = client.requestSecret(
`m.cross_signing.${type}`, [deviceId],
@@ -801,7 +808,7 @@ export async function requestKeysDuringVerification(
});
// also request and cache the key backup key
const backupKeyPromise = (async () => {
const backupKeyPromise = (async (): Promise<void> => {
const cachedKey = await client.crypto!.getSessionBackupPrivateKey();
if (!cachedKey) {
logger.info("No cached backup key found. Requesting...");
+4 -4
View File
@@ -102,7 +102,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
private readonly serialiser: DeviceListUpdateSerialiser;
constructor(
public constructor(
baseApis: MatrixClient,
private readonly cryptoStore: CryptoStore,
olmDevice: OlmDevice,
@@ -117,7 +117,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
/**
* Load the device tracking state from storage
*/
public async load() {
public async load(): Promise<void> {
await this.cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this.cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
@@ -150,7 +150,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
}
}
public stop() {
public stop(): void {
if (this.saveTimer !== null) {
clearTimeout(this.saveTimer);
}
@@ -693,7 +693,7 @@ class DeviceListUpdateSerialiser {
* @param {object} olmDevice The Olm Device
* @param {object} deviceList The device list object, the device list to be updated
*/
constructor(
public constructor(
private readonly baseApis: MatrixClient,
private readonly olmDevice: OlmDevice,
private readonly deviceList: DeviceList,
+5 -5
View File
@@ -61,7 +61,7 @@ export class EncryptionSetupBuilder {
* @param {Object.<String, MatrixEvent>} accountData pre-existing account data, will only be read, not written.
* @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet
*/
constructor(accountData: Record<string, MatrixEvent>, delegateCryptoCallbacks?: ICryptoCallbacks) {
public constructor(accountData: Record<string, MatrixEvent>, delegateCryptoCallbacks?: ICryptoCallbacks) {
this.accountDataClientAdapter = new AccountDataClientAdapter(accountData);
this.crossSigningCallbacks = new CrossSigningCallbacks();
this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks);
@@ -192,7 +192,7 @@ export class EncryptionSetupOperation {
* @param {Object} keyBackupInfo
* @param {Object} keySignatures
*/
constructor(
public constructor(
private readonly accountData: Map<string, object>,
private readonly crossSigningKeys?: ICrossSigningKeys,
private readonly keyBackupInfo?: IKeyBackupInfo,
@@ -272,7 +272,7 @@ class AccountDataClientAdapter
/**
* @param {Object.<String, MatrixEvent>} existingValues existing account data
*/
constructor(private readonly existingValues: Record<string, MatrixEvent>) {
public constructor(private readonly existingValues: Record<string, MatrixEvent>) {
super();
}
@@ -342,7 +342,7 @@ class CrossSigningCallbacks implements ICryptoCallbacks, ICacheCallbacks {
return Promise.resolve(this.privateKeys.get(type) ?? null);
}
public saveCrossSigningKeys(privateKeys: Record<string, Uint8Array>) {
public saveCrossSigningKeys(privateKeys: Record<string, Uint8Array>): void {
for (const [type, privateKey] of Object.entries(privateKeys)) {
this.privateKeys.set(type, privateKey);
}
@@ -356,7 +356,7 @@ class CrossSigningCallbacks implements ICryptoCallbacks, ICacheCallbacks {
class SSSSCryptoCallbacks {
private readonly privateKeys = new Map<string, Uint8Array>();
constructor(private readonly delegateCryptoCallbacks?: ICryptoCallbacks) {}
public constructor(private readonly delegateCryptoCallbacks?: ICryptoCallbacks) {}
public async getSecretStorageKey(
{ keys }: { keys: Record<string, ISecretStorageKeyInfo> },
+12 -12
View File
@@ -177,13 +177,13 @@ export class OlmDevice {
// Used by olm to serialise prekey message decryptions
public olmPrekeyPromise: Promise<any> = Promise.resolve(); // set by consumers
constructor(private readonly cryptoStore: CryptoStore) {
public constructor(private readonly cryptoStore: CryptoStore) {
}
/**
* @return {array} The version of Olm.
*/
static getOlmVersion(): [number, number, number] {
public static getOlmVersion(): [number, number, number] {
return global.Olm.get_library_version();
}
@@ -916,6 +916,7 @@ export class OlmDevice {
}
public async recordSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise<void> {
logger.info(`Recording problem on olm session with ${deviceKey} of type ${type}. Recreating: ${fixed}`);
await this.cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed);
}
@@ -1139,17 +1140,14 @@ export class OlmDevice {
}
if (existingSession) {
logger.log(
"Update for megolm session "
+ senderKey + "/" + sessionId,
);
logger.log(`Update for megolm session ${senderKey}|${sessionId}`);
if (existingSession.first_known_index() <= session.first_known_index()) {
if (!existingSessionData!.untrusted || extraSessionData.untrusted) {
// existing session has less-than-or-equal index
// (i.e. can decrypt at least as much), and the
// new session's trust does not win over the old
// session's trust, so keep it
logger.log(`Keeping existing megolm session ${sessionId}`);
logger.log(`Keeping existing megolm session ${senderKey}|${sessionId}`);
return;
}
if (existingSession.first_known_index() < session.first_known_index()) {
@@ -1164,7 +1162,7 @@ export class OlmDevice {
) {
logger.info(
"Upgrading trust of existing megolm session " +
sessionId + " based on newly-received trusted session",
`${senderKey}|${sessionId} based on newly-received trusted session`,
);
existingSessionData!.untrusted = false;
this.cryptoStore.storeEndToEndInboundGroupSession(
@@ -1172,7 +1170,7 @@ export class OlmDevice {
);
} else {
logger.warn(
"Newly-received megolm session " + sessionId +
`Newly-received megolm session ${senderKey}|$sessionId}` +
" does not match existing session! Keeping existing session",
);
}
@@ -1183,8 +1181,8 @@ export class OlmDevice {
}
logger.info(
"Storing megolm session " + senderKey + "/" + sessionId +
" with first index " + session.first_known_index(),
`Storing megolm session ${senderKey}|${sessionId} with first index `+
session.first_known_index(),
);
const sessionData = Object.assign({}, extraSessionData, {
@@ -1517,7 +1515,9 @@ export class OlmDevice {
});
}
async getSharedHistoryInboundGroupSessions(roomId: string): Promise<[senderKey: string, sessionId: string][]> {
public async getSharedHistoryInboundGroupSessions(
roomId: string,
): Promise<[senderKey: string, sessionId: string][]> {
let result: Promise<[senderKey: string, sessionId: string][]>;
await this.cryptoStore.doTxn(
'readonly', [
+2 -2
View File
@@ -102,7 +102,7 @@ export class OutgoingRoomKeyRequestManager {
private clientRunning = true;
constructor(
public constructor(
private readonly baseApis: MatrixClient,
private readonly deviceId: string,
private readonly cryptoStore: CryptoStore,
@@ -352,7 +352,7 @@ export class OutgoingRoomKeyRequestManager {
return;
}
const startSendingOutgoingRoomKeyRequests = () => {
const startSendingOutgoingRoomKeyRequests = (): void => {
if (this.sendOutgoingRoomKeyRequestsRunning) {
throw new Error("RoomKeyRequestSend already in progress!");
}
+1 -1
View File
@@ -38,7 +38,7 @@ export class RoomList {
// Object of roomId -> room e2e info object (body of the m.room.encryption event)
private roomEncryption: Record<string, IRoomEncryption> = {};
constructor(private readonly cryptoStore?: CryptoStore) {}
public constructor(private readonly cryptoStore?: CryptoStore) {}
public async init(): Promise<void> {
await this.cryptoStore!.doTxn(
+2 -2
View File
@@ -78,7 +78,7 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
// as you don't request any secrets.
// A better solution would probably be to split this class up into secret storage and
// secret sharing which are really two separate things, even though they share an MSC.
constructor(
public constructor(
private readonly accountDataAdapter: IAccountDataClient,
private readonly cryptoCallbacks: ICryptoCallbacks,
private readonly baseApis: B,
@@ -381,7 +381,7 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
const deferred = defer<string>();
this.requests.set(requestId, { name, devices, deferred });
const cancel = (reason: string) => {
const cancel = (reason: string): void => {
// send cancellation event
const cancelData = {
action: "request_cancellation",
+4 -4
View File
@@ -78,7 +78,7 @@ export abstract class EncryptionAlgorithm {
protected readonly baseApis: MatrixClient;
protected readonly roomId?: string;
constructor(params: IParams) {
public constructor(params: IParams) {
this.userId = params.userId;
this.deviceId = params.deviceId;
this.crypto = params.crypto;
@@ -150,7 +150,7 @@ export abstract class DecryptionAlgorithm {
protected readonly baseApis: MatrixClient;
protected readonly roomId?: string;
constructor(params: DecryptionClassParams) {
public constructor(params: DecryptionClassParams) {
this.userId = params.userId;
this.crypto = params.crypto;
this.olmDevice = params.olmDevice;
@@ -242,7 +242,7 @@ export abstract class DecryptionAlgorithm {
export class DecryptionError extends Error {
public readonly detailedString: string;
constructor(public readonly code: string, msg: string, details?: Record<string, string | Error>) {
public constructor(public readonly code: string, msg: string, details?: Record<string, string | Error>) {
super(msg);
this.code = code;
this.name = 'DecryptionError';
@@ -272,7 +272,7 @@ function detailedStringForDecryptionError(err: DecryptionError, details?: Record
* @extends Error
*/
export class UnknownDeviceError extends Error {
constructor(
public constructor(
msg: string,
public readonly devices: Record<string, Record<string, object>>,
public event?: MatrixEvent,
+25 -21
View File
@@ -136,7 +136,7 @@ class OutboundSessionInfo {
public sharedWithDevices: Record<string, Record<string, SharedWithData>> = {};
public blockedDevicesNotified: Record<string, Record<string, boolean>> = {};
constructor(public readonly sessionId: string, public readonly sharedHistory = false) {
public constructor(public readonly sessionId: string, public readonly sharedHistory = false) {
this.creationTime = new Date().getTime();
}
@@ -248,7 +248,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
protected readonly roomId: string;
constructor(params: IParams & Required<Pick<IParams, "roomId">>) {
public constructor(params: IParams & Required<Pick<IParams, "roomId">>) {
super(params);
this.roomId = params.roomId;
@@ -347,7 +347,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
singleOlmCreationPhase: boolean,
blocked: IBlockedMap,
session: OutboundSessionInfo,
) {
): Promise<void> {
// now check if we need to share with any devices
const shareMap: Record<string, DeviceInfo[]> = {};
@@ -386,13 +386,13 @@ class MegolmEncryption extends EncryptionAlgorithm {
);
await Promise.all([
(async () => {
(async (): Promise<void> => {
// share keys with devices that we already have a session for
logger.debug(`Sharing keys with existing Olm sessions in ${this.roomId}`, olmSessions);
await this.shareKeyWithOlmSessions(session, key, payload, olmSessions);
logger.debug(`Shared keys with existing Olm sessions in ${this.roomId}`);
})(),
(async () => {
(async (): Promise<void> => {
logger.debug(
`Sharing keys (start phase 1) with new Olm sessions in ${this.roomId}`,
devicesWithoutSession,
@@ -415,7 +415,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
if (!singleOlmCreationPhase && (Date.now() - start < 10000)) {
// perform the second phase of olm session creation if requested,
// and if the first phase didn't take too long
(async () => {
(async (): Promise<void> => {
// Retry sending keys to devices that we were unable to establish
// an olm session for. This time, we use a longer timeout, but we
// do this in the background and don't block anything else while we
@@ -452,7 +452,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
}
logger.debug(`Shared keys (all phases done) with new Olm sessions in ${this.roomId}`);
})(),
(async () => {
(async (): Promise<void> => {
logger.debug(`There are ${Object.entries(blocked).length} blocked devices in ${this.roomId}`,
Object.entries(blocked));
@@ -696,28 +696,27 @@ class MegolmEncryption extends EncryptionAlgorithm {
): Promise<void> {
const obSessionInfo = this.outboundSessions[sessionId];
if (!obSessionInfo) {
logger.debug(`megolm session ${sessionId} not found: not re-sharing keys`);
logger.debug(`megolm session ${senderKey}|${sessionId} not found: not re-sharing keys`);
return;
}
// The chain index of the key we previously sent this device
if (obSessionInfo.sharedWithDevices[userId] === undefined) {
logger.debug(`megolm session ${sessionId} never shared with user ${userId}`);
logger.debug(`megolm session ${senderKey}|${sessionId} never shared with user ${userId}`);
return;
}
const sessionSharedData = obSessionInfo.sharedWithDevices[userId][device.deviceId];
if (sessionSharedData === undefined) {
logger.debug(
"megolm session ID " + sessionId + " never shared with device " +
userId + ":" + device.deviceId,
`megolm session ${senderKey}|${sessionId} never shared with device ${userId}:${device.deviceId}`,
);
return;
}
if (sessionSharedData.deviceKey !== device.getIdentityKey()) {
logger.warn(
`Session has been shared with device ${device.deviceId} but with identity ` +
`key ${sessionSharedData.deviceKey}. Key is now ${device.getIdentityKey()}!`,
`Megolm session ${senderKey}|${sessionId} has been shared with device ${device.deviceId} but ` +
`with identity key ${sessionSharedData.deviceKey}. Key is now ${device.getIdentityKey()}!`,
);
return;
}
@@ -730,7 +729,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
if (!key) {
logger.warn(
`No inbound session key found for megolm ${sessionId}: not re-sharing keys`,
`No inbound session key found for megolm session ${senderKey}|${sessionId}: not re-sharing keys`,
);
return;
}
@@ -776,7 +775,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
[device.deviceId]: encryptedContent,
},
});
logger.debug(`Re-shared key for megolm session ${sessionId} with ${userId}:${device.deviceId}`);
logger.debug(`Re-shared key for megolm session ${senderKey}|${sessionId} with ${userId}:${device.deviceId}`);
}
/**
@@ -810,7 +809,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
errorDevices: IOlmDevice[],
otkTimeout: number,
failedServers?: string[],
) {
): Promise<void> {
logger.debug(`Ensuring Olm sessions for devices in ${this.roomId}`);
const devicemap = await olmlib.ensureOlmSessionsForDevices(
this.olmDevice, this.baseApis, devicesByUser, false, otkTimeout, failedServers,
@@ -970,7 +969,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
this.encryptionPreparation = {
startTime: Date.now(),
promise: (async () => {
promise: (async (): Promise<void> => {
try {
logger.debug(`Getting devices in ${this.roomId}`);
const [devicesInRoom, blocked] = await this.getDevicesInRoom(room);
@@ -1232,7 +1231,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
protected readonly roomId: string;
constructor(params: DecryptionClassParams<IParams & Required<Pick<IParams, "roomId">>>) {
public constructor(params: DecryptionClassParams<IParams & Required<Pick<IParams, "roomId">>>) {
super(params);
this.roomId = params.roomId;
}
@@ -1312,6 +1311,10 @@ class MegolmDecryption extends DecryptionAlgorithm {
content.sender_key, event.getTs() - 120000,
);
if (problem) {
logger.info(
`When handling UISI from ${event.getSender()} (sender key ${content.sender_key}): ` +
`recent session problem with that sender: ${problem}`,
);
let problemDescription = PROBLEM_DESCRIPTIONS[problem.type as "no_olm"] || PROBLEM_DESCRIPTIONS.unknown;
if (problem.fixed) {
problemDescription +=
@@ -1902,10 +1905,11 @@ class MegolmDecryption extends DecryptionAlgorithm {
public async sendSharedHistoryInboundSessions(devicesByUser: Record<string, DeviceInfo[]>): Promise<void> {
await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser);
logger.log("sendSharedHistoryInboundSessions to users", Object.keys(devicesByUser));
const sharedHistorySessions = await this.olmDevice.getSharedHistoryInboundGroupSessions(this.roomId);
logger.log("shared-history sessions", sharedHistorySessions);
logger.log(
`Sharing history in ${this.roomId} with users ${Object.keys(devicesByUser)}`,
sharedHistorySessions.map(([senderKey, sessionId]) => `${senderKey}|${sessionId}`),
);
for (const [senderKey, sessionId] of sharedHistorySessions) {
const payload = await this.buildKeyForwardingMessage(this.roomId, senderKey, sessionId);
+5 -5
View File
@@ -120,7 +120,7 @@ export class BackupManager {
private sendingBackups: boolean; // Are we currently sending backups?
private sessionLastCheckAttemptedTime: Record<string, number> = {}; // When did we last try to check the server for a given session id?
constructor(private readonly baseApis: MatrixClient, public readonly getKey: GetKey) {
public constructor(private readonly baseApis: MatrixClient, public readonly getKey: GetKey) {
this.checkedForBackup = false;
this.sendingBackups = false;
}
@@ -609,7 +609,7 @@ export class BackupManager {
export class Curve25519 implements BackupAlgorithm {
public static algorithmName = "m.megolm_backup.v1.curve25519-aes-sha2";
constructor(
public constructor(
public authData: ICurve25519AuthData,
private publicKey: any, // FIXME: PkEncryption
private getKey: () => Promise<Uint8Array>,
@@ -661,7 +661,7 @@ export class Curve25519 implements BackupAlgorithm {
}
}
public get untrusted() { return true; }
public get untrusted(): boolean { return true; }
public async encryptSession(data: Record<string, any>): Promise<any> {
const plainText: Record<string, any> = Object.assign({}, data);
@@ -735,7 +735,7 @@ const UNSTABLE_MSC3270_NAME = new UnstableValue(
export class Aes256 implements BackupAlgorithm {
public static algorithmName = UNSTABLE_MSC3270_NAME.name;
constructor(
public constructor(
public readonly authData: IAes256AuthData,
private readonly key: Uint8Array,
) {}
@@ -786,7 +786,7 @@ export class Aes256 implements BackupAlgorithm {
}
}
public get untrusted() { return false; }
public get untrusted(): boolean { return false; }
public encryptSession(data: Record<string, any>): Promise<any> {
const plainText: Record<string, any> = Object.assign({}, data);
+2 -2
View File
@@ -62,7 +62,7 @@ export class DehydrationManager {
private keyInfo?: {[props: string]: any};
private deviceDisplayName?: string;
constructor(private readonly crypto: Crypto) {
public constructor(private readonly crypto: Crypto) {
this.getDehydrationKeyFromCache();
}
@@ -294,7 +294,7 @@ export class DehydrationManager {
}
}
public stop() {
public stop(): void {
if (this.timeoutId) {
global.clearTimeout(this.timeoutId);
this.timeoutId = undefined;
+1 -1
View File
@@ -94,7 +94,7 @@ export class DeviceInfo {
public unsigned: Record<string, any> = {};
public signatures: ISignatures = {};
constructor(public readonly deviceId: string) {}
public constructor(public readonly deviceId: string) {}
/**
* Prepare a DeviceInfo for JSON serialisation in the session store
+30 -30
View File
@@ -278,7 +278,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
/**
* @return {string} The version of Olm.
*/
static getOlmVersion(): [number, number, number] {
public static getOlmVersion(): [number, number, number] {
return OlmDevice.getOlmVersion();
}
@@ -372,7 +372,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* Each element can either be a string from MatrixClient.verificationMethods
* or a class that implements a verification method.
*/
constructor(
public constructor(
public readonly baseApis: MatrixClient,
public readonly userId: string,
private readonly deviceId: string,
@@ -465,7 +465,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// Assuming no app-supplied callback, default to getting from SSSS.
if (!cryptoCallbacks.getCrossSigningKey && cryptoCallbacks.getSecretStorageKey) {
cryptoCallbacks.getCrossSigningKey = async (type) => {
cryptoCallbacks.getCrossSigningKey = async (type): Promise<Uint8Array | null> => {
return CrossSigningInfo.getFromSecretStorage(type, this.secretStorage);
};
}
@@ -709,7 +709,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
);
// Reset the cross-signing keys
const resetCrossSigning = async () => {
const resetCrossSigning = async (): Promise<void> => {
crossSigningInfo.resetKeys();
// Sign master key with device key
await this.signObject(crossSigningInfo.keys.master);
@@ -846,12 +846,12 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
*/
// TODO this does not resolve with what it says it does
public async bootstrapSecretStorage({
createSecretStorageKey = async () => ({} as IRecoveryKey),
createSecretStorageKey = async (): Promise<IRecoveryKey> => ({} as IRecoveryKey),
keyBackupInfo,
setupNewKeyBackup,
setupNewSecretStorage,
getKeyBackupPassphrase,
}: ICreateSecretStorageOpts = {}) {
}: ICreateSecretStorageOpts = {}): Promise<void> {
logger.log("Bootstrapping Secure Secret Storage");
const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks;
const builder = new EncryptionSetupBuilder(
@@ -868,7 +868,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
let newKeyId: string | null = null;
// create a new SSSS key and set it as default
const createSSSS = async (opts: IAddSecretStorageKeyOpts, privateKey?: Uint8Array) => {
const createSSSS = async (opts: IAddSecretStorageKeyOpts, privateKey?: Uint8Array): Promise<string> => {
if (privateKey) {
opts.key = privateKey;
}
@@ -884,7 +884,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return keyId;
};
const ensureCanCheckPassphrase = async (keyId: string, keyInfo: ISecretStorageKeyInfo) => {
const ensureCanCheckPassphrase = async (keyId: string, keyInfo: ISecretStorageKeyInfo): Promise<void> => {
if (!keyInfo.mac) {
const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey?.(
{ keys: { [keyId]: keyInfo } }, "",
@@ -903,7 +903,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
};
const signKeyBackupWithCrossSigning = async (keyBackupAuthData: IKeyBackupInfo["auth_data"]) => {
const signKeyBackupWithCrossSigning = async (keyBackupAuthData: IKeyBackupInfo["auth_data"]): Promise<void> => {
if (
this.crossSigningInfo.getId() &&
await this.crossSigningInfo.isStoredInKeyCache("master")
@@ -1241,7 +1241,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device);
logger.info(`Starting background key sig upload for ${this.deviceId}`);
const upload = ({ shouldEmit = false }) => {
const upload = ({ shouldEmit = false }): Promise<void> => {
return this.baseApis.uploadKeySignatures({
[this.userId]: {
[this.deviceId]: signedDevice!,
@@ -1475,7 +1475,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
/*
* Event handler for DeviceList's userNewDevices event
*/
private onDeviceListUserCrossSigningUpdated = async (userId: string) => {
private onDeviceListUserCrossSigningUpdated = async (userId: string): Promise<void> => {
if (userId === this.userId) {
// An update to our own cross-signing key.
// Get the new key first:
@@ -1657,7 +1657,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
const keysToUpload = Object.keys(keySignatures);
if (keysToUpload.length) {
const upload = ({ shouldEmit = false }) => {
const upload = ({ shouldEmit = false }): Promise<void> => {
logger.info(`Starting background key sig upload for ${keysToUpload}`);
return this.baseApis.uploadKeySignatures({ [this.userId]: keySignatures })
.then((response) => {
@@ -1864,7 +1864,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
}
public setNeedsNewFallback(needsNewFallback: boolean) {
public setNeedsNewFallback(needsNewFallback: boolean): void {
this.needsNewFallback = needsNewFallback;
}
@@ -1873,7 +1873,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
// check if it's time to upload one-time keys, and do so if so.
private maybeUploadOneTimeKeys() {
private maybeUploadOneTimeKeys(): void {
// frequency with which to check & upload one-time keys
const uploadPeriod = 1000 * 60; // one minute
@@ -1919,7 +1919,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// out stale private keys that won't receive a message.
const keyLimit = Math.floor(maxOneTimeKeys / 2);
const uploadLoop = async (keyCount: number) => {
const uploadLoop = async (keyCount: number): Promise<void> => {
while (keyLimit > keyCount || this.getNeedsNewFallback()) {
// Ask olm to generate new one time keys, then upload them to synapse.
if (keyLimit > keyCount) {
@@ -2155,7 +2155,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
);
const device = await this.crossSigningInfo.signUser(xsk);
if (device) {
const upload = async ({ shouldEmit = false }) => {
const upload = async ({ shouldEmit = false }): Promise<void> => {
logger.info("Uploading signature for " + userId + "...");
const response = await this.baseApis.uploadKeySignatures({
[userId]: {
@@ -2246,7 +2246,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
if (device) {
const upload = async ({ shouldEmit = false }) => {
const upload = async ({ shouldEmit = false }): Promise<void> => {
logger.info("Uploading signature for " + deviceId);
const response = await this.baseApis.uploadKeySignatures({
[userId]: {
@@ -2645,7 +2645,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @returns {Promise} when all devices for the room have been fetched and marked to track
*/
public trackRoomDevices(roomId: string): Promise<void> {
const trackMembers = async () => {
const trackMembers = async (): Promise<void> => {
// not an encrypted room
if (!this.roomEncryptors.has(roomId)) {
return;
@@ -2747,7 +2747,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
let failures = 0;
const total = keys.length;
function updateProgress() {
function updateProgress(): void {
opts.progressCallback?.({
stage: "load_keys",
successes,
@@ -3187,7 +3187,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
}
private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string) => {
private onMembership = (event: MatrixEvent, member: RoomMember, oldMembership?: string): void => {
try {
this.onRoomMembership(event, member, oldMembership);
} catch (e) {
@@ -3269,9 +3269,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
logger.info(
`Got room key withheld event from ${event.getSender()} (${content.sender_key}) `
+ `for ${content.algorithm}/${content.room_id}/${content.session_id} `
+ `with reason ${content.code} (${content.reason})`,
`Got room key withheld event from ${event.getSender()} `
+ `for ${content.algorithm} session ${content.sender_key}|${content.session_id} `
+ `in room ${content.room_id} with code ${content.code} (${content.reason})`,
);
const alg = this.getRoomDecryptor(content.room_id, content.algorithm);
@@ -3340,7 +3340,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
if (!InRoomChannel.validateEvent(event, this.baseApis)) {
return;
}
const createRequest = (event: MatrixEvent) => {
const createRequest = (event: MatrixEvent): VerificationRequest => {
const channel = new InRoomChannel(this.baseApis, event.getRoomId()!);
return new VerificationRequest(
channel, this.verificationMethods, this.baseApis);
@@ -3361,7 +3361,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
try {
await new Promise<void>((resolve, reject) => {
eventIdListener = resolve;
statusListener = () => {
statusListener = (): void => {
if (event.status == EventStatus.CANCELLED) {
reject(new Error("Event status set to CANCELLED."));
}
@@ -3420,7 +3420,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// retry decryption for all events sent by the sender_key. This will
// update the events to show a message indicating that the olm session was
// wedged.
const retryDecryption = () => {
const retryDecryption = (): void => {
const roomDecryptors = this.getRoomDecryptors(olmlib.MEGOLM_ALGORITHM);
for (const decryptor of roomDecryptors) {
decryptor.retryDecryptionFromSender(deviceKey);
@@ -3692,7 +3692,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return;
}
req.share = () => {
req.share = (): void => {
decryptor.shareKeysWithDevice(req);
};
@@ -3863,14 +3863,14 @@ export class IncomingRoomKeyRequest {
public readonly requestBody: IRoomKeyRequestBody;
public share: () => void;
constructor(event: MatrixEvent) {
public constructor(event: MatrixEvent) {
const content = event.getContent();
this.userId = event.getSender()!;
this.deviceId = content.requesting_device_id;
this.requestId = content.request_id;
this.requestBody = content.body || {};
this.share = () => {
this.share = (): void => {
throw new Error("don't know how to share keys for this request yet");
};
}
@@ -3888,7 +3888,7 @@ class IncomingRoomKeyRequestCancellation {
public readonly deviceId: string;
public readonly requestId: string;
constructor(event: MatrixEvent) {
public constructor(event: MatrixEvent) {
const content = event.getContent();
this.userId = event.getSender()!;
+5 -5
View File
@@ -82,7 +82,7 @@ export async function encryptMessageForDevice(
recipientUserId: string,
recipientDevice: DeviceInfo,
payloadFields: Record<string, any>,
) {
): Promise<void> {
const deviceKey = recipientDevice.getIdentityKey();
const sessionId = await olmDevice.getSessionIdForDevice(deviceKey);
if (sessionId === null) {
@@ -173,7 +173,7 @@ export async function getExistingOlmSessions(
for (const deviceInfo of devices) {
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
promises.push((async () => {
promises.push((async (): Promise<void> => {
const sessionId = await olmDevice.getSessionIdForDevice(
key, true,
);
@@ -256,7 +256,7 @@ export async function ensureOlmSessionsForDevices(
// conditions. If we find that we already have a session, then
// we'll resolve
olmDevice.sessionsInProgress[key] = new Promise(resolve => {
resolveSession[key] = (v: any) => {
resolveSession[key] = (v: any): void => {
delete olmDevice.sessionsInProgress[key];
resolve(v);
};
@@ -463,7 +463,7 @@ export async function verifySignature(
signingUserId: string,
signingDeviceId: string,
signingKey: string,
) {
): Promise<void> {
const signKeyId = "ed25519:" + signingDeviceId;
const signatures = obj.signatures || {};
const userSigs = signatures[signingUserId] || {};
@@ -527,7 +527,7 @@ export function pkSign(obj: IObject, key: PkSigning, userId: string, pubKey: str
* @param {string} pubKey The public key to use to verify
* @param {string} userId The user ID who signed the object
*/
export function pkVerify(obj: IObject, pubKey: string, userId: string) {
export function pkVerify(obj: IObject, pubKey: string, userId: string): void {
const keyId = "ed25519:" + pubKey;
if (!(obj.signatures && obj.signatures[userId] && obj.signatures[userId][keyId])) {
throw new Error("No signature");
@@ -48,11 +48,11 @@ export class Backend implements CryptoStore {
/**
* @param {IDBDatabase} db
*/
constructor(private db: IDBDatabase) {
public constructor(private db: IDBDatabase) {
// make sure we close the db on `onversionchange` - otherwise
// attempts to delete the database will block (and subsequent
// attempts to re-create it will also block).
db.onversionchange = () => {
db.onversionchange = (): void => {
logger.log(`versionchange for indexeddb ${this.db.name}: closing`);
db.close();
};
@@ -103,7 +103,7 @@ export class Backend implements CryptoStore {
`enqueueing key request for ${requestBody.room_id} / ` +
requestBody.session_id,
);
txn.oncomplete = () => {resolve(request);};
txn.oncomplete = (): void => {resolve(request);};
const store = txn.objectStore("outgoingRoomKeyRequests");
store.add(request);
});
@@ -157,7 +157,7 @@ export class Backend implements CryptoStore {
requestBody.session_id,
]);
cursorReq.onsuccess = () => {
cursorReq.onsuccess = (): void => {
const cursor = cursorReq.result;
if (!cursor) {
// no match found
@@ -201,7 +201,7 @@ export class Backend implements CryptoStore {
let stateIndex = 0;
let result: OutgoingRoomKeyRequest;
function onsuccess(this: IDBRequest<IDBCursorWithValue | null>) {
function onsuccess(this: IDBRequest<IDBCursorWithValue | null>): void {
const cursor = this.result;
if (cursor) {
// got a match
@@ -243,8 +243,8 @@ export class Backend implements CryptoStore {
const index = store.index("state");
const request = index.getAll(wantedState);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
request.onsuccess = (): void => resolve(request.result);
request.onerror = (): void => reject(request.error);
});
}
@@ -256,7 +256,7 @@ export class Backend implements CryptoStore {
let stateIndex = 0;
const results: OutgoingRoomKeyRequest[] = [];
function onsuccess(this: IDBRequest<IDBCursorWithValue | null>) {
function onsuccess(this: IDBRequest<IDBCursorWithValue | null>): void {
const cursor = this.result;
if (cursor) {
const keyReq = cursor.value;
@@ -309,7 +309,7 @@ export class Backend implements CryptoStore {
): Promise<OutgoingRoomKeyRequest | null> {
let result: OutgoingRoomKeyRequest | null = null;
function onsuccess(this: IDBRequest<IDBCursorWithValue | null>) {
function onsuccess(this: IDBRequest<IDBCursorWithValue | null>): void {
const cursor = this.result;
if (!cursor) {
return;
@@ -348,7 +348,7 @@ export class Backend implements CryptoStore {
): Promise<OutgoingRoomKeyRequest | null> {
const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite");
const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
cursorReq.onsuccess = () => {
cursorReq.onsuccess = (): void => {
const cursor = cursorReq.result;
if (!cursor) {
return;
@@ -371,7 +371,7 @@ export class Backend implements CryptoStore {
public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get("-");
getReq.onsuccess = function() {
getReq.onsuccess = function(): void {
try {
func(getReq.result || null);
} catch (e) {
@@ -391,7 +391,7 @@ export class Backend implements CryptoStore {
): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get("crossSigningKeys");
getReq.onsuccess = function() {
getReq.onsuccess = function(): void {
try {
func(getReq.result || null);
} catch (e) {
@@ -407,7 +407,7 @@ export class Backend implements CryptoStore {
): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get(`ssss_cache:${type}`);
getReq.onsuccess = function() {
getReq.onsuccess = function(): void {
try {
func(getReq.result || null);
} catch (e) {
@@ -435,7 +435,7 @@ export class Backend implements CryptoStore {
public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void {
const objectStore = txn.objectStore("sessions");
const countReq = objectStore.count();
countReq.onsuccess = function() {
countReq.onsuccess = function(): void {
try {
func(countReq.result);
} catch (e) {
@@ -453,7 +453,7 @@ export class Backend implements CryptoStore {
const idx = objectStore.index("deviceKey");
const getReq = idx.openCursor(deviceKey);
const results: Parameters<Parameters<Backend["getEndToEndSessions"]>[2]>[0] = {};
getReq.onsuccess = function() {
getReq.onsuccess = function(): void {
const cursor = getReq.result;
if (cursor) {
results[cursor.value.sessionId] = {
@@ -479,7 +479,7 @@ export class Backend implements CryptoStore {
): void {
const objectStore = txn.objectStore("sessions");
const getReq = objectStore.get([deviceKey, sessionId]);
getReq.onsuccess = function() {
getReq.onsuccess = function(): void {
try {
if (getReq.result) {
func({
@@ -498,7 +498,7 @@ export class Backend implements CryptoStore {
public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo | null) => void): void {
const objectStore = txn.objectStore("sessions");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
getReq.onsuccess = function(): void {
try {
const cursor = getReq.result;
if (cursor) {
@@ -546,7 +546,7 @@ export class Backend implements CryptoStore {
const objectStore = txn.objectStore("session_problems");
const index = objectStore.index("deviceKey");
const req = index.getAll(deviceKey);
req.onsuccess = () => {
req.onsuccess = (): void => {
const problems = req.result;
if (!problems.length) {
result = null;
@@ -583,7 +583,7 @@ export class Backend implements CryptoStore {
return new Promise<void>((resolve) => {
const { userId, deviceInfo } = device;
const getReq = objectStore.get([userId, deviceInfo.deviceId]);
getReq.onsuccess = function() {
getReq.onsuccess = function(): void {
if (!getReq.result) {
objectStore.put({ userId, deviceId: deviceInfo.deviceId });
ret.push(device);
@@ -608,7 +608,7 @@ export class Backend implements CryptoStore {
let withheld: IWithheld | null | boolean = false;
const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.get([senderCurve25519Key, sessionId]);
getReq.onsuccess = function() {
getReq.onsuccess = function(): void {
try {
if (getReq.result) {
session = getReq.result.session;
@@ -625,7 +625,7 @@ export class Backend implements CryptoStore {
const withheldObjectStore = txn.objectStore("inbound_group_sessions_withheld");
const withheldGetReq = withheldObjectStore.get([senderCurve25519Key, sessionId]);
withheldGetReq.onsuccess = function() {
withheldGetReq.onsuccess = function(): void {
try {
if (withheldGetReq.result) {
withheld = withheldGetReq.result.session;
@@ -644,7 +644,7 @@ export class Backend implements CryptoStore {
public getAllEndToEndInboundGroupSessions(txn: IDBTransaction, func: (session: ISession | null) => void): void {
const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
getReq.onsuccess = function(): void {
const cursor = getReq.result;
if (cursor) {
try {
@@ -677,7 +677,7 @@ export class Backend implements CryptoStore {
const addReq = objectStore.add({
senderCurve25519Key, sessionId, session: sessionData,
});
addReq.onerror = (ev) => {
addReq.onerror = (ev): void => {
if (addReq.error?.name === 'ConstraintError') {
// This stops the error from triggering the txn's onerror
ev.stopPropagation();
@@ -722,7 +722,7 @@ export class Backend implements CryptoStore {
public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void {
const objectStore = txn.objectStore("device_data");
const getReq = objectStore.get("-");
getReq.onsuccess = function() {
getReq.onsuccess = function(): void {
try {
func(getReq.result || null);
} catch (e) {
@@ -745,7 +745,7 @@ export class Backend implements CryptoStore {
const rooms: Parameters<Parameters<Backend["getEndToEndRooms"]>[1]>[0] = {};
const objectStore = txn.objectStore("rooms");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
getReq.onsuccess = function(): void {
const cursor = getReq.result;
if (cursor) {
rooms[cursor.key as string] = cursor.value;
@@ -771,17 +771,17 @@ export class Backend implements CryptoStore {
"readonly",
);
txn.onerror = reject;
txn.oncomplete = function() {
txn.oncomplete = function(): void {
resolve(sessions);
};
const objectStore = txn.objectStore("sessions_needing_backup");
const sessionStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
getReq.onsuccess = function(): void {
const cursor = getReq.result;
if (cursor) {
const sessionGetReq = sessionStore.get(cursor.key);
sessionGetReq.onsuccess = function() {
sessionGetReq.onsuccess = function(): void {
sessions.push({
senderKey: sessionGetReq.result.senderCurve25519Key,
sessionId: sessionGetReq.result.sessionId,
@@ -804,7 +804,7 @@ export class Backend implements CryptoStore {
return new Promise((resolve, reject) => {
const req = objectStore.count();
req.onerror = reject;
req.onsuccess = () => resolve(req.result);
req.onsuccess = (): void => resolve(req.result);
});
}
@@ -852,7 +852,7 @@ export class Backend implements CryptoStore {
}
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
const req = objectStore.get([roomId]);
req.onsuccess = () => {
req.onsuccess = (): void => {
const { sessions } = req.result || { sessions: [] };
sessions.push([senderKey, sessionId]);
objectStore.put({ roomId, sessions });
@@ -871,7 +871,7 @@ export class Backend implements CryptoStore {
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
const req = objectStore.get([roomId]);
return new Promise((resolve, reject) => {
req.onsuccess = () => {
req.onsuccess = (): void => {
const { sessions } = req.result || { sessions: [] };
resolve(sessions);
};
@@ -891,7 +891,7 @@ export class Backend implements CryptoStore {
}
const objectStore = txn.objectStore("parked_shared_history");
const req = objectStore.get([roomId]);
req.onsuccess = () => {
req.onsuccess = (): void => {
const { parked } = req.result || { parked: [] };
parked.push(parkedData);
objectStore.put({ roomId, parked });
@@ -909,7 +909,7 @@ export class Backend implements CryptoStore {
}
const cursorReq = txn.objectStore("parked_shared_history").openCursor(roomId);
return new Promise((resolve, reject) => {
cursorReq.onsuccess = () => {
cursorReq.onsuccess = (): void => {
const cursor = cursorReq.result;
if (!cursor) {
resolve([]);
@@ -957,32 +957,32 @@ export class Backend implements CryptoStore {
type DbMigration = (db: IDBDatabase) => void;
const DB_MIGRATIONS: DbMigration[] = [
(db) => { createDatabase(db); },
(db) => { db.createObjectStore("account"); },
(db) => {
(db): void => { createDatabase(db); },
(db): void => { db.createObjectStore("account"); },
(db): void => {
const sessionsStore = db.createObjectStore("sessions", {
keyPath: ["deviceKey", "sessionId"],
});
sessionsStore.createIndex("deviceKey", "deviceKey");
},
(db) => {
(db): void => {
db.createObjectStore("inbound_group_sessions", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
},
(db) => { db.createObjectStore("device_data"); },
(db) => { db.createObjectStore("rooms"); },
(db) => {
(db): void => { db.createObjectStore("device_data"); },
(db): void => { db.createObjectStore("rooms"); },
(db): void => {
db.createObjectStore("sessions_needing_backup", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
},
(db) => {
(db): void => {
db.createObjectStore("inbound_group_sessions_withheld", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
},
(db) => {
(db): void => {
const problemsStore = db.createObjectStore("session_problems", {
keyPath: ["deviceKey", "time"],
});
@@ -992,12 +992,12 @@ const DB_MIGRATIONS: DbMigration[] = [
keyPath: ["userId", "deviceId"],
});
},
(db) => {
(db): void => {
db.createObjectStore("shared_history_inbound_group_sessions", {
keyPath: ["roomId"],
});
},
(db) => {
(db): void => {
db.createObjectStore("parked_shared_history", {
keyPath: ["roomId"],
});
@@ -1037,7 +1037,7 @@ interface IWrappedIDBTransaction extends IDBTransaction {
* Aborts a transaction with a given exception
* The transaction promise will be rejected with this exception.
*/
function abortWithException(txn: IDBTransaction, e: Error) {
function abortWithException(txn: IDBTransaction, e: Error): void {
// We cheekily stick our exception onto the transaction object here
// We could alternatively make the thing we pass back to the app
// an object containing the transaction and exception.
@@ -1052,13 +1052,13 @@ function abortWithException(txn: IDBTransaction, e: Error) {
function promiseifyTxn<T>(txn: IDBTransaction): Promise<T | null> {
return new Promise((resolve, reject) => {
txn.oncomplete = () => {
txn.oncomplete = (): void => {
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
reject((txn as IWrappedIDBTransaction)._mx_abortexception);
}
resolve(null);
};
txn.onerror = (event) => {
txn.onerror = (event): void => {
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
reject((txn as IWrappedIDBTransaction)._mx_abortexception);
} else {
@@ -1066,7 +1066,7 @@ function promiseifyTxn<T>(txn: IDBTransaction): Promise<T | null> {
reject(txn.error);
}
};
txn.onabort = (event) => {
txn.onabort = (event): void => {
if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) {
reject((txn as IWrappedIDBTransaction)._mx_abortexception);
} else {
+9 -9
View File
@@ -73,7 +73,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {IDBFactory} indexedDB global indexedDB instance
* @param {string} dbName name of db to connect to
*/
constructor(private readonly indexedDB: IDBFactory, private readonly dbName: string) {}
public constructor(private readonly indexedDB: IDBFactory, private readonly dbName: string) {}
/**
* Ensure the database exists and is up-to-date, or fall back to
@@ -99,24 +99,24 @@ export class IndexedDBCryptoStore implements CryptoStore {
const req = this.indexedDB.open(this.dbName, IndexedDBCryptoStoreBackend.VERSION);
req.onupgradeneeded = (ev) => {
req.onupgradeneeded = (ev): void => {
const db = req.result;
const oldVersion = ev.oldVersion;
IndexedDBCryptoStoreBackend.upgradeDatabase(db, oldVersion);
};
req.onblocked = () => {
req.onblocked = (): void => {
logger.log(
`can't yet open IndexedDBCryptoStore because it is open elsewhere`,
);
};
req.onerror = (ev) => {
req.onerror = (ev): void => {
logger.log("Error connecting to indexeddb", ev);
reject(req.error);
};
req.onsuccess = () => {
req.onsuccess = (): void => {
const db = req.result;
logger.log(`connected to indexeddb ${this.dbName}`);
@@ -179,18 +179,18 @@ export class IndexedDBCryptoStore implements CryptoStore {
logger.log(`Removing indexeddb instance: ${this.dbName}`);
const req = this.indexedDB.deleteDatabase(this.dbName);
req.onblocked = () => {
req.onblocked = (): void => {
logger.log(
`can't yet delete IndexedDBCryptoStore because it is open elsewhere`,
);
};
req.onerror = (ev) => {
req.onerror = (ev): void => {
logger.log("Error deleting data from indexeddb", ev);
reject(req.error);
};
req.onsuccess = () => {
req.onsuccess = (): void => {
logger.log(`Removed indexeddb instance: ${this.dbName}`);
resolve();
};
@@ -322,7 +322,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param {*} txn An active transaction. See doTxn().
* @param {function(string)} func Called with the account pickle
*/
public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void) {
public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void): void {
this.backend!.getAccount(txn, func);
}
@@ -76,7 +76,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
return false;
}
constructor(private readonly store: Storage) {
public constructor(private readonly store: Storage) {
super();
}
@@ -154,7 +154,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
setJsonItem(this.store, key, problems);
}
async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise<IProblem | null> {
const key = keyEndToEndSessionProblems(deviceKey);
const problems = getJsonItem<IProblem[]>(this.store, key) || [];
if (!problems.length) {
@@ -408,7 +408,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
setJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`, key);
}
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T): Promise<T> {
public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T): Promise<T> {
return Promise.resolve(func(null));
}
}
+1 -1
View File
@@ -279,7 +279,7 @@ export class MemoryCryptoStore implements CryptoStore {
// Olm Account
public getAccount(txn: unknown, func: (accountPickle: string | null) => void) {
public getAccount(txn: unknown, func: (accountPickle: string | null) => void): void {
func(this.account);
}
+4 -4
View File
@@ -34,7 +34,7 @@ import { ListenerMap, TypedEventEmitter } from "../../models/typed-event-emitter
const timeoutException = new Error("Verification timed out");
export class SwitchStartEventError extends Error {
constructor(public readonly startEvent: MatrixEvent | null) {
public constructor(public readonly startEvent: MatrixEvent | null) {
super();
}
}
@@ -91,7 +91,7 @@ export class VerificationBase<
* @param {object} [request] the key verification request object related to
* this verification, if any
*/
constructor(
public constructor(
public readonly channel: IVerificationChannel,
public readonly baseApis: MatrixClient,
public readonly userId: string,
@@ -286,12 +286,12 @@ export class VerificationBase<
if (this.promise) return this.promise;
this.promise = new Promise((resolve, reject) => {
this.resolve = (...args) => {
this.resolve = (...args): void => {
this._done = true;
this.endTimer();
resolve(...args);
};
this.reject = (e: Error | MatrixEvent) => {
this.reject = (e: Error | MatrixEvent): void => {
this._done = true;
this.endTimer();
reject(e);
+5 -5
View File
@@ -154,7 +154,7 @@ interface IQrData {
}
export class QRCodeData {
constructor(
public constructor(
public readonly mode: Mode,
private readonly sharedSecret: string,
// only set when mode is MODE_VERIFY_OTHER_USER, master key of other party at time of generating QR code
@@ -283,21 +283,21 @@ export class QRCodeData {
private static generateBuffer(qrData: IQrData): Buffer {
let buf = Buffer.alloc(0); // we'll concat our way through life
const appendByte = (b) => {
const appendByte = (b): void => {
const tmpBuf = Buffer.from([b]);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendInt = (i) => {
const appendInt = (i): void => {
const tmpBuf = Buffer.alloc(2);
tmpBuf.writeInt16BE(i, 0);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendStr = (s, enc, withLengthPrefix = true) => {
const appendStr = (s, enc, withLengthPrefix = true): void => {
const tmpBuf = Buffer.from(s, enc);
if (withLengthPrefix) appendInt(tmpBuf.byteLength);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendEncBase64 = (b64) => {
const appendEncBase64 = (b64): void => {
const b = decodeBase64(b64);
const tmpBuf = Buffer.from(b);
buf = Buffer.concat([buf, tmpBuf]);
+10 -8
View File
@@ -170,10 +170,12 @@ const macMethods = {
"hmac-sha256": "calculate_mac_long_kdf",
};
function calculateMAC(olmSAS: OlmSAS, method: string) {
return function(...args) {
type Method = keyof typeof macMethods;
function calculateMAC(olmSAS: OlmSAS, method: Method) {
return function(...args): string {
const macFunction = olmSAS[macMethods[method]];
const mac = macFunction.apply(olmSAS, args);
const mac: string = macFunction.apply(olmSAS, args);
logger.log("SAS calculateMAC:", method, args, mac);
return mac;
};
@@ -208,7 +210,7 @@ const calculateKeyAgreement = {
*/
const KEY_AGREEMENT_LIST = ["curve25519-hkdf-sha256", "curve25519"];
const HASHES_LIST = ["sha256"];
const MAC_LIST = ["org.matrix.msc3783.hkdf-hmac-sha256", "hkdf-hmac-sha256", "hmac-sha256"];
const MAC_LIST: Method[] = ["org.matrix.msc3783.hkdf-hmac-sha256", "hkdf-hmac-sha256", "hmac-sha256"];
const SAS_LIST = Object.keys(sasGenerators);
const KEY_AGREEMENT_SET = new Set(KEY_AGREEMENT_LIST);
@@ -300,13 +302,13 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
keyAgreement: string,
sasMethods: string[],
olmSAS: OlmSAS,
macMethod: string,
macMethod: Method,
): Promise<void> {
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
const verifySAS = new Promise<void>((resolve, reject) => {
this.sasEvent = {
sas: generateSas(sasBytes, sasMethods),
confirm: async () => {
confirm: async (): Promise<void> => {
try {
await this.sendMAC(olmSAS, macMethod);
resolve();
@@ -443,7 +445,7 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
}
}
private sendMAC(olmSAS: OlmSAS, method: string): Promise<void> {
private sendMAC(olmSAS: OlmSAS, method: Method): Promise<void> {
const mac = {};
const keyList: string[] = [];
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
@@ -475,7 +477,7 @@ export class SAS extends Base<SasEvent, EventHandlerMap> {
return this.send(EventType.KeyVerificationMac, { mac, keys });
}
private async checkMAC(olmSAS: OlmSAS, content: IContent, method: string): Promise<void> {
private async checkMAC(olmSAS: OlmSAS, content: IContent, method: Method): Promise<void> {
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this.userId + this.deviceId
+ this.baseApis.getUserId() + this.baseApis.deviceId
@@ -44,7 +44,7 @@ export class InRoomChannel implements IVerificationChannel {
* @param {string} roomId id of the room where verification events should be posted in, should be a DM with the given user.
* @param {string} userId id of user that the verification request is directed at, should be present in the room.
*/
constructor(
public constructor(
private readonly client: MatrixClient,
public readonly roomId: string,
public userId?: string,
@@ -42,7 +42,7 @@ export class ToDeviceChannel implements IVerificationChannel {
public request?: VerificationRequest;
// userId and devices of user we're about to verify
constructor(
public constructor(
private readonly client: MatrixClient,
public readonly userId: string,
private readonly devices: string[],
@@ -116,7 +116,7 @@ export class VerificationRequest<
public _cancellingUserId?: string; // Used in tests only
private _verifier?: VerificationBase<any, any>;
constructor(
public constructor(
public readonly channel: C,
private readonly verificationMethods: Map<VerificationMethod, typeof VerificationBase>,
private readonly client: MatrixClient,
@@ -498,7 +498,7 @@ export class VerificationRequest<
*/
public waitFor(fn: (request: VerificationRequest) => boolean): Promise<VerificationRequest> {
return new Promise((resolve, reject) => {
const check = () => {
const check = (): boolean => {
let handled = false;
if (fn(this)) {
resolve(this);
@@ -539,7 +539,7 @@ export class VerificationRequest<
private calculatePhaseTransitions(): ITransition[] {
const transitions: ITransition[] = [{ phase: PHASE_UNSENT }];
const phase = () => transitions[transitions.length - 1].phase;
const phase = (): Phase => transitions[transitions.length - 1].phase;
// always pass by .request first to be sure channel.userId has been set
const hasRequestByThem = this.eventsByThem.has(REQUEST_TYPE);
@@ -816,7 +816,7 @@ export class VerificationRequest<
}
}
private cancelOnTimeout = async () => {
private cancelOnTimeout = async (): Promise<void> => {
try {
if (this.initiatedByMe) {
await this.cancel({
+9 -7
View File
@@ -101,7 +101,7 @@ export class RoomWidgetClient extends MatrixClient {
private lifecycle?: AbortController;
private syncState: SyncState | null = null;
constructor(
public constructor(
private readonly widgetApi: WidgetApi,
private readonly capabilities: ICapabilities,
private readonly roomId: string,
@@ -211,7 +211,7 @@ export class RoomWidgetClient extends MatrixClient {
if (this.capabilities.turnServers) this.watchTurnServers();
}
public stopClient() {
public stopClient(): void {
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
@@ -288,7 +288,7 @@ export class RoomWidgetClient extends MatrixClient {
return this.syncState;
}
private setSyncState(state: SyncState) {
private setSyncState(state: SyncState): void {
const oldState = this.syncState;
this.syncState = state;
this.emit(ClientEvent.Sync, state, oldState);
@@ -298,7 +298,7 @@ export class RoomWidgetClient extends MatrixClient {
await this.widgetApi.transport.reply<IWidgetApiAcknowledgeResponseData>(ev.detail, {});
}
private onEvent = async (ev: CustomEvent<ISendEventToWidgetActionRequest>) => {
private onEvent = async (ev: CustomEvent<ISendEventToWidgetActionRequest>): Promise<void> => {
ev.preventDefault();
// Verify the room ID matches, since it's possible for the client to
@@ -317,7 +317,7 @@ export class RoomWidgetClient extends MatrixClient {
await this.ack(ev);
};
private onToDevice = async (ev: CustomEvent<ISendToDeviceToWidgetActionRequest>) => {
private onToDevice = async (ev: CustomEvent<ISendToDeviceToWidgetActionRequest>): Promise<void> => {
ev.preventDefault();
const event = new MatrixEvent({
@@ -333,9 +333,11 @@ export class RoomWidgetClient extends MatrixClient {
await this.ack(ev);
};
private async watchTurnServers() {
private async watchTurnServers(): Promise<void> {
const servers = this.widgetApi.getTurnServers();
const onClientStopped = () => servers.return(undefined);
const onClientStopped = (): void => {
servers.return(undefined);
};
this.lifecycle!.signal.addEventListener("abort", onClientStopped);
try {
+1 -1
View File
@@ -29,7 +29,7 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
let preventReEmit = Boolean(options.preventReEmit);
const decrypt = options.decrypt !== false;
function mapper(plainOldJsObject: Partial<IEvent>) {
function mapper(plainOldJsObject: Partial<IEvent>): MatrixEvent {
if (options.toDevice) {
delete plainOldJsObject.room_id;
}
+1 -1
View File
@@ -73,7 +73,7 @@ export interface IFilterComponent {
* @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true }
*/
export class FilterComponent {
constructor(private filterJson: IFilterComponent, public readonly userId?: string | undefined | null) {}
public constructor(private filterJson: IFilterComponent, public readonly userId?: string | undefined | null) {}
/**
* Checks with the filter component matches the given event
+6 -6
View File
@@ -31,7 +31,7 @@ import { MatrixEvent } from "./models/event";
* @param {string} keyNesting
* @param {*} val
*/
function setProp(obj: object, keyNesting: string, val: any) {
function setProp(obj: object, keyNesting: string, val: any): void {
const nestedKeys = keyNesting.split(".");
let currentObj = obj;
for (let i = 0; i < (nestedKeys.length - 1); i++) {
@@ -88,7 +88,7 @@ interface IRoomFilter {
* @prop {?string} filterId The filter ID
*/
export class Filter {
static LAZY_LOADING_MESSAGES_FILTER = {
public static LAZY_LOADING_MESSAGES_FILTER = {
lazy_load_members: true,
};
@@ -110,7 +110,7 @@ export class Filter {
private roomFilter?: FilterComponent;
private roomTimelineFilter?: FilterComponent;
constructor(public readonly userId: string | undefined | null, public filterId?: string) {}
public constructor(public readonly userId: string | undefined | null, public filterId?: string) {}
/**
* Get the ID of this filter on your homeserver (if known)
@@ -132,7 +132,7 @@ export class Filter {
* Set the JSON body of the filter
* @param {Object} definition The filter definition
*/
public setDefinition(definition: IFilterDefinition) {
public setDefinition(definition: IFilterDefinition): void {
this.definition = definition;
// This is all ported from synapse's FilterCollection()
@@ -225,7 +225,7 @@ export class Filter {
* Set the max number of events to return for each room's timeline.
* @param {Number} limit The max number of events to return for each room.
*/
public setTimelineLimit(limit: number) {
public setTimelineLimit(limit: number): void {
setProp(this.definition, "room.timeline.limit", limit);
}
@@ -255,7 +255,7 @@ export class Filter {
* @param {boolean} includeLeave True to make rooms the user has left appear
* in responses.
*/
public setIncludeLeaveRooms(includeLeave: boolean) {
public setIncludeLeaveRooms(includeLeave: boolean): void {
setProp(this.definition, "room.include_leave", includeLeave);
}
}
+4 -4
View File
@@ -31,7 +31,7 @@ interface IErrorJson extends Partial<IUsageLimit> {
* @param {number} httpStatus The HTTP response status code.
*/
export class HTTPError extends Error {
constructor(msg: string, public readonly httpStatus?: number) {
public constructor(msg: string, public readonly httpStatus?: number) {
super(msg);
}
}
@@ -51,7 +51,7 @@ export class MatrixError extends HTTPError {
public readonly errcode?: string;
public data: IErrorJson;
constructor(
public constructor(
errorJson: IErrorJson = {},
public readonly httpStatus?: number,
public url?: string,
@@ -79,11 +79,11 @@ export class MatrixError extends HTTPError {
* @constructor
*/
export class ConnectionError extends Error {
constructor(message: string, cause?: Error) {
public constructor(message: string, cause?: Error) {
super(message + (cause ? `: ${cause.message}` : ""));
}
get name() {
public get name(): string {
return "ConnectionError";
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ export type ResponseType<T, O extends IHttpOpts> =
export class FetchHttpApi<O extends IHttpOpts> {
private abortController = new AbortController();
constructor(
public constructor(
private eventEmitter: TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>,
public readonly opts: O,
) {
+3 -3
View File
@@ -77,7 +77,7 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
if (global.XMLHttpRequest) {
const xhr = new global.XMLHttpRequest();
const timeoutFn = function() {
const timeoutFn = function(): void {
xhr.abort();
defer.reject(new Error("Timeout"));
};
@@ -85,7 +85,7 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
// set an initial timeout of 30s; we'll advance it each time we get a progress notification
let timeoutTimer = callbacks.setTimeout(timeoutFn, 30000);
xhr.onreadystatechange = function() {
xhr.onreadystatechange = function(): void {
switch (xhr.readyState) {
case global.XMLHttpRequest.DONE:
callbacks.clearTimeout(timeoutTimer);
@@ -113,7 +113,7 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
}
};
xhr.upload.onprogress = (ev: ProgressEvent) => {
xhr.upload.onprogress = (ev: ProgressEvent): void => {
callbacks.clearTimeout(timeoutTimer);
upload.loaded = ev.loaded;
upload.total = ev.total;
+2 -2
View File
@@ -36,13 +36,13 @@ export function anySignal(signals: AbortSignal[]): {
} {
const controller = new AbortController();
function cleanup() {
function cleanup(): void {
for (const signal of signals) {
signal.removeEventListener("abort", onAbort);
}
}
function onAbort() {
function onAbort(): void {
controller.abort();
cleanup();
}
+4 -4
View File
@@ -26,13 +26,13 @@ export function exists(indexedDB: IDBFactory, dbName: string): Promise<boolean>
return new Promise<boolean>((resolve, reject) => {
let exists = true;
const req = indexedDB.open(dbName);
req.onupgradeneeded = () => {
req.onupgradeneeded = (): void => {
// Since we did not provide an explicit version when opening, this event
// should only fire if the DB did not exist before at any version.
exists = false;
};
req.onblocked = () => reject(req.error);
req.onsuccess = () => {
req.onblocked = (): void => reject(req.error);
req.onsuccess = (): void => {
const db = req.result;
db.close();
if (!exists) {
@@ -45,6 +45,6 @@ export function exists(indexedDB: IDBFactory, dbName: string): Promise<boolean>
}
resolve(exists);
};
req.onerror = ev => reject(req.error);
req.onerror = (): void => reject(req.error);
});
}
+3 -3
View File
@@ -100,7 +100,7 @@ class NoAuthFlowFoundError extends Error {
public name = "NoAuthFlowFoundError";
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
constructor(m: string, public readonly required_stages: string[], public readonly flows: IFlow[]) {
public constructor(m: string, public readonly required_stages: string[], public readonly flows: IFlow[]) {
super(m);
}
}
@@ -218,7 +218,7 @@ export class InteractiveAuth {
// the promise the will resolve/reject when it completes
private submitPromise: Promise<void> | null = null;
constructor(opts: IOpts) {
public constructor(opts: IOpts) {
this.matrixClient = opts.matrixClient;
this.data = opts.authData || {};
this.requestCallback = opts.doRequest;
@@ -419,7 +419,7 @@ export class InteractiveAuth {
/**
* Requests a new email token and sets the email sid for the validation session
*/
public requestEmailToken = async () => {
public requestEmailToken = async (): Promise<void> => {
if (!this.requestingEmailToken) {
logger.trace("Requesting email token. Attempt: " + this.emailAttempt);
// If we've picked a flow with email auth, we send the email
+2 -2
View File
@@ -35,7 +35,7 @@ const DEFAULT_NAMESPACE = "matrix";
// console methods at initialization time by a factory that looks up the console methods
// when logging so we always get the current value of console methods.
log.methodFactory = function(methodName, logLevel, loggerName) {
return function(this: PrefixedLogger, ...args) {
return function(this: PrefixedLogger, ...args): void {
/* eslint-disable @typescript-eslint/no-invalid-this */
if (this.prefix) {
args.unshift(this.prefix);
@@ -67,7 +67,7 @@ export interface PrefixedLogger extends Logger {
prefix: string;
}
function extendLogger(logger: Logger) {
function extendLogger(logger: Logger): void {
(<PrefixedLogger>logger).withPrefix = function(prefix: string): PrefixedLogger {
const existingPrefix = this.prefix || "";
return getPrefixedLogger(existingPrefix + prefix);
+3 -2
View File
@@ -21,6 +21,7 @@ import { MemoryStore } from "./store/memory";
import { MatrixScheduler } from "./scheduler";
import { MatrixClient, ICreateClientOpts } from "./client";
import { RoomWidgetClient, ICapabilities } from "./embedded";
import { CryptoStore } from "./crypto/store/base";
export * from "./client";
export * from "./embedded";
@@ -64,7 +65,7 @@ export {
} from "./webrtc/groupCall";
export type { GroupCall } from "./webrtc/groupCall";
let cryptoStoreFactory = () => new MemoryCryptoStore;
let cryptoStoreFactory = (): CryptoStore => new MemoryCryptoStore;
/**
* Configure a different factory to be used for creating crypto stores
@@ -72,7 +73,7 @@ let cryptoStoreFactory = () => new MemoryCryptoStore;
* @param {Function} fac a function which will return a new
* {@link module:crypto.store.base~CryptoStore}.
*/
export function setCryptoStoreFactory(fac) {
export function setCryptoStoreFactory(fac: () => CryptoStore): void {
cryptoStoreFactory = fac;
}
+2 -2
View File
@@ -172,7 +172,7 @@ export class MSC3089TreeSpace {
const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels");
const pls = currentPls.getContent() || {};
const pls = currentPls?.getContent() || {};
const viewLevel = pls['users_default'] || 0;
const editLevel = pls['events_default'] || 50;
const adminLevel = pls['events']?.[EventType.RoomPowerLevels] || 100;
@@ -207,7 +207,7 @@ export class MSC3089TreeSpace {
const currentPls = this.room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels");
const pls = currentPls.getContent() || {};
const pls = currentPls?.getContent() || {};
const viewLevel = pls['users_default'] || 0;
const editLevel = pls['events_default'] || 50;
const adminLevel = pls['events']?.[EventType.RoomPowerLevels] || 100;
+2 -2
View File
@@ -56,7 +56,7 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
private livenessWatchTimeout?: ReturnType<typeof setTimeout>;
private _latestLocationEvent?: MatrixEvent;
constructor(
public constructor(
private rootEvent: MatrixEvent,
) {
super();
@@ -180,7 +180,7 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
}
}
private clearLatestLocation = () => {
private clearLatestLocation = (): void => {
this._latestLocationEvent = undefined;
this.emit(BeaconEvent.LocationUpdate, this.latestLocationState!);
};
+1 -1
View File
@@ -42,7 +42,7 @@ export class EventContext {
*
* @constructor
*/
constructor(public readonly ourEvent: MatrixEvent) {
public constructor(public readonly ourEvent: MatrixEvent) {
this.timeline = [ourEvent];
}
+3 -3
View File
@@ -36,7 +36,7 @@ if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
debuglog = logger.log.bind(logger);
} else {
debuglog = function() {};
debuglog = function(): void {};
}
interface IOpts {
@@ -135,7 +135,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
* @param {boolean} isThreadTimeline Whether this timeline set relates to a thread list timeline
* (e.g., All threads or My threads)
*/
constructor(
public constructor(
public readonly room: Room | undefined,
opts: IOpts = {},
client?: MatrixClient,
@@ -702,7 +702,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
}
if (timeline.getTimelineSet() !== this) {
throw new Error(`EventTimelineSet.addEventToTimeline: Timeline=${timeline.toString()} does not belong " +
throw new Error(`EventTimelineSet.addEventToTimeline: Timeline=${timeline.toString()} does not belong " +
"in timelineSet(threadId=${this.thread?.id})`);
}
+2 -2
View File
@@ -67,7 +67,7 @@ export class EventTimeline {
* @param {RoomState} stateContext the room state to be queried
* @param {boolean} toStartOfTimeline if true the event's forwardLooking flag is set false
*/
static setEventMetadata(event: MatrixEvent, stateContext: RoomState, toStartOfTimeline: boolean): void {
public static setEventMetadata(event: MatrixEvent, stateContext: RoomState, toStartOfTimeline: boolean): void {
// When we try to generate a sentinel member before we have that member
// in the members object, we still generate a sentinel but it doesn't
// have a membership event, so test to see if events.member is set. We
@@ -130,7 +130,7 @@ export class EventTimeline {
* @param {EventTimelineSet} eventTimelineSet the set of timelines this is part of
* @constructor
*/
constructor(private readonly eventTimelineSet: EventTimelineSet) {
public constructor(private readonly eventTimelineSet: EventTimelineSet) {
this.roomId = eventTimelineSet.room?.roomId ?? null;
if (this.roomId) {
this.startState = new RoomState(this.roomId);
+16 -6
View File
@@ -309,7 +309,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* that getDirectionalContent() will return event.content and not event.prev_content.
* Default: true. <strong>This property is experimental and may change.</strong>
*/
constructor(public event: Partial<IEvent> = {}) {
public constructor(public event: Partial<IEvent> = {}) {
super();
// intern the values of matrix events to force share strings and reduce the
@@ -352,7 +352,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
return this._cachedExtEv;
}
private invalidateExtensibleEvent() {
private invalidateExtensibleEvent(): void {
// just reset the flag - that'll trick the getter into parsing a new event
this._hasCachedExtEv = false;
}
@@ -679,7 +679,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
return this.clearEvent?.content?.msgtype === "m.bad.encrypted";
}
public shouldAttemptDecryption() {
public shouldAttemptDecryption(): boolean {
if (this.isRedacted()) return false;
if (this.isBeingDecrypted()) return false;
if (this.clearEvent) return false;
@@ -1443,11 +1443,21 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* Checks if this event is associated with another event. See `getAssociatedId`.
*
* @return {boolean}
* @deprecated use hasAssociation instead.
*/
public hasAssocation(): boolean {
return !!this.getAssociatedId();
}
/**
* Checks if this event is associated with another event. See `getAssociatedId`.
*
* @return {boolean}
*/
public hasAssociation(): boolean {
return !!this.getAssociatedId();
}
/**
* Update the related id with a new one.
*
@@ -1480,7 +1490,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* more information.
* @returns {boolean} True if the event is cancelled, false otherwise.
*/
isCancelled(): boolean {
public isCancelled(): boolean {
return this._isCancelled;
}
@@ -1498,7 +1508,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* features (such as sender) surrounding the event.
* @returns {MatrixEvent} A snapshot of this event.
*/
toSnapshot(): MatrixEvent {
public toSnapshot(): MatrixEvent {
const ev = new MatrixEvent(JSON.parse(JSON.stringify(this.event)));
for (const [p, v] of Object.entries(this)) {
if (p !== "event") { // exclude the thing we just cloned
@@ -1515,7 +1525,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* @param {MatrixEvent} otherEvent The other event to check against.
* @returns {boolean} True if the events are the same, false otherwise.
*/
isEquivalentTo(otherEvent: MatrixEvent): boolean {
public isEquivalentTo(otherEvent: MatrixEvent): boolean {
if (!otherEvent) return false;
if (otherEvent === this) return true;
const myProps = deepSortedObjectEntries(this.event);
+3 -3
View File
@@ -73,7 +73,7 @@ export enum PolicyScope {
* our data structures.
*/
export class IgnoredInvites {
constructor(
public constructor(
private readonly client: MatrixClient,
) {
}
@@ -99,7 +99,7 @@ export class IgnoredInvites {
/**
* Remove a rule.
*/
public async removeRule(event: MatrixEvent) {
public async removeRule(event: MatrixEvent): Promise<void> {
await this.client.redactEvent(event.getRoomId()!, event.getId()!);
}
@@ -314,7 +314,7 @@ export class IgnoredInvites {
/**
* Modify in place the `IGNORE_INVITES_POLICIES` object from account data.
*/
private async withIgnoreInvitesPolicies(cb: (ignoreInvitesPolicies: {[key: string]: any}) => void) {
private async withIgnoreInvitesPolicies(cb: (ignoreInvitesPolicies: {[key: string]: any}) => void): Promise<void> {
const { policies, ignoreInvitesPolicies } = this.getPoliciesAndIgnoreInvitesPolicies();
cb(ignoreInvitesPolicies);
policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name] = ignoreInvitesPolicies;
+10 -36
View File
@@ -11,15 +11,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ReceiptType } from "../@types/read_receipts";
import {
CachedReceipt,
MAIN_ROOM_TIMELINE,
Receipt,
ReceiptCache,
Receipts,
ReceiptType,
WrappedReceipt,
} from "../@types/read_receipts";
import { ListenerMap, TypedEventEmitter } from "./typed-event-emitter";
import * as utils from "../utils";
import { MatrixEvent } from "./event";
import { EventType } from "../@types/event";
import { EventTimelineSet } from "./event-timeline-set";
export const MAIN_ROOM_TIMELINE = "main";
export function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent {
return new MatrixEvent({
content: {
@@ -27,7 +33,7 @@ export function synthesizeReceipt(userId: string, event: MatrixEvent, receiptTyp
[receiptType]: {
[userId]: {
ts: event.getTs(),
threadId: event.threadRootId ?? MAIN_ROOM_TIMELINE,
thread_id: event.threadRootId ?? MAIN_ROOM_TIMELINE,
},
},
},
@@ -37,40 +43,8 @@ export function synthesizeReceipt(userId: string, event: MatrixEvent, receiptTyp
});
}
export interface Receipt {
ts: number;
thread_id?: string;
}
export interface WrappedReceipt {
eventId: string;
data: Receipt;
}
interface CachedReceipt {
type: ReceiptType;
userId: string;
data: Receipt;
}
type ReceiptCache = {[eventId: string]: CachedReceipt[]};
export interface ReceiptContent {
[eventId: string]: {
[key in ReceiptType]: {
[userId: string]: Receipt;
};
};
}
const ReceiptPairRealIndex = 0;
const ReceiptPairSyntheticIndex = 1;
// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer.
type Receipts = {
[receiptType: string]: {
[userId: string]: [WrappedReceipt | null, WrappedReceipt | null]; // Pair<real receipt, synthetic receipt> (both nullable)
};
};
export abstract class ReadReceipt<
Events extends string,
+2 -2
View File
@@ -29,11 +29,11 @@ export class RelatedRelations {
return this.relations.reduce<MatrixEvent[]>((c, p) => [...c, ...p.getRelations()], []);
}
public on<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>) {
public on<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>): void {
this.relations.forEach(r => r.on(ev, fn));
}
public off<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>) {
public off<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>): void {
this.relations.forEach(r => r.off(ev, fn));
}
}
+2 -2
View File
@@ -26,7 +26,7 @@ export class RelationsContainer {
// this.relations.get(parentEventId).get(relationType).get(relationEventType)
private relations = new Map<string, Map<RelationType | string, Map<EventType | string, Relations>>>();
constructor(private readonly client: MatrixClient, private readonly room?: Room) {
public constructor(private readonly client: MatrixClient, private readonly room?: Room) {
}
/**
@@ -98,7 +98,7 @@ export class RelationsContainer {
const relation = event.getRelation();
if (!relation) return;
const onEventDecrypted = () => {
const onEventDecrypted = (): void => {
if (event.isDecryptionFailure()) {
// This could for example happen if the encryption keys are not yet available.
// The event may still be decrypted later. Register the listener again.
+6 -6
View File
@@ -60,7 +60,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
* @param {MatrixClient|Room} client
* The client which created this instance. For backwards compatibility also accepts a Room.
*/
constructor(
public constructor(
public readonly relationType: RelationType | string,
public readonly eventType: string,
client: MatrixClient | Room,
@@ -75,7 +75,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
* @param {MatrixEvent} event
* The new relation event to be added.
*/
public async addEvent(event: MatrixEvent) {
public async addEvent(event: MatrixEvent): Promise<void> {
if (this.relationEventIds.has(event.getId()!)) {
return;
}
@@ -123,7 +123,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
* @param {MatrixEvent} event
* The relation event to remove.
*/
private async removeEvent(event: MatrixEvent) {
private async removeEvent(event: MatrixEvent): Promise<void> {
if (!this.relations.has(event)) {
return;
}
@@ -160,7 +160,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
* @param {MatrixEvent} event The event whose status has changed
* @param {EventStatus} status The new status
*/
private onEventStatus = (event: MatrixEvent, status: EventStatus | null) => {
private onEventStatus = (event: MatrixEvent, status: EventStatus | null): void => {
if (!event.isSending()) {
// Sending is done, so we don't need to listen anymore
event.removeListener(MatrixEventEvent.Status, this.onEventStatus);
@@ -356,7 +356,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
/*
* @param {MatrixEvent} targetEvent the event the relations are related to.
*/
public async setTargetEvent(event: MatrixEvent) {
public async setTargetEvent(event: MatrixEvent): Promise<void> {
if (this.targetEvent) {
return;
}
@@ -374,7 +374,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
this.maybeEmitCreated();
}
private maybeEmitCreated() {
private maybeEmitCreated(): void {
if (this.creationEmitted) {
return;
}
+2 -2
View File
@@ -83,7 +83,7 @@ export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEve
* @prop {MatrixEvent} events.member The m.room.member event for this RoomMember.
* @prop {boolean} disambiguate True if the member's name is disambiguated.
*/
constructor(public readonly roomId: string, public readonly userId: string) {
public constructor(public readonly roomId: string, public readonly userId: string) {
super();
this.name = userId;
@@ -232,7 +232,7 @@ export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEve
/**
* Update the last modified time to the current time.
*/
private updateModifiedTime() {
private updateModifiedTime(): void {
this.modified = Date.now();
}
+4 -4
View File
@@ -136,7 +136,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
* events dictionary, keyed on the event type and then the state_key value.
* @prop {string} paginationToken The pagination token for this state.
*/
constructor(public readonly roomId: string, private oobMemberFlags = { status: OobStatus.NotStarted }) {
public constructor(public readonly roomId: string, private oobMemberFlags = { status: OobStatus.NotStarted }) {
super();
this.updateModifiedTime();
}
@@ -250,8 +250,8 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
* <code>undefined</code>, else a single event (or null if no match found).
*/
public getStateEvents(eventType: EventType | string): MatrixEvent[];
public getStateEvents(eventType: EventType | string, stateKey: string): MatrixEvent;
public getStateEvents(eventType: EventType | string, stateKey?: string) {
public getStateEvents(eventType: EventType | string, stateKey: string): MatrixEvent | null;
public getStateEvents(eventType: EventType | string, stateKey?: string): MatrixEvent[] | MatrixEvent | null {
if (!this.events.has(eventType)) {
// no match
return stateKey === undefined ? [] : null;
@@ -342,7 +342,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
* @fires module:client~MatrixClient#event:"RoomState.events"
* @fires module:client~MatrixClient#event:"RoomStateEvent.Marker"
*/
public setStateEvents(stateEvents: MatrixEvent[], markerFoundOptions?: IMarkerFoundOptions) {
public setStateEvents(stateEvents: MatrixEvent[], markerFoundOptions?: IMarkerFoundOptions): void {
this.updateModifiedTime();
// update the core event dict
+1 -1
View File
@@ -46,6 +46,6 @@ interface IInfo {
* @param {Number} info.timestamp The timestamp for this room.
*/
export class RoomSummary {
constructor(public readonly roomId: string, info?: IInfo) {}
public constructor(public readonly roomId: string, info?: IInfo) {}
}
+34 -19
View File
@@ -54,14 +54,11 @@ import {
FILTER_RELATED_BY_SENDERS,
ThreadFilterType,
} from "./thread";
import { ReceiptType } from "../@types/read_receipts";
import { MAIN_ROOM_TIMELINE, Receipt, ReceiptContent, ReceiptType } from "../@types/read_receipts";
import { IStateEventWithRoomId } from "../@types/search";
import { RelationsContainer } from "./relations-container";
import {
MAIN_ROOM_TIMELINE,
ReadReceipt,
Receipt,
ReceiptContent,
synthesizeReceipt,
} from "./read-receipt";
import { Feature, ServerSupport } from "../feature";
@@ -320,7 +317,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
* @param {boolean} [opts.timelineSupport = false] Set to true to enable improved
* timeline support.
*/
constructor(
public constructor(
public readonly roomId: string,
public readonly client: MatrixClient,
public readonly myUserId: string,
@@ -1186,7 +1183,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
* for this type.
*/
public getUnreadNotificationCount(type = NotificationCountType.Total): number {
let count = this.notificationCounts[type] ?? 0;
let count = this.getRoomUnreadNotificationCount(type);
if (this.client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported) {
for (const threadNotification of this.threadNotifications.values()) {
count += threadNotification[type] ?? 0;
@@ -1195,6 +1192,27 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
return count;
}
/**
* Get the notification for the event context (room or thread timeline)
*/
public getUnreadCountForEventContext(type = NotificationCountType.Total, event: MatrixEvent): number {
const isThreadEvent = !!event.threadRootId && !event.isThreadRoot;
return (isThreadEvent
? this.getThreadUnreadNotificationCount(event.threadRootId, type)
: this.getRoomUnreadNotificationCount(type)) ?? 0;
}
/**
* Get one of the notification counts for this room
* @param {String} type The type of notification count to get. default: 'total'
* @return {Number} The notification count, or undefined if there is no count
* for this type.
*/
public getRoomUnreadNotificationCount(type = NotificationCountType.Total): number {
return this.notificationCounts[type] ?? 0;
}
/**
* @experimental
* Get one of the notification counts for a thread
@@ -1678,7 +1696,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
Array.from(this.threads)
.forEach(([, thread]) => {
if (thread.length === 0) return;
const currentUserParticipated = thread.events.some(event => {
const currentUserParticipated = thread.timeline.some(event => {
return event.getSender() === this.client.getUserId();
});
if (filterType !== ThreadFilterType.My || currentUserParticipated) {
@@ -1761,20 +1779,17 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
let latestMyThreadsRootEvent: MatrixEvent | undefined;
const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
for (const rootEvent of threadRoots) {
this.threadsTimelineSets[0]?.addLiveEvent(rootEvent, {
const opts = {
duplicateStrategy: DuplicateStrategy.Ignore,
fromCache: false,
roomState,
});
};
this.threadsTimelineSets[0]?.addLiveEvent(rootEvent, opts);
const threadRelationship = rootEvent
.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
if (threadRelationship?.current_user_participated) {
this.threadsTimelineSets[1]?.addLiveEvent(rootEvent, {
duplicateStrategy: DuplicateStrategy.Ignore,
fromCache: false,
roomState,
});
this.threadsTimelineSets[1]?.addLiveEvent(rootEvent, opts);
latestMyThreadsRootEvent = rootEvent;
}
}
@@ -1953,7 +1968,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
));
}
private updateThreadRootEvents = (thread: Thread, toStartOfTimeline: boolean) => {
private updateThreadRootEvents = (thread: Thread, toStartOfTimeline: boolean): void => {
if (thread.length) {
this.updateThreadRootEvent(this.threadsTimelineSets?.[0], thread, toStartOfTimeline);
if (thread.hasCurrentUserParticipated) {
@@ -1966,7 +1981,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
timelineSet: Optional<EventTimelineSet>,
thread: Thread,
toStartOfTimeline: boolean,
) => {
): void => {
if (timelineSet && thread.rootEvent) {
if (Thread.hasServerSideSupport) {
timelineSet.addLiveEvent(thread.rootEvent, {
@@ -2053,7 +2068,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
redactedEvent.getType(),
redactedEvent.getStateKey()!,
);
if (currentStateEvent.getId() === redactedEvent.getId()) {
if (currentStateEvent?.getId() === redactedEvent.getId()) {
this.currentState.setStateEvents([redactedEvent]);
}
}
@@ -2916,7 +2931,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
let excludedUserIds: string[] = [];
const mFunctionalMembers = this.currentState.getStateEvents(UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, "");
if (Array.isArray(mFunctionalMembers?.getContent().service_members)) {
excludedUserIds = mFunctionalMembers.getContent().service_members;
excludedUserIds = mFunctionalMembers!.getContent().service_members;
}
// get members that are NOT ourselves and are actually in the room.
@@ -3080,7 +3095,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
originalEvent.applyVisibilityEvent(visibilityChange);
}
private redactVisibilityChangeEvent(event: MatrixEvent) {
private redactVisibilityChangeEvent(event: MatrixEvent): void {
// Sanity checks.
if (!event.isVisibilityEvent) {
throw new Error("expected a visibility change event");
+1 -1
View File
@@ -60,5 +60,5 @@ export class SearchResult {
*
* @constructor
*/
constructor(public readonly rank: number, public readonly context: EventContext) {}
public constructor(public readonly rank: number, public readonly context: EventContext) {}
}
+25 -14
View File
@@ -81,6 +81,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
* A reference to all the events ID at the bottom of the threads
*/
public readonly timelineSet: EventTimelineSet;
public timeline: MatrixEvent[] = [];
private _currentUserParticipated = false;
@@ -94,7 +95,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
public initialEventsFetched = !Thread.hasServerSideSupport;
constructor(
public constructor(
public readonly id: string,
public rootEvent: MatrixEvent | undefined,
opts: IThreadOpts,
@@ -123,7 +124,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
this.room.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
this.room.on(RoomEvent.Redaction, this.onRedaction);
this.room.on(RoomEvent.LocalEchoUpdated, this.onEcho);
this.timelineSet.on(RoomEvent.Timeline, this.onEcho);
this.timelineSet.on(RoomEvent.Timeline, this.onTimelineEvent);
// even if this thread is thought to be originating from this client, we initialise it as we may be in a
// gappy sync and a thread around this event may already exist.
@@ -167,7 +168,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
Thread.hasServerSideFwdPaginationSupport = status;
}
private onBeforeRedaction = (event: MatrixEvent, redaction: MatrixEvent) => {
private onBeforeRedaction = (event: MatrixEvent, redaction: MatrixEvent): void => {
if (event?.isRelation(THREAD_RELATION_TYPE.name) &&
this.room.eventShouldLiveIn(event).threadId === this.id &&
event.getId() !== this.id && // the root event isn't counted in the length so ignore this redaction
@@ -178,10 +179,10 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
}
};
private onRedaction = async (event: MatrixEvent) => {
private onRedaction = async (event: MatrixEvent): Promise<void> => {
if (event.threadRootId !== this.id) return; // ignore redactions for other timelines
if (this.replyCount <= 0) {
for (const threadEvent of this.events) {
for (const threadEvent of this.timeline) {
this.clearEventMetadata(threadEvent);
}
this.lastEvent = this.rootEvent;
@@ -192,7 +193,19 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
}
};
private onEcho = async (event: MatrixEvent) => {
private onTimelineEvent = (
event: MatrixEvent,
room: Room | undefined,
toStartOfTimeline: boolean | undefined,
): void => {
// Add a synthesized receipt when paginating forward in the timeline
if (!toStartOfTimeline) {
room!.addLocalEchoReceipt(event.getSender()!, event, ReceiptType.Read);
}
this.onEcho(event);
};
private onEcho = async (event: MatrixEvent): Promise<void> => {
if (event.threadRootId !== this.id) return; // ignore echoes for other timelines
if (this.lastEvent === event) return;
if (!event.isRelation(THREAD_RELATION_TYPE.name)) return;
@@ -216,6 +229,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
roomState: this.roomState,
},
);
this.timeline = this.events;
}
}
@@ -275,6 +289,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
this.setEventMetadata(event);
await this.fetchEditsWhereNeeded(event);
}
this.timeline = this.events;
}
private getRootEventBundledRelationship(rootEvent = this.rootEvent): IThreadBundledRelationship | undefined {
@@ -349,7 +364,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
/**
* Finds an event by ID in the current thread
*/
public findEventById(eventId: string) {
public findEventById(eventId: string): MatrixEvent | undefined {
// Check the lastEvent as it may have been created based on a bundled relationship and not in a timeline
if (this.lastEvent?.getId() === eventId) {
return this.lastEvent;
@@ -361,9 +376,9 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
/**
* Return last reply to the thread, if known.
*/
public lastReply(matches: (ev: MatrixEvent) => boolean = () => true): MatrixEvent | null {
for (let i = this.events.length - 1; i >= 0; i--) {
const event = this.events[i];
public lastReply(matches: (ev: MatrixEvent) => boolean = (): boolean => true): MatrixEvent | null {
for (let i = this.timeline.length - 1; i >= 0; i--) {
const event = this.timeline[i];
if (matches(event)) {
return event;
}
@@ -411,10 +426,6 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
return this.timelineSet;
}
public get timeline(): MatrixEvent[] {
return this.events;
}
public addReceipt(event: MatrixEvent, synthetic: boolean): void {
throw new Error("Unsupported function on the thread model");
}
+1 -1
View File
@@ -76,7 +76,7 @@ export class User extends TypedEventEmitter<UserEvent, UserEventHandlerMap> {
* @prop {Object} events The events describing this user.
* @prop {MatrixEvent} events.presence The m.presence event for this user.
*/
constructor(public readonly userId: string) {
public constructor(public readonly userId: string) {
super();
this.displayName = userId;
this.rawDisplayName = userId;
+1 -1
View File
@@ -126,7 +126,7 @@ export class PushProcessor {
* @constructor
* @param {Object} client The Matrix client object to use
*/
constructor(private readonly client: MatrixClient) {}
public constructor(private readonly client: MatrixClient) {}
/**
* Convert a list of actions into a object with the actions as keys and their values
+1 -1
View File
@@ -48,7 +48,7 @@ type Callback = {
const callbackList: Callback[] = [];
// var debuglog = logger.log.bind(logger);
const debuglog = function(...params: any[]) {};
const debuglog = function(...params: any[]): void {};
/**
* reimplementation of window.setTimeout, which will call the callback if
+2 -2
View File
@@ -143,11 +143,11 @@ export class MSC3906Rendezvous {
return await this.channel.receive() as MSC3906RendezvousPayload;
}
private async send(payload: MSC3906RendezvousPayload) {
private async send(payload: MSC3906RendezvousPayload): Promise<void> {
await this.channel.send(payload);
}
public async declineLoginOnExistingDevice() {
public async declineLoginOnExistingDevice(): Promise<void> {
logger.info('User declined sign in');
await this.send({ type: PayloadType.Finish, outcome: Outcome.Declined });
}
+1 -1
View File
@@ -17,7 +17,7 @@ limitations under the License.
import { RendezvousFailureReason } from ".";
export class RendezvousError extends Error {
constructor(message: string, public readonly code: RendezvousFailureReason) {
public constructor(message: string, public readonly code: RendezvousFailureReason) {
super(message);
}
}
+1 -1
View File
@@ -47,7 +47,7 @@ export class RoomHierarchy {
* @param {boolean} suggestedOnly whether to only return rooms with suggested=true.
* @constructor
*/
constructor(
public constructor(
public readonly root: Room,
private readonly pageSize?: number,
private readonly maxDepth?: number,
+4 -4
View File
@@ -97,9 +97,9 @@ export class MatrixScheduler<T = ISendEventResponse> {
* @see module:scheduler~queueAlgorithm
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
public static QUEUE_MESSAGES(event: MatrixEvent) {
public static QUEUE_MESSAGES(event: MatrixEvent): "message" | null {
// enqueue messages or events that associate with another event (redactions and relations)
if (event.getType() === EventType.RoomMessage || event.hasAssocation()) {
if (event.getType() === EventType.RoomMessage || event.hasAssociation()) {
// put these events in the 'message' queue.
return "message";
}
@@ -116,7 +116,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
private activeQueues: string[] = [];
private procFn: ProcessFunction<T> | null = null;
constructor(
public constructor(
public readonly retryAlgorithm = MatrixScheduler.RETRY_BACKOFF_RATELIMIT,
public readonly queueAlgorithm = MatrixScheduler.QUEUE_MESSAGES,
) {}
@@ -283,7 +283,7 @@ export class MatrixScheduler<T = ISendEventResponse> {
}
}
function debuglog(...args: any[]) {
function debuglog(...args: any[]): void {
if (DEBUG) {
logger.log(...args);
}
+90 -9
View File
@@ -45,7 +45,7 @@ import { RoomMemberEvent } from "./models/room-member";
const FAILED_SYNC_ERROR_THRESHOLD = 3;
class ExtensionE2EE implements Extension {
constructor(private readonly crypto: Crypto) {}
public constructor(private readonly crypto: Crypto) {}
public name(): string {
return "e2ee";
@@ -95,7 +95,7 @@ class ExtensionE2EE implements Extension {
class ExtensionToDevice implements Extension {
private nextBatch: string | null = null;
constructor(private readonly client: MatrixClient) {}
public constructor(private readonly client: MatrixClient) {}
public name(): string {
return "to_device";
@@ -170,7 +170,7 @@ class ExtensionToDevice implements Extension {
}
class ExtensionAccountData implements Extension {
constructor(private readonly client: MatrixClient) {}
public constructor(private readonly client: MatrixClient) {}
public name(): string {
return "account_data";
@@ -233,6 +233,72 @@ class ExtensionAccountData implements Extension {
}
}
class ExtensionTyping implements Extension {
public constructor(private readonly client: MatrixClient) {}
public name(): string {
return "typing";
}
public when(): ExtensionState {
return ExtensionState.PostProcess;
}
public onRequest(isInitial: boolean): object | undefined {
if (!isInitial) {
return undefined; // don't send a JSON object for subsequent requests, we don't need to.
}
return {
enabled: true,
};
}
public onResponse(data: {rooms: Record<string, IMinimalEvent>}): void {
if (!data || !data.rooms) {
return;
}
for (const roomId in data.rooms) {
processEphemeralEvents(
this.client, roomId, [data.rooms[roomId]],
);
}
}
}
class ExtensionReceipts implements Extension {
public constructor(private readonly client: MatrixClient) {}
public name(): string {
return "receipts";
}
public when(): ExtensionState {
return ExtensionState.PostProcess;
}
public onRequest(isInitial: boolean): object | undefined {
if (isInitial) {
return {
enabled: true,
};
}
return undefined; // don't send a JSON object for subsequent requests, we don't need to.
}
public onResponse(data: {rooms: Record<string, IMinimalEvent>}): void {
if (!data || !data.rooms) {
return;
}
for (const roomId in data.rooms) {
processEphemeralEvents(
this.client, roomId, [data.rooms[roomId]],
);
}
}
}
/**
* A copy of SyncApi such that it can be used as a drop-in replacement for sync v2. For the actual
* sliding sync API, see sliding-sync.ts or the class SlidingSync.
@@ -244,7 +310,7 @@ export class SlidingSyncSdk {
private failCount = 0;
private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response
constructor(
public constructor(
private readonly slidingSync: SlidingSync,
private readonly client: MatrixClient,
private readonly opts: Partial<IStoredClientOpts> = {},
@@ -256,7 +322,7 @@ export class SlidingSyncSdk {
this.opts.experimentalThreadSupport = this.opts.experimentalThreadSupport === true;
if (!opts.canResetEntireTimeline) {
opts.canResetEntireTimeline = (_roomId: string) => {
opts.canResetEntireTimeline = (_roomId: string): boolean => {
return false;
};
}
@@ -273,6 +339,8 @@ export class SlidingSyncSdk {
const extensions: Extension[] = [
new ExtensionToDevice(this.client),
new ExtensionAccountData(this.client),
new ExtensionTyping(this.client),
new ExtensionReceipts(this.client),
];
if (this.opts.crypto) {
extensions.push(
@@ -348,7 +416,7 @@ export class SlidingSyncSdk {
* Sync rooms the user has left.
* @return {Promise} Resolved when they've been added to the store.
*/
public async syncLeftRooms() {
public async syncLeftRooms(): Promise<Room[]> {
return []; // TODO
}
@@ -457,7 +525,7 @@ export class SlidingSyncSdk {
return false;
}
private async processRoomData(client: MatrixClient, room: Room, roomData: MSC3575RoomData) {
private async processRoomData(client: MatrixClient, room: Room, roomData: MSC3575RoomData): Promise<void> {
roomData = ensureNameEvent(client, room.roomId, roomData);
const stateEvents = mapEvents(this.client, room.roomId, roomData.required_state);
// Prevent events from being decrypted ahead of time
@@ -632,7 +700,7 @@ export class SlidingSyncSdk {
// we'll purge this once we've fully processed the sync response
this.addNotifications(timelineEvents);
const processRoomEvent = async (e: MatrixEvent) => {
const processRoomEvent = async (e: MatrixEvent): Promise<void> => {
client.emit(ClientEvent.Event, e);
if (e.isState() && e.getType() == EventType.RoomEncryption && this.opts.crypto) {
await this.opts.crypto.onCryptoEvent(e);
@@ -767,7 +835,7 @@ export class SlidingSyncSdk {
/**
* Main entry point. Blocks until stop() is called.
*/
public async sync() {
public async sync(): Promise<void> {
logger.debug("Sliding sync init loop");
// 1) We need to get push rules so we can check if events should bing as we get
@@ -888,3 +956,16 @@ function mapEvents(client: MatrixClient, roomId: string | undefined, events: obj
return mapper(e);
});
}
function processEphemeralEvents(client: MatrixClient, roomId: string, ephEvents: IMinimalEvent[]): void {
const ephemeralEvents = mapEvents(client, roomId, ephEvents);
const room = client.getRoom(roomId);
if (!room) {
logger.warn("got ephemeral events for room but room doesn't exist on client:", roomId);
return;
}
room.addEphemeralEvents(ephemeralEvents);
ephemeralEvents.forEach((e) => {
client.emit(ClientEvent.Event, e);
});
}
+14 -10
View File
@@ -27,6 +27,10 @@ import { HTTPError } from "./http-api";
// to determine the max time we're willing to wait.
const BUFFER_PERIOD_MS = 10 * 1000;
export const MSC3575_WILDCARD = "*";
export const MSC3575_STATE_KEY_ME = "$ME";
export const MSC3575_STATE_KEY_LAZY = "$LAZY";
/**
* Represents a subscription to a room or set of rooms. Controls which events are returned.
*/
@@ -165,7 +169,7 @@ class SlidingList {
* Construct a new sliding list.
* @param {MSC3575List} list The range, sort and filter values to use for this list.
*/
constructor(list: MSC3575List) {
public constructor(list: MSC3575List) {
this.replaceList(list);
}
@@ -369,7 +373,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
* @param {MatrixClient} client The client to use for /sync calls.
* @param {number} timeoutMS The number of milliseconds to wait for a response.
*/
constructor(
public constructor(
private readonly proxyBaseUrl: string,
lists: MSC3575List[],
private roomSubscriptionInfo: MSC3575RoomSubscription,
@@ -387,7 +391,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
* useCustomSubscription.
* @param sub The subscription information.
*/
public addCustomSubscription(name: string, sub: MSC3575RoomSubscription) {
public addCustomSubscription(name: string, sub: MSC3575RoomSubscription): void {
this.customSubscriptions.set(name, sub);
}
@@ -398,7 +402,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
* @param name The name of the subscription. If this name is unknown, the default subscription
* will be used.
*/
public useCustomSubscription(roomId: string, name: string) {
public useCustomSubscription(roomId: string, name: string): void {
this.roomIdToCustomSubscription.set(roomId, name);
// unconfirm this subscription so a resend() will send it up afresh.
this.confirmedRoomSubscriptions.delete(roomId);
@@ -570,7 +574,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
this.emit(SlidingSyncEvent.Lifecycle, state, resp, err);
}
private shiftRight(listIndex: number, hi: number, low: number) {
private shiftRight(listIndex: number, hi: number, low: number): void {
// l h
// 0,1,2,3,4 <- before
// 0,1,2,2,3 <- after, hi is deleted and low is duplicated
@@ -584,7 +588,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
}
}
private shiftLeft(listIndex: number, hi: number, low: number) {
private shiftLeft(listIndex: number, hi: number, low: number): void {
// l h
// 0,1,2,3,4 <- before
// 0,1,3,4,4 <- after, low is deleted and hi is duplicated
@@ -598,7 +602,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
}
}
private removeEntry(listIndex: number, index: number) {
private removeEntry(listIndex: number, index: number): void {
// work out the max index
let max = -1;
for (const n in this.lists[listIndex].roomIndexToRoomId) {
@@ -614,7 +618,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
delete this.lists[listIndex].roomIndexToRoomId[max];
}
private addEntry(listIndex: number, index: number) {
private addEntry(listIndex: number, index: number): void {
// work out the max index
let max = -1;
for (const n in this.lists[listIndex].roomIndexToRoomId) {
@@ -743,7 +747,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
return d.promise;
}
private resolveTransactionDefers(txnId?: string) {
private resolveTransactionDefers(txnId?: string): void {
if (!txnId) {
return;
}
@@ -807,7 +811,7 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
/**
* Start syncing with the server. Blocks until stopped.
*/
public async start() {
public async start(): Promise<void> {
this.abortController = new AbortController();
let currentPos: string | undefined;
+24 -24
View File
@@ -25,7 +25,7 @@ import { IndexedToDeviceBatch, ToDeviceBatchWithTxnId } from "../models/ToDevice
type DbMigration = (db: IDBDatabase) => void;
const DB_MIGRATIONS: DbMigration[] = [
(db) => {
(db): void => {
// Make user store, clobber based on user ID. (userId property of User objects)
db.createObjectStore("users", { keyPath: ["userId"] });
@@ -36,15 +36,15 @@ const DB_MIGRATIONS: DbMigration[] = [
// Make /sync store (sync tokens, room data, etc), always clobber (const key).
db.createObjectStore("sync", { keyPath: ["clobber"] });
},
(db) => {
(db): void => {
const oobMembersStore = db.createObjectStore(
"oob_membership_events", {
keyPath: ["room_id", "state_key"],
});
oobMembersStore.createIndex("room", "room_id");
},
(db) => { db.createObjectStore("client_options", { keyPath: ["clobber"] }); },
(db) => { db.createObjectStore("to_device_queue", { autoIncrement: true }); },
(db): void => { db.createObjectStore("client_options", { keyPath: ["clobber"] }); },
(db): void => { db.createObjectStore("to_device_queue", { autoIncrement: true }); },
// Expand as needed.
];
const VERSION = DB_MIGRATIONS.length;
@@ -67,11 +67,11 @@ function selectQuery<T>(
const query = store.openCursor(keyRange);
return new Promise((resolve, reject) => {
const results: T[] = [];
query.onerror = () => {
query.onerror = (): void => {
reject(new Error("Query failed: " + query.error));
};
// collect results
query.onsuccess = () => {
query.onsuccess = (): void => {
const cursor = query.result;
if (!cursor) {
resolve(results);
@@ -85,10 +85,10 @@ function selectQuery<T>(
function txnAsPromise(txn: IDBTransaction): Promise<Event> {
return new Promise((resolve, reject) => {
txn.oncomplete = function(event) {
txn.oncomplete = function(event): void {
resolve(event);
};
txn.onerror = function() {
txn.onerror = function(): void {
reject(txn.error);
};
});
@@ -96,10 +96,10 @@ function txnAsPromise(txn: IDBTransaction): Promise<Event> {
function reqAsEventPromise(req: IDBRequest): Promise<Event> {
return new Promise((resolve, reject) => {
req.onsuccess = function(event) {
req.onsuccess = function(event): void {
resolve(event);
};
req.onerror = function() {
req.onerror = function(): void {
reject(req.error);
};
});
@@ -107,8 +107,8 @@ function reqAsEventPromise(req: IDBRequest): Promise<Event> {
function reqAsPromise(req: IDBRequest): Promise<IDBRequest> {
return new Promise((resolve, reject) => {
req.onsuccess = () => resolve(req);
req.onerror = (err) => reject(err);
req.onsuccess = (): void => resolve(req);
req.onerror = (err): void => reject(err);
});
}
@@ -141,7 +141,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
* @param {string=} dbName Optional database name. The same name must be used
* to open the same database.
*/
constructor(private readonly indexedDB: IDBFactory, dbName = "default") {
public constructor(private readonly indexedDB: IDBFactory, dbName = "default") {
this.dbName = "matrix-js-sdk:" + dbName;
this.syncAccumulator = new SyncAccumulator();
}
@@ -161,7 +161,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
logger.log(`LocalIndexedDBStoreBackend.connect: connecting...`);
const req = this.indexedDB.open(this.dbName, VERSION);
req.onupgradeneeded = (ev) => {
req.onupgradeneeded = (ev): void => {
const db = req.result;
const oldVersion = ev.oldVersion;
logger.log(
@@ -176,22 +176,22 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
});
};
req.onblocked = () => {
req.onblocked = (): void => {
logger.log(`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`);
};
logger.log(`LocalIndexedDBStoreBackend.connect: awaiting connection...`);
return reqAsEventPromise(req).then(() => {
return reqAsEventPromise(req).then(async () => {
logger.log(`LocalIndexedDBStoreBackend.connect: connected`);
this.db = req.result;
// add a poorly-named listener for when deleteDatabase is called
// so we can close our db connections.
this.db.onversionchange = () => {
this.db.onversionchange = (): void => {
this.db?.close();
};
return this.init();
await this.init();
});
}
@@ -204,7 +204,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
* Having connected, load initial data from the database and prepare for use
* @return {Promise} Resolves on success
*/
private init() {
private init(): Promise<unknown> {
return Promise.all([
this.loadAccountData(),
this.loadSyncData(),
@@ -243,7 +243,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
// were all known already
let oobWritten = false;
request.onsuccess = () => {
request.onsuccess = (): void => {
const cursor = request.result;
if (!cursor) {
// Unknown room
@@ -260,7 +260,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
}
cursor.continue();
};
request.onerror = (err) => {
request.onerror = (err): void => {
reject(err);
};
}).then((events) => {
@@ -346,11 +346,11 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
logger.log(`Removing indexeddb instance: ${this.dbName}`);
const req = this.indexedDB.deleteDatabase(this.dbName);
req.onblocked = () => {
req.onblocked = (): void => {
logger.log(`can't yet delete indexeddb ${this.dbName} because it is open elsewhere`);
};
req.onerror = () => {
req.onerror = (): void => {
// in firefox, with indexedDB disabled, this fails with a
// DOMError. We treat this as non-fatal, so that we can still
// use the app.
@@ -358,7 +358,7 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
resolve();
};
req.onsuccess = () => {
req.onsuccess = (): void => {
logger.log(`Removed indexeddb instance: ${this.dbName}`);
resolve();
};
+1 -1
View File
@@ -42,7 +42,7 @@ export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
* @param {string=} dbName Optional database name. The same name must be used
* to open the same database.
*/
constructor(
public constructor(
private readonly workerFactory: () => Worker,
private readonly dbName?: string,
) {}
+1 -1
View File
@@ -45,7 +45,7 @@ export class IndexedDBStoreWorker {
* @param {function} postMessage The web worker postMessage function that
* should be used to communicate back to the main script.
*/
constructor(private readonly postMessage: InstanceType<typeof Worker>["postMessage"]) {}
public constructor(private readonly postMessage: InstanceType<typeof Worker>["postMessage"]) {}
/**
* Passes a message event from the main script into the class. This method
+2 -2
View File
@@ -53,7 +53,7 @@ type EventHandlerMap = {
};
export class IndexedDBStore extends MemoryStore {
static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
public static exists(indexedDB: IDBFactory, dbName: string): Promise<boolean> {
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
}
@@ -109,7 +109,7 @@ export class IndexedDBStore extends MemoryStore {
* this API if you need to perform specific indexeddb actions like deleting the
* database.
*/
constructor(opts: IOpts) {
public constructor(opts: IOpts) {
super(opts);
if (!opts.indexedDB) {
+6 -6
View File
@@ -69,7 +69,7 @@ export class MemoryStore implements IStore {
private pendingToDeviceBatches: IndexedToDeviceBatch[] = [];
private nextToDeviceBatchId = 0;
constructor(opts: IOpts = {}) {
public constructor(opts: IOpts = {}) {
this.localStorage = opts.localStorage;
}
@@ -90,7 +90,7 @@ export class MemoryStore implements IStore {
* Set the token to stream from.
* @param {string} token The token to stream from.
*/
public setSyncToken(token: string) {
public setSyncToken(token: string): void {
this.syncToken = token;
}
@@ -98,7 +98,7 @@ export class MemoryStore implements IStore {
* Store the given room.
* @param {Room} room The room to be stored. All properties must be stored.
*/
public storeRoom(room: Room) {
public storeRoom(room: Room): void {
this.rooms[room.roomId] = room;
// add listeners for room member changes so we can keep the room member
// map up-to-date.
@@ -116,7 +116,7 @@ export class MemoryStore implements IStore {
* @param {RoomState} state
* @param {RoomMember} member
*/
private onRoomMember = (event: MatrixEvent | null, state: RoomState, member: RoomMember) => {
private onRoomMember = (event: MatrixEvent | null, state: RoomState, member: RoomMember): void => {
if (member.membership === "invite") {
// We do NOT add invited members because people love to typo user IDs
// which would then show up in these lists (!)
@@ -217,7 +217,7 @@ export class MemoryStore implements IStore {
* @param {string} token The token associated with these events.
* @param {boolean} toStart True if these are paginated results.
*/
public storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean) {
public storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean): void {
// no-op because they've already been added to the room instance.
}
@@ -275,7 +275,7 @@ export class MemoryStore implements IStore {
* @param {string} filterName
* @param {string} filterId
*/
public setFilterIdByName(filterName: string, filterId?: string) {
public setFilterIdByName(filterName: string, filterId?: string): void {
if (!this.localStorage) {
return;
}
+9 -9
View File
@@ -56,7 +56,7 @@ export class StubStore implements IStore {
* Set the sync token.
* @param {string} token
*/
public setSyncToken(token: string) {
public setSyncToken(token: string): void {
this.fromToken = token;
}
@@ -64,7 +64,7 @@ export class StubStore implements IStore {
* No-op.
* @param {Room} room
*/
public storeRoom(room: Room) {}
public storeRoom(room: Room): void {}
/**
* No-op.
@@ -87,7 +87,7 @@ export class StubStore implements IStore {
* Permanently delete a room.
* @param {string} roomId
*/
public removeRoom(roomId: string) {
public removeRoom(roomId: string): void {
return;
}
@@ -103,7 +103,7 @@ export class StubStore implements IStore {
* No-op.
* @param {User} user
*/
public storeUser(user: User) {}
public storeUser(user: User): void {}
/**
* No-op.
@@ -139,13 +139,13 @@ export class StubStore implements IStore {
* @param {string} token The token associated with these events.
* @param {boolean} toStart True if these are paginated results.
*/
public storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean) {}
public storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean): void {}
/**
* Store a filter.
* @param {Filter} filter
*/
public storeFilter(filter: Filter) {}
public storeFilter(filter: Filter): void {}
/**
* Retrieve a filter.
@@ -171,13 +171,13 @@ export class StubStore implements IStore {
* @param {string} filterName
* @param {string} filterId
*/
public setFilterIdByName(filterName: string, filterId?: string) {}
public setFilterIdByName(filterName: string, filterId?: string): void {}
/**
* Store user-scoped account data events
* @param {Array<MatrixEvent>} events The events to store.
*/
public storeAccountDataEvents(events: MatrixEvent[]) {}
public storeAccountDataEvents(events: MatrixEvent[]): void {}
/**
* Get account data event by event type
@@ -209,7 +209,7 @@ export class StubStore implements IStore {
/**
* Save does nothing as there is no backing data store.
*/
public save() {}
public save(): void {}
/**
* Startup does nothing.
+47 -16
View File
@@ -24,7 +24,7 @@ import { deepCopy, isSupportedReceiptType } from "./utils";
import { IContent, IUnsigned } from "./models/event";
import { IRoomSummary } from "./models/room-summary";
import { EventType } from "./@types/event";
import { ReceiptType } from "./@types/read_receipts";
import { MAIN_ROOM_TIMELINE, ReceiptContent, ReceiptType } from "./@types/read_receipts";
import { UNREAD_THREAD_NOTIFICATIONS } from './@types/sync';
interface IOpts {
@@ -165,6 +165,15 @@ interface IRoom {
eventId: string;
};
};
_threadReadReceipts: {
[threadId: string]: {
[userId: string]: {
data: IMinimalEvent;
type: ReceiptType;
eventId: string;
};
};
};
}
export interface ISyncData {
@@ -202,7 +211,7 @@ export class SyncAccumulator {
* never be more. This cannot be 0 or else it makes it impossible to scroll
* back in a room. Default: 50.
*/
constructor(private readonly opts: IOpts = {}) {
public constructor(private readonly opts: IOpts = {}) {
this.opts.maxTimelineEntries = this.opts.maxTimelineEntries || 50;
}
@@ -369,6 +378,7 @@ export class SyncAccumulator {
_unreadThreadNotifications: {},
_summary: {},
_readReceipts: {},
_threadReadReceipts: {},
};
}
const currentData = this.joinRooms[roomId];
@@ -425,23 +435,30 @@ export class SyncAccumulator {
// of a hassle to work with. We'll inflate this back out when
// getJSON() is called.
Object.keys(e.content).forEach((eventId) => {
Object.entries<{
[eventId: string]: {
[receiptType: string]: {
[userId: string]: IMinimalEvent;
};
};
}>(e.content[eventId]).forEach(([key, value]) => {
Object.entries<ReceiptContent>(e.content[eventId]).forEach(([key, value]) => {
if (!isSupportedReceiptType(key)) return;
Object.keys(value).forEach((userId) => {
// clobber on user ID
currentData._readReceipts[userId] = {
for (const userId of Object.keys(value)) {
const data = e.content[eventId][key][userId];
const receipt = {
data: e.content[eventId][key][userId],
type: key as ReceiptType,
eventId: eventId,
};
});
if (!data.thread_id || data.thread_id === MAIN_ROOM_TIMELINE) {
currentData._readReceipts[userId] = receipt;
} else {
currentData._threadReadReceipts = {
...currentData._threadReadReceipts,
[data.thread_id]: {
...(currentData._threadReadReceipts[data.thread_id] ?? {}),
[userId]: receipt,
},
};
}
}
});
});
});
@@ -566,8 +583,8 @@ export class SyncAccumulator {
// $event_id: { "m.read": { $user_id: $json } }
},
};
Object.keys(roomData._readReceipts).forEach((userId) => {
const receiptData = roomData._readReceipts[userId];
for (const [userId, receiptData] of Object.entries(roomData._readReceipts)) {
if (!receiptEvent.content[receiptData.eventId]) {
receiptEvent.content[receiptData.eventId] = {};
}
@@ -577,7 +594,21 @@ export class SyncAccumulator {
receiptEvent.content[receiptData.eventId][receiptData.type][userId] = (
receiptData.data
);
});
}
for (const threadReceipts of Object.values(roomData._threadReadReceipts)) {
for (const [userId, receiptData] of Object.entries(threadReceipts)) {
if (!receiptEvent.content[receiptData.eventId]) {
receiptEvent.content[receiptData.eventId] = {};
}
if (!receiptEvent.content[receiptData.eventId][receiptData.type]) {
receiptEvent.content[receiptData.eventId][receiptData.type] = {};
}
receiptEvent.content[receiptData.eventId][receiptData.type][userId] = (
receiptData.data
);
}
}
// add only if we have some receipt data
if (Object.keys(receiptEvent.content).length > 0) {
roomJson.ephemeral.events.push(receiptEvent as IMinimalEvent);
+8 -8
View File
@@ -106,7 +106,7 @@ function getFilterName(userId: string, suffix?: string): string {
return `FILTER_SYNC_${userId}` + (suffix ? "_" + suffix : "");
}
function debuglog(...params) {
function debuglog(...params): void {
if (!DEBUG) return;
logger.log(...params);
}
@@ -175,7 +175,7 @@ export class SyncApi {
private failedSyncCount = 0; // Number of consecutive failed /sync requests
private storeIsInvalid = false; // flag set if the store needs to be cleared before we can start
constructor(private readonly client: MatrixClient, private readonly opts: Partial<IStoredClientOpts> = {}) {
public constructor(private readonly client: MatrixClient, private readonly opts: Partial<IStoredClientOpts> = {}) {
this.opts.initialSyncLimit = this.opts.initialSyncLimit ?? 8;
this.opts.resolveInvitesToProfiles = this.opts.resolveInvitesToProfiles || false;
this.opts.pollTimeout = this.opts.pollTimeout || (30 * 1000);
@@ -183,7 +183,7 @@ export class SyncApi {
this.opts.experimentalThreadSupport = this.opts.experimentalThreadSupport === true;
if (!opts.canResetEntireTimeline) {
opts.canResetEntireTimeline = (roomId: string) => {
opts.canResetEntireTimeline = (roomId: string): boolean => {
return false;
};
}
@@ -554,7 +554,7 @@ export class SyncApi {
return false;
}
private getPushRules = async () => {
private getPushRules = async (): Promise<void> => {
try {
debuglog("Getting push rules...");
const result = await this.client.getPushRules();
@@ -572,7 +572,7 @@ export class SyncApi {
}
};
private buildDefaultFilter = () => {
private buildDefaultFilter = (): Filter => {
const filter = new Filter(this.client.credentials.userId);
if (this.client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported) {
filter.setUnreadThreadNotifications(true);
@@ -580,7 +580,7 @@ export class SyncApi {
return filter;
};
private checkLazyLoadStatus = async () => {
private checkLazyLoadStatus = async (): Promise<void> => {
debuglog("Checking lazy load status...");
if (this.opts.lazyLoadMembers && this.client.isGuest()) {
this.opts.lazyLoadMembers = false;
@@ -1389,7 +1389,7 @@ export class SyncApi {
this.processEventsForNotifs(room, events);
const processRoomEvent = async (e) => {
const processRoomEvent = async (e): Promise<void> => {
client.emit(ClientEvent.Event, e);
if (e.isState() && e.getType() == "m.room.encryption" && this.opts.crypto) {
await this.opts.crypto.onCryptoEvent(e);
@@ -1521,7 +1521,7 @@ export class SyncApi {
* @param {boolean} connDidFail True if a connectivity failure has been detected. Optional.
*/
private pokeKeepAlive(connDidFail = false): void {
const success = () => {
const success = (): void => {
clearTimeout(this.keepAliveTimer);
if (this.connectionReturnedDefer) {
this.connectionReturnedDefer.resolve(connDidFail);
+4 -4
View File
@@ -32,7 +32,7 @@ const DEBUG = false;
/**
* @private
*/
const debuglog = DEBUG ? logger.log.bind(logger) : function() {};
const debuglog = DEBUG ? logger.log.bind(logger) : function(): void {};
/**
* the number of times we ask the server for more events before giving up
@@ -84,7 +84,7 @@ export class TimelineWindow {
*
* @constructor
*/
constructor(
public constructor(
private readonly client: MatrixClient,
private readonly timelineSet: EventTimelineSet,
opts: IOpts = {},
@@ -104,7 +104,7 @@ export class TimelineWindow {
public load(initialEventId?: string, initialWindowSize = 20): Promise<void> {
// given an EventTimeline, find the event we were looking for, and initialise our
// fields so that the event in question is in the middle of the window.
const initFields = (timeline: Optional<EventTimeline>) => {
const initFields = (timeline: Optional<EventTimeline>): void => {
if (!timeline) {
throw new Error("No timeline given to initFields");
}
@@ -430,7 +430,7 @@ export class TimelineIndex {
public pendingPaginate?: Promise<boolean>;
// index: the indexes are relative to BaseIndex, so could well be negative.
constructor(public timeline: EventTimeline, public index: number) {}
public constructor(public timeline: EventTimeline, public index: number) {}
/**
* @return {number} the minimum possible value for the index in the current
+17 -4
View File
@@ -178,7 +178,7 @@ export function removeElement<T>(
* @param {*} value The thing to check.
* @return {boolean} True if it is a function.
*/
export function isFunction(value: any) {
export function isFunction(value: any): boolean {
return Object.prototype.toString.call(value) === "[object Function]";
}
@@ -189,7 +189,7 @@ export function isFunction(value: any) {
* @throws If the object is missing keys.
*/
// note using 'keys' here would shadow the 'keys' function defined above
export function checkObjectHasKeys(obj: object, keys: string[]) {
export function checkObjectHasKeys(obj: object, keys: string[]): void {
for (const key of keys) {
if (!obj.hasOwnProperty(key)) {
throw new Error("Missing required key: " + key);
@@ -383,7 +383,7 @@ export function globToRegexp(glob: string, extended = false): string {
if (!extended) {
replacements.push([
/\\\[(!|)(.*)\\]/g,
(_match: string, neg: string, pat: string) => [
(_match: string, neg: string, pat: string): string => [
'[',
neg ? '^' : '',
pat.replace(/\\-/, '-'),
@@ -490,7 +490,7 @@ export function simpleRetryOperation<T>(promiseFn: (attempt: number) => Promise<
* The default alphabet used by string averaging in this SDK. This matches
* all usefully printable ASCII characters (0x20-0x7E, inclusive).
*/
export const DEFAULT_ALPHABET = (() => {
export const DEFAULT_ALPHABET = ((): string => {
let str = "";
for (let c = 0x20; c <= 0x7E; c++) {
str += String.fromCharCode(c);
@@ -694,3 +694,16 @@ export function sortEventsByLatestContentTimestamp(left: MatrixEvent, right: Mat
export function isSupportedReceiptType(receiptType: string): boolean {
return [ReceiptType.Read, ReceiptType.ReadPrivate].includes(receiptType as ReceiptType);
}
/**
* Determines whether two maps are equal.
* @param eq The equivalence relation to compare values by. Defaults to strict equality.
*/
export function mapsEqual<K, V>(x: Map<K, V>, y: Map<K, V>, eq = (v1: V, v2: V): boolean => v1 === v2): boolean {
if (x.size !== y.size) return false;
for (const [k, v1] of x) {
const v2 = y.get(k);
if (v2 === undefined || !eq(v1, v2)) return false;
}
return true;
}
+1 -1
View File
@@ -35,7 +35,7 @@ export const acquireContext = (): AudioContext => {
* released, allowing the context and associated hardware resources to be
* cleaned up if nothing else is using it.
*/
export const releaseContext = () => {
export const releaseContext = (): void => {
refCount--;
if (refCount === 0) {
audioContext?.close();
+16 -7
View File
@@ -268,7 +268,7 @@ const CALL_TIMEOUT_MS = 60000;
export class CallError extends Error {
public readonly code: string;
constructor(code: CallErrorCode, msg: string, err: Error) {
public constructor(code: CallErrorCode, msg: string, err: Error) {
// Still don't think there's any way to have proper nested errors
super(msg + ": " + err);
@@ -404,7 +404,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
private opponentSessionId?: string;
public groupCallId?: string;
constructor(opts: CallOpts) {
public constructor(opts: CallOpts) {
super();
this.roomId = opts.roomId;
@@ -452,7 +452,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
* @param label A human readable label for this datachannel
* @param options An object providing configuration options for the data channel.
*/
public createDataChannel(label: string, options: RTCDataChannelInit | undefined) {
public createDataChannel(label: string, options: RTCDataChannelInit | undefined): RTCDataChannel {
const dataChannel = this.peerConn!.createDataChannel(label, options);
this.emit(CallEvent.DataChannel, dataChannel);
return dataChannel;
@@ -462,6 +462,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
return this.opponentMember;
}
public getOpponentDeviceId(): string | undefined {
return this.opponentDeviceId;
}
public getOpponentSessionId(): string | undefined {
return this.opponentSessionId;
}
@@ -564,7 +568,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
return this.feeds.filter((feed) => !feed.isLocal());
}
private async initOpponentCrypto() {
private async initOpponentCrypto(): Promise<void> {
if (!this.opponentDeviceId) return;
if (!this.client.getUseE2eForGroupCall()) return;
// It's possible to want E2EE and yet not have the means to manage E2EE
@@ -644,6 +648,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
client: this.client,
roomId: this.roomId,
userId,
deviceId: this.getOpponentDeviceId(),
stream,
purpose,
audioMuted,
@@ -688,6 +693,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
audioMuted: false,
videoMuted: false,
userId,
deviceId: this.getOpponentDeviceId(),
stream,
purpose,
}));
@@ -718,6 +724,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
audioMuted: false,
videoMuted: false,
userId,
deviceId: this.getOpponentDeviceId(),
stream,
purpose,
}),
@@ -938,7 +945,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
}
}, invite.lifetime - event.getLocalAge());
const onState = (state: CallState) => {
const onState = (state: CallState): void => {
if (state !== CallState.Ringing) {
clearTimeout(ringingTimer);
this.off(CallEvent.State, onState);
@@ -1009,6 +1016,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
client: this.client,
roomId: this.roomId,
userId: this.client.getUserId()!,
deviceId: this.client.getDeviceId() ?? undefined,
stream,
purpose: SDPStreamMetadataPurpose.Usermedia,
audioMuted: false,
@@ -1079,7 +1087,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
logger.debug(`Ending call ${this.callId} with 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 (this.state === CallState.WaitLocalMedia) return;
if ([CallState.Fledgling, CallState.WaitLocalMedia].includes(this.state)) return;
const content = {};
// Don't send UserHangup reason to older clients
if ((this.opponentVersion && this.opponentVersion !== 0) || reason !== CallErrorCode.UserHangup) {
@@ -2132,7 +2140,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.pushRemoteFeed(stream);
if (!this.removeTrackListeners.has(stream)) {
const onRemoveTrack = () => {
const onRemoveTrack = (): void => {
if (stream.getTracks().length === 0) {
logger.info(`Call ${this.callId} removing track streamId: ${stream.id}`);
this.deleteFeedByStream(stream);
@@ -2584,6 +2592,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
client: this.client,
roomId: this.roomId,
userId: this.client.getUserId()!,
deviceId: this.client.getDeviceId() ?? undefined,
stream,
purpose: SDPStreamMetadataPurpose.Usermedia,
audioMuted: false,

Some files were not shown because too many files have changed in this diff Show More