Compare commits

...

485 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
RiotRobot 3702ac56f4 v21.2.0 2022-11-22 11:24:19 +00:00
RiotRobot af4811b327 Prepare changelog for v21.2.0 2022-11-22 11:24:19 +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
RiotRobot d4601d9910 v21.2.0-rc.1 2022-11-15 17:41:32 +00:00
RiotRobot 2ced5e1aa4 Prepare changelog for v21.2.0-rc.1 2022-11-15 17:41:32 +00:00
David Baker 45e19e51c1 Merge pull request #2880 from matrix-org/dbkr/revert_to_connecting
Make calls go back to 'connecting' state when media lost
2022-11-15 17:36:50 +00:00
David Baker 3f1c3392d4 Make calls go back to 'connecting' state when media lost
This is a change in how the state machine works: technically it's
a breaking change. Calls will now now go back into the connecting
state if the media connection is lost (they'll try to re-establish
the connection).
2022-11-15 16:10:45 +00:00
Richard van der Hoff f86f67f5a5 Improve logs for decrypting events (#2879)
Some attempts to make debugging UISIs a bit easier
2022-11-15 15:11:13 +00:00
Mahdi Bagvand 4dcf54f448 fix registration add phone number not working (#2876)
Co-authored-by: Michael Weimann <michaelw@matrix.org>
2022-11-15 10:42:37 +01:00
Germain d43e664594 Add ability to send unthreaded receipt (#2878) 2022-11-14 16:14:07 +00:00
Michael Telatynski 0e322848f9 Add way to abort search requests (#2877) 2022-11-14 16:11:53 +00:00
Šimon Brandner b454318684 Use an underride rule for Element Call notifications (#2873) 2022-11-14 11:42:51 +01:00
Richard van der Hoff 692f1d49b9 Deprecate/remove some get/set methods on Crypto (#2874)
* Deprecate Crypto.{get,set}GlobalBlacklistUnverifiedDevices

... in favour of just exposing the properties.

* Remove Crypto.{get,set}GlobalErrorOnUnknownDevices

... in favour of exposing the property.

These methods are UNSTABLE so we can safely remove them, right?
2022-11-14 10:03:12 +00:00
Richard van der Hoff b40cf75c9d Remove Crypto.start() (#2871)
This isn't really needed, and its semantics are poorly defined. (Contrary to
the comment, it dos *not* set background processes running).
2022-11-14 10:01:05 +00:00
kegsay ba6a001d67 Merge pull request #2834 from matrix-org/kegan/custom-room-subs
sliding sync: add custom room subscriptions support
2022-11-14 09:19:40 +00:00
kegsay d0c71ec516 Merge branch 'develop' into kegan/custom-room-subs 2022-11-14 09:11:37 +00:00
Richard van der Hoff 67f343d6f0 Switch to typedoc for documentation (#2869)
This seems to give *much* better results than jsdoc, and means that we can
start to get rid of all the duplicated type information.
2022-11-11 11:38:04 +00:00
Germain a7f0ba97cd Fixes unwanted highlight notifications with encrypted threads (#2862) 2022-11-11 09:27:16 +00:00
Michael Telatynski 54d11e1745 Upload docs artifact for PRs (#2868) 2022-11-10 18:01:34 +00:00
David Baker 14744fd4dc Merge pull request #2865 from matrix-org/dbkr/log_call_id
Log the call ID when receiving a call
2022-11-09 16:03:51 +00:00
David Baker 9b0919350c Log the call ID when receiving a call 2022-11-09 15:47:37 +00:00
David Baker b53ad2c081 Merge pull request #2864 from matrix-org/dbkr/log_on_hangup
Add logging saying why we hung up calls
2022-11-09 15:42:11 +00:00
David Baker 6222d238e4 Add logging saying why we hung up calls 2022-11-09 15:20:10 +00:00
Michael Telatynski c6ee258789 Remove patch-package and update matrix-events-sdk (#2863) 2022-11-08 17:01:38 +00:00
László Várady a584324a0d webrtc: add advanced audio settings (#2434) 2022-11-08 16:02:31 +00:00
renovate[bot] 059b07cfa0 Lock file maintenance (#2857)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-08 14:40:33 +00:00
RiotRobot b628cabe58 Resetting package fields for development 2022-11-08 14:36:10 +00:00
RiotRobot b7d925f5ec Merge branch 'master' into develop
# Conflicts:
#	package.json
2022-11-08 14:36:02 +00:00
Michael Telatynski 1c901e3137 Fix Node 19 compatibility and run CI against it (#2842) 2022-11-08 14:01:06 +00:00
Germain bd4589fcc4 Hide pending events in thread timelines (#2843) 2022-11-08 10:04:20 +00:00
Robin 0fbd0b3685 Merge pull request #2860 from robintown/patch-package-dependency
Move patch-package out of dev dependencies
2022-11-07 15:36:20 -05:00
Eric Eastwood 1646ea05bc Extra insurance that we don't mix events in the wrong timelines - v2 (#2856)
Add checks to `addEventToTimeline` as extra insurance that we don't mix events in the wrong timelines (main timeline vs thread timeline).

Split out from https://github.com/matrix-org/matrix-js-sdk/pull/2521

This PR is a v2 of https://github.com/matrix-org/matrix-js-sdk/pull/2848 since it was reverted in https://github.com/matrix-org/matrix-js-sdk/pull/2853

Previously, we just relied on the callers to make sure they're doing the right thing and since it's easy to get it wrong, we mixed and bugs happened.

Call stacks for how events get added to a timeline:

 - `TimelineSet.addEventsToTimeline` -> `TimelineSet.addEventToTimeline` -> `Timeline.addEvent`
 - `TimelineSet.addEventToTimeline` -> `Timeline.addEvent`
 - `TimelineSet.addLiveEvent` -> `TimelineSet.addEventToTimeline` -> `Timeline.addEvent`
2022-11-07 14:24:43 -06:00
Robin Townsend 885ec1fc73 Move patch-package out of dev dependencies
patch-package is used as a postinstall hook, but since it was in devDependencies, upstream packages would not install it. Moving it to dependencies isn't ideal since it's not needed at runtime, but the patch-package approach is only a temporary workaround for https://github.com/matrix-org/matrix-events-sdk/pull/16#pullrequestreview-1166721652 anyways.
2022-11-07 15:24:04 -05:00
David Baker df2b65f111 Merge pull request #2553 from matrix-org/robertlong/group-call
Add support for group calls using MSC3401
2022-11-07 17:24:31 +00:00
David Baker f09853ccb1 Merge remote-tracking branch 'origin/develop' into robertlong/group-call 2022-11-07 17:04:15 +00:00
Michael Telatynski 6c543382e6 Make SonarCloud happier (#2850)
* Make SonarCloud happier

* Revert one change due to lack of strict mode upstream

* Fix typo
2022-11-07 12:16:48 +00:00
Michael Telatynski 52932f59ab Fix pagination token tracking for mixed room timelines (#2855) 2022-11-05 12:36:20 +00:00
David Baker 4f63ff21ea Merge branch 'develop' into robertlong/group-call 2022-11-04 16:05:04 +00:00
David Baker c8dc71eb69 Merge pull request #2854 from matrix-org/dbkr/gcmerge_20221104
Merge changes from develop
2022-11-04 16:04:23 +00:00
David Baker 2dda837db6 Fix strict mode errors 2022-11-04 14:44:21 +00:00
David Baker fff4cdab7c Merge remote-tracking branch 'origin/develop' into dbkr/gcmerge_20221104 2022-11-04 14:05:48 +00:00
Šimon Brandner 4f00566b9f Do not freeze state in initialiseState() (#2846) 2022-11-04 14:48:42 +01:00
Michael Telatynski c1a3b95073 Revert "Extra insurance that we don't mix events in the wrong timelines" (#2853)
This reverts commit 433b7afd71.
2022-11-04 12:21:04 +00:00
Eric Eastwood 38adbaf923 Ignore random macOS cruft (.DS_Store) (#2851) 2022-11-04 06:25:10 -05:00
Travis Ralston 9459a95134 Delete .DS_Store 2022-11-04 10:59:23 +00:00
Eric Eastwood 433b7afd71 Extra insurance that we don't mix events in the wrong timelines (#2848)
Add checks to `addEventToTimeline` as extra insurance that we don't mix events in the wrong timelines (main timeline vs thread timeline).

Split out from https://github.com/matrix-org/matrix-js-sdk/pull/2521

Previously, we just relied on the callers to make sure they're doing the right thing and since it's easy to get it wrong, we mixed and bugs happened.

Call stacks for how events get added to a timeline:

 - `TimelineSet.addEventsToTimeline` -> `TimelineSet.addEventToTimeline` -> `Timeline.addEvent`
 - `TimelineSet.addEventToTimeline` -> `Timeline.addEvent`
 - `TimelineSet.addLiveEvent` -> `TimelineSet.addEventToTimeline` -> `Timeline.addEvent`
2022-11-04 05:54:23 -05:00
Michael Telatynski 777cf1f135 Remove tsc-strict ci, it has served its purpose (#2847) 2022-11-04 09:12:07 +00:00
Robin 8235b65d71 Merge pull request #2844 from robintown/dont-remove-self
Don't remove our own member for a split second when entering a call
2022-11-03 14:38:39 -04:00
Hubert Chathi 4a33e584b0 Add unit test for device de-/rehydration (#2821) 2022-11-03 13:12:57 -04:00
Michael Telatynski 6ee185e93f Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	.github/workflows/sonarqube.yml
2022-11-03 14:13:25 +00:00
Michael Telatynski 5df9705bae Update sonarqube.yml 2022-11-03 14:12:42 +00:00
Michael Telatynski 76458d3a40 Update sonarqube.yml 2022-11-03 13:58:50 +00:00
Michael Telatynski 27bb79a29a Switch to @casualbot/jest-sonar-reporter 2022-11-03 13:47:36 +00:00
Michael Telatynski 82d942dcc5 Update sonarqube.yml 2022-11-03 13:17:48 +00:00
Michael Telatynski ce6d0e2cb1 Update sonarqube.yml 2022-11-03 12:59:16 +00:00
Michael Telatynski db49cd8d13 Make the js-sdk conform to tsc --strict (#2835)
Co-authored-by: Faye Duxovni <fayed@matrix.org>
2022-11-03 12:50:05 +00:00
Michael Telatynski 42b08eca57 Update sonarqube.yml 2022-11-03 12:49:38 +00:00
Michael Telatynski a92c148f15 Update sonarqube.yml 2022-11-03 12:23:04 +00:00
Michael Telatynski 6cd60e32dc Update sonarqube.yml 2022-11-03 12:14:16 +00:00
Michael Telatynski b6633ad4b0 Update sonarqube.yml 2022-11-03 12:06:52 +00:00
Michael Telatynski e4dd7bcc87 Update sonarcloud.yml 2022-11-03 12:01:33 +00:00
Michael Telatynski b6e97fcecb Update sonarqube.yml 2022-11-03 12:01:20 +00:00
Michael Telatynski dee2b60c3d Update sonarqube.yml 2022-11-03 11:59:54 +00:00
Michael Telatynski a07fe44565 Update matrix-org/sonarcloud-workflow-action (#2845) 2022-11-03 11:46:31 +00:00
Michael Telatynski 0a35f2e2c7 Fix queueToDevice.spec.ts test flakiness (#2841) 2022-11-03 11:45:35 +00:00
Robin Townsend 32d535c2b1 Don't remove our own member for a split second when entering a call 2022-11-02 22:43:54 -04:00
David Baker 7fb313c17c Merge remote-tracking branch 'origin/develop' into robertlong/group-call 2022-11-02 10:38:05 +00:00
renovate[bot] d8f6449422 Update jest monorepo to v29.2.1 (#2837)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-02 08:53:09 +00:00
renovate[bot] c043e36f50 Update all (#2836)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-02 08:52:32 +00:00
renovate[bot] e6524239bd Update dependency @babel/runtime to v7.20.1 (#2838)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-02 02:20:24 -04:00
renovate[bot] daed4b9dcc Update typescript-eslint monorepo to v5.42.0 (#2839)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-01 20:29:45 +00:00
Kegan Dougal 135d2da143 Maybe satisfy the strict ts checker 2022-11-01 16:35:40 +00:00
Kegan Dougal fef53be5b4 Use Map not Objects 2022-11-01 16:31:17 +00:00
David Baker 7ec726e10b Give everything that isn't web rtc back to element-web 2022-11-01 16:18:14 +00:00
David Baker 476f6f78b1 Add more access modifiers 2022-11-01 16:14:48 +00:00
David Baker cb8123dec7 Add public/private modifiers 2022-11-01 16:07:07 +00:00
David Baker 6729c7d421 Merge remote-tracking branch 'origin/develop' into robertlong/group-call 2022-11-01 14:38:16 +00:00
Kegan Dougal 7ddd198df8 Linting 2022-11-01 11:08:47 +00:00
Kegan Dougal 81681f4090 Add custom room subscriptions support
This is mostly useful when you need to change the subscription depending
on the room. For example, unencrypted rooms have lazy-loaded members, but
encrypted rooms do not.
2022-11-01 11:04:40 +00:00
Robin 94072a096d Merge pull request #2826 from robintown/init-leave-race
Resolve races between `initLocalCallFeed` and `leave`
2022-10-31 13:46:30 -04:00
Robin Townsend b9cccf9109 Resolve races between initLocalCallFeed and leave
Unfortunately there are still other methods that could race with leave and result in broken group call state, such as enter and terminate. For the future, should consider writing a more careful specification of how the whole group call state machine is meant to work.
2022-10-31 12:09:06 -04:00
David Baker f0d4ef7f99 Merge remote-tracking branch 'origin/develop' into robertlong/group-call 2022-10-31 15:36:30 +00:00
Robin 849e3d67c2 Merge pull request #2815 from robintown/keepalive-leave
Let leave requests outlive the window
2022-10-27 08:35:30 -04:00
Robin Townsend 4c6e1e5c21 Replace the keepAlive flag with request options 2022-10-27 08:19:41 -04:00
David Baker 9ff6b357fc Merge branch 'develop' into robertlong/group-call 2022-10-27 09:56:36 +01:00
David Baker 77ef8558bd Merge pull request #2816 from matrix-org/dbkr/groupcall_more_strict
A few more strict mode fixes
2022-10-27 09:55:17 +01:00
David Baker d979302e9b A few more strict mode fixes 2022-10-27 09:37:41 +01:00
Robin Townsend dbdaa1540a Let leave requests outlive the window 2022-10-26 17:50:20 -04:00
David Baker 13c751c060 Merge pull request #2808 from matrix-org/dbkr/groupcall_more_strict
More TS strict mode fixes
2022-10-26 13:44:00 +01:00
David Baker 87115d181d Don't commit the strict mode flag 2022-10-26 12:34:18 +01:00
David Baker 5679c86ca6 More TS strict mode fixes 2022-10-26 12:33:06 +01:00
David Baker 4cd50e4871 Merge pull request #2807 from matrix-org/dbkr/gcmerge_26oct22
Merge changes from develop again
2022-10-26 12:08:28 +01:00
David Baker 0f1012278a Fix types 2022-10-26 12:01:53 +01:00
David Baker 0d211dfbad Clean up group call tests (#2806) 2022-10-26 11:57:16 +01:00
David Baker 384116c8f5 Merge remote-tracking branch 'origin/develop' into dbkr/gcmerge_26oct22 2022-10-26 11:56:41 +01:00
David Baker c374ba2367 TS strict mode compliance in the call / groupcall code (#2805)
* TS strict mode compliance in the call / groupcall code

* Also the test

* Fix initOpponentCrypto

to not panic if it doesn't actually need to init crypto
2022-10-26 11:45:03 +01:00
David Baker 450ff00c3e Merge pull request #2800 from matrix-org/dbkr/gcmerge_oct22_3
Merge develop into group-call branch
2022-10-24 19:03:06 +01:00
David Baker b4ab7fc0b3 Merge branch 'robertlong/group-call' into dbkr/gcmerge_oct22_3 2022-10-24 18:53:54 +01:00
Robin 35f697a04b Merge pull request #2797 from robintown/matryoshka-events
Add event and message capabilities to RoomWidgetClient
2022-10-24 13:50:09 -04:00
David Baker 193d8a429a Merge remote-tracking branch 'origin/develop' into dbkr/gcmerge_oct22_3 2022-10-24 18:38:46 +01:00
Robin Townsend 8cd5aac128 Add event and message capabilities to RoomWidgetClient 2022-10-24 12:18:02 -04:00
David Baker eddd0cafe8 Add throwOnFail to groupCall.setScreensharingEnabled (#2787)
* Add throwOnFail to groupCall.setScreensharingEnabled

For https://github.com/vector-im/element-call/pull/652

* Update mediaHandler.ts
2022-10-24 10:20:04 +01:00
David Baker 5a0787349d Fix connectivity regressions (#2780)
* Fix connectivity regressions

Switches back to addTrack, digging the transceivers out manually
to re-use, because the only way to group tracks into streams re-using
trasceivers from the offer is to use setStreams which FF doesn't
implement.

* Remove comments
2022-10-19 18:03:12 +01:00
David Baker c57c8978cf Fix screenshare failing after several attempts (#2771)
* Fix screenshare failing after several attempts

Re-use any existing transceivers when screen sharing. This prevents
transceivers accumulating and making the SDP too big: see linked bug.

This also switches from `addTrack()` to `addTransceiver ()` which is
not that large of a change, other than having to explicitly find the
transceivers after an offer has arrived rather than just adding tracks
and letting WebRTC take care of it.

Fixes https://github.com/vector-im/element-call/issues/625

* Fix tests

* Unused import

* Use a map instead of an array

* Add comment

* more comment

* Remove commented code

* Remove unintentional debugging

* Add test for screenshare transceiver re-use

* Type alias for transceiver map
2022-10-19 16:00:54 +01:00
David Baker dfe535bc07 More debugging for multiple group calls (#2766) 2022-10-17 20:14:44 +01:00
Robin Townsend 3c33c422e6 Merge branch 'develop' into robertlong/group-call 2022-10-13 20:21:51 -04:00
Robin d521f97411 Merge pull request #2754 from robintown/unblock-mute
Don't block muting/unmuting on network requests
2022-10-13 19:54:57 -04:00
Robin Townsend c0a5299704 Don't block muting/unmuting on network requests
(PTT mode will still block on them, as expected)
2022-10-13 11:56:46 -04:00
Robin ce3b72c850 Merge pull request #2712 from robintown/merge
Merge develop
2022-09-29 08:03:36 -04:00
Robin Townsend 935517746a Merge branch 'develop' into robertlong/group-call 2022-09-28 14:13:45 -04:00
David Baker e48d919cd4 Fix ICE restarts (#2702)
We didn't reset the 'seen end of candidates' flag when doign an ICE
restart, so we would have ignored all locally gathered candidates
on an ICE restart.
2022-09-27 17:25:04 +01:00
Šimon Brandner ab39ee37d6 Add more MatrixCall tests (#2697)
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-09-26 12:02:41 +02:00
Šimon Brandner af6f9d49f4 Add CallEventHandler tests (#2696)
* Add `CallEventHandler` tests

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Avoid tests hanging

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-09-26 12:02:19 +02:00
Šimon Brandner a2981efac3 Add MatrixClient group call tests (#2692)
Co-authored-by: Robin <robin@robin.town>
2022-09-23 18:33:31 +02:00
Šimon Brandner 4625ed73cf Merge pull request #2695 from matrix-org/SimonBrandner/task/gc-merge 2022-09-23 17:43:23 +02:00
Šimon Brandner 6f7a72d69e Merge remote-tracking branch 'upstream/develop' into SimonBrandner/task/gc-merge
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-09-23 15:36:44 +02:00
Šimon Brandner 2a0ffe1223 Fix group call tests getting stuck (#2689) 2022-09-22 17:06:01 +02:00
Šimon Brandner 72a6ec0dd3 Add a few group call event handler tests (#2679) 2022-09-22 17:05:51 +02:00
Šimon Brandner 72b89fde6e Add test for call transfers (#2677) 2022-09-20 19:41:03 +02:00
Šimon Brandner c400dd4ff8 Add a few new GroupCall tests (#2678)
Co-authored-by: Robin <robin@robin.town>
2022-09-20 19:40:47 +02:00
Robin f41b7706e4 Upgrade matrix-widget-api (and fix the lockfile) (#2676) 2022-09-16 11:08:13 -04:00
Robin de694459be Target widget actions at a specific room (#2670)
Otherwise, the RoomWidgetClient class can end up accidentally sending and receiving events from rooms it didn't intend to, if it's an always-on-screen widget.
2022-09-16 10:26:03 -04:00
David Baker 6fc9827b10 Add tests for ice candidate sending (#2674) 2022-09-16 09:26:37 +01:00
David Baker f52c5eb667 Unused imports from merge 2022-09-14 09:53:07 +01:00
David Baker c05cb3ad2b Merge branch 'develop' into robertlong/group-call 2022-09-14 09:51:43 +01:00
David Baker 586a313c8d Add tests for call answering / candidate sending (#2666)
* Add tests for call answering / candidate sending

* Remopve unused stuff

* Capitalise

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Capitalisation

* Capitalise

* Fix typescript strict error

* Actually fix TS strict error(?)

* TS strict mode try 3

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-09-14 09:42:57 +01:00
David Baker c605310b87 Prevent exception when muting (#2667)
Fixes https://github.com/vector-im/element-call/issues/578
2022-09-13 20:02:14 +01:00
David Baker 41cee6f1cc Fix race in creating calls (#2662)
* Fix race in creating calls

We ran an async function between checking for an existing call and
adding the new one to the map, so it would have been possible to
start creating another call while we were placing the first call.
This changes the code to add the call to the map as soon as we've
created it.

Also adds more logging.

* Switch to logger.debug

* Fix unit tests
2022-09-13 16:30:34 +01:00
David Baker 3e1e99f8e5 Fix import in failed merge 2022-09-12 10:34:35 +01:00
David Baker 276849f068 Merge branch 'develop' into robertlong/group-call 2022-09-12 10:03:48 +01:00
David Baker 37118991f5 Add test for removing RTX codec (#2660)
* Add test for removing RTX codec

* Use mocked to cast
2022-09-12 09:40:28 +01:00
David Baker 00629e6dc9 Test fallback screensharing (#2659)
* Test fallback screensharing

* Test replacetrack is called

* Unused import

* Return type

* Fix other test after new track IDs
2022-09-09 21:15:34 +01:00
David Baker 02f6a09bcf Test active speaker events (#2658)
Fixes https://github.com/vector-im/element-call/issues/527
2022-09-09 18:57:25 +01:00
Robin 36a6117ee2 Misc fixes for group call widgets (#2657)
* Fix GroupCallEventHandler in matryoshka mode

GroupCallEventHandler needs to see a 'Syncing' event before it starts handling any events, so emit one immediately in matryoshka mode.

* Implement joinRoom on RoomWidgetClient

Element Call has undergone some changes to how it loads rooms, meaning that this method must be implemented for the app to work in matryoshka mode.

* Allow audio and video to be muted before local call feed exists

This is desirable for the Element Web integration of Element Call, because we need to be able to mute our devices before ever joining the call or creating a call feed, if the users requests it.

* Fix a strict mode error
2022-09-09 09:53:27 -04:00
David Baker aebe26db96 GroupCallEventhandler Tests (#2654)
* GroupCallEventhandler Tests

Fixes https://github.com/vector-im/element-call/issues/545

* Fix long line

* Fix strict mode error

Co-authored-by: Robin <robin@robin.town>

* Fix typo

Co-authored-by: Robin <robin@robin.town>

Co-authored-by: Robin <robin@robin.town>
2022-09-08 21:46:28 +01:00
David Baker 60e175a0e0 Merge branch 'develop' into robertlong/group-call 2022-09-08 20:52:08 +01:00
David Baker d950cda05c Merge branch 'develop' into robertlong/group-call 2022-09-08 15:03:55 +01:00
David Baker 83c848093f MediaHandler Tests (#2646)
* MediaHandler Tests, part 1

Haven't got through all the methods yet

For https://github.com/vector-im/element-call/issues/544

* Didn't need these in the end

* Rest of the media handler tests

* getUserMediaStream takes args

* use mockResolvedValue

* Add .off & reuse the mock we already made

* Re-use mock handler again

* Move updateLocalUsermediaStream to beforeEach

* add .off

* Add types

* Add more .offs
2022-09-07 15:56:38 +01:00
David Baker fa6f70f708 Log ID instead of object (#2643)
as otherwise it recurses and logs the entire client + store
2022-09-06 18:09:34 +01:00
David Baker 98d119d6e1 Add client.waitUntilRoomReadyForGroupCalls() (#2641)
See comment, although this still feels like a poor solution to the
problem. Might be better if the js-sdk processed everything internally
before emitting the 'Room' event (or indeed before joinRoom resolved)
so the app knows everything is ready when it gets that event.
2022-09-06 13:54:48 +01:00
David Baker aca51fd8a3 Test call mute status set on call state chnage (#2638) 2022-09-05 17:06:49 +01:00
David Baker c78631bdee Test that calls in a group call are retried (#2637)
* Test that calls in a group call are retried

* Add new flushpromises file
2022-09-05 09:45:32 +01:00
David Baker 0d6a93b5f6 Refactor the group call placing calls test (#2636)
Add some types & use mock-typed versions directly - it's clearer which
client we're making assertions about.
2022-09-02 15:33:22 +01:00
David Baker 40ecfa7932 Test disabling screenshare in group calls (#2634)
Also add a few more types
2022-09-02 12:57:29 +01:00
David Baker d656b848f8 Wait for client to start syncing before making group calls (#2632)
As hopefully explained in comment

Fixes https://github.com/matrix-org/matrix-js-sdk/issues/2589
2022-09-01 17:57:38 +01:00
David Baker 0981652de4 Add GroupCallEventHandlerEvent.Room (#2631)
* Add GroupCalEventHandlerEvent.Room

Emit an event when the group call event handler has processed all
pending group calls.

* Remove unused return value

* Add void return type

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-09-01 16:18:37 +01:00
David Baker db32420d16 Add logging to diagnose connection issue (#2629)
For https://github.com/vector-im/element-call/issues/559
2022-08-31 13:40:55 +01:00
David Baker d5b82e343a Add types to the call unit test suites (#2627)
* Add types to the call unit test suites

Still involves quite a few casts to any unfortunately as it turns
out we access quite a few private methods on the Call class in these
tests.

* Remove commented line & use better expect syntax

* Replace more calls.length with toHaveBeenCalled

* Remove mistakenly added id field
2022-08-31 11:15:13 +01:00
David Baker 965f4fb13b Fix ICE end-of-candidates messages (#2622)
* Fix ICE end-of-candidates messages

We were casting a POJO to an RTCIceCandidate for the dummy
end-of-candidates candidate, but https://github.com/matrix-org/matrix-js-sdk/pull/2473
started calling .toJSON() on these objects.

Store separately whether we've seen the end of candidates rather than
adding on a dummy candidate object.

A test for this will follow, but a) I want to get this fix out and
b) I'm currently rewriting the call test file to add typing.

Fixes https://github.com/vector-im/element-call/issues/553

* Remove hacks for testing

* Switch if branches
2022-08-26 10:04:07 +01:00
David Baker 9e1b126854 1:1 screenshare tests (#2617)
* 1:1 screenshare tests

Fixes https://github.com/vector-im/element-call/issues/548

* Always hang up calls after tests

to prevent hanging tests

Also fix a null dereference as we may not have an invitee or opponent
member when sending voip events if not using to-device messages.

* use mockImplementationOnce

Co-authored-by: Robin <robin@robin.town>

* use mockImplementationOnce

Co-authored-by: Robin <robin@robin.town>

* Add type on mock

* Add corresponding call.off

* Merge enable & disable screenshare tests

Co-authored-by: Robin <robin@robin.town>
2022-08-24 15:45:53 +01:00
David Baker c527f85fb1 Revert 4a294c9dd3
Pushed to wrong branch
2022-08-23 22:03:25 +01:00
David Baker 4a294c9dd3 1:1 screenshare tests
Fixes https://github.com/vector-im/element-call/issues/548
2022-08-23 22:02:32 +01:00
David Baker be94f5ea93 Fix imports 2022-08-22 20:30:27 +01:00
David Baker 5f9369abee Merge branch 'develop' into robertlong/group-call 2022-08-22 20:19:53 +01:00
David Baker e7a7ec0673 Test call timeouts (#2611)
Fixes https://github.com/vector-im/element-call/issues/547
2022-08-22 20:17:19 +01:00
David Baker 92cd84fc0c Add unit tests for hangup / reject (#2606)
* Add unit tests for hangup / reject

Fixes https://github.com/vector-im/element-call/issues/537

* Fix some bugs where we carried on with the call after it had been ended
2022-08-22 13:29:54 +01:00
Šimon Brandner 45e56f8cc3 Add disposed to CallFeed (#2604) 2022-08-19 17:33:55 +02:00
Robin e95947dc73 Update lockfile (#2603) 2022-08-19 08:22:37 -04:00
Šimon Brandner 448a5c9a77 Add screensharing tests (#2598) 2022-08-18 10:42:03 +02:00
David Baker 9589a97952 Test muting in PTT mode (#2599)
Fixes https://github.com/vector-im/element-call/issues/523
2022-08-17 18:09:46 +01:00
David Baker 2566c40e96 Add tests for incoming calls in group calls (#2597)
* Add tests for incoming calls in group calls

Inspiration wwlecome for the renamed describe group which we're
really abusing for a bunch of things that happen to have the same
dependencies.

Fixes https://github.com/vector-im/element-call/issues/532

* Extract incoming call tests out into their own describe

and get the lexicographical ordering to match who should be calling who

* Trailing space
2022-08-17 15:10:03 +01:00
David Baker 099cac0162 Merge branch 'develop' into robertlong/group-call 2022-08-17 12:34:25 +01:00
David Baker e4cf5b26ee Add test for updateLocalUsermediaStream (#2596) 2022-08-17 12:33:11 +01:00
Šimon Brandner c698317f3f Add group call tests for muting (#2590) 2022-08-17 10:59:54 +02:00
David Baker e8f682f452 Test placing a call in a group call (#2593)
* Test placing a call in a group call

Refactors a bit of the call testing stuff

Fixes https://github.com/vector-im/element-call/issues/521

* Unused imports

* Use expect.toHaveBeenCalledWith()

* Types

* More types

* Add comment on mock typing

* Use toHaveBeenCalledWith()

* Initialise groupcall & room in beforeEach

* Initialise mockMediahandler sensibly

* Add type params to mock

* Rename mute tests

* Move comment

* Join / leave in parallel

* Remove leftover expect
2022-08-16 18:22:36 +01:00
David Baker 020743141b Tidy up imports (#2584)
Duplicate 'call' imports
2022-08-10 18:07:49 +01:00
David Baker 5f5a9b1a43 Merge branch 'develop' into robertlong/group-call 2022-08-10 14:31:39 +01:00
Robin 3334c01191 Support nested Matrix clients via the widget API (#2473)
* WIP RoomWidgetClient

* Wait for the widget API to become ready before backfilling

* Add support for sending user-defined encrypted to-device messages

This is a port of the same change from the robertlong/group-call branch.

* Fix tests

* Emit an event when the client receives TURN servers

* Expose the method in MatrixClient

* Override the encryptAndSendToDevices method

* Add support for TURN servers in embedded mode and make calls mostly work

* Don't put unclonable objects into VoIP events

RoomWidget clients were unable to send m.call.candidate events, because
the candidate objects were not clonable for use with postMessage.
Converting such objects to their canonical JSON form before attempting
to send them over the wire solves this.

* Fix types

* Fix more types

* Fix lint

* Upgrade matrix-widget-api

* Save lockfile

* Untangle dependencies to fix tests

* Add some preliminary tests

* Fix tests

* Fix indirect export

* Add more tests

* Resolve TODOs

* Add queueToDevice to RoomWidgetClient
2022-08-09 09:45:58 -04:00
David Baker 0b8de251bf Add basic creation / entering tests for group calls (#2575)
* Add basic creation / entering tests for group calls

* Missing space

Co-authored-by: Robin <robin@robin.town>

* Assert more of the group call member event

and also move call leaving to a finally so it doesn't leaving a call
hagning if it fails.

Co-authored-by: Robin <robin@robin.town>
2022-08-09 13:43:02 +01:00
David Baker 88ce017333 Fix return types of event sending functions (#2576)
These had somehow got mixed up so the type check was failing.
Nothing uses the response return type, so just return void.
2022-08-09 13:11:59 +01:00
David Baker 471f174889 Merge remote-tracking branch 'origin/develop' into robertlong/group-call 2022-08-05 15:59:01 +01:00
David Baker c0dacb5037 Merge remote-tracking branch 'origin/develop' into robertlong/group-call 2022-08-05 11:06:10 +01:00
David Baker 2cc51e0db7 Merge changes from develop (#2563)
* Prepare changelog for v19.2.0-rc.1

* v19.2.0-rc.1

* Sliding sync: add missing filters from latest MSC

* Gracefully handle missing room_ids

* Prepare changelog for v19.2.0

* v19.2.0

* Resetting package fields for development

* Use EventType enum values instead of hardcoded strings (#2557)

* Retry to-device messages (#2549)

* Retry to-device messages

This adds a queueToDevice API alongside sendToDevice which is a
much higher-level API that adds the messages to a queue, stored in
persistent storage, and retries them periodically. Also converts
sending of megolm keys to use the new API.

Other uses of sendToDevice are nopt converted in this PR, but could
be later.

Requires https://github.com/matrix-org/matrix-mock-request/pull/17

* Bump matrix-mock-request

* Add more waits to make indexeddb tests pass

* Switch some test expectations to queueToDevice

* Stop straight away if the client has been stopped

Hopefully will fix tests being flakey and logging after tests have
finished.

* Add return types & fix constant usage

* Fix return type

Co-authored-by: Germain <germains@element.io>

* Fix return type

Co-authored-by: Germain <germains@element.io>

* Fix return type

Co-authored-by: Germain <germains@element.io>

* Stop the client in all test cases

Co-authored-by: Germain <germains@element.io>

* Add support for sending user-defined encrypted to-device messages (#2528)

* Add support for sending user-defined encrypted to-device messages

This is a port of the same change from the robertlong/group-call branch.

* Fix tests

* Expose the method in MatrixClient

* Fix a code smell

* Fix types

* Test the MatrixClient method

* Fix some types in Crypto test suite

* Test the Crypto method

* Fix tests

* Upgrade matrix-mock-request

* Move useRealTimers to afterEach

* Remove stream-replacement (#2551)

* Reintroduce setNewStream method, fix test, update yarn.lock

Co-authored-by: RiotRobot <releases@riot.im>
Co-authored-by: Kegan Dougal <kegan@matrix.org>
Co-authored-by: Germain <germains@element.io>
Co-authored-by: Robin <robin@robin.town>
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-08-04 17:28:54 +01:00
Šimon Brandner 22c5999fed Delint group calls (#2554) 2022-08-01 18:45:14 +02:00
David Baker b711781f16 Merge remote-tracking branch 'origin/develop' into robertlong/group-call 2022-07-29 10:56:01 +01:00
Šimon Brandner 8ba2d257ae Add support for audio sharing (#2530) 2022-07-28 18:12:11 +02:00
David Baker 9e2e144530 Make SDP munging media type specific (#2526)
* Make SDP munging media type specific

We were trying to apply modifications to all media types which led
to confusing warning messages saying opus wasn't present (when it
was for the video stream). Make the modifications media-type specific
to avoid this.

* Make codec * mediatype into enums
2022-07-28 09:38:57 +01:00
Timo 38a6949e5d add missing events from reemitter to GroupCall (#2527) 2022-07-25 09:44:37 -07:00
Šimon Brandner e876482e62 Add local volume control (#2525) 2022-07-25 15:51:06 +02:00
David Baker 544b1c6742 Merge develop into group call branch again (#2513)
* Send call version `1` as a string (#2471)

* test typescriptification - backup.spec (#2468)

* renamed:    spec/unit/crypto/crypto-utils.js -> spec/unit/crypto/crypto-utils.ts

* ts fixes in crypto-utils

* renamed:    spec/unit/crypto/backup.spec.js -> spec/unit/crypto/backup.spec.ts

* ts fixes in backup.spec

* remove fit

* remove debug

* Prepare changelog for v19.0.0-rc.1

* v19.0.0-rc.1

* Update jest monorepo (#2476)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Update all (#2475)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Update dependency @types/jest to v28 (#2478)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Fix call.collectCallStats() (#2480)

Regressed by https://github.com/matrix-org/matrix-js-sdk/pull/2352
(you can just use RTCStatsReport as an iterator directly (which
was was what that code was doing before) which uses entries(
which gives you key/value pairs, but using forEach gives you just
the value.

* Go back to forEach in collectcallstats (#2481)

Older typescript library doesn't know about .values() on the stats
object, so it was failing in react sdk which had an older typescript.
https://github.com/matrix-org/matrix-react-sdk/pull/8935 was an
attempt to upgrade it but did not seem to be helping on CI, despite
being fine locally.

* Update babel monorepo to v7.18.6 (#2477)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Expose KNOWN_SAFE_ROOM_VERSION (#2474)

* Fix return type on funcs in matrixClient to be optionally null (#2488)

* Update pull_request.yaml (#2490)

* Lock file maintenance (#2491)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Prepare changelog for v19.0.0

* v19.0.0

* Resetting package fields for development

* Improve VoIP integrations testing (#2495)

* Remove MSC3244 support (#2504)

* Actually store the identity server in the client when given as an option (#2503)

* Actually store the identity server in the client when given as an option

* Update requestRegisterEmailToken to a modern spec version too

* Properly re-insert room ID in bundled thread relation messages from sync (#2505)

Events returned by the `/sync` endpoint, including relations bundled with other events, may have their `room_id`s stripped out. This causes decryption errors if the IDs aren't repopulated.

Fixes vector-im/element-web#22094.

* Remove `setNow` from `realtime-callbacks.ts` (#2509)

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Remove dead code (#2510)

* Don't crash with undefined room in `processBeaconEvents()` (#2500)

* Add a basic PR checklist for all PRs (#2511)

It'll be mildly annoying for core developers who have to constantly remove or edit this, but it'll also serve as a good reminder to do these things.

Note that signoff is not required for core developers.

* Fix tests

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
Co-authored-by: Kerry <kerrya@element.io>
Co-authored-by: RiotRobot <releases@riot.im>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Weimann <michaelw@matrix.org>
Co-authored-by: texuf <texuf.eth@gmail.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Travis Ralston <travisr@matrix.org>
Co-authored-by: Faye Duxovni <fayed@element.io>
2022-07-12 19:27:41 +01:00
David Baker 984dd26a13 Prevent double mute status changed events (#2502)
Audio & video mute status were set in separate calls but share a
mute status changed event, so you'd always get two mute status
changed events emitted. We could suppress events where the mute
status didn't change, but this would still get two events saying
the same thing when they both changed. Instead, merge setAudioMuted
& setVideoMuted into a single call that sets either or both.
2022-07-08 09:45:02 +01:00
David Baker bdb91b3806 Set max average bitrate on PTT calls (#2499)
* Set max average bitrate on PTT calls

Via SDP munging. Also makes the SDP munging a bit more generic and
codec-specific (we were previously adding usedtx to any codec that had an fmtp
line already, which was probably not really the intention).

* Make SDP munging for codecs that don't already have fmtp lines

* Use sensible typescript syntax

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>

Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-07-06 22:04:41 +01:00
David Baker 9a15094374 Add config option for e2e group call signalling (#2492) 2022-07-05 13:17:23 +01:00
Robin e980c88901 Don't mute the remote side immediately in PTT calls (#2487)
This clause was causing all PTT calls to mute the remote side
immediately upon ICE connection status changing to 'checking'.
2022-07-02 09:19:07 -04:00
David Baker 6ea2885796 Remove empty decryption listener (#2486)
* Remove empty decryption listener

This listener looks like it was left over from something as it just
did nothing at all. The todevice event gets put into the call
event buffer which awaits on decryption for each event before
processing, so it should already wait for decryption.

More info: https://github.com/vector-im/element-call/issues/428

* Unused import

* Unused function!
2022-07-01 17:44:00 +01:00
David Baker ca5ac79927 Revert hack to only clone streams on safari (#2485)
Reverts https://github.com/matrix-org/matrix-js-sdk/pull/2450

Looks like this wasn't really the problem (although may have made it
happens faster) and the actual problem was multiple audio contexts
and/or leaking peer connections as fixed in https://github.com/matrix-org/matrix-js-sdk/pull/2484
2022-07-01 17:32:17 +01:00
Robin f9672cf307 Fix some MatrixCall leaks and use a shared AudioContext (#2484)
* Fix some MatrixCall leaks and use a shared AudioContext

These leaks, combined with the dozens of AudioContexts floating around in memory across different CallFeeds, could cause some really bad performance issues and audio crashes on Chrome.

* Fully release the AudioContext in CallFeed's dispose method

* Fix tests
2022-07-01 11:58:00 -04:00
Robin e7493fd417 Enable DTX on audio tracks in calls (#2482)
This greatly reduces the amount of bandwidth used when transmitting
silence.
2022-06-29 16:06:17 -04:00
David Baker f553854730 Remove the feature to disable audio from muted members (#2479)
At the moment it looks like its more valuable to get the audio from
people even if they're not actually shown as speaking. We can always
re-introduce it later.
2022-06-29 18:47:01 +01:00
David Baker c89bbf4bf5 Fix call.collectCallStats() (#2480)
Regressed by https://github.com/matrix-org/matrix-js-sdk/pull/2352
(you can just use RTCStatsReport as an iterator directly (which
was was what that code was doing before) which uses entries(
which gives you key/value pairs, but using forEach gives you just
the value.
2022-06-29 12:39:36 +01:00
Robin Townsend ebcb26f1b3 Merge branch 'develop' into robertlong/group-call 2022-06-21 11:21:03 -04:00
Robin 5b4263bf55 Don't block muting on determining whether the device exists (#2461)
* Don't block muting on determining whether the device exists

* Add comments
2022-06-16 13:51:32 -04:00
Robin df9ffdc408 Don't ignore call member events with a distant future expiration date (#2466)
to match updates to MSC3401
2022-06-16 12:55:37 -04:00
Robin 70449ea003 Expire call member state events after 1 hour (#2446)
* Expire call member state events after 1 hour

* Fix lints

* Avoid a possible race
2022-06-16 11:31:13 -04:00
David Baker 9192b876d2 Disable playback of audio for muted users (#2456)
* Disable playback of audio for muted users

As hopefully explained by the comment

* forEach instead of map
2022-06-13 20:46:34 +01:00
Travis Ralston 04d0d61a0e Change CODEOWNERS for element-call feature branch (#2457)
To reduce review requests going to the "wrong" team. The team has been mirrored from the vector-im side.
2022-06-13 12:23:50 -06:00
David Baker 404f8e130e Only clone streams on Safari (#2450)
Only enable the stream cloning behaviour on Safari: it was causing
the audio renderer on Chrome (both desktop and Android) to hang,
causing audio to fail sometimes in Element Call and other Chrome
tabs (eg. YouTube) to fail to play audio.

Fixes https://github.com/vector-im/element-call/issues/267
2022-06-10 21:02:04 +01:00
David Baker b97b862fb6 Emit unknown device errors for group call participants without e2e (#2447)
* Emit unknown device errors for group call participants without e2e

There are a number of different cases here: there were some before
when dealing with versions that didn't send deviceId. This catches
all of them and makes all these cases emit the same error.

* Add type
2022-06-10 12:01:40 +01:00
David Baker 5e766978b8 Set PTT mode on call correctly (#2445)
And not always to true. This was causing audio & video to start muted
sometimes on normal calls because the ICE connection state would change
to 'checking', causing the feeds to be muted.
2022-06-08 19:28:53 +01:00
David Baker 34ef7bc64a Mute disconnected peers in PTT mode (#2421)
When we lose ICE connection to peers, set the status of their feeds
to muted so to end their talking session.

For https://github.com/vector-im/element-call/issues/331
2022-05-31 13:43:37 +01:00
David Baker 18e2052af2 Wait for mute event to send in PTT mode (#2401)
This waits until the mute metadata update is sent to all the calls
before telling the user they're unmuted, when in PTT mode (and only
when starting to talk, ie. unmuting). This should help avoid situations
where the signalling connection is slow enough that the unmute event
takes long enough to reach the other side that you hear someone speak
before they've apparently unmuted.

Involves splitting out the method to send the metadata update.
2022-05-26 13:06:57 +01:00
David Baker aa0d3bd1f5 Handle other members having no e2e keys (#2383)
Fetch the device info once at the start of the cal and cache it
rather than fetching every time, and throw if we're supposed to be
using e2e but the other end has no e2e keys.
2022-05-19 19:09:15 +01:00
Robert Long 942a28ddf6 Add support for sending encrypted to-device events with OLM (#2322) 2022-05-18 14:45:26 +01:00
David Baker 87791cd391 Fix races when muting/unmuting (#2370)
await on the async operation so the promise we return resolves once
everything's actuall complete
2022-05-13 19:26:25 +01:00
David Baker 38e54ae7f2 Remove PTT 'other user speaking' logic (#2362)
This was also in Element Call, and whilst js-=sdk might be a more
sensible place, EC has all the information to do it properly (this
impl didn't take admin talk-over into account).
2022-05-11 16:31:14 +01:00
David Baker acef1d7dd0 Merge branch 'dbkr/group-call-merge' of github.com:matrix-org/matrix-js-sdk into dbkr/group-call-merge 2022-05-10 17:24:40 +01:00
David Baker da615fd512 More setTimeout typings 2022-05-10 17:24:21 +01:00
Michael Telatynski f4f05550ef Merge branch 'develop' into dbkr/group-call-merge 2022-05-10 17:11:20 +01:00
David Baker f475251ddd Merge remote-tracking branch 'origin/develop' into dbkr/group-call-merge 2022-05-10 16:50:45 +01:00
David Baker 83f61c96f3 Merge remote-tracking branch 'origin/develop' into dbkr/group-call-merge 2022-05-10 16:39:07 +01:00
David Baker 85a6a552b5 Make tests pass again
Now we know what that bit in the crypto unit test was for...
2022-05-10 16:30:04 +01:00
David Baker 9702e8a5fa Remove test 'fix'
as I can't work out why it was needed, so I can't justify keeping
it in the group calls merge. It should be PRed to develop separately
if needed.
2022-05-09 22:49:32 +01:00
David Baker d82c041b99 Merge remote-tracking branch 'origin/develop' into robertlong/group-call 2022-05-09 22:46:43 +01:00
David Baker 8d9cd0fcb3 Support for PTT group call mode (#2338)
* Add PTT call mode & mute by default in PTT calls (#2311)

No other parts of PTT calls implemented yet

* Make the tests pass again (#2316)

https://github.com/matrix-org/matrix-js-sdk/commit/3280394bf93622c096e3e260296f7f089b97846b
made call use a bunch of methods that weren't mocked in the tests.

* Add maximum trasmit time for PTT (#2312)

on sender side by muting mic after the max transmit time has elapsed.

* Don't allow user to unmute if another user is speaking  (#2313)

* Add maximum trasmit time for PTT

on sender side by muting mic after the max transmit time has elapsed.

* Don't allow user to unmute if another user is speaking

Based on https://github.com/matrix-org/matrix-js-sdk/pull/2312
For https://github.com/vector-im/element-call/issues/298

* Fix createGroupCall arguments (#2325)

Comma instead of a colon...
2022-05-03 13:14:52 +01:00
Robert Long 96ba061732 Fix shouldRequestAudio logging 2022-03-01 16:27:41 -08:00
David Baker ee4cbd1ec9 Don't remove streams that still have tracks (#2104)
If a renogotiation ends up with one track being removed, we removed
the whole stream, which would cause us to lose, for example, audio
rather than just video.
2022-03-01 15:39:36 -08:00
David Baker 2a0dc39eec Fix bug with ine-way audio after a transfer (#2193)
Seems chrome at least will give you a disabled audio track if you
already had another user media audio track and disabled it, so make
sure our tracks are enabled when we add them. We already did this
on one code path but it didn't get moved over when a new code path
was added.

On the plus side, we now know the reason for the ancient code that
had the comment asking what it was for, so update that.
2022-03-01 15:36:48 -08:00
Robert Long 6e25b13312 Send / add end-of-candidates messages 2022-02-28 16:07:36 -08:00
Robert Long 94c5e37570 Fix import 2022-02-25 10:01:17 -08:00
Robert Long 09fee4a2d9 Allow calls to terminate properly when calling stopClient 2022-02-25 09:48:19 -08:00
Robert Long 49994ac4fc Add checks for call/groupCall ended for updateLocalUsermediaStream 2022-02-25 09:42:41 -08:00
Robert Long e68cabc70e Add logging for all stream creation/cloning/muting 2022-02-24 12:55:08 -08:00
Robert Long c819ac634f Fix updating local media streams 2022-02-24 12:10:02 -08:00
Robert Long 17f5ab4191 Move device changes to the application. Add methods to set device ids 2022-02-23 15:07:13 -08:00
Robert Long e270f075a4 Fix call log 2022-02-22 16:57:43 -08:00
Robert Long 0ef6c2e35f Add callId to all logs 2022-02-18 17:47:01 -08:00
Robert Long 7a249e3ef5 Switch media devices on disconnect 2022-02-18 14:35:09 -08:00
Robert Long 353d6bab47 Fix and add a test for toDevice ordering 2022-02-18 11:35:56 -08:00
Robert Long 7f21f569d5 Process toDevice events in order 2022-02-17 14:08:17 -08:00
Robert Long fa5eae70dd Log complete sync errors 2022-02-17 14:07:21 -08:00
David Baker 3db056ad3e Enable max-bundle (#2182)
No particular reason to worry about old user agents here
2022-02-16 11:06:46 -08:00
Robert Long a2a127d9a4 Remove unused isSafari check 2022-02-15 10:53:28 -08:00
Robert Long d12bccd211 Remove safari hack 2022-02-15 10:51:22 -08:00
Robert Long d8e597ccdf Avoid glare 2022-02-11 17:01:22 -08:00
Robert Long c801690e28 Don't reuse local call feeds that have been added to a RTCPeerConnection 2022-02-11 16:56:47 -08:00
Robert Long b4fe00a3a8 Add answer/negotiate response promise chain 2022-02-10 10:31:52 -08:00
Robert Long d42e2fe2c0 Ignore duplicate streams when adding local feeds 2022-02-10 09:32:34 -08:00
Robert Long 4a4465b9fc Don't send candidates after the call has ended 2022-02-07 14:42:07 -08:00
Robert Long 1a78301adb Fix restartIce on FF Android 2022-02-04 12:11:37 -08:00
Robert Long bbf7020755 Remove log 2022-01-18 10:39:02 -08:00
Robert Long 592fb0cf10 Re-enable retries 2022-01-18 10:37:49 -08:00
Robert Long 015eb5d5c4 Add sender/dest session ids 2022-01-14 13:40:42 -08:00
Robert Long 42fef0e7aa Add user id to all send voip events 2022-01-13 14:10:39 -08:00
Robert Long 28f3169a28 Use replace error code when replacing incoming calls 2022-01-12 13:17:19 -08:00
Robert Long d8285aad00 Remove call from callEventHandler after hangup 2022-01-12 13:17:03 -08:00
Robert Long eeacf8c22c Dont filter unstable call events 2022-01-11 17:47:01 -08:00
Robert Long ee995cb39b Ensure call events are processed once and in order 2022-01-11 16:54:40 -08:00
Robert Long 7529af43e4 Add NewSession CallErrorCode 2022-01-11 16:54:12 -08:00
Robert Long 3fac6d7180 Replace outbound calls only 2022-01-10 16:46:55 -08:00
Robert Long 487bfc88ef Merge branch 'robertlong/group-call-session-id' into robertlong/group-call-disable-retries 2022-01-10 16:25:39 -08:00
Robert Long c91617a799 Force hangup replaced calls 2022-01-10 16:22:52 -08:00
Robert Long 87bf115967 Use session ids to resolve refresh during invite/answer 2022-01-10 15:57:40 -08:00
Robert Long 18bb5c3079 Log opponentDeviceId 2022-01-07 11:14:44 -08:00
Robert Long f3f9e41787 Emit sent voip events 2022-01-07 11:14:27 -08:00
Robert Long 7993dd7630 Log opponentDeviceId 2022-01-06 15:46:55 -08:00
Robert Long bef557976b Emit sent voip events 2022-01-06 15:24:59 -08:00
Robert Long 549f9b7e29 Disable retries 2022-01-06 14:44:16 -08:00
Robert Long 06d9d6207c Send device id along with to device signaling messages 2021-11-30 13:39:43 -08:00
Robert Long e336aceaba Expose webrtc related types/props 2021-11-30 13:01:35 -08:00
Robert Long fcc4b71f06 Add LocalStreamsChanged event to MediaHandler 2021-11-29 14:34:43 -08:00
Robert Long d1a62eddfc Set initial audio/video input ids 2021-11-24 12:43:50 -08:00
Robert Long ffbd10a7b8 Make updateLocalUsermediaStreams stop tracks 2021-11-22 13:26:29 -08:00
Robert Long d0e37ee323 Hopefully resolve a race condition with missing device ids 2021-11-19 16:49:20 -08:00
Robert Long 96ef535ebb Make unknown device error more useful 2021-11-19 16:06:29 -08:00
Robert Long 0683133d5b Dont start retry loop until weve sent the member state event 2021-11-19 16:02:26 -08:00
Robert Long 64c3ac55a4 Stop screenshare when screensharing track ended 2021-11-19 13:56:50 -08:00
Robert Long 5f06df8a87 Properly stop screensharing feed 2021-11-18 13:53:30 -08:00
Robert Long 3291846714 Merge branch 'robertlong/abort-sync-error' into robertlong/group-call 2021-11-17 16:10:20 -08:00
Robert Long 139904f297 Update sync state to error when aborting 2021-11-17 16:05:02 -08:00
Robert Long c2fe2ab270 Add additional logging for removing feeds/tracks 2021-11-15 14:20:55 -08:00
Robert Long 4e26f29032 Add unknown device errors 2021-11-15 12:05:34 -08:00
Robert Long 31391121dc Clean up logging 2021-11-15 11:38:47 -08:00
Robert Long 7d48a8394d Don't immediately start retry call loop 2021-11-15 10:48:24 -08:00
Robert Long 28da62c01c Add retry call loop 2021-11-09 15:31:27 -08:00
Robert Long e880cece93 Add restart ICE 2021-11-09 14:40:29 -08:00
Robert Long 97e8fcea75 Clean up replacing calls for Safari 2021-11-09 14:01:16 -08:00
Robert Long f28cb48fe1 Re-enable safari hack 2021-11-08 13:07:22 -08:00
Robert Long a2e255c2c9 Merge branch 'robertlong/group-call' of github.com:matrix-org/matrix-js-sdk into robertlong/group-call 2021-11-08 12:58:54 -08:00
Robert Long 74c5a20371 Temporarily disable safari hack 2021-11-08 12:57:38 -08:00
Robert Long 4b87907b92 Update local usermedia streams serially 2021-11-08 12:30:56 -08:00
Robert Long f76f708c96 Ad a longer wait to safari media stream hack 2021-11-05 14:31:38 -07:00
Robert Long 17f7dc5463 Keep track of original stream id for sdp stream metadata 2021-11-04 17:44:47 -07:00
Robert Long b253ad9e81 Preserve the disabled tracks when updating local usermedia stream 2021-11-04 17:22:37 -07:00
Robert Long c1f56ba3c4 Fix indentation 2021-11-04 11:46:20 -07:00
Robert Long 7998817f7e Send candidate queue again on finish to flush out queue 2021-11-04 11:44:11 -07:00
Robert Long bdc12a2544 Revert changes to gotCallFeedsForAnswer 2021-11-02 17:25:41 -07:00
Robert Long 5a92597abd Check if call ended before getting user media 2021-11-02 15:33:58 -07:00
Robert Long 6f695c1b82 Ignore call call state in glare resolution 2021-11-02 15:30:38 -07:00
Robert Long d99428f2c1 Remove duplicate call answer 2021-11-02 11:39:45 -07:00
Robert Long 4c9648a23b Sanitize call member state 2021-10-28 13:46:27 -07:00
Robert Long 8c5f88c4a7 Fix handling null call 2021-10-28 13:27:35 -07:00
Robert Long 923e9c4ada Ensure that member call state is set correctly 2021-10-28 12:25:00 -07:00
Robert Long 13d62e71b6 Fix stopping all media streams 2021-10-26 16:50:56 -07:00
Robert Long 32aca09f47 Merge branch 'to-device-olm' into robertlong/group-call 2021-10-26 15:27:44 -07:00
Matthew Hodgson 067ac62271 lint 2021-10-26 15:15:30 -07:00
Matthew Hodgson 841e6e999d handle promises normally now tests are fixed 2021-10-26 15:15:16 -07:00
Matthew Hodgson a48546f60d fix the tests (thanks @turt2live!!!) 2021-10-26 15:15:02 -07:00
Matthew Hodgson 2f09e9641c chain promises correctly; log rejects 2021-10-26 15:14:52 -07:00
Matthew Hodgson f46355e7c0 don't choke on missing promise 2021-10-26 15:14:42 -07:00
Matthew Hodgson 53397ee0d1 lint 2021-10-26 15:14:30 -07:00
Matthew Hodgson 5a83635ef5 switch encryptAndSendToDevices to return a promise rather than use a cb
and assert that olm sessions are open to the destination devices
2021-10-26 15:14:13 -07:00
Matthew Hodgson 56c0c9be4d fix example in readme 2021-10-26 15:14:01 -07:00
Matthew Hodgson 24406d2411 make it build 2021-10-26 15:13:46 -07:00
Matthew Hodgson aeeed6ecd7 clarify the factoring 2021-10-26 15:13:27 -07:00
Matthew Hodgson 9f3f9990ef untested first cut at factoring out a encryptAndSendToDevices method 2021-10-26 15:13:13 -07:00
Robert Long 119ce2e46f Fix inbound calls in Safari 2021-10-26 12:44:37 -07:00
Šimon Brandner fc8a867e8e Start processing member state events only after we've set out own (#2000)
This avoids a race condition where the other side would first receive the to-device messages and only then the member state event which would result in the call being ignored

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-25 14:02:28 -07:00
Robert Long b4d8c0b603 Fix updating member state with no existing calls 2021-10-22 11:27:02 -07:00
Robert Long 3b0d1b2696 Add check for existing group call session 2021-10-21 14:23:27 -07:00
Robert Long 5110e0b91e Merge branch 'develop' into robertlong/group-call 2021-10-21 13:10:05 -07:00
Robert Long 305de54106 Fix screensharing and webrtc races 2021-10-21 12:57:30 -07:00
Robert Long 0555f9db1c Only send to device messages to a single device 2021-10-19 17:05:21 -07:00
Robert Long 159e825877 Fix unnecessary param to placeCallWithCallFeeds 2021-10-19 15:31:59 -07:00
Robert Long 8131b3900d Use glare resolution to manage group call setup 2021-10-19 15:30:20 -07:00
Robert Long 431d7a0933 Merge branch 'develop' into robertlong/group-call 2021-10-19 12:37:39 -07:00
Robert Long e9b52e23d2 Rermove session id 2021-10-19 10:57:09 -07:00
Šimon Brandner 0148ad0766 Group call improvements (#1985)
* Add group call events to EventType

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Use EventType instead of a const

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Make logging around sending group call member state event a bit better

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Fix m.calls elements being null

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-19 10:25:47 -07:00
Robert Long 213f1134b6 Reduce logging for group calls 2021-10-18 11:49:15 -07:00
Robert Long 50e6a8f6b1 Add session_id check to group calls 2021-10-18 11:49:04 -07:00
Robert Long 4a82e1bf05 Merge pull request #1983 from SimonBrandner/robertlong/group-call
Remove left-over from old screen-sharing code
2021-10-15 16:39:21 -07:00
Šimon Brandner 843973c4da Remove left-over from old screen-sharing code
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-15 17:19:22 +02:00
Robert Long debeb66d6f Initialize speakingVolumeSamples for screenshare feeds 2021-10-14 17:24:05 -07:00
Robert Long 015d0f9135 Don't set local user as active speaker 2021-10-14 17:14:12 -07:00
Robert Long 5c8e7f2be0 Improve speaking detection using history 2021-10-14 13:34:20 -07:00
Robert Long 411b5f111c Fix talking indicator 2021-10-13 13:42:40 -07:00
Robert Long 2d231c0ae2 Fix how streams are stopped 2021-10-13 12:05:14 -07:00
Robert Long ec37eb8b6f Add support for switching media devices 2021-10-12 15:20:14 -07:00
Robert Long 1cdcebb5db Merge branch 'robertlong/change-media-device' into robertlong/group-call 2021-10-12 15:15:21 -07:00
Robert Long a0f6eea363 Add support for replacing existing sender tracks 2021-10-12 14:48:59 -07:00
Robert Long 18b1a44df7 Merge branch 'robertlong/change-media-device' into robertlong/group-call 2021-10-12 14:24:02 -07:00
Robert Long 4b6b1599a2 Change media devices mid-call 2021-10-12 14:11:57 -07:00
Robert Long a582b19435 Merge branch 'robertlong/group-call' of github.com:matrix-org/matrix-js-sdk into robertlong/group-call 2021-10-12 12:14:55 -07:00
Robert Long 4a8c3d273f Merge branch 'feature/answer-no-cam' into robertlong/group-call 2021-10-12 12:11:32 -07:00
Robert Long 8dc608d917 Fix connecting to a call without a webcam 2021-10-07 13:47:50 -07:00
Robert Long 7ef38ed1b2 Fix speaking threshold 2021-10-06 16:20:07 -07:00
Robert Long 593f62c1c4 Move to correct event types 2021-10-05 16:51:28 -07:00
Robert Long 04d674b8c7 Merge branch 'SimonBrandner-robertlong/group-call' into robertlong/group-call 2021-10-05 11:42:00 -07:00
Robert Long 27eb88f4a1 Update GroupCall to use new CallFeed constructor 2021-10-05 11:38:03 -07:00
Robert Long 1409a4f814 Merge branch 'develop' into robertlong/group-call 2021-10-05 11:34:55 -07:00
Šimon Brandner 8232896c85 Don't run screen-sharing code for each 1:1 call, share one call feed between them instead
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-05 06:17:01 +02:00
Šimon Brandner e2ed80ffa0 Add removeLocalFeed()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-05 06:17:01 +02:00
Šimon Brandner 8ac3841a2f Handle joining a call after someone has started screen-sharing
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-10-05 06:16:59 +02:00
Robert Long ba57736bf6 Remove log that's stalling FF 2021-10-04 15:43:56 -07:00
Robert Long 8be4ca909e Add participants to GroupCall 2021-10-04 15:36:36 -07:00
Robert Long 0d964523a9 Add screensharing to GroupCall 2021-09-30 16:04:15 -07:00
Robert Long bb504bc001 Handle getUserMedia permissions blocked 2021-09-30 11:39:34 -07:00
Robert Long 326aec9f9e Fix getUserMediaStream 2021-09-29 17:30:34 -07:00
Robert Long 688327dab5 Handle new group call rooms 2021-09-29 17:00:17 -07:00
Robert Long 3f4522ba88 Add more verbose logging for testing. Create group calls on first sync. 2021-09-29 16:22:44 -07:00
Robert Long 625983a2b2 Revert changes to package.json and .npmignore 2021-09-29 14:37:07 -07:00
Robert Long 137fd2bd40 Export group call enums 2021-09-29 14:35:42 -07:00
Robert Long 1e65bfd316 Fix initLocalCallFeed state 2021-09-29 14:35:32 -07:00
Robert Long 5da072712d Use prepack instead of prepare 2021-09-29 11:47:09 -07:00
Robert Long 529d61b5f4 Add .npmignore 2021-09-29 11:44:12 -07:00
Robert Long 5111ca622a Change main path 2021-09-29 11:36:11 -07:00
Robert Long f627507b86 Test prepare script for git installs 2021-09-29 11:03:42 -07:00
Robert Long aee4459201 Merge pull request #1955 from SimonBrandner/robertlong/group-call
De-duplication for group calls
2021-09-28 10:31:55 -07:00
Šimon Brandner 1a824750dd Don't emit the same thing twice
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-28 19:11:53 +02:00
Šimon Brandner 73cb5e1ee9 Fix order of execution
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-28 19:10:42 +02:00
Šimon Brandner 96bde1f706 Fix field keys
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-28 18:17:36 +02:00
Šimon Brandner 5251dcf67f De-duplicate pushNewLocalFeed
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-28 08:58:01 +02:00
Šimon Brandner ce0b0ea182 De-duplicate invite/answer code
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-28 08:57:44 +02:00
Robert Long 7a142e9102 Implement new group call state events 2021-09-27 17:06:09 -07:00
Robert Long f85aa44f28 Remove duplicate FeedChanged event 2021-09-27 15:02:59 -07:00
Robert Long efbf252e22 Merge pull request #1951 from SimonBrandner/robertlong/group-call
Use to-device events for group calls and other improvements
2021-09-27 14:50:39 -07:00
Robert Long d873f14b6d Merge branch 'develop' into robertlong/group-call 2021-09-27 12:10:31 -07:00
Šimon Brandner cf1ba12232 Use arrays of CallFeeds
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:22 +02:00
Šimon Brandner df208e4de8 Avoid having duplicate call feeds
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:22 +02:00
Šimon Brandner d8ef7f9f63 pushLocalFeed() -> pushNewLocalFeed()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:22 +02:00
Šimon Brandner 2515ba31a0 Use createNewMatrixCall()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:21 +02:00
Šimon Brandner 715c4577d0 Add a prop for not stopping local feeds on end
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:21 +02:00
Šimon Brandner a2f23900c9 Use groupCallId isntead of roomId in to-device messages
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:21 +02:00
Šimon Brandner e9e65cf484 Change type key
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:21 +02:00
Šimon Brandner 205c80ea28 Add groupCallId
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:20 +02:00
Šimon Brandner 678023717b Add a setState() method
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:20 +02:00
Šimon Brandner b535969845 Use to-device messages in group calls
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:20 +02:00
Šimon Brandner 027bc6bfc9 Handle incoming to-device call signalling
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:19 +02:00
Šimon Brandner 71ca424712 Allow for sending toDevice messages
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:19 +02:00
Šimon Brandner 3280394bf9 Figure out opponentMember from the userId rather than the sender
This is because to-device messages don't have a sender

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:19 +02:00
Šimon Brandner fc07530434 Add useToDevice to CallOpts
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-27 18:07:18 +02:00
Robert Long f592d4dbc5 Merge branch 'develop' into robertlong/group-call 2021-09-26 11:30:55 -07:00
Robert Long 96f48929ac Cleaning up group call state 2021-09-24 15:41:05 -07:00
Robert Long 454da84f6e Initialize activeSpeakerSamples 2021-09-24 13:29:23 -07:00
Robert Long 89bda6c2e5 Move from groupCallsParticipants to calls 2021-09-24 12:39:43 -07:00
Robert Long ac70dcfc91 Expose call feed getters on call 2021-09-23 16:23:32 -07:00
Robert Long 9c7cb3cbea Handle more edge cases around creating/ending group calls 2021-09-22 15:03:48 -07:00
Robert Long d8d7bd548f Merge branch 'SimonBrandner-robertlong/group-call' into robertlong/group-call 2021-09-22 12:18:53 -07:00
Šimon Brandner 55ef57ead8 Add GroupCallEventHandler
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-22 21:00:19 +02:00
Šimon Brandner 9996afed03 Throw with no room
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-22 21:00:18 +02:00
Šimon Brandner 61a80a11c9 Export CONF_ROOM
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-09-22 21:00:18 +02:00
Robert Long 6a8e8ed0a6 Merge branch 'develop' into robertlong/group-call 2021-09-22 11:58:42 -07:00
Robert Long 5895ce32fa Revert unintended babelrc edit 2021-09-21 21:00:47 -07:00
Robert Long fe0a268991 Merge branch 'robertlong/group-call' of github.com:matrix-org/matrix-js-sdk into robertlong/group-call 2021-09-21 17:11:12 -07:00
Robert Long 7f189b0abd Add endCall 2021-09-21 17:10:45 -07:00
Robert Long 6e07c9e900 Clean up group call event listeners properly on hangup 2021-09-21 14:48:22 -07:00
Robert Long bbeea51a36 Add callType to room state event 2021-09-21 14:24:51 -07:00
Robert Long 151b54ed65 Clean up GroupCallParticipant listeners on remove 2021-09-21 14:24:37 -07:00
Robert Long 18986cb33a Fix typo 2021-09-20 17:31:02 -07:00
Robert Long aef5d73de4 Fix emitting participants_changed event 2021-09-20 17:30:49 -07:00
Robert Long e4fc1f3628 Merge branch 'develop' into robertlong/group-call 2021-09-17 09:16:05 -07:00
Robert Long 8b1c173659 Avoid changing member on replaced call 2021-09-16 16:35:41 -07:00
Robert Long f0916f14d1 Merge branch 'develop' into robertlong/group-call 2021-09-15 14:26:13 -07:00
Robert Long a291f5ab05 Merge branch 'robertlong/clone-streams' into robertlong/group-call 2021-09-15 13:23:56 -07:00
Robert Long 2d7e07f4ed Update to use latest datachannel / clone media stream PRs 2021-09-15 12:45:42 -07:00
Robert Long 2427f75f98 Merge branch 'robertlong/datachannels' into robertlong/group-call 2021-09-15 12:40:37 -07:00
Robert Long d25fb71eba Merge branch 'robertlong/clone-streams' into robertlong/group-call 2021-09-15 12:39:50 -07:00
Robert Long c81b9d2fd9 Merge branch 'develop' into robertlong/group-call 2021-09-15 11:27:59 -07:00
Robert Long fb3ca90bc9 Fix private method signatures 2021-09-10 16:06:26 -07:00
Robert Long eb2a47623f Fix active speaker 2021-09-10 15:58:44 -07:00
Robert Long f18d8ead08 Fix usermedia feeds 2021-09-10 14:31:39 -07:00
Robert Long 2da14bd6e9 Fix call feed changed event handler 2021-09-10 10:01:54 -07:00
Robert Long 1dbb776e12 Revert register types 2021-09-09 17:07:18 -07:00
Robert Long 07b2c57064 Remove CallFeed export 2021-09-09 16:40:08 -07:00
Robert Long 7021f70a66 Move from constants to configureable public variables 2021-09-09 16:24:26 -07:00
Robert Long 503e954671 Merge branch 'develop' into robertlong/group-call 2021-09-09 11:27:06 -07:00
Robert Long 2add1fcbcb Clean up group call events 2021-09-08 14:37:21 -07:00
Robert Long 4fe115b2c4 Add initial group call logic 2021-09-08 13:27:38 -07:00
Robert Long 60e168806d Properly dispose of call feeds 2021-09-02 13:27:13 -07:00
Robert Long 03dfab1282 Export CallFeed 2021-09-02 13:01:43 -07:00
Robert Long 19302ea4fb Fix initWithInvitePromise 2021-08-31 16:10:37 -07:00
Robert Long d5aaed67ba Merge branch 'develop' into robertlong/full-mesh-voip 2021-08-31 16:08:02 -07:00
Robert Long 8fe6afd9ab Merge branch 'master' into robertlong/full-mesh-voip 2021-08-31 16:02:05 -07:00
Robert Long 782fbb115f Stop all media on hangup 2021-08-20 14:42:41 -07:00
Robert Long 3971bf34ed Merge branch 'master' into robertlong/full-mesh-voip 2021-08-20 12:09:27 -07:00
Robert Long 6dac6e53f7 Merge branch 'develop' into robertlong/full-mesh-voip 2021-08-11 11:49:52 -07:00
Robert Long 7ec84e92a0 Merge branch 'develop' into robertlong/full-mesh-voip 2021-08-11 11:38:15 -07:00
Robert Long 154e5c45a6 Clear localAVStream when stopping local media stream. 2021-08-09 11:02:09 -07:00
Robert Long 2cd5c813ac Add local media stream functions to client 2021-08-05 18:22:29 -07:00
Robert Long 1c5101aa1a Add ice disconnected timeout 2021-08-04 18:23:21 -07:00
Robert Long 76f11bee9e Fix invitee glare detection and incoming call event 2021-07-26 11:38:18 -07:00
Robert Long 91f409e8f4 Add invitee field 2021-07-21 23:29:08 -07:00
145 changed files with 11353 additions and 2695 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",
},
}],
};
+3
View File
@@ -1 +1,4 @@
* @matrix-org/element-web
/src/webrtc @matrix-org/element-call-reviewers
/spec/*/webrtc @matrix-org/element-call-reviewers
+34
View File
@@ -0,0 +1,34 @@
name: Deploy documentation PR preview
on:
workflow_run:
workflows: [ "Static Analysis" ]
types:
- completed
jobs:
netlify:
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
steps:
# 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@e6e25ac3a2b93187502a8be1ef9e9603afc34925 # v2.24.2
with:
workflow: static_analysis.yml
run_id: ${{ github.event.workflow_run.id }}
name: docs
path: docs
- name: 📤 Deploy to Netlify
uses: matrix-org/netlify-pr-preview@v1
with:
path: docs
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
site_id: ${{ secrets.NETLIFY_SITE_ID }}
desc: Documentation preview
deployment_env: PR Documentation Preview
+5 -5
View File
@@ -24,10 +24,7 @@ jobs:
- name: 📋 Copy to temp
run: |
tag="${{ github.ref_name }}"
version="${tag#v}"
echo "VERSION=$version" >> $GITHUB_ENV
cp -a "./.jsdoc/matrix-js-sdk/$version" $RUNNER_TEMP
cp -a "./_docs" "$RUNNER_TEMP/"
- name: 🧮 Checkout gh-pages
uses: actions/checkout@v3
@@ -36,7 +33,10 @@ jobs:
- name: 🔪 Prepare
run: |
cp -a "$RUNNER_TEMP/$VERSION" .
tag="${{ github.ref_name }}"
VERSION="${tag#v}"
[ ! -e "$VERSION" ] || rm -r $VERSION
cp -r $RUNNER_TEMP/docs/ $VERSION
# Add the new directory to the index if it isn't there already
if ! grep -q ">Version $VERSION</a>" index.html; then
+8 -3
View File
@@ -5,6 +5,11 @@ on:
secrets:
SONAR_TOKEN:
required: true
inputs:
extra_args:
type: string
required: false
description: "Extra args to pass to SonarCloud"
jobs:
sonarqube:
runs-on: ubuntu-latest
@@ -22,7 +27,7 @@ jobs:
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v2.2
uses: matrix-org/sonarcloud-workflow-action@v2.3
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
@@ -33,8 +38,8 @@ jobs:
coverage_run_id: ${{ github.event.workflow_run.id }}
coverage_workflow_name: tests.yml
coverage_extract_path: coverage
extra_args: ${{ inputs.extra_args }}
- uses: Sibz/github-status-action@v1
if: always()
with:
+28
View File
@@ -8,8 +8,36 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
jobs:
# This is a workaround for https://github.com/SonarSource/SonarJS/issues/578
prepare:
name: Prepare
runs-on: ubuntu-latest
outputs:
reportPaths: ${{ steps.extra_args.outputs.reportPaths }}
testExecutionReportPaths: ${{ steps.extra_args.outputs.testExecutionReportPaths }}
steps:
# 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@v2
with:
workflow: tests.yaml
run_id: ${{ github.event.workflow_run.id }}
name: coverage
path: coverage
- id: extra_args
run: |
coverage=$(find coverage -type f -name '*lcov.info' | tr '\n' ',' | sed 's/,$//g')
echo "reportPaths=$coverage" >> $GITHUB_OUTPUT
reports=$(find coverage -type f -name 'jest-sonar-report*.xml' | tr '\n' ',' | sed 's/,$//g')
echo "testExecutionReportPaths=$reports" >> $GITHUB_OUTPUT
sonarqube:
name: 🩻 SonarQube
needs: prepare
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
extra_args: -Dsonar.javascript.lcov.reportPaths=${{ needs.prepare.outputs.reportPaths }} -Dsonar.testExecutionReportPaths=${{ needs.prepare.outputs.testExecutionReportPaths }}
+9 -1
View File
@@ -64,6 +64,14 @@ jobs:
- name: Generate Docs
run: "yarn run gendoc"
- name: Upload Artifact
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
@@ -94,7 +102,7 @@ jobs:
use-check: false
check-fail-mode: added
output-behaviour: annotate
ts-extra-args: '--strict'
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 }}
+27 -3
View File
@@ -8,25 +8,49 @@ concurrency:
cancel-in-progress: true
jobs:
jest:
name: Jest
name: 'Jest [${{ matrix.specs }}] (Node ${{ matrix.node }})'
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
specs: [browserify, integ, unit]
node: [16, 18, latest]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Yarn cache
- name: Setup Node
uses: actions/setup-node@v3
with:
cache: 'yarn'
node-version: ${{ matrix.node }}
- name: Install dependencies
run: "yarn install"
- name: Build
if: matrix.specs == 'browserify'
run: "yarn build"
- name: Get number of CPU cores
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
run: "yarn coverage --ci --reporters github-actions"
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
env:
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
- name: Upload Artifact
uses: actions/upload-artifact@v3
+2 -2
View File
@@ -1,5 +1,5 @@
/.jsdocbuild
/.jsdoc
/_docs
.DS_Store
node_modules
/.npmrc
+84
View File
@@ -1,3 +1,87 @@
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)
==================================================================================================
## ✨ Features
* Make calls go back to 'connecting' state when media lost ([\#2880](https://github.com/matrix-org/matrix-js-sdk/pull/2880)).
* Add ability to send unthreaded receipt ([\#2878](https://github.com/matrix-org/matrix-js-sdk/pull/2878)).
* Add way to abort search requests ([\#2877](https://github.com/matrix-org/matrix-js-sdk/pull/2877)).
* sliding sync: add custom room subscriptions support ([\#2834](https://github.com/matrix-org/matrix-js-sdk/pull/2834)).
* webrtc: add advanced audio settings ([\#2434](https://github.com/matrix-org/matrix-js-sdk/pull/2434)). Contributed by @MrAnno.
* Add support for group calls using MSC3401 ([\#2553](https://github.com/matrix-org/matrix-js-sdk/pull/2553)).
* Make the js-sdk conform to tsc --strict ([\#2835](https://github.com/matrix-org/matrix-js-sdk/pull/2835)). Fixes #2112 #2116 and #2124.
* Let leave requests outlive the window ([\#2815](https://github.com/matrix-org/matrix-js-sdk/pull/2815)). Fixes vector-im/element-call#639.
* Add event and message capabilities to RoomWidgetClient ([\#2797](https://github.com/matrix-org/matrix-js-sdk/pull/2797)).
* Misc fixes for group call widgets ([\#2657](https://github.com/matrix-org/matrix-js-sdk/pull/2657)).
* Support nested Matrix clients via the widget API ([\#2473](https://github.com/matrix-org/matrix-js-sdk/pull/2473)).
* Set max average bitrate on PTT calls ([\#2499](https://github.com/matrix-org/matrix-js-sdk/pull/2499)). Fixes vector-im/element-call#440.
* Add config option for e2e group call signalling ([\#2492](https://github.com/matrix-org/matrix-js-sdk/pull/2492)).
* Enable DTX on audio tracks in calls ([\#2482](https://github.com/matrix-org/matrix-js-sdk/pull/2482)).
* Don't ignore call member events with a distant future expiration date ([\#2466](https://github.com/matrix-org/matrix-js-sdk/pull/2466)).
* Expire call member state events after 1 hour ([\#2446](https://github.com/matrix-org/matrix-js-sdk/pull/2446)).
* Emit unknown device errors for group call participants without e2e ([\#2447](https://github.com/matrix-org/matrix-js-sdk/pull/2447)).
* Mute disconnected peers in PTT mode ([\#2421](https://github.com/matrix-org/matrix-js-sdk/pull/2421)).
* Add support for sending encrypted to-device events with OLM ([\#2322](https://github.com/matrix-org/matrix-js-sdk/pull/2322)). Contributed by @robertlong.
* Support for PTT group call mode ([\#2338](https://github.com/matrix-org/matrix-js-sdk/pull/2338)).
## 🐛 Bug Fixes
* Fix registration add phone number not working ([\#2876](https://github.com/matrix-org/matrix-js-sdk/pull/2876)). Contributed by @bagvand.
* Use an underride rule for Element Call notifications ([\#2873](https://github.com/matrix-org/matrix-js-sdk/pull/2873)). Fixes vector-im/element-web#23691.
* Fixes unwanted highlight notifications with encrypted threads ([\#2862](https://github.com/matrix-org/matrix-js-sdk/pull/2862)).
* Extra insurance that we don't mix events in the wrong timelines - v2 ([\#2856](https://github.com/matrix-org/matrix-js-sdk/pull/2856)). Contributed by @MadLittleMods.
* Hide pending events in thread timelines ([\#2843](https://github.com/matrix-org/matrix-js-sdk/pull/2843)). Fixes vector-im/element-web#23684.
* Fix pagination token tracking for mixed room timelines ([\#2855](https://github.com/matrix-org/matrix-js-sdk/pull/2855)). Fixes vector-im/element-web#23695.
* Extra insurance that we don't mix events in the wrong timelines ([\#2848](https://github.com/matrix-org/matrix-js-sdk/pull/2848)). Contributed by @MadLittleMods.
* Do not freeze state in `initialiseState()` ([\#2846](https://github.com/matrix-org/matrix-js-sdk/pull/2846)).
* Don't remove our own member for a split second when entering a call ([\#2844](https://github.com/matrix-org/matrix-js-sdk/pull/2844)).
* Resolve races between `initLocalCallFeed` and `leave` ([\#2826](https://github.com/matrix-org/matrix-js-sdk/pull/2826)).
* Add throwOnFail to groupCall.setScreensharingEnabled ([\#2787](https://github.com/matrix-org/matrix-js-sdk/pull/2787)).
* Fix connectivity regressions ([\#2780](https://github.com/matrix-org/matrix-js-sdk/pull/2780)).
* Fix screenshare failing after several attempts ([\#2771](https://github.com/matrix-org/matrix-js-sdk/pull/2771)). Fixes vector-im/element-call#625.
* Don't block muting/unmuting on network requests ([\#2754](https://github.com/matrix-org/matrix-js-sdk/pull/2754)). Fixes vector-im/element-call#592.
* Fix ICE restarts ([\#2702](https://github.com/matrix-org/matrix-js-sdk/pull/2702)).
* Target widget actions at a specific room ([\#2670](https://github.com/matrix-org/matrix-js-sdk/pull/2670)).
* Add tests for ice candidate sending ([\#2674](https://github.com/matrix-org/matrix-js-sdk/pull/2674)).
* Prevent exception when muting ([\#2667](https://github.com/matrix-org/matrix-js-sdk/pull/2667)). Fixes vector-im/element-call#578.
* Fix race in creating calls ([\#2662](https://github.com/matrix-org/matrix-js-sdk/pull/2662)).
* Add client.waitUntilRoomReadyForGroupCalls() ([\#2641](https://github.com/matrix-org/matrix-js-sdk/pull/2641)).
* Wait for client to start syncing before making group calls ([\#2632](https://github.com/matrix-org/matrix-js-sdk/pull/2632)). Fixes #2589.
* Add GroupCallEventHandlerEvent.Room ([\#2631](https://github.com/matrix-org/matrix-js-sdk/pull/2631)).
* Add missing events from reemitter to GroupCall ([\#2527](https://github.com/matrix-org/matrix-js-sdk/pull/2527)). Contributed by @toger5.
* Prevent double mute status changed events ([\#2502](https://github.com/matrix-org/matrix-js-sdk/pull/2502)).
* Don't mute the remote side immediately in PTT calls ([\#2487](https://github.com/matrix-org/matrix-js-sdk/pull/2487)). Fixes vector-im/element-call#425.
* Fix some MatrixCall leaks and use a shared AudioContext ([\#2484](https://github.com/matrix-org/matrix-js-sdk/pull/2484)). Fixes vector-im/element-call#412.
* Don't block muting on determining whether the device exists ([\#2461](https://github.com/matrix-org/matrix-js-sdk/pull/2461)).
* Only clone streams on Safari ([\#2450](https://github.com/matrix-org/matrix-js-sdk/pull/2450)). Fixes vector-im/element-call#267.
* Set PTT mode on call correctly ([\#2445](https://github.com/matrix-org/matrix-js-sdk/pull/2445)). Fixes vector-im/element-call#382.
* Wait for mute event to send in PTT mode ([\#2401](https://github.com/matrix-org/matrix-js-sdk/pull/2401)).
* Handle other members having no e2e keys ([\#2383](https://github.com/matrix-org/matrix-js-sdk/pull/2383)). Fixes vector-im/element-call#338.
* Fix races when muting/unmuting ([\#2370](https://github.com/matrix-org/matrix-js-sdk/pull/2370)).
Changes in [21.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v21.1.0) (2022-11-08)
==================================================================================================
+10 -4
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
==========
@@ -295,12 +301,12 @@ API Reference
A hosted reference can be found at
http://matrix-org.github.io/matrix-js-sdk/index.html
This SDK uses JSDoc3 style comments. You can manually build and
This SDK uses [Typedoc](https://typedoc.org/guides/doccomments) doc comments. You can manually build and
host the API reference from the source files like this:
```
$ yarn gendoc
$ cd .jsdoc
$ cd _docs
$ python -m http.server 8005
```
-30
View File
@@ -1,30 +0,0 @@
{
"tags": {
"allowUnknownTags": true
},
"plugins": [
"node_modules/better-docs/category",
"node_modules/better-docs/typescript"
],
"source": {
"include": [
"src"
],
"includePattern": ".(ts|js)$"
},
"opts": {
"encoding": "utf8",
"destination": ".jsdoc",
"readme": "README.md",
"recurse": true,
"verbose": true,
"template": "node_modules/docdash"
},
"docdash": {
"static": true,
"private": false,
"search": true,
"collapse": true,
"typedefs": true
}
}
+20 -14
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "21.1.0",
"version": "22.0.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=16.0.0"
@@ -16,7 +16,7 @@
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "jsdoc -c jsdoc.json -P package.json",
"gendoc": "typedoc",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 0 src spec",
"lint:js-fix": "eslint --fix src spec",
@@ -54,13 +54,16 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/sdp-transform": "^2.4.5",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
"loglevel": "^1.7.1",
"matrix-events-sdk": "^0.0.1-beta.7",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.0.0",
"p-retry": "4",
"qs": "^6.9.6",
"sdp-transform": "^2.14.1",
"unhomoglyph": "^1.0.6"
},
"devDependencies": {
@@ -76,12 +79,13 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@casualbot/jest-sonar-reporter": "^2.2.5",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz",
"@types/bs58": "^4.0.1",
"@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",
@@ -89,25 +93,26 @@
"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.25.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": "^29.0.0",
"jest-localstorage-mock": "^2.4.6",
"jest-mock": "^29.0.0",
"jest-sonar-reporter": "^2.0.0",
"jsdoc": "^3.6.6",
"matrix-mock-request": "^2.5.0",
"rimraf": "^3.0.2",
"terser": "^5.5.1",
"tsify": "^5.0.2",
"typedoc": "^0.23.20",
"typedoc-plugin-missing-exports": "^1.0.0",
"typescript": "^4.5.3"
},
"jest": {
@@ -125,11 +130,12 @@
"text-summary",
"lcov"
],
"testResultsProcessor": "jest-sonar-reporter"
"testResultsProcessor": "@casualbot/jest-sonar-reporter"
},
"jestSonar": {
"reportPath": "coverage",
"sonar56x": true
"@casualbot/jest-sonar-reporter": {
"outputDirectory": "coverage",
"outputName": "jest-sonar-report.xml",
"relativePaths": true
},
"typings": "./lib/index.d.ts"
}
+1 -1
View File
@@ -11,6 +11,6 @@ sonar.exclusions=docs,examples,git-hooks
sonar.typescript.tsconfigPath=./tsconfig.json
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.coverage.exclusions=spec/**/*
sonar.testExecutionReportPaths=coverage/test-report.xml
sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
+21
View File
@@ -14,6 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
import type { MatrixClient, ClientEvent } from "../../src";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace NodeJS {
interface Global {
matrixcs: {
MatrixClient: typeof MatrixClient;
ClientEvent: typeof ClientEvent;
};
}
}
}
// stub for browser-matrix browserify tests
// @ts-ignore
global.XMLHttpRequest = jest.fn();
@@ -23,3 +38,9 @@ afterAll(() => {
// @ts-ignore
global.XMLHttpRequest = undefined;
});
// Akin to spec/setupTests.ts - but that won't affect the browser-matrix bundle
global.matrixcs = {
...global.matrixcs,
timeoutSignal: () => new AbortController().signal,
};
+2 -15
View File
@@ -16,27 +16,14 @@ limitations under the License.
import HttpBackend from "matrix-mock-request";
import "./setupTests";
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
import type { MatrixClient, ClientEvent } from "../../src";
import "./setupTests";// uses browser-matrix instead of the src
import type { MatrixClient } from "../../src";
const USER_ID = "@user:test.server";
const DEVICE_ID = "device_id";
const ACCESS_TOKEN = "access_token";
const ROOM_ID = "!room_id:server.test";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace NodeJS {
interface Global {
matrixcs: {
MatrixClient: typeof MatrixClient;
ClientEvent: typeof ClientEvent;
};
}
}
}
describe("Browserify Test", function() {
let client: MatrixClient;
let httpBackend: HttpBackend;
@@ -18,12 +18,14 @@ import * as utils from "../test-utils/test-utils";
import {
ClientEvent,
Direction,
EventStatus,
EventTimeline,
EventTimelineSet,
Filter,
IEvent,
MatrixClient,
MatrixEvent,
PendingEventOrdering,
Room,
} from "../../src/matrix";
import { logger } from "../../src/logger";
@@ -1162,6 +1164,25 @@ describe("MatrixClient event timelines", function() {
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
await flushHttp(room.fetchRoomThreads());
});
it("should prevent displaying pending events", async function() {
const room = new Room("room123", client, "john", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
const timelineSets = await room!.createThreadsTimelineSets();
expect(timelineSets).not.toBeNull();
const event = utils.mkMessage({
room: roomId, user: userId, msg: "a body", event: true,
});
event.status = EventStatus.SENDING;
room.addPendingEvent(event, "txn");
const [allThreads, myThreads] = timelineSets!;
expect(allThreads.getPendingEvents()).toHaveLength(0);
expect(myThreads.getPendingEvents()).toHaveLength(0);
expect(room.getPendingEvents()).toHaveLength(1);
});
});
describe("without server compatibility", function() {
+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 });
+5 -9
View File
@@ -707,17 +707,13 @@ describe("MatrixClient syncing", () => {
awaitSyncEvent(2),
]).then(() => {
const room = client!.getRoom(roomOne)!;
const stateAtStart = room.getLiveTimeline().getState(
EventTimeline.BACKWARDS,
);
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 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');
});
});
@@ -1603,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
});
});
});
+150
View File
@@ -1112,6 +1112,156 @@ describe("SlidingSync", () => {
});
});
describe("custom room subscriptions", () => {
beforeAll(setupClient);
afterAll(teardownClient);
const roomA = "!a";
const roomB = "!b";
const roomC = "!c";
const roomD = "!d";
const defaultSub = {
timeline_limit: 1,
required_state: [["m.room.create", ""]],
};
const customSubName1 = "sub1";
const customSub1 = {
timeline_limit: 2,
required_state: [["*", "*"]],
};
const customSubName2 = "sub2";
const customSub2 = {
timeline_limit: 3,
required_state: [["*", "*"]],
};
it("should be possible to use custom subscriptions on startup", async () => {
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
// the intention is for clients to set this up at startup
slidingSync.addCustomSubscription(customSubName1, customSub1);
slidingSync.addCustomSubscription(customSubName2, customSub2);
// then call these depending on the kind of room / context
slidingSync.useCustomSubscription(roomA, customSubName1);
slidingSync.useCustomSubscription(roomB, customSubName1);
slidingSync.useCustomSubscription(roomC, customSubName2);
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA, roomB, roomC, roomD]));
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.log("custom subs", body);
expect(body.room_subscriptions).toBeTruthy();
expect(body.room_subscriptions[roomA]).toEqual(customSub1);
expect(body.room_subscriptions[roomB]).toEqual(customSub1);
expect(body.room_subscriptions[roomC]).toEqual(customSub2);
expect(body.room_subscriptions[roomD]).toEqual(defaultSub);
}).respond(200, {
pos: "b",
lists: [],
extensions: {},
rooms: {},
});
slidingSync.start();
await httpBackend!.flushAllExpected();
slidingSync.stop();
});
it("should be possible to use custom subscriptions mid-connection", async () => {
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
// the intention is for clients to set this up at startup
slidingSync.addCustomSubscription(customSubName1, customSub1);
slidingSync.addCustomSubscription(customSubName2, customSub2);
// initially no subs
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.log("custom subs", body);
expect(body.room_subscriptions).toBeFalsy();
}).respond(200, {
pos: "b",
lists: [],
extensions: {},
rooms: {},
});
slidingSync.start();
await httpBackend!.flushAllExpected();
// now the user clicks on a room which uses the default sub
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.log("custom subs", body);
expect(body.room_subscriptions).toBeTruthy();
expect(body.room_subscriptions[roomA]).toEqual(defaultSub);
}).respond(200, {
pos: "b",
lists: [],
extensions: {},
rooms: {},
});
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
await httpBackend!.flushAllExpected();
// now the user clicks on a room which uses a custom sub
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.log("custom subs", body);
expect(body.room_subscriptions).toBeTruthy();
expect(body.room_subscriptions[roomB]).toEqual(customSub1);
expect(body.unsubscribe_rooms).toEqual([roomA]);
}).respond(200, {
pos: "b",
lists: [],
extensions: {},
rooms: {},
});
slidingSync.useCustomSubscription(roomB, customSubName1);
slidingSync.modifyRoomSubscriptions(new Set<string>([roomB]));
await httpBackend!.flushAllExpected();
// now the user uses a different sub for the same room: we don't unsub but just resend
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.log("custom subs", body);
expect(body.room_subscriptions).toBeTruthy();
expect(body.room_subscriptions[roomB]).toEqual(customSub2);
expect(body.unsubscribe_rooms).toBeFalsy();
}).respond(200, {
pos: "b",
lists: [],
extensions: {},
rooms: {},
});
slidingSync.useCustomSubscription(roomB, customSubName2);
slidingSync.modifyRoomSubscriptions(new Set<string>([roomB]));
await httpBackend!.flushAllExpected();
slidingSync.stop();
});
it("uses the default subscription for unknown subscription names", async () => {
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
slidingSync.addCustomSubscription(customSubName1, customSub1);
slidingSync.useCustomSubscription(roomA, "unknown name");
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data;
logger.log("custom subs", body);
expect(body.room_subscriptions).toBeTruthy();
expect(body.room_subscriptions[roomA]).toEqual(defaultSub);
}).respond(200, {
pos: "b",
lists: [],
extensions: {},
rooms: {},
});
slidingSync.start();
await httpBackend!.flushAllExpected();
slidingSync.stop();
});
});
describe("extensions", () => {
beforeAll(setupClient);
afterAll(teardownClient);
+6
View File
@@ -17,3 +17,9 @@ limitations under the License.
import DOMException from "domexception";
global.DOMException = DOMException;
jest.mock("../src/http-api/utils", () => ({
...jest.requireActual("../src/http-api/utils"),
// We mock timeoutSignal otherwise it causes tests to leave timers running
timeoutSignal: () => new AbortController().signal,
}));
+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;
+28
View File
@@ -0,0 +1,28 @@
/*
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.
*/
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
// other async methods which break the event loop, letting scheduled promise
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
// it manually (this is what sinon does under the hood). We do both in a loop
// until the thing we expect happens: hopefully this is the least flakey way
// and avoids assuming anything about the app's behaviour.
const realSetTimeout = setTimeout;
export function flushPromises() {
return new Promise(r => {
realSetTimeout(r, 1);
});
}
+10 -2
View File
@@ -5,7 +5,7 @@ import EventEmitter from "events";
import '../olm-loader';
import { logger } from '../../src/logger';
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { IContent, IEvent, IEventRelation, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src";
import { SyncState } from "../../src/sync";
import { eventMapperFor } from "../../src/event-mapper";
@@ -78,6 +78,7 @@ interface IEventOpts {
user?: string;
unsigned?: IUnsigned;
redacts?: string;
ts?: number;
}
let testEventIndex = 1; // counter for events, easier for comparison of randomly generated events
@@ -109,6 +110,7 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
txn_id: "~" + Math.random(),
redacts: opts.redacts,
origin_server_ts: opts.ts ?? 0,
};
if (opts.skey !== undefined) {
event.state_key = opts.skey;
@@ -237,11 +239,13 @@ export function mkMembershipCustom<T>(
});
}
interface IMessageOpts {
export interface IMessageOpts {
room?: string;
user: string;
msg?: string;
event?: boolean;
relatesTo?: IEventRelation;
ts?: number;
}
/**
@@ -269,6 +273,10 @@ export function mkMessage(
},
};
if (opts.relatesTo) {
eventOpts.content["m.relates_to"] = opts.relatesTo;
}
if (!eventOpts.content.body) {
eventOpts.content.body = "Random->" + Math.random();
}
+134
View File
@@ -0,0 +1,134 @@
/*
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.
*/
import { RelationType } from "../../src/@types/event";
import { MatrixClient } from "../../src/client";
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { Room } from "../../src/models/room";
import { Thread } from "../../src/models/thread";
import { mkMessage } from "./test-utils";
export const makeThreadEvent = ({ rootEventId, replyToEventId, ...props }: any & {
rootEventId: string; replyToEventId: string; event?: boolean;
}): MatrixEvent => mkMessage({
...props,
relatesTo: {
event_id: rootEventId,
rel_type: "m.thread",
['m.in_reply_to']: {
event_id: replyToEventId,
},
},
});
type MakeThreadEventsProps = {
roomId: Room["roomId"];
// root message user id
authorId: string;
// user ids of thread replies
// cycled through until thread length is fulfilled
participantUserIds: string[];
// number of messages in the thread, root message included
// optional, default 2
length?: number;
ts?: number;
// provide to set current_user_participated accurately
currentUserId?: string;
};
export const makeThreadEvents = ({
roomId, authorId, participantUserIds, length = 2, ts = 1, currentUserId,
}: MakeThreadEventsProps): { rootEvent: MatrixEvent, events: MatrixEvent[] } => {
const rootEvent = mkMessage({
user: authorId,
room: roomId,
msg: 'root event message ' + Math.random(),
ts,
event: true,
});
const rootEventId = rootEvent.getId();
const events = [rootEvent];
for (let i = 1; i < length; i++) {
const prevEvent = events[i - 1];
const replyToEventId = prevEvent.getId();
const user = participantUserIds[i % participantUserIds.length];
events.push(makeThreadEvent({
user,
room: roomId,
event: true,
msg: `reply ${i} by ${user}`,
rootEventId,
replyToEventId,
// replies are 1ms after each other
ts: ts + i,
}));
}
rootEvent.setUnsigned({
"m.relations": {
[RelationType.Thread]: {
latest_event: events[events.length - 1],
count: length,
current_user_participated: [...participantUserIds, authorId].includes(currentUserId ?? ""),
},
},
});
return { rootEvent, events };
};
type MakeThreadProps = {
room: Room;
client: MatrixClient;
authorId: string;
participantUserIds: string[];
length?: number;
ts?: number;
};
export const mkThread = ({
room,
client,
authorId,
participantUserIds,
length = 2,
ts = 1,
}: MakeThreadProps): { thread: Thread, rootEvent: MatrixEvent, events: MatrixEvent[] } => {
const { rootEvent, events } = makeThreadEvents({
roomId: room.roomId,
authorId,
participantUserIds,
length,
ts,
currentUserId: client.getUserId() ?? "",
});
expect(rootEvent).toBeTruthy();
for (const evt of events) {
room?.reEmitter.reEmit(evt, [
MatrixEventEvent.BeforeRedaction,
]);
}
const thread = room.createThread(rootEvent.getId() ?? "", rootEvent, events, true);
// So that we do not have to mock the thread loading
thread.initialEventsFetched = true;
thread.addEvents(events, true);
return { thread, rootEvent, events };
};
+390 -29
View File
@@ -14,6 +14,32 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {
ClientEvent,
ClientEventHandlerMap,
EventType,
GroupCall,
GroupCallIntent,
GroupCallType,
IContent,
ISendEventResponse,
MatrixClient,
MatrixEvent,
Room,
RoomState,
RoomStateEvent,
RoomStateEventHandlerMap,
} from "../../src";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { ReEmitter } from "../../src/ReEmitter";
import { SyncState } from "../../src/sync";
import { CallEvent, CallEventHandlerMap, MatrixCall } from "../../src/webrtc/call";
import { CallEventHandlerEvent, CallEventHandlerEventHandlerMap } from "../../src/webrtc/callEventHandler";
import { CallFeed } from "../../src/webrtc/callFeed";
import { GroupCallEventHandlerMap } from "../../src/webrtc/groupCall";
import { GroupCallEventHandlerEvent } from "../../src/webrtc/groupCallEventHandler";
import { IScreensharingOpts, MediaHandler } from "../../src/webrtc/mediaHandler";
export const DUMMY_SDP = (
"v=0\r\n" +
"o=- 5022425983810148698 2 IN IP4 127.0.0.1\r\n" +
@@ -54,8 +80,50 @@ export const DUMMY_SDP = (
"a=ssrc:3619738545 cname:2RWtmqhXLdoF4sOi\r\n"
);
export const USERMEDIA_STREAM_ID = "mock_stream_from_media_handler";
export const SCREENSHARE_STREAM_ID = "mock_screen_stream_from_media_handler";
class MockMediaStreamAudioSourceNode {
public connect() {}
}
class MockAnalyser {
public getFloatFrequencyData() { return 0.0; }
}
export class MockAudioContext {
constructor() {}
public createAnalyser() { return new MockAnalyser(); }
public createMediaStreamSource() { return new MockMediaStreamAudioSourceNode(); }
public close() {}
}
export class MockRTCPeerConnection {
localDescription: RTCSessionDescription;
private static instances: MockRTCPeerConnection[] = [];
private negotiationNeededListener?: () => void;
public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void;
public onTrackListener?: (e: RTCTrackEvent) => void;
public needsNegotiation = false;
public readyToNegotiate: Promise<void>;
private onReadyToNegotiate?: () => void;
public localDescription: RTCSessionDescription;
public signalingState: RTCSignalingState = "stable";
public transceivers: MockRTCRtpTransceiver[] = [];
public static triggerAllNegotiations(): void {
for (const inst of this.instances) {
inst.doNegotiation();
}
}
public static hasAnyPendingNegotiations(): boolean {
return this.instances.some(i => i.needsNegotiation);
}
public static resetInstances() {
this.instances = [];
}
constructor() {
this.localDescription = {
@@ -63,34 +131,133 @@ export class MockRTCPeerConnection {
type: 'offer',
toJSON: function() { },
};
this.readyToNegotiate = new Promise<void>(resolve => {
this.onReadyToNegotiate = resolve;
});
MockRTCPeerConnection.instances.push(this);
}
addEventListener() { }
createDataChannel(label: string, opts: RTCDataChannelInit) { return { label, ...opts }; }
createOffer() {
return Promise.resolve({});
public addEventListener(type: string, listener: () => void) {
if (type === 'negotiationneeded') {
this.negotiationNeededListener = listener;
} else if (type == 'icecandidate') {
this.iceCandidateListener = listener;
} else if (type == 'track') {
this.onTrackListener = listener;
}
}
setRemoteDescription() {
public createDataChannel(label: string, opts: RTCDataChannelInit) { return { label, ...opts }; }
public createOffer() {
return Promise.resolve({
type: 'offer',
sdp: DUMMY_SDP,
});
}
public createAnswer() {
return Promise.resolve({
type: 'answer',
sdp: DUMMY_SDP,
});
}
public setRemoteDescription() {
return Promise.resolve();
}
setLocalDescription() {
public setLocalDescription() {
return Promise.resolve();
}
close() { }
getStats() { return []; }
addTrack(track: MockMediaStreamTrack) { return new MockRTCRtpSender(track); }
public close() { }
public getStats() { return []; }
public addTransceiver(track: MockMediaStreamTrack): MockRTCRtpTransceiver {
this.needsNegotiation = true;
if (this.onReadyToNegotiate) this.onReadyToNegotiate();
const newSender = new MockRTCRtpSender(track);
const newReceiver = new MockRTCRtpReceiver(track);
const newTransceiver = new MockRTCRtpTransceiver(this);
newTransceiver.sender = newSender as unknown as RTCRtpSender;
newTransceiver.receiver = newReceiver as unknown as RTCRtpReceiver;
this.transceivers.push(newTransceiver);
return newTransceiver;
}
public addTrack(track: MockMediaStreamTrack): MockRTCRtpSender {
return this.addTransceiver(track).sender as unknown as MockRTCRtpSender;
}
public removeTrack() {
this.needsNegotiation = true;
if (this.onReadyToNegotiate) this.onReadyToNegotiate();
}
public getTransceivers(): MockRTCRtpTransceiver[] { return this.transceivers; }
public getSenders(): MockRTCRtpSender[] {
return this.transceivers.map(t => t.sender as unknown as MockRTCRtpSender);
}
public doNegotiation() {
if (this.needsNegotiation && this.negotiationNeededListener) {
this.needsNegotiation = false;
this.negotiationNeededListener();
}
}
}
export class MockRTCRtpSender {
constructor(public track: MockMediaStreamTrack) { }
replaceTrack(track: MockMediaStreamTrack) { this.track = track; }
public replaceTrack(track: MockMediaStreamTrack) { this.track = track; }
}
export class MockRTCRtpReceiver {
constructor(public track: MockMediaStreamTrack) { }
}
export class MockRTCRtpTransceiver {
constructor(private peerConn: MockRTCPeerConnection) {}
public sender?: RTCRtpSender;
public receiver?: RTCRtpReceiver;
public set direction(_: string) {
this.peerConn.needsNegotiation = true;
}
public setCodecPreferences = jest.fn<void, RTCRtpCodecCapability[]>();
}
export class MockMediaStreamTrack {
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) { }
stop() { }
public stop = jest.fn<void, []>();
public listeners: [string, (...args: any[]) => any][] = [];
public isStopped = false;
public settings?: MediaTrackSettings;
public getSettings(): MediaTrackSettings { return this.settings!; }
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
// implementation
public dispatchEvent(eventType: string) {
this.listeners.forEach(([t, c]) => {
if (t !== eventType) return;
c();
});
}
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.push([eventType, callback]);
}
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.filter(([t, c]) => {
return t !== eventType || c !== callback;
});
}
public typed(): MediaStreamTrack { return this as unknown as MediaStreamTrack; }
}
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
@@ -101,46 +268,240 @@ export class MockMediaStream {
private tracks: MockMediaStreamTrack[] = [],
) {}
listeners: [string, (...args: any[]) => any][] = [];
public listeners: [string, (...args: any[]) => any][] = [];
public isStopped = false;
dispatchEvent(eventType: string) {
public dispatchEvent(eventType: string) {
this.listeners.forEach(([t, c]) => {
if (t !== eventType) return;
c();
});
}
getTracks() { return this.tracks; }
getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); }
getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); }
addEventListener(eventType: string, callback: (...args: any[]) => any) {
public getTracks() { return this.tracks; }
public getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); }
public getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); }
public addEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.push([eventType, callback]);
}
removeEventListener(eventType: string, callback: (...args: any[]) => any) {
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.filter(([t, c]) => {
return t !== eventType || c !== callback;
});
}
addTrack(track: MockMediaStreamTrack) {
public addTrack(track: MockMediaStreamTrack) {
this.tracks.push(track);
this.dispatchEvent("addtrack");
}
removeTrack(track: MockMediaStreamTrack) { this.tracks.splice(this.tracks.indexOf(track), 1); }
public removeTrack(track: MockMediaStreamTrack) { this.tracks.splice(this.tracks.indexOf(track), 1); }
public clone(): MediaStream {
return new MockMediaStream(this.id + ".clone", this.tracks).typed();
}
public isCloneOf(stream: MediaStream) {
return this.id === stream.id + ".clone";
}
// syntactic sugar for typing
public typed(): MediaStream {
return this as unknown as MediaStream;
}
}
export class MockMediaDeviceInfo {
constructor(
public kind: "audio" | "video",
public kind: "audioinput" | "videoinput" | "audiooutput",
) { }
public typed(): MediaDeviceInfo { return this as unknown as MediaDeviceInfo; }
}
export class MockMediaHandler {
getUserMediaStream(audio: boolean, video: boolean) {
const tracks: MockMediaStreamTrack[] = [];
if (audio) tracks.push(new MockMediaStreamTrack("audio_track", "audio"));
if (video) tracks.push(new MockMediaStreamTrack("video_track", "video"));
public userMediaStreams: MockMediaStream[] = [];
public screensharingStreams: MockMediaStream[] = [];
return new MockMediaStream("mock_stream_from_media_handler", tracks);
public getUserMediaStream(audio: boolean, video: boolean) {
const tracks: MockMediaStreamTrack[] = [];
if (audio) tracks.push(new MockMediaStreamTrack("usermedia_audio_track", "audio"));
if (video) tracks.push(new MockMediaStreamTrack("usermedia_video_track", "video"));
const stream = new MockMediaStream(USERMEDIA_STREAM_ID, tracks);
this.userMediaStreams.push(stream);
return stream;
}
stopUserMediaStream() { }
hasAudioDevice() { return true; }
public stopUserMediaStream(stream: MockMediaStream) {
stream.isStopped = true;
}
public getScreensharingStream = jest.fn((opts?: IScreensharingOpts) => {
const tracks = [new MockMediaStreamTrack("screenshare_video_track", "video")];
if (opts?.audio) tracks.push(new MockMediaStreamTrack("screenshare_audio_track", "audio"));
const stream = new MockMediaStream(SCREENSHARE_STREAM_ID, tracks);
this.screensharingStreams.push(stream);
return stream;
});
public stopScreensharingStream(stream: MockMediaStream) {
stream.isStopped = true;
}
public hasAudioDevice() { return true; }
public hasVideoDevice() { return true; }
public stopAllStreams() {}
public typed(): MediaHandler { return this as unknown as MediaHandler; }
}
export class MockMediaDevices {
public enumerateDevices = jest.fn<Promise<MediaDeviceInfo[]>, []>().mockResolvedValue([
new MockMediaDeviceInfo("audioinput").typed(),
new MockMediaDeviceInfo("videoinput").typed(),
]);
public getUserMedia = jest.fn<Promise<MediaStream>, [MediaStreamConstraints]>().mockReturnValue(
Promise.resolve(new MockMediaStream("local_stream").typed()),
);
public getDisplayMedia = jest.fn<Promise<MediaStream>, [MediaStreamConstraints]>().mockReturnValue(
Promise.resolve(new MockMediaStream("local_display_stream").typed()),
);
public typed(): MediaDevices { return this as unknown as MediaDevices; }
}
type EmittedEvents = CallEventHandlerEvent | CallEvent | ClientEvent | RoomStateEvent | GroupCallEventHandlerEvent;
type EmittedEventMap = CallEventHandlerEventHandlerMap &
CallEventHandlerMap &
ClientEventHandlerMap &
RoomStateEventHandlerMap &
GroupCallEventHandlerMap;
export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, EmittedEventMap> {
public mediaHandler = new MockMediaHandler();
constructor(public userId: string, public deviceId: string, public sessionId: string) {
super();
}
public groupCallEventHandler = {
groupCalls: new Map<string, GroupCall>(),
};
public callEventHandler = {
calls: new Map<string, MatrixCall>(),
};
public sendStateEvent = jest.fn<Promise<ISendEventResponse>, [
roomId: string, eventType: EventType, content: any, statekey: string,
]>();
public sendToDevice = jest.fn<Promise<{}>, [
eventType: string,
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
txnId?: string,
]>();
public getMediaHandler(): MediaHandler { return this.mediaHandler.typed(); }
public getUserId(): string { return this.userId; }
public getDeviceId(): string { return this.deviceId; }
public getSessionId(): string { return this.sessionId; }
public getTurnServers = () => [];
public isFallbackICEServerAllowed = () => false;
public reEmitter = new ReEmitter(new TypedEventEmitter());
public getUseE2eForGroupCall = () => false;
public checkTurnServers = () => null;
public getSyncState = jest.fn<SyncState | null, []>().mockReturnValue(SyncState.Syncing);
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 {
this.emit(
RoomStateEvent.Events,
event,
state,
null,
);
}
}
export class MockCallFeed {
constructor(
public userId: string,
public deviceId: string | undefined,
public stream: MockMediaStream,
) {}
public measureVolumeActivity(val: boolean) {}
public dispose() {}
public typed(): CallFeed {
return this as unknown as CallFeed;
}
}
export function installWebRTCMocks() {
global.navigator = {
mediaDevices: new MockMediaDevices().typed(),
} as unknown as Navigator;
global.window = {
// @ts-ignore Mock
RTCPeerConnection: MockRTCPeerConnection,
// @ts-ignore Mock
RTCSessionDescription: {},
// @ts-ignore Mock
RTCIceCandidate: {},
getUserMedia: () => new MockMediaStream("local_stream"),
};
// @ts-ignore Mock
global.document = {};
// @ts-ignore Mock
global.AudioContext = MockAudioContext;
// @ts-ignore Mock
global.RTCRtpReceiver = {
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
codecs: [],
headerExtensions: [],
}),
};
// @ts-ignore Mock
global.RTCRtpSender = {
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
codecs: [],
headerExtensions: [],
}),
};
}
export function makeMockGroupCallStateEvent(roomId: string, groupCallId: string, content: IContent = {
"m.type": GroupCallType.Video,
"m.intent": GroupCallIntent.Prompt,
}): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix),
getRoomId: jest.fn().mockReturnValue(roomId),
getTs: jest.fn().mockReturnValue(0),
getContent: jest.fn().mockReturnValue(content),
getStateKey: jest.fn().mockReturnValue(groupCallId),
} as unknown as MatrixEvent;
}
export function makeMockGroupCallMemberStateEvent(roomId: string, groupCallId: string): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getRoomId: jest.fn().mockReturnValue(roomId),
getTs: jest.fn().mockReturnValue(0),
getContent: jest.fn().mockReturnValue({}),
getStateKey: jest.fn().mockReturnValue(groupCallId),
} as unknown as MatrixEvent;
}
+18
View File
@@ -1122,4 +1122,22 @@ describe("Crypto", function() {
expect(free).toHaveBeenCalled();
});
});
describe("start", () => {
let client: TestClient;
beforeEach(async () => {
client = new TestClient("@alice:example.org", "aliceweb");
await client.client.initCrypto();
});
afterEach(async function() {
await client!.stop();
});
// start() is a no-op nowadays, so there's not much to test here.
it("should complete successfully", async () => {
await client!.client.crypto!.start();
});
});
});
+143
View File
@@ -0,0 +1,143 @@
/*
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.
*/
import '../../olm-loader';
import { TestClient } from '../../TestClient';
import { logger } from '../../../src/logger';
import { DEHYDRATION_ALGORITHM } from '../../../src/crypto/dehydration';
const Olm = global.Olm;
describe("Dehydration", () => {
if (!global.Olm) {
logger.warn('Not running dehydration unit tests: libolm not present');
return;
}
beforeAll(function() {
return global.Olm.init();
});
it("should rehydrate a dehydrated device", async () => {
const key = new Uint8Array([1, 2, 3]);
const alice = new TestClient(
"@alice:example.com", "Osborne2", undefined, undefined,
{
cryptoCallbacks: {
getDehydrationKey: async t => key,
},
},
);
const dehydratedDevice = new Olm.Account();
dehydratedDevice.create();
alice.httpBackend.when("GET", "/dehydrated_device").respond(200, {
device_id: "ABCDEFG",
device_data: {
algorithm: DEHYDRATION_ALGORITHM,
account: dehydratedDevice.pickle(new Uint8Array(key)),
},
});
alice.httpBackend.when("POST", "/dehydrated_device/claim").respond(200, {
success: true,
});
expect((await Promise.all([
alice.client.rehydrateDevice(),
alice.httpBackend.flushAllExpected(),
]))[0])
.toEqual("ABCDEFG");
expect(alice.client.getDeviceId()).toEqual("ABCDEFG");
});
it("should dehydrate a device", async () => {
const key = new Uint8Array([1, 2, 3]);
const alice = new TestClient(
"@alice:example.com", "Osborne2", undefined, undefined,
{
cryptoCallbacks: {
getDehydrationKey: async t => key,
},
},
);
await alice.client.initCrypto();
alice.httpBackend.when("GET", "/room_keys/version").respond(404, {
errcode: "M_NOT_FOUND",
});
let pickledAccount = "";
alice.httpBackend.when("PUT", "/dehydrated_device")
.check((req) => {
expect(req.data.device_data).toMatchObject({
algorithm: DEHYDRATION_ALGORITHM,
account: expect.any(String),
});
pickledAccount = req.data.device_data.account;
})
.respond(200, {
device_id: "ABCDEFG",
});
alice.httpBackend.when("POST", "/keys/upload/ABCDEFG")
.check((req) => {
expect(req.data).toMatchObject({
"device_keys": expect.objectContaining({
algorithms: expect.any(Array),
device_id: "ABCDEFG",
user_id: "@alice:example.com",
keys: expect.objectContaining({
"ed25519:ABCDEFG": expect.any(String),
"curve25519:ABCDEFG": expect.any(String),
}),
signatures: expect.objectContaining({
"@alice:example.com": expect.objectContaining({
"ed25519:ABCDEFG": expect.any(String),
}),
}),
}),
"one_time_keys": expect.any(Object),
"org.matrix.msc2732.fallback_keys": expect.any(Object),
});
})
.respond(200, {});
try {
const deviceId =
(await Promise.all([
alice.client.createDehydratedDevice(new Uint8Array(key), {}),
alice.httpBackend.flushAllExpected(),
]))[0];
expect(deviceId).toEqual("ABCDEFG");
expect(deviceId).not.toEqual("");
// try to rehydrate the dehydrated device
const rehydrated = new Olm.Account();
try {
rehydrated.unpickle(new Uint8Array(key), pickledAccount);
} finally {
rehydrated.free();
}
} finally {
alice.client?.crypto?.dehydrationManager?.stop();
alice.client?.crypto?.deviceList.stop();
}
});
});
@@ -18,7 +18,7 @@ import "../../../olm-loader";
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
import { logger } from "../../../../src/logger";
import { SAS } from "../../../../src/crypto/verification/SAS";
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
import { makeTestClients } from './util';
const Olm = global.Olm;
@@ -31,14 +31,9 @@ describe("verification request integration tests with crypto layer", function()
}
beforeAll(function() {
setupWebcrypto();
return Olm.init();
});
afterAll(() => {
teardownWebcrypto();
});
it("should request and accept a verification", async function() {
const [[alice, bob], clearTestClientTimeouts] = await makeTestClients(
[
+1 -6
View File
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import "../../../olm-loader";
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
import { makeTestClients } from './util';
import { MatrixEvent } from "../../../../src/models/event";
import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS";
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
@@ -41,14 +41,9 @@ describe("SAS verification", function() {
}
beforeAll(function() {
setupWebcrypto();
return Olm.init();
});
afterAll(() => {
teardownWebcrypto();
});
it("should error on an unexpected event", async function() {
//channel, baseApis, userId, deviceId, startEvent, request
const request = {
@@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
import '../../../olm-loader';
import { MatrixClient, MatrixEvent } from '../../../../src/matrix';
import { encodeBase64 } from "../../../../src/crypto/olmlib";
import { setupWebcrypto, teardownWebcrypto } from './util';
import { VerificationBase } from '../../../../src/crypto/verification/Base';
import { MatrixClient, MatrixEvent } from '../../../../src';
import "../../../../src/crypto"; // import this to cycle-break
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
import { VerificationRequest } from '../../../../src/crypto/verification/request/VerificationRequest';
import { IVerificationChannel } from '../../../../src/crypto/verification/request/Channel';
import { VerificationBase } from '../../../../src/crypto/verification/Base';
jest.useFakeTimers();
@@ -35,14 +36,9 @@ const testKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
describe("self-verifications", () => {
beforeAll(function() {
setupWebcrypto();
return global.Olm.init();
});
afterAll(() => {
teardownWebcrypto();
});
it("triggers a request for key sharing upon completion", async () => {
const userId = "@test:localhost";
-15
View File
@@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import nodeCrypto from "crypto";
import { TestClient } from '../../../TestClient';
import { MatrixEvent } from "../../../../src/models/event";
import { IRoomTimelineData } from "../../../../src/models/event-timeline-set";
@@ -118,16 +116,3 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[]
return [clients, destroy];
}
export function setupWebcrypto() {
global.crypto = {
getRandomValues: (buf) => {
return nodeCrypto.randomFillSync(buf as any);
},
} as unknown as Crypto;
}
export function teardownWebcrypto() {
// @ts-ignore undefined != Crypto
global.crypto = undefined;
}
@@ -20,7 +20,6 @@ import { ToDeviceChannel } from
"../../../../src/crypto/verification/request/ToDeviceChannel";
import { MatrixEvent } from "../../../../src/models/event";
import { MatrixClient } from "../../../../src/client";
import { setupWebcrypto, teardownWebcrypto } from "./util";
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
import { VerificationBase } from "../../../../src/crypto/verification/Base";
@@ -147,14 +146,6 @@ async function distributeEvent(
jest.useFakeTimers();
describe("verification request unit tests", function() {
beforeAll(function() {
setupWebcrypto();
});
afterAll(() => {
teardownWebcrypto();
});
it("transition from UNSENT to DONE through happy path", async function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
+331
View File
@@ -0,0 +1,331 @@
/**
* @jest-environment jsdom
*/
/*
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.
*/
// We have to use EventEmitter here to mock part of the matrix-widget-api
// project, which doesn't know about our TypeEventEmitter implementation at all
// eslint-disable-next-line no-restricted-imports
import { EventEmitter } from "events";
import { MockedObject } from "jest-mock";
import {
WidgetApi,
WidgetApiToWidgetAction,
MatrixCapabilities,
ITurnServer,
IRoomEvent,
} from "matrix-widget-api";
import { createRoomWidgetClient, MsgType } from "../../src/matrix";
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
import { SyncState } from "../../src/sync";
import { ICapabilities } from "../../src/embedded";
import { MatrixEvent } from "../../src/models/event";
import { ToDeviceBatch } from "../../src/models/ToDeviceMessage";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
class MockWidgetApi extends EventEmitter {
public start = jest.fn();
public requestCapability = jest.fn();
public requestCapabilities = jest.fn();
public requestCapabilityForRoomTimeline = jest.fn();
public requestCapabilityToSendEvent = jest.fn();
public requestCapabilityToReceiveEvent = jest.fn();
public requestCapabilityToSendMessage = jest.fn();
public requestCapabilityToReceiveMessage = jest.fn();
public requestCapabilityToSendState = jest.fn();
public requestCapabilityToReceiveState = jest.fn();
public requestCapabilityToSendToDevice = jest.fn();
public requestCapabilityToReceiveToDevice = jest.fn();
public sendRoomEvent = jest.fn(() => ({ event_id: `$${Math.random()}` }));
public sendStateEvent = jest.fn();
public sendToDevice = jest.fn();
public readStateEvents = jest.fn(() => []);
public getTurnServers = jest.fn(() => []);
public transport = { reply: jest.fn() };
}
describe("RoomWidgetClient", () => {
let widgetApi: MockedObject<WidgetApi>;
let client: MatrixClient;
beforeEach(() => {
widgetApi = new MockWidgetApi() as unknown as MockedObject<WidgetApi>;
});
afterEach(() => {
client.stopClient();
});
const makeClient = async (capabilities: ICapabilities): Promise<void> => {
const baseUrl = "https://example.org";
client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl });
expect(widgetApi.start).toHaveBeenCalled(); // needs to have been called early in order to not miss messages
widgetApi.emit("ready");
await client.startClient();
};
describe("events", () => {
it("sends", async () => {
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToSendEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
await client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 });
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
"org.matrix.rageshake_request", { request_id: 123 }, "!1:example.org",
);
});
it("receives", async () => {
const event = new MatrixEvent({
type: "org.matrix.rageshake_request",
event_id: "$pduhfiidph",
room_id: "!1:example.org",
sender: "@alice:example.org",
content: { request_id: 123 },
}).getEffectiveEvent();
await makeClient({ receiveEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveEvent).toHaveBeenCalledWith("org.matrix.rageshake_request");
const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.Event, resolve));
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve));
widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendEvent}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
);
// The client should've emitted about the received event
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
expect(await emittedSync).toEqual(SyncState.Syncing);
// It should've also inserted the event into the room object
const room = client.getRoom("!1:example.org");
expect(room).not.toBeNull();
expect(room!.getLiveTimeline().getEvents().map(e => e.getEffectiveEvent())).toEqual([event]);
});
});
describe("messages", () => {
it("requests permissions for specific message types", async () => {
await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToSendMessage).toHaveBeenCalledWith(MsgType.Text);
expect(widgetApi.requestCapabilityToReceiveMessage).toHaveBeenCalledWith(MsgType.Text);
});
it("requests permissions for all message types", async () => {
await makeClient({ sendMessage: true, receiveMessage: true });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToSendMessage).toHaveBeenCalledWith();
expect(widgetApi.requestCapabilityToReceiveMessage).toHaveBeenCalledWith();
});
// No point in testing sending and receiving since it's done exactly the
// same way as non-message events
});
describe("state events", () => {
const event = new MatrixEvent({
type: "org.example.foo",
event_id: "$sfkjfsksdkfsd",
room_id: "!1:example.org",
sender: "@alice:example.org",
state_key: "bar",
content: { hello: "world" },
}).getEffectiveEvent();
it("sends", async () => {
await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToSendState).toHaveBeenCalledWith("org.example.foo", "bar");
await client.sendStateEvent("!1:example.org", "org.example.foo", { hello: "world" }, "bar");
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
"org.example.foo", "bar", { hello: "world" }, "!1:example.org",
);
});
it("receives", async () => {
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.Event, resolve));
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve));
widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendEvent}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, { detail: { data: event } }),
);
// The client should've emitted about the received event
expect((await emittedEvent).getEffectiveEvent()).toEqual(event);
expect(await emittedSync).toEqual(SyncState.Syncing);
// 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);
});
it("backfills", async () => {
widgetApi.readStateEvents.mockImplementation(async (eventType, limit, stateKey) =>
eventType === "org.example.foo" && (limit ?? Infinity) > 0 && stateKey === "bar"
? [event as IRoomEvent]
: [],
);
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
expect(widgetApi.requestCapabilityToReceiveState).toHaveBeenCalledWith("org.example.foo", "bar");
const room = client.getRoom("!1:example.org");
expect(room).not.toBeNull();
expect(room!.currentState.getStateEvents("org.example.foo", "bar")?.getEffectiveEvent()).toEqual(event);
});
});
describe("to-device messages", () => {
const unencryptedContentMap = {
"@alice:example.org": { "*": { hello: "alice!" } },
"@bob:example.org": { bobDesktop: { hello: "bob!" } },
};
it("sends unencrypted (sendToDevice)", async () => {
await makeClient({ sendToDevice: ["org.example.foo"] });
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
await client.sendToDevice("org.example.foo", unencryptedContentMap);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
});
it("sends unencrypted (queueToDevice)", async () => {
await makeClient({ sendToDevice: ["org.example.foo"] });
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
const batch: ToDeviceBatch = {
eventType: "org.example.foo",
batch: [
{ userId: "@alice:example.org", deviceId: "*", payload: { hello: "alice!" } },
{ userId: "@bob:example.org", deviceId: "bobDesktop", payload: { hello: "bob!" } },
],
};
await client.queueToDevice(batch);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
});
it("sends encrypted (encryptAndSendToDevices)", async () => {
await makeClient({ sendToDevice: ["org.example.foo"] });
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
const payload = { type: "org.example.foo", hello: "world" };
await client.encryptAndSendToDevices(
[
{ userId: "@alice:example.org", deviceInfo: new DeviceInfo("aliceWeb") },
{ userId: "@bob:example.org", deviceInfo: new DeviceInfo("bobDesktop") },
],
payload,
);
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", true, {
"@alice:example.org": { aliceWeb: payload },
"@bob:example.org": { bobDesktop: payload },
});
});
it.each([
{ encrypted: false, title: "unencrypted" },
{ encrypted: true, title: "encrypted" },
])("receives $title", async ({ encrypted }) => {
await makeClient({ receiveToDevice: ["org.example.foo"] });
expect(widgetApi.requestCapabilityToReceiveToDevice).toHaveBeenCalledWith("org.example.foo");
const event = {
type: "org.example.foo",
sender: "@alice:example.org",
encrypted,
content: { hello: "world" },
};
const emittedEvent = new Promise<MatrixEvent>(resolve => client.once(ClientEvent.ToDeviceEvent, resolve));
const emittedSync = new Promise<SyncState>(resolve => client.once(ClientEvent.Sync, resolve));
widgetApi.emit(
`action:${WidgetApiToWidgetAction.SendToDevice}`,
new CustomEvent(`action:${WidgetApiToWidgetAction.SendToDevice}`, { detail: { data: event } }),
);
expect((await emittedEvent).getEffectiveEvent()).toEqual({
type: event.type,
sender: event.sender,
content: event.content,
});
expect((await emittedEvent).isEncrypted()).toEqual(encrypted);
expect(await emittedSync).toEqual(SyncState.Syncing);
});
});
it("gets TURN servers", async () => {
const server1: ITurnServer = {
uris: [
"turn:turn.example.com:3478?transport=udp",
"turn:10.20.30.40:3478?transport=tcp",
"turns:10.20.30.40:443?transport=tcp",
],
username: "1443779631:@user:example.com",
password: "JlKfBy1QwLrO20385QyAtEyIv0=",
};
const server2: ITurnServer = {
uris: [
"turn:turn.example.com:3478?transport=udp",
"turn:10.20.30.40:3478?transport=tcp",
"turns:10.20.30.40:443?transport=tcp",
],
username: "1448999322:@user:example.com",
password: "hunter2",
};
const clientServer1: IClientTurnServer = {
urls: server1.uris,
username: server1.username,
credential: server1.password,
};
const clientServer2: IClientTurnServer = {
urls: server2.uris,
username: server2.username,
credential: server2.password,
};
let emitServer2: () => void;
const getServer2 = new Promise<ITurnServer>(resolve => emitServer2 = () => resolve(server2));
widgetApi.getTurnServers.mockImplementation(async function* () {
yield server1;
yield await getServer2;
});
await makeClient({ turnServers: true });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC3846TurnServers);
// The first server should've arrived immediately
expect(client.getTurnServers()).toEqual([clientServer1]);
// Subsequent servers arrive asynchronously and should emit an event
const emittedServer = new Promise<IClientTurnServer[]>(resolve =>
client.once(ClientEvent.TurnServers, resolve),
);
emitServer2!();
expect(await emittedServer).toEqual([clientServer2]);
expect(client.getTurnServers()).toEqual([clientServer2]);
});
});
+76
View File
@@ -55,6 +55,23 @@ describe('EventTimelineSet', () => {
});
};
const mkThreadResponse = (root: MatrixEvent) => utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: userA,
room: roomId,
content: {
"body": "Thread response :: " + Math.random(),
"m.relates_to": {
"event_id": root.getId(),
"m.in_reply_to": {
"event_id": root.getId(),
},
"rel_type": "m.thread",
},
},
}, room.client);
beforeEach(() => {
client = utils.mock(MatrixClient, 'MatrixClient');
client.reEmitter = utils.mock(ReEmitter, 'ReEmitter');
@@ -117,6 +134,13 @@ describe('EventTimelineSet', () => {
});
describe('addEventToTimeline', () => {
let thread: Thread;
beforeEach(() => {
(client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true);
thread = new Thread("!thread_id:server", messageEvent, { room, client });
});
it("Adds event to timeline", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);
@@ -144,6 +168,58 @@ describe('EventTimelineSet', () => {
);
}).not.toThrow();
});
it("should not add an event to a timeline that does not belong to the timelineSet", () => {
const eventTimelineSet2 = new EventTimelineSet(room);
const liveTimeline2 = eventTimelineSet2.getLiveTimeline();
expect(liveTimeline2.getEvents().length).toStrictEqual(0);
expect(() => {
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline2, {
toStartOfTimeline: true,
});
}).toThrowError();
});
it("should not add a threaded reply to the main room timeline", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);
const threadedReplyEvent = mkThreadResponse(messageEvent);
eventTimelineSet.addEventToTimeline(threadedReplyEvent, liveTimeline, {
toStartOfTimeline: true,
});
expect(liveTimeline.getEvents().length).toStrictEqual(0);
});
it("should not add a normal message to the timelineSet representing a thread", () => {
const eventTimelineSetForThread = new EventTimelineSet(room, {}, client, thread);
const liveTimeline = eventTimelineSetForThread.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSetForThread.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
});
expect(liveTimeline.getEvents().length).toStrictEqual(0);
});
describe('non-room timeline', () => {
it('Adds event to timeline', () => {
const nonRoomEventTimelineSet = new EventTimelineSet(
// This is what we're specifically testing against, a timeline
// without a `room` defined
undefined,
);
const nonRoomEventTimeline = new EventTimeline(nonRoomEventTimelineSet);
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(0);
nonRoomEventTimelineSet.addEventToTimeline(messageEvent, nonRoomEventTimeline, {
toStartOfTimeline: true,
});
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(1);
});
});
});
describe('aggregateRelations', () => {
+32 -17
View File
@@ -1,15 +1,13 @@
import { mocked } from 'jest-mock';
import * as utils from "../test-utils/test-utils";
import { EventTimeline } from "../../src/models/event-timeline";
import { Direction, EventTimeline } from "../../src/models/event-timeline";
import { RoomState } from "../../src/models/room-state";
import { MatrixClient } from "../../src/matrix";
import { Room } from "../../src/models/room";
import { RoomMember } from "../../src/models/room-member";
import { EventTimelineSet } from "../../src/models/event-timeline-set";
jest.mock("../../src/models/room-state");
describe("EventTimeline", function() {
const roomId = "!foo:bar";
const userA = "@alice:bar";
@@ -23,7 +21,14 @@ describe("EventTimeline", function() {
const timelineSet = new EventTimelineSet(room);
jest.spyOn(room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet);
return new EventTimeline(timelineSet);
const timeline = new EventTimeline(timelineSet);
// We manually stub the methods we'll be mocking out later instead of mocking the whole module
// otherwise the default member property values (e.g. paginationToken) will be incorrect
timeline.getState(Direction.Backward)!.setStateEvents = jest.fn();
timeline.getState(Direction.Backward)!.getSentinelMember = jest.fn();
timeline.getState(Direction.Forward)!.setStateEvents = jest.fn();
timeline.getState(Direction.Forward)!.getSentinelMember = jest.fn();
return timeline;
};
beforeEach(function() {
@@ -55,13 +60,13 @@ describe("EventTimeline", function() {
];
timeline.initialiseState(events);
// @ts-ignore private prop
const timelineStartState = timeline.startState;
const timelineStartState = timeline.startState!;
expect(mocked(timelineStartState).setStateEvents).toHaveBeenCalledWith(
events,
{ timelineWasEmpty: undefined },
);
// @ts-ignore private prop
const timelineEndState = timeline.endState;
const timelineEndState = timeline.endState!;
expect(mocked(timelineEndState).setStateEvents).toHaveBeenCalledWith(
events,
{ timelineWasEmpty: undefined },
@@ -98,7 +103,17 @@ describe("EventTimeline", function() {
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toBe(null);
});
it("setPaginationToken should set token", function() {
it("setPaginationToken should set token", function() {
timeline.setPaginationToken("back", EventTimeline.BACKWARDS);
timeline.setPaginationToken("fwd", EventTimeline.FORWARDS);
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("back");
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toEqual("fwd");
});
it("should be able to store pagination tokens for mixed room timelines", () => {
const timelineSet = new EventTimelineSet(undefined);
const timeline = new EventTimeline(timelineSet);
timeline.setPaginationToken("back", EventTimeline.BACKWARDS);
timeline.setPaginationToken("fwd", EventTimeline.FORWARDS);
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("back");
@@ -185,14 +200,14 @@ describe("EventTimeline", function() {
sentinel.name = "Old Alice";
sentinel.membership = "join";
mocked(timeline.getState(EventTimeline.FORWARDS)).getSentinelMember
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember
.mockImplementation(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
mocked(timeline.getState(EventTimeline.BACKWARDS)).getSentinelMember
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember
.mockImplementation(function(uid) {
if (uid === userA) {
return oldSentinel;
@@ -225,14 +240,14 @@ describe("EventTimeline", function() {
sentinel.name = "Old Alice";
sentinel.membership = "join";
mocked(timeline.getState(EventTimeline.FORWARDS)).getSentinelMember
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember
.mockImplementation(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
mocked(timeline.getState(EventTimeline.BACKWARDS)).getSentinelMember
mocked(timeline.getState(EventTimeline.BACKWARDS)!).getSentinelMember
.mockImplementation(function(uid) {
if (uid === userA) {
return oldSentinel;
@@ -269,15 +284,15 @@ describe("EventTimeline", function() {
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false });
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
expect(events[0].forwardLooking).toBe(true);
expect(events[1].forwardLooking).toBe(true);
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
not.toHaveBeenCalled();
});
@@ -298,15 +313,15 @@ describe("EventTimeline", function() {
timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.addEvent(events[1], { toStartOfTimeline: true });
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
toHaveBeenCalledWith([events[0]], { timelineWasEmpty: undefined });
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).
toHaveBeenCalledWith([events[1]], { timelineWasEmpty: undefined });
expect(events[0].forwardLooking).toBe(false);
expect(events[1].forwardLooking).toBe(false);
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).
not.toHaveBeenCalled();
});
+2
View File
@@ -28,6 +28,8 @@ import {
import { sleep } from "../../../src/utils";
jest.mock("../../../src/utils");
// setupTests mocks `timeoutSignal` due to hanging timers
jest.unmock("../../../src/http-api/utils");
describe("timeoutSignal", () => {
jest.useFakeTimers();
+35
View File
@@ -1703,4 +1703,39 @@ describe("MatrixClient", function() {
expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy();
});
});
describe("using E2EE in group calls", () => {
const opts = {
baseUrl: "https://my.home.server",
idBaseUrl: identityServerUrl,
accessToken: "my.access.token",
store: store,
scheduler: scheduler,
userId: userId,
};
it("enables E2EE by default", () => {
const client = new MatrixClient(opts);
expect(client.getUseE2eForGroupCall()).toBe(true);
});
it("enables E2EE when enabled explicitly", () => {
const client = new MatrixClient({
useE2eForGroupCall: true,
...opts,
});
expect(client.getUseE2eForGroupCall()).toBe(true);
});
it("disables E2EE if disabled explicitly", () => {
const client = new MatrixClient({
useE2eForGroupCall: false,
...opts,
});
expect(client.getUseE2eForGroupCall()).toBe(false);
});
});
});
+52
View File
@@ -14,7 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClient } from "../../../src/client";
import { Room } from "../../../src/models/room";
import { Thread } from "../../../src/models/thread";
import { mkThread } from "../../test-utils/thread";
import { TestClient } from "../../TestClient";
describe('Thread', () => {
describe("constructor", () => {
@@ -25,4 +29,52 @@ describe('Thread', () => {
}).toThrow("element-web#22141: A thread requires a room in order to function");
});
});
describe("hasUserReadEvent", () => {
const myUserId = "@bob:example.org";
let client: MatrixClient;
let room: Room;
beforeEach(() => {
const testClient = new TestClient(
myUserId,
"DEVICE",
"ACCESS_TOKEN",
undefined,
{ timelineSupport: false },
);
client = testClient.client;
room = new Room("123", client, myUserId);
jest.spyOn(client, "getRoom").mockReturnValue(room);
});
afterAll(() => {
jest.resetAllMocks();
});
it("considers own events with no RR as read", () => {
const { thread, events } = mkThread({
room,
client,
authorId: myUserId,
participantUserIds: [myUserId],
length: 2,
});
expect(thread.hasUserReadEvent(myUserId, events.at(-1)!.getId() ?? "")).toBeTruthy();
});
it("considers other events with no RR as unread", () => {
const { thread, events } = mkThread({
room,
client,
authorId: myUserId,
participantUserIds: ["@alice:example.org"],
length: 2,
});
expect(thread.hasUserReadEvent("@alice:example.org", events.at(-1)!.getId() ?? "")).toBeFalsy();
});
});
});
+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);
+7 -16
View File
@@ -22,6 +22,8 @@ import { MatrixClient } from "../../src/client";
import { ToDeviceBatch } from '../../src/models/ToDeviceMessage';
import { logger } from '../../src/logger';
import { IStore } from '../../src/store';
import { flushPromises } from '../test-utils/flushPromises';
import { removeElement } from "../../src/utils";
const FAKE_USER = "@alice:example.org";
const FAKE_DEVICE_ID = "AAAAAAAA";
@@ -47,19 +49,6 @@ enum StoreType {
IndexedDB = 'IndexedDB',
}
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
// other async methods which break the event loop, letting scheduled promise
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
// it manually (this is what sinon does under the hood). We do both in a loop
// until the thing we expect happens: hopefully this is the least flakey way
// and avoids assuming anything about the app's behaviour.
const realSetTimeout = setTimeout;
function flushPromises() {
return new Promise(r => {
realSetTimeout(r, 1);
});
}
async function flushAndRunTimersUntil(cond: () => boolean) {
while (!cond()) {
await flushPromises();
@@ -75,6 +64,8 @@ describe.each([
let client: MatrixClient;
beforeEach(async function() {
jest.runOnlyPendingTimers();
jest.useRealTimers();
httpBackend = new MockHttpBackend();
let store: IStore;
@@ -300,7 +291,7 @@ describe.each([
],
});
expect(await httpBackend.flush(undefined, 1, 1)).toEqual(1);
expect(await httpBackend.flush(undefined, 1, 20)).toEqual(1);
await flushPromises();
const dummyEvent = new MatrixEvent({
@@ -328,12 +319,12 @@ describe.each([
});
}
const expectedCounts = [20, 1];
httpBackend.when(
"PUT", "/sendToDevice/org.example.foo/",
).check((request) => {
expect(Object.keys(request.data.messages).length).toEqual(20);
expect(removeElement(expectedCounts, c => c === Object.keys(request.data.messages).length)).toBeTruthy();
}).respond(200, {});
httpBackend.when(
"PUT", "/sendToDevice/org.example.foo/",
).check((request) => {
+43 -15
View File
@@ -16,10 +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";
@@ -69,15 +70,8 @@ const roomEvent = utils.mkEvent({
},
});
function mockServerSideSupport(client, hasServerSideSupport) {
const doesServerSupportUnstableFeature = client.doesServerSupportUnstableFeature;
client.doesServerSupportUnstableFeature = (unstableFeature) => {
if (unstableFeature === "org.matrix.msc3771") {
return Promise.resolve(hasServerSideSupport);
} else {
return doesServerSupportUnstableFeature(unstableFeature);
}
};
function mockServerSideSupport(client, serverSideSupport: ServerSupport) {
client.canSupport.set(Feature.ThreadUnreadNotifications, serverSideSupport);
}
describe("Read receipt", () => {
@@ -103,13 +97,31 @@ describe("Read receipt", () => {
expect(request.data.thread_id).toEqual(THREAD_ID);
}).respond(200, {});
mockServerSideSupport(client, true);
mockServerSideSupport(client, ServerSupport.Stable);
client.sendReceipt(threadEvent, ReceiptType.Read, {});
await httpBackend.flushAllExpected();
await flushPromises();
});
it("sends an unthreaded receipt", async () => {
httpBackend.when(
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
$roomId: ROOM_ID,
$receiptType: ReceiptType.Read,
$eventId: threadEvent.getId()!,
}),
).check((request) => {
expect(request.data.thread_id).toBeUndefined();
}).respond(200, {});
mockServerSideSupport(client, ServerSupport.Stable);
client.sendReadReceipt(threadEvent, ReceiptType.Read, true);
await httpBackend.flushAllExpected();
await flushPromises();
});
it("sends a room read receipt", async () => {
httpBackend.when(
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
@@ -121,7 +133,7 @@ describe("Read receipt", () => {
expect(request.data.thread_id).toEqual(MAIN_ROOM_TIMELINE);
}).respond(200, {});
mockServerSideSupport(client, true);
mockServerSideSupport(client, ServerSupport.Stable);
client.sendReceipt(roomEvent, ReceiptType.Read, {});
await httpBackend.flushAllExpected();
@@ -139,7 +151,7 @@ describe("Read receipt", () => {
expect(request.data.thread_id).toBeUndefined();
}).respond(200, {});
mockServerSideSupport(client, false);
mockServerSideSupport(client, ServerSupport.Unsupported);
client.sendReceipt(threadEvent, ReceiptType.Read, {});
await httpBackend.flushAllExpected();
@@ -157,11 +169,27 @@ describe("Read receipt", () => {
expect(request.data).toEqual({});
}).respond(200, {});
mockServerSideSupport(client, false);
mockServerSideSupport(client, ServerSupport.Unsupported);
client.sendReceipt(threadEvent, ReceiptType.Read, undefined);
await httpBackend.flushAllExpected();
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 {
File diff suppressed because it is too large Load Diff
+214 -6
View File
@@ -20,22 +20,117 @@ import {
EventTimeline,
EventTimelineSet,
EventType,
GroupCallIntent,
GroupCallType,
IRoomTimelineData,
MatrixCall,
MatrixEvent,
Room,
RoomEvent,
RoomMember,
} from "../../../src";
import { MatrixClient } from "../../../src/client";
import { CallEventHandler, CallEventHandlerEvent } from "../../../src/webrtc/callEventHandler";
import { GroupCallEventHandler } from "../../../src/webrtc/groupCallEventHandler";
import { SyncState } from "../../../src/sync";
import { installWebRTCMocks, MockRTCPeerConnection } from "../../test-utils/webrtc";
import { sleep } from "../../../src/utils";
describe("callEventHandler", () => {
it("should ignore a call if invite & hangup come within a single sync", () => {
const testClient = new TestClient();
const client = testClient.client;
const room = new Room("!room:id", client, "@user:id");
const timelineData: IRoomTimelineData = { timeline: new EventTimeline(new EventTimelineSet(room, {})) };
describe("CallEventHandler", () => {
let client: MatrixClient;
beforeEach(() => {
installWebRTCMocks();
client = new TestClient("@alice:foo", "somedevice", "token", undefined, {}).client;
client.callEventHandler = new CallEventHandler(client);
client.callEventHandler.start();
client.groupCallEventHandler = new GroupCallEventHandler(client);
client.groupCallEventHandler.start();
client.sendStateEvent = jest.fn().mockResolvedValue({});
});
afterEach(() => {
client.callEventHandler!.stop();
client.groupCallEventHandler!.stop();
});
const sync = async () => {
client.getSyncState = jest.fn().mockReturnValue(SyncState.Syncing);
client.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Prepared);
// We can't await the event processing
await sleep(10);
};
it("should enforce inbound toDevice message ordering", async () => {
const callEventHandler = client.callEventHandler!;
const event1 = new MatrixEvent({
type: EventType.CallInvite,
content: {
call_id: "123",
seq: 0,
},
});
callEventHandler["onToDeviceEvent"](event1);
expect(callEventHandler.callEventBuffer.length).toBe(1);
expect(callEventHandler.callEventBuffer[0]).toBe(event1);
const event2 = new MatrixEvent({
type: EventType.CallCandidates,
content: {
call_id: "123",
seq: 1,
},
});
callEventHandler["onToDeviceEvent"](event2);
expect(callEventHandler.callEventBuffer.length).toBe(2);
expect(callEventHandler.callEventBuffer[1]).toBe(event2);
const event3 = new MatrixEvent({
type: EventType.CallCandidates,
content: {
call_id: "123",
seq: 3,
},
});
callEventHandler["onToDeviceEvent"](event3);
expect(callEventHandler.callEventBuffer.length).toBe(2);
expect(callEventHandler.nextSeqByCall.get("123")).toBe(2);
expect(callEventHandler.toDeviceEventBuffers.get("123")?.length).toBe(1);
const event4 = new MatrixEvent({
type: EventType.CallCandidates,
content: {
call_id: "123",
seq: 4,
},
});
callEventHandler["onToDeviceEvent"](event4);
expect(callEventHandler.callEventBuffer.length).toBe(2);
expect(callEventHandler.nextSeqByCall.get("123")).toBe(2);
expect(callEventHandler.toDeviceEventBuffers.get("123")?.length).toBe(2);
const event5 = new MatrixEvent({
type: EventType.CallCandidates,
content: {
call_id: "123",
seq: 2,
},
});
callEventHandler["onToDeviceEvent"](event5);
expect(callEventHandler.callEventBuffer.length).toBe(5);
expect(callEventHandler.nextSeqByCall.get("123")).toBe(5);
expect(callEventHandler.toDeviceEventBuffers.get("123")?.length).toBe(0);
});
it("should ignore a call if invite & hangup come within a single sync", () => {
const room = new Room("!room:id", client, "@user:id");
const timelineData: IRoomTimelineData = { timeline: new EventTimeline(new EventTimelineSet(room, {})) };
// Fire off call invite then hangup within a single sync
const callInvite = new MatrixEvent({
@@ -62,4 +157,117 @@ describe("callEventHandler", () => {
expect(incomingCallEmitted).not.toHaveBeenCalled();
});
it("should ignore non-call events", async () => {
// @ts-ignore Mock handleCallEvent is private
jest.spyOn(client.callEventHandler, "handleCallEvent");
jest.spyOn(client, "checkTurnServers").mockReturnValue(Promise.resolve(true));
const room = new Room("!room:id", client, "@user:id");
const timelineData: IRoomTimelineData = { timeline: new EventTimeline(new EventTimelineSet(room, {})) };
client.emit(RoomEvent.Timeline, new MatrixEvent({
type: EventType.RoomMessage,
room_id: "!room:id",
content: {
text: "hello",
},
}), room, false, false, timelineData);
await sync();
// @ts-ignore Mock handleCallEvent is private
expect(client.callEventHandler.handleCallEvent).not.toHaveBeenCalled();
});
describe("handleCallEvent()", () => {
const incomingCallListener = jest.fn();
let timelineData: IRoomTimelineData;
let room: Room;
beforeEach(() => {
room = new Room("!room:id", client, client.getUserId()!);
timelineData = { timeline: new EventTimeline(new EventTimelineSet(room, {})) };
jest.spyOn(client, "checkTurnServers").mockReturnValue(Promise.resolve(true));
jest.spyOn(client, "getRoom").mockReturnValue(room);
jest.spyOn(room, "getMember").mockReturnValue({ user_id: client.getUserId() } as unknown as RoomMember);
client.on(CallEventHandlerEvent.Incoming, incomingCallListener);
});
afterEach(() => {
MockRTCPeerConnection.resetInstances();
jest.resetAllMocks();
});
it("should create a call when receiving an invite", async () => {
client.emit(RoomEvent.Timeline, new MatrixEvent({
type: EventType.CallInvite,
room_id: "!room:id",
content: {
call_id: "123",
},
}), room, false, false, timelineData);
await sync();
expect(incomingCallListener).toHaveBeenCalled();
});
it("should handle group call event", async () => {
let call: MatrixCall;
const groupCall = await client.createGroupCall(
room.roomId,
GroupCallType.Voice,
false,
GroupCallIntent.Ring,
);
const SESSION_ID = "sender_session_id";
const GROUP_CALL_ID = "group_call_id";
const DEVICE_ID = "device_id";
incomingCallListener.mockImplementation((c) => call = c);
jest.spyOn(client.groupCallEventHandler!, "getGroupCallById").mockReturnValue(groupCall);
// @ts-ignore Mock onIncomingCall is private
jest.spyOn(groupCall, "onIncomingCall");
await groupCall.enter();
client.emit(RoomEvent.Timeline, new MatrixEvent({
type: EventType.CallInvite,
room_id: "!room:id",
content: {
call_id: "123",
conf_id: GROUP_CALL_ID,
device_id: DEVICE_ID,
sender_session_id: SESSION_ID,
dest_session_id: client.getSessionId(),
},
}), room, false, false, timelineData);
await sync();
expect(incomingCallListener).toHaveBeenCalled();
expect(call!.groupCallId).toBe(GROUP_CALL_ID);
// @ts-ignore Mock opponentDeviceId is private
expect(call.opponentDeviceId).toBe(DEVICE_ID);
expect(call!.getOpponentSessionId()).toBe(SESSION_ID);
// @ts-ignore Mock onIncomingCall is private
expect(groupCall.onIncomingCall).toHaveBeenCalledWith(call);
groupCall.terminate(false);
});
it("ignores a call with a different invitee than us", async () => {
client.emit(RoomEvent.Timeline, new MatrixEvent({
type: EventType.CallInvite,
room_id: "!room:id",
content: {
call_id: "123",
invitee: "@bob:bar",
},
}), room, false, false, timelineData);
await sync();
expect(incomingCallListener).not.toHaveBeenCalled();
});
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,305 @@
/*
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.
*/
import { mocked } from "jest-mock";
import { ClientEvent } from "../../../src/client";
import { RoomMember } from "../../../src/models/room-member";
import { SyncState } from "../../../src/sync";
import {
GroupCallIntent,
GroupCallState,
GroupCallType,
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 { makeMockGroupCallStateEvent, MockCallMatrixClient } from "../../test-utils/webrtc";
const FAKE_USER_ID = "@alice:test.dummy";
const FAKE_DEVICE_ID = "AAAAAAA";
const FAKE_SESSION_ID = "session1";
const FAKE_ROOM_ID = "!roomid:test.dummy";
const FAKE_GROUP_CALL_ID = "fakegroupcallid";
describe('Group Call Event Handler', function() {
let groupCallEventHandler: GroupCallEventHandler;
let mockClient: MockCallMatrixClient;
let mockRoom: Room;
let mockMember: RoomMember;
beforeEach(() => {
mockClient = new MockCallMatrixClient(
FAKE_USER_ID, FAKE_DEVICE_ID, FAKE_SESSION_ID,
);
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((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);
});
describe("reacts to state changes", () => {
it("terminates call", async () => {
await groupCallEventHandler.start();
mockClient.emitRoomState(
makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID),
{ roomId: FAKE_ROOM_ID } as unknown as RoomState,
);
const groupCall = groupCallEventHandler.groupCalls.get(FAKE_ROOM_ID)!;
expect(groupCall.state).toBe(GroupCallState.LocalCallFeedUninitialized);
mockClient.emitRoomState(
makeMockGroupCallStateEvent(
FAKE_ROOM_ID, FAKE_GROUP_CALL_ID, {
"m.type": GroupCallType.Video,
"m.intent": GroupCallIntent.Prompt,
"m.terminated": GroupCallTerminationReason.CallEnded,
},
),
{
roomId: FAKE_ROOM_ID,
} as unknown as RoomState,
);
expect(groupCall.state).toBe(GroupCallState.Ended);
});
});
it("waits until client starts syncing", async () => {
mockClient.getSyncState.mockReturnValue(null);
let isStarted = false;
(async () => {
await groupCallEventHandler.start();
isStarted = true;
})();
const setSyncState = async (newState: SyncState) => {
const oldState = mockClient.getSyncState();
mockClient.getSyncState.mockReturnValue(newState);
mockClient.emit(ClientEvent.Sync, newState, oldState, undefined);
await flushPromises();
};
await flushPromises();
expect(isStarted).toEqual(false);
await setSyncState(SyncState.Prepared);
expect(isStarted).toEqual(false);
await setSyncState(SyncState.Syncing);
expect(isStarted).toEqual(true);
});
it("finds existing group calls when started", async () => {
const mockClientEmit = mockClient.emit = jest.fn();
mockClient.getRooms.mockReturnValue([mockRoom]);
await groupCallEventHandler.start();
expect(mockClientEmit).toHaveBeenCalledWith(
GroupCallEventHandlerEvent.Incoming,
expect.objectContaining({
groupCallId: FAKE_GROUP_CALL_ID,
}),
);
groupCallEventHandler.stop();
});
it("can wait until a room is ready for group calls", async () => {
await groupCallEventHandler.start();
const prom = groupCallEventHandler.waitUntilRoomReadyForGroupCalls(FAKE_ROOM_ID);
let resolved = false;
(async () => {
await prom;
resolved = true;
})();
expect(resolved).toEqual(false);
mockClient.emit(ClientEvent.Room, mockRoom);
await prom;
expect(resolved).toEqual(true);
groupCallEventHandler.stop();
});
it("fires events for incoming calls", async () => {
const onIncomingGroupCall = jest.fn();
mockClient.on(GroupCallEventHandlerEvent.Incoming, onIncomingGroupCall);
await groupCallEventHandler.start();
mockClient.emitRoomState(
makeMockGroupCallStateEvent(
FAKE_ROOM_ID, FAKE_GROUP_CALL_ID,
),
{
roomId: FAKE_ROOM_ID,
} as unknown as RoomState,
);
expect(onIncomingGroupCall).toHaveBeenCalledWith(expect.objectContaining({
groupCallId: FAKE_GROUP_CALL_ID,
}));
mockClient.off(GroupCallEventHandlerEvent.Incoming, onIncomingGroupCall);
});
it("handles data channel", async () => {
await groupCallEventHandler.start();
const dataChannelOptions = {
"maxPacketLifeTime": "life_time",
"maxRetransmits": "retransmits",
"ordered": "ordered",
"protocol": "protocol",
};
mockClient.emitRoomState(
makeMockGroupCallStateEvent(
FAKE_ROOM_ID,
FAKE_GROUP_CALL_ID,
{
"m.type": GroupCallType.Video,
"m.intent": GroupCallIntent.Prompt,
"dataChannelsEnabled": true,
dataChannelOptions,
},
),
{
roomId: FAKE_ROOM_ID,
} as unknown as RoomState,
);
// @ts-ignore Mock dataChannelsEnabled is private
expect(groupCallEventHandler.groupCalls.get(FAKE_ROOM_ID)?.dataChannelsEnabled).toBe(true);
// @ts-ignore Mock dataChannelOptions is private
expect(groupCallEventHandler.groupCalls.get(FAKE_ROOM_ID)?.dataChannelOptions).toStrictEqual(
dataChannelOptions,
);
});
describe("ignoring invalid group call state events", () => {
let mockClientEmit: jest.Func;
beforeEach(() => {
mockClientEmit = mockClient.emit = jest.fn();
});
afterEach(() => {
groupCallEventHandler.stop();
jest.clearAllMocks();
});
const setupCallAndStart = async (content?: IContent) => {
mocked(mockRoom.currentState.getStateEvents).mockReturnValue([
makeMockGroupCallStateEvent(
FAKE_ROOM_ID,
FAKE_GROUP_CALL_ID,
content,
),
] as unknown as MatrixEvent);
mockClient.getRooms.mockReturnValue([mockRoom]);
await groupCallEventHandler.start();
};
it("ignores terminated calls", async () => {
await setupCallAndStart({
"m.type": GroupCallType.Video,
"m.intent": GroupCallIntent.Prompt,
"m.terminated": GroupCallTerminationReason.CallEnded,
});
expect(mockClientEmit).not.toHaveBeenCalledWith(
GroupCallEventHandlerEvent.Incoming,
expect.objectContaining({
groupCallId: FAKE_GROUP_CALL_ID,
}),
);
});
it("ignores calls with invalid type", async () => {
await setupCallAndStart({
"m.type": "fake_type",
"m.intent": GroupCallIntent.Prompt,
});
expect(mockClientEmit).not.toHaveBeenCalledWith(
GroupCallEventHandlerEvent.Incoming,
expect.objectContaining({
groupCallId: FAKE_GROUP_CALL_ID,
}),
);
});
it("ignores calls with invalid intent", async () => {
await setupCallAndStart({
"m.type": GroupCallType.Video,
"m.intent": "fake_intent",
});
expect(mockClientEmit).not.toHaveBeenCalledWith(
GroupCallEventHandlerEvent.Incoming,
expect.objectContaining({
groupCallId: FAKE_GROUP_CALL_ID,
}),
);
});
it("ignores calls without a room", async () => {
mockClient.getRoom.mockReturnValue(undefined);
await setupCallAndStart();
expect(mockClientEmit).not.toHaveBeenCalledWith(
GroupCallEventHandlerEvent.Incoming,
expect.objectContaining({
groupCallId: FAKE_GROUP_CALL_ID,
}),
);
});
});
});
+462
View File
@@ -0,0 +1,462 @@
/*
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.
*/
import { GroupCall, MatrixCall, MatrixClient } from "../../../src";
import { MediaHandler, MediaHandlerEvent } from "../../../src/webrtc/mediaHandler";
import { MockMediaDeviceInfo, MockMediaDevices, MockMediaStream, MockMediaStreamTrack } from "../../test-utils/webrtc";
const FAKE_AUDIO_INPUT_ID = "aaaaaaaa";
const FAKE_VIDEO_INPUT_ID = "vvvvvvvv";
const FAKE_DESKTOP_SOURCE_ID = "ddddddd";
describe('Media Handler', function() {
let mockMediaDevices: MockMediaDevices;
let mediaHandler: MediaHandler;
let calls: Map<string, MatrixCall>;
let groupCalls: Map<string, GroupCall>;
beforeEach(() => {
mockMediaDevices = new MockMediaDevices();
global.navigator = {
mediaDevices: mockMediaDevices.typed(),
} as unknown as Navigator;
calls = new Map();
groupCalls = new Map();
mediaHandler = new MediaHandler({
callEventHandler: {
calls,
},
groupCallEventHandler: {
groupCalls,
},
} as unknown as MatrixClient);
});
it("does not trigger update after restore media settings ", () => {
mediaHandler.restoreMediaSettings(FAKE_AUDIO_INPUT_ID, FAKE_VIDEO_INPUT_ID);
expect(mockMediaDevices.getUserMedia).not.toHaveBeenCalled();
});
it("sets device IDs on restore media settings", async () => {
mediaHandler.restoreMediaSettings(FAKE_AUDIO_INPUT_ID, FAKE_VIDEO_INPUT_ID);
await mediaHandler.getUserMediaStream(true, true);
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
audio: expect.objectContaining({
deviceId: { ideal: FAKE_AUDIO_INPUT_ID },
}),
video: expect.objectContaining({
deviceId: { ideal: FAKE_VIDEO_INPUT_ID },
}),
}));
});
it("sets audio device ID", async () => {
await mediaHandler.setAudioInput(FAKE_AUDIO_INPUT_ID);
await mediaHandler.getUserMediaStream(true, false);
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
audio: expect.objectContaining({
deviceId: { ideal: FAKE_AUDIO_INPUT_ID },
}),
}));
});
it("sets audio settings", async () => {
await mediaHandler.setAudioSettings({
autoGainControl: false,
echoCancellation: true,
noiseSuppression: false,
});
await mediaHandler.getUserMediaStream(true, false);
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
audio: expect.objectContaining({
autoGainControl: { ideal: false },
echoCancellation: { ideal: true },
noiseSuppression: { ideal: false },
}),
}));
});
it("sets video device ID", async () => {
await mediaHandler.setVideoInput(FAKE_VIDEO_INPUT_ID);
await mediaHandler.getUserMediaStream(false, true);
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
video: expect.objectContaining({
deviceId: { ideal: FAKE_VIDEO_INPUT_ID },
}),
}));
});
it("sets media inputs", async () => {
await mediaHandler.setMediaInputs(FAKE_AUDIO_INPUT_ID, FAKE_VIDEO_INPUT_ID);
await mediaHandler.getUserMediaStream(true, true);
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
audio: expect.objectContaining({
deviceId: { ideal: FAKE_AUDIO_INPUT_ID },
}),
video: expect.objectContaining({
deviceId: { ideal: FAKE_VIDEO_INPUT_ID },
}),
}));
});
describe("updateLocalUsermediaStreams", () => {
let localStreamsChangedHandler: jest.Mock<void, []>;
beforeEach(() => {
localStreamsChangedHandler = jest.fn();
mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, localStreamsChangedHandler);
});
afterEach(() => {
mediaHandler.off(MediaHandlerEvent.LocalStreamsChanged, localStreamsChangedHandler);
});
it("does nothing if it has no streams", async () => {
mediaHandler.updateLocalUsermediaStreams();
expect(mockMediaDevices.getUserMedia).not.toHaveBeenCalled();
});
it("does not emit LocalStreamsChanged if it had no streams", async () => {
await mediaHandler.updateLocalUsermediaStreams();
expect(localStreamsChangedHandler).not.toHaveBeenCalled();
});
describe("with existing streams", () => {
let stopTrack: jest.Mock<void, []>;
let updateLocalUsermediaStream: jest.Mock;
beforeEach(() => {
stopTrack = jest.fn();
mediaHandler.userMediaStreams = [
{
getTracks: () => [{
stop: stopTrack,
} as unknown as MediaStreamTrack],
} as unknown as MediaStream,
];
updateLocalUsermediaStream = jest.fn();
});
it("stops existing streams", async () => {
mediaHandler.updateLocalUsermediaStreams();
expect(stopTrack).toHaveBeenCalled();
});
it("replaces streams on calls", async () => {
calls.set("some_call", {
hasLocalUserMediaAudioTrack: true,
hasLocalUserMediaVideoTrack: true,
callHasEnded: jest.fn().mockReturnValue(false),
updateLocalUsermediaStream,
} as unknown as MatrixCall);
await mediaHandler.updateLocalUsermediaStreams();
expect(updateLocalUsermediaStream).toHaveBeenCalled();
});
it("doesn't replace streams on ended calls", async () => {
calls.set("some_call", {
hasLocalUserMediaAudioTrack: true,
hasLocalUserMediaVideoTrack: true,
callHasEnded: jest.fn().mockReturnValue(true),
updateLocalUsermediaStream,
} as unknown as MatrixCall);
await mediaHandler.updateLocalUsermediaStreams();
expect(updateLocalUsermediaStream).not.toHaveBeenCalled();
});
it("replaces streams on group calls", async () => {
groupCalls.set("some_group_call", {
localCallFeed: {},
updateLocalUsermediaStream,
} as unknown as GroupCall);
await mediaHandler.updateLocalUsermediaStreams();
expect(updateLocalUsermediaStream).toHaveBeenCalled();
});
it("doesn't replace streams on group calls with no localCallFeed", async () => {
groupCalls.set("some_group_call", {
localCallFeed: null,
updateLocalUsermediaStream,
} as unknown as GroupCall);
await mediaHandler.updateLocalUsermediaStreams();
expect(updateLocalUsermediaStream).not.toHaveBeenCalled();
});
it("emits LocalStreamsChanged", async () => {
await mediaHandler.updateLocalUsermediaStreams();
expect(localStreamsChangedHandler).toHaveBeenCalled();
});
});
});
describe("hasAudioDevice", () => {
it("returns true if the system has audio inputs", async () => {
expect(await mediaHandler.hasAudioDevice()).toEqual(true);
});
it("returns false if the system has no audio inputs", async () => {
mockMediaDevices.enumerateDevices.mockReturnValue(Promise.resolve([
new MockMediaDeviceInfo("videoinput").typed(),
]));
expect(await mediaHandler.hasAudioDevice()).toEqual(false);
});
});
describe("hasVideoDevice", () => {
it("returns true if the system has video inputs", async () => {
expect(await mediaHandler.hasVideoDevice()).toEqual(true);
});
it("returns false if the system has no video inputs", async () => {
mockMediaDevices.enumerateDevices.mockReturnValue(Promise.resolve([
new MockMediaDeviceInfo("audioinput").typed(),
]));
expect(await mediaHandler.hasVideoDevice()).toEqual(false);
});
});
describe("getUserMediaStream", () => {
beforeEach(() => {
// replace this with one that returns a new object each time so we can
// tell whether we've ended up with the same stream
mockMediaDevices.getUserMedia.mockImplementation((constraints: MediaStreamConstraints) => {
const stream = new MockMediaStream("local_stream");
if (constraints.audio) {
const track = new MockMediaStreamTrack("audio_track", "audio");
track.settings = { deviceId: FAKE_AUDIO_INPUT_ID };
stream.addTrack(track);
}
if (constraints.video) {
const track = new MockMediaStreamTrack("video_track", "video");
track.settings = { deviceId: FAKE_VIDEO_INPUT_ID };
stream.addTrack(track);
}
return Promise.resolve(stream.typed());
});
mediaHandler.restoreMediaSettings(FAKE_AUDIO_INPUT_ID, FAKE_VIDEO_INPUT_ID);
});
it("returns the same stream for reusable streams", async () => {
const stream1 = await mediaHandler.getUserMediaStream(true, false);
const stream2 = await mediaHandler.getUserMediaStream(true, false) as unknown as MockMediaStream;
expect(stream2.isCloneOf(stream1)).toEqual(true);
});
it("doesn't re-use stream if reusable is false", async () => {
const stream1 = await mediaHandler.getUserMediaStream(true, false, false);
const stream2 = await mediaHandler.getUserMediaStream(true, false) as unknown as MockMediaStream;
expect(stream2.isCloneOf(stream1)).toEqual(false);
});
it("doesn't re-use stream if existing stream lacks audio", async () => {
const stream1 = await mediaHandler.getUserMediaStream(false, true);
const stream2 = await mediaHandler.getUserMediaStream(true, false) as unknown as MockMediaStream;
expect(stream2.isCloneOf(stream1)).toEqual(false);
});
it("doesn't re-use stream if existing stream lacks video", async () => {
const stream1 = await mediaHandler.getUserMediaStream(true, false);
const stream2 = await mediaHandler.getUserMediaStream(false, true) as unknown as MockMediaStream;
expect(stream2.isCloneOf(stream1)).toEqual(false);
});
it("strips unwanted audio tracks from re-used stream", async () => {
const stream1 = await mediaHandler.getUserMediaStream(true, true);
const stream2 = await mediaHandler.getUserMediaStream(false, true) as unknown as MockMediaStream;
expect(stream2.isCloneOf(stream1)).toEqual(true);
expect(stream2.getAudioTracks().length).toEqual(0);
});
it("strips unwanted video tracks from re-used stream", async () => {
const stream1 = await mediaHandler.getUserMediaStream(true, true);
const stream2 = await mediaHandler.getUserMediaStream(true, false) as unknown as MockMediaStream;
expect(stream2.isCloneOf(stream1)).toEqual(true);
expect(stream2.getVideoTracks().length).toEqual(0);
});
});
describe("getScreensharingStream", () => {
it("gets any screen sharing stream when called with no args", async () => {
const stream = await mediaHandler.getScreensharingStream();
expect(stream).toBeTruthy();
expect(stream.getTracks()).toBeTruthy();
});
it("re-uses streams", async () => {
const stream = await mediaHandler.getScreensharingStream(undefined, true);
expect(mockMediaDevices.getDisplayMedia).toHaveBeenCalled();
mockMediaDevices.getDisplayMedia.mockClear();
const stream2 = await mediaHandler.getScreensharingStream() as unknown as MockMediaStream;
expect(mockMediaDevices.getDisplayMedia).not.toHaveBeenCalled();
expect(stream2.isCloneOf(stream)).toEqual(true);
});
it("passes through desktopCapturerSourceId for Electron", async () => {
await mediaHandler.getScreensharingStream({
desktopCapturerSourceId: FAKE_DESKTOP_SOURCE_ID,
});
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(expect.objectContaining({
video: {
mandatory: expect.objectContaining({
chromeMediaSource: "desktop",
chromeMediaSourceId: FAKE_DESKTOP_SOURCE_ID,
}),
},
}));
});
it("emits LocalStreamsChanged", async () => {
const onLocalStreamChanged = jest.fn();
mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
await mediaHandler.getScreensharingStream();
expect(onLocalStreamChanged).toHaveBeenCalled();
mediaHandler.off(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
});
});
describe("stopUserMediaStream", () => {
let stream: MediaStream;
beforeEach(async () => {
stream = await mediaHandler.getUserMediaStream(true, false);
});
it("stops tracks on streams", async () => {
const mockTrack = new MockMediaStreamTrack("audio_track", "audio");
stream.addTrack(mockTrack.typed());
mediaHandler.stopUserMediaStream(stream);
expect(mockTrack.stop).toHaveBeenCalled();
});
it("removes stopped streams", async () => {
expect(mediaHandler.userMediaStreams).toContain(stream);
mediaHandler.stopUserMediaStream(stream);
expect(mediaHandler.userMediaStreams).not.toContain(stream);
});
it("emits LocalStreamsChanged", async () => {
const onLocalStreamChanged = jest.fn();
mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
mediaHandler.stopUserMediaStream(stream);
expect(onLocalStreamChanged).toHaveBeenCalled();
mediaHandler.off(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
});
});
describe("stopUserMediaStream", () => {
let stream: MediaStream;
beforeEach(async () => {
stream = await mediaHandler.getScreensharingStream();
});
it("stops tracks on streams", async () => {
const mockTrack = new MockMediaStreamTrack("audio_track", "audio");
stream.addTrack(mockTrack.typed());
mediaHandler.stopScreensharingStream(stream);
expect(mockTrack.stop).toHaveBeenCalled();
});
it("removes stopped streams", async () => {
expect(mediaHandler.screensharingStreams).toContain(stream);
mediaHandler.stopScreensharingStream(stream);
expect(mediaHandler.screensharingStreams).not.toContain(stream);
});
it("emits LocalStreamsChanged", async () => {
const onLocalStreamChanged = jest.fn();
mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
mediaHandler.stopScreensharingStream(stream);
expect(onLocalStreamChanged).toHaveBeenCalled();
mediaHandler.off(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
});
});
describe("stopAllStreams", () => {
let userMediaStream: MediaStream;
let screenSharingStream: MediaStream;
beforeEach(async () => {
userMediaStream = await mediaHandler.getUserMediaStream(true, false);
screenSharingStream = await mediaHandler.getScreensharingStream();
});
it("stops tracks on streams", async () => {
const mockUserMediaTrack = new MockMediaStreamTrack("audio_track", "audio");
userMediaStream.addTrack(mockUserMediaTrack.typed());
const mockScreenshareTrack = new MockMediaStreamTrack("audio_track", "audio");
screenSharingStream.addTrack(mockScreenshareTrack.typed());
mediaHandler.stopAllStreams();
expect(mockUserMediaTrack.stop).toHaveBeenCalled();
expect(mockScreenshareTrack.stop).toHaveBeenCalled();
});
it("removes stopped streams", async () => {
expect(mediaHandler.userMediaStreams).toContain(userMediaStream);
expect(mediaHandler.screensharingStreams).toContain(screenSharingStream);
mediaHandler.stopAllStreams();
expect(mediaHandler.userMediaStreams).not.toContain(userMediaStream);
expect(mediaHandler.screensharingStreams).not.toContain(screenSharingStream);
});
it("emits LocalStreamsChanged", async () => {
const onLocalStreamChanged = jest.fn();
mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
mediaHandler.stopAllStreams();
expect(onLocalStreamChanged).toHaveBeenCalled();
mediaHandler.off(MediaHandlerEvent.LocalStreamsChanged, onLocalStreamChanged);
});
});
});
+5 -4
View File
@@ -30,7 +30,7 @@ export enum TweakName {
export type Tweak<N extends TweakName, V> = {
set_tweak: N;
value: V;
value?: V;
};
export type TweakHighlight = Tweak<TweakName.Highlight, boolean>;
@@ -76,7 +76,8 @@ export interface IPushRuleCondition<N extends ConditionKind | string> {
export interface IEventMatchCondition extends IPushRuleCondition<ConditionKind.EventMatch> {
key: string;
pattern: string;
pattern?: string;
value?: string;
}
export interface IContainsDisplayNameCondition extends IPushRuleCondition<ConditionKind.ContainsDisplayName> {
@@ -168,8 +169,8 @@ export interface IPusher {
lang: string;
profile_tag?: string;
pushkey: string;
enabled?: boolean | null | undefined;
"org.matrix.msc3881.enabled"?: boolean | null | undefined;
enabled?: boolean | null;
"org.matrix.msc3881.enabled"?: boolean | null;
device_id?: string | null;
"org.matrix.msc3881.device_id"?: string | null;
}
+4
View File
@@ -87,6 +87,10 @@ export enum EventType {
RoomKeyRequest = "m.room_key_request",
ForwardedRoomKey = "m.forwarded_room_key",
Dummy = "m.dummy",
// Group call events
GroupCallPrefix = "org.matrix.msc3401.call",
GroupCallMemberPrefix = "org.matrix.msc3401.call.member",
}
export enum RelationType {
+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)
};
};
+1
View File
@@ -114,5 +114,6 @@ export interface ISearchResults {
count?: number;
next_batch?: string;
pendingRequest?: Promise<ISearchResults>;
abortSignal?: AbortSignal;
}
/* eslint-enable camelcase */
+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 {
+48 -32
View File
@@ -32,6 +32,28 @@ export enum AutoDiscoveryAction {
FAIL_ERROR = "FAIL_ERROR",
}
enum AutoDiscoveryError {
Invalid = "Invalid homeserver discovery response",
GenericFailure = "Failed to get autodiscovery configuration from server",
InvalidHsBaseUrl = "Invalid base_url for m.homeserver",
InvalidHomeserver = "Homeserver URL does not appear to be a valid Matrix homeserver",
InvalidIsBaseUrl = "Invalid base_url for m.identity_server",
InvalidIdentityServer = "Identity server URL does not appear to be a valid identity server",
InvalidIs = "Invalid identity server discovery response",
MissingWellknown = "No .well-known JSON file found",
InvalidJson = "Invalid JSON",
}
interface WellKnownConfig extends Omit<IWellKnownConfig, "error"> {
state: AutoDiscoveryAction;
error?: IWellKnownConfig["error"] | null;
}
interface ClientConfig {
"m.homeserver": WellKnownConfig;
"m.identity_server": WellKnownConfig;
}
/**
* Utilities for automatically discovery resources, such as homeservers
* for users to log in to.
@@ -42,36 +64,25 @@ export class AutoDiscovery {
// translate the meaning of the states in the spec, but also
// support our own if needed.
public static readonly ERROR_INVALID = "Invalid homeserver discovery response";
public static readonly ERROR_INVALID = AutoDiscoveryError.Invalid;
public static readonly ERROR_GENERIC_FAILURE = "Failed to get autodiscovery configuration from server";
public static readonly ERROR_GENERIC_FAILURE = AutoDiscoveryError.GenericFailure;
public static readonly ERROR_INVALID_HS_BASE_URL = "Invalid base_url for m.homeserver";
public static readonly ERROR_INVALID_HS_BASE_URL = AutoDiscoveryError.InvalidHsBaseUrl;
public static readonly ERROR_INVALID_HOMESERVER = "Homeserver URL does not appear to be a valid Matrix homeserver";
public static readonly ERROR_INVALID_HOMESERVER = AutoDiscoveryError.InvalidHomeserver;
public static readonly ERROR_INVALID_IS_BASE_URL = "Invalid base_url for m.identity_server";
public static readonly ERROR_INVALID_IS_BASE_URL = AutoDiscoveryError.InvalidIsBaseUrl;
// eslint-disable-next-line
public static readonly ERROR_INVALID_IDENTITY_SERVER = "Identity server URL does not appear to be a valid identity server";
public static readonly ERROR_INVALID_IDENTITY_SERVER = AutoDiscoveryError.InvalidIdentityServer;
public static readonly ERROR_INVALID_IS = "Invalid identity server discovery response";
public static readonly ERROR_INVALID_IS = AutoDiscoveryError.InvalidIs;
public static readonly ERROR_MISSING_WELLKNOWN = "No .well-known JSON file found";
public static readonly ERROR_MISSING_WELLKNOWN = AutoDiscoveryError.MissingWellknown;
public static readonly ERROR_INVALID_JSON = "Invalid JSON";
public static readonly ERROR_INVALID_JSON = AutoDiscoveryError.InvalidJson;
public static readonly ALL_ERRORS = [
AutoDiscovery.ERROR_INVALID,
AutoDiscovery.ERROR_GENERIC_FAILURE,
AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
AutoDiscovery.ERROR_INVALID_HOMESERVER,
AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
AutoDiscovery.ERROR_INVALID_IS,
AutoDiscovery.ERROR_MISSING_WELLKNOWN,
AutoDiscovery.ERROR_INVALID_JSON,
];
public static readonly ALL_ERRORS = Object.keys(AutoDiscoveryError);
/**
* The auto discovery failed. The client is expected to communicate
@@ -120,13 +131,13 @@ export class AutoDiscovery {
* configuration, which may include error states. Rejects on unexpected
* failure, not when verification fails.
*/
public static async fromDiscoveryConfig(wellknown: any): Promise<IClientWellKnown> {
public static async fromDiscoveryConfig(wellknown: any): Promise<ClientConfig> {
// Step 1 is to get the config, which is provided to us here.
// We default to an error state to make the first few checks easier to
// write. We'll update the properties of this object over the duration
// of this function.
const clientConfig = {
const clientConfig: ClientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: AutoDiscovery.ERROR_INVALID,
@@ -197,7 +208,7 @@ export class AutoDiscovery {
if (wellknown["m.identity_server"]) {
// We prepare a failing identity server response to save lines later
// in this branch.
const failingClientConfig = {
const failingClientConfig: ClientConfig = {
"m.homeserver": clientConfig["m.homeserver"],
"m.identity_server": {
state: AutoDiscovery.FAIL_PROMPT,
@@ -279,7 +290,7 @@ export class AutoDiscovery {
* configuration, which may include error states. Rejects on unexpected
* failure, not when discovery fails.
*/
public static async findClientConfig(domain: string): Promise<IClientWellKnown> {
public static async findClientConfig(domain: string): Promise<ClientConfig> {
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length");
}
@@ -298,7 +309,7 @@ export class AutoDiscovery {
// We default to an error state to make the first few checks easier to
// write. We'll update the properties of this object over the duration
// of this function.
const clientConfig = {
const clientConfig: ClientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: AutoDiscovery.ERROR_INVALID,
@@ -367,18 +378,18 @@ export class AutoDiscovery {
* @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
* @private
*/
private static sanitizeWellKnownUrl(url: string): string | boolean {
private static sanitizeWellKnownUrl(url: string): string | false {
if (!url) return false;
try {
let parsed = null;
let parsed: URL | undefined;
try {
parsed = new URL(url);
} catch (e) {
logger.error("Could not parse url", e);
}
if (!parsed || !parsed.hostname) return false;
if (!parsed?.hostname) return false;
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
const port = parsed.port ? `:${parsed.port}` : "";
@@ -448,12 +459,17 @@ export class AutoDiscovery {
};
}
} catch (err) {
const error = err as Error | string | undefined;
const error = err as AutoDiscoveryError | string | undefined;
let reason = "";
if (typeof error === "object") {
reason = (<Error>error)?.message;
}
return {
error,
raw: {},
action: AutoDiscoveryAction.FAIL_PROMPT,
reason: (<Error>error)?.message || "General failure",
reason: reason || "General failure",
};
}
@@ -463,7 +479,7 @@ export class AutoDiscovery {
action: AutoDiscoveryAction.SUCCESS,
};
} catch (err) {
const error = err as Error | string | undefined;
const error = err as Error;
return {
error,
raw: {},
+237 -132
View File
@@ -35,6 +35,7 @@ import { StubStore } from "./store/stub";
import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, supportsMatrixCall } from "./webrtc/call";
import { Filter, IFilterDefinition, IRoomEventFilter } from "./filter";
import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler';
import { GroupCallEventHandlerEvent, GroupCallEventHandlerEventHandlerMap } from './webrtc/groupCallEventHandler';
import * as utils from './utils';
import { replaceParam, QueryDict, sleep } from './utils';
import { Direction, EventTimeline } from "./models/event-timeline";
@@ -64,12 +65,14 @@ import {
FileType,
UploadResponse,
HTTPError,
IRequestOpts,
} from "./http-api";
import {
Crypto,
CryptoEvent,
CryptoEventHandlerMap,
fixBackupKey,
ICryptoCallbacks,
IBootstrapCrossSigningOpts,
ICheckOwnCrossSigningTrustOpts,
IMegolmSessionData,
@@ -99,29 +102,9 @@ import {
} from "./crypto/keybackup";
import { IIdentityServerProvider } from "./@types/IIdentityServerProvider";
import { MatrixScheduler } from "./scheduler";
import {
IAuthData,
ICryptoCallbacks,
IMinimalEvent,
IRoomEvent,
IStateEvent,
NotificationCountType,
BeaconEvent,
BeaconEventHandlerMap,
RoomEvent,
RoomEventHandlerMap,
RoomMemberEvent,
RoomMemberEventHandlerMap,
RoomStateEvent,
RoomStateEventHandlerMap,
INotificationsResponse,
IFilterResponse,
ITagsResponse,
IStatusResponse,
IPushRule,
PushRuleActionName,
IAuthDict,
} from "./matrix";
import { BeaconEvent, BeaconEventHandlerMap } from "./models/beacon";
import { IAuthData, IAuthDict } from "./interactive-auth";
import { IMinimalEvent, IRoomEvent, IStateEvent } from "./sync-accumulator";
import {
CrossSigningKey,
IAddSecretStorageKeyOpts,
@@ -136,7 +119,9 @@ import { VerificationRequest } from "./crypto/verification/request/VerificationR
import { VerificationBase as Verification } from "./crypto/verification/Base";
import * as ContentHelpers from "./content-helpers";
import { CrossSigningInfo, DeviceTrustLevel, ICacheCallbacks, UserTrustLevel } from "./crypto/CrossSigning";
import { Room, RoomNameState } from "./models/room";
import { Room, NotificationCountType, RoomEvent, RoomEventHandlerMap, RoomNameState } from "./models/room";
import { RoomMemberEvent, RoomMemberEventHandlerMap } from "./models/room-member";
import { RoomStateEvent, RoomStateEventHandlerMap } from "./models/room-state";
import {
IAddThreePidOnlyBody,
IBindThreePidBody,
@@ -153,6 +138,10 @@ import {
IRoomDirectoryOptions,
ISearchOpts,
ISendEventResponse,
INotificationsResponse,
IFilterResponse,
ITagsResponse,
IStatusResponse,
} from "./@types/requests";
import {
EventType,
@@ -184,13 +173,29 @@ import {
} from "./@types/search";
import { ISynapseAdminDeactivateResponse, ISynapseAdminWhoisResponse } from "./@types/synapse";
import { IHierarchyRoom } from "./@types/spaces";
import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, RuleId } from "./@types/PushRules";
import {
IPusher,
IPusherRequest,
IPushRule,
IPushRules,
PushRuleAction,
PushRuleActionName,
PushRuleKind,
RuleId,
} from "./@types/PushRules";
import { IThreepid } from "./@types/threepids";
import { CryptoStore } from "./crypto/store/base";
import {
GroupCall,
IGroupCallDataChannelOptions,
GroupCallIntent,
GroupCallType,
} from "./webrtc/groupCall";
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 {
@@ -205,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";
@@ -359,6 +363,12 @@ export interface ICreateClientOpts {
*/
fallbackICEServerAllowed?: boolean;
/**
* If true, to-device signalling for group calls will be encrypted
* with Olm. Default: true.
*/
useE2eForGroupCall?: boolean;
cryptoCallbacks?: ICryptoCallbacks;
/**
@@ -507,7 +517,7 @@ export interface IUploadKeySignaturesResponse {
}
export interface IPreviewUrlResponse {
[key: string]: string | number;
[key: string]: undefined | string | number;
"og:title": string;
"og:type": string;
"og:url": string;
@@ -705,8 +715,9 @@ export interface IMyDevice {
display_name?: string;
last_seen_ip?: string;
last_seen_ts?: number;
[UNSTABLE_MSC3852_LAST_SEEN_UA.stable]?: string;
[UNSTABLE_MSC3852_LAST_SEEN_UA.unstable]?: string;
// UNSTABLE_MSC3852_LAST_SEEN_UA
last_seen_user_agent?: string;
"org.matrix.msc3852.last_seen_user_agent"?: string;
}
export interface Keys {
@@ -824,6 +835,7 @@ export enum ClientEvent {
DeleteRoom = "deleteRoom",
SyncUnexpectedError = "sync.unexpectedError",
ClientWellKnown = "WellKnown.client",
ReceivedVoipEvent = "received_voip_event",
TurnServers = "turnServers",
TurnServersError = "turnServers.error",
}
@@ -883,6 +895,10 @@ export type EmittedEvents = ClientEvent
| UserEvents
| CallEvent // re-emitted by call.ts using Object.values
| CallEventHandlerEvent.Incoming
| GroupCallEventHandlerEvent.Incoming
| GroupCallEventHandlerEvent.Outgoing
| GroupCallEventHandlerEvent.Ended
| GroupCallEventHandlerEvent.Participants
| HttpApiEvent.SessionLoggedOut
| HttpApiEvent.NoConsent
| BeaconEvent;
@@ -896,6 +912,7 @@ export type ClientEventHandlerMap = {
[ClientEvent.DeleteRoom]: (roomId: string) => void;
[ClientEvent.SyncUnexpectedError]: (error: Error) => void;
[ClientEvent.ClientWellKnown]: (data: IClientWellKnown) => void;
[ClientEvent.ReceivedVoipEvent]: (event: MatrixEvent) => void;
[ClientEvent.TurnServers]: (servers: ITurnServer[]) => void;
[ClientEvent.TurnServersError]: (error: Error, fatal: boolean) => void;
} & RoomEventHandlerMap
@@ -905,6 +922,7 @@ export type ClientEventHandlerMap = {
& RoomMemberEventHandlerMap
& UserEventHandlerMap
& CallEventHandlerEventHandlerMap
& GroupCallEventHandlerEventHandlerMap
& CallEventHandlerMap
& HttpApiEventHandlerMap
& BeaconEventHandlerMap;
@@ -935,6 +953,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public crypto?: Crypto; // XXX: Intended private, used in code.
public cryptoCallbacks: ICryptoCallbacks; // XXX: Intended private, used in code.
public callEventHandler?: CallEventHandler; // XXX: Intended private, used in code.
public groupCallEventHandler?: GroupCallEventHandler;
public supportsCallTransfer = false; // XXX: Intended private, used in code.
public forceTURN = false; // XXX: Intended private, used in code.
public iceCandidatePoolSize = 0; // XXX: Intended private, used in code.
@@ -983,14 +1002,16 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
protected exportedOlmDeviceToImport?: IExportedOlmDevice;
protected txnCtr = 0;
protected mediaHandler = new MediaHandler(this);
protected sessionId: string;
protected pendingEventEncryption = new Map<string, Promise<void>>();
private useE2eForGroupCall = true;
private toDeviceMessageQueue: ToDeviceMessageQueue;
// 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);
@@ -1003,6 +1024,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.usingExternalCrypto = opts.usingExternalCrypto ?? false;
this.store = opts.store || new StubStore();
this.deviceId = opts.deviceId || null;
this.sessionId = randomString(10);
const userId = opts.userId || null;
this.credentials = { userId };
@@ -1061,6 +1083,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (supportsMatrixCall()) {
this.callEventHandler = new CallEventHandler(this);
this.groupCallEventHandler = new GroupCallEventHandler(this);
this.canSupportVoip = true;
// Start listening for calls after the initial sync is done
// We do not need to backfill the call event buffer
@@ -1079,6 +1102,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.supportsCallTransfer = opts.supportsCallTransfer || false;
this.fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false;
if (opts.useE2eForGroupCall !== undefined) this.useE2eForGroupCall = opts.useE2eForGroupCall;
// List of which rooms have encryption enabled: separate from crypto because
// we still want to know which rooms are encrypted even if crypto is disabled:
// we don't want to start sending unencrypted events to them.
@@ -1175,7 +1200,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (this.crypto) {
this.crypto.uploadDeviceKeys();
this.crypto.start();
}
// periodically poll for turn servers if we support voip
@@ -1209,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;
}
@@ -1236,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
@@ -1251,7 +1275,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.peekSync?.stopPeeking();
this.callEventHandler?.stop();
this.groupCallEventHandler?.stop();
this.callEventHandler = undefined;
this.groupCallEventHandler = undefined;
global.clearInterval(this.checkTurnServersIntervalID);
this.checkTurnServersIntervalID = undefined;
@@ -1482,6 +1508,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.deviceId;
}
/**
* Get the session ID of this client
* @return {string} session ID
*/
public getSessionId(): string {
return this.sessionId;
}
/**
* Check if the runtime environment supports VoIP calling.
* @return {boolean} True if VoIP is supported.
@@ -1503,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;
}
@@ -1511,10 +1545,19 @@ 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;
}
/**
* Returns true if to-device signalling for group calls will be encrypted with Olm.
* If false, it will be sent unencrypted.
* @returns boolean Whether group call signalling will be encrypted
*/
public getUseE2eForGroupCall(): boolean {
return this.useE2eForGroupCall;
}
/**
* Creates a new call.
* The place*Call methods on the returned call can be used to actually place a call
@@ -1526,6 +1569,67 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return createNewMatrixCall(this, roomId);
}
/**
* Creates a new group call and sends the associated state event
* to alert other members that the room now has a group call.
*
* @param {string} roomId The room the call is to be placed in.
* @return {GroupCall}
*/
public async createGroupCall(
roomId: string,
type: GroupCallType,
isPtt: boolean,
intent: GroupCallIntent,
dataChannelsEnabled?: boolean,
dataChannelOptions?: IGroupCallDataChannelOptions,
): Promise<GroupCall> {
if (this.getGroupCallForRoom(roomId)) {
throw new Error(`${roomId} already has an existing group call`);
}
const room = this.getRoom(roomId);
if (!room) {
throw new Error(`Cannot find room ${roomId}`);
}
return new GroupCall(
this,
room,
type,
isPtt,
intent,
undefined,
dataChannelsEnabled,
dataChannelOptions,
).create();
}
/**
* Wait until an initial state for the given room has been processed by the
* client and the client is aware of any ongoing group calls. Awaiting on
* the promise returned by this method before calling getGroupCallForRoom()
* avoids races where getGroupCallForRoom is called before the state for that
* room has been processed. It does not, however, fix other races, eg. two
* clients both creating a group call at the same time.
* @param roomId The room ID to wait for
* @returns A promise that resolves once existing group calls in the room
* have been processed.
*/
public waitUntilRoomReadyForGroupCalls(roomId: string): Promise<void> {
return this.groupCallEventHandler!.waitUntilRoomReadyForGroupCalls(roomId);
}
/**
* Get an existing group call for the provided room.
* @param roomId
* @returns {GroupCall} The group call or null if it doesn't already exist.
*/
public getGroupCallForRoom(roomId: string): GroupCall | null {
return this.groupCallEventHandler!.groupCalls.get(roomId) || null;
}
/**
* Get the current sync state.
* @return {?SyncState} the sync state, which may be null.
@@ -1575,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
@@ -1618,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;
}
@@ -2003,11 +2107,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @param {boolean} value whether to blacklist all unverified devices by default
*/
public setGlobalBlacklistUnverifiedDevices(value: boolean) {
public setGlobalBlacklistUnverifiedDevices(value: boolean): boolean {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.setGlobalBlacklistUnverifiedDevices(value);
this.crypto.globalBlacklistUnverifiedDevices = value;
return value;
}
/**
@@ -2017,7 +2122,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.getGlobalBlacklistUnverifiedDevices();
return this.crypto.globalBlacklistUnverifiedDevices;
}
/**
@@ -2030,11 +2135,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @param {boolean} value whether error on unknown devices
*/
public setGlobalErrorOnUnknownDevices(value: boolean) {
public setGlobalErrorOnUnknownDevices(value: boolean): void {
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.setGlobalErrorOnUnknownDevices(value);
this.crypto.globalErrorOnUnknownDevices = value;
}
/**
@@ -2046,7 +2151,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (!this.crypto) {
throw new Error("End-to-end encryption disabled");
}
return this.crypto.getGlobalErrorOnUnknownDevices();
return this.crypto.globalErrorOnUnknownDevices;
}
/**
@@ -2175,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);
}
/**
@@ -2220,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");
}
@@ -2248,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);
}
/**
@@ -2393,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");
}
@@ -2471,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");
}
@@ -2612,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");
}
@@ -2736,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");
}
@@ -2929,7 +3034,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
throw new Error("End-to-end encryption disabled");
}
const path = this.makeKeyBackupPath(roomId!, sessionId!, version!);
const path = this.makeKeyBackupPath(roomId!, sessionId!, version);
await this.http.authedRequest(
Method.Put, path.path, path.queryData, data,
{ prefix: ClientPrefix.V3 },
@@ -2940,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");
}
@@ -3121,7 +3226,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
opts: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult> {
const privKey = decodeRecoveryKey(recoveryKey);
return this.restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts);
return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts);
}
public async restoreKeyBackupWithCache(
@@ -3283,7 +3388,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
throw new Error("End-to-end encryption disabled");
}
const path = this.makeKeyBackupPath(roomId, sessionId, version);
const path = this.makeKeyBackupPath(roomId!, sessionId!, version);
await this.http.authedRequest(
Method.Delete, path.path, path.queryData, undefined,
{ prefix: ClientPrefix.V3 },
@@ -3297,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");
}
@@ -3525,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[]> = {};
@@ -3581,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);
}
@@ -3730,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);
}
@@ -3745,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()!);
}
@@ -3775,7 +3879,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
roomId: string,
threadId: string | null,
eventType: string | IContent,
content: IContent | string,
content?: IContent | string,
txnId?: string,
): Promise<ISendEventResponse> {
if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) {
@@ -3787,10 +3891,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// If we expect that an event is part of a thread but is missing the relation
// we need to add it manually, as well as the reply fallback
if (threadId && !content["m.relates_to"]?.rel_type) {
const isReply = !!content["m.relates_to"]?.["m.in_reply_to"];
content["m.relates_to"] = {
...content["m.relates_to"],
if (threadId && !content!["m.relates_to"]?.rel_type) {
const isReply = !!content!["m.relates_to"]?.["m.in_reply_to"];
content!["m.relates_to"] = {
...content!["m.relates_to"],
"rel_type": THREAD_RELATION_TYPE.name,
"event_id": threadId,
// Set is_falling_back to true unless this is actually intended to be a reply
@@ -3798,7 +3902,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
};
const thread = this.getRoom(roomId)?.getThread(threadId);
if (thread && !isReply) {
content["m.relates_to"]["m.in_reply_to"] = {
content!["m.relates_to"]["m.in_reply_to"] = {
"event_id": thread.lastReply((ev: MatrixEvent) => {
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
})?.getId() ?? threadId,
@@ -3888,9 +3992,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param room
* @param event
* @returns {Promise} returns a promise which resolves with the result of the send request
* @private
*/
private encryptAndSendEvent(room: Room | null, event: MatrixEvent): Promise<ISendEventResponse> {
protected encryptAndSendEvent(room: Room | null, event: MatrixEvent): Promise<ISendEventResponse> {
let cancelled = false;
// Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections,
// so that we can handle synchronous and asynchronous exceptions with the
@@ -4018,7 +4121,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.isRoomEncrypted(roomId) ? EventType.RoomMessageEncrypted : eventType;
}
private 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 {
@@ -4097,7 +4200,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (!eventId?.startsWith(EVENT_ID_PREFIX)) {
opts = txnId as IRedactOpts;
txnId = eventId;
eventId = threadId;
eventId = threadId!;
threadId = null;
}
const reason = opts?.reason;
@@ -4130,7 +4233,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public sendMessage(
roomId: string,
threadId: string | null | IContent,
content: IContent | string,
content?: IContent | string,
txnId?: string,
): Promise<ISendEventResponse> {
if (typeof threadId !== "string" && threadId !== null) {
@@ -4180,7 +4283,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.sendEvent(
roomId,
threadId as (string | null),
threadId as string | null,
eventType,
sendContent,
txnId,
@@ -4314,7 +4417,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public sendImageMessage(
roomId: string,
threadId: string | null,
url: string | IImageInfo,
url?: string | IImageInfo,
info?: IImageInfo | string,
text = "Image",
): Promise<ISendEventResponse> {
@@ -4358,7 +4461,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public sendStickerMessage(
roomId: string,
threadId: string | null,
url: string | IImageInfo,
url?: string | IImageInfo,
info?: IImageInfo | string,
text = "Sticker",
): Promise<ISendEventResponse> {
@@ -4484,6 +4587,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {ReceiptType} receiptType The kind of receipt e.g. "m.read". Other than
* ReceiptType.Read are experimental!
* @param {object} body Additional content to send alongside the receipt.
* @param {boolean} unthreaded An unthreaded receipt will clear room+thread notifications
* @return {Promise} Resolves: to an empty object {}
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
@@ -4491,6 +4595,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
event: MatrixEvent,
receiptType: ReceiptType,
body: any,
unthreaded = false,
): Promise<{}> {
if (this.isGuest()) {
return Promise.resolve({}); // guests cannot send receipts so don't bother.
@@ -4502,12 +4607,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$eventId: event.getId()!,
});
// TODO: Add a check for which spec version this will be released in
if (await this.doesServerSupportUnstableFeature("org.matrix.msc3771")) {
const supportsThreadRR = this.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported;
if (supportsThreadRR && !unthreaded) {
const isThread = !!event.threadRootId;
body.thread_id = isThread
? event.threadRootId
: MAIN_ROOM_TIMELINE;
body = {
...body,
thread_id: isThread
? event.threadRootId
: MAIN_ROOM_TIMELINE,
};
}
const promise = this.http.authedRequest<{}>(Method.Post, path, undefined, body || {});
@@ -4529,6 +4637,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public async sendReadReceipt(
event: MatrixEvent | null,
receiptType = ReceiptType.Read,
unthreaded = false,
): Promise<{} | undefined> {
if (!event) return;
const eventId = event.getId()!;
@@ -4537,7 +4646,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
throw new Error(`Cannot set read receipt to a pending event (${eventId})`);
}
return this.sendReceipt(event, receiptType, {});
return this.sendReceipt(event, receiptType, {}, unthreaded);
}
/**
@@ -4835,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
});
};
@@ -5224,13 +5333,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
];
// Here we handle non-thread timelines only, but still process any thread events to populate thread summaries.
let timeline = timelineSet.getTimelineForEvent(events[0].getId()!);
let timeline = timelineSet.getTimelineForEvent(events[0].getId());
if (timeline) {
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(res.state.map(mapper));
timeline.getState(EventTimeline.BACKWARDS)!.setUnknownStateEvents(res.state.map(mapper));
} else {
timeline = timelineSet.addTimeline();
timeline.initialiseState(res.state.map(mapper));
timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
timeline.getState(EventTimeline.FORWARDS)!.paginationToken = res.end;
}
const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(events);
@@ -5319,7 +5428,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// Here we handle non-thread timelines only, but still process any thread events to populate thread summaries.
let timeline = timelineSet.getTimelineForEvent(event.getId());
if (timeline) {
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(res.state.map(mapper));
timeline.getState(EventTimeline.BACKWARDS)!.setUnknownStateEvents(res.state.map(mapper));
} else {
timeline = timelineSet.addTimeline();
timeline.initialiseState(res.state.map(mapper));
@@ -5380,7 +5489,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// Here we handle non-thread timelines only, but still process any thread events to populate thread
// summaries.
const timeline = timelineSet.getLiveTimeline();
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(res.state.map(mapper));
timeline.getState(EventTimeline.BACKWARDS)!.setUnknownStateEvents(res.state.map(mapper));
timelineSet.addEventsToTimeline(events, true, timeline, null);
if (!resOlder.next_batch) {
@@ -5683,7 +5792,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
eventTimeline.getFilter(),
).then((res) => {
if (res.state) {
const roomState = eventTimeline.getState(dir);
const roomState = eventTimeline.getState(dir)!;
const stateEvents = res.state.map(this.getEventMapper());
roomState.setUnknownStateEvents(stateEvents);
}
@@ -5721,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;
@@ -5758,7 +5873,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
eventTimeline.getFilter(),
).then((res) => {
if (res.state) {
const roomState = eventTimeline.getState(dir);
const roomState = eventTimeline.getState(dir)!;
const stateEvents = res.state.map(this.getEventMapper());
roomState.setUnknownStateEvents(stateEvents);
}
@@ -5795,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;
}
@@ -5840,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;
@@ -6114,15 +6229,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// There can be only room-kind push rule per room
// and its id is the room id.
if (this.pushRules) {
if (!this.pushRules[scope] || !this.pushRules[scope].room) {
return;
}
for (let i = 0; i < this.pushRules[scope].room.length; i++) {
const rule = this.pushRules[scope].room[i];
if (rule.rule_id === roomId) {
return rule;
}
}
return this.pushRules[scope]?.room?.find(rule => rule.rule_id === roomId);
} else {
throw new Error(
"SyncApi.sync() must be done before accessing to push rules.",
@@ -6293,7 +6400,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
next_batch: searchResults.next_batch,
};
const promise = this.search(searchOpts)
const promise = this.search(searchOpts, searchResults.abortSignal)
.then(res => this.processRoomEventsSearch(searchResults, res))
.finally(() => {
searchResults.pendingRequest = undefined;
@@ -6472,8 +6579,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// create a new filter
const createdFilter = await this.createFilter(filter.getDefinition());
// debuglog("Created new filter ID %s: %s", createdFilter.filterId,
// JSON.stringify(createdFilter.getDefinition()));
this.store.setFilterIdByName(filterName, createdFilter.filterId);
return createdFilter.filterId!;
}
@@ -6495,7 +6600,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
private startCallEventHandler = (): void => {
if (this.isInitialSyncComplete()) {
this.callEventHandler?.start();
this.callEventHandler!.start();
this.groupCallEventHandler!.start();
this.off(ClientEvent.Sync, this.startCallEventHandler);
}
};
@@ -6583,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;
}
@@ -6924,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;
}
@@ -7064,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);
}
@@ -7081,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;
}
@@ -7640,6 +7746,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {string} eventType
* @param {Object} content
* @param {string} stateKey
* @param {IRequestOpts} opts Options for the request function.
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
@@ -7648,6 +7755,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
eventType: string,
content: any,
stateKey = "",
opts: IRequestOpts = {},
): Promise<ISendEventResponse> {
const pathParams = {
$roomId: roomId,
@@ -7658,7 +7766,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (stateKey !== undefined) {
path = utils.encodeUri(path + "/$stateKey", pathParams);
}
return this.http.authedRequest(Method.Put, path, undefined, content);
return this.http.authedRequest(Method.Put, path, undefined, content, opts);
}
/**
@@ -8342,17 +8450,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {Object} opts
* @param {string} opts.next_batch the batch token to pass in the query string
* @param {Object} opts.body the JSON object to pass to the request body.
* @param {AbortSignal=} abortSignal optional signal used to cancel the http request.
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public search(
opts: { body: ISearchRequestBody, next_batch?: string }, // eslint-disable-line camelcase
abortSignal?: AbortSignal,
): Promise<ISearchResponse> {
const queryParams: any = {};
if (opts.next_batch) {
queryParams.next_batch = opts.next_batch;
}
return this.http.authedRequest(Method.Post, "/search", queryParams, opts.body);
return this.http.authedRequest(Method.Post, "/search", queryParams, opts.body, { abortSignal });
}
/**
@@ -8433,9 +8543,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
keyAlgorithm = "signed_curve25519";
}
for (let i = 0; i < devices.length; ++i) {
const userId = devices[i][0];
const deviceId = devices[i][1];
for (const [userId, deviceId] of devices) {
const query = queries[userId] || {};
queries[userId] = query;
query[deviceId] = keyAlgorithm;
@@ -8654,11 +8762,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
clientSecret: string,
msisdnToken: string,
): Promise<any> { // TODO: Types
const u = new URL(url);
u.searchParams.set("sid", sid);
u.searchParams.set("client_secret", clientSecret);
u.searchParams.set("token", msisdnToken);
return this.http.requestOtherUrl(Method.Post, u);
const params = {
sid: sid,
client_secret: clientSecret,
token: msisdnToken,
};
return this.http.requestOtherUrl(Method.Post, url, params);
}
/**
@@ -9235,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
@@ -9272,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) {
+21 -20
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,
@@ -118,12 +119,12 @@ export function makeEmoteMessage(body: string) {
/** Location content helpers */
export const getTextForLocationEvent = (
uri: string,
uri: string | undefined,
assetType: LocationAssetType,
timestamp: number,
description?: string,
timestamp?: number,
description?: string | null,
): string => {
const date = `at ${new Date(timestamp).toISOString()}`;
const date = `at ${new Date(timestamp!).toISOString()}`;
const assetName = assetType === LocationAssetType.Self ? 'User' : undefined;
const quotedDescription = description ? `"${description}"` : undefined;
@@ -147,10 +148,10 @@ export const getTextForLocationEvent = (
export const makeLocationContent = (
// this is first but optional
// to avoid a breaking change
text: string | undefined,
uri: string,
timestamp: number,
description?: string,
text?: string,
uri?: string,
timestamp?: number,
description?: string | null,
assetType?: LocationAssetType,
): LegacyLocationEventContent & MLocationEventContent => {
const defaultedText = text ??
@@ -187,7 +188,7 @@ export const parseLocationEvent = (wireEventContent: LocationEventWireContent):
const assetType = asset?.type ?? LocationAssetType.Self;
const fallbackText = text ?? wireEventContent.body;
return makeLocationContent(fallbackText, geoUri, timestamp, description, assetType);
return makeLocationContent(fallbackText, geoUri, timestamp ?? undefined, description, assetType);
};
/**
@@ -201,7 +202,7 @@ export type MakeTopicContent = (
export const makeTopicContent: MakeTopicContent = (topic, htmlTopic) => {
const renderings = [{ body: topic, mimetype: "text/plain" }];
if (isProvided(htmlTopic)) {
renderings.push({ body: htmlTopic, mimetype: "text/html" });
renderings.push({ body: htmlTopic!, mimetype: "text/html" });
}
return { topic, [M_TOPIC.name]: renderings };
};
@@ -247,14 +248,14 @@ export const makeBeaconInfoContent: MakeBeaconInfoContent = (
export type BeaconInfoState = MBeaconInfoContent & {
assetType?: LocationAssetType;
timestamp: number;
timestamp?: number;
};
/**
* Flatten beacon info event content
*/
export const parseBeaconInfoContent = (content: MBeaconInfoEventContent): BeaconInfoState => {
const { description, timeout, live } = content;
const timestamp = M_TIMESTAMP.findIn<number>(content);
const timestamp = M_TIMESTAMP.findIn<number>(content) ?? undefined;
const asset = M_ASSET.findIn<MAssetContent>(content);
return {
@@ -290,14 +291,14 @@ export const makeBeaconContent: MakeBeaconContent = (
},
});
export type BeaconLocationState = MLocationContent & {
export type BeaconLocationState = Omit<MLocationContent, "uri"> & {
uri?: string; // override from MLocationContent to allow optionals
timestamp?: number;
};
export const parseBeaconContent = (content: MBeaconEventContent): BeaconLocationState => {
const location = M_LOCATION.findIn<MLocationContent>(content);
const timestamp = M_TIMESTAMP.findIn<number>(content);
const timestamp = M_TIMESTAMP.findIn<number>(content) ?? undefined;
return {
description: location?.description,
+16 -9
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';
@@ -29,7 +29,7 @@ import { DeviceInfo } from "./deviceinfo";
import { SecretStorage } from "./SecretStorage";
import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
import { OlmDevice } from "./OlmDevice";
import { ICryptoCallbacks } from "../matrix";
import { ICryptoCallbacks } from ".";
import { ISignatures } from "../@types/signed";
import { CryptoStore, SecretStorePrivateKeys } from "./store/base";
import { ISecretStorageKeyInfo } from "./api";
@@ -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...");
+5 -5
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);
}
@@ -230,7 +230,7 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
}, delay);
}
return savePromise!;
return savePromise;
}
/**
@@ -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,
+8 -9
View File
@@ -19,16 +19,15 @@ import { IContent, MatrixEvent } from "../models/event";
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { Method, ClientPrefix } from "../http-api";
import { Crypto, IBootstrapCrossSigningOpts } from "./index";
import { Crypto, ICryptoCallbacks, IBootstrapCrossSigningOpts } from "./index";
import {
ClientEvent,
CrossSigningKeys,
ClientEventHandlerMap,
CrossSigningKeys,
ICrossSigningKey,
ICryptoCallbacks,
ISignedKey,
KeySignatures,
} from "../matrix";
} from "../client";
import { ISecretStorageKeyInfo } from "./api";
import { IKeyBackupInfo } from "./keybackup";
import { TypedEventEmitter } from "../models/typed-event-emitter";
@@ -62,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);
@@ -193,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,
@@ -273,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();
}
@@ -343,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);
}
@@ -357,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> },
+13 -13
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();
}
@@ -804,7 +804,7 @@ export class OlmDevice {
log,
);
return info!;
return info;
}
/**
@@ -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', [
+3 -10
View File
@@ -100,21 +100,14 @@ export class OutgoingRoomKeyRequestManager {
// of sendOutgoingRoomKeyRequests
private sendOutgoingRoomKeyRequestsRunning = false;
private clientRunning = false;
private clientRunning = true;
constructor(
public constructor(
private readonly baseApis: MatrixClient,
private readonly deviceId: string,
private readonly cryptoStore: CryptoStore,
) {}
/**
* Called when the client is started. Sets background processes running.
*/
public start(): void {
this.clientRunning = true;
}
/**
* Called when the client is stopped. Stops any running background processes.
*/
@@ -359,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(
+5 -4
View File
@@ -19,8 +19,9 @@ import * as olmlib from './olmlib';
import { encodeBase64 } from './olmlib';
import { randomString } from '../randomstring';
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes';
import { ClientEvent, IContent, ICryptoCallbacks, MatrixEvent } from '../matrix';
import { ClientEventHandlerMap, MatrixClient } from "../client";
import { ICryptoCallbacks } from ".";
import { IContent, MatrixEvent } from "../models/event";
import { ClientEvent, ClientEventHandlerMap, MatrixClient } from "../client";
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api';
import { TypedEventEmitter } from '../models/typed-event-emitter';
import { defer, IDeferred } from "../utils";
@@ -77,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,
@@ -380,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,
+35 -31
View File
@@ -38,7 +38,7 @@ import { IOlmSessionResult } from "../olmlib";
import { DeviceInfoMap } from "../DeviceList";
import { MatrixEvent } from "../../models/event";
import { EventType, MsgType } from '../../@types/event';
import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
import { IEncryptedContent, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager';
import { OlmGroupSessionExtraData } from "../../@types/crypto";
import { MatrixError } from "../../http-api";
@@ -105,12 +105,6 @@ interface IPayload extends Partial<IMessage> {
algorithm?: string;
sender_key?: string;
}
interface IEncryptedContent {
algorithm: string;
sender_key: string;
ciphertext: Record<string, string>;
}
/* eslint-enable camelcase */
interface SharedWithData {
@@ -142,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();
}
@@ -254,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;
@@ -353,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[]> = {};
@@ -392,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,
@@ -421,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
@@ -458,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));
@@ -702,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;
}
@@ -736,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;
}
@@ -782,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}`);
}
/**
@@ -816,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,
@@ -976,12 +969,12 @@ 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);
if (this.crypto.getGlobalErrorOnUnknownDevices()) {
if (this.crypto.globalErrorOnUnknownDevices) {
// Drop unknown devices for now. When the message gets sent, we'll
// throw an error, but we'll still be prepared to send to the known
// devices.
@@ -1034,7 +1027,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
// check if any of these devices are not yet known to the user.
// if so, warn the user so they can verify or ignore.
if (this.crypto.getGlobalErrorOnUnknownDevices()) {
if (this.crypto.globalErrorOnUnknownDevices) {
this.checkForUnknownDevices(devicesInRoom);
}
@@ -1169,7 +1162,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
});
// The global value is treated as a default for when rooms don't specify a value.
let isBlacklisting = this.crypto.getGlobalBlacklistUnverifiedDevices();
let isBlacklisting = this.crypto.globalBlacklistUnverifiedDevices;
const isRoomBlacklisting = room.getBlacklistUnverifiedDevices();
if (typeof isRoomBlacklisting === 'boolean') {
isBlacklisting = isRoomBlacklisting;
@@ -1238,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;
}
@@ -1318,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 +=
@@ -1660,6 +1657,9 @@ class MegolmDecryption extends DecryptionAlgorithm {
return;
}
}
// XXX: switch this to use encryptAndSendToDevices() rather than duplicating it?
await olmlib.ensureOlmSessionsForDevices(
this.olmDevice, this.baseApis, { [sender]: [device] }, false,
);
@@ -1717,6 +1717,8 @@ class MegolmDecryption extends DecryptionAlgorithm {
const deviceInfo = this.crypto.getStoredDevice(userId, deviceId)!;
const body = keyRequest.requestBody;
// XXX: switch this to use encryptAndSendToDevices()?
this.olmlib.ensureOlmSessionsForDevices(
this.olmDevice, this.baseApis, {
[userId]: [deviceInfo],
@@ -1903,13 +1905,15 @@ 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);
// FIXME: use encryptAndSendToDevices() rather than duplicating it here.
const promises: Promise<unknown>[] = [];
const contentMap: Record<string, Record<string, IEncryptedContent>> = {};
for (const [userId, devices] of Object.entries(devicesByUser)) {
+3 -6
View File
@@ -119,12 +119,10 @@ class OlmEncryption extends EncryptionAlgorithm {
const promises: Promise<void>[] = [];
for (let i = 0; i < users.length; ++i) {
const userId = users[i];
for (const userId of users) {
const devices = this.crypto.getStoredDevicesForUser(userId) || [];
for (let j = 0; j < devices.length; ++j) {
const deviceInfo = devices[j];
for (const deviceInfo of devices) {
const key = deviceInfo.getIdentityKey();
if (key == this.olmDevice.deviceCurve25519Key) {
// don't bother sending to ourself
@@ -304,8 +302,7 @@ class OlmDecryption extends DecryptionAlgorithm {
// try each session in turn.
const decryptionErrors: Record<string, string> = {};
for (let i = 0; i < sessionIds.length; i++) {
const sessionId = sessionIds[i];
for (const sessionId of sessionIds) {
try {
const payload = await this.olmDevice.decryptMessage(
theirDeviceIdentityKey, sessionId, message.type, message.body,
+7 -8
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;
}
@@ -302,7 +302,7 @@ export class BackupManager {
|| now - this.sessionLastCheckAttemptedTime[targetSessionId!] > KEY_BACKUP_CHECK_RATE_LIMIT
) {
this.sessionLastCheckAttemptedTime[targetSessionId!] = now;
await this.baseApis.restoreKeyBackupWithCache(targetRoomId, targetSessionId, this.backupInfo, {});
await this.baseApis.restoreKeyBackupWithCache(targetRoomId!, targetSessionId!, this.backupInfo, {});
}
}
@@ -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);
@@ -680,8 +680,7 @@ export class Curve25519 implements BackupAlgorithm {
const backupPubKey = decryption.init_with_private_key(privKey);
if (backupPubKey !== this.authData.public_key) {
// eslint-disable-next-line no-throw-literal
throw { errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY };
throw new MatrixError({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY });
}
const keys: IMegolmSessionData[] = [];
@@ -736,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,
) {}
@@ -787,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
+69 -65
View File
@@ -127,6 +127,29 @@ export interface IBootstrapCrossSigningOpts {
authUploadDeviceSigningKeys?(makeRequest: (authData: any) => Promise<{}>): Promise<void>;
}
export interface ICryptoCallbacks {
getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array | null>;
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
shouldUpgradeDeviceVerifications?: (
users: Record<string, any>
) => Promise<string[]>;
getSecretStorageKey?: (
keys: {keys: Record<string, ISecretStorageKeyInfo>}, name: string
) => Promise<[string, Uint8Array] | null>;
cacheSecretStorageKey?: (
keyId: string, keyInfo: ISecretStorageKeyInfo, key: Uint8Array
) => void;
onSecretRequested?: (
userId: string, deviceId: string,
requestId: string, secretName: string, deviceTrust: DeviceTrustLevel
) => Promise<string>;
getDehydrationKey?: (
keyInfo: ISecretStorageKeyInfo,
checkFunc: (key: Uint8Array) => void,
) => Promise<Uint8Array>;
getBackupKey?: () => Promise<Uint8Array>;
}
/* eslint-disable camelcase */
interface IRoomKey {
room_id: string;
@@ -255,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();
}
@@ -285,8 +308,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
private deviceKeys: Record<string, string> = {}; // type: key
private globalBlacklistUnverifiedDevices = false;
private globalErrorOnUnknownDevices = true;
public globalBlacklistUnverifiedDevices = false;
public globalErrorOnUnknownDevices = true;
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
// we received in the current sync.
@@ -349,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,
@@ -442,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);
};
}
@@ -686,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);
@@ -823,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(
@@ -845,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;
}
@@ -861,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 } }, "",
@@ -880,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")
@@ -1218,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!,
@@ -1452,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:
@@ -1634,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) => {
@@ -1752,9 +1775,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
eventEmitter.on(MatrixEventEvent.Decrypted, this.onTimelineEvent);
}
/** Start background processes related to crypto */
/**
* @deprecated this does nothing and will be removed in a future version
*/
public start(): void {
this.outgoingRoomKeyRequestManager.start();
logger.warn("MatrixClient.crypto.start() is deprecated");
}
/** Stop background processes related to crypto */
@@ -1788,6 +1813,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* do not specify a value.
*
* @param {boolean} value whether to blacklist all unverified devices by default
*
* @deprecated For external code, use {@link MatrixClient#setGlobalBlacklistUnverifiedDevices}. For
* internal code, set {@link #globalBlacklistUnverifiedDevices} directly.
*/
public setGlobalBlacklistUnverifiedDevices(value: boolean): void {
this.globalBlacklistUnverifiedDevices = value;
@@ -1795,34 +1823,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
/**
* @return {boolean} whether to blacklist all unverified devices by default
*
* @deprecated For external code, use {@link MatrixClient#getGlobalBlacklistUnverifiedDevices}. For
* internal code, reference {@link #globalBlacklistUnverifiedDevices} directly.
*/
public getGlobalBlacklistUnverifiedDevices(): boolean {
return this.globalBlacklistUnverifiedDevices;
}
/**
* Set whether sendMessage in a room with unknown and unverified devices
* should throw an error and not send them message. This has 'Global' for
* symmetry with setGlobalBlacklistUnverifiedDevices but there is currently
* no room-level equivalent for this setting.
*
* This API is currently UNSTABLE and may change or be removed without notice.
*
* @param {boolean} value whether error on unknown devices
*/
public setGlobalErrorOnUnknownDevices(value: boolean): void {
this.globalErrorOnUnknownDevices = value;
}
/**
* @return {boolean} whether to error on unknown devices
*
* This API is currently UNSTABLE and may change or be removed without notice.
*/
public getGlobalErrorOnUnknownDevices(): boolean {
return this.globalErrorOnUnknownDevices;
}
/**
* Upload the device keys to the homeserver.
* @return {object} A promise that will resolve when the keys are uploaded.
@@ -1856,7 +1864,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
}
public setNeedsNewFallback(needsNewFallback: boolean) {
public setNeedsNewFallback(needsNewFallback: boolean): void {
this.needsNewFallback = needsNewFallback;
}
@@ -1865,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
@@ -1911,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) {
@@ -2147,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]: {
@@ -2238,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]: {
@@ -2379,9 +2387,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
*/
public async getOlmSessionsForUser(userId: string): Promise<Record<string, IUserOlmSession>> {
const devices = this.getStoredDevicesForUser(userId) || [];
const result = {};
for (let j = 0; j < devices.length; ++j) {
const device = devices[j];
const result: { [deviceId: string]: IUserOlmSession } = {};
for (const device of devices) {
const deviceKey = device.getIdentityKey();
const sessions = await this.olmDevice.getSessionInfoForDevice(deviceKey);
@@ -2638,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;
@@ -2682,14 +2689,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
): Promise<Record<string, Record<string, olmlib.IOlmSessionResult>>> {
const devicesByUser: Record<string, DeviceInfo[]> = {};
for (let i = 0; i < users.length; ++i) {
const userId = users[i];
for (const userId of users) {
devicesByUser[userId] = [];
const devices = this.getStoredDevicesForUser(userId) || [];
for (let j = 0; j < devices.length; ++j) {
const deviceInfo = devices[j];
for (const deviceInfo of devices) {
const key = deviceInfo.getIdentityKey();
if (key == this.olmDevice.deviceCurve25519Key) {
// don't bother setting up session to ourself
@@ -2743,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,
@@ -3183,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) {
@@ -3265,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);
@@ -3336,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);
@@ -3357,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."));
}
@@ -3416,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);
@@ -3688,7 +3692,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return;
}
req.share = () => {
req.share = (): void => {
decryptor.shareKeysWithDevice(req);
};
@@ -3859,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");
};
}
@@ -3884,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()!;
+12 -9
View File
@@ -82,18 +82,22 @@ 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) {
// If we don't have a session for a device then
// we can't encrypt a message for it.
logger.log(
`[olmlib.encryptMessageForDevice] Unable to find Olm session for device ` +
`${recipientUserId}:${recipientDevice.deviceId}`,
);
return;
}
logger.log(
"Using sessionid " + sessionId + " for device " +
recipientUserId + ":" + recipientDevice.deviceId,
`[olmlib.encryptMessageForDevice] Using Olm session ${sessionId} for device ` +
`${recipientUserId}:${recipientDevice.deviceId}`,
);
const payload = {
@@ -169,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,
);
@@ -252,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);
};
@@ -335,8 +339,7 @@ export async function ensureOlmSessionsForDevices(
const promises: Promise<void>[] = [];
for (const [userId, devices] of Object.entries(devicesByUser)) {
const userRes = otkResult[userId] || {};
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
for (const deviceInfo of devices) {
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
@@ -460,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] || {};
@@ -524,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>) {
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>) {
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>) {
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({
+359
View File
@@ -0,0 +1,359 @@
/*
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.
*/
import {
WidgetApi,
WidgetApiToWidgetAction,
MatrixCapabilities,
IWidgetApiRequest,
IWidgetApiAcknowledgeResponseData,
ISendEventToWidgetActionRequest,
ISendToDeviceToWidgetActionRequest,
ISendEventFromWidgetResponseData,
} from "matrix-widget-api";
import { IEvent, IContent, EventStatus } from "./models/event";
import { ISendEventResponse } from "./@types/requests";
import { EventType } from "./@types/event";
import { logger } from "./logger";
import { MatrixClient, ClientEvent, IMatrixClientCreateOpts, IStartClientOpts } from "./client";
import { SyncApi, SyncState } from "./sync";
import { SlidingSyncSdk } from "./sliding-sync-sdk";
import { MatrixEvent } from "./models/event";
import { User } from "./models/user";
import { Room } from "./models/room";
import { ToDeviceBatch } from "./models/ToDeviceMessage";
import { DeviceInfo } from "./crypto/deviceinfo";
import { IOlmDevice } from "./crypto/algorithms/megolm";
interface IStateEventRequest {
eventType: string;
stateKey?: string;
}
export interface ICapabilities {
/**
* Event types that this client expects to send.
*/
sendEvent?: string[];
/**
* Event types that this client expects to receive.
*/
receiveEvent?: string[];
/**
* Message types that this client expects to send, or true for all message
* types.
*/
sendMessage?: string[] | true;
/**
* Message types that this client expects to receive, or true for all
* message types.
*/
receiveMessage?: string[] | true;
/**
* Types of state events that this client expects to send.
*/
sendState?: IStateEventRequest[];
/**
* Types of state events that this client expects to receive.
*/
receiveState?: IStateEventRequest[];
/**
* To-device event types that this client expects to send.
*/
sendToDevice?: string[];
/**
* To-device event types that this client expects to receive.
*/
receiveToDevice?: string[];
/**
* Whether this client needs access to TURN servers.
* @default false
*/
turnServers?: boolean;
}
/**
* A MatrixClient that routes its requests through the widget API instead of the
* real CS API.
* @experimental This class is considered unstable!
*/
export class RoomWidgetClient extends MatrixClient {
private room?: Room;
private widgetApiReady = new Promise<void>(resolve => this.widgetApi.once("ready", resolve));
private lifecycle?: AbortController;
private syncState: SyncState | null = null;
public constructor(
private readonly widgetApi: WidgetApi,
private readonly capabilities: ICapabilities,
private readonly roomId: string,
opts: IMatrixClientCreateOpts,
) {
super(opts);
// Request capabilities for the functionality this client needs to support
if (
capabilities.sendEvent?.length
|| capabilities.receiveEvent?.length
|| capabilities.sendMessage === true
|| (Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length)
|| capabilities.receiveMessage === true
|| (Array.isArray(capabilities.receiveMessage) && capabilities.receiveMessage.length)
|| capabilities.sendState?.length
|| capabilities.receiveState?.length
) {
widgetApi.requestCapabilityForRoomTimeline(roomId);
}
capabilities.sendEvent?.forEach(eventType =>
widgetApi.requestCapabilityToSendEvent(eventType),
);
capabilities.receiveEvent?.forEach(eventType =>
widgetApi.requestCapabilityToReceiveEvent(eventType),
);
if (capabilities.sendMessage === true) {
widgetApi.requestCapabilityToSendMessage();
} else if (Array.isArray(capabilities.sendMessage)) {
capabilities.sendMessage.forEach(msgType =>
widgetApi.requestCapabilityToSendMessage(msgType),
);
}
if (capabilities.receiveMessage === true) {
widgetApi.requestCapabilityToReceiveMessage();
} else if (Array.isArray(capabilities.receiveMessage)) {
capabilities.receiveMessage.forEach(msgType =>
widgetApi.requestCapabilityToReceiveMessage(msgType),
);
}
capabilities.sendState?.forEach(({ eventType, stateKey }) =>
widgetApi.requestCapabilityToSendState(eventType, stateKey),
);
capabilities.receiveState?.forEach(({ eventType, stateKey }) =>
widgetApi.requestCapabilityToReceiveState(eventType, stateKey),
);
capabilities.sendToDevice?.forEach(eventType =>
widgetApi.requestCapabilityToSendToDevice(eventType),
);
capabilities.receiveToDevice?.forEach(eventType =>
widgetApi.requestCapabilityToReceiveToDevice(eventType),
);
if (capabilities.turnServers) {
widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
}
widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
// Open communication with the host
widgetApi.start();
}
public async startClient(opts: IStartClientOpts = {}): Promise<void> {
this.lifecycle = new AbortController();
// Create our own user object artificially (instead of waiting for sync)
// so it's always available, even if the user is not in any rooms etc.
const userId = this.getUserId();
if (userId) {
this.store.storeUser(new User(userId));
}
// Even though we have no access token and cannot sync, the sync class
// still has some valuable helper methods that we make use of, so we
// instantiate it anyways
if (opts.slidingSync) {
this.syncApi = new SlidingSyncSdk(opts.slidingSync, this, opts);
} else {
this.syncApi = new SyncApi(this, opts);
}
this.room = this.syncApi.createRoom(this.roomId);
this.store.storeRoom(this.room);
await this.widgetApiReady;
// Backfill the requested events
// We only get the most recent event for every type + state key combo,
// so it doesn't really matter what order we inject them in
await Promise.all(
this.capabilities.receiveState?.map(async ({ eventType, stateKey }) => {
const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [this.roomId]);
const events = rawEvents.map(rawEvent => new MatrixEvent(rawEvent as Partial<IEvent>));
await this.syncApi!.injectRoomEvents(this.room!, [], events);
events.forEach(event => {
this.emit(ClientEvent.Event, event);
logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
});
}) ?? [],
);
this.setSyncState(SyncState.Syncing);
logger.info("Finished backfilling events");
// Watch for TURN servers, if requested
if (this.capabilities.turnServers) this.watchTurnServers();
}
public stopClient(): void {
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
this.widgetApi.off(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
super.stopClient();
this.lifecycle!.abort(); // Signal to other async tasks that the client has stopped
}
public async joinRoom(roomIdOrAlias: string): Promise<Room> {
if (roomIdOrAlias === this.roomId) return this.room!;
throw new Error(`Unknown room: ${roomIdOrAlias}`);
}
protected async encryptAndSendEvent(room: Room, event: MatrixEvent): Promise<ISendEventResponse> {
let response: ISendEventFromWidgetResponseData;
try {
response = await this.widgetApi.sendRoomEvent(event.getType(), event.getContent(), room.roomId);
} catch (e) {
this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
throw e;
}
room.updatePendingEvent(event, EventStatus.SENT, response.event_id);
return { event_id: response.event_id };
}
public async sendStateEvent(
roomId: string,
eventType: string,
content: any,
stateKey = "",
): Promise<ISendEventResponse> {
return await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId);
}
public async sendToDevice(
eventType: string,
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
): Promise<{}> {
await this.widgetApi.sendToDevice(eventType, false, contentMap);
return {};
}
public async queueToDevice({ eventType, batch }: ToDeviceBatch): Promise<void> {
const contentMap: { [userId: string]: { [deviceId: string]: object } } = {};
for (const { userId, deviceId, payload } of batch) {
if (!contentMap[userId]) contentMap[userId] = {};
contentMap[userId][deviceId] = payload;
}
await this.widgetApi.sendToDevice(eventType, false, contentMap);
}
public async encryptAndSendToDevices(
userDeviceInfoArr: IOlmDevice<DeviceInfo>[],
payload: object,
): Promise<void> {
const contentMap: { [userId: string]: { [deviceId: string]: object } } = {};
for (const { userId, deviceInfo: { deviceId } } of userDeviceInfoArr) {
if (!contentMap[userId]) contentMap[userId] = {};
contentMap[userId][deviceId] = payload;
}
await this.widgetApi.sendToDevice((payload as { type: string }).type, true, contentMap);
}
// Overridden since we get TURN servers automatically over the widget API,
// and this method would otherwise complain about missing an access token
public async checkTurnServers(): Promise<boolean> {
return this.turnServers.length > 0;
}
// Overridden since we 'sync' manually without the sync API
public getSyncState(): SyncState | null {
return this.syncState;
}
private setSyncState(state: SyncState): void {
const oldState = this.syncState;
this.syncState = state;
this.emit(ClientEvent.Sync, state, oldState);
}
private async ack(ev: CustomEvent<IWidgetApiRequest>): Promise<void> {
await this.widgetApi.transport.reply<IWidgetApiAcknowledgeResponseData>(ev.detail, {});
}
private onEvent = async (ev: CustomEvent<ISendEventToWidgetActionRequest>): Promise<void> => {
ev.preventDefault();
// Verify the room ID matches, since it's possible for the client to
// send us events from other rooms if this widget is always on screen
if (ev.detail.data.room_id === this.roomId) {
const event = new MatrixEvent(ev.detail.data as Partial<IEvent>);
await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
this.emit(ClientEvent.Event, event);
this.setSyncState(SyncState.Syncing);
logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
} else {
const { event_id: eventId, room_id: roomId } = ev.detail.data;
logger.info(`Received event ${eventId} for a different room ${roomId}; discarding`);
}
await this.ack(ev);
};
private onToDevice = async (ev: CustomEvent<ISendToDeviceToWidgetActionRequest>): Promise<void> => {
ev.preventDefault();
const event = new MatrixEvent({
type: ev.detail.data.type,
sender: ev.detail.data.sender,
content: ev.detail.data.content as IContent,
});
// Mark the event as encrypted if it was, using fake contents and keys since those are unknown to us
if (ev.detail.data.encrypted) event.makeEncrypted(EventType.RoomMessageEncrypted, {}, "", "");
this.emit(ClientEvent.ToDeviceEvent, event);
this.setSyncState(SyncState.Syncing);
await this.ack(ev);
};
private async watchTurnServers(): Promise<void> {
const servers = this.widgetApi.getTurnServers();
const onClientStopped = (): void => {
servers.return(undefined);
};
this.lifecycle!.signal.addEventListener("abort", onClientStopped);
try {
for await (const server of servers) {
this.turnServers = [{
urls: server.uris,
username: server.username,
credential: server.password,
}];
this.emit(ClientEvent.TurnServers, this.turnServers);
logger.log(`Received TURN server: ${server.uris}`);
}
} catch (e) {
logger.warn("Error watching TURN servers", e);
} finally {
this.lifecycle!.signal.removeEventListener("abort", onClientStopped);
}
}
}
+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;
}
+2 -3
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
@@ -150,8 +150,7 @@ export class FilterComponent {
},
};
for (let n = 0; n < Object.keys(literalKeys).length; n++) {
const name = Object.keys(literalKeys)[n];
for (const name in literalKeys) {
const matchFunc = literalKeys[name];
const notName = "not_" + name;
const disallowedValues: string[] = this.filterJson[notName];
+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);
}
}
+5 -5
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);
}
}
@@ -49,9 +49,9 @@ export class HTTPError extends Error {
*/
export class MatrixError extends HTTPError {
public readonly errcode?: string;
public readonly data: IErrorJson;
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";
}
}
+4 -2
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,
) {
@@ -236,7 +236,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
method: Method,
url: URL | string,
body?: Body,
opts: Pick<IRequestOpts, "headers" | "json" | "localTimeoutMs" | "abortSignal"> = {},
opts: Pick<IRequestOpts, "headers" | "json" | "localTimeoutMs" | "keepAlive" | "abortSignal"> = {},
): Promise<ResponseType<T, O>> {
const headers = Object.assign({}, opts.headers || {});
const json = opts.json ?? true;
@@ -254,6 +254,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
}
const timeout = opts.localTimeoutMs ?? this.opts.localTimeoutMs;
const keepAlive = opts.keepAlive ?? false;
const signals = [
this.abortController.signal,
];
@@ -286,6 +287,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
referrerPolicy: "no-referrer",
cache: "no-cache",
credentials: "omit", // we send credentials via headers
keepalive: keepAlive,
});
} catch (e) {
if ((<Error>e).name === "AbortError") {
+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;
+1
View File
@@ -38,6 +38,7 @@ export interface IRequestOpts {
headers?: Record<string, string>;
abortSignal?: AbortSignal;
localTimeoutMs?: number;
keepAlive?: boolean; // defaults to false
json?: boolean; // defaults to true
// Set to true to prevent the request function from emitting a Session.logged_out event.
+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);
});
}
+22 -23
View File
@@ -21,6 +21,7 @@ limitations under the License.
import { logger } from './logger';
import { MatrixClient } from "./client";
import { defer, IDeferred } from "./utils";
import { MatrixError } from "./http-api";
const EMAIL_STAGE_TYPE = "m.login.email.identity";
const MSISDN_STAGE_TYPE = "m.login.msisdn";
@@ -99,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);
}
}
@@ -111,7 +112,7 @@ interface IOpts {
sessionId?: string;
clientSecret?: string;
emailSid?: string;
doRequest(auth: IAuthData, background: boolean): Promise<IAuthData>;
doRequest(auth: IAuthData | null, background: boolean): Promise<IAuthData>;
stateUpdated(nextStage: AuthType, status: IStageStatus): void;
requestEmailToken(email: string, secret: string, attempt: number, session: string): Promise<{ sid: string }>;
busyChanged?(busy: boolean): void;
@@ -217,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;
@@ -328,7 +329,7 @@ export class InteractiveAuth {
* @param {string} loginType login type for the stage
* @return {object?} any parameters from the server for this stage
*/
public getStageParams(loginType: string): Record<string, any> {
public getStageParams(loginType: string): Record<string, any> | undefined {
return this.data.params?.[loginType];
}
@@ -418,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
@@ -428,10 +429,10 @@ export class InteractiveAuth {
this.requestingEmailToken = true;
try {
const requestTokenResult = await this.requestEmailTokenCallback(
this.inputs.emailAddress,
this.inputs.emailAddress!,
this.clientSecret,
this.emailAttempt++,
this.data.session,
this.data.session!,
);
this.emailSid = requestTokenResult.sid;
logger.trace("Email token request succeeded");
@@ -454,16 +455,16 @@ export class InteractiveAuth {
* This can be set to true for requests that just poll to see if auth has
* been completed elsewhere.
*/
private async doRequest(auth: IAuthData, background = false): Promise<void> {
private async doRequest(auth: IAuthData | null, background = false): Promise<void> {
try {
const result = await this.requestCallback(auth, background);
this.attemptAuthDeferred!.resolve(result);
this.attemptAuthDeferred = null;
} catch (error) {
// sometimes UI auth errors don't come with flows
const errorFlows = error.data?.flows ?? null;
const errorFlows = (<MatrixError>error).data?.flows ?? null;
const haveFlows = this.data.flows || Boolean(errorFlows);
if (error.httpStatus !== 401 || !error.data || !haveFlows) {
if ((<MatrixError>error).httpStatus !== 401 || !(<MatrixError>error).data || !haveFlows) {
// doesn't look like an interactive-auth failure.
if (!background) {
this.attemptAuthDeferred?.reject(error);
@@ -474,20 +475,23 @@ export class InteractiveAuth {
logger.log("Background poll request failed doing UI auth: ignoring", error);
}
}
if (!error.data) {
error.data = {};
if (!(<MatrixError>error).data) {
(<MatrixError>error).data = {};
}
// if the error didn't come with flows, completed flows or session ID,
// copy over the ones we have. Synapse sometimes sends responses without
// any UI auth data (eg. when polling for email validation, if the email
// has not yet been validated). This appears to be a Synapse bug, which
// we workaround here.
if (!error.data.flows && !error.data.completed && !error.data.session) {
error.data.flows = this.data.flows;
error.data.completed = this.data.completed;
error.data.session = this.data.session;
if (!(<MatrixError>error).data.flows &&
!(<MatrixError>error).data.completed &&
!(<MatrixError>error).data.session
) {
(<MatrixError>error).data.flows = this.data.flows;
(<MatrixError>error).data.completed = this.data.completed;
(<MatrixError>error).data.session = this.data.session;
}
this.data = error.data;
this.data = (<MatrixError>error).data;
try {
this.startNextAuthStage();
} catch (e) {
@@ -627,11 +631,6 @@ export class InteractiveAuth {
*/
private firstUncompletedStage(flow: IFlow): AuthType | undefined {
const completed = this.data.completed || [];
for (let i = 0; i < flow.stages.length; ++i) {
const stageType = flow.stages[i];
if (completed.indexOf(stageType) === -1) {
return stageType;
}
}
return flow.stages.find(stageType => !completed.includes(stageType));
}
}
+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);
+36 -58
View File
@@ -1,5 +1,5 @@
/*
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
Copyright 2015-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.
@@ -14,14 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { WidgetApi } from "matrix-widget-api";
import { MemoryCryptoStore } from "./crypto/store/memory-crypto-store";
import { MemoryStore } from "./store/memory";
import { MatrixScheduler } from "./scheduler";
import { MatrixClient, ICreateClientOpts } from "./client";
import { DeviceTrustLevel } from "./crypto/CrossSigning";
import { ISecretStorageKeyInfo } from "./crypto/api";
import { RoomWidgetClient, ICapabilities } from "./embedded";
import { CryptoStore } from "./crypto/store/base";
export * from "./client";
export * from "./embedded";
export * from "./http-api";
export * from "./autodiscovery";
export * from "./sync-accumulator";
@@ -51,11 +54,18 @@ export * from './@types/requests';
export * from './@types/search';
export * from './models/room-summary';
export * as ContentHelpers from "./content-helpers";
export type { ICryptoCallbacks } from "./crypto"; // used to be located here
export { createNewMatrixCall } from "./webrtc/call";
export type { MatrixCall } from "./webrtc/call";
export {
createNewMatrixCall,
} from "./webrtc/call";
GroupCallEvent,
GroupCallIntent,
GroupCallState,
GroupCallType,
} 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
@@ -63,38 +73,24 @@ 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;
}
export interface ICryptoCallbacks {
getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array | null>;
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
shouldUpgradeDeviceVerifications?: (
users: Record<string, any>
) => Promise<string[]>;
getSecretStorageKey?: (
keys: {keys: Record<string, ISecretStorageKeyInfo>}, name: string
) => Promise<[string, Uint8Array] | null>;
cacheSecretStorageKey?: (
keyId: string, keyInfo: ISecretStorageKeyInfo, key: Uint8Array
) => void;
onSecretRequested?: (
userId: string, deviceId: string,
requestId: string, secretName: string, deviceTrust: DeviceTrustLevel
) => Promise<string>;
getDehydrationKey?: (
keyInfo: ISecretStorageKeyInfo,
checkFunc: (key: Uint8Array) => void,
) => Promise<Uint8Array>;
getBackupKey?: () => Promise<Uint8Array>;
function amendClientOpts(opts: ICreateClientOpts): ICreateClientOpts {
opts.store = opts.store ?? new MemoryStore({
localStorage: global.localStorage,
});
opts.scheduler = opts.scheduler ?? new MatrixScheduler();
opts.cryptoStore = opts.cryptoStore ?? cryptoStoreFactory();
return opts;
}
/**
* Construct a Matrix Client. Similar to {@link module:client.MatrixClient}
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
* @param {(Object)} opts The configuration options for this client. If
* this is a string, it is assumed to be the base URL. These configuration
* @param {Object} opts The configuration options for this client. These configuration
* options will be passed directly to {@link module:client.MatrixClient}.
* @param {Object} opts.store If not set, defaults to
* {@link module:store/memory.MemoryStore}.
@@ -111,33 +107,15 @@ export interface ICryptoCallbacks {
* @see {@link module:client.MatrixClient} for the full list of options for
* <code>opts</code>.
*/
export function createClient(opts: ICreateClientOpts) {
opts.store = opts.store || new MemoryStore({
localStorage: global.localStorage,
});
opts.scheduler = opts.scheduler || new MatrixScheduler();
opts.cryptoStore = opts.cryptoStore || cryptoStoreFactory();
return new MatrixClient(opts);
export function createClient(opts: ICreateClientOpts): MatrixClient {
return new MatrixClient(amendClientOpts(opts));
}
/**
* A wrapper for the request function interface.
* @callback requestWrapperFunction
* @param {requestFunction} origRequest The underlying request function being
* wrapped
* @param {Object} opts The options for this HTTP request, given in the same
* form as {@link requestFunction}.
* @param {requestCallback} callback The request callback.
*/
/**
* The request callback interface for performing HTTP requests. This matches the
* API for the {@link https://github.com/request/request#requestoptions-callback|
* request NPM module}. The SDK will implement a callback which meets this
* interface in order to handle the HTTP response.
* @callback requestCallback
* @param {Error} err The error if one occurred, else falsey.
* @param {Object} response The HTTP response which consists of
* <code>{statusCode: {Number}, headers: {Object}}</code>
* @param {Object} body The parsed HTTP response body.
*/
export function createRoomWidgetClient(
widgetApi: WidgetApi,
capabilities: ICapabilities,
roomId: string,
opts: ICreateClientOpts,
): MatrixClient {
return new RoomWidgetClient(widgetApi, capabilities, roomId, amendClientOpts(opts));
}
+1 -1
View File
@@ -145,7 +145,7 @@ export class MSC3089Branch {
let event: MatrixEvent | undefined = room.getUnfilteredTimelineSet().findEventById(this.id);
// keep scrolling back if needed until we find the event or reach the start of the room:
while (!event && room.getLiveTimeline().getState(EventTimeline.BACKWARDS).paginationToken) {
while (!event && room.getLiveTimeline().getState(EventTimeline.BACKWARDS)!.paginationToken) {
await this.client.scrollback(room, 100);
event = room.getUnfilteredTimelineSet().findEventById(this.id);
}
+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;

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