Compare commits

...

1336 Commits

Author SHA1 Message Date
RiotRobot a491508543 v28.2.0 2023-09-26 12:07:30 +01:00
RiotRobot 0abba3e626 Prepare changelog for v28.2.0 2023-09-26 12:07:26 +01:00
RiotRobot d669ddfab2 v28.2.0-rc.1 2023-09-19 12:34:58 +01:00
RiotRobot 9caa38d386 Prepare changelog for v28.2.0-rc.1 2023-09-19 12:34:55 +01:00
maheichyk 1c16b5cae6 Delete knocked room when knock membership changes (#3729)
* Store leave state when knock is denied

Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net>

* Delete knocked room when knock request is cancelled or denied

Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net>

* Test is updated

Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net>

---------

Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net>
Co-authored-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net>
2023-09-18 17:08:53 +00:00
Richard van der Hoff cb375e1351 rust impl of getEncryptionInfoForEvent (#3718) 2023-09-18 14:49:24 +00:00
Richard van der Hoff 5e542b3869 Fix potential delay in sending out requests from the rust SDK (#3717)
* Emit a `UserTrustStatusChanged` when user identity is updated

* Remove redundant `onCrossSigningKeysImport` callback

This now happens as a side-effect of importing the keys.

* bump to alpha release of matrix-rust-sdk-crypto-wasm

* fixup! Remove redundant `onCrossSigningKeysImport` callback

* Fix potential delay in sending out requests from the rust SDK

There was a potential race which could cause us to be very slow to send out
pending HTTP requests, particularly when handling a user verification. Add some
resiliece to make sure we handle it correctly.

* add comments

* Add a unit test

---------

Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
2023-09-18 14:11:33 +00:00
Valere c9435af637 Cleanup, separate backup bootstrap and mgmt (#3726)
* Cleanup, separate backup bootstrap and mgmt

* fix non spec compliant account data format
2023-09-15 11:38:49 +00:00
Valere 40168d4419 Rust: Query backup on fail to decrypt similar to libolm (#3711)
* Refactor key backup recovery to prepare for rust

* rust backup restore support

* map decryption errors correctly from rust

* query backup on fail to decrypt
2023-09-14 10:10:53 +00:00
Valere 6d118008b6 Map decryption errors correctly from rust (#3710)
* Refactor key backup recovery to prepare for rust

* rust backup restore support

* map decryption errors correctly from rust

* Move export out of old crypto to api with re-export

* extract base64 utility

* add tests for base64 util

* more efficient regex

* fix typo

* use different vector for bob

* missing import

* Group tests for decryption errors

* Do not map unneeded rust error for now
2023-09-13 13:34:55 +00:00
Valere 1503acb30a rust backup restore support (#3709)
* Refactor key backup recovery to prepare for rust

* rust backup restore support

* Move export out of old crypto to api with re-export

* extract base64 utility

* add tests for base64 util

* more efficient regex

* fix typo
2023-09-13 09:08:26 +00:00
David Baker 1b8507c060 Merge pull request #3723 from matrix-org/dbkr/fix_codeowners
Fix codeowners
2023-09-13 10:05:03 +01:00
David Baker d95b5ab27a Fix codeowners
Accidental change from merging https://github.com/matrix-org/matrix-js-sdk/pull/3663
2023-09-13 09:53:19 +01:00
ElementRobot 658e7b1be3 Resetting package fields for development 2023-09-12 16:53:47 +01:00
ElementRobot 95110eb889 Merge branch 'master' into develop 2023-09-12 16:53:44 +01:00
ElementRobot 9fbcef556e v28.1.0 2023-09-12 16:52:42 +01:00
ElementRobot b68ad00394 Prepare changelog for v28.1.0 2023-09-12 16:52:40 +01:00
David Baker 6836720e1e Introduce MatrixRTCSession lower level group call primitive (#3663)
* Add hacky option to disable the actual calling part of group calls.

So we can try using livekit instead.

* Put LiveKit info into the `m.call` state event (#3522)

* Put LK info into state

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

* Update to the new way the LK service works

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

---------

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

* Send 'contentLoaded' event

As per comment, so we can start digging ourselves out of the widget
API hole we're currently in.

* Add comment on updating the livekit service URL

* Appease CI on `livekit` branch (#3566)

* Update codeowners on `livekit` branch (#3567)

* add getOpenIdToken to embedded client backend

Signed-off-by: Timo K <toger5@hotmail.de>

* add test and update comment

Signed-off-by: Timo K <toger5@hotmail.de>

* Merge `develop` into `livekit` (#3569)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: RiotRobot <releases@riot.im>
Co-authored-by: Florian Duros <florianduros@element.io>
Co-authored-by: Kerry <kerrya@element.io>
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
Co-authored-by: Erik Johnston <erik@matrix.org>
Co-authored-by: Valere <bill.carson@valrsoft.com>
Co-authored-by: Hubert Chathi <hubertc@matrix.org>
Close IDB database before deleting it to prevent spurious unexpected close errors (#3478)
Fix export type `GeneratedSecretStorageKey` (#3479)
Fix order of things in `crypto-api.ts` (#3491)
Fix bug where switching media caused media in subsequent calls to fail (#3489)
fixes (#3515)
fix the integ tests, where #3509 etc fix the unit tests.
fix breakage on node 16 (#3527)
Fix an instance of failed to decrypt error when an in flight `/keys/query` fails. (#3486)
Fix `TypedEventEmitter::removeAllListeners(void)` not working (#3561)

* Revert "Merge `develop` into `livekit`" (#3572)

* Don't update calls with no livekit URL & expose method to update it instead

and generally simplify a bit: change it to a single string rather than
an array of structs.

* Fix other instances of passing focusInfo / livekit url

* Add temporary setter

* WIP refactor for removing m.call events

* Always remember rtcsessions since we need to only have one instance

* Fix tests

* Fix import loop

* Fix more cyclic imports & tests

* Test session joining

* Attempt to make tests happy

* Always leave calls in the tests to clean up

* comment + desperate attempt to work out what's failing

* More test debugging

* Okay, so these ones are fine?

* Stop more timers and hopefully have happy tests

* Test no rejoin

* Test malformed m.call.member events

* Test event emitting

and also move some code to a more sensible place in the file

* Test getActiveFoci()

* Test event emitting (and also fix it)

* Test membership updating & pruning on join

* Test getOldestMembership()

* Test member event renewal

* Don't start the rtc manager until the client has synced

Then we can initialise from the state once it's completed.

* Fix type

* Remove listeners added in constructor

* Stop the client here too

* Stop the client here also also

* ARGH. Disable tests to work out which one is causing the exception

* Disable everything

* Re-jig to avoid setting listeners in the constructor

and re-enable tests

* No need to rename this anymore

* argh, remove the right listener

* Is it this test???

* Re-enable some tests

* Try mocking getRooms to return something valid

* Re-enable other tests

* Give up trying to get the tests to work sensibly and deal with getRooms() returning nothing

* Oops, don't enable the ones that were skipped before

* One more try at the sensible way

* Didn't work, go back to the hack way.

* Log when we manage to send the member event update

* Support `getOpenIdToken()` in embedded mode (#3676)

* Call `sendContentLoaded()` (#3677)

* Start MatrixRTC in embedded mode (#3679)

* Reschedule the membership event check

* Bump widget api version

* Add mock for sendContentLoaded()

* More log detail

* Fix tests

and also better assert because the tests were passing undefined which
was considered fine because we were only checking for null.

* Simplify updateCallMembershipEvent a bit

* Split up updateCallMembershipEvent some more

* Typo

Co-authored-by: Daniel Abramov <inetcrack2@gmail.com>

* Expand comment

* Add comment

* More comments

* Better comment

* Sesson

* Rename some variables

* Comment

* Remove unused method

* Wrap updatecallMembershipEvent so it only runs one at a time

* Do another update if another one is triggered while the update happens

* Make triggerCallMembershipEventUpdate async

* Fix test & some missed timer removals

* Mark session manager as unstable

---------

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
Co-authored-by: Timo K <toger5@hotmail.de>
Co-authored-by: Timo <16718859+toger5@users.noreply.github.com>
Co-authored-by: Daniel Abramov <inetcrack2@gmail.com>
2023-09-12 15:08:15 +00:00
renovate[bot] 6f517478df Update jest to v29.5.4 (#3670)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-12 14:50:39 +00:00
Andy Balaam 35ba4074de Avoid an infinite loop in the interactive-auth test (#3722)
Reading the test "should fire stateUpdated callback with error when a
request fails" it looks like this would always cause an infinite loop
between doRequest and statusUpdated calls. I don't know why this wasn't
a problem until we updated Jest to v29.5.4, but after that point it was.

This change should fix the test failures for the Jest upgrade PR:
https://github.com/matrix-org/matrix-js-sdk/pull/3670 .
2023-09-12 14:03:47 +00:00
Valere c7827d971c Refactor key backup recovery to prepare for rust (#3708)
* Refactor key backup recovery to prepare for rust

* code review

* quick doc format

* code review fix
2023-09-12 11:19:35 +00:00
Richard van der Hoff f963ca5562 Element-R: Emit CryptoEvent.UserTrustStatusChanged when user identity is updated (#3716)
* Emit a `UserTrustStatusChanged` when user identity is updated

* Remove redundant `onCrossSigningKeysImport` callback

This now happens as a side-effect of importing the keys.

* bump to alpha release of matrix-rust-sdk-crypto-wasm

* fixup! Remove redundant `onCrossSigningKeysImport` callback
2023-09-08 04:40:02 +00:00
Malte Finsterwalder 8c30b0d12c Dont access indexed db when undefined (#3707)
* T-Defect: handle undefined indexedDB gracefully

Signed-off-by: Malte Finsterwalder <malte@holi.team>

* T-Defect: test to check handling of undefined indexedDB gracefully

Signed-off-by: Malte Finsterwalder <malte@holi.team>

---------

Signed-off-by: Malte Finsterwalder <malte@holi.team>
Co-authored-by: Malte Finsterwalder <malte@holi.team>
2023-09-07 21:52:32 +00:00
Andy Balaam 5d4334ba4c Explain why synthetic receipts don't mark the room as read (#3715)
* Explain why synthetic receipts don't mark the room as read

* Fix misleading "local echo" comment with "remote echo"

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-09-07 15:31:54 +00:00
Richard van der Hoff 7e691bf700 Implement getEncryptionInfoForEvent and deprecate getEventEncryptionInfo (#3693)
* Implement `getEncryptionInfoForEvent` and deprecate `getEventEncryptionInfo`

* fix tsdoc

* fix tests

* Improve test coverage
2023-09-07 09:39:10 +00:00
Andy Balaam 0700e86f58 Don't reset unread count when adding a synthetic receipt (#3706)
Fixes https://github.com/matrix-org/matrix-js-sdk/issues/3684
and there are lots more details about why we chose this solution in that
issue.
2023-09-07 07:24:51 +00:00
maheichyk 6c307d4c63 Sync knock rooms (#3703)
Signed-off-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net>
Co-authored-by: Mikhail Aheichyk <mikhail.aheichyk@nordeck.net>
2023-09-06 17:10:14 +00:00
renovate[bot] 88ec0e3e17 Update babel monorepo to v7.22.15 (#3704)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-06 12:56:01 +00:00
renovate[bot] 015e9a5be7 Update all non-major dependencies (#3700)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-06 12:42:44 +00:00
renovate[bot] 2918d686ae Update definitelyTyped (#3698)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-06 09:04:49 +00:00
renovate[bot] 327c18ddc1 Update babel monorepo (#3697)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-06 09:04:32 +00:00
renovate[bot] 8cdd8e882b Update peter-evans/repository-dispatch digest to bf47d10 (#3696)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-06 09:02:54 +00:00
renovate[bot] 76e0d5a896 Update actions/checkout action to v4 (#3701)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-05 17:30:43 +00:00
ElementRobot 836238c3ba v28.1.0-rc.1 2023-09-05 16:51:26 +01:00
ElementRobot 014b29b303 Prepare changelog for v28.1.0-rc.1 2023-09-05 16:51:23 +01:00
Richard van der Hoff 74160806c0 Deprecate MatrixClient.checkUserTrust (#3691)
* New `CryptoApi.getUserVerificationStatus` API

* Add `RustCrypto#getUserVerificationStatus` tests

---------

Co-authored-by: Florian Duros <florianduros@element.io>
2023-09-05 14:58:10 +00:00
Michael Telatynski 8e0ef98bcc Provide better error for ICE Server SyntaxError (#3694)
* Provide better error for ICE Server SyntaxError

* Refactor

* Add test
2023-09-05 14:18:30 +00:00
Valere d7831f9e5b Implement key backup APIs for rust and create backup in bootstrapSecretStorage (#3690)
* new resetKeyBackup API

* add delete backup version test

* code review

* support backup creation in rust

* code review
2023-09-05 13:52:49 +00:00
Michael Telatynski 989c5a3dda Allow calls without ICE/TURN/STUN servers (#3695) 2023-09-05 12:44:39 +00:00
Richard van der Hoff 0778c4e01e Re-check key backup after bootstrapSecretStorage (#3692)
Fixes https://github.com/vector-im/element-web/issues/26115
2023-09-05 09:10:58 +00:00
Valere c65e329101 Deprecate MatrixClient.{prepare,create}KeyBackupVersion in favour of new CryptoApi.resetKeyBackup API (#3689)
* new resetKeyBackup API

* add delete backup version test

* code review

* code review
2023-09-04 20:00:28 +00:00
Germain 5ddd453699 Emit summary update event (#3687)
* Emit summary update event

* Add documentation

* Update RoomSummary event documentation

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-08-31 08:48:16 +00:00
Richard van der Hoff 42d982dd69 OutgoingRequestProcessor: do not throw errors if shutting down (#3683)
* `OutgoingRequestProcessor`: do not throw errors if shutting down

* Optimised builds throw a different error
2023-08-30 14:56:06 +00:00
Richard van der Hoff f406ffd3dd RustCrypto.getCrossSigningStatus: check the client is not stopped (#3682)
* `RustCrypto.getCrossSigningStatus`: check the client is not stopped

Better error handling for the case that a call to `MatrixClient.stop` happens
while the call to `getCrossSigningStatus` (or `isCrossSigningReady`) is in
flight.

* fix up tsdoc
2023-08-30 09:30:31 +00:00
Florian Duros dec4650d3d ElementR: Update CryptoApi.userHasCrossSigningKeys (#3646)
* WIP `CryptoApi.getStoredCrossSigningForUser`

* Fix QRCode

* Add docs and rename

* Add tests for `RustCrossSigningInfo.ts`

* Do `/keys/query` instead of using `UserIdentity`

* Review changes

* Get rid of `CrossSigningInfo`

* Merge `hasCrossSigningKeysForUser` into `userHasCrossSigningKeys`

* Apply suggestions from code review

* More review comments

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <richard@matrix.org>
2023-08-29 11:27:28 +00:00
RiotRobot 4c00b41046 Resetting package fields for development 2023-08-29 10:55:15 +01:00
RiotRobot a1845ba0ff Merge branch 'master' into develop 2023-08-29 10:55:09 +01:00
RiotRobot fb9e258468 v28.0.0 2023-08-29 10:53:33 +01:00
RiotRobot 974723ceef Prepare changelog for v28.0.0 2023-08-29 10:53:30 +01:00
renovate[bot] 5788d9744b Update all non-major dependencies (#3671)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-24 13:01:48 +00:00
Johannes Marbach 65cbbaaf01 Use sender instead of content.creator field on m.room.create events (#3675)
* Use sender instead of content.creator field on m.room.create events

* Restore room_version fields in fixtures

* Add test case for undefined sender scenario
2023-08-24 11:58:04 +00:00
renovate[bot] c5245a887b Update dependency @types/node to v18.17.6 (#3669)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-23 13:47:35 +00:00
Charly Nguyen 321679fd63 Add join_rule field to /publicRooms response (#3673)
Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net>
2023-08-23 13:35:43 +00:00
Michael Weimann 15c679b29e Improve/fix restoreKeyBackupWithPassword docs (#3674) 2023-08-23 12:58:10 +00:00
Michael Telatynski 85ba069117 Export de-facto public types out of @types (#3666)
* Export de-facto public types out of @types

* Make typedoc happier
2023-08-22 15:47:22 +00:00
RiotRobot 9b8dcf53ed v28.0.0-rc.1 2023-08-22 15:18:45 +01:00
RiotRobot 324af3ee67 Prepare changelog for v28.0.0-rc.1 2023-08-22 15:18:42 +01:00
RiotRobot ec6c0946d4 v27.3.0-rc.1 2023-08-22 15:13:46 +01:00
RiotRobot e5f480b032 Prepare changelog for v27.3.0-rc.1 2023-08-22 15:13:43 +01:00
Florian Duros 6bf4ed8672 ElementR: Add CryptoApi.requestVerificationDM (#3643)
* Add `CryptoApi.requestVerificationDM`

* Fix RoomMessageRequest url

* Review changes

* Merge fixes

* Add BOB test data

* `requestVerificationDM` test works against old crypto (encrypted verification request)

* Update test data
2023-08-21 14:48:32 +00:00
Valere c18d691ef5 RustCrypto | Implement keybackup loop (#3652)
* Implement `CryptoApi.checkKeyBackup`

* Deprecate `MatrixClient.enableKeyBackup`.

* fix integ test

* more tests

* Implement keybackup loop

* cleaning

* update matrix-sdk-crypto-wasm to 1.2.1

* fix lint

* avoid real timer stuff

* Simplify test

* post merge lint fix

* revert change on yarn.lock

* code review

* Generate test data for exported keys

* code review cleaning

* cleanup legacy backup loop

* Update spec/test-utils/test-data/generate-test-data.py

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update spec/test-utils/test-data/generate-test-data.py

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* update yarn.lock for new wasm bindings

---------

Co-authored-by: Richard van der Hoff <richard@matrix.org>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-08-17 14:10:57 +00:00
ElementRobot 97cf73bc52 Resetting package fields for development 2023-08-15 13:19:02 +01:00
ElementRobot aa25103665 Merge branch 'master' into develop 2023-08-15 13:18:59 +01:00
ElementRobot 858db67778 v27.2.0 2023-08-15 13:17:57 +01:00
ElementRobot e230abee45 Prepare changelog for v27.2.0 2023-08-15 13:17:55 +01:00
Travis Ralston 8c16d69f3c Set minimum supported Matrix 1.1 version (drop legacy r0 versions) (#3007)
Co-authored-by: Germain <germains@element.io>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-08-14 09:25:15 +01:00
Michael Telatynski 55b9116c99 Re-export localStorage-crypto-store (#3660) 2023-08-11 06:02:52 +00:00
Florian Duros 3a5d66057e ElementR: Process all verification events, not just requests (#3650)
* Process all verification event

* Add test for `isVerificationEvent`

* Review changes

* Remove null comparison and add doc to remote echo

* review changes
2023-08-09 14:14:58 +00:00
Richard van der Hoff 3f7af189e4 Implement CryptoApi.checkKeyBackupAndEnable (#3633)
* Implement `CryptoApi.checkKeyBackup`

* Deprecate `MatrixClient.enableKeyBackup`.

* fix integ test

* more tests

---------

Co-authored-by: valere <valeref@matrix.org>
2023-08-09 09:59:03 +00:00
renovate[bot] 16ddcb0ed0 Lock file maintenance (#3659)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-08 15:32:57 +00:00
renovate[bot] 9e35b8dd0a Update all non-major dependencies (#3656)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-08 15:09:57 +00:00
renovate[bot] bed787b749 Update tibdex/backport digest to 9565281 (#3658)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-08 14:14:28 +00:00
renovate[bot] d260b8be56 Update dependency eslint-config-prettier to v9 (#3657)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-08 14:10:12 +00:00
renovate[bot] 97991dad02 Update dependency @types/node to v18.17.3 (#3655)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-08 14:09:52 +00:00
renovate[bot] b8c19c47ab Update babel monorepo to v7.22.10 (#3654)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-08 14:07:45 +00:00
ElementRobot 1476ffbd15 v27.2.0-rc.1 2023-08-08 15:01:44 +01:00
ElementRobot 62f0a65472 Prepare changelog for v27.2.0-rc.1 2023-08-08 15:01:41 +01:00
Charly Nguyen 2ef7ae7661 Allow knocking rooms (#3647)
Signed-off-by: Charly Nguyen <charly.nguyen@nordeck.net>
2023-08-03 08:16:18 +00:00
Michael Telatynski 61c0a49971 Mark more logs as debug to tidy the console (#3645) 2023-08-02 20:40:32 +00:00
Michael Telatynski 2172f28888 Fix wrong handling of encrypted rooms when loading them from sync accumulator (#3640)
* Revert "Ensure we don't overinflate the total notification count (#3634)"

This reverts commit fd0c4a7f56.

* Fix wrong handling of encrypted rooms when loading them from sync accumulator

* Tidy up code, removing sections which didn't make any difference

* Add test
2023-08-02 09:53:34 +00:00
Florian Duros 2e9b34e0c3 Throw error if missing userId in CryptoApi.findVerificationRequestDMInProgress (#3641) 2023-08-01 12:20:01 +00:00
ElementRobot 5a782b7377 Resetting package fields for development 2023-08-01 12:28:37 +01:00
ElementRobot 54bc807056 Merge branch 'master' into develop 2023-08-01 12:28:33 +01:00
ElementRobot 9e07710d80 v27.1.0 2023-08-01 12:27:34 +01:00
ElementRobot e9ed91d800 Prepare changelog for v27.1.0 2023-08-01 12:27:31 +01:00
Michael Telatynski 88ba4fad71 Skip processing thread roots and fetching threads list when support is disabled (#3642)
* Skip processing thread roots and fetching threads list when support is disabled

* Enable threads support in tests
2023-07-31 18:16:42 +00:00
Michael Telatynski 21b3471453 Bump pagination limit to account for threaded events (#3638) 2023-07-31 16:59:55 +00:00
Florian Duros 0ada9803ab ElementR: Add CryptoApi.findVerificationRequestDMInProgress (#3601)
* Add `CryptoApi.findVerificationRequestDMInProgress`

* Fix linting and missing parameters

* Move `ROOM_ID` into `test-data`

* Remove verification request from `EventDecryptor` pending list

* Fix duplicate timeline event processing

* Add extra documentation

* Try to fix sonar error

* Use `roomId`

* Fix typo

* Review changes

* Review changes

* Fix `initRustCrypto` jsdoc

* Listen to `ClientEvent.Event` instead of `RoomEvent.Timeline`

* Fix missing room id in `generate-test-data.py`

* Review changes

* Review changes

* Handle encrypted event

* Fix linting

* Comments and run timers

* Ignore 404

* Fix test
2023-07-31 15:00:15 +00:00
Michael Telatynski 1744f0e97b Revert "Ensure we don't overinflate the total notification count (#3634)" (#3639) 2023-07-31 11:57:11 +01:00
Michael Telatynski fd0c4a7f56 Ensure we don't overinflate the total notification count (#3634)
* Ensure we don't overinflate the total notification count

By correctly comparing push rules before & after decryption

* DRY the code

* Testsssss

* Update tests
2023-07-28 15:05:11 +00:00
Michael Telatynski 615f7f9e72 Export more into the public interface (#3614) 2023-07-28 11:54:17 +00:00
renovate[bot] 77259e81c9 Update all non-major dependencies (#3630)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-28 10:55:22 +00:00
Richard van der Hoff 2193cd9d1c Implement CryptoApi.isKeyBackupTrusted (#3632)
* Implement `CryptoApi.isKeyBackupTrusted`

Fixes https://github.com/vector-im/crypto-internal/issues/110

* Bump matrix-sdk-crypto-wasm to v1.2.0

* Back out some changes

These are unneeded, and break backwards compat
2023-07-28 09:54:55 +00:00
Valere 6d28154dcd Add CryptoApi.getActiveSessionBackupVersion() (#3555)
* stub backupmanager

* Implement `CryptoApi.getActiveSessionBackupVersion`

* Revert unnecessary change

we can do this later, once we have better test coverage

* more test coverage

---------

Co-authored-by: Richard van der Hoff <richard@matrix.org>
2023-07-28 08:04:20 +00:00
Richard van der Hoff 83d447adfe Clean up megolm-backup integ test (#3631)
* Add `CryptoApi.setDeviceVerified`

I need a way to mark devices as trusted for the backup tests.

* More tests

* Simplify E2EKeyResponder.addDeviceKeys

The user and device IDs are in the test data, so no need to pass them in

* Clean up key backup integration test

Make it use the CryptoApi rather than legacy `MatrixClient.crypto`, and use a
pre-signed backup instead of requiring a "blindlySignAnything" method.

* run megolm-backup tests on both crypto stacks

* avoid internal backupManager
2023-07-27 15:23:02 +00:00
Richard van der Hoff 73c9f4e322 Add CryptoApi.setDeviceVerified (#3624)
I need a way to mark devices as trusted for the backup tests.
2023-07-27 13:16:10 +01:00
renovate[bot] e6fa4cdb3c Lock file maintenance (#3629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-27 09:45:53 +00:00
Michael Telatynski a04653a72c Upgrade matrix-widget-api (#3621)
* Fix threads ending up with chunks of their timelines missing

* delint

* Upgrade matrix-widget-api
2023-07-27 09:21:14 +00:00
renovate[bot] 5f9341f39c Update dependency eslint-plugin-unicorn to v48 (#3628)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-27 08:37:46 +00:00
renovate[bot] 906946c419 Update all non-major dependencies (#3626)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-27 08:37:23 +00:00
renovate[bot] 4397b9d640 Update dependency @types/node to v18.17.0 (#3627)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-27 08:37:02 +00:00
ElementRobot 90da2cf439 v27.1.0-rc.1 2023-07-27 09:28:56 +01:00
ElementRobot 6edd45787b Prepare changelog for v27.1.0-rc.1 2023-07-27 09:28:52 +01:00
Florian Duros 84444ec11e ElementR: Add CryptoApi.getCrossSigningKeyId (#3619)
* Add `CryptoApi.getCrossSigningKeyId`

* Rename `CrossSigningPubKey` to `CrossSigningKeyInfo`

* Remove old eslint disable

* Review changes

* Review changes
2023-07-26 16:09:49 +00:00
Richard van der Hoff 0e95df5dba Element-R: implement {get,store}SessionBackupPrivateKey (#3622) 2023-07-26 12:00:43 +01:00
Valere 29b815b678 Replace deprecated TestClient with fetchMock (#3550)
* replace deprecated TestClient with fetchMock

* add stop() api to BackupManager for clean shutdown

* fix merge

* code review cleaning

* lint

* Address review comments

* Remove unused `TestClient.expectKeyBackupQuery`

* clean up imports

---------

Co-authored-by: Richard van der Hoff <richard@matrix.org>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-07-25 20:42:41 +00:00
Richard van der Hoff 0cf056958b Fix broken unit tests for FetchHttpApi.getUrl (#3620)
These tests have broken on Node.js 18.17.0.

This is due to Node.js adopting an updated version of the URL parser, in which
the internal `Symbol(query)` property is populated lazily.

We shouldn't be relying on the internal state of the URL object anyway. Let's
just compare the stringified copy.
2023-07-25 19:48:15 +00:00
Florian Duros 79d4113a6b ElementR: Stub CheckOwnCrossSigningTrust, import cross signing keys and verify local device in bootstrapCrossSigning (#3608) 2023-07-25 18:03:43 +01:00
Michael Telatynski 8a80886358 Fix threads ending up with chunks of their timelines missing (#3618)
* Fix threads ending up with chunks of their timelines missing

* delint
2023-07-25 15:28:52 +00:00
Michael Telatynski de7959de6c Ensure we do not clobber a newer RR with an older unthreaded one (#3617)
* Ensure we do not clobber a newer RR with an older unthreaded one

or vice versa

* Fix test
2023-07-24 16:35:56 +00:00
Michael Telatynski 533c21a515 Fix registration check your emails stage regression (#3616)
* Fix registration check your emails stage regression

* Simplify diff

* Add test
2023-07-24 14:08:17 +00:00
Michael Telatynski 6b018b6927 Fix how Room::eventShouldLiveIn handles replies to unknown parents (#3615)
* Add warning

* Fix how Room::eventShouldLiveIn handles replies to unknown parents
2023-07-24 07:37:28 +00:00
Michael Telatynski 38c3abb364 Update downstream-artifacts.yml (#3613) 2023-07-20 21:43:49 +00:00
Michael Telatynski a47f319665 Only send threaded read receipts if threads support is enabled (#3612)
* Only send threaded read receipts if threads support is enabled

* Tests
2023-07-20 15:44:52 +00:00
Florian Duros ecef9fd755 Fix CryptoApi#getVerificationRequestsToDeviceInProgress (#3611) 2023-07-20 09:46:55 +00:00
Richard van der Hoff 7dffd8ffd3 Make sure to drop references to user device lists (#3610)
Empirically, this seems to fix some problems with leaking references to
IndexedDB.
2023-07-20 08:47:30 +00:00
Michael Telatynski 66492e7ba8 Fix edge cases around non-thread relations to thread roots and read receipts (#3607)
* Ensure non-thread relations to a thread root are actually in both timelines

* Make thread in sendReceipt & sendReadReceipt explicit rather than guessing it

* Apply suggestions from code review

* Fix Room::eventShouldLiveIn to better match Synapse to diverging ideas of notifications

* Update read receipt sending behaviour to align with Synapse

* Fix tests

* Fix thread rel type
2023-07-19 11:21:50 +00:00
Michael Telatynski 43b2404865 Specify /preview_url requests as low priority (#3609)
* Specify /preview_url requests as low priority

* Update src/@types/global.d.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Simplify interface

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-07-19 09:29:41 +00:00
Michael Telatynski fed9910fa1 Remove unused better-docs & docdash (#3605) 2023-07-19 07:44:11 +00:00
RiotRobot f77662406c Resetting package fields for development 2023-07-18 12:53:43 +01:00
RiotRobot 8cc0cf1a70 Merge branch 'master' into develop 2023-07-18 12:53:38 +01:00
RiotRobot dfa2429094 v27.0.0 2023-07-18 12:52:00 +01:00
RiotRobot 3e2460707c Prepare changelog for v27.0.0 2023-07-18 12:51:57 +01:00
Michael Telatynski 706c084fa7 Add tests for room-hierarchy (#3606)
* Add tests for room-hierarchy

* overwriteroutes
2023-07-18 10:37:17 +00:00
renovate[bot] eb7faa6c07 Lock file maintenance (#3604)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-17 17:59:37 +00:00
RiotRobot d45a0b894a v27.0.0-rc.2 2023-07-14 16:10:12 +01:00
RiotRobot 102739e0fb Prepare changelog for v27.0.0-rc.2 2023-07-14 16:10:09 +01:00
Andy Balaam 0d7e4a0fa5 Merge pull request #3603 from matrix-org/backport-3600-to-staging
[Backport staging] Fix read receipt sending behaviour around thread roots
2023-07-14 16:03:41 +01:00
Michael Telatynski d4628e78d4 Fix read receipt sending behaviour around thread roots (#3600)
* Fix read receipt sending behaviour around thread roots

* Update src/client.ts

Co-authored-by: Eric Eastwood <erice@element.io>

---------

Co-authored-by: Eric Eastwood <erice@element.io>
(cherry picked from commit b05f933d83)
2023-07-14 15:01:58 +00:00
Richard van der Hoff 0b193f4665 matrix-sdk-crypto-js -> matrix-sdk-crypto-wasm (#3602)
We've renamed matrix-sdk-crypto-js and released a v1.0.
2023-07-13 17:11:57 +00:00
Eric Eastwood 8ef2e848b9 Log query parameters on HTTP requests (#3591)
* Log query parameters on HTTP requests

Follow-up to https://github.com/matrix-org/matrix-js-sdk/pull/3485

* Only stringify once

See https://github.com/matrix-org/matrix-js-sdk/pull/3591#discussion_r1261300323
2023-07-13 13:07:01 +00:00
Richard van der Hoff d92936fba5 Element-R: support for displaying QR codes during verification (#3588)
* Support for showing QR codes

* Emit `VerificationRequestEvent.Change` events when the verifier changes

* Minor integ test tweaks

* Handle transitions from QR code display to SAS

* Fix naming

* Add a test for `ShowQrCodeCallbacks.cancel`
2023-07-13 11:11:13 +00:00
Michael Telatynski f005984df3 Export typed event emitter key types (#3597)
* Export typed event emitter key types

* Update src/matrix.ts
2023-07-13 11:10:24 +00:00
Richard van der Hoff 13fec49e74 Element-R: ensure that userHasCrossSigningKeys uses up-to-date data (#3599)
* Element-R: ensure that `userHasCrossSigningKeys` uses up-to-date data

* Bump matrix-sdk-crypto-js
2023-07-13 10:46:56 +00:00
renovate[bot] 008294cfc6 Update babel monorepo to v7.22.9 (#3434)
* Update babel monorepo to v7.22.9

* Make babel happier

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-07-13 09:41:03 +00:00
Michael Telatynski b05f933d83 Fix read receipt sending behaviour around thread roots (#3600)
* Fix read receipt sending behaviour around thread roots

* Update src/client.ts

Co-authored-by: Eric Eastwood <erice@element.io>

---------

Co-authored-by: Eric Eastwood <erice@element.io>
2023-07-13 08:25:23 +00:00
Michael Telatynski b186d79dde Fix jest/valid-expects lints (#3586) 2023-07-12 17:11:52 +00:00
Michael Telatynski e82b5fe1db Fix types in getSessionBackupPrivateKey (#3595)
* Fix type issue around `getSessionBackupPrivateKey`

* Fix sending auth: null due to broken types around UIA

* Discard changes to src/crypto/index.ts

* Add comment

* Fix types

* Fix types for MatrixClient::addThreePid

* Iterate
2023-07-12 14:38:14 +00:00
Michael Telatynski 9602aa88ea Fix sending auth: null due to broken types around UIA (#3594)
* Fix type issue around `getSessionBackupPrivateKey`

* Fix sending auth: null due to broken types around UIA

* Discard changes to src/crypto/index.ts

* Add comment
2023-07-12 13:55:02 +00:00
renovate[bot] 0fb3dc1b13 Update typescript-eslint monorepo to v5.62.0 (#3583)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-12 11:01:00 +00:00
renovate[bot] aeede332be Update dependency @types/jest to v29.5.3 (#3582)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-12 10:59:05 +00:00
renovate[bot] b052950a19 Update JS-DevTools/npm-publish action to v2.2.1 (#3581)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-12 10:57:45 +00:00
Michael Telatynski 1cb5fff5a1 Improve types (#3589)
* Improve types

* Improve coverage
2023-07-12 10:39:33 +00:00
Michael Telatynski 01226e41d9 Fix broken DeviceList.spec.ts test (#3590) 2023-07-12 10:01:57 +00:00
dependabot[bot] e3919fd93b Bump semver from 5.7.1 to 5.7.2 (#3575)
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kerry <kerrya@element.io>
2023-07-12 02:20:47 +00:00
Kerry 3b88ea19b7 Stabilize support for MSC3952: intentional mentions (#3397)
* use stable identifiers for MSC3952: intentional mentions

* add matrix version to feature support for intentional mentions
2023-07-11 22:04:06 +00:00
Richard van der Hoff dcf26f3e48 bump rust-sdk (#3587) 2023-07-11 17:40:39 +00:00
Valere 3385adf5f6 Improve logging of http requests to aid debugging (#3485)
* Simple request logging with status and duration

* remove url params from logs

* superfluous toString()

* Add tests

* Apply suggestions from code review

* update snapshots

* update log format

* Apply suggestions from code review

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* update snapshot

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <richard@matrix.org>
2023-07-11 17:27:42 +00:00
Richard van der Hoff 9db6ce107a Add support for scanning QR codes during verification, with Rust crypto (#3565)
* Offer `m.qr_code.scan.v1` verification method by default

Normally, the application specifies the supported verification methods when
creating the MatrixClient (and matrix-react-sdk does so). If the application
leaves it unset, then the idea is that the js-sdk offers all known verification
methods.

However, by default, the rust-sdk doesn't specify `m.qr_code.scan.v1`. So
basically, we need to set our own list of supported methods, rather than
relying on the rust-sdk's defaults.

* Factor out base class from `RustSASVerifier`

* Implement QR code scanning

* Update src/rust-crypto/verification.ts
2023-07-11 16:00:59 +00:00
Florian Duros d5b22e1deb Use cryptoBackend in client.ts for new rust-crypto implementation (#3576)
* Use `cryptoBackend` in `client.ts` for new rust-crypto implementation for backward compatibility

* Use `cryptoBackend` in `client.ts` for new rust-crypto implementation for backward compatibility
2023-07-11 14:13:53 +00:00
Richard van der Hoff a5e606a1e7 Mark all the rust crypto stuff internal (#3574)
... for the avoidance of doubt.
2023-07-11 14:11:35 +00:00
Michael Telatynski f2471b6dbd Add methods to influence set_presence on /sync API calls (#3578)
* Add methods to influence set_presence on /sync API calls

* Tweak comment

* Improve coverage
2023-07-11 13:31:12 +00:00
RiotRobot dcf71e0c8f v27.0.0-rc.1 2023-07-11 13:37:19 +01:00
RiotRobot 77267e393c Prepare changelog for v27.0.0-rc.1 2023-07-11 13:37:16 +01:00
Michael Telatynski 1fdc0af5b7 Throw saner error when peeking has its room pulled out from under it (#3577) 2023-07-11 10:24:57 +00:00
Michael Telatynski d2b782a2f5 Simplify MatrixClient::setPowerLevel API (#3570)
* Simplify `MatrixClient::setPowerLevel` API

While making it more resilient to causing issues like nuking room state

* Handle edge case

* Fix tests

* Add test coverage
2023-07-11 07:26:30 +00:00
Kerry 5df4ebaada OIDC: Log in (#3554)
* use oidc-client-ts during oidc discovery

* export new type for auth config

* deprecate generateAuthorizationUrl in favour of generateOidcAuthorizationUrl

* testing util for oidc configurations

* test generateOidcAuthorizationUrl

* lint

* test discovery

* dont pass whole client wellknown to oidc validation funcs

* add nonce

* use oidc-client-ts for oidc response

* validate user state and update tests

* use oidc-client-ts for code exchange

* use oidc-client-ts in completing auth grant

* use client userState for homeserver

* more comments
2023-07-11 02:20:19 +00:00
renovate[bot] e68a1471c1 Update all non-major dependencies (#3564)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-10 14:38:14 +00:00
Richard van der Hoff e42dd74426 Add async method for generating a QR code (#3562)
The api to generate a QR code is async in rust, and the easiest way to deal
with it is to make a new method.
2023-07-10 14:22:10 +01:00
renovate[bot] 2751e191d3 Lock file maintenance (#3392)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-10 10:08:16 +00:00
Michael Telatynski b5b86bf1b5 Fix TypedEventEmitter::removeAllListeners(void) not working (#3561) 2023-07-10 10:04:38 +00:00
dependabot[bot] 4990bf5ca0 Bump tough-cookie from 4.1.2 to 4.1.3 (#3560)
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.1.2...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-10 08:24:08 +00:00
Kerry b8fa030d5d OIDC: use oidc-client-ts (#3544)
* use oidc-client-ts during oidc discovery

* export new type for auth config

* deprecate generateAuthorizationUrl in favour of generateOidcAuthorizationUrl

* testing util for oidc configurations

* test generateOidcAuthorizationUrl

* lint

* test discovery

* dont pass whole client wellknown to oidc validation funcs

* add nonce

* use client userState for homeserver
2023-07-09 21:19:32 +00:00
Hubert Chathi b606d1e54b Don't allow Olm unwedging rate-limiting to race (#3549)
* don't allow Olm unwedging rate-limiting to race

* apply changes from code review
2023-07-07 10:43:25 +00:00
Michael Telatynski cd7c519dc4 Prevent threads code from making identical simultaneous API hits (#3541) 2023-07-07 09:48:09 +01:00
Michael Telatynski 30dd28960c Update IUnsigned type to be extensible (#3547) 2023-07-07 07:43:17 +00:00
Valere 5b635df08d add stop() api to BackupManager for clean shutdown (#3553) 2023-07-06 16:43:47 +00:00
Florian Duros 592c497902 Upgrade @matrix-org/matrix-sdk-crypto-js to ^0.1.1 (#3552) 2023-07-06 16:00:36 +00:00
Richard van der Hoff 8e3f2f3262 Log message ID for undecryptable to-device messages (#3543)
... to help with debugging.
2023-07-06 06:09:39 +00:00
ElementRobot 5751df1288 Resetting package fields for development 2023-07-04 15:08:16 +01:00
ElementRobot 40a71101e2 Merge branch 'master' into develop 2023-07-04 15:08:12 +01:00
ElementRobot 3f095caf2d v26.2.0 2023-07-04 15:07:04 +01:00
ElementRobot 12a94bdd94 Prepare changelog for v26.2.0 2023-07-04 15:07:02 +01:00
Michael Telatynski 1c1ac137d3 Improve types around login, registration, UIA and identity servers (#3537) 2023-07-04 14:49:24 +01:00
Michael Telatynski 89cabc4912 Ignore thread relations on state events for consistency with edits (#3540)
* Ignore thread relations on state events for consistency with edits

* Add test
2023-07-04 12:07:49 +00:00
Erik Johnston 5be4548b3d Fix an instance of failed to decrypt error when an in flight /keys/query fails. (#3486)
* Fix an instance of failed to decrypt error

Specifically, when checking the event sender matches who sent us the
session keys we skip waiting for pending device list updates if we
already know who owns the session key.

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/crypto/algorithms/olm.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Fix line wrapping

* Update src/crypto/algorithms/olm.ts

* Fix null check

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-07-04 11:31:03 +00:00
Kerry 09de76bd43 OIDC: validate id token (#3531)
* validate id token

* comments

* tidy comments
2023-07-03 21:12:15 +00:00
Richard van der Hoff 3a694f4998 Element-R: Implement VerificationRequest.{timeout,pending} (#3532)
* implement `VerificationRequest.pending`

* Implement `VerificationRequest.timeout`

* Rust crypto: allow using a memory store (#3536)

* Rust crypto: allow using a memory store

It turns out that, for some usecases (in particular, "bot users" for cypress
tests), we don't need persistent storage and an in-memory store will be fine.

* Rust crypto: use a memory store for the unit tests
2023-07-03 11:27:38 +00:00
Richard van der Hoff 3a8a1389f5 Element-R: Implement VerificationRequest.accept (#3526)
* Pass `supportedVerificationMethods` into `VerificationRequest`

... so that the application can later call `accept()` and we know what to send.

* Implement `VerificationRequest.accept`

* Implement `VerificationRequest.declining`

* Update src/rust-crypto/verification.ts
2023-07-03 11:02:19 +00:00
Richard van der Hoff c271e1533a Use the right anchor emoji for SAS verification (#3534)
Currently, the anchor emoji has a ["Variation
Selector-16"](https://codepoints.net/U+FE0F) (U+FE0F) character after it.

The unicode specs do define U+2694 U+FE0F as a valid sequence (with suggested
rendering https://www.unicode.org/cgi-bin/varglyph?24-2693-FE0F), but our spec
spec doesn't include the variation selector, and the difference means that my
cypress tests (which attempt a verification between Element-R and unrusty
Element Web) fail intermittently.

Something of a follow-up to
https://github.com/matrix-org/matrix-js-sdk/pull/3523, but hopefully this will
be the last, because I have regenerated the whole list from the spec data.
2023-06-29 22:05:57 +00:00
Richard van der Hoff 722debe8f9 Drop support for Node 16 (#3533)
* Stop running tests on Node 16

* update package.json
2023-06-29 16:36:09 +00:00
Richard van der Hoff 5165899e82 Element-R: support for starting a SAS verification (#3528)
* integ tests: factor out some utility methods

* Add `VerificationRequest.startVerification` to replace `beginKeyVerification`

The rust SDK ties together creating the verifier and sending the
`m.key.verification.start` message, so we need to combine
`.beginKeyVerification` and `.verify`.

* add some unit tests
2023-06-29 16:34:49 +00:00
Richard van der Hoff 1828826661 QRCode: fix breakage on node 16 (#3527)
[`crypto.getRandomValues`](https://nodejs.org/docs/latest-v18.x/api/crypto.html#cryptogetrandomvaluestypedarray)
was added to the nodejs library in node 17. However, it was actually available
in node 16, hiding under
[`crypto.webcrypto`](https://nodejs.org/docs/latest-v16.x/api/webcrypto.html#cryptogetrandomvaluestypedarray). We
have some shims in `src/crypto/crypto.ts`, so let's use them.

All of this means that we don't need to monkey-patch `crypto` to run the tests
on node 16.
2023-06-29 09:58:38 +00:00
Richard van der Hoff 24cee68fa2 Rust crypto: emit VerificationRequestReceived events (#3525) 2023-06-28 14:32:27 +00:00
Richard van der Hoff e645af1fc5 use the right smiley in emoji verification (#3523) 2023-06-28 10:01:46 +00:00
Michael Telatynski de64779c27 Improve types to match reality (#3510) 2023-06-28 09:06:10 +00:00
Richard van der Hoff acbcb4658a Force coloured output from jest (#3521) 2023-06-27 15:14:47 +00:00
renovate[bot] 815484b543 Update dependency eslint-plugin-jsdoc to v46.3.0 (#3520)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-27 12:40:26 +00:00
renovate[bot] 5a3d1a2a67 Update typescript-eslint monorepo to v5.60.0 (#3519)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-06-27 12:37:30 +00:00
Richard van der Hoff 18626169e4 Create a new event type for verification requests (#3514)
* More slow test fixes

* Create a new event type for verification requests

Previous PRs (https://github.com/matrix-org/matrix-js-sdk/pull/3449, etc) have
pulled out an interface from the `VerificationRequest` class, but applications
registering for the `CryptoEvent.VerificationRequest` event could still be
expecting a fully-fledged class rather than the interface.

To handle this without breaking backwards compat, add a new event type that
carries the interface, not the class.
2023-06-27 11:24:29 +00:00
renovate[bot] e4a9f958a0 Update peter-evans/create-pull-request digest to 1534078 (#3518)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-27 11:11:12 +00:00
Michael Telatynski ff29de743c Update README.md 2023-06-27 12:38:58 +01:00
renovate[bot] 5a68861418 Update all non-major dependencies (#3513)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-27 11:00:37 +00:00
ElementRobot e285932776 v26.2.0-rc.1 2023-06-27 11:57:18 +01:00
ElementRobot 2af0706b16 Prepare changelog for v26.2.0-rc.1 2023-06-27 11:57:15 +01:00
Richard van der Hoff 4382d2a425 Increase another crypto test timeout (#3509)
Followup to https://github.com/matrix-org/matrix-js-sdk/pull/3500: increase the
timeout for another test which is also timing out.
2023-06-27 10:06:48 +00:00
Kerry 9de4a057df OIDC: navigate to authorization endpoint (#3499)
* utils for authorization step in OIDC code grant

* tidy

* completeAuthorizationCodeGrant util functions

* response_mode=query

* add scope to bearertoken type

* add is_guest to whoami response type

* doc comments

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* use shimmed TextEncoder

* fetchMockJest -> fetchMock

* comment

* bearertokenresponse

* test for lowercase bearer

* handle lowercase token_type

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-06-26 23:46:53 +00:00
Richard van der Hoff b703d4a2cc More slow test fixes (#3515)
We still seem to be suffering test timeouts. Hopefully this will fix the integ tests, where #3509 etc fix the unit tests.
2023-06-26 22:15:56 +01:00
Richard van der Hoff d1dec4cd08 Implement VerificationRequest.cancel (#3505) 2023-06-26 16:56:57 +00:00
Richard van der Hoff 326a13bcfe Rearrange the verification integration tests, again (#3504)
* Element-R: Implement `CryptoApi.getVerificationRequestsToDeviceInProgress`

* Element-R: Implement `requestOwnUserVerification`

* init aliceClient *after* the fetch interceptors

* Initialise the test client separately for each test

* Avoid running all the tests twice

Currently all of these tests are running twice, with different client
configurations. That's not really adding much value; we just need to run
specific tests that way.

* Factor out functions for building responses
2023-06-26 14:44:42 +00:00
renovate[bot] e8fb47fdca Update all non-major dependencies (#3467)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-26 14:30:06 +00:00
Richard van der Hoff bd66e3859d Element R: Implement requestOwnUserVerification (#3508)
Part of https://github.com/vector-im/element-web/issues/25319.
2023-06-26 15:17:35 +01:00
Richard van der Hoff 96e484a3fe Element-R: implement CryptoApi.getVerificationRequestsToDeviceInProgress (#3497)
* Element-R: Implement `CryptoApi.getVerificationRequestsToDeviceInProgress`

* Element-R: Implement `requestOwnUserVerification` (#3503)

* Revert "Element-R: Implement `requestOwnUserVerification` (#3503)"

This reverts commit 8da756503c3d72b8ecbf50b4c2cf807ac36229aa.

oops, merged too soon
2023-06-26 13:31:35 +00:00
Richard van der Hoff 3e646bdfa0 Bump version of the react-sdk cypress workflow file (#3501)
`cypress.yaml` is currently pinned to an old version of the react-sdk, meaning
that each attempt to run it is currently failing with an error.

(Introduced by https://github.com/matrix-org/matrix-js-sdk/pull/3480)
2023-06-26 09:09:20 +00:00
Richard van der Hoff 48c4127035 Element-R: Basic implementation of SAS verification (#3490)
* Return uploaded keys from `/keys/query`

* Basic implementation of SAS verification in Rust

* Update the `verifier` *before* emitting `erificationRequestEvent.Change`

* remove dead code
2023-06-26 08:48:44 +00:00
Michael Telatynski f16a6bc654 Aggregate relations regardless of whether event fits into the timeline (#3496) 2023-06-26 09:39:25 +01:00
Richard van der Hoff f884c78579 Improve integration test for interactive verification (#3495)
* Tweaks to the integ test to conform to the spec

Rust is a bit more insistent than legacy crypto...

* Improve documentation on request*Verification

* Check more things in the integration test

* Create an E2EKeyResponder

* Test verification with custom method list

* Add a test for SAS cancellation

* Update spec/integ/crypto/verification.spec.ts
2023-06-23 14:38:38 +00:00
Florian Duros 3c59476cf7 Element-R: Store cross signing keys in secret storage (#3498)
* Store cross signing keys in secret storage

* Update `bootstrapSecretStorage` doc

* Throw error when `createSecretStorageKey` is not set

* Move mocking functions

* Store cross signing keys and user signing keys

* Fix `awaitCrossSigningKeyUpload` documentation

* Remove useless comment

* Fix formatting after merge conflict
2023-06-23 13:10:54 +00:00
Richard van der Hoff c8f6c4dd0d Increase crypto test timeout (#3500)
For some reason, some tests seem to be timing out in GHA. Let's try bumping up
the timeout.
2023-06-23 12:32:56 +00:00
Richard van der Hoff e8c89e9977 Element-R: speed up slow unit test (#3492)
A couple of tests were waiting for a request that wasn't happening, so timing
out after 1.5 seconds. Let's avoid the extra slowth.

(This was introduced by changes in
https://github.com/matrix-org/matrix-js-sdk/pull/3487, but the changes in this
PR do no harm anyway)
2023-06-22 09:43:39 +00:00
Kerry df78d7cf67 OIDC: add dynamic client registration util function (#3481)
* rename OidcDiscoveryError to OidcError

* oidc client registration functions

* test registerOidcClient

* tidy test file

* reexport OidcDiscoveryError for backwards compatibility
2023-06-21 21:55:25 +00:00
Michael Telatynski 80fec814a2 Add getLastUnthreadedReceiptFor utility to Thread delegating to the underlying Room (#3493) 2023-06-21 12:54:30 +01:00
Michael Telatynski 8b9672ba43 Add debug logging to figure out missing reactions in main timeline (#3494)
* Fix debug logging not working

* Add debug logging to figure out missing reactions in main timeline
2023-06-20 15:28:02 +00:00
David Baker ca00094e67 Fix bug where switching media caused media in subsequent calls to fail (#3489) 2023-06-20 15:16:43 +00:00
Richard van der Hoff 9c6d5a6c55 Element-R: wait for OlmMachine on startup (#3487)
* Element-R: wait for OlmMachine on startup

Previously, if you called `CryptoApi.getUserDeviceInfo()` before the first
`/sync` request happened, it would return an empty list, which made a bunch of
the tests racy. Add a hack to get the OlmMachine to think about its device
lists during startup.

* add a test
2023-06-20 09:29:41 +00:00
RiotRobot b77fe465f7 Resetting package fields for development 2023-06-20 10:14:37 +01:00
RiotRobot 8d93f49443 Merge branch 'master' into develop 2023-06-20 10:14:32 +01:00
RiotRobot f03fbed668 v26.1.0 2023-06-20 10:12:27 +01:00
RiotRobot c84efb57ef Prepare changelog for v26.1.0 2023-06-20 10:12:24 +01:00
Florian Duros 49f11578f7 ElementR: Add CryptoApi#bootstrapSecretStorage (#3483)
* Add WIP bootstrapSecretStorage

* Add new test if `createSecretStorageKey` is not set

* Remove old comments

* Add docs for `crypto-api.bootstrapSecretStorage`

* Remove default parameter for `createSecretStorageKey`

* Move `bootstrapSecretStorage` next to `isSecretStorageReady`

* Deprecate `bootstrapSecretStorage` in `MatrixClient`

* Update documentations

* Raise error if missing `keyInfo`

* Update behavior around `setupNewSecretStorage`

* Move `ICreateSecretStorageOpts` to `rust-crypto`

* Move `ICryptoCallbacks` to `rust-crypto`

* Update `bootstrapSecretStorage` documentation

* Add partial `CryptoCallbacks` documentation

* Fix typo

* Review changes

* Review changes
2023-06-20 08:40:11 +00:00
Richard van der Hoff 8df4be0939 Fix order of things in crypto-api.ts (#3491)
* `CryptoApi` should be first
* `export *` should be last
* everything else in the middle
2023-06-20 08:12:33 +00:00
Richard van der Hoff 80cdbe1058 Element-R: implement userHasCrossSigningKeys (#3488) 2023-06-19 21:11:04 +00:00
Michael Telatynski 9c62d15447 Specify git tags for cypress workflow so updating tests is gated by renovate PRs (#3480) 2023-06-19 07:38:24 +00:00
renovate[bot] afc70528cc Update definitelyTyped (#3465)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-16 02:35:02 +00:00
Kerry f938d10f7b remove polls from room state on redaction (#3475) 2023-06-15 22:12:03 +00:00
Richard van der Hoff 22f0b781ea Add new methods for verification to CryptoApi (#3474)
* Add accessors for verification requests to CryptoApi

Part of https://github.com/vector-im/crypto-internal/issues/97

* Add new methods for verification to `CryptoApi` and deprecate old method

https://github.com/vector-im/crypto-internal/issues/98
2023-06-15 14:56:50 +01:00
Florian Duros 1bae10c4b2 Fix export type GeneratedSecretStorageKey (#3479) 2023-06-15 12:46:42 +00:00
Michael Telatynski 9b5b533c6f Close IDB database before deleting it to prevent spurious unexpected close errors (#3478) 2023-06-15 12:25:29 +00:00
Richard van der Hoff c425945353 Avoid deprecated classes in verification integ test (#3473)
https://github.com/matrix-org/matrix-js-sdk/pull/3449 deprecated a bunch of
exports from `src/crypto/verification/request/VerificationRequest`. Let's stop
using them in the integration test.
2023-06-15 11:24:01 +00:00
Florian Duros 0545f6df09 ElementR: Add rust-crypto#createRecoveryKeyFromPassphrase implementation (#3472)
* Add `rust-crypto#createRecoveryKeyFromPassphrase` implementation

* Use `crypto`

* Rename `IRecoveryKey` into `GeneratedSecretStorageKey` for rust crypto

* Improve comments

* Improve `createRecoveryKeyFromPassphrase`
2023-06-14 14:38:43 +00:00
renovate[bot] d14fc426e6 Update dependency eslint-plugin-jsdoc to v46 (#3468)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-14 10:49:28 +00:00
Michael Cousins 9f30defcd1 Upgrade JS-DevTools/npm-publish to v2 (#3456)
- Upgrade JS-DevTools/npm-publish to v2.2.0
- Remove workaround for bug JS-DevTools/npm-publish#15
- Remove usage of `jq` in favor of npm-publish output

Signed-off-by: Michael Cousins <michael@cousins.io>
2023-06-14 08:02:48 +00:00
renovate[bot] 9b175a4985 Update typescript-eslint monorepo to v5.59.9 (#3466)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-13 20:52:40 +00:00
Richard van der Hoff 826ea5bc58 Pull out a new VerificationRequest interface (#3449)
* add a test for incoming verification requests

* Move `VerificationRequestEvent` to crypto-api

* Move `VerificationPhase` to `crypto-api`

* Define `VerificationRequest` interface

* Implement `canAcceptVerificationRequest`
2023-06-13 11:55:37 +00:00
RiotRobot 7b316613f6 v26.1.0-rc1 2023-06-13 12:15:32 +01:00
RiotRobot 387b3485ae Prepare changelog for v26.1.0-rc1 2023-06-13 12:15:29 +01:00
Valere 9f6073478f Element-R: support for manual import/export of Room keys (#3364)
* Rust manual import/export for keys

* code review

* code review

* post merge fix

* code review

* doc: comma splice

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Better test name

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* quick doc update

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-06-13 10:28:35 +00:00
Florian Duros b47c87f909 Add rust-crypto#isCrossSigningReady implementation (#3462) 2023-06-12 14:00:31 +00:00
Kerry c66850e897 OIDC: Validate m.authentication configuration (#3419)
* validate m.authentication, fetch issuer wellknown

* move validation functions into separate file

* test validateWellKnownAuthentication

* test validateOIDCIssuerWellKnown

* add authentication cases to autodiscovery tests

* test invalid authentication config on wk

* improve types

* test case for account:false

* use hasOwnProperty in validateWellKnownAuthentication

* comments

* make registration_endpoint optional
2023-06-11 21:32:44 +00:00
Florian Duros 2766146c49 ElementR: Add CryptoApi.getCrossSigningStatus (#3452)
* Add `crypto.getCrossSigningStatus`

* Fix imports and boolean casting

* Moved `isStoredInSecretStorage` into a single function

* Review changes `CrossSigningStatus`

* Review changes for `cross-signing.spec.ts`

* Add test in case when cross signing is not setup

* Handle when the `crossSigningStatus` returned by the olmMachine is null

* Review changes for `crypto-api` documentation

* Update `cross-signing.spec.ts` according to review changes

* Moved and renamed `isStoredInSecretStorage`

* Remove noise in `CrossSigning.ts` imports

* Fix `returns` sentence in `secretStorageContainsCrossSigningKeys`

* Fix typos

* Add test for `secret-storage.ts`

* Improve documentation

* Add doc about fetch mock request name
2023-06-09 13:15:01 +00:00
Richard van der Hoff 61497c9a8f Pass Kiwi secrets to react-sdk Cypress build (#3461) 2023-06-09 11:03:33 +00:00
Richard van der Hoff 5981feeb44 Element-R: Pull out an interface from VerificationBase (#3414)
* pull out `Verifier` interface

* Mark old classes as deprecated

* Update integration tests to use new interface
2023-06-09 10:09:31 +00:00
Michael Telatynski 51218ddc1d Fix thread list being ordered based on all updates (#3458)
* Add test for thread list stability around non-reply updates

* Fix thread list being ordered based on all updates

* Fix test

* Update spec/integ/matrix-client-event-timeline.spec.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Iterate

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-06-09 08:29:21 +00:00
RiotRobot b907433d38 Merge branch 'master' into develop 2023-06-09 09:27:19 +01:00
RiotRobot 32c0b81332 v26.0.1 2023-06-09 09:25:30 +01:00
RiotRobot 634b8ebbb4 Prepare changelog for v26.0.1 2023-06-09 09:25:28 +01:00
Andy Balaam 4ab8066e1f Merge pull request #3460 from matrix-org/backport-3455-to-staging
[Backport staging] Fix: handle `baseUrl` with trailing slash in `fetch.getUrl`
2023-06-09 09:19:58 +01:00
Kerry 13dccb3d71 Fix: handle baseUrl with trailing slash in fetch.getUrl (#3455)
* tests

* tidy trailing slash in fetch.getUrl before forming url

* make sonar happy about Polynomial regular expression used on uncontrolled data

(cherry picked from commit ef1f5bf232)
2023-06-09 08:11:56 +00:00
Kerry ef1f5bf232 Fix: handle baseUrl with trailing slash in fetch.getUrl (#3455)
* tests

* tidy trailing slash in fetch.getUrl before forming url

* make sonar happy about Polynomial regular expression used on uncontrolled data
2023-06-08 21:36:34 +00:00
Florian Duros a03e3dd501 Update @matrix-org/matrix-sdk-crypto-js to 0.1.0-alpha.10 (#3453) 2023-06-07 14:10:38 +00:00
Enrico Schwendig 3cfad3cdeb Expand webrtc stats with connection and call feed track information (#3421)
* Refactor names in webrtc stats

* Refactor summary stats reporter to gatherer

* Add call and opponent member id to call stats reports

* Update opponent member when we know them

* Add missing return type

* remove async in test

* add call feed webrtc report

* add logger for error case in stats gathering

* gather connection track report

* expand call feed stats with call feed

* formation code and fix lint issues

* clean up new track stats

 * set label for call feed stats and
 * remove stream in track stats
 * transceiver stats based on mid
 * call feed stats based on stream id
 * fix lint and test issues

* Fix merge issues

* Add test for expanding call feed stats in group call

* Fix export issue from prv PR

* explain test data and fixed some linter issues

* convert tests to snapshot tests
2023-06-07 14:05:51 +00:00
Timo 60c715d5df Extend stats summary with call device and user count based on room state (#3424)
* send expected peer connections to posthog.
(based on roomState event)

* add tests

* change GroupCallStats initialized

* prettier

* more test and catch for promise

* seperate the participant logic in a summary extend function

Signed-off-by: Timo K <toger5@hotmail.de>

* remove unused

Signed-off-by: Timo K <toger5@hotmail.de>

* rename summaryStatsReportGatherer to "Reporter"
for the summary stats there is only one instance because there is only
one summary. Since we dont have a list of gatherers it this class only reports.
Hence we rename it to be a reporter.

Signed-off-by: Timo K <toger5@hotmail.de>

* review

Signed-off-by: Timo K <toger5@hotmail.de>

* Update src/webrtc/stats/groupCallStats.ts

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

* revert rename

Signed-off-by: Timo K <toger5@hotmail.de>

* Update all non-major dependencies (#3433)

* Update all non-major dependencies

* Remove name wrap-ansi-cjs

* Remove name string-width-cjs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* Update definitelyTyped (#3430)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* Export FALLBACK_ICE_SERVER (#3429)

* Add an integration test for verification (#3436)

* Move existing crypto integ tests into a subdirectory

* Factor out some common bits from `crypto.spec.ts`

* Integration test for device verification

* Ignore generated file in prettier

* Always show a summary after Jest tests (#3440)

... because it is otherwise impossible to see what failed.

* Use correct /v3 prefix for /refresh (#3016)

* Add tests to ensure /v3/refresh is called + automatic /v1 retry

* Request /refresh with v3 prefix, and quietly fall back to v1

* Add tests checking re-raising errors

* Update spec/unit/login.spec.ts

* Update comment

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update Mutual Rooms (MSC2666) support (#3381)

* update mutual rooms support

* clarify docs and switch eslint comment with todo

* please the holy linter

* change query variable names around

* add mock tests and fix issue

* ye holy linter

* GHA: build and cypress-test a copy of element-web after each push (#3412)

* Build a copy of element-web after each push

* Run cypress after each build of element-web

* Fix downstream-artifacts build (#3443)

* Fix downstream-artifacts build

* Update cypress.yml

* Fix edge cases around 2nd order relations and threads (#3437)

* Fix tests oversimplifying threads fixtures

* Check for unsigned thread_id in MatrixEvent::threadRootId

* Fix threads order being racy

* Make Sonar happier

* Iterate

* Make sliding sync linearize processing of sync requests (#3442)

* Make sliding sync linearize processing of sync requests

* Iterate

* Iterate

* Iterate

* Iterate

* Disable downstream artifacts build for develop branch (#3444)

* Export thread-related types from SDK (#3447)

* Export thread-related types from SDK

* address PR feedback

* Integration test for QR code verification (#3439)

* Integration test for QR code verification

Followup to https://github.com/matrix-org/matrix-js-sdk/pull/3436: another
integration test, this time using the QR code flow

* Use Object.defineProperty, and restore afterwards

Apparently global.crypto exists in some environments

* apply ts-ignore

* remove stray comment

* Update spec/integ/crypto/verification.spec.ts

* Add `getShowSasCallbacks`, `getShowQrCodeCallbacks` to VerifierBase (#3422)

* Add `getShowSasCallbacks`, `getShowQrCodeCallbacks` to VerifierBase

... to avoid some type-casting

* Integration test for QR code verification

Followup to https://github.com/matrix-org/matrix-js-sdk/pull/3436: another
integration test, this time using the QR code flow

* Rename method

... it turns out not to be used quite as I thought.

* tests for new methods

* Use Object.defineProperty, and restore afterwards

Apparently global.crypto exists in some environments

* apply ts-ignore

* More test coverage

* fix bad merge

* Fix changelog_head.py script to be Python 3 compatible

* Prepare changelog for v25.2.0-rc.1

* v25.2.0-rc.1

* Fix tsconfig-build.json

* Prepare changelog for v25.2.0-rc.2

* v25.2.0-rc.2

* Fix docs deployment

* Prepare changelog for v25.2.0-rc.3

* v25.2.0-rc.3

* Prepare changelog for v25.2.0-rc.4

* v25.2.0-rc.4

* [Backport staging] Attempt a potential workaround for stuck notifs (#3387)

Co-authored-by: Andy Balaam <andy.balaam@matrix.org>

* Prepare changelog for v25.2.0-rc.5

* v25.2.0-rc.5

* [Backport staging] Fix mark as unread button (#3401)

Co-authored-by: Michael Weimann <michaelw@matrix.org>

* Prepare changelog for v26.0.0-rc.1

* v26.0.0-rc.1

* Prepare changelog for v26.0.0

* v26.0.0

* Resetting package fields for development

* use cli.canSupport to determine intentional mentions support (#3445)

* use cli.canSupport to determine intentional mentions support

* more specific comment

* Update src/client.ts

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

* git fixup

Signed-off-by: Timo K <toger5@hotmail.de>

* import updates

Signed-off-by: Timo K <toger5@hotmail.de>

* dont revert enricos change

Signed-off-by: Timo K <toger5@hotmail.de>

* temp rename for lowercase

* lowercase

---------

Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Robin <robin@robin.town>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: David Lee <david@david-lee.net>
Co-authored-by: Jonathan de Jong <jonathan@automatia.nl>
Co-authored-by: Stanislav Demydiuk <stas-demydiuk@users.noreply.github.com>
Co-authored-by: ElementRobot <releases@riot.im>
Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
Co-authored-by: Michael Weimann <michaelw@matrix.org>
Co-authored-by: Kerry <kerrya@element.io>
2023-06-07 13:36:40 +00:00
Richard van der Hoff c2942ddbc7 Add new methods to VerificationRequest (#3441)
* Add new method `VerificationRequest.getQRCodeBytes`

... which requires fewer complicated classes than the existing `qrCodeData`

* Add new property `VerificationRequest.otherDeviceId`

... to save going via `.channel`

* Add more methods to `VerificationRequest`

... to avoid the need for `channel`

* Use new methods in integration tests
2023-06-07 13:23:27 +00:00
Michael Weimann 2d7fdde7ed Update MSC3912 implementation to use with_rel_type instead of with_relations (#3420)
* Migrate MSC3912

* Fix doc blocks

* Remove with_relations fallback

* Implement PR feedback

* Fix typo
2023-06-07 12:05:14 +00:00
Enrico Schwendig cf34e90cb4 Fix export issue from thread module (#3451)
* Fix export issue from thread module

* separate type from class export

* follow the pattern of what other exports do
2023-06-07 09:35:20 +00:00
Kerry f0fa4d2cc8 use cli.canSupport to determine intentional mentions support (#3445)
* use cli.canSupport to determine intentional mentions support

* more specific comment

* Update src/client.ts

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-06-06 21:52:55 +00:00
RiotRobot c52e4b6329 Resetting package fields for development 2023-06-06 13:45:40 +01:00
RiotRobot 9451b55985 Merge branch 'master' into develop 2023-06-06 13:45:34 +01:00
RiotRobot f41fa84e72 v26.0.0 2023-06-06 13:44:07 +01:00
RiotRobot 3c4134537a Prepare changelog for v26.0.0 2023-06-06 13:44:03 +01:00
Richard van der Hoff 39714bfe6f Add getShowSasCallbacks, getShowQrCodeCallbacks to VerifierBase (#3422)
* Add `getShowSasCallbacks`, `getShowQrCodeCallbacks` to VerifierBase

... to avoid some type-casting

* Integration test for QR code verification

Followup to https://github.com/matrix-org/matrix-js-sdk/pull/3436: another
integration test, this time using the QR code flow

* Rename method

... it turns out not to be used quite as I thought.

* tests for new methods

* Use Object.defineProperty, and restore afterwards

Apparently global.crypto exists in some environments

* apply ts-ignore

* More test coverage

* fix bad merge
2023-06-06 11:16:19 +00:00
Richard van der Hoff f5f6100b1e Integration test for QR code verification (#3439)
* Integration test for QR code verification

Followup to https://github.com/matrix-org/matrix-js-sdk/pull/3436: another
integration test, this time using the QR code flow

* Use Object.defineProperty, and restore afterwards

Apparently global.crypto exists in some environments

* apply ts-ignore

* remove stray comment

* Update spec/integ/crypto/verification.spec.ts
2023-06-06 10:22:12 +00:00
Stanislav Demydiuk ecd700a36e Export thread-related types from SDK (#3447)
* Export thread-related types from SDK

* address PR feedback
2023-06-06 07:59:10 +00:00
Richard van der Hoff ea7042efb9 Disable downstream artifacts build for develop branch (#3444) 2023-06-05 17:38:08 +00:00
Michael Telatynski 04a6c4e6c4 Make sliding sync linearize processing of sync requests (#3442)
* Make sliding sync linearize processing of sync requests

* Iterate

* Iterate

* Iterate

* Iterate
2023-06-05 16:40:19 +00:00
Michael Telatynski 0329824cab Fix edge cases around 2nd order relations and threads (#3437)
* Fix tests oversimplifying threads fixtures

* Check for unsigned thread_id in MatrixEvent::threadRootId

* Fix threads order being racy

* Make Sonar happier

* Iterate
2023-06-05 15:18:56 +00:00
Richard van der Hoff 3351c4f57a Fix downstream-artifacts build (#3443)
* Fix downstream-artifacts build

* Update cypress.yml
2023-06-05 15:05:14 +00:00
Richard van der Hoff e70a1a1eff GHA: build and cypress-test a copy of element-web after each push (#3412)
* Build a copy of element-web after each push

* Run cypress after each build of element-web
2023-06-05 09:11:00 +00:00
Jonathan de Jong 1a5af9d8e3 Update Mutual Rooms (MSC2666) support (#3381)
* update mutual rooms support

* clarify docs and switch eslint comment with todo

* please the holy linter

* change query variable names around

* add mock tests and fix issue

* ye holy linter
2023-06-05 08:23:44 +00:00
David Lee 258f157ebc Use correct /v3 prefix for /refresh (#3016)
* Add tests to ensure /v3/refresh is called + automatic /v1 retry

* Request /refresh with v3 prefix, and quietly fall back to v1

* Add tests checking re-raising errors

* Update spec/unit/login.spec.ts

* Update comment

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-06-03 14:56:12 +00:00
Richard van der Hoff dfb079a76f Always show a summary after Jest tests (#3440)
... because it is otherwise impossible to see what failed.
2023-06-02 17:38:17 +00:00
Richard van der Hoff 858155e0ef Add an integration test for verification (#3436)
* Move existing crypto integ tests into a subdirectory

* Factor out some common bits from `crypto.spec.ts`

* Integration test for device verification

* Ignore generated file in prettier
2023-06-02 15:01:21 +00:00
Michael Telatynski 946a1cef0f Export FALLBACK_ICE_SERVER (#3429) 2023-06-02 14:29:49 +00:00
renovate[bot] 6c1fdbb7e9 Update definitelyTyped (#3430)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-06-02 13:36:53 +00:00
renovate[bot] 8cd7c96496 Update all non-major dependencies (#3433)
* Update all non-major dependencies

* Remove name wrap-ansi-cjs

* Remove name string-width-cjs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-06-02 13:33:05 +00:00
renovate[bot] cc9d530bcf Update dependency eslint-plugin-jsdoc to v45 (#3435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-01 16:38:45 +00:00
renovate[bot] cabf6da6a7 Update typescript-eslint monorepo to v5.59.7 (#3432)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-01 16:38:30 +00:00
renovate[bot] 895a82efcd Update dependency @types/jest to v29.5.2 (#3431)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-01 16:38:26 +00:00
ElementRobot d4414341d6 v26.0.0-rc.1 2023-06-01 16:54:25 +01:00
ElementRobot f173014fc0 Prepare changelog for v26.0.0-rc.1 2023-06-01 16:54:22 +01:00
ElementRobot 81cb44db7f Merge branch 'develop' into staging
# Conflicts:
#	spec/unit/room.spec.ts
#	src/models/event-timeline.ts
#	src/models/thread.ts
2023-06-01 16:49:33 +01:00
Michael Telatynski 71f9b25db7 Ensure we do not add relations to the wrong timeline (#3427)
* Do not assume that a relation lives in main timeline if we do not know its parent

* For pagination, partition relations with unknown parents into a separate bucket

And only add them to relation map, no timelines

* Make addLiveEvents async and have it fetch parent events of unknown relations to not insert into the wrong timeline

* Fix tests not awaiting addLIveEvents

* Fix handling of thread roots in eventShouldLiveIn

* Fix types

* Fix tests

* Fix import

* Stash thread ID of relations in unsigned to be stashed in sync accumulator

* Persist after processing

* Revert "Persist after processing"

This reverts commit 05ed6409b35f5e9bea3b699d0abcaac3d02588c5.

* Update unsigned field name to match MSC4023

* Persist after processing to store thread id in unsigned sync accumulator

* Add test

* Fix replayEvents getting doubled up due to Thread::addEvents being called in createThread and separately

* Fix test

* Switch to using UnstableValue

* Add comment

* Iterate
2023-06-01 15:29:05 +00:00
Enrico Schwendig 9c5c7ddb17 Add remote user and call id in webrtc call stats (#3413)
* Refactor names in webrtc stats

* Refactor summary stats reporter to gatherer

* Add call and opponent member id to call stats reports

* Update opponent member when we know them

* Add missing return type

* remove async in test

* mark new stats property as optional to avoid braking changes
2023-06-01 15:07:44 +00:00
sigmaSd feb424b0a9 mention deno support in the README (#3417)
* mention deno support in the README

It seems to work fine, and I have a demo bot with it https://github.com/sigmaSd/deno-matrix-bot

* Update README.md
2023-06-01 07:52:55 +00:00
Travis Ralston 648a1a09b1 Mark room version 10 as safe (#3425)
As should have been done a year ago.
2023-05-31 14:15:49 +00:00
Michael Telatynski 56c5375bbd Remove spec non-compliant extended glob format (#3423)
* Remove spec non-compliant extended glob format

* Simplify

* Remove tests for non spec compliant behaviour

* Remove stale rules
2023-05-31 09:05:55 +00:00
Andy Balaam b29e1e9d21 Adjust fetchEditsWhereNeeded to use a clearer filter and async function (#3411)
* Make a clear and explicit filter on which events are considered for fetchEventsWhereNeeded

* Convert the logic in fetchEventsWhereNeeded to an async function
2023-05-26 08:29:35 +00:00
Enrico Schwendig 7ade461a4c Refactor naming of webrtc stats reporter (#3404)
* Refactor names in webrtc stats

* Refactor summary stats reporter to gatherer

* Add test for signal change

* rename test
2023-05-26 07:56:49 +00:00
Andy Balaam b5414ea914 Fix bug where original event was inserted into timeline instead of the edit event (#3398)
* Fix an existing test for editing messages in threads

While attempting to test a new change, I discovered that the test
"should allow edits to be added to thread timeline" did not actually
fail if its assertions failed. Further, those assertions were incorrect.

So this change fixes the test to create the thread, wait for it to be
initialised, and then add events to it. This simplifies the flow and
ensures the test fails if something unexpected happens.

* Move editing test into thread.spec.ts

* Isolate Thread global modification in beforeAll()

* Delete unneeded setUnsigned call

* Use standard message-creation methods

* Rename event variables

* Rename sender->user

* Remove unneeded variables

* Extract distractions into functions

* Fetch edits for thread messages

Modifies fetchEditsWhereNeeded to allow edits of threaded messages. The
code before prevented any relations from fetching edits, but of course
events in threads are relations.

We definitely want thread messages to get their edits fetched, and I
assume this is working in the real code, probably because the event
being looked at is some kind of eventmapped thing that doesn't have
proper relations visible on it.

In tests, if we don't make this change, we can't see edits getting
fetched.

* Add a test for fetching edits on demand in a thread

This test demonstrates the current behaviour, which contains a bug - we
don't actually add the right event to the timeline.

* Fix bug where original event was inserted into timeline instead of the edit event
2023-05-25 15:48:48 +00:00
Andy Balaam 711bf4710d Test inserting edit events into the thread timeline "on demand" (#3410)
* Fix an existing test for editing messages in threads

While attempting to test a new change, I discovered that the test
"should allow edits to be added to thread timeline" did not actually
fail if its assertions failed. Further, those assertions were incorrect.

So this change fixes the test to create the thread, wait for it to be
initialised, and then add events to it. This simplifies the flow and
ensures the test fails if something unexpected happens.

* Move editing test into thread.spec.ts

* Isolate Thread global modification in beforeAll()

* Delete unneeded setUnsigned call

* Use standard message-creation methods

* Rename event variables

* Rename sender->user

* Remove unneeded variables

* Extract distractions into functions

* Fetch edits for thread messages

Modifies fetchEditsWhereNeeded to allow edits of threaded messages. The
code before prevented any relations from fetching edits, but of course
events in threads are relations.

We definitely want thread messages to get their edits fetched, and I
assume this is working in the real code, probably because the event
being looked at is some kind of eventmapped thing that doesn't have
proper relations visible on it.

In tests, if we don't make this change, we can't see edits getting
fetched.

* Add a test for fetching edits on demand in a thread

This test demonstrates the current behaviour, which contains a bug - we
don't actually add the right event to the timeline.
2023-05-25 16:32:42 +01:00
Andy Balaam 48b60bb885 Simplify thread-editing test (#3409)
* Fix an existing test for editing messages in threads

While attempting to test a new change, I discovered that the test
"should allow edits to be added to thread timeline" did not actually
fail if its assertions failed. Further, those assertions were incorrect.

So this change fixes the test to create the thread, wait for it to be
initialised, and then add events to it. This simplifies the flow and
ensures the test fails if something unexpected happens.

* Move editing test into thread.spec.ts

* Isolate Thread global modification in beforeAll()

* Delete unneeded setUnsigned call

* Use standard message-creation methods

* Rename event variables

* Rename sender->user

* Remove unneeded variables

* Extract distractions into functions
2023-05-25 14:03:58 +00:00
Andy Balaam 7ed14dc749 Only add a local receipt if it's after an existing receipt (#3399)
* Add a test for creating local echo receipts in threads

* Only add local receipt if it's after existing receipt

* Refactor local receipt tests to be shorter

* Tests for local receipts where we DO have recursive relations support
2023-05-25 13:59:14 +00:00
Andy Balaam 26736b6bb1 Move editing test into thread.spec.ts (#3408)
* Fix an existing test for editing messages in threads

While attempting to test a new change, I discovered that the test
"should allow edits to be added to thread timeline" did not actually
fail if its assertions failed. Further, those assertions were incorrect.

So this change fixes the test to create the thread, wait for it to be
initialised, and then add events to it. This simplifies the flow and
ensures the test fails if something unexpected happens.

* Move editing test into thread.spec.ts
2023-05-25 13:20:59 +00:00
Andy Balaam 056aae823d Fix an existing test for editing messages in threads (#3407)
While attempting to test a new change, I discovered that the test
"should allow edits to be added to thread timeline" did not actually
fail if its assertions failed. Further, those assertions were incorrect.

So this change fixes the test to create the thread, wait for it to be
initialised, and then add events to it. This simplifies the flow and
ensures the test fails if something unexpected happens.
2023-05-25 12:38:53 +00:00
Andy Balaam d1bfdca0c9 Isolate changes to Thread global into a single describe block (#3390) 2023-05-25 11:45:47 +00:00
Andy Balaam c0577c29c4 Minor improvements to the code inserting events into timelines (#3389)
* Remove unneeded pling

* Comment clarifying why we bail out in insertEventIntoTimeline
2023-05-25 11:44:56 +00:00
Richard van der Hoff a2d1dee0a1 Minor fixes to CryptoApi doc comments (#3406)
Followups to #3287 and #3360: minor clarifications to the doc-comments
2023-05-24 16:02:40 +00:00
Richard van der Hoff d7bcdff29b Fix re-exports for SAS.ts (#3405)
These three are only types, not objects we can export.

Fixes warnings in EW (and probably some build failures for someone somewhere):

```
2023-05-24 11:27:28.294 [element-js] WARNING in ../matrix-js-sdk/src/crypto/verification/SAS.ts 31:0-123
2023-05-24 11:27:28.294 [element-js] "export 'EmojiMapping' was not found in '../../crypto-api/verification'
2023-05-24 11:27:28.294 [element-js]
2023-05-24 11:27:28.294 [element-js] WARNING in ../matrix-js-sdk/src/crypto/verification/SAS.ts 31:0-123
2023-05-24 11:27:28.294 [element-js] "export 'GeneratedSas' (reexported as 'IGeneratedSas') was not found in '../../crypto-api/verification'
2023-05-24 11:27:28.294 [element-js]
2023-05-24 11:27:28.294 [element-js] WARNING in ../matrix-js-sdk/src/crypto/verification/SAS.ts 31:0-123
2023-05-24 11:27:28.294 [element-js] "export 'ShowSasCallbacks' (reexported as 'ISasEvent') was not found in '../../crypto-api/verification'
```
2023-05-24 10:54:10 +00:00
Eric Eastwood ed71cdeccd Add more context for why threaded vs unthreaded read receipts (#3378)
* Add more context for why threaded vs unthreaded read receipts

See https://github.com/matrix-org/matrix-js-sdk/pull/3339#discussion_r1195364120

* Language updates from Andy

See https://github.com/matrix-org/matrix-js-sdk/pull/3378#discussion_r1197622113

* Fix lints
2023-05-24 10:48:22 +00:00
Michael Telatynski 729f924de1 Prioritise entirely supported flows for UIA (#3402)
* Prioritise entirely supported flows for UIA

* Add tests

* Fix types

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/interactive-auth.ts

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-05-24 10:33:48 +00:00
Andy Balaam 4732098731 Test for inserting events into the timeline in timestamp order (#3391)
* Move mkReaction into test-utils so it can be used by other code

* Basic test for inserting messages into the thread timeline
2023-05-24 10:29:57 +00:00
ElementRobot a7d503a8ac [Backport staging] Fix mark as unread button (#3401)
Co-authored-by: Michael Weimann <michaelw@matrix.org>
2023-05-24 09:02:18 +01:00
Michael Weimann 063d69eff1 Fix mark as unread button (#3393)
* Fix mark as unread button

* Revert to prefer the last event from the main timeline

* Refactor room test

* Fix type

* Improve docs

* Insert events to the end of the timeline

* Improve test doc
2023-05-23 20:34:18 +00:00
Richard van der Hoff 614f446361 Combine QrCodeEvent, SasEvent and VerificationEvent (#3386)
* Move IReciprocateQr to `crypto-api` and rename

* Move ISasEvent to `crypto-api`, and rename

... and add some comments

* Combine QrCodeEvent, SasEvent and VerificationEvent together

... as a precursor to extracting a single `Verifier` interface for `SAS` and `ReciprocateQRCode`.

`enum`s are slightly magical things that have both a type and a value, so we
have to re-export their backwards-compatibility fudges twice.

* Update src/crypto/verification/Base.ts
2023-05-22 11:02:10 +00:00
Michael Telatynski b62fac9dad Update client.ts 2023-05-19 16:39:02 +01:00
ElementRobot ff07cb642f v25.2.0-rc.5 2023-05-19 16:35:34 +01:00
ElementRobot fba3cf2756 Prepare changelog for v25.2.0-rc.5 2023-05-19 16:35:32 +01:00
Michael Telatynski 5973c66726 Make sonar happier about our code & tests (#3388) 2023-05-19 16:33:19 +01:00
ElementRobot b22fc1f9d9 [Backport staging] Attempt a potential workaround for stuck notifs (#3387)
Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
2023-05-19 15:57:55 +01:00
Richard van der Hoff 3f48a954d8 Move crypto classes into a separate namespace (#3385)
* Move crypto classes into a separate namespace

* Add in re-exports for backwards compatibility

* Update src/matrix.ts
2023-05-19 12:43:16 +00:00
Michael Telatynski b5d544df68 Update sonarqube.yml (#3376) 2023-05-19 12:16:29 +00:00
Andy Balaam cd26ba67d4 Attempt a potential workaround for stuck notifs (#3384)
* Attempt a potential workaround for stuck notifs

* Remove TODOs

* Fix backwards logic about server support for MSC3981 in fetchEditsWhereNeeded

* Check for lack of MSC3981 server support before calling insertEventIntoTimeline

* If no parent event is found, insert purely based on timestamp

* Mark temporary methods as internal
2023-05-19 12:34:26 +01:00
Michael Telatynski 488390365a Add typedoc-plugin-coverage (#3379)
* Add typedoc-plugin-coverage

* Specify coverageLabel
2023-05-19 10:35:19 +00:00
Richard van der Hoff b7b1129478 Fix bug with pendingEventOrdering: "chronological" (#3382)
If we don't do this, then we end up trying to match the MAC on our own
`m.key.verification.mac`, which of course fails.
2023-05-18 13:47:24 +00:00
Richard van der Hoff 0fa9528a3f Comments for typed-event-emitter classes (#3380)
* Comments for typed-event-emitter classes

* Export `TypedEventEmitter`

... to stop tsdoc complaining.
2023-05-18 12:22:51 +00:00
Enrico Schwendig a7b1dcaf95 Filter all summary stats which collect the first time webrtc metrics. (#3377)
* remove comment

* Filter all summery stats which collect the first time webrtc stats

* Fix lint issues

* Fix lint second issues
2023-05-17 15:40:33 +00:00
Richard van der Hoff bb5bccbf78 Element-R: initial implementation of bootstrapCrossSigning (#3368)
* Working `bootstrapCrossSigning` for rust

* Remove unused `oldBackendOnly`

* update tests

* another test
2023-05-16 20:30:32 +00:00
renovate[bot] ece3ccb958 Lock file maintenance (#3375)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-16 16:31:36 +00:00
renovate[bot] a769cf88f7 Update dependency eslint-plugin-unicorn to v47 (#3373)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-16 15:23:34 +00:00
renovate[bot] 8b0f1a0c59 Update dependency @types/node to v18.16.9 (#3370)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-05-16 15:19:43 +00:00
renovate[bot] 978e748246 Update dependency eslint-plugin-jsdoc to v44 (#3372)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-16 15:15:21 +00:00
renovate[bot] 32a5cc4728 Update matrix-org/netlify-pr-preview action to v2 (#3374)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-16 15:01:13 +00:00
renovate[bot] d580f56c0b Update all non-major dependencies (#3371)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-16 14:51:21 +00:00
Michael Telatynski 789458e0d4 Add methods to terminate idb worker (#3362) 2023-05-16 16:05:55 +01:00
renovate[bot] 8d14d45272 Update typescript-eslint monorepo to v5.59.6 (#3334)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-16 14:37:05 +00:00
ElementRobot 2ec1fa6605 v25.2.0-rc.4 2023-05-16 14:22:04 +01:00
ElementRobot f15d682938 Prepare changelog for v25.2.0-rc.4 2023-05-16 14:22:02 +01:00
ElementRobot 21a10a2d14 v25.2.0-rc.3 2023-05-16 14:17:04 +01:00
ElementRobot fc02e550bd Prepare changelog for v25.2.0-rc.3 2023-05-16 14:17:02 +01:00
Michael Telatynski 78637a0689 Fix docs deployment 2023-05-16 14:15:27 +01:00
ElementRobot 4ca882fcd4 v25.2.0-rc.2 2023-05-16 13:58:35 +01:00
ElementRobot 13ee0eb7f5 Prepare changelog for v25.2.0-rc.2 2023-05-16 13:58:33 +01:00
ElementRobot cb018dfc80 Fix tsconfig-build.json 2023-05-16 13:58:05 +01:00
ElementRobot 7574dacdb3 v25.2.0-rc.1 2023-05-16 13:37:19 +01:00
ElementRobot 0c417b7c32 Prepare changelog for v25.2.0-rc.1 2023-05-16 13:37:17 +01:00
ElementRobot daf845d7bd Fix changelog_head.py script to be Python 3 compatible 2023-05-16 13:36:40 +01:00
Michael Telatynski 52792ec89b Fix CI failure on develop due to force merged PR and prettier failure (#3369) 2023-05-16 11:52:58 +00:00
Matthew Hodgson 6dc4a62e8c Merge pull request #3367 from matrix-org/matthew/fix-accumulated-sync-summaries
fix accumulated sync summaries - andy review
2023-05-16 12:34:54 +01:00
Matthew Hodgson 1cd8ea61ea merge 2023-05-16 12:08:02 +01:00
Matthew Hodgson 0c5eb277e4 incorporate andy review 2023-05-16 12:05:56 +01:00
Matthew Hodgson d459a91af3 correctly accumulate sync summaries. (#3366)
if a sync summary for (say) invited_member_count goes from 1 to 0, it should be
accumluated as 0, rather than 1.

Should fix https://github.com/vector-im/element-web/issues/23345
2023-05-16 10:41:35 +00:00
Matthew Hodgson 18722d0031 correctly accumulate sync summaries.
if a sync summary for (say) invited_member_count goes from 1 to 0, it should be
accumluated as 0, rather than 1.

Should fix https://github.com/vector-im/element-web/issues/23345
2023-05-16 11:34:06 +01:00
RiotRobot 5119934268 Merge branch 'master' into develop 2023-05-16 09:12:28 +01:00
RiotRobot 077da23d08 v25.1.1 2023-05-16 09:10:34 +01:00
RiotRobot 083b4cb17e Prepare changelog for v25.1.1 2023-05-16 09:10:31 +01:00
Richard van der Hoff 4316009401 Element-R: support for SigningKeysUploadRequest (#3365)
* OutgoingRequestProcessor: support for SigningKeysUploadRequest

* Tests

* Bump matrix-org/matrix-sdk-crypto-js

... to pick up bug fixes for outgoing requests
2023-05-15 19:14:05 +00:00
Richard van der Hoff 72f3c360b6 Add CryptoApi.getCrossSigningKeyId (#3360) 2023-05-15 18:46:33 +01:00
Enrico Schwendig fcbc195fbe Check permission on mute mic, only if no audio track exists. (#3359)
* check permission only if no audio track

* fix linter issues

* add missing tests for perfect negotiation pattern

* add null case in unit tests for audio muting

* fix issue with type MediaStream

* force right type of mock methode

* format code
2023-05-15 14:38:43 +00:00
Richard van der Hoff af38021d28 Deprecate device methods in MatrixClient (#3357) 2023-05-15 10:37:35 +00:00
Michael Telatynski 5e8cb9fa18 Fix typedoc release documentation deployment (#3358)
* Prune typedoc docs before generating new ones

* Only maintain 10 major versions

* Switch to deploy mechanism which doesn't mangle symlinks

* Convert absolute symlinks to relative
2023-05-15 09:14:35 +00:00
Michael Telatynski 6ef9f6c55e Enable better tree shaking (#3356) 2023-05-15 08:10:44 +00:00
Timo e6a3b0ebc0 Total summary count (#3351)
* add audio concealment to stats report

* audio concealment to summary

* make ts linter happy

* format and rename

* fix and add tests

* make it prettier!

* we can make it even prettier ?!

* review

* fix tests

* pretty

* one empty line to ...

* remove ratio in audio concealment (ratio is now done in the summary)

* remove comment

* fix test

* add peer connections to summary report

* tests
2023-05-14 16:58:38 +00:00
Robin aaae55736f Keep measuring a call feed's volume after a stream replacement (#3361) 2023-05-13 17:54:29 +00:00
Timo 9e586ab634 Audio concealment (#3349)
* add audio concealment to stats report

* audio concealment to summary

* make ts linter happy

* format and rename

* fix and add tests

* make it prettier!

* we can make it even prettier ?!

* review

* fix tests

* pretty

* one empty line to ...

* remove ratio in audio concealment (ratio is now done in the summary)

* remove comment

* fix test
2023-05-12 16:24:27 +00:00
Richard van der Hoff 7ff44d4a50 Integration test for bootstrapCrossSigning (#3355)
* Stub implementation of bootstrapCrossSigning

* Integration test for `bootstrapCrossSigning`
2023-05-12 16:19:18 +00:00
Richard van der Hoff 63abd00ca7 Element-R: Stub out isCrossSigningReady and isSecretStorageReady (#3354)
* Stub implementation of `isCrossSigningReady`

* Stub implementation of `isSecretStorageReady`

* add tests to meet quality gate

* factor out common

* Remove accidentally-added file
2023-05-12 12:21:52 +00:00
Richard van der Hoff 40f2579158 Pass SecretStorage into RustCrypto (#3353)
* Pass SecretStorage into RustCrypto

* Update src/rust-crypto/rust-crypto.ts
2023-05-12 09:38:33 +00:00
Richard van der Hoff ceb2a57feb Rename and move crypto.IBootstrapCrossSigningOpts (#3352)
* Define `UIAuthCallback` type and use in `IBootstrapCrossSigningOpts`

* Move `IBootstrapCrossSigningOpts` to `crypto-api` and rename

* Replace uses of `IBootstrapCrossSigningOpts`

... with `BootstrapCrossSigningOpts`

* Update src/crypto-api.ts
2023-05-11 18:41:58 +00:00
Enrico Schwendig 90e8336797 Do an ICE Restart if WebRTC become disconnected (#3341)
* Do an ice restart if ICE disconnected

  - Waite two seconds after disconnected
  - Remove check for finish ICE gathering and try to add each local candidate. Avoid race in multible ICE gathering

* Add tests for failed iceConnectionState

* suppress type check in unit test

* fix pr issues
2023-05-11 14:00:26 +00:00
Andy Balaam 73ca9c9ed2 Revert "Update JS-DevTools/npm-publish action to v2 (#3336)" (#3350)
Attempting to fix failure to publish to NPM due to an "invalid token"
error https://github.com/matrix-org/matrix-js-sdk/actions/runs/4926808655/jobs/8819822367 .

This reverts commit ca9263fa64.
2023-05-10 10:32:22 +00:00
RiotRobot cc065c2772 Resetting package fields for development 2023-05-09 15:14:29 +01:00
RiotRobot 028be0fee2 Merge branch 'master' into develop 2023-05-09 15:14:24 +01:00
RiotRobot d4500da59a v25.1.0 2023-05-09 15:12:52 +01:00
RiotRobot b6aef6772e Prepare changelog for v25.1.0 2023-05-09 15:12:49 +01:00
Michael Telatynski 49696cecbd Use WeakMap in ReEmitter to help enable garbage collection (#3344) 2023-05-09 13:24:34 +00:00
renovate[bot] 83cb52c89c Lock file maintenance (#3312)
* Lock file maintenance

* Fix types

* Iterate

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-05-09 13:11:30 +00:00
renovate[bot] e82bae2c4d Update dependency @types/node to v18.16.6 (#3347)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-09 10:52:45 +00:00
renovate[bot] ee2b0204aa Update dependency @types/node to v18.16.4 (#3335)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-09 04:58:36 +00:00
renovate[bot] 1ec7670f6a Update babel monorepo (#3332)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-08 02:47:30 +00:00
Michael Telatynski 8ab2e10471 Update types to match spec (#3330) 2023-05-05 08:13:07 +00:00
renovate[bot] 8ff8685ae5 Update dependency rimraf to v5 (#3298)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-04 21:43:38 +00:00
Florian Duros f3772cdf82 Avoid upload a new fallback key at every /sync (#3338) 2023-05-03 13:10:02 +00:00
Andy Balaam ee2f1cdfd4 Accumulate receipts for the main thread and unthreaded separately. (#3339)
Fixes matrix-org/element-web#24629
2023-05-02 15:57:53 +00:00
Florian Duros 0de73a0b3e Update @matrix-org/matrix-sdk-crypto-js to ^0.1.0-alpha.8 (#3337) 2023-05-02 15:53:36 +00:00
renovate[bot] be742e811c Update dependency @types/jest to v29.5.1 (#3333)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-02 13:58:33 +00:00
renovate[bot] ca9263fa64 Update JS-DevTools/npm-publish action to v2 (#3336)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-02 13:56:42 +00:00
renovate[bot] 9f619be08d Update peter-evans/create-pull-request digest to 284f54f (#3331)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-02 13:40:31 +00:00
RiotRobot 7792254c12 v25.1.0-rc.1 2023-05-02 11:31:10 +01:00
RiotRobot fff41f1f27 Prepare changelog for v25.1.0-rc.1 2023-05-02 11:31:04 +01:00
renovate[bot] 3e0f9f582e Update all non-major dependencies (#3329)
* Update all non-major dependencies

* Create typedoc.json

* Update typedoc-plugin-missing-exports

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-04-28 08:41:28 +00:00
Michael Telatynski 47ba8cfa24 Correct interactive-auth types (#3328) 2023-04-28 07:52:47 +00:00
renovate[bot] 54bb4c8011 Update all non-major dependencies (#3295)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-27 23:55:40 +00:00
Kerry 71e763263e add client method to remove pusher (#3324)
* add client method to remove pusher

* remove unused type
2023-04-27 21:49:35 +00:00
Andy Balaam 2ebcda2a55 Tests for the ReceiptAccumulator (#3326) 2023-04-27 13:06:17 +00:00
Janne Mareike Koschinski e10db6f7e9 Implement MSC 3981 (#3248)
* Implement MSC 3891

* Add necessary mocks to tests

* Only set recurse parameter if supported

* fix: address review comments

* task: unify unstable prefix code between client and tests

* Add test for relations recursion

* Make prettier happier :)

* Revert "task: unify unstable prefix code between client and tests"

This reverts commit f7401e05

* Fix broken tests
2023-04-27 13:01:48 +00:00
Michael Weimann 1e041a2957 Add Room.getLastLiveEvent and Room.getLastThread (#3321)
* Add room.getLastLiveEvent and remove room.lastThread

* Deprecate Room.lastThread

* Add comments about timestamps

* Improve lastThread prop doc

* Simplify test structure
2023-04-27 11:44:27 +00:00
Florian Duros 1631d6f3c0 Fix racing between one-time-keys processing and sync (#3327) 2023-04-27 10:21:01 +00:00
Andy Balaam 3d86821258 Move receipt-specific logic into ReceiptAccumulator (#3320)
* Extract receipt accumulation logic into ReceiptAccumulator

* Rename readReceipts to unthreadedReadReceipts

* Move AccumulatedReceipt into receipt-accumulator

* Move the logic for consuming events into ReceiptAccumulator
2023-04-27 07:25:16 +00:00
Enrico Schwendig 261bc81554 Make webrtc stats collection optional (#3307)
* stats: disable stats collection if interval zero

* stats: add groupcall property for stats interval

* stats: disable collecting webrtc stats by default

* add setup methode for group call stats

* suppress lint errors in test
2023-04-27 06:56:58 +00:00
Michael Telatynski 8f701f43fb Use safeSet in recursivelyAssign (#3323) 2023-04-27 06:13:38 +00:00
Richard van der Hoff 4bdb9111dd Fix incorrect uses of IAuthData (#3322)
`IAuthData` is the response, not the request
2023-04-26 18:15:58 +00:00
Andy Balaam 93e2135223 Factor out ReceiptAccumulator (#3319)
* Performance tests for receipt accumulation

* Split ReceiptAccumulator into its own module
2023-04-26 15:07:28 +00:00
Enrico Schwendig 56cb05aac0 Adjust negotiation process for pending answer process (#3314)
* add debug statements

* adjust negotiation process

* switch tp simpler proof setLocalDescription()

* fix second race in answer pending state and renegotiation trigger

* revert simpler proof setLocalDescription because of pre SDP munging. I will refactor this in an extra PR
* add state of answer pending process on the second received answer methode as well. Now in any case of receiving answer we take care of this state.

* Clean up pending state in error case
2023-04-26 15:04:50 +00:00
renovate[bot] 38d5b202c9 Update peter-evans/create-pull-request action to v5 (#3300)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-26 12:47:31 +00:00
David Baker cfffa9c518 Fix lack of media when a user reconnects (#3318)
* Fix lack of media when a user reconnects

This fixes broken media when someone reconnects to the call after
a forced disconnect (when their old call gets replaced immediately
by a new call). We listen for changes in the call feeds and the tearing
down of the feeds for the old call caused us to remove the feed for
the new call.

Also adds the call to the calls map before it'as initialised, such that
it's the active call for the user/device when the feedsChanged event arrives,
otherwise we'll ignore the event.

* Fix tests
2023-04-26 10:00:08 +00:00
Florian Duros 73dbd709d8 Element-R: Stub findVerificationRequestDMInProgress and getStoredCrossSigningForUser (#3315)
* Add `findVerificationRequestDMInProgress` into `CryptoBackend` and stub it `rust-crypto`

* Add `getStoredCrossSigningForUser` into `CryptoBackend` and stub it `rust-crypto`
2023-04-25 17:52:20 +01:00
Michael Telatynski eef67e2c03 Deprecate MatrixClient::resolveRoomAlias (#3316) 2023-04-25 15:39:01 +00:00
RiotRobot e3498f0668 Resetting package fields for development 2023-04-25 09:44:25 +01:00
RiotRobot f04c147faa Merge branch 'master' into develop 2023-04-25 09:44:20 +01:00
RiotRobot 33bbd45f1e v25.0.0 2023-04-25 09:42:44 +01:00
RiotRobot fd91a534c7 Prepare changelog for v25.0.0 2023-04-25 09:42:41 +01:00
Richard van der Hoff c2afc357b9 Add some comments to OlmDecryption.decryptEvent (#3308) 2023-04-24 11:25:07 +00:00
Richard van der Hoff 1d3f67f2ce DeviceVerificationStatus: add new signedByOwner property (#3311) 2023-04-24 10:41:36 +00:00
Michael Telatynski 514e4f07f1 Update sonarcloud.yml 2023-04-24 08:38:14 +01:00
Florian Duros fbb1c4b2bd Element-R: wire up device lists (#3272)
* Add `getUserDeviceInfo` to `CryptoBackend` and old crypto impl

* Add `getUserDeviceInfo` WIP impl to `rust-crypto`

* Add tests for `downloadUncached`

* WIP test

* Fix typo and use `downloadDeviceToJsDevice`

* Add `getUserDeviceInfo` to `client.ts`

* Use new `Device` class instead of `IDevice`

* Add tests for `device-convertor`

* Add method description for `isInRustUserIds` in `rust-crypto.ts`

* Misc

* Fix typo

* Fix `rustDeviceToJsDevice`

* Fix comments and new one

* Review of `device.ts`

* Remove `getUserDeviceInfo` from `client.ts`

* Review of `getUserDeviceInfo` in `rust-crypto.ts`

* Fix typo in `index.ts`

* Review `device-converter.ts`

* Add documentation to `getUserDeviceInfo` in `crypto-api.ts`

* Last changes in comments
2023-04-21 14:03:02 +00:00
Florian Duros 63dea599b1 Update @matrix-org/matrix-sdk-crypto-js to 0.1.0-alpha.7 (#3309) 2023-04-20 16:37:09 +00:00
Janne Mareike Koschinski 743ba5f050 Increase log level for scheduler (#3152) 2023-04-20 12:31:58 +00:00
Michael Telatynski cb180b4195 Improve types (#3305) 2023-04-20 08:02:38 +00:00
Richard van der Hoff c805b7e29d DeviceVerificationStatus: take a param object (#3303) 2023-04-19 14:38:23 +01:00
Michael Weimann 8f6814450b Add proper deprecation doc to room fields (#3291) 2023-04-19 08:08:37 +00:00
Michael Telatynski 37e8391cde Node 20 support (#3302) 2023-04-19 09:11:32 +01:00
Enrico Schwendig 90234402a7 stats: calculate received media and ignore not added tracks (#3301)
* stats: calculate received media by ignore not added tracks

* stats: fix lint issue

---------

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2023-04-19 06:50:27 +00:00
renovate[bot] 8ecf603d73 Update dependency eslint-plugin-jsdoc to v41 (#3297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Kerry <kerrya@element.io>
2023-04-19 03:11:41 +00:00
renovate[bot] af243581ff Update typescript-eslint monorepo to v5.58.0 (#3296)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-18 22:10:19 +00:00
renovate[bot] 01afed9ff9 Update dawidd6/action-download-artifact digest to 246dbf4 (#3294)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-18 22:10:19 +00:00
Michael Telatynski 2687bb37fb Update tests.yml 2023-04-18 17:51:06 +01:00
Michael Telatynski 65b1a10803 Fix TimelineWindow getEvents exploding if no neigbouring timeline (#3285) 2023-04-18 12:45:58 +00:00
Michael Telatynski 9d230ef0d6 Use typedoc-plugin-versions (#3289)
* Install typedoc-plugin-versions

* Iterate

* Remove typedoc.json

* Simplify

* Simplify

* gnu vs bsd

* Iterate

* Iterate
2023-04-18 12:39:17 +00:00
Richard van der Hoff a03438f2af New CryptoApi.getDeviceVerificationStatus api (#3287)
* Element-R: implement `{get,set}TrustCrossSignedDevices`

A precursor to https://github.com/vector-im/element-web/issues/25092

* Pull out new `DeviceVerificationStatus`

Define a new base class to replace `DeviceTrustLevel`. The intention is to have
a cleaner interface which is easier to expose from the new crypto impl

* Define, and implement, a new `CryptoApi.getDeviceVerificationStatus`

This is similar to `checkDeviceTrust`, which we're deprecating, but:
 * is `async`, meaning we can implement it in Rust
 * Returns a `DeviceVerificationStatus` instead of a `DeviceTrustLevel`
 * Returns `null` rather than "not verified" if the device is unknown

* add some tests

* Export DeviceVerificationStatus as a proper class

... so that we can instantiate it in tests
2023-04-18 10:52:13 +00:00
RiotRobot c622e9260f v25.0.0-rc.1 2023-04-18 11:41:03 +01:00
RiotRobot 8bf53d6f90 Prepare changelog for v25.0.0-rc.1 2023-04-18 11:41:00 +01:00
Enrico Schwendig 8c30a3b0df Add max jitter and max packet loss (#3293)
* stats: add max jitter and max packet loss

* stats: add test for max jitter and packet loss

* stats: add build summery report tests

* stats: switch to packetsLost instead of packetsTotal
2023-04-18 10:28:59 +00:00
Richard van der Hoff c61d53eed0 Element-R: implement {get,set}TrustCrossSignedDevices (#3281)
A precursor to https://github.com/vector-im/element-web/issues/25092
2023-04-18 10:28:47 +00:00
Michael Telatynski 95f7d1d347 Add typedoc-plugin-mdn-links (#3292) 2023-04-18 10:00:37 +00:00
Michael Telatynski 72d70bb929 Improve types and their safety (#3290)
* Improve types and their safety

* Iterate
2023-04-18 07:32:40 +00:00
Kerry 4f67e59692 Annotate events with executed push rule (#3284)
* unit test paginating /notifications

* add push rule to event

* 1% more test coverage
2023-04-17 21:35:56 +00:00
Kerry d40d5c8a39 unit test paginating /notifications (#3283) 2023-04-16 22:33:52 +00:00
Tulir Asokan 87398ac555 Fix screen sharing on Firefox 113 (#3282)
`getCapabilities` exists now(?), but `setCodecPreferences` doesn't,
which means it would throw an error and fail the call.

Signed-off-by: Tulir Asokan <tulir@maunium.net>
2023-04-15 05:08:21 +00:00
Michael Weimann de3d5ead42 Add Room.threadsTimelineSets doc (#3279)
* Add Room.threadsTimelineSets doc

* Tweak type checks

* Tweak type check again
2023-04-14 09:48:28 +00:00
Richard van der Hoff 1e1b571b28 Expose ServerSideSecretStorage independently of Crypto (#3280)
There is no reason to indirect secret storage via the Crypto layer, and
exposing it directly means it will work for Element-R.

Fixes: https://github.com/vector-im/element-web/issues/24982
2023-04-13 17:21:38 +01:00
Richard van der Hoff f400a7b1b2 Remove eventNames from TypedEventEmitter (#3276) 2023-04-13 10:43:57 +00:00
Enrico Schwendig a0bcb5777f Add Jitter to exported webrtc stats (#3270)
* stats: Add Jitter stats

* Update src/webrtc/stats/trackStatsReporter.ts

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

* stats: Fix typos in tests

* stats: differences between 0 and undefined in jitter val

---------

Co-authored-by: Robin <robin@robin.town>
2023-04-13 08:51:13 +00:00
Richard van der Hoff f8a625eddb OutgoingRoomKeyRequest is only a type (#3278)
Followup to #3275

Fixes a warning from webpack:

    [element-js] WARNING in ../matrix-js-sdk/src/matrix.ts 46:0-61
    [element-js] "export 'OutgoingRoomKeyRequest' was not found in './crypto/store/base'
2023-04-13 07:43:19 +00:00
Richard van der Hoff 69a2a15b95 Add missing methods to ServerSideSecretStorgage (#3277)
(and improve some doc-comments)

Seems like I missed a couple of methods when I created this interface in #3267.
2023-04-13 06:08:53 +00:00
Richard van der Hoff b9d0596dd7 Fix typedoc warnings (#3275)
There aren't that many of these, so I've gone through and fixed them, and
configured the GHA workflow to complain if any creep back in.
2023-04-12 20:30:57 +00:00
Richard van der Hoff 72af8c193c Typescript fix to AccountDataClient (#3274)
* Typescript fix to `AccountDataClient`

obviously, `getAccountDataFromServer` can return null

* Update src/secret-storage.ts

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-04-12 18:38:05 +00:00
Richard van der Hoff ed8c326856 Pass UserId into receiveSyncChanges (#3273) 2023-04-12 18:33:12 +00:00
Michael Telatynski f5bf6b1be6 Update types to match reality (#3271) 2023-04-12 17:49:16 +00:00
Richard van der Hoff 0e19f8dc69 Split SecretStorage into two parts (#3267)
* Pull `SecretStorageCallbacks` out of `ICryptoCallbacks`

* Pull the storage part of SecretStorage out to a new class

* Move SecretSharing to a separate class

* Move `ISecretRequest` into `SecretSharing.ts`

* Pull out ISecretStorage interface, and use it

* Mark old `SecretStorage` as deprecated, and rename accesses to it

* Move a `SecretStorage` unit test into its own file

* Use new `SecretStorage` in a couple of places

* add some more unit tests

* Fix test file name

... to match the unit under test

* even more tests

* Add a load of comments

* Rename classes

* Fix some broken tsdoc links

* fix broken test

* Fix compaints about superlinear regex

* just one more test
2023-04-12 16:10:43 +00:00
Richard van der Hoff 6049c0bf37 Improve output in github actions for jest tests (#3269) 2023-04-11 18:53:10 +00:00
texuf a102253f30 Update storage interface so that save() is async to match indexeddb impl (#3221)
The only implementation of this is an async function, but I can’t await it because the interface hides the return type.

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-04-11 18:52:53 +00:00
Florian Duros f71d86f005 Element-R: pass device list change notifications into rust crypto-sdk (#3254) 2023-04-11 14:42:19 +01:00
RiotRobot 70e34ffb76 Resetting package fields for development 2023-04-11 14:00:55 +01:00
RiotRobot 91aa7b26e6 Merge branch 'master' into develop 2023-04-11 14:00:49 +01:00
RiotRobot 7d45947fb3 v24.1.0 2023-04-11 13:59:08 +01:00
RiotRobot 6e174328e8 Prepare changelog for v24.1.0 2023-04-11 13:59:05 +01:00
renovate[bot] 5fb97fcce4 Lock file maintenance (#3268)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-11 09:24:40 +00:00
Hugh Nimmo-Smith 3d1a450129 Export type for return of getCapabilities() (#3266)
* Export type for return of getCapabilities()

Renamed because it clashes with ICapabilities from embedded

* Export type for return of getCapabilities()

Renamed because it clashes with ICapabilities from embedded

* Rename to Capabilities
2023-04-06 12:46:44 +00:00
Michael Telatynski a58c5aacdf Update pull_request.yaml 2023-04-06 12:53:43 +01:00
Michael Weimann d7e165a279 Retry processing potential poll events after decryption (#3246)
* Retry processing potential poll events after decryption

* Point `typedoc` at `matrix.ts`, not `index.ts` (#3239)

This gets rid of the rather pointless "default" module in the generated docs.

* Split up, rename, and move `ISecretStorageKeyInfo` (#3242)

* Move SecretStorageKeyInfo interfaces out to a new module

* Replace usages of ISecretStorageKeyInfo with SecretStorageKeyDescription

* Skip clear text non-poll events

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-04-06 09:59:39 +00:00
renovate[bot] a57ee803f1 Update dependency typescript to v5 (#3262)
* Update dependency typescript to v5

* Update dependency typescript to v5

* Iterate

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-04-06 09:54:02 +00:00
Enrico Schwendig 170a52b09f Measure whether we receive media and add the mute state as an exception (#3249)
* stats: add summery stats reporter

* stats: export summery stats reports

* stats: fix typo of event name

* stats: check promise condition for node 16 test linter

* stats: remove weak test to figure out memory leak

* stats: remove second weak test

* stats: add starting processing test

* stats: fix tests

* stats: fix typo in group call

* stats: fix stats report gathering test

* stats: reactivate promise merge

* stats: add track counter and track mute counter in summary stats

* stats: add summery calculation

* stats: fix PR issues

* stats: adjust summery reporter for inbound and mute state

* stats: check async state

* stats: switch from an `Or` to `And` condition for entire received media value

* stats: Add property description

---------

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2023-04-06 09:32:09 +00:00
renovate[bot] 2be5889d18 Update peter-evans/create-pull-request digest to 38e0b6e (#3257)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-06 09:20:40 +00:00
Michael Telatynski ca6b574bee Update upgrade_dependencies.yml (#3264) 2023-04-06 07:51:58 +00:00
Michael Telatynski 57b0172a2d Update pull_request.yaml 2023-04-06 09:17:30 +01:00
Michael Telatynski 53260ee25d Update pull_request.yaml (#3255) 2023-04-06 07:45:44 +00:00
renovate[bot] 964281322f Update all non-major dependencies (#3259)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-05 18:38:55 +00:00
David Baker 7c3f483396 Fix another export type (#3265)
Another case where we were importing types as normal imports which
confuses bundlers.
2023-04-05 17:14:14 +00:00
Hugh Nimmo-Smith 59784aa9fe Support for MSC3882 revision 1 (#3228)
* Support for MSC3882 revision 1

* Additional comments

* Revised field names

* Use UnstableValue for capability
2023-04-05 16:12:29 +00:00
Florian Duros 72a2b6d571 Use processKeyCounts in sliding sync (#3253) 2023-04-05 15:39:46 +00:00
renovate[bot] 5854af0eae Update definitelyTyped (#3260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-05 15:39:24 +00:00
renovate[bot] acd3d3a804 Update peaceiris/actions-gh-pages digest to 373f7f2 (#3256)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-05 15:23:40 +00:00
renovate[bot] 1b8c04a430 Update babel monorepo (#3258)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-05 15:12:15 +00:00
renovate[bot] 378b73f8b8 Update typescript-eslint monorepo to v5.57.0 (#3261)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-05 15:10:56 +00:00
Michael Telatynski c482a6ab15 Tidy up merge queue automation (#3252)
* Tidy up merge queue automation

* Iterate

* Iterate

* Iterate
2023-04-05 13:48:57 +00:00
Florian Duros 2daa429b77 Improve #3215 implementation (#3226)
* Improve key upload request

* Add fallback keys check

* Review fixes

* Add comments about sliding sync usage of `processKeyCounts`

* Review fixes

* Better wording
2023-04-05 12:35:10 +00:00
Richard van der Hoff 6ebbc15359 Move SecretStorage-related interfaces out to new module (#3244)
* Remove redundant `IAccountDataClient.getAccountData`

This is never called, so we may as well get rid of it

* Move a few more interfaces into `secret-storage.ts`

* Use interfaces from `secret-storage`

* Move IAccountDataClient to secret-storage

* Use `AccountDataClient` from `secret-storage`

* move SECRET_STORAGE_ALGORITHM_V1_AES to secret-storage

* Use `SECRET_STORAGE_ALGORITHM_V1_AES` from `secret-storage`

* Add a test case for the quality gate

* Update src/secret-storage.ts
2023-04-05 11:42:15 +00:00
Richard van der Hoff 9a840d484c Element-R: handle events which arrive before their keys (#3230)
* minor cleanups to the crypto tests

mostly, this is about using `testUtils.awaitDecryption` rather than custom
code. Some other cleanups too.

* Keep a record of events which are missing their keys

* Retry event decryption when we receive megolm keys
2023-04-05 10:00:08 +00:00
David Baker e89467c9fb Add an event emitted when a Call creates a PeerConnection (#3251)
For apps that need acces to the per connection as soon as it's created
for debugging etc.
2023-04-05 08:32:50 +00:00
Enrico Schwendig 0b396c005c indicator whether call members send media. (#3241)
* stats: add summery stats reporter

* stats: export summery stats reports

* stats: fix typo of event name

* stats: check promise condition for node 16 test linter

* stats: remove weak test to figure out memory leak

* stats: remove second weak test

* stats: add starting processing test

* stats: fix tests

* stats: fix typo in group call

* stats: fix stats report gathering test

* stats: reactivate promise merge

* stats: fix PR issues

---------

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2023-04-05 06:48:12 +00:00
David Baker d05313f95e Switch some imports to type imports (#3250)
Having these as regular imports confuses Vite for some reason.
2023-04-04 15:31:16 +00:00
RiotRobot 3f97853011 v24.1.0-rc.1 2023-04-04 11:53:22 +01:00
RiotRobot 2a5c4b1edf Prepare changelog for v24.1.0-rc.1 2023-04-04 11:53:18 +01:00
texuf 65a3c6707c indexddb-local-backend - return the current sync to database promise if a sync is in flight (#3222)
I’m trying to shutdown my matrix clients while using an indexdb, but awaiting the save() function has no effect because a previous sync was in flight. I ended up deleting the matrix client while the save was in flight and I saw a crash.

signed-off-by Austin Ellis <austin@hntlabs.com>

fix linter
2023-04-04 09:36:44 +00:00
Enrico Schwendig 942477f0bf screen share: remove set stream (#3238)
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2023-04-04 08:53:32 +00:00
Michael Telatynski 1770b3131a Skip sonarcloud & coverage in merge_queue (#3247) 2023-04-04 08:46:41 +01:00
Richard van der Hoff 41d3ffdab9 Split up, rename, and move ISecretStorageKeyInfo (#3242)
* Move SecretStorageKeyInfo interfaces out to a new module

* Replace usages of ISecretStorageKeyInfo with SecretStorageKeyDescription
2023-04-03 10:11:03 +00:00
Richard van der Hoff 78aa6cb62b Point typedoc at matrix.ts, not index.ts (#3239)
This gets rid of the rather pointless "default" module in the generated docs.
2023-04-03 09:55:11 +00:00
Michael Telatynski b56b8040e6 Use frozen lockfile instead of pure lockfile on yarn install (#3245) 2023-03-31 14:49:49 +00:00
Andy Balaam d1cf98b177 Allow via_servers property in findPredecessor (update to MSC3946) (#3240) 2023-03-31 12:51:10 +00:00
Michael Telatynski 5fc6b3ed17 Fire closed event when IndexedDB closes unexpectedly (#3218)
* Fire `closed` event when IndexedDB closes unexpectedly

* Add test

* Add tests

* Add test

* Add test

* Add test coverage
2023-03-31 08:46:11 +00:00
renovate[bot] 6e41533fed Update jest monorepo to v29.5.0 (#3204)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-31 07:52:07 +00:00
David Baker bc76532bd5 Refactor the way group calls hang up (#3234)
* Refactor how group call end calls

We previously used disposeCall to terminate the call which meant that
sometimes a call would never get a hangup event. This changes it so
that we always end a call by calling hangup, then do the cleanup
when the hangup event arrives, so the cleanup is the same whether
we hang up or the other side does.

* Some fixes for failing & hanging tests

* Add type for the call map
2023-03-30 15:57:47 +00:00
Enrico Schwendig 62f1dd79bc Add webrtc stats in full mesh calls (#3232)
* stats: merge stats classes in this branch

* stats: merge call object

* stats: merge export metric events

* stats: fix code style changes

* stats: add missing stats value formatter

* stats: add missing methode to call mock

* stats: add stats for callee

* stats: stop sending stats if call finish

* stats: rename StatsCollector to StatsReportGatherer
2023-03-30 15:17:59 +00:00
David Baker fd3f53e814 Merge pull request #3237 from matrix-org/dbkr/call_events_pass_call_2
Re-apply "Add the call object to Call events"
2023-03-30 10:22:17 +01:00
David Baker 798ac7b94c Revert "Revert "Add the call object to Call events"" 2023-03-29 15:32:25 +01:00
Richard van der Hoff eb0c0f7b93 Element-R: Add support for /discardsession (#3209)
Fixes https://github.com/vector-im/element-web/issues/24431
2023-03-29 13:26:02 +00:00
David Baker f03293f53d Merge pull request #3236 from matrix-org/revert-3229-dbkr/call_events_pass_call
Revert "Add the call object to Call events"
2023-03-29 14:41:59 +01:00
David Baker 7d062387b7 Revert "Add the call object to Call events" 2023-03-29 14:27:49 +01:00
Andy Balaam 9acf3b18ca Update changelog for v24.0.0 now the security issue is public (#3235) 2023-03-29 13:06:32 +00:00
David Baker b41d067c94 Merge pull request #3229 from matrix-org/dbkr/call_events_pass_call
Add the call object to Call events
2023-03-29 12:26:34 +01:00
David Baker c8503b3120 Run prettier on groupCall.ts (#3233) 2023-03-28 15:35:00 +00:00
David Baker da03c3b529 Fix an issue where participants could potentially view video without being displayed themselves
Fix from @robintown, manual merge due to Github having issues.
2023-03-28 15:20:34 +01:00
Robin d48b19e052 Handle group call redaction (#3231)
Redacted group call events should be interpreted as terminated calls.
2023-03-28 13:07:44 +00:00
RiotRobot 6861c67f56 Merge branch 'master' into develop 2023-03-28 14:15:09 +01:00
RiotRobot c87048bd9f v24.0.0 2023-03-28 14:10:53 +01:00
RiotRobot 02269f33b7 Prepare changelog for v24.0.0 2023-03-28 14:10:50 +01:00
Michael Weimann 7f46ae7b97 Further changes for v24.0.0 2023-03-28 14:05:24 +01:00
David Baker 2ab3566f95 Add comment on confusing exception handler I've failed to add a test for 2023-03-28 13:47:22 +01:00
Michael Weimann 9a504af18e Changes for v24.0.0 2023-03-28 11:22:02 +01:00
David Baker 8b50986906 Add test for incoming data channel 2023-03-28 09:30:17 +01:00
David Baker f9a222ecea Mock out media getter after setting up the vocie call
Otherwise we won't get far enough to test the right part
2023-03-27 16:18:00 +01:00
David Baker 5b5a3d8b5e Test failed call upgrade 2023-03-27 15:44:56 +01:00
David Baker 037cbdd214 Basic test for call replace 2023-03-27 14:33:58 +01:00
David Baker 0349411e6d Rename group call error event
So it doesn't clash with the call error event
2023-03-24 14:26:03 +00:00
David Baker d7b75e4b9e Add the call object to Call events
As explained in the comment. I've added it to the end so this should
be completely backwards compatible (although it would be much nicer
if it were the first arg, probably).
2023-03-24 14:09:22 +00:00
Richard van der Hoff 254f043ab0 Enable the prepareToEncrypt test for Element-R (#3225)
This Just Works, so I don't know why I didn't enable the test sooner.
2023-03-24 11:55:17 +00:00
J. Ryan Stinnett 5f3e115545 Stop doing O(n^2) work to find event's home (#3227)
* Stop doing O(n^2) work to find event's home

In certain rooms (e.g. with many state changes hidden via user preferences), the
events array presented to `eventShouldLiveIn` may contain 100s of events. As
part of its various checks, `eventShouldLiveIn` would get an event's associated
ID (reply / relation / redaction parent). It would then use `events.find` to
search the entire (possibly large) `events` array to look for the parent. (This
by itself seems sub-optimal and should probably change to use a map.)

For many events in a room, there is no associated ID. Unfortunately,
`eventShouldLiveIn` did not check whether the associated ID actually exists
before running off to search all of `events`, resulting in O(n^2) work.

This changes `eventShouldLiveIn` to first check that there is an associated ID
before proceeding with its (slow) search. For some rooms, this change
drastically improves performance from ~100% CPU usage to nearly idle.

Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>

* Add type to `parentEvent`

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-03-23 19:02:55 +00:00
Patrick Cloke fc55c4c72a Implement MSC3952: intentional mentions (#3092)
* Add experimental push rules.

* Update for changes to MSC3952: Use event_property_is and event_property_contains.

* Revert custom user/room mention conditions.

* Skip legacy rule processing if mentions exist.

* Add client option for intentional mentions.

* Fix tests.

* Test leagcy behavior with intentional mentions.

* Handle simple review comments.
2023-03-22 20:22:34 +00:00
Florian Duros f795577e14 Send one time key count and unused fallback keys for rust-crypto (#3215)
* Send one time key count and unused fallback keys for rust-crypto

* Add tests

* Remove useless type in promise return

* Add test for one time key upload

* Fix rust-crypto.spec.ts tests

* Remove unneeded code in test

* Add key upload request test

* Fix tests
2023-03-22 10:19:04 +00:00
Eric Eastwood f12cee984a Export TimestampToEventResponse to use in matrix-react-sdk tests (#3223)
* Export ITimestampToEventResponse to use in matrix-react-sdk tests

Part of https://github.com/matrix-org/matrix-react-sdk/pull/10405

* Remove I from interface

As suggested by @weeman1337,
https://github.com/matrix-org/matrix-js-sdk/pull/3223#pullrequestreview-1350612347

See code style guide, https://github.com/vector-im/element-web/blob/50f8be4a623d2280a72920eb1679a4754961f807/code_style.md#typescript--javascript-typescript-javascript

> Interface names should not be marked with an uppercase `I`.
2023-03-21 17:10:28 +00:00
Richard van der Hoff c3b4572841 Add a new test for event encryption, which works with rust (#3203)
* crypto.spec.ts: factor out `expactAliceKeyClaim` utility

* Add a new test for event encryption

... one that actually works on the rust SDK.

* Bump matrix-sdk-crypto-js version

... to pick up recent fixes to race conditions
2023-03-21 16:24:57 +00:00
David Baker 40fe159c10 Merge pull request #3219 from robintown/matrix-widget-api
Update matrix-widget-api
2023-03-21 10:59:50 +00:00
Robin Townsend ddecc87947 Update matrix-widget-api 2023-03-20 10:28:46 -04:00
David Baker 23837266fc Merge pull request #3217 from matrix-org/dbkr/type_sendvoipevent
Add a type to the structure used by the SendVoipEvent event
2023-03-16 10:52:09 +00:00
David Baker 3c9ca8c373 Add a type to the structure used by the SendVoipEvent event 2023-03-15 16:07:10 +00:00
RiotRobot 7f2a4c2568 Resetting package fields for development 2023-03-15 12:41:24 +00:00
RiotRobot 2ad647a73c Merge branch 'master' into develop 2023-03-15 12:41:18 +00:00
RiotRobot 26663e67fd v23.5.0 2023-03-15 12:39:41 +00:00
RiotRobot 0cfc67c679 Prepare changelog for v23.5.0 2023-03-15 12:39:38 +00:00
David Baker 7faba5c2f0 Merge pull request #3213 from matrix-org/dbkr/fix_starting_with_video_muted
Fix bug where video would not unmute if it started muted
2023-03-14 11:18:39 +00:00
Michael Telatynski 1d9250b277 Fix CallOpts types (#3214) 2023-03-14 09:53:59 +00:00
David Baker 08054c1d6d Fix bug where video would not unmute if it started muted
Fixes https://github.com/vector-im/element-call/issues/925
2023-03-13 17:04:43 +00:00
Richard van der Hoff 333872e878 Pull out a public CryptoApi (#3211)
... so that we don't have to implement annoying shims in `MatrixClient`
2023-03-13 12:02:49 +00:00
Janne Mareike Koschinski 913cd257f4 Pin versions of third-party github actions (#3208) 2023-03-10 12:56:23 +00:00
Richard van der Hoff 69f7789c40 Merge pull request #3202 from matrix-org/rav/element-r/encryption_fixes
Fixes to event encryption in the Rust Crypto implementation
2023-03-10 11:00:18 +00:00
renovate[bot] e79ef1f33a Update typescript-eslint monorepo to v5.54.0 (#3198)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Kerry <kerrya@element.io>
2023-03-10 09:06:58 +00:00
renovate[bot] fb8f61a5ec Update dependency eslint-plugin-unicorn to v46 (#3199)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-09 21:28:20 +00:00
Kerry 9b1f2a1d11 adjust beacon model to make beaconInfo definitely assigned in constructor (#3201)
* adjust beacon model to make beaconInfo definitely assigned

* fix

* improvement from PR
2023-03-09 21:03:42 +00:00
Richard van der Hoff 686216fb75 Merge branch 'develop' into rav/element-r/encryption_fixes 2023-03-09 13:34:17 +00:00
Richard van der Hoff f4b83e1a27 Stop a failed /keys/claim request getting stuck
Putting the new request inside a `finally` block meant we would never actually
transition the promise chain from failure to success. Sticking a no-op `catch`
in the chain makes sure that we can recover from an error.
2023-03-09 10:54:50 +00:00
Richard van der Hoff 97f21b6635 Send the outgoing m.room_key messages returned by shareRoomKey
I forgot this in https://github.com/matrix-org/matrix-js-sdk/pull/3122 :(.

To be honest, I'm not sure how it ever worked.
2023-03-09 10:50:49 +00:00
Michael Telatynski 87641a6803 Improve processBeaconEvents hotpath (#3200)
* Attempt at improving beacons hotpath

* Iterate and fix tests
2023-03-09 09:24:57 +00:00
renovate[bot] 7e4331172a chore(deps): update all non-major dependencies (#3197)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-08 15:33:44 +00:00
renovate[bot] a976080d1b chore(deps): update dependency @types/node to v18.14.6 (#3196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-08 08:16:55 +00:00
Patrick Cloke bcf3bba44e Implement MSC3966: a push rule condition to check if an array contains a value (#3180)
* Support MSC3966 to match values in an array in push rule conditions.

* Update to stable identifiers.

* Appease the linter.
2023-03-07 16:36:06 +00:00
Michael Telatynski 54ac36d424 Revert "Add GHA for requiring all reviews to be dismissed or approving before merging (#2391)" (#3195)
This reverts commit e84c90dbbc.
2023-03-07 15:21:20 +00:00
Michael Telatynski e84c90dbbc Add GHA for requiring all reviews to be dismissed or approving before merging (#2391)
* Add GHA for requiring all reviews to be dismissed or approving before merging

* Attempt again

* Try try try again

* stash

* Stash

* Dummy

* Fix indentation

* Iterate

* Tweak

* Iterate

* toJSON

* Iterate

* Iterate

* Fix

* typo

* Permissions

* tidy

* Update pending_reviews.yml

* delint
2023-03-07 14:23:56 +00:00
Michael Telatynski 4424438658 Fix jest/no-conditional-expect lint and enable it (#3194) 2023-03-07 12:44:03 +00:00
RiotRobot 13d95c8219 v23.5.0-rc.1 2023-03-07 11:19:25 +00:00
RiotRobot e119dc4e89 Prepare changelog for v23.5.0-rc.1 2023-03-07 11:19:22 +00:00
Patrick Cloke b4cdc5a923 Implement MSC3758: a push rule condition to match event properties exactly (#3179)
* Add some comments.

* Support MSC3758 to exactly match values in push rule conditions.

* Update to stable prefix.
2023-03-06 14:52:43 +00:00
Andy Balaam a82e22b5de Remove items incorrectly included in changelog for 23.4.0 (#3190) 2023-03-03 13:30:38 +00:00
Patrick Cloke c894d09d8c Room call is an underride, not an override. (#3185) 2023-03-03 13:30:28 +00:00
Richard van der Hoff 585ce07260 Stub out the rust crypto implementation for browserify (#3187)
Fixes #3182.
2023-03-03 10:18:45 +00:00
Enrico Schwendig 8cbbdaa239 groupCall: make no media call param optional (#3186)
- ensure group call backwards-compatibility and move `allowCallWithoutVideoAndAudio`to the end of the `CroupCall` constructor
2023-03-02 18:55:09 +00:00
Damir Jelić cd526a254d Stop requesting room keys from other users (#2982)
* Refactor the room key handling method

* Fix the forwarded room key test to use the same user ids.

We have some tests that check if receiving a forwarded room key works.
These claim to use the same user id, but in fact they change the user id
in the last moment before the event is passed into the client.

Let's change this so we're always operating with the same user id.

* Stop requesting room keys from other users

We never accept such room keys, so there isn't a point in requesting
them.

* fixup! Refactor the room key handling method

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* fixup! Refactor the room key handling method

* fixup! Apply suggestions from code review

* fixup! Refactor the room key handling method

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-03-02 16:38:24 +00:00
Enrico Schwendig e782a2afa3 Enable group calls without video and audio track by configuration of MatrixClient (#3162)
* groupCall: add configuration param to allow no audio and no camera

* groupCall: enable datachannel to do no media group calls

* groupCall: changed call no media property as object property

* groupCall: fix existing unit tests

* groupCall: remove not needed flag

* groupCall: rename property to allow no media calls

* groupCall: mute unmute even without device

* groupCall: switch to promise callbacks

* groupCall: switch to try catch

* test: filter dummy code from coverage

* test: extend media mute tests

* groupCall: move permission check to device handler

* mediaHandler: add error in log statement
2023-03-02 16:35:52 +00:00
Hugh Nimmo-Smith 565339b1fd Remove experimental support for MSC3903 v1 (#3184)
* v2 of MSC3903 implementation

This is a deliberate breaking change on an unstable feature.

* Reinstate v1 support to make this a non-breaking change

Deprecates several experimental types

* Remove MSC3903 v1 support

This is a breaking change in code marked unstable/experimental

Revert "Reinstate v1 support to make this a non-breaking change"

This reverts commit 89773458b9a1e5f332938e5574f35b16d204d75d.
2023-03-02 16:34:22 +00:00
Hugh Nimmo-Smith 493203050a Support for v2 of MSC3903 (#3155)
* v2 of MSC3903 implementation

This is a deliberate breaking change on an unstable feature.

* Test correct protocol version

* Fix up test

* v2 of MSC3903 implementation

This is a deliberate breaking change on an unstable feature.

* Test correct protocol version

* Fix up test

* Reinstate v1 support to make this a non-breaking change

Deprecates several experimental types
2023-03-02 13:15:17 +00:00
renovate[bot] 41782c4593 Update dependency eslint-plugin-jsdoc to v40 (#3173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-02 07:23:55 +00:00
renovate[bot] 86256a4e74 Update definitelyTyped (#3170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-02 07:23:42 +00:00
Kerry 933a0c9909 Polls push rules (#3181)
* add poll push rule ids

* add getPushRuleAndKindById method to pushprocessor
2023-03-01 20:30:40 +00:00
Patrick Cloke c8a4d9b88a Implement MSC3873 to handle escaped dots in push rule keys (#3134)
* Add comments.

* Implment MSC3873 to handle escaped dots in keys.

* Add some comments about tests.

* Clarify spec behavior.

* Fix typo.

* Don't manually iterate string.

* Clean-up tests.

* Simplify tests.

* Add more tests & fix bug with empty parts.

* Add more edge cases.

* Add a regular expression solution.

This is ~80% slower than the basic split(".").

* Split on a simpler regular expression.

This is ~50% slower than a simple split(".").

* Remove redundant case in regex.

* Enable sticky regex.

* Rollback use of regex.

* Cache values in the PushProcessor.

* Use more each in tests.

* Pre-calculate the key parts instead of caching them.

* Fix typo.

* Switch back to external cache, but clean out obsolete cached values.

* Remove obsolete property.

* Remove more obsolete properties.
2023-03-01 12:23:40 +00:00
RiotRobot 437128d11b Resetting package fields for development 2023-02-28 10:37:23 +00:00
RiotRobot c18d09fd22 Merge branch 'master' into develop 2023-02-28 10:36:51 +00:00
RiotRobot 2a5e5e6a59 v23.4.0 2023-02-28 10:25:34 +00:00
RiotRobot 68e354752b Prepare changelog for v23.4.0 2023-02-28 10:25:31 +00:00
Michael Telatyński d80b7499fd Fix spec compliance issue around encrypted m.relates_to (#3178)
* Fix spec compliance issue around encrypted `m.relates_to`

* Add test
2023-02-27 22:12:45 +00:00
Janne Mareike Koschinski 9c8093eb3e Fix reactions in threads sometimes causing stuck notifications (#3146)
* Associate event with thread before adding it to the thread timeline

* Make sure events can be added to thread correctly

* Write initial test case

* Add additional comment for why the code had to be reordered
2023-02-24 13:12:06 +00:00
renovate[bot] aec1c11037 Update all non-major dependencies (#3171)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-24 12:17:19 +00:00
renovate[bot] e2e9986059 Update babel monorepo to v7.21.0 (#3177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-23 16:43:01 +00:00
Michael Telatynski d70ffdbc02 Improve types (#3175)
* Improve types

* Add test
2023-02-22 17:39:37 +00:00
RiotRobot 0f1f1db3d2 v23.4.0-rc.2 2023-02-22 11:07:38 +00:00
RiotRobot 77b91a45cb Prepare changelog for v23.4.0-rc.2 2023-02-22 11:07:35 +00:00
RiotRobot fe6add9396 Merge branch 'develop' into staging 2023-02-22 10:59:21 +00:00
renovate[bot] 21cc9c3d8a Update jest monorepo to v29.4.3 (#3169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-22 10:23:29 +00:00
renovate[bot] 2d59c4647d Update typescript-eslint monorepo to v5.52.0 (#3172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-22 10:19:53 +00:00
Florian Duros 1f0c6a6dc9 Add easy way to determine if the decryption failure is due to "DecryptionError: The sender has disabled encrypting to unverified devices." (#3167)
* Add isEncryptedDisabledForUnverifiedDevices in event.ts

* Add Tests

* Add isEncryptedDisabledForUnverifiedDevices properties to event

* Use WITHHELD_MESSAGES instead of hardcoded string

* Use getter instead of function

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

* Debug

* delint

* Fix typo

* Test

* Iterate

* Fix checkout

* rerun

* experiment

* Iterate

* Iterate

* iterate

* Iterate

* Finalise

* Disable coverage for downstream tests

* Update tests.yml

* Apply merge_queue trigger to other CI

* delint

* delint

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

* Iterate

* Update comment

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

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

* Add tests for ignoring non notifying events

* Fix possibly incorrect tests?

* lint fix

* Refactor currentTotalCount

* Track Total locally too

* Lots of total count assumptions and comments

* Adjust for threading too

* Fixup tests

* a word

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

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

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

So... I've introduced a couple of new classes (SyncResponder, E2EKeyReceiver) which do some of the useful bits of TestClient, but in a more granular way, and have switched crypto.spec.ts over so that rather than instantiating a TestClient for each test, it creates a MatrixClient directly and intercepts the endpoints necessary.
2023-02-15 10:39:24 +00:00
Kerry cdd7dbbb2b decrypt poll relations before processing (#3148) 2023-02-14 21:49:52 +00:00
RiotRobot 108f157324 Resetting package fields for development 2023-02-14 10:23:07 +00:00
RiotRobot 8da39ec8f4 Merge branch 'master' into develop 2023-02-14 10:23:02 +00:00
RiotRobot 182534288c v23.3.0 2023-02-14 10:21:24 +00:00
RiotRobot f81b7e5e6f Prepare changelog for v23.3.0 2023-02-14 10:21:21 +00:00
ElementRobot 6dda9e532d Include 'browser' in list of adjusted properties in release.sh (#3149) (#3151)
(cherry picked from commit 5e17626fe0)

Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
2023-02-14 09:58:17 +00:00
Andy Balaam 5e17626fe0 Include 'browser' in list of adjusted properties in release.sh (#3149) 2023-02-14 09:50:55 +00:00
David Baker abc9c9dcb0 Merge pull request #3147 from matrix-org/dbkr/stop_ice_timer_on_terminate
Stop the ICE disconnected timer on call terminate
2023-02-13 17:35:45 +01:00
David Baker f346fcb056 Stop the ICE disconnected timer on call terminate
It just wasn't getting stopped, so if the call ended while ICE was
disconnected, we'd get confusing error messages after the call ended.
2023-02-13 16:15:57 +00:00
Eric Eastwood c67325ba07 Add matrix-org/jest linting (#2973) 2023-02-10 12:05:40 +01:00
renovate[bot] a063ae8ce7 Update jest monorepo to v29.4.1 (#3133)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-09 15:44:23 +00:00
renovate[bot] f9e5535492 Update typescript-eslint monorepo to v5.50.0 (#3136)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-09 16:29:03 +01:00
renovate[bot] 015d9c5c4f Update dependency @types/node to v18.11.19 (#3137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-09 16:28:22 +01:00
Germain b6d40078d9 Clear notifications when we can infer read status from receipts (#3139) 2023-02-09 10:18:18 +00:00
David Baker b8a8f4850a Merge pull request #3123 from matrix-org/SimonBrandner/task/logging
Improve WebRTC logging
2023-02-08 17:10:40 +00:00
Šimon Brandner 1cc23d789c Add new tests to groupCall
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-02-08 17:24:15 +01:00
Janne Mareike Koschinski 5cf0bb46a4 Messages sent out of order after one message fails (#3131)
* Instead of skipping, bail out by clearing queue
* Allow additional status transition for events from QUEUED to NOT_SENT
2023-02-08 13:23:30 +01:00
renovate[bot] 16672b3d0c Update all non-major dependencies (#3132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-07 17:36:38 +00:00
RiotRobot f61db81961 v23.3.0-rc.1 2023-02-07 12:00:46 +00:00
RiotRobot e2a694115f Prepare changelog for v23.3.0-rc.1 2023-02-07 12:00:44 +00:00
Michael Telatynski 71cf812d24 Add @typescript-eslint/no-base-to-string (#3129) 2023-02-07 10:08:03 +00:00
Richard van der Hoff 8a3d7d5671 Element-R: log outgoing HTTP requests (#3127)
otherwise it's rather hard to see them, at least in Firefox.
2023-02-06 10:50:05 +00:00
Richard van der Hoff 2a363598dd Element-R: fix a bug which prevented encryption working after a reload (#3126)
Upgrade matrix-sdk-crypto-js, to pick up
https://github.com/matrix-org/matrix-rust-sdk/pull/1429
2023-02-03 16:16:32 +00:00
Richard van der Hoff 05bf6428bc Element-R: implement encryption of outgoing events (#3122)
This PR wires up the Rust-SDK into the event encryption path
2023-02-03 15:58:50 +00:00
Michael Telatynski e492a44dde Update typescript bits to aid matrix-react-sdk achieve noImplicitAny (#3079) 2023-02-03 14:01:53 +00:00
Richard van der Hoff 44d2e47f96 Fix invite processing on Element-R (#3121)
Currently, whenever we receive an invite on element R, it crashes the sync
loop. A quick fix to make it not do that.
2023-02-03 10:56:24 +00:00
Šimon Brandner 31459a5d63 Improve logging
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-02-03 11:40:48 +01:00
Richard van der Hoff 8f5db463e7 Element R: Fix obscure errors when we fail to decrypt to-device events (#3117)
Previously, if we failed to decrypt a to-device event, we would raise an
"expected a string" error when we later tried to decrypt it as a room
event. This at least makes the error clearer.
2023-02-02 08:28:44 +00:00
Kerry 4e8affafcc Poll model - page /relations results (#3073)
* first cut poll model

* process incoming poll relations

* allow alt event types in relations model

* allow alt event types in relations model

* remove unneccesary checks on remove relation

* comment

* Revert "allow alt event types in relations model"

This reverts commit e578d84464403d4a15ee8a7cf3ac643f4fb86d69.

* Revert "Revert "allow alt event types in relations model""

This reverts commit 515db7a8bc2df5a1c619a37c86e17ccbe287ba7a.

* basic handling for new poll relations

* tests

* test room.processPollEvents

* join processBeaconEvents and poll events in client

* tidy and set 23 copyrights

* use rooms instance of matrixClient

* tidy

* more copyright

* simplify processPollEvent code

* throw when poll start event has no roomId

* updates for events-sdk move

* more type changes for events-sdk changes

* page poll relation results

* validate poll end event senders

* reformatted copyright

* undo more comment reformatting

* test paging

* use correct pollstartevent type

* emit after updating _isFetchingResponses state

* make rootEvent public readonly

* fix poll end validation logic to allow poll creator to end poll regardless of redaction
2023-02-01 20:44:40 +00:00
Kerry 2800681bb1 Poll model - validate end events (#3072)
* first cut poll model

* process incoming poll relations

* allow alt event types in relations model

* allow alt event types in relations model

* remove unneccesary checks on remove relation

* comment

* Revert "allow alt event types in relations model"

This reverts commit e578d84464403d4a15ee8a7cf3ac643f4fb86d69.

* Revert "Revert "allow alt event types in relations model""

This reverts commit 515db7a8bc2df5a1c619a37c86e17ccbe287ba7a.

* basic handling for new poll relations

* tests

* test room.processPollEvents

* join processBeaconEvents and poll events in client

* tidy and set 23 copyrights

* use rooms instance of matrixClient

* tidy

* more copyright

* simplify processPollEvent code

* throw when poll start event has no roomId

* updates for events-sdk move

* more type changes for events-sdk changes

* validate poll end event senders

* reformatted copyright

* undo more comment reformatting

* fix poll end validation logic to allow poll creator to end poll regardless of redaction

* Update src/models/poll.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* correct creator == sender validationin poll end

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-02-01 20:32:37 +00:00
Andy Balaam b2a9e6f12f Handle optional last_known_event_id property in m.predecessor (#3119) 2023-02-01 14:31:07 +00:00
Andy Balaam fbd2c97f87 Move the logic of roomPredecessor into the RoomState class (#3114)
* Move the logic of roomPredecessor into the RoomState class

* Fix review comments
2023-02-01 10:44:32 +00:00
Germain 6c6304a620 Cleanup pre MSC3773 thread unread notif logic (#3115) 2023-01-31 16:59:13 +00:00
Richard van der Hoff 0c1d5f6b25 Element-R: implement remaining OutgoingMessage request types (#3083)
This is a follow-up to #3019: it implements the remaining two types of message types, now that rust SDK has sensibly-shaped types for them.
2023-01-31 15:44:14 +00:00
RiotRobot 1c26dc0233 Resetting package fields for development 2023-01-31 10:47:39 +00:00
RiotRobot a163a202e7 Merge branch 'master' into develop 2023-01-31 10:47:34 +00:00
RiotRobot e15cf9976f v23.2.0 2023-01-31 10:46:15 +00:00
RiotRobot bdc3926417 Prepare changelog for v23.2.0 2023-01-31 10:46:12 +00:00
Hubert Chathi 4f918f684e add support for stable identifier for fixed MAC in SAS verification (#3101) 2023-01-30 09:26:43 -05:00
Germain c142232f4d Stop labelling threads as experimental (#3064) 2023-01-30 11:25:27 +00:00
Šimon Brandner c9bc20aa4d Don't throw with no opponentDeviceInfo (#3107) 2023-01-26 16:28:56 +00:00
renovate[bot] 5c0cb3a536 fix(deps): update all non-major dependencies (#3099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-26 14:50:36 +00:00
Andy Balaam 415576d0a0 Provide eventId as well as roomId from Room.findPredecessor (#3095) 2023-01-26 10:58:33 +00:00
Andy Balaam 4f9fad66e4 MSC3946 Dynamic room predecessors (#3042)
* Implement MSC3946 for getVisibleRooms

* Implement MSC3946 for getRoomUpgradeHistory
2023-01-26 10:28:07 +00:00
renovate[bot] ebd9854980 chore(deps): update dependency @types/jest to v29.2.6 (#3096)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-26 09:42:54 +00:00
renovate[bot] cb2fab64d8 chore(deps): update typescript-eslint monorepo to v5.48.2 (#3097)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-26 09:41:59 +00:00
renovate[bot] f446b49e49 fix(deps): update dependency @babel/runtime to v7.20.13 (#3100)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-26 09:41:40 +00:00
renovate[bot] 22f5e41058 chore(deps): update dependency @types/uuid to v9 (#3102)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-26 09:41:33 +00:00
renovate[bot] fee5a006f1 chore(deps): update dependency rimraf to v4 (#3103)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-26 09:41:21 +00:00
Kerry ef51ee28fd Poll model (#3036)
* first cut poll model

* process incoming poll relations

* allow alt event types in relations model

* allow alt event types in relations model

* remove unneccesary checks on remove relation

* comment

* Revert "allow alt event types in relations model"

This reverts commit e578d84464403d4a15ee8a7cf3ac643f4fb86d69.

* Revert "Revert "allow alt event types in relations model""

This reverts commit 515db7a8bc2df5a1c619a37c86e17ccbe287ba7a.

* basic handling for new poll relations

* tests

* test room.processPollEvents

* join processBeaconEvents and poll events in client

* tidy and set 23 copyrights

* use rooms instance of matrixClient

* tidy

* more copyright

* simplify processPollEvent code

* throw when poll start event has no roomId

* updates for events-sdk move

* more type changes for events-sdk changes

* comment
2023-01-26 02:07:55 +00:00
David Baker cb61345780 Merge pull request #3091 from matrix-org/dbkr/video_mute_no_renegotiate
Remove video tracks on video mute without renegotiating
2023-01-25 15:38:51 +00:00
David Baker a18d4e226e Actually check audio sender too 2023-01-25 15:07:51 +00:00
David Baker b09b33eb4c Add tests 2023-01-25 15:06:36 +00:00
Clark Fischer 5fedc06d7c Remove flaky test (#3098)
I introduced a flaky test to confirm that `MegolmEncryption#prepareToEncrypt`
didn't block the main thread too much, but it turns out that, when run in
varying environments, it tends to fail.

The same behavior is guaranteed by the following cancellation test - if the
thread is blocked, it can't be cancelled.

Signed-off-by: Clark Fischer <clark.fischer@gmail.com>

Signed-off-by: Clark Fischer <clark.fischer@gmail.com>
2023-01-25 14:27:02 +00:00
David Baker d8c9f6db33 Check we have both a sender and a track to send 2023-01-25 11:20:25 +00:00
David Baker 0af5fa0328 Actually check we have a sender, not just a transceiver 2023-01-25 11:16:19 +00:00
David Baker b328b72cd5 Move timeout clear to be with its friends 2023-01-25 11:07:14 +00:00
David Baker ce2a9d7036 Fix test 2023-01-25 10:59:03 +00:00
Andy Balaam 66ae985af5 Refactor getRoomUpgradeHistory to use Room.findPredecessorRoomId (#3090)
* Refactor getRoomUpgradeHistory to use Room.findPredecessorRoomId

* Simplify getRoomUpgradeHistory implementation a little
2023-01-25 09:48:56 +01:00
David Baker 1828e2849c Remove timer on call terminate 2023-01-24 20:30:10 +00:00
David Baker b4f8b0fe4f Remove video tracks on video mute without renegotiating 2023-01-24 18:19:19 +00:00
Robin 70656e954f Merge pull request #3035 from clarkf/megolm-cancellation
Reduce Megolm blocking and add cancellation
2023-01-24 12:26:23 -05:00
Robin 40a4c8d954 Merge branch 'develop' into megolm-cancellation 2023-01-24 12:20:42 -05:00
Andy Balaam 7ed787b86a Fix bug in getRoomUpgradeHistory's verifyLinks functionality (#3089) 2023-01-24 13:44:03 +00:00
RiotRobot 1f58ee7f2c v23.2.0-rc.1 2023-01-24 11:21:25 +00:00
RiotRobot 2e28e9117a Prepare changelog for v23.2.0-rc.1 2023-01-24 11:21:22 +00:00
Andy Balaam a58a36e062 Tests for getRoomUpgradeHistory (#3088) 2023-01-24 10:45:04 +00:00
kegsay 6cf6a0c522 refactor: sliding sync: swap to lists-as-keys (#3086)
* refactor: sliding sync: swap to lists-as-keys

Update the request/response API shape to match the latest
MSC3575 version, which converts `lists` from being an array
of list objects to being a map of list objects.

* Linting

* prettier

* add extra setListRanges test

* Default to right type
2023-01-23 15:26:25 +00:00
Andy Balaam 02aa3edda4 Revert "refactor: sliding sync: swap to lists-as-keys (#3076)"
Reverting because the companion matrix-react-sdk change is not ready so
this is breaking our builds.

This reverts commit e04ea02c62.
2023-01-23 12:21:47 +00:00
kegsay e04ea02c62 refactor: sliding sync: swap to lists-as-keys (#3076)
* refactor: sliding sync: swap to lists-as-keys

Update the request/response API shape to match the latest
MSC3575 version, which converts `lists` from being an array
of list objects to being a map of list objects.

* Linting

* prettier

* add extra setListRanges test

* Default to right type
2023-01-23 11:45:22 +00:00
David Baker 64197bf4db Merge pull request #3082 from matrix-org/dbkr/ptt_null_member_workarounds
Add null check for our own member event
2023-01-20 13:11:16 +00:00
RiotRobot c309fe6942 Merge branch 'master' into develop 2023-01-20 12:28:33 +00:00
RiotRobot 8408f36c12 v23.1.1 2023-01-20 12:26:59 +00:00
RiotRobot dcd8f91e02 Prepare changelog for v23.1.1 2023-01-20 12:26:57 +00:00
ElementRobot cabe14d7e2 replace .at(-1) with array.length-1 (#3080) (#3081)
(cherry picked from commit baeb4acddf)

Co-authored-by: Germain <germains@element.io>
2023-01-20 12:13:36 +00:00
David Baker c019f2bb19 Add null check for our own member event
As per comment
2023-01-20 12:09:28 +00:00
Germain baeb4acddf replace .at(-1) with array.length-1 (#3080) 2023-01-20 12:05:35 +00:00
David Baker 4a6e9a0f8f Merge pull request #3078 from matrix-org/dbkr/group_call_double_stream_init
Handle group call getting initialised twice in quick succession
2023-01-20 11:01:46 +00:00
David Baker ea5ce8d1d8 Also wrap the initLocalFeed method of groupCall 2023-01-19 15:28:56 +00:00
David Baker 41854918a5 Merge branch 'develop' into dbkr/group_call_double_stream_init 2023-01-19 14:13:35 +00:00
David Baker 495642d041 Handle group call getting initialised twice in quick succession
This happens in dev mode with React 18 and caused extra user media
stream to end up floating around.

Fixes https://github.com/vector-im/element-call/issues/847
2023-01-19 14:04:59 +00:00
Richard van der Hoff c7210b9e9d Rename some of the .spec files which test crypto (#3077)
* `matrix-client-crypto.spec.ts` only tested a very specific bit of crypto (olm
  encryption). It goes back to the very early days, before Megolm was invented.
  I've renamed it to `olm-encryption-spec.ts`.

* `megolm-integ.spec.ts` is more of a general crypto test; it was just called
  `megolm` to distinguish it from the Olm tests above. Renamed to
  `crypto.spec.ts`.
2023-01-19 12:20:21 +00:00
Richard van der Hoff 83563c7a01 Implement decryption via the rust sdk (#3074)
A bunch of changes to tests, and wire up decryption.
2023-01-18 16:47:44 +00:00
RiotRobot 2fcc4811dd Resetting package fields for development 2023-01-18 13:38:53 +00:00
RiotRobot c392bc455d Merge branch 'master' into develop 2023-01-18 13:36:45 +00:00
RiotRobot b8711f15fd v23.1.0 2023-01-18 13:30:21 +00:00
RiotRobot 81f3aef960 Prepare changelog for v23.1.0 2023-01-18 13:30:19 +00:00
Richard van der Hoff d6b8332567 Element-R: stub implementations of some methods (#3075)
These are all called by the react-sdk when showing an encrypted event:

 * `getEventEncryptionInfo`
 * `checkUserTrust`
 * `checkDeviceTrust`

I don't particularly want to keep this API, but as a rapid means to an end,
let's stub them for now.
2023-01-18 12:07:49 +00:00
Richard van der Hoff 85b34b46c5 Remove brokenheaded encryption test (#3070)
This test seemed to be testing the behaviour of decrypting redacted events, but
that seems... strange. A redaction event cannot be encrypted (at least, there
is no spec for it), and it should be impossible to decrypt a (correctly)
redacted event, because such an event will lack a `ciphertext` property.

This test is just sticking a "redacted_because" property into a regular event,
which is a bit of a nonsense.
2023-01-17 11:27:30 +00:00
RiotRobot 4179f2978d v23.1.0-rc.4 2023-01-17 09:11:54 +00:00
RiotRobot 97df6db49c Prepare changelog for v23.1.0-rc.4 2023-01-17 09:11:52 +00:00
ElementRobot 3e693fab23 [Backport staging] Correctly handle limited sync responses by resetting the thread timeline (#3069)
Co-authored-by: Janne Mareike Koschinski <jannemk@element.io>
2023-01-17 09:06:28 +00:00
Clark Fischer 1ee487a2ff Make prepareToEncrypt cancellable.
NOTE: This commit introduces a backwards-compatible API change.

Adds the ability to cancel `MegolmEncryption#prepareToEncrypt` by returning
a cancellation function. The bulk of the processing happens in
`getDevicesInRoom`, which now accepts a 'getter' that allows the caller to
indicate cancellation.

See https://github.com/matrix-org/matrix-js-sdk/issues/1255
Closes #1255

Signed-off-by: Clark Fischer <clark.fischer@gmail.com>
2023-01-16 10:23:13 -08:00
Clark Fischer b76e7ca782 Reduce blocking while pre-fetching Megolm keys
Currently, calling `Client#prepareToEncrypt` in a megolm room has the potential
to block for multiple seconds while it crunches numbers.

Sleeping for 0 seconds (approximating `setImmediate`) allows the engine to
process other events, updates, or re-renders in between checks.

See
- https://github.com/vector-im/element-web/issues/21612
- https://github.com/vector-im/element-web/issues/11836

Signed-off-by: Clark Fischer <clark.fischer@gmail.com>
2023-01-16 10:11:37 -08:00
Clark Fischer ddce1bcd28 Add async setImmediate util
Adds an async/promise-based version of `setImmediate`. Note that, despite being
poorly adopted, `setImmediate` is polyfilled, and should be more performant
than `sleep(0)`.

Signed-off-by: Clark Fischer <clark.fischer@gmail.com>
2023-01-16 10:11:37 -08:00
Janne Mareike Koschinski a34d06c7c2 Correctly handle limited sync responses by resetting the thread timeline (#3056)
* Reset thread livetimelines when desynced
* Implement workaround for https://github.com/matrix-org/synapse/issues/14830
2023-01-16 16:27:28 +00:00
David Baker 7b10fa367d Merge pull request #3066 from matrix-org/dbkr/olm_savesession_undecryptable_todevice_debug
Add some debugging & a debug event for decryption
2023-01-13 21:40:36 +00:00
Šimon Brandner 7f5d7091de Fix typos in src/webrtc/ (#3065) 2023-01-13 19:56:55 +01:00
David Baker 96f673ae92 Merge branch 'develop' into dbkr/olm_savesession_undecryptable_todevice_debug 2023-01-13 18:36:22 +00:00
David Baker 79faee7a67 Add emit so tests don't throw 2023-01-13 18:32:21 +00:00
David Baker 89d2984432 Add some debugging & a debug event for decryption
Adds a log line whenever we save a session and also adds an event
that's fired whenever we get a to-device event we can't decrypt
(hopefully the comment explains all).
2023-01-13 18:24:33 +00:00
Travis Ralston bc78784688 Extract v1 extensible events polls types out of the events-sdk (#3062)
* Extract v1 extensible events polls out of events-sdk

* Appease tsdoc?

* Appease naming standards

* Bring the tests over too
2023-01-13 10:02:27 -07:00
Richard van der Hoff eb058edb1b Fix spurious "Decryption key withheld" messages (#3061)
When we receive an `m.unavailable` notification, do not show it as "Decryption
key withheld".
2023-01-13 13:17:48 +00:00
Richard van der Hoff 4847d78b42 Improvements to megolm integration tests (#3060)
The megolm tests were making a few assumptions which they really shouldn't; in
particular:

 * They were creating mock events with event_ids not starting `$`, and lacking
   `sender`, `origin_server_ts` and `unsigned` properties

 * They were not including the (now) required `keys.ed25519` property inside
   the ciphertext of an olm message.

These work ok currently, but they aren't really correct, and they cause
problems when testing the new rust implementation.
2023-01-13 13:14:44 +00:00
RiotRobot 94f1eda830 v23.1.0-rc.3 2023-01-13 10:40:31 +00:00
RiotRobot bc2a182ee9 Prepare changelog for v23.1.0-rc.3 2023-01-13 10:40:29 +00:00
renovate[bot] 789aec732a Update typescript-eslint monorepo to v5.48.1 (#3063)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-13 09:08:54 +00:00
renovate[bot] 3246114772 Update dependency docdash to v2.0.1 (#3057)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-12 18:38:54 +00:00
ElementRobot 39cf1863f1 Fix failure to start in firefox private browser (#3058) (#3059)
(cherry picked from commit aa1e118f18)

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-01-12 17:10:42 +00:00
Richard van der Hoff aa1e118f18 Fix failure to start in firefox private browser (#3058) 2023-01-12 17:03:16 +00:00
RiotRobot c10152e098 v23.1.0-rc.2 2023-01-12 13:31:20 +00:00
RiotRobot 6b7efbcd91 Prepare changelog for v23.1.0-rc.2 2023-01-12 13:31:18 +00:00
Richard van der Hoff d23c3cb8b2 Improve logging in legacy megolm code (#3043)
* Use a PrefixedLogger throughout `megolm.ts`

Rather than manually adding `in ${this.roomId}` to each log line, use a
PrefixedLogger to achieve the same effect more consistently.

* Clean up logging in megolm.ts

Where we log a list of devices, we don't need the whole deviceinfo, just the
device id. All that noise makes it very hard to read the logs.

* Log users that we find in the room when encrypting

* Reduce log verbosity on decryption retries
2023-01-12 11:49:32 +00:00
Richard van der Hoff 9e37980e2d Handle edits which are bundled with an event, per MSC3925 (#3045) 2023-01-12 10:53:36 +00:00
renovate[bot] de176dbd66 Update all non-major dependencies (#3053)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-11 16:55:07 +00:00
renovate[bot] ac10b40f67 Update dependency @babel/core to v7.20.12 (#3054)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-11 16:46:47 +00:00
Michael Telatynski f35298a326 [Release] Fix browser entrypoint (#3052) 2023-01-11 16:31:02 +00:00
Michael Telatynski d7bf0f85c0 Fix browser entrypoint (#3051) 2023-01-11 15:17:55 +00:00
RiotRobot 1d87f5b163 v23.1.0-rc.1 2023-01-11 13:29:05 +00:00
RiotRobot 3e97067b3e Prepare changelog for v23.1.0-rc.1 2023-01-11 13:29:02 +00:00
Michael Telatynski 3ce582d004 Fix dated example (#3049) 2023-01-11 13:08:30 +00:00
Michael Telatynski 3e48c76a77 Avoid use of mkdirp (#3050) 2023-01-11 12:02:56 +00:00
Germain 8e29f8ead0 Improve hasUserReadEvent and getUserReadUpTo realibility with threads (#3031)
Co-authored-by: Patrick Cloke <clokep@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-01-11 09:53:27 +00:00
Travis Ralston 185ded4ebc Remove extensible events v1 field population on legacy events (#3040)
* Remove extensible events v1 field population on legacy events

With extensible events v2, affected events are now gated by a room version, so we don't need this code anymore. 

The proposal has generally moved away from mixing m.room.message with extensible fields as well.

* Run prettier

* Remove unstable identifier from tests too

* Run prettier again
2023-01-10 09:19:55 -07:00
Andy Balaam 3564a3546f Merge pull request #3039 from andybalaam/factor-out-findPredecessor
Factor out a (public) function to find a room's predecessor
2023-01-10 13:43:59 +00:00
Andy Balaam c6090325b3 Update id string to reflect spec 2023-01-10 13:29:06 +00:00
Andy Balaam 999e355136 Use Room.getLiveTimeline instead of deprecated this.currentState 2023-01-10 10:16:22 +00:00
Andy Balaam 7de4164444 Factor out a (public) function to find a room's predecessor 2023-01-10 10:16:22 +00:00
Andy Balaam e2ce379b56 Merge pull request #3038 from andybalaam/andybalaam/tests-for-getVisibleRooms
Tests for getVisibleRooms
2023-01-10 10:07:53 +00:00
Andy Balaam 424212cd65 Merge pull request #2915 from matrix-org/madlittlemods/stablize-msc3030-timestamp-to-event
Prefer stable `/timestamp_to_event` endpoint first - MSC3030
2023-01-09 17:01:10 +00:00
Andy Balaam c7c16256df Update comment to reflect commonality between 404 and 405 status 2023-01-09 16:53:52 +00:00
Andy Balaam 8a4c95ee72 Tests for getVisibleRooms 2023-01-09 12:17:53 +00:00
Clark Fischer c3d422f5fb Remove 'qs' dependency (#3033)
'qs' appears to be unused since 34c5598 (PR #2719).

Signed-off-by: Clark Fischer <clark.fischer@gmail.com>

Signed-off-by: Clark Fischer <clark.fischer@gmail.com>
2023-01-06 22:37:05 +00:00
Šimon Brandner fdb80ad259 Remove video track when muting video (#3028) 2023-01-06 12:09:01 -05:00
Andy Balaam 981acf0044 Rename test to fit renamed function.
Co-authored-by: Eric Eastwood <erice@element.io>
2023-01-06 16:38:02 +00:00
Andy Balaam f00f70bfb8 Merge branch 'develop' into madlittlemods/stablize-msc3030-timestamp-to-event 2023-01-06 15:38:08 +00:00
Andy Balaam 12cc7be31c Test 400, 429 and 502 responses 2023-01-06 15:33:48 +00:00
Andy Balaam 628bcbf33a Fall back to the unstable endpoint if we receive a 405 status 2023-01-06 15:22:58 +00:00
Andy Balaam c4ca0b2e07 Refactor timestampToEvent tests 2023-01-06 15:13:44 +00:00
Andy Balaam d7442147b9 Rename convertQueryDictToMap 2023-01-06 14:21:35 +00:00
Andy Balaam b1566ee540 Switch to a Map for convertQueryDictToStringRecord 2023-01-06 14:20:24 +00:00
Andy Balaam ca98d9ff11 Tests for convertQueryDictToStringRecord 2023-01-06 14:20:24 +00:00
Andy Balaam bba4a35665 Merge pull request #3034 from matrix-org/johannes/poll-start-sent-receipt
Make poll start event type available (PSG-962)
2023-01-06 11:10:00 +00:00
Germain 896f6227a0 Fix threaded cache receipt when event holds multiple receipts (#3026) 2023-01-06 10:27:35 +00:00
Johannes Marbach 4d10cf3074 Make poll start event type available 2023-01-06 10:17:29 +01:00
Kerry bb23df9423 Add alt event type matching in Relations model (#3018)
* allow alt event types in relations model

* remove unneccesary checks on remove relation

* comment

* assert on event emitted
2023-01-05 20:00:12 +00:00
Richard van der Hoff d02559cf3c Make error handling in decryptionLoop more generic (#3024)
Not everything is a `DecryptionError`, and there's no real reason that we
should only do retries for `DecryptionError`s
2023-01-05 15:02:19 +00:00
Richard van der Hoff ec6272aa3d Fix outgoing messages for rust-crypto (#3025)
It turns out that MatrixClient uses a `FetchHttpApi` instance with
`opts.onlyData = true`, so it was returning the json-parsed response rather
than the raw response. Change the way we call `authedRequest` so that we get
the raw body back.
2023-01-05 15:00:37 +00:00
Michael Weimann 695b773f8b Fix false key requests after verifying new device (#3029) 2023-01-05 15:27:09 +01:00
Richard van der Hoff 030abe1563 Pass to-device messages into rust crypto-sdk (#3021)
We need a separate API, because `ClientEvent.ToDeviceEvent` is only emitted for
successfully decrypted to-device events
2023-01-05 09:54:56 +00:00
Richard van der Hoff 22f10f71b8 indirect decryption attempts via Client (#3023)
... to reduce the number of things referring to `client.crypto`
2023-01-05 09:54:44 +00:00
renovate[bot] 9ca3e7272e chore(deps): lock file maintenance (#3014)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-04 13:44:26 -07:00
Richard van der Hoff 64119ef915 Avoid logical assignment operator (#3022)
Apparently `??=` was only added to javascript in ES12, and our eleweb build
doesn't support it.

Fixes breakage introduced in #3019.
2023-01-04 14:17:55 +00:00
Richard van der Hoff 9ac7165e99 Handle outgoing requests from rust crypto SDK (#3019)
The rust matrix-sdk-crypto has an `outgoingRequests()` method which we need to poll, and make the requested requests.
2023-01-04 12:17:42 +00:00
Hubert Chathi 6168cedf32 Avoid triggering decryption errors when decrypting redacted events (#3004) 2023-01-03 11:06:54 -05:00
Richard van der Hoff 7c34deecb6 Pass CryptoBackend into SyncApi (#3010)
I need to start calling back into the new rust crypto implementation from the /sync loops, so I need to pass it into SyncApi. To reduce the coupling, I've defined a new interface specifying the methods which exist for that purpose. Currently it's only onSyncCompleted.
2023-01-03 15:37:51 +00:00
Ankit cef5507ab1 Include 'yarn install' . (#3011) 2023-01-03 14:07:37 +00:00
Richard van der Hoff 9b372d23ca Factor SyncApi options out of IStoredClientOptions (#3009)
There are a couple of callback interfaces which are currently stuffed into
`IStoredClientOpts` to make it easier to pass them into the `SyncApi`
constructor.

Before we add more fields to this, let's separate it out to a separate object.
2023-01-03 13:38:21 +00:00
renovate[bot] e9fef19c8f Update dependency @types/node to v18.11.18 (#2984)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-03 13:28:02 +00:00
Andy Balaam 9ebc91aa5a Merge pull request #3017 from andybalaam/andybalaam/use-env-variable-for-ref_name
In release.yml use an env section to get ref_name
2023-01-03 13:10:05 +00:00
Andy Balaam fcb75d547e Remove unneeded variable in workflow 2023-01-03 12:25:13 +00:00
Andy Balaam 33c9af952e Reformat release.yml 2023-01-03 10:25:59 +00:00
Andy Balaam 3b66b28e71 In release.yml use an env section to get ref_name 2023-01-03 10:21:40 +00:00
Travis Ralston 7d37bb1edb Remove usage of v1 Identity Server API (#3003)
* Remove usage of v1 Identity Server API

It's been deprecated for over a year at this point - everyone should be able to support v2.

* Missed one.
2023-01-03 00:59:12 -07:00
Travis Ralston 0f717a9306 Update comment for m.key.verification.ready event type definition (#3006) 2023-01-03 00:58:55 -07:00
Richard van der Hoff ce776b9989 Break coupling between Room and Crypto.trackRoomDevices (#2998)
`Room` and `Crypto` currently have some tight coupling in the form of a call to
`trackRoomDevices` when out-of-band members are loaded. We can improve this by
instead having Crypto listen out for a `RoomSateEvent.Update` notification.
2022-12-23 11:03:14 +00:00
kegsay ff1b0e51ea Merge pull request #3008 from matrix-org/kegan/upload-otks
bugfix: upload OTKs in sliding sync mode
2022-12-22 16:26:47 +01:00
Kegan Dougal 21e66a5c34 bugfix: upload OTKs in sliding sync mode 2022-12-22 14:52:16 +00:00
Germain aead401005 Apply edits discovered from sync after thread is initialised (#3002) 2022-12-22 08:54:55 +00:00
Travis Ralston af9525ed5f Add device_id to /account/whoami types (#3005)
* Add `device_id` to `/account/whoami` types

https://spec.matrix.org/v1.5/client-server-api/#get_matrixclientv3accountwhoami

* Appease the linter

* Modernize area of code

* Remove unused eslint disable comment
2022-12-21 16:46:10 -07:00
RiotRobot 1ebcac37cc Resetting package fields for development 2022-12-21 16:49:19 +00:00
RiotRobot 51a4cc5e18 Fix post-release script compatibility with prettier 2022-12-21 16:49:11 +00:00
RiotRobot 48baa6315c Merge branch 'master' into develop
# Conflicts:
#	release.sh
#	spec/integ/matrix-client-event-timeline.spec.ts
2022-12-21 16:48:43 +00:00
RiotRobot efc87d8084 v23.0.0 2022-12-21 16:40:30 +00:00
RiotRobot aab873fc58 Prepare changelog for v23.0.0 2022-12-21 16:40:28 +00:00
RiotRobot 52ed04c825 Fix release call to prettier 2022-12-21 16:38:37 +00:00
ElementRobot fdd1428e19 [Backport staging] Fix release scripts to not fight with prettier (#3000)
(cherry picked from commit ec2405ac99)

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-12-21 09:37:40 +00:00
Michael Telatynski ec2405ac99 Fix release scripts to not fight with prettier (#2999) 2022-12-21 09:31:16 +00:00
ElementRobot b31712f2ff [Backport staging] Threads are missing from the timeline (#2997)
(cherry picked from commit 4f86eee250)

Co-authored-by: Janne Mareike Koschinski <jannemk@element.io>
2022-12-20 17:27:18 +00:00
kegsay 61e2606bc4 Merge pull request #2991 from S7evinK/s7evink/slidingsync-unsub
sliding sync: Fix issue where no unsubs are sent when switching rooms
2022-12-20 17:57:19 +01:00
Richard van der Hoff 45f6c5b079 Add exportRoomKeys to CryptoBackend (#2970)
Element-web calls `exportRoomKeys` on logout, so we need a stub implementation
to get it EW working with the rust crypto sdk.
2022-12-20 11:11:00 +00:00
Michael Weimann b83c372848 Implement MSC3912: Relation-based redactions (#2954) 2022-12-20 09:22:26 +01:00
Šimon Brandner 6d58a54039 Don't use RoomMember as a calls a key on GroupCall (#2993) 2022-12-19 14:53:08 +01:00
Till 3872c5f099 Update src/sliding-sync.ts
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-12-19 11:54:20 +01:00
Janne Mareike Koschinski 4f86eee250 Threads are missing from the timeline (#2996) 2022-12-19 10:32:37 +00:00
David Baker 618242ef3c Merge pull request #2992 from matrix-org/dbkr/close_all_streams
Close all streams when a call ends
2022-12-19 10:31:36 +00:00
David Baker 96ee5b1256 Close all streams when a call ends
We didn't close streams in group calls (presumably from back when
we used the same stream for all calls rather than cloning?) but this
left stray screenshare streams in the mediahandler when a participant
left whilst we were screensharing.

Fixes https://github.com/vector-im/element-call/issues/742
2022-12-16 18:19:36 +00:00
Till Faelligen 8af0ff111b Add tests for custom subscriptions 2022-12-16 13:30:40 +01:00
Till Faelligen a04800f030 Fix issue where no unsubs are sent when switching rooms 2022-12-16 11:49:31 +01:00
Eric Eastwood 4683fbe848 Merge branch 'develop' into madlittlemods/stablize-msc3030-timestamp-to-event
Conflicts:
	spec/unit/matrix-client.spec.ts
2022-12-16 00:56:06 -06:00
renovate[bot] c973b26fa2 Update all non-major dependencies (#2981)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-15 22:06:40 -07:00
renovate[bot] 7b96c730b8 Update typescript-eslint monorepo to v5.46.0 (#2985)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-15 22:05:22 -07:00
renovate[bot] f8bf6083de Update dependency uuid to v9 (#2988)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-15 22:05:05 -07:00
Anderas 447319737a Merge pull request #2920 from Anderas/resume-todevice-queue
Resume to-device message queue after resumed sync
2022-12-14 14:27:47 +00:00
Janne Mareike Koschinski 39e127b4e3 Write test to validate #2971 (#2972) 2022-12-14 13:01:52 +01:00
Richard van der Hoff 15ef8fabb7 Introduce a mechanism for using the rust-crypto-sdk (#2969)
This PR introduces MatrixClient.initRustCrypto, which is similar to initCrypto, except that it will use the Rust crypto SDK instead of the old libolm-based implementation.

This is very much not something you want to use in production code right now, because the integration with the rust sdk is extremely skeletal and almost everything crypto-related will raise an exception rather than doing anything useful.

It is, however, enough to demonstrate the loading of the wasmified rust sdk in element web, and a react sdk with light modifications can successfully log in and out.

Part of vector-im/element-web#21972.
2022-12-14 11:02:02 +00:00
renovate[bot] df42014ef5 Update dependency @types/jest to v29.2.4 (#2980)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-14 10:55:40 +00:00
Michael Weimann b765b18381 Add prettier formatting to .git-blame-ignore-revs (#2979) 2022-12-14 11:46:18 +01:00
RiotRobot 6cc8e4436c v23.0.0-rc.1 2022-12-14 08:49:15 +00:00
RiotRobot 7f1fe46c7c Prepare changelog for v23.0.0-rc.1 2022-12-14 08:49:15 +00:00
Kerry b2a10e6db3 Support MSC3391: Account data deletion (#2967)
* add deleteAccountData endpoint

* check server support and test

* test current state of memorystore

* interpret account data events with empty content as deleted

* add handling for (future) stable version of endpoint

* add getSafeUserId

* user getSafeUserId in deleteAccountData

* better jsdoc for throws documentation
2022-12-14 04:14:21 +00:00
Eric Eastwood a0aa5074ed Fix prefix lint
See https://github.com/matrix-org/matrix-js-sdk/pull/2915#discussion_r1043951639
2022-12-13 16:37:47 -06:00
Eric Eastwood 70a033c2fd Prettier fixes 2022-12-13 16:26:14 -06:00
Eric Eastwood fcf12b49e3 Merge branch 'develop' into madlittlemods/stablize-msc3030-timestamp-to-event
Conflicts:
	spec/unit/matrix-client.spec.ts
	src/client.ts
2022-12-13 16:18:33 -06:00
RiotRobot 284a475dfb Exclude CHANGELOG from prettier and undo what it did 2022-12-13 15:35:12 +00:00
Janne Mareike Koschinski 193c38523c Fix messages loaded during initial fetch ending up out of order (#2971)
* Fix messages loaded during initial fetch ending up out of order
2022-12-13 15:23:43 +01:00
Andy Uhnak 11ac3d9e58 Add unit tests 2022-12-13 12:26:52 +00:00
Andy Uhnak 071d5e71e4 Resume to-device message queue after resumed sync 2022-12-13 11:54:52 +00:00
Michael Telatynski acd220b8a9 Tweak how the SonarCloud scan fails (#2947) 2022-12-13 09:59:52 +00:00
Germain 74b30246d0 Avoid creating duplicate threads (#2968) 2022-12-13 09:54:44 +00:00
Richard van der Hoff 9c17eb6c14 Begin factoring out a CryptoBackend interface (#2955)
Part of https://github.com/vector-im/element-web/issues/21972. Eventually I want to replace the whole of the current `Crypto` implementation with an alternative implementation, but in order to get from here to there, I'm factoring out a common interface which will be implemented by both implementations.

I'm also determined to fix the problem where the innards of the crypto implementation are exposed to applications via the `MatrixClient.crypto` property.

It's not (yet) entirely clear what shape this interface should be, so I'm going with a minimal approach and adding things as we know we need them. This means that we need to keep the old `client.crypto` property around as well as a new `client.cryptoBackend` property. Eventually `client.crypto` will go away, but that will be a breaking change in the js-sdk.
2022-12-12 17:49:39 +00:00
Janne Mareike Koschinski 8293011ee2 Fix #23916: Prevent edits of the last message in a thread getting lost (#2951)
* Fix issue where the root event of a thread had to be loaded in a complicated way
* Fix issue where edits to the last event of a thread would get lost
* Fix issue where thread reply count would desync
* Refactor relations pagination mocking for tests
2022-12-12 18:22:16 +01:00
Richard van der Hoff 4c5f416b32 Factor out some utility functions in the megolm integration tests (#2958)
There's a lot of repetition here, which can be reduced with some utility functions.
2022-12-12 16:31:56 +00:00
Janne Mareike Koschinski 66c678e9fb Fix #23919: Root message for new thread loaded from network (#2965)
* Load root events of threads without additional network roundtrip
2022-12-12 16:30:48 +01:00
Richard van der Hoff 8a892ede23 Exclude generated files from prettier (#2961)
The example directories include symlinks to the generated matrix.js, which we
should not prettify.
2022-12-12 10:37:52 +00:00
Germain 2dd06e368e Fix infinite loop when restoring cached read receipts (#2963) 2022-12-09 15:20:50 +00:00
Damir Jelić fc501de081 Don't swallow up errors coming from the shareSession call (#2962)
A call to ensureSession() has two steps:
    1. prepareSession(), where an outbound group session might get created
       or rotated
    2. shareSession(), where an outbound group session might get
       encrypted and queued up to be sent to other devices

Both of those calls may mostly fail due to storage errors, yet only the
errors from prepareSession get propagated to the caller.

Errors from prepareSession will mean that you can't get an
outbound group session so you can't encrypt an event.

Errors from shareSession, especially if the error happens in the part
where the to-device requests are queued up to be sent out, mean that
other people will not be able to decrypt the events that will get
encrypted using the outbound group session.

Both of those cases are catastrophic, the second case is just much
harder to debug, since the error happens on another device at some
arbitrary point in the future.

Let's just return the error instead, people can then retry and the
storage issue might have been resolved, or at least the error becomes
visible when it happens.
2022-12-09 15:07:42 +00:00
Damir Jelić ada401f4c0 Make sure that MegolmEncryption.setupPromise always resolves (#2960)
ensureOutboundSession uses and modifies the setupPromise of the
MegolmEncryption class. Some comments suggest that setupPromise will
always resolve, in other words it should never contain a promise that
will get rejected.

Other comments also seem to suggest that the return value of
ensureOutboundSession, a promise as well, may fail.

The critical error here is that the promise that gets set as
the next setupPromise, as well as the promise that ensureOutboundSession
returns, is the same promise.

It seems that the intention was for setupPromise to contain a promise
that will always resolve to either `null` or `OutboundSessionInfo`.

We can see that a couple of lines before we set setupPromise to its new
value we construct a promise that logs and discards errors using the
`Promise.catch()` method.

The `Promise.catch()` method does not mutate the promise, instead it
returns a new promise. The intention of the original author might have
been to set the next setupPromise to the promise which `Promise.catch()`
produces.

This patch modifies the updating of setupPromise in the
ensureOutboundSession so that setupPromise discards errors correctly.

Using `>>=` to represent the promise chaining operation, setupPromise is
now updated using the following logic:

    setupPromise = previousSetupPromise >>= setup >>= discardErrors
2022-12-09 14:46:33 +00:00
Damir Jelić 41d762171e Apply prettier to the client.ts file (#2959) 2022-12-09 15:03:49 +01:00
Germain 5b6bebc1d7 Do not calculate highlight notifs for threads unknown to the room (#2957) 2022-12-09 12:41:51 +00:00
Andy Balaam 7b5e137ec0 Merge pull request #2906 from matrix-org/weeman1337/prettier
Add prettier
2022-12-09 12:16:16 +00:00
Michael Weimann 6e0901258c Switch to eslint-plugin-matrix-org 0.9 2022-12-09 10:51:15 +01:00
Michael Weimann 559fbdda26 Update eslint-plugin-matrix-org 2022-12-09 09:54:36 +01:00
Michael Weimann 72dac9a107 Apply manual code style fixes after prettier 2022-12-09 09:43:22 +01:00
Michael Weimann 349c2c2587 Apply prettier formatting 2022-12-09 09:38:20 +01:00
Michael Weimann 08a9073bd5 Add prettier 2022-12-09 09:34:01 +01:00
Eric Eastwood 9841f92415 Fix some eslint 2022-12-08 18:37:33 -06:00
Eric Eastwood ed91bd9c11 Merge branch 'develop' into madlittlemods/stablize-msc3030-timestamp-to-event
Conflicts:
	spec/unit/matrix-client.spec.ts
	src/client.ts
2022-12-08 18:29:14 -06:00
Eric Eastwood c953fc9fb7 Update casing
See https://github.com/matrix-org/matrix-js-sdk/pull/2915#discussion_r1041542066
2022-12-08 17:56:53 -06:00
Eric Eastwood bf78a64d82 Remove console coloring in favor of future PR
See https://github.com/matrix-org/matrix-js-sdk/pull/2915#discussion_r1041539703
2022-12-08 17:47:56 -06:00
Šimon Brandner ae849fdd46 Minor VoIP stack improvements (#2946)
* Add `IGroupCallRoomState`

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

* Export values into `const`s

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

* Add `should correctly emit LengthChanged`

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

* Add `ICE disconnected timeout`

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

* Improve typing

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

* Don't cast `getContent()`

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

* Use `Date.now()` for call length

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

* Type fix

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

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2022-12-08 19:51:05 +01:00
Richard van der Hoff 39cf212628 Expose a new 'userHasCrossSigningKeys' method (#2950) 2022-12-08 11:53:38 +00:00
Richard van der Hoff 224e592701 Fix examples/browser/browserTest.js (#2952)
This seems to have been broken for ages
2022-12-08 10:43:20 +00:00
Germain 16d791b038 Cache read receipts for unknown threads (#2953) 2022-12-08 09:54:10 +00:00
Michael Telatynski c4006d752a Improve tsdoc types (#2940)
* Install eslint-plugin-jsdoc

* Enable lint rule jsdoc/no-types

* Make tsdoc more valid, add required hyphens and s/return/returns/g

* Stash tsdoc work

* Fix mistypes

* Stash

* Stash

* More tsdoc work

* Remove useless doc params

* Fixup docs

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/crypto/verification/request/ToDeviceChannel.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/client.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/client.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/client.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Iterate

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2022-12-07 18:01:54 +00:00
Richard van der Hoff a9e7a46c56 Upload device keys during initCrypto (#2872)
Rather than waiting for the application to call `.startClient`, upload the
device keys during `initCrypto()`. Element-R is going to approach this slightly
differently (it wants to manage the decision on key uploads itself), so this
lays some groundwork by collecting the libolm-specific bits together.
2022-12-07 13:48:41 +00:00
Michael Telatynski 4a7365f32f Fix release documentation (#2949) 2022-12-07 13:06:41 +00:00
Germain a071a82a03 Update test runner instructions (#2948) 2022-12-07 11:18:32 +00:00
Michael Telatynski 8d018f9c2d Enable noImplicitAny (#2895)
* Stash noImplicitAny work

* Enable noImplicitAny

* Update olm

* Fun

* Fix msgid stuff

* Fix tests

* Attempt to fix Browserify
2022-12-06 18:21:44 +00:00
Richard van der Hoff 6f81371e61 Fix the message ID on key-share messages (#2945)
https://github.com/matrix-org/matrix-js-sdk/pull/2938 introduced message IDs on
outgoing to-device messages, but a typo meant that the IDs on key-share
messages were excessive.
2022-12-06 16:45:21 +00:00
RiotRobot ccab6985ad Resetting package fields for development 2022-12-06 12:34:38 +00:00
RiotRobot 569adc7b0c Merge branch 'master' into develop 2022-12-06 12:34:35 +00:00
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
Richard van der Hoff 683e7fba4a Add a message ID on each to-device message (#2938)
To make it easier to track down where to-device messages are getting lost,
add a custom property to each one, and log its value. Synapse will also log
this property.
2022-12-06 10:31:48 +00:00
Šimon Brandner 2c8eece5ca Don't expose calls on GroupCall (#2941) 2022-12-05 18:44:13 +01:00
kegsay 4a4d493856 bugfix: sliding sync initial room timelines shouldn't notify (#2933)
* bugfix: sliding sync initial room timelines shouldn't notify

Flag timeline events as `fromCache` when `initial: true` rooms
are received. This stops notifications appearing inappropriately
when you scroll the room list or spider the room list, as it
causes `liveEvent=false`.

* Use num_live to detect liveness; with jest test

* Linting

* jsdoc

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2022-12-05 17:00:06 +00:00
Michael Weimann 11d8f562c5 Redo key sharing after own device verification (#2921) 2022-12-05 14:31:58 +01:00
Michael Telatynski 7799804762 Move @types deps into devDeps (#2927) 2022-12-02 16:27:59 +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
Janne Mareike Koschinski 8a7fd270e4 Move updated threads to the end of the thread list (#2923)
* Move updated threads to the end of the thread list
* Write new tests
2022-12-02 17:11:18 +01: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
Germain 53a45a34df Fix highlight notifications increasing when total notification is zero (#2937) 2022-12-02 15:41:15 +00:00
Janne Mareike Koschinski fa2eeac5b8 Improve perceived performance for threads (#2901)
* Improve perceived performance for threads
* Improve method naming and make it private
2022-12-02 15:50:19 +01:00
Janne Mareike Koschinski 720248466f Include pending events in thread summary and count again (#2922)
* Include pending events in thread summary and count again
* Pass through pending event status
2022-12-02 15:01:43 +01:00
Janne Mareike Koschinski 43bfa0c020 Switch to stable /relations endpoint, stop using unspecced original_event field (#2911)
* Switch to stable /relations endpoint, stop using unspecced original_event field
* Adapt the tests to the changed endpoint
2022-12-02 15:01:15 +01:00
Robin c17deb0806 Backport "Make GroupCall work better with widgets" to staging (#2936) 2022-12-02 10:34:41 +00:00
Robin 79ccd7c330 Merge pull request #2935 from robintown/entered-via-widget
Make GroupCall work better with widgets
2022-12-01 23:33:44 -05:00
Robin Townsend 9de9ff76b5 Test cleanMemberState 2022-12-01 23:27:31 -05:00
Robin Townsend c0090852ad Make GroupCall work better with widgets
If the client uses a widget to join group calls, like Element Web does, then the local device could be joined to the call without GroupCall knowing. This adds a field to GroupCall that allows the client to tell GroupCall when it's using another session to join the call.
2022-12-01 10:45:34 -05:00
Faye Duxovni 3870e3395d Add method to get outgoing room key requests for a given event (#2930)
* Add method to get outgoing room key requests for a given event

* Write test, fix typo

* Add test case for non-encrypted event
2022-12-01 09:49:36 +00:00
renovate[bot] a0f3e5d3bf Update babel monorepo (#2929)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-01 07:25:08 +00:00
renovate[bot] 720ea0e12e Update all non-major dependencies (#2928)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-01 07:24:50 +00:00
Eric Eastwood 9a98e8008f Fix relevant strict ts error 2022-11-30 19:13:29 -06:00
Eric Eastwood ad8bb5d2cd Fix lints 2022-11-30 19:01:20 -06:00
Eric Eastwood 9a731cdf4f Add some comments 2022-11-30 18:53:23 -06:00
Eric Eastwood d1ede036e2 Add return type 2022-11-30 18:51:49 -06:00
Eric Eastwood d3f08fec03 Add tests 2022-11-30 18:45:05 -06:00
renovate[bot] d692a5dbe2 Update tspascoal/get-user-teams-membership action to v2 (#2925)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-30 14:22:05 +00:00
renovate[bot] 4362297edc Update dependency @typescript-eslint/eslint-plugin to v5.44.0 (#2924)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-30 14:21:43 +00:00
Richard van der Hoff 1606274c36 Process m.room.encryption events before emitting RoomMember events (#2914)
vector-im/element-web#23819 is an intermittent failure to correctly initiate a user verification process. The
root cause is as follows:

* In matrix-react-sdk, ensureDMExists tries to create an encrypted DM room, and assumes it is ready for use
  (including sending encrypted events) as soon as it receives a RoomStateEvent.NewMember notification
  indicating that the other user has been invited or joined. 

* However, in sync.ts, we process the membership events in a /sync response (including emitting
  RoomStateEvent.NewMember notifications), which is long before we process any m.room.encryption event.
    
* The upshot is that we can end up trying to send an encrypted event in the new room before processing
  the m.room.encryption event, which causes the crypto layer to blow up with an error of "Room was 
  previously configured to use encryption, but is no longer".

Strictly speaking, ensureDMExists probably ought to be listening for ClientEvent.Room as well as RoomStateEvent.NewMember; but that doesn't help us, because ClientEvent.Room is also emitted
before we process the crypto event.

So, we need to process the crypto event before we start emitting these other events; but a corollary of that 
is that we need to do so before we store the new room in the client's store. That makes things tricky, because
currently the crypto layer expects the room to have been stored in the client first.

So... we have to rearrange everything to pass the newly-created Room object into the crypto layer, rather than
just the room id, so that it doesn't need to rely on getting the Room from the client's store.
2022-11-30 10:53:38 +00:00
Michael Telatynski 8d6d262e5f Update CODEOWNERS (#2918) 2022-11-30 09:27:23 +00:00
Germain 3577aa98b5 Fix synthesizeReceipt (#2916) 2022-11-30 07:58:29 +00:00
renovate[bot] e0df53c2ed Update dependency terser to v5.16.0 (#2919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-30 01:11:32 +00:00
Marco Bartelt 6611cfa253 add-privileged-users-in-room (#2892) 2022-11-29 19:23:57 +00:00
Michael Weimann 25296bb486 Update @typescript-eslint/parser (#2917) 2022-11-29 18:38:38 +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
Eric Eastwood 3a1897629a Prefer stable endpoint first 2022-11-28 22:50:37 -06: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
402 changed files with 77506 additions and 31618 deletions
+9 -6
View File
@@ -1,12 +1,15 @@
{
"sourceMaps": true,
"presets": [
["@babel/preset-env", {
"targets": {
"node": 10
},
"modules": "commonjs"
}],
[
"@babel/preset-env",
{
"targets": {
"node": 10
},
"modules": "commonjs"
}
],
"@babel/preset-typescript"
],
"plugins": [
+1 -1
View File
@@ -23,4 +23,4 @@ indent_size = 4
trim_trailing_whitespace = true
[*.{yml,yaml}]
indent_size = 2
indent_size = 4
+93 -53
View File
@@ -1,12 +1,9 @@
module.exports = {
plugins: [
"matrix-org",
"import",
],
extends: [
"plugin:matrix-org/babel",
"plugin:import/typescript",
],
plugins: ["matrix-org", "import", "jsdoc"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
parserOptions: {
project: ["./tsconfig.json"],
},
env: {
browser: true,
node: true,
@@ -27,63 +24,106 @@ module.exports = {
"padded-blocks": ["error"],
"no-extend-native": ["error"],
"camelcase": ["error"],
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
"space-before-function-paren": ["error", {
"anonymous": "never",
"named": "never",
"asyncArrow": "always",
}],
"no-multi-spaces": ["error", { ignoreEOLComments: true }],
"space-before-function-paren": [
"error",
{
anonymous: "never",
named: "never",
asyncArrow: "always",
},
],
"arrow-parens": "off",
"prefer-promise-reject-errors": "off",
"quotes": "off",
"indent": "off",
"no-constant-condition": "off",
"no-async-promise-executor": "off",
// We use a `logger` intermediary module
"no-console": "error",
// restrict EventEmitters to force callers to use TypedEventEmitter
"no-restricted-imports": ["error", {
name: "events",
message: "Please use TypedEventEmitter instead"
}],
"no-restricted-imports": [
"error",
{
name: "events",
message: "Please use TypedEventEmitter instead",
},
],
"import/no-restricted-paths": ["error", {
"zones": [{
"target": "./src/",
"from": "./src/index.ts",
"message": "The package index is dynamic between src and lib depending on " +
"whether release or development, target the specific module or matrix.ts instead",
}],
}],
"import/no-restricted-paths": [
"error",
{
zones: [
{
target: "./src/",
from: "./src/index.ts",
message:
"The package index is dynamic between src and lib depending on " +
"whether release or development, target the specific module or matrix.ts instead",
},
],
},
],
// Disabled tests are a reality for now but as soon as all of the xits are
// eliminated, we should enforce this.
"jest/no-disabled-tests": "off",
// Also treat "oldBackendOnly" as a test function.
// Used in some crypto tests.
"jest/no-standalone-expect": [
"error",
{
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly", "newBackendOnly"],
},
],
},
overrides: [{
files: [
"**/*.ts",
],
extends: [
"plugin:matrix-org/typescript",
],
rules: {
// TypeScript has its own version of this
"@babel/no-invalid-this": "off",
overrides: [
{
files: ["**/*.ts"],
plugins: ["eslint-plugin-tsdoc"],
extends: ["plugin:matrix-org/typescript"],
rules: {
// TypeScript has its own version of this
"@babel/no-invalid-this": "off",
// We're okay being explicit at the moment
"@typescript-eslint/no-empty-interface": "off",
// We disable this while we're transitioning
"@typescript-eslint/no-explicit-any": "off",
// We'd rather not do this but we do
"@typescript-eslint/ban-ts-comment": "off",
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
// We're okay being explicit at the moment
"@typescript-eslint/no-empty-interface": "off",
// We disable this while we're transitioning
"@typescript-eslint/no-explicit-any": "off",
// We'd rather not do this but we do
"@typescript-eslint/ban-ts-comment": "off",
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
// The non-TypeScript rule produces false positives
"func-call-spacing": "off",
"@typescript-eslint/func-call-spacing": ["error"],
// The non-TypeScript rule produces false positives
"func-call-spacing": "off",
"@typescript-eslint/func-call-spacing": ["error"],
"quotes": "off",
// We use a `logger` intermediary module
"no-console": "error",
"quotes": "off",
// We use a `logger` intermediary module
"no-console": "error",
},
},
}],
{
// We don't need amazing docs in our spec files
files: ["src/**/*.ts"],
rules: {
"tsdoc/syntax": "error",
// We use some select jsdoc rules as the tsdoc linter has only one rule
"jsdoc/no-types": "error",
"jsdoc/empty-tags": "error",
"jsdoc/check-property-names": "error",
"jsdoc/check-values": "error",
// These need a bit more work before we can enable
// "jsdoc/check-param-names": "error",
// "jsdoc/check-indentation": "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",
},
},
],
};
+2 -1
View File
@@ -38,4 +38,5 @@ cee7f7a280a8c20bafc21c0a2911f60851f7a7ca
7ed65407e6cdf292ce3cf659310c68d19dcd52b2
# Switch to ESLint from JSHint (Google eslint rules as a base)
e057956ede9ad1a931ff8050c411aca7907e0394
# prettier
349c2c2587c2885bb69eda4aa078b5383724cf5e
+8 -1
View File
@@ -1 +1,8 @@
* @matrix-org/element-web
* @matrix-org/element-web
/.github/workflows/** @matrix-org/element-web-app-team
/package.json @matrix-org/element-web-app-team
/yarn.lock @matrix-org/element-web-app-team
/src/webrtc @matrix-org/element-call-reviewers
/src/matrixrtc @matrix-org/element-call-reviewers
/spec/*/webrtc @matrix-org/element-call-reviewers
/spec/*/matrixrtc @matrix-org/element-call-reviewers
+3 -3
View File
@@ -2,9 +2,9 @@
## Checklist
* [ ] Tests written for new code (and old code if feasible)
* [ ] Linter and other CI checks pass
* [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md))
- [ ] Tests written for new code (and old code if feasible)
- [ ] Linter and other CI checks pass
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md))
<!--
If you would like to specify text for the changelog entry other than your PR title, add the following:
+2 -4
View File
@@ -1,6 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"github>matrix-org/renovate-config-element-web"
]
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>matrix-org/renovate-config-element-web"]
}
+26 -26
View File
@@ -1,30 +1,30 @@
name: Backport
on:
pull_request_target:
types:
- closed
- labeled
branches:
- develop
pull_request_target:
types:
- closed
- labeled
branches:
- develop
jobs:
backport:
name: Backport
runs-on: ubuntu-latest
# Only react to merged PRs for security reasons.
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
if: >
github.event.pull_request.merged
&& (
github.event.action == 'closed'
|| (
github.event.action == 'labeled'
&& contains(github.event.label.name, 'backport')
)
)
steps:
- uses: tibdex/backport@v2
with:
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
backport:
name: Backport
runs-on: ubuntu-latest
# Only react to merged PRs for security reasons.
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
if: >
github.event.pull_request.merged
&& (
github.event.action == 'closed'
|| (
github.event.action == 'labeled'
&& contains(github.event.label.name, 'backport')
)
)
steps:
- uses: tibdex/backport@9565281eda0731b1d20c4025c43339fb0a23812e # v2
with:
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+31
View File
@@ -0,0 +1,31 @@
# Triggers after the "Downstream artifacts" build has finished, to run the
# cypress tests (with access to repo secrets)
name: matrix-react-sdk Cypress End to End Tests
on:
workflow_run:
workflows: ["Build downstream artifacts"]
types:
- completed
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
jobs:
cypress:
name: Cypress
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@v3.79.0
permissions:
actions: read
issues: read
statuses: write
pull-requests: read
secrets:
# secrets are not automatically shared with called workflows, so share the cypress dashboard key, and the Kiwi login details
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
TCMS_USERNAME: ${{ secrets.TCMS_USERNAME }}
TCMS_PASSWORD: ${{ secrets.TCMS_PASSWORD }}
with:
react-sdk-repository: matrix-org/matrix-react-sdk
rust-crypto: true
+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@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2
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@v2
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
@@ -0,0 +1,27 @@
name: Build downstream artifacts
on:
# We only want the Rust Crypto Cypress tests on merge queue to prevent regressions
# from creeping in. They take a long time to run and consume 4 concurrent runners.
# Anyone working on Rust Crypto is able to run the tests locally if required.
merge_group:
types: [checks_requested]
# For now at least, we don't run this or the cypress-tests against pushes
# to develop or master.
#
# Note that if we later choose to do so, we'll need to find a way to stop
# the results in Cypress Cloud from clobbering those from the 'develop'
# branch of matrix-react-sdk.
#
#push:
# branches: [develop, master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-element-web:
name: Build element-web
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.79.0
with:
matrix-js-sdk-sha: ${{ github.sha }}
react-sdk-repository: matrix-org/matrix-react-sdk
+22 -22
View File
@@ -1,27 +1,27 @@
name: Notify Downstream Projects
on:
push:
branches: [ develop ]
push:
branches: [develop]
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
notify-downstream:
# Only respect triggers from our develop branch, ignore that of forks
if: github.repository == 'matrix-org/matrix-js-sdk'
continue-on-error: true
strategy:
fail-fast: false
matrix:
include:
- repo: vector-im/element-web
event: element-web-notify
- repo: matrix-org/matrix-react-sdk
event: upstream-sdk-notify
notify-downstream:
# Only respect triggers from our develop branch, ignore that of forks
if: github.repository == 'matrix-org/matrix-js-sdk'
continue-on-error: true
strategy:
fail-fast: false
matrix:
include:
- repo: vector-im/element-web
event: element-web-notify
- repo: matrix-org/matrix-react-sdk
event: upstream-sdk-notify
runs-on: ubuntu-latest
steps:
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: ${{ matrix.repo }}
event-type: ${{ matrix.event }}
runs-on: ubuntu-latest
steps:
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@bf47d102fdb849e755b0b0023ea3e81a44b6f570 # v2
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: ${{ matrix.repo }}
event-type: ${{ matrix.event }}
+80 -83
View File
@@ -1,91 +1,88 @@
name: Pull Request
on:
pull_request_target:
types: [ opened, edited, labeled, unlabeled, synchronize ]
workflow_call:
inputs:
labels:
type: string
default: "T-Defect,T-Deprecation,T-Enhancement,T-Task"
required: false
description: "No longer used, uses allchange logic now, will be removed at a later date"
secrets:
ELEMENT_BOT_TOKEN:
required: true
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
pull_request_target:
types: [opened, edited, labeled, unlabeled, synchronize]
merge_group:
types: [checks_requested]
workflow_call:
secrets:
ELEMENT_BOT_TOKEN:
required: true
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
jobs:
changelog:
name: Preview Changelog
runs-on: ubuntu-latest
steps:
- uses: matrix-org/allchange@main
with:
ghToken: ${{ secrets.GITHUB_TOKEN }}
requireLabel: true
changelog:
name: Preview Changelog
runs-on: ubuntu-latest
steps:
- uses: matrix-org/allchange@main
if: github.event_name != 'merge_group'
with:
ghToken: ${{ secrets.GITHUB_TOKEN }}
requireLabel: true
prevent-blocked:
name: Prevent Blocked
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- name: Add notice
uses: actions/github-script@v6
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with:
script: |
core.setFailed("Preventing merge whilst PR is marked blocked!");
prevent-blocked:
name: Prevent Blocked
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- name: Add notice
uses: actions/github-script@v6
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with:
script: |
core.setFailed("Preventing merge whilst PR is marked blocked!");
community-prs:
name: Label Community PRs
runs-on: ubuntu-latest
if: github.event.action == 'opened'
steps:
- name: Check membership
uses: tspascoal/get-user-teams-membership@v1
id: teams
with:
username: ${{ github.event.pull_request.user.login }}
organization: matrix-org
team: Core Team
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
community-prs:
name: Label Community PRs
runs-on: ubuntu-latest
if: github.event.action == 'opened'
steps:
- name: Check membership
uses: tspascoal/get-user-teams-membership@37c08f7b52a72ca95d12af2e7ab2553ca9adf13b # v2
id: teams
with:
username: ${{ github.event.pull_request.user.login }}
organization: matrix-org
team: Core Team
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
- name: Add label
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
uses: actions/github-script@v6
with:
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['Z-Community-PR']
});
- name: Add label
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
uses: actions/github-script@v6
with:
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['Z-Community-PR']
});
close-if-fork-develop:
name: Forbid develop branch fork contributions
runs-on: ubuntu-latest
if: >
github.event.action == 'opened' &&
github.event.pull_request.head.ref == 'develop' &&
github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Close pull request
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "Thanks for opening this pull request, unfortunately we do not accept contributions from the main" +
" branch of your fork, please re-open once you switch to an alternative branch for everyone's sanity." +
" See https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md",
});
close-if-fork-develop:
name: Forbid develop branch fork contributions
runs-on: ubuntu-latest
if: >
github.event.action == 'opened' &&
github.event.pull_request.head.ref == 'develop' &&
github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Close pull request
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "Thanks for opening this pull request, unfortunately we do not accept contributions from the main" +
" branch of your fork, please re-open once you switch to an alternative branch for everyone's sanity." +
" See https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md",
});
github.rest.pulls.update({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed'
});
github.rest.pulls.update({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed'
});
+31 -33
View File
@@ -1,41 +1,39 @@
# Must only be called from `release#published` triggers
name: Publish to npm
on:
workflow_call:
secrets:
NPM_TOKEN:
required: true
workflow_call:
secrets:
NPM_TOKEN:
required: true
jobs:
npm:
name: Publish to npm
runs-on: ubuntu-latest
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v3
npm:
name: Publish to npm
runs-on: ubuntu-latest
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v4
- name: 🔧 Yarn cache
uses: actions/setup-node@v3
with:
cache: "yarn"
registry-url: 'https://registry.npmjs.org'
- name: 🔧 Yarn cache
uses: actions/setup-node@v3
with:
cache: "yarn"
registry-url: "https://registry.npmjs.org"
- name: 🔨 Install dependencies
run: "yarn install --pure-lockfile"
- name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile"
- name: 🚀 Publish to npm
id: npm-publish
uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}
access: public
tag: next
- name: 🚀 Publish to npm
id: npm-publish
uses: JS-DevTools/npm-publish@5a85faf05d2ade2d5b6682bfe5359915d5159c6c # v2.2.1
with:
token: ${{ secrets.NPM_TOKEN }}
access: public
tag: next
ignore-scripts: false
- name: 🎖️ Add `latest` dist-tag to final releases
if: github.event.release.prerelease == false
run: |
package=$(cat package.json | jq -er .name)
npm dist-tag add "$package@$release" latest
env:
# JS-DevTools/npm-publish overrides `NODE_AUTH_TOKEN` with `INPUT_TOKEN` in .npmrc
INPUT_TOKEN: ${{ secrets.NPM_TOKEN }}
release: ${{ steps.npm-publish.outputs.version }}
- name: 🎖️ Add `latest` dist-tag to final releases
if: github.event.release.prerelease == false && steps.npm-publish.outputs.id
run: npm dist-tag add "$release" latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
release: ${{ steps.npm-publish.outputs.id }}
+41 -47
View File
@@ -1,58 +1,52 @@
name: Release Process
on:
release:
types: [ published ]
release:
types: [published]
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
jsdoc:
name: Publish Documentation
runs-on: ubuntu-latest
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v3
jsdoc:
name: Publish Documentation
runs-on: ubuntu-latest
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v4
- name: 🔧 Yarn cache
uses: actions/setup-node@v3
with:
cache: "yarn"
- name: 🧮 Checkout gh-pages
uses: actions/checkout@v4
with:
ref: gh-pages
path: _docs
- name: 🔨 Install dependencies
run: "yarn install --pure-lockfile"
- name: 🔧 Yarn cache
uses: actions/setup-node@v3
with:
cache: "yarn"
- name: 📖 Generate JSDoc
run: "yarn gendoc"
- name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile"
- 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
- name: 🔨 Install symlinks
run: |
sudo apt-get update
sudo apt-get install -y symlinks
- name: 🧮 Checkout gh-pages
uses: actions/checkout@v3
with:
ref: gh-pages
- name: 📖 Generate docs
run: |
yarn tpv purge --yes --out _docs --stale --major 10
yarn gendoc
symlinks -rc _docs
- name: 🔪 Prepare
run: |
cp -a "$RUNNER_TEMP/$VERSION" .
- name: 🚀 Deploy
run: |
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
git add . --all
git commit -m "Update docs"
git push
working-directory: _docs
# Add the new directory to the index if it isn't there already
if ! grep -q ">Version $VERSION</a>" index.html; then
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' "$VERSION" index.html
fi
- name: 🚀 Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
keep_files: true
publish_dir: .
npm:
name: Publish
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
npm:
name: Publish
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+47 -40
View File
@@ -1,45 +1,52 @@
# Must only be called from a workflow_run in the context of the upstream repo
name: SonarCloud
on:
workflow_call:
secrets:
SONAR_TOKEN:
required: true
workflow_call:
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
if: github.event.workflow_run.conclusion == 'success'
steps:
# We create the status here and then update it to success/failure in the `report` stage
# This provides an easy link to this workflow_run from the PR before Cypress is done.
- uses: Sibz/github-status-action@v1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
sonarqube:
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
steps:
# We create the status here and then update it to success/failure in the `report` stage
# This provides an easy link to this workflow_run from the PR before Cypress is done.
- uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v2.2
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
version_cmd: 'cat package.json | jq -r .version'
branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }}
coverage_run_id: ${{ github.event.workflow_run.id }}
coverage_workflow_name: tests.yml
coverage_extract_path: coverage
- uses: Sibz/github-status-action@v1
if: always()
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: ${{ steps.sonarcloud.outcome == 'success' && 'success' || 'failure' }}
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v2.5
# workflow_run fails report against the develop commit always, we don't want that for PRs
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
version_cmd: "cat package.json | jq -r .version"
branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }}
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@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
if: always()
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: ${{ steps.sonarcloud.outcome == 'success' && 'success' || 'failure' }}
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
+41 -11
View File
@@ -1,15 +1,45 @@
name: SonarQube
on:
workflow_run:
workflows: [ "Tests" ]
types:
- completed
workflow_run:
workflows: ["Tests"]
types:
- completed
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
jobs:
sonarqube:
name: 🩻 SonarQube
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# This is a workaround for https://github.com/SonarSource/SonarJS/issues/578
prepare:
name: Prepare
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'merge_group'
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@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # 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
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'merge_group'
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 }}
+64 -82
View File
@@ -1,101 +1,83 @@
name: Static Analysis
on:
pull_request: { }
push:
branches: [ develop, master ]
pull_request: {}
merge_group:
types: [checks_requested]
push:
branches: [develop, master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ts_lint:
name: "Typescript Syntax Check"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
ts_lint:
name: "Typescript Syntax Check"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Install Deps
run: "yarn install"
- name: Install Deps
run: "yarn install"
- name: Typecheck
run: "yarn run lint:types"
- name: Typecheck
run: "yarn run lint:types"
- name: Switch js-sdk to release mode
run: |
scripts/switch_package_to_release.js
yarn install
yarn run build:compile
yarn run build:types
- name: Switch js-sdk to release mode
run: |
scripts/switch_package_to_release.js
yarn install
yarn run build:compile
yarn run build:types
- name: Typecheck (release mode)
run: "yarn run lint:types"
- name: Typecheck (release mode)
run: "yarn run lint:types"
js_lint:
name: "ESLint"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
js_lint:
name: "ESLint"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Install Deps
run: "yarn install"
- name: Install Deps
run: "yarn install"
- name: Run Linter
run: "yarn run lint:js"
- name: Run Linter
run: "yarn run lint:js"
docs:
name: "JSDoc Checker"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
docs:
name: "JSDoc Checker"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Install Deps
run: "yarn install"
- name: Install Deps
run: "yarn install"
- name: Generate Docs
run: "yarn run gendoc"
- name: Generate Docs
run: "yarn run gendoc --treatWarningsAsErrors"
tsc-strict:
name: Typescript Strict Error Checker
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
pull-requests: read
checks: write
steps:
- uses: actions/checkout@v3
# Upload artifact duplicates symlink contents so we do this to save 75% space
- name: Flatten symlink and write _redirects
run: |
find _docs -mindepth 1 -maxdepth 1 ! -type f ! -name stable -printf '/%f/* /stable/:splat\n' > _docs/_redirects
find _docs -mindepth 1 -maxdepth 1 -type l -delete
find _docs -mindepth 1 -maxdepth 1 -type d -execdir mv {} stable \; -quit
- name: Get diff lines
id: diff
uses: Equip-Collaboration/diff-line-numbers@v1.0.0
with:
include: '["\\.tsx?$"]'
- name: Detecting files changed
id: files
uses: futuratrepadeira/changed-files@v4.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pattern: '^.*\.tsx?$'
- uses: t3chguy/typescript-check-action@main
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
use-check: false
check-fail-mode: added
output-behaviour: annotate
ts-extra-args: '--strict'
files-changed: ${{ steps.files.outputs.files_updated }}
files-added: ${{ steps.files.outputs.files_created }}
files-deleted: ${{ steps.files.outputs.files_deleted }}
line-numbers: ${{ steps.diff.outputs.lineNumbers }}
- 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
+90 -29
View File
@@ -1,37 +1,98 @@
name: Tests
on:
pull_request: { }
push:
branches: [ develop, master ]
pull_request: {}
merge_group:
types: [checks_requested]
push:
branches: [develop, master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }}
jobs:
jest:
name: Jest
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
jest:
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node }})"
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
specs: [browserify, integ, unit]
node: [18, latest]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Yarn cache
uses: actions/setup-node@v3
- 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@410541432439795d30db6501fb1d8178eb41e502 # v1
- name: Run tests
run: |
yarn test \
--coverage=${{ env.ENABLE_COVERAGE }} \
--ci \
--max-workers ${{ steps.cpu-cores.outputs.count }} \
./spec/${{ matrix.specs }}
env:
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
# tell jest to use coloured output
FORCE_COLOR: true
- name: Move coverage files into place
if: env.ENABLE_COVERAGE == 'true'
run: mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
- name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true'
uses: actions/upload-artifact@v3
with:
name: coverage
path: |
coverage
!coverage/lcov-report
matrix-react-sdk:
name: Downstream test matrix-react-sdk
if: github.event_name == 'merge_group'
uses: matrix-org/matrix-react-sdk/.github/workflows/tests.yml@develop
with:
cache: 'yarn'
disable_coverage: true
matrix-js-sdk-sha: ${{ github.sha }}
- name: Install dependencies
run: "yarn install"
# Hook for branch protection to skip downstream testing outside of merge queues
# and skip sonarcloud coverage within merge queues
downstream:
name: Downstream tests
runs-on: ubuntu-latest
if: always()
needs:
- matrix-react-sdk
steps:
- name: Skip SonarCloud on merge queues
if: env.ENABLE_COVERAGE == 'false'
uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success
description: SonarCloud skipped
context: SonarCloud Code Analysis
sha: ${{ github.sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: Build
run: "yarn build"
- name: Run tests with coverage
run: "yarn coverage --ci --reporters github-actions"
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: coverage
path: |
coverage
!coverage/lcov-report
- if: needs.matrix-react-sdk.result != 'skipped' && needs.matrix-react-sdk.result != 'success'
run: exit 1
+32 -32
View File
@@ -1,38 +1,38 @@
name: Upgrade Dependencies
on:
workflow_dispatch: { }
workflow_call:
secrets:
ELEMENT_BOT_TOKEN:
required: true
workflow_dispatch: {}
workflow_call:
secrets:
ELEMENT_BOT_TOKEN:
required: true
jobs:
upgrade:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
upgrade:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Upgrade
run: yarn upgrade && yarn install
- name: Upgrade
run: yarn upgrade && yarn install
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@v4
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/upgrade-deps
delete-branch: true
title: Upgrade dependencies
labels: |
Dependencies
T-Task
- name: Enable automerge
uses: peter-evans/enable-pull-request-automerge@v2
if: steps.cpr.outputs.pull-request-operation == 'created'
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/upgrade-deps
delete-branch: true
title: Upgrade dependencies
labels: |
Dependencies
T-Task
- name: Enable automerge
run: gh pr merge --merge --auto "$PR_NUMBER"
if: steps.cpr.outputs.pull-request-operation == 'created'
env:
GH_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
PR_NUMBER: ${{ steps.cpr.outputs.pull-request-number }}
+3 -2
View File
@@ -1,5 +1,5 @@
/.jsdocbuild
/.jsdoc
/_docs
.DS_Store
node_modules
/.npmrc
@@ -19,3 +19,4 @@ out
.vscode
.vscode/
.idea/
+29
View File
@@ -0,0 +1,29 @@
/_docs
.DS_Store
/.npmrc
/*.log
package-lock.json
.lock-wscript
build/Release
coverage
lib-cov
out
/dist
/lib
/examples/browser/lib
/examples/crypto-browser/lib
/examples/voip/lib
# version file and tarball created by `npm pack` / `yarn pack`
/git-revision.txt
/matrix-js-sdk-*.tgz
.vscode
.vscode/
# This file is owned, parsed, and generated by allchange, which doesn't comply with prettier
/CHANGELOG.md
# This file is also autogenerated
/spec/test-utils/test-data/index.ts
+1
View File
@@ -0,0 +1 @@
module.exports = require("eslint-plugin-matrix-org/.prettierrc.js");
+495
View File
@@ -1,3 +1,498 @@
Changes in [28.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v28.2.0) (2023-09-26)
==================================================================================================
## 🦖 Deprecations
* Implement `getEncryptionInfoForEvent` and deprecate `getEventEncryptionInfo` ([\#3693](https://github.com/matrix-org/matrix-js-sdk/pull/3693)).
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
## ✨ Features
* Delete knocked room when knock membership changes ([\#3729](https://github.com/matrix-org/matrix-js-sdk/pull/3729)). Contributed by @maheichyk.
* Introduce MatrixRTCSession lower level group call primitive ([\#3663](https://github.com/matrix-org/matrix-js-sdk/pull/3663)).
* Sync knock rooms ([\#3703](https://github.com/matrix-org/matrix-js-sdk/pull/3703)). Contributed by @maheichyk.
## 🐛 Bug Fixes
* Dont access indexed db when undefined ([\#3707](https://github.com/matrix-org/matrix-js-sdk/pull/3707)). Contributed by @finsterwalder.
* Don't reset unread count when adding a synthetic receipt ([\#3706](https://github.com/matrix-org/matrix-js-sdk/pull/3706)). Fixes #3684. Contributed by @andybalaam.
Changes in [28.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v28.1.0) (2023-09-12)
============================================================================================================
## 🦖 Deprecations
* Deprecate `MatrixClient.checkUserTrust` ([\#3691](https://github.com/matrix-org/matrix-js-sdk/pull/3691)).
* Deprecate `MatrixClient.{prepare,create}KeyBackupVersion` in favour of new `CryptoApi.resetKeyBackup` API ([\#3689](https://github.com/matrix-org/matrix-js-sdk/pull/3689)).
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
## ✨ Features
* Allow calls without ICE/TURN/STUN servers ([\#3695](https://github.com/matrix-org/matrix-js-sdk/pull/3695)).
* Emit summary update event ([\#3687](https://github.com/matrix-org/matrix-js-sdk/pull/3687)). Fixes vector-im/element-web#26033.
* ElementR: Update `CryptoApi.userHasCrossSigningKeys` ([\#3646](https://github.com/matrix-org/matrix-js-sdk/pull/3646)). Contributed by @florianduros.
* Add `join_rule` field to /publicRooms response ([\#3673](https://github.com/matrix-org/matrix-js-sdk/pull/3673)). Contributed by @charlynguyen.
* Use sender instead of content.creator field on m.room.create events ([\#3675](https://github.com/matrix-org/matrix-js-sdk/pull/3675)).
## 🐛 Bug Fixes
* Provide better error for ICE Server SyntaxError ([\#3694](https://github.com/matrix-org/matrix-js-sdk/pull/3694)). Fixes vector-im/element-web#21804.
* Legacy crypto: re-check key backup after `bootstrapSecretStorage` ([\#3692](https://github.com/matrix-org/matrix-js-sdk/pull/3692)). Fixes vector-im/element-web#26115.
Changes in [28.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v28.0.0) (2023-08-29)
==================================================================================================
## 🚨 BREAKING CHANGES
* Set minimum supported Matrix 1.1 version (drop legacy r0 versions) ([\#3007](https://github.com/matrix-org/matrix-js-sdk/pull/3007)). Fixes vector-im/element-web#16876.
## 🦖 Deprecations
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
## ✨ Features
* ElementR: Add `CryptoApi.requestVerificationDM` ([\#3643](https://github.com/matrix-org/matrix-js-sdk/pull/3643)). Contributed by @florianduros.
* Implement `CryptoApi.checkKeyBackupAndEnable` ([\#3633](https://github.com/matrix-org/matrix-js-sdk/pull/3633)). Fixes vector-im/crypto-internal#111 and vector-im/crypto-internal#112.
## 🐛 Bug Fixes
* ElementR: Process all verification events, not just requests ([\#3650](https://github.com/matrix-org/matrix-js-sdk/pull/3650)). Contributed by @florianduros.
Changes in [27.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v27.2.0) (2023-08-15)
==================================================================================================
## 🦖 Deprecations
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
## ✨ Features
* Allow knocking rooms ([\#3647](https://github.com/matrix-org/matrix-js-sdk/pull/3647)). Contributed by @charlynguyen.
* Bump pagination limit to account for threaded events ([\#3638](https://github.com/matrix-org/matrix-js-sdk/pull/3638)).
* ElementR: Add `CryptoApi.findVerificationRequestDMInProgress` ([\#3601](https://github.com/matrix-org/matrix-js-sdk/pull/3601)). Contributed by @florianduros.
* Export more into the public interface ([\#3614](https://github.com/matrix-org/matrix-js-sdk/pull/3614)).
## 🐛 Bug Fixes
* Fix wrong handling of encrypted rooms when loading them from sync accumulator ([\#3640](https://github.com/matrix-org/matrix-js-sdk/pull/3640)). Fixes vector-im/element-web#25803.
* Skip processing thread roots and fetching threads list when support is disabled ([\#3642](https://github.com/matrix-org/matrix-js-sdk/pull/3642)).
* Ensure we don't overinflate the total notification count ([\#3634](https://github.com/matrix-org/matrix-js-sdk/pull/3634)). Fixes vector-im/element-web#25803.
Changes in [27.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v27.1.0) (2023-08-01)
==================================================================================================
## 🦖 Deprecations
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
## ✨ Features
* ElementR: Add `CryptoApi.getCrossSigningKeyId` ([\#3619](https://github.com/matrix-org/matrix-js-sdk/pull/3619)). Contributed by @florianduros.
* ElementR: Stub `CheckOwnCrossSigningTrust`, import cross signing keys and verify local device in `bootstrapCrossSigning` ([\#3608](https://github.com/matrix-org/matrix-js-sdk/pull/3608)). Contributed by @florianduros.
* Specify /preview_url requests as low priority ([\#3609](https://github.com/matrix-org/matrix-js-sdk/pull/3609)). Fixes vector-im/element-web#7292.
* Element-R: support for displaying QR codes during verification ([\#3588](https://github.com/matrix-org/matrix-js-sdk/pull/3588)). Fixes vector-im/crypto-internal#124.
* Add support for scanning QR codes during verification, with Rust crypto ([\#3565](https://github.com/matrix-org/matrix-js-sdk/pull/3565)).
* Add methods to influence set_presence on /sync API calls ([\#3578](https://github.com/matrix-org/matrix-js-sdk/pull/3578)).
## 🐛 Bug Fixes
* Fix threads ending up with chunks of their timelines missing ([\#3618](https://github.com/matrix-org/matrix-js-sdk/pull/3618)). Fixes vector-im/element-web#24466.
* Ensure we do not clobber a newer RR with an older unthreaded one ([\#3617](https://github.com/matrix-org/matrix-js-sdk/pull/3617)). Fixes vector-im/element-web#25806.
* Fix registration check your emails stage regression ([\#3616](https://github.com/matrix-org/matrix-js-sdk/pull/3616)).
* Fix how `Room::eventShouldLiveIn` handles replies to unknown parents ([\#3615](https://github.com/matrix-org/matrix-js-sdk/pull/3615)). Fixes vector-im/element-web#22603.
* Only send threaded read receipts if threads support is enabled ([\#3612](https://github.com/matrix-org/matrix-js-sdk/pull/3612)).
* ElementR: Fix `userId` parameter usage in `CryptoApi#getVerificationRequestsToDeviceInProgress` ([\#3611](https://github.com/matrix-org/matrix-js-sdk/pull/3611)). Contributed by @florianduros.
* Fix edge cases around non-thread relations to thread roots and read receipts ([\#3607](https://github.com/matrix-org/matrix-js-sdk/pull/3607)).
* Fix read receipt sending behaviour around thread roots ([\#3600](https://github.com/matrix-org/matrix-js-sdk/pull/3600)).
* Export typed event emitter key types ([\#3597](https://github.com/matrix-org/matrix-js-sdk/pull/3597)). Fixes #3506.
* Element-R: ensure that `userHasCrossSigningKeys` uses up-to-date data ([\#3599](https://github.com/matrix-org/matrix-js-sdk/pull/3599)). Fixes vector-im/element-web#25773.
* Fix sending `auth: null` due to broken types around UIA ([\#3594](https://github.com/matrix-org/matrix-js-sdk/pull/3594)).
Changes in [27.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v27.0.0) (2023-07-18)
==================================================================================================
## 🚨 BREAKING CHANGES
* Drop support for Node 16 ([\#3533](https://github.com/matrix-org/matrix-js-sdk/pull/3533)).
* Improve types around login, registration, UIA and identity servers ([\#3537](https://github.com/matrix-org/matrix-js-sdk/pull/3537)).
## 🦖 Deprecations
* **The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. (#3189)**
* Simplify `MatrixClient::setPowerLevel` API ([\#3570](https://github.com/matrix-org/matrix-js-sdk/pull/3570)). Fixes vector-im/element-web#13900 and #1844.
* Deprecate `VerificationRequest.getQRCodeBytes` and replace it with the asynchronous `generateQRCode`. ([\#3562](https://github.com/matrix-org/matrix-js-sdk/pull/3562)).
* Deprecate `VerificationRequest.beginKeyVerification()` in favour of `VerificationRequest.startVerification()`. ([\#3528](https://github.com/matrix-org/matrix-js-sdk/pull/3528)).
* Deprecate `Crypto.VerificationRequest` application event, replacing it with `Crypto.VerificationRequestReceived`. ([\#3514](https://github.com/matrix-org/matrix-js-sdk/pull/3514)).
## ✨ Features
* Throw saner error when peeking has its room pulled out from under it ([\#3577](https://github.com/matrix-org/matrix-js-sdk/pull/3577)). Fixes vector-im/element-web#18679.
* OIDC: Log in ([\#3554](https://github.com/matrix-org/matrix-js-sdk/pull/3554)). Contributed by @kerryarchibald.
* Prevent threads code from making identical simultaneous API hits ([\#3541](https://github.com/matrix-org/matrix-js-sdk/pull/3541)). Fixes vector-im/element-web#25395.
* Update IUnsigned type to be extensible ([\#3547](https://github.com/matrix-org/matrix-js-sdk/pull/3547)).
* add stop() api to BackupManager for clean shutdown ([\#3553](https://github.com/matrix-org/matrix-js-sdk/pull/3553)).
* Log the message ID of any undecryptable to-device messages ([\#3543](https://github.com/matrix-org/matrix-js-sdk/pull/3543)).
* Ignore thread relations on state events for consistency with edits ([\#3540](https://github.com/matrix-org/matrix-js-sdk/pull/3540)).
* OIDC: validate id token ([\#3531](https://github.com/matrix-org/matrix-js-sdk/pull/3531)). Contributed by @kerryarchibald.
## 🐛 Bug Fixes
* Fix read receipt sending behaviour around thread roots ([\#3600](https://github.com/matrix-org/matrix-js-sdk/pull/3600)).
* Fix `TypedEventEmitter::removeAllListeners(void)` not working ([\#3561](https://github.com/matrix-org/matrix-js-sdk/pull/3561)).
* Don't allow Olm unwedging rate-limiting to race ([\#3549](https://github.com/matrix-org/matrix-js-sdk/pull/3549)). Fixes vector-im/element-web#25716.
* Fix an instance of failed to decrypt error when an in flight `/keys/query` fails. ([\#3486](https://github.com/matrix-org/matrix-js-sdk/pull/3486)).
* Use the right anchor emoji for SAS verification ([\#3534](https://github.com/matrix-org/matrix-js-sdk/pull/3534)).
* fix a bug which caused the wrong emoji to be shown during SAS device verification. ([\#3523](https://github.com/matrix-org/matrix-js-sdk/pull/3523)).
Changes in [26.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.2.0) (2023-07-04)
==================================================================================================
## 🦖 Deprecations
* The Browserify artifact is being deprecated, scheduled for removal in the October 10th release cycle. ([\#3189](https://github.com/matrix-org/matrix-js-sdk/issues/3189)).
* ElementR: Add `CryptoApi#bootstrapSecretStorage` ([\#3483](https://github.com/matrix-org/matrix-js-sdk/pull/3483)). Contributed by @florianduros.
* Deprecate `MatrixClient.findVerificationRequestDMInProgress`, `MatrixClient.getVerificationRequestsToDeviceInProgress`, and `MatrixClient.requestVerification`, in favour of methods in `CryptoApi`. ([\#3474](https://github.com/matrix-org/matrix-js-sdk/pull/3474)).
* Introduce a new `Crypto.VerificationRequest` interface, and deprecate direct access to the old `VerificationRequest` class. Also deprecate some related classes that were exported from `src/crypto/verification/request/VerificationRequest` ([\#3449](https://github.com/matrix-org/matrix-js-sdk/pull/3449)).
## ✨ Features
* OIDC: navigate to authorization endpoint ([\#3499](https://github.com/matrix-org/matrix-js-sdk/pull/3499)). Contributed by @kerryarchibald.
* Support for interactive device verification in Element-R. ([\#3505](https://github.com/matrix-org/matrix-js-sdk/pull/3505)).
* Support for interactive device verification in Element-R. ([\#3508](https://github.com/matrix-org/matrix-js-sdk/pull/3508)).
* Support for interactive device verification in Element-R. ([\#3490](https://github.com/matrix-org/matrix-js-sdk/pull/3490)). Fixes vector-im/element-web#25316.
* Element-R: Store cross signing keys in secret storage ([\#3498](https://github.com/matrix-org/matrix-js-sdk/pull/3498)). Contributed by @florianduros.
* OIDC: add dynamic client registration util function ([\#3481](https://github.com/matrix-org/matrix-js-sdk/pull/3481)). Contributed by @kerryarchibald.
* Add getLastUnthreadedReceiptFor utility to Thread delegating to the underlying Room ([\#3493](https://github.com/matrix-org/matrix-js-sdk/pull/3493)).
* ElementR: Add `rust-crypto#createRecoveryKeyFromPassphrase` implementation ([\#3472](https://github.com/matrix-org/matrix-js-sdk/pull/3472)). Contributed by @florianduros.
## 🐛 Bug Fixes
* Aggregate relations regardless of whether event fits into the timeline ([\#3496](https://github.com/matrix-org/matrix-js-sdk/pull/3496)). Fixes vector-im/element-web#25596.
* Fix bug where switching media caused media in subsequent calls to fail ([\#3489](https://github.com/matrix-org/matrix-js-sdk/pull/3489)).
* Fix: remove polls from room state on redaction ([\#3475](https://github.com/matrix-org/matrix-js-sdk/pull/3475)). Fixes vector-im/element-web#25573. Contributed by @kerryarchibald.
* Fix export type `GeneratedSecretStorageKey` ([\#3479](https://github.com/matrix-org/matrix-js-sdk/pull/3479)). Contributed by @florianduros.
* Close IDB database before deleting it to prevent spurious unexpected close errors ([\#3478](https://github.com/matrix-org/matrix-js-sdk/pull/3478)). Fixes vector-im/element-web#25597.
Changes in [26.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.1.0) (2023-06-20)
==================================================================================================
## 🦖 Deprecations
* Introduce a new `Crypto.Verifier` interface, and deprecate direct access to `VerificationBase`, `SAS` and `ReciprocateQRCode` ([\#3414](https://github.com/matrix-org/matrix-js-sdk/pull/3414)).
## ✨ Features
* Add `rust-crypto#isCrossSigningReady` implementation ([\#3462](https://github.com/matrix-org/matrix-js-sdk/pull/3462)). Contributed by @florianduros.
* OIDC: Validate `m.authentication` configuration ([\#3419](https://github.com/matrix-org/matrix-js-sdk/pull/3419)). Contributed by @kerryarchibald.
* ElementR: Add `CryptoApi.getCrossSigningStatus` ([\#3452](https://github.com/matrix-org/matrix-js-sdk/pull/3452)). Contributed by @florianduros.
* Extend stats summary with call device and user count based on room state ([\#3424](https://github.com/matrix-org/matrix-js-sdk/pull/3424)). Contributed by @toger5.
* Update MSC3912 implementation to use `with_rel_type` instead of `with_relations` ([\#3420](https://github.com/matrix-org/matrix-js-sdk/pull/3420)).
* Export thread-related types from SDK ([\#3447](https://github.com/matrix-org/matrix-js-sdk/pull/3447)). Contributed by @stas-demydiuk.
* Use correct /v3 prefix for /refresh ([\#3016](https://github.com/matrix-org/matrix-js-sdk/pull/3016)). Contributed by @davidisaaclee.
## 🐛 Bug Fixes
* Fix thread list being ordered based on all updates ([\#3458](https://github.com/matrix-org/matrix-js-sdk/pull/3458)). Fixes vector-im/element-web#25522.
* Fix: handle `baseUrl` with trailing slash in `fetch.getUrl` ([\#3455](https://github.com/matrix-org/matrix-js-sdk/pull/3455)). Fixes vector-im/element-web#25526. Contributed by @kerryarchibald.
* use cli.canSupport to determine intentional mentions support ([\#3445](https://github.com/matrix-org/matrix-js-sdk/pull/3445)). Fixes vector-im/element-web#25497. Contributed by @kerryarchibald.
* Make sliding sync linearize processing of sync requests ([\#3442](https://github.com/matrix-org/matrix-js-sdk/pull/3442)).
* Fix edge cases around 2nd order relations and threads ([\#3437](https://github.com/matrix-org/matrix-js-sdk/pull/3437)).
Changes in [26.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.0.1) (2023-06-09)
==================================================================================================
## 🐛 Bug Fixes
* Fix: handle `baseUrl` with trailing slash in `fetch.getUrl` ([\#3455](https://github.com/matrix-org/matrix-js-sdk/pull/3455)). Fixes vector-im/element-web#25526. Contributed by @kerryarchibald.
Changes in [26.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.0.0) (2023-06-06)
==================================================================================================
## 🚨 BREAKING CHANGES
* Ensure we do not add relations to the wrong timeline ([\#3427](https://github.com/matrix-org/matrix-js-sdk/pull/3427)). Fixes vector-im/element-web#25450 and vector-im/element-web#25494.
* Deprecate `QrCodeEvent`, `SasEvent` and `VerificationEvent` ([\#3386](https://github.com/matrix-org/matrix-js-sdk/pull/3386)).
## 🦖 Deprecations
* Move crypto classes into a separate namespace ([\#3385](https://github.com/matrix-org/matrix-js-sdk/pull/3385)).
## ✨ Features
* Mention deno support in the README ([\#3417](https://github.com/matrix-org/matrix-js-sdk/pull/3417)). Contributed by @sigmaSd.
* Mark room version 10 as safe ([\#3425](https://github.com/matrix-org/matrix-js-sdk/pull/3425)).
* Prioritise entirely supported flows for UIA ([\#3402](https://github.com/matrix-org/matrix-js-sdk/pull/3402)).
* Add methods to terminate idb worker ([\#3362](https://github.com/matrix-org/matrix-js-sdk/pull/3362)).
* Total summary count ([\#3351](https://github.com/matrix-org/matrix-js-sdk/pull/3351)). Contributed by @toger5.
* Audio concealment ([\#3349](https://github.com/matrix-org/matrix-js-sdk/pull/3349)). Contributed by @toger5.
## 🐛 Bug Fixes
* Correctly accumulate sync summaries. ([\#3366](https://github.com/matrix-org/matrix-js-sdk/pull/3366)). Fixes vector-im/element-web#23345.
* Keep measuring a call feed's volume after a stream replacement ([\#3361](https://github.com/matrix-org/matrix-js-sdk/pull/3361)). Fixes vector-im/element-call#1051.
* Element-R: Avoid uploading a new fallback key at every `/sync` ([\#3338](https://github.com/matrix-org/matrix-js-sdk/pull/3338)). Fixes vector-im/element-web#25215.
* Accumulate receipts for the main thread and unthreaded separately ([\#3339](https://github.com/matrix-org/matrix-js-sdk/pull/3339)). Fixes vector-im/element-web#24629.
* Remove spec non-compliant extended glob format ([\#3423](https://github.com/matrix-org/matrix-js-sdk/pull/3423)). Fixes vector-im/element-web#25474.
* Fix bug where original event was inserted into timeline instead of the edit event ([\#3398](https://github.com/matrix-org/matrix-js-sdk/pull/3398)). Contributed by @andybalaam.
* Only add a local receipt if it's after an existing receipt ([\#3399](https://github.com/matrix-org/matrix-js-sdk/pull/3399)). Contributed by @andybalaam.
* Attempt a potential workaround for stuck notifs ([\#3384](https://github.com/matrix-org/matrix-js-sdk/pull/3384)). Fixes vector-im/element-web#25406. Contributed by @andybalaam.
* Fix verification bug with `pendingEventOrdering: "chronological"` ([\#3382](https://github.com/matrix-org/matrix-js-sdk/pull/3382)).
Changes in [25.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.1.1) (2023-05-16)
==================================================================================================
## 🐛 Bug Fixes
* Rebuild to fix packaging glitch in 25.1.0. Fixes #3363
Changes in [25.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.1.0) (2023-05-09)
==================================================================================================
## 🦖 Deprecations
* Deprecate MatrixClient::resolveRoomAlias ([\#3316](https://github.com/matrix-org/matrix-js-sdk/pull/3316)).
## ✨ Features
* add client method to remove pusher ([\#3324](https://github.com/matrix-org/matrix-js-sdk/pull/3324)). Contributed by @kerryarchibald.
* Implement MSC 3981 ([\#3248](https://github.com/matrix-org/matrix-js-sdk/pull/3248)). Fixes vector-im/element-web#25021. Contributed by @justjanne.
* Added `Room.getLastLiveEvent` and `Room.getLastThread`. Deprecated `Room.lastThread` in favour of `Room.getLastThread`. ([\#3321](https://github.com/matrix-org/matrix-js-sdk/pull/3321)).
* Element-R: wire up device lists ([\#3272](https://github.com/matrix-org/matrix-js-sdk/pull/3272)). Contributed by @florianduros.
* Node 20 support ([\#3302](https://github.com/matrix-org/matrix-js-sdk/pull/3302)).
## 🐛 Bug Fixes
* Fix racing between one-time-keys processing and sync ([\#3327](https://github.com/matrix-org/matrix-js-sdk/pull/3327)). Fixes vector-im/element-web#25214. Contributed by @florianduros.
* Fix lack of media when a user reconnects ([\#3318](https://github.com/matrix-org/matrix-js-sdk/pull/3318)).
* Fix TimelineWindow getEvents exploding if no neigbouring timeline ([\#3285](https://github.com/matrix-org/matrix-js-sdk/pull/3285)). Fixes vector-im/element-web#25104.
Changes in [25.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v25.0.0) (2023-04-25)
==================================================================================================
## 🚨 BREAKING CHANGES
* Change `Store.save()` to return a `Promise` ([\#3221](https://github.com/matrix-org/matrix-js-sdk/pull/3221)). Contributed by @texuf.
## ✨ Features
* Add typedoc-plugin-mdn-links ([\#3292](https://github.com/matrix-org/matrix-js-sdk/pull/3292)).
* Annotate events with executed push rule ([\#3284](https://github.com/matrix-org/matrix-js-sdk/pull/3284)). Contributed by @kerryarchibald.
* Element-R: pass device list change notifications into rust crypto-sdk ([\#3254](https://github.com/matrix-org/matrix-js-sdk/pull/3254)). Fixes vector-im/element-web#24795. Contributed by @florianduros.
* Support for MSC3882 revision 1 ([\#3228](https://github.com/matrix-org/matrix-js-sdk/pull/3228)). Contributed by @hughns.
## 🐛 Bug Fixes
* Fix screen sharing on Firefox 113 ([\#3282](https://github.com/matrix-org/matrix-js-sdk/pull/3282)). Contributed by @tulir.
* Retry processing potential poll events after decryption ([\#3246](https://github.com/matrix-org/matrix-js-sdk/pull/3246)). Fixes vector-im/element-web#24568.
* Element-R: handle events which arrive before their keys ([\#3230](https://github.com/matrix-org/matrix-js-sdk/pull/3230)). Fixes vector-im/element-web#24489.
Changes in [24.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v24.1.0) (2023-04-11)
==================================================================================================
## ✨ Features
* Allow via_servers property in findPredecessor (update to MSC3946) ([\#3240](https://github.com/matrix-org/matrix-js-sdk/pull/3240)). Contributed by @andybalaam.
* Fire `closed` event when IndexedDB closes unexpectedly ([\#3218](https://github.com/matrix-org/matrix-js-sdk/pull/3218)).
* Implement MSC3952: intentional mentions ([\#3092](https://github.com/matrix-org/matrix-js-sdk/pull/3092)). Fixes vector-im/element-web#24376.
* Send one time key count and unused fallback keys for rust-crypto ([\#3215](https://github.com/matrix-org/matrix-js-sdk/pull/3215)). Fixes vector-im/element-web#24795. Contributed by @florianduros.
* Improve `processBeaconEvents` hotpath ([\#3200](https://github.com/matrix-org/matrix-js-sdk/pull/3200)).
* Implement MSC3966: a push rule condition to check if an array contains a value ([\#3180](https://github.com/matrix-org/matrix-js-sdk/pull/3180)).
## 🐛 Bug Fixes
* indexddb-local-backend - return the current sync to database promise … ([\#3222](https://github.com/matrix-org/matrix-js-sdk/pull/3222)). Contributed by @texuf.
* Revert "Add the call object to Call events" ([\#3236](https://github.com/matrix-org/matrix-js-sdk/pull/3236)).
* Handle group call redaction ([\#3231](https://github.com/matrix-org/matrix-js-sdk/pull/3231)). Fixes vector-im/voip-internal#128.
* Stop doing O(n^2) work to find event's home (`eventShouldLiveIn`) ([\#3227](https://github.com/matrix-org/matrix-js-sdk/pull/3227)). Contributed by @jryans.
* Fix bug where video would not unmute if it started muted ([\#3213](https://github.com/matrix-org/matrix-js-sdk/pull/3213)). Fixes vector-im/element-call#925.
* Fixes to event encryption in the Rust Crypto implementation ([\#3202](https://github.com/matrix-org/matrix-js-sdk/pull/3202)).
Changes in [24.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v24.0.0) (2023-03-28)
==================================================================================================
## 🔒 Security
* Fixes for [CVE-2023-28427](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE-2023-28427) / GHSA-mwq8-fjpf-c2gr
Changes in [23.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.5.0) (2023-03-15)
==================================================================================================
## ✨ Features
* Implement MSC3758: a push rule condition to match event properties exactly ([\#3179](https://github.com/matrix-org/matrix-js-sdk/pull/3179)).
* Enable group calls without video and audio track by configuration of MatrixClient ([\#3162](https://github.com/matrix-org/matrix-js-sdk/pull/3162)). Contributed by @EnricoSchw.
* Updates to protocol used for Sign in with QR code ([\#3155](https://github.com/matrix-org/matrix-js-sdk/pull/3155)). Contributed by @hughns.
* Implement MSC3873 to handle escaped dots in push rule keys ([\#3134](https://github.com/matrix-org/matrix-js-sdk/pull/3134)). Fixes undefined/matrix-js-sdk#1454.
## 🐛 Bug Fixes
* Fix spec compliance issue around encrypted `m.relates_to` ([\#3178](https://github.com/matrix-org/matrix-js-sdk/pull/3178)).
* Fix reactions in threads sometimes causing stuck notifications ([\#3146](https://github.com/matrix-org/matrix-js-sdk/pull/3146)). Fixes vector-im/element-web#24000. Contributed by @justjanne.
Changes in [23.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.4.0) (2023-02-28)
==================================================================================================
## ✨ Features
* Add easy way to determine if the decryption failure is due to "DecryptionError: The sender has disabled encrypting to unverified devices." ([\#3167](https://github.com/matrix-org/matrix-js-sdk/pull/3167)). Contributed by @florianduros.
* Polls: expose end event id on poll model ([\#3160](https://github.com/matrix-org/matrix-js-sdk/pull/3160)). Contributed by @kerryarchibald.
* Polls: count undecryptable poll relations ([\#3163](https://github.com/matrix-org/matrix-js-sdk/pull/3163)). Contributed by @kerryarchibald.
## 🐛 Bug Fixes
* Better type guard parseTopicContent ([\#3165](https://github.com/matrix-org/matrix-js-sdk/pull/3165)). Fixes matrix-org/element-web-rageshakes#20177 and matrix-org/element-web-rageshakes#20178.
* Fix a bug where events in encrypted rooms would sometimes erroneously increment the total unread counter after being processed locally. ([\#3130](https://github.com/matrix-org/matrix-js-sdk/pull/3130)). Fixes vector-im/element-web#24448. Contributed by @Half-Shot.
* Stop the ICE disconnected timer on call terminate ([\#3147](https://github.com/matrix-org/matrix-js-sdk/pull/3147)).
* Clear notifications when we can infer read status from receipts ([\#3139](https://github.com/matrix-org/matrix-js-sdk/pull/3139)). Fixes vector-im/element-web#23991.
* Messages sent out of order after one message fails ([\#3131](https://github.com/matrix-org/matrix-js-sdk/pull/3131)). Fixes vector-im/element-web#22885 and vector-im/element-web#18942. Contributed by @justjanne.
Changes in [23.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.3.0) (2023-02-14)
==================================================================================================
## ✨ Features
* Element-R: implement encryption of outgoing events ([\#3122](https://github.com/matrix-org/matrix-js-sdk/pull/3122)).
* Poll model - page /relations results ([\#3073](https://github.com/matrix-org/matrix-js-sdk/pull/3073)). Contributed by @kerryarchibald.
* Poll model - validate end events ([\#3072](https://github.com/matrix-org/matrix-js-sdk/pull/3072)). Contributed by @kerryarchibald.
* Handle optional last_known_event_id property in m.predecessor ([\#3119](https://github.com/matrix-org/matrix-js-sdk/pull/3119)). Contributed by @andybalaam.
* Add support for stable identifier for fixed MAC in SAS verification ([\#3101](https://github.com/matrix-org/matrix-js-sdk/pull/3101)).
* Provide eventId as well as roomId from Room.findPredecessor ([\#3095](https://github.com/matrix-org/matrix-js-sdk/pull/3095)). Contributed by @andybalaam.
* MSC3946 Dynamic room predecessors ([\#3042](https://github.com/matrix-org/matrix-js-sdk/pull/3042)). Contributed by @andybalaam.
* Poll model ([\#3036](https://github.com/matrix-org/matrix-js-sdk/pull/3036)). Contributed by @kerryarchibald.
* Remove video tracks on video mute without renegotiating ([\#3091](https://github.com/matrix-org/matrix-js-sdk/pull/3091)).
* Introduces a backwards-compatible API change. `MegolmEncrypter#prepareToEncrypt`'s return type has changed from `void` to `() => void`. ([\#3035](https://github.com/matrix-org/matrix-js-sdk/pull/3035)). Contributed by @clarkf.
## 🐛 Bug Fixes
* Stop the ICE disconnected timer on call terminate ([\#3147](https://github.com/matrix-org/matrix-js-sdk/pull/3147)).
* Clear notifications when we can infer read status from receipts ([\#3139](https://github.com/matrix-org/matrix-js-sdk/pull/3139)). Fixes vector-im/element-web#23991.
* Messages sent out of order after one message fails ([\#3131](https://github.com/matrix-org/matrix-js-sdk/pull/3131)). Fixes vector-im/element-web#22885 and vector-im/element-web#18942. Contributed by @justjanne.
* Element-R: fix a bug which prevented encryption working after a reload ([\#3126](https://github.com/matrix-org/matrix-js-sdk/pull/3126)).
* Element-R: Fix invite processing ([\#3121](https://github.com/matrix-org/matrix-js-sdk/pull/3121)).
* Don't throw with no `opponentDeviceInfo` ([\#3107](https://github.com/matrix-org/matrix-js-sdk/pull/3107)).
* Remove flaky megolm test ([\#3098](https://github.com/matrix-org/matrix-js-sdk/pull/3098)). Contributed by @clarkf.
* Fix "verifyLinks" functionality of getRoomUpgradeHistory ([\#3089](https://github.com/matrix-org/matrix-js-sdk/pull/3089)). Contributed by @andybalaam.
Changes in [23.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.2.0) (2023-01-31)
==================================================================================================
## ✨ Features
* Implement decryption via the rust sdk ([\#3074](https://github.com/matrix-org/matrix-js-sdk/pull/3074)).
* Handle edits which are bundled with an event, per MSC3925 ([\#3045](https://github.com/matrix-org/matrix-js-sdk/pull/3045)).
## 🐛 Bug Fixes
* Add null check for our own member event ([\#3082](https://github.com/matrix-org/matrix-js-sdk/pull/3082)).
* Handle group call getting initialised twice in quick succession ([\#3078](https://github.com/matrix-org/matrix-js-sdk/pull/3078)). Fixes vector-im/element-call#847.
* Correctly handle limited sync responses by resetting the thread timeline ([\#3056](https://github.com/matrix-org/matrix-js-sdk/pull/3056)). Fixes vector-im/element-web#23952. Contributed by @justjanne.
* Fix failure to start in firefox private browser ([\#3058](https://github.com/matrix-org/matrix-js-sdk/pull/3058)). Fixes vector-im/element-web#24216.
* Fix spurious "Decryption key withheld" messages ([\#3061](https://github.com/matrix-org/matrix-js-sdk/pull/3061)). Fixes vector-im/element-web#23803.
* Fix browser entrypoint ([\#3051](https://github.com/matrix-org/matrix-js-sdk/pull/3051)). Fixes #3013.
Changes in [23.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.1) (2023-01-20)
==================================================================================================
## 🐛 Bug Fixes
* Fix backwards compability for environment not support Array.prototype.at ([\#3080](https://github.com/matrix-org/matrix-js-sdk/pull/3080)).
Changes in [23.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0) (2023-01-18)
==================================================================================================
## 🦖 Deprecations
* Remove extensible events v1 field population on legacy events ([\#3040](https://github.com/matrix-org/matrix-js-sdk/pull/3040)).
## ✨ Features
* Improve hasUserReadEvent and getUserReadUpTo realibility with threads ([\#3031](https://github.com/matrix-org/matrix-js-sdk/pull/3031)). Fixes vector-im/element-web#24164.
* Remove video track when muting video ([\#3028](https://github.com/matrix-org/matrix-js-sdk/pull/3028)). Fixes vector-im/element-call#209.
* Make poll start event type available (PSG-962) ([\#3034](https://github.com/matrix-org/matrix-js-sdk/pull/3034)).
* Add alt event type matching in Relations model ([\#3018](https://github.com/matrix-org/matrix-js-sdk/pull/3018)).
* Remove usage of v1 Identity Server API ([\#3003](https://github.com/matrix-org/matrix-js-sdk/pull/3003)).
* Add `device_id` to `/account/whoami` types ([\#3005](https://github.com/matrix-org/matrix-js-sdk/pull/3005)).
* Implement MSC3912: Relation-based redactions ([\#2954](https://github.com/matrix-org/matrix-js-sdk/pull/2954)).
* Introduce a mechanism for using the rust-crypto-sdk ([\#2969](https://github.com/matrix-org/matrix-js-sdk/pull/2969)).
* Support MSC3391: Account data deletion ([\#2967](https://github.com/matrix-org/matrix-js-sdk/pull/2967)).
## 🐛 Bug Fixes
* Fix threaded cache receipt when event holds multiple receipts ([\#3026](https://github.com/matrix-org/matrix-js-sdk/pull/3026)).
* Fix false key requests after verifying new device ([\#3029](https://github.com/matrix-org/matrix-js-sdk/pull/3029)). Fixes vector-im/element-web#24167 and vector-im/element-web#23333.
* Avoid triggering decryption errors when decrypting redacted events ([\#3004](https://github.com/matrix-org/matrix-js-sdk/pull/3004)). Fixes vector-im/element-web#24084.
* bugfix: upload OTKs in sliding sync mode ([\#3008](https://github.com/matrix-org/matrix-js-sdk/pull/3008)).
* Apply edits discovered from sync after thread is initialised ([\#3002](https://github.com/matrix-org/matrix-js-sdk/pull/3002)). Fixes vector-im/element-web#23921.
* Sliding sync: Fix issue where no unsubs are sent when switching rooms ([\#2991](https://github.com/matrix-org/matrix-js-sdk/pull/2991)).
* Threads are missing from the timeline ([\#2996](https://github.com/matrix-org/matrix-js-sdk/pull/2996)). Fixes vector-im/element-web#24036.
* Close all streams when a call ends ([\#2992](https://github.com/matrix-org/matrix-js-sdk/pull/2992)). Fixes vector-im/element-call#742.
* Resume to-device message queue after resumed sync ([\#2920](https://github.com/matrix-org/matrix-js-sdk/pull/2920)). Fixes matrix-org/element-web-rageshakes#17170.
* Fix browser entrypoint ([\#3051](https://github.com/matrix-org/matrix-js-sdk/pull/3051)). Fixes #3013.
* Fix failure to start in firefox private browser ([\#3058](https://github.com/matrix-org/matrix-js-sdk/pull/3058)). Fixes vector-im/element-web#24216.
* Correctly handle limited sync responses by resetting the thread timeline ([\#3056](https://github.com/matrix-org/matrix-js-sdk/pull/3056)). Fixes vector-im/element-web#23952.
Changes in [23.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.0.0) (2022-12-21)
==================================================================================================
## 🚨 BREAKING CHANGES
* Process `m.room.encryption` events before emitting `RoomMember` events ([\#2914](https://github.com/matrix-org/matrix-js-sdk/pull/2914)). Fixes vector-im/element-web#23819.
* Don't expose `calls` on `GroupCall` ([\#2941](https://github.com/matrix-org/matrix-js-sdk/pull/2941)).
## ✨ Features
* Support MSC3391: Account data deletion ([\#2967](https://github.com/matrix-org/matrix-js-sdk/pull/2967)).
* Add a message ID on each to-device message ([\#2938](https://github.com/matrix-org/matrix-js-sdk/pull/2938)).
* Enable multiple users' power levels to be set at once ([\#2892](https://github.com/matrix-org/matrix-js-sdk/pull/2892)). Contributed by @GoodGuyMarco.
* Include pending events in thread summary and count again ([\#2922](https://github.com/matrix-org/matrix-js-sdk/pull/2922)). Fixes vector-im/element-web#23642.
* Make GroupCall work better with widgets ([\#2935](https://github.com/matrix-org/matrix-js-sdk/pull/2935)).
* Add method to get outgoing room key requests for a given event ([\#2930](https://github.com/matrix-org/matrix-js-sdk/pull/2930)).
## 🐛 Bug Fixes
* Fix messages loaded during initial fetch ending up out of order ([\#2971](https://github.com/matrix-org/matrix-js-sdk/pull/2971)). Fixes vector-im/element-web#23972.
* Fix #23919: Root message for new thread loaded from network ([\#2965](https://github.com/matrix-org/matrix-js-sdk/pull/2965)). Fixes vector-im/element-web#23919.
* Fix #23916: Prevent edits of the last message in a thread getting lost ([\#2951](https://github.com/matrix-org/matrix-js-sdk/pull/2951)). Fixes vector-im/element-web#23916 and vector-im/element-web#23942.
* Fix infinite loop when restoring cached read receipts ([\#2963](https://github.com/matrix-org/matrix-js-sdk/pull/2963)). Fixes vector-im/element-web#23951.
* Don't swallow errors coming from the shareSession call ([\#2962](https://github.com/matrix-org/matrix-js-sdk/pull/2962)). Fixes vector-im/element-web#23792.
* Make sure that MegolmEncryption.setupPromise always resolves ([\#2960](https://github.com/matrix-org/matrix-js-sdk/pull/2960)).
* Do not calculate highlight notifs for threads unknown to the room ([\#2957](https://github.com/matrix-org/matrix-js-sdk/pull/2957)).
* Cache read receipts for unknown threads ([\#2953](https://github.com/matrix-org/matrix-js-sdk/pull/2953)).
* bugfix: sliding sync initial room timelines shouldn't notify ([\#2933](https://github.com/matrix-org/matrix-js-sdk/pull/2933)).
* Redo key sharing after own device verification ([\#2921](https://github.com/matrix-org/matrix-js-sdk/pull/2921)). Fixes vector-im/element-web#23333.
* Move updated threads to the end of the thread list ([\#2923](https://github.com/matrix-org/matrix-js-sdk/pull/2923)). Fixes vector-im/element-web#23876.
* 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 [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)
==================================================================================================
+1 -3
View File
@@ -1,5 +1,3 @@
Contributing code to matrix-js-sdk
==================================
# Contributing code to matrix-js-sdk
matrix-js-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md
+161 -151
View File
@@ -6,21 +6,29 @@
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=matrix-js-sdk&metric=bugs)](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
Matrix Javascript SDK
=====================
# 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.
Quickstart
==========
#### Minimum Matrix server version: v1.1
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
## In a browser
### Note, the browserify build has been deprecated. Please use a bundler like webpack or vite instead.
In a browser
------------
Download the browser version from
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
``<script>`` to your page. There will be a global variable ``matrixcs``
attached to ``window`` through which you can access the SDK. See below for how to
`<script>` to your page. There will be a global variable `matrixcs`
attached to `window` through which you can access the SDK. See below for how to
include libolm to enable end-to-end-encryption.
The browser bundle supports recent versions of browsers. Typically this is ES2015
@@ -29,8 +37,7 @@ or `> 0.5%, last 2 versions, Firefox ESR, not dead` if using
Please check [the working browser example](examples/browser) for more information.
In Node.js
----------
## In Node.js
Ensure you have the latest LTS version of Node.js installed.
This library relies on `fetch` which is available in Node from v18.0.0 - it should work fine also with polyfills.
@@ -39,30 +46,32 @@ If you wish to use a ponyfill or adapter of some sort then pass it as `fetchFn`
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
if you do not have it already.
``yarn add matrix-js-sdk``
`yarn add matrix-js-sdk`
```javascript
import * as sdk from "matrix-js-sdk";
const client = sdk.createClient("https://matrix.org");
client.publicRooms(function(err, data) {
import * as sdk from "matrix-js-sdk";
const client = sdk.createClient({ baseUrl: "https://matrix.org" });
client.publicRooms(function (err, data) {
console.log("Public Rooms: %s", JSON.stringify(data));
});
});
```
See below for how to include libolm to enable end-to-end-encryption. Please check
[the Node.js terminal app](examples/node) for a more complex example.
You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officialy supported.
To start the client:
```javascript
await client.startClient({initialSyncLimit: 10});
await client.startClient({ initialSyncLimit: 10 });
```
You can perform a call to `/sync` to get the current state of the client:
```javascript
client.once('sync', function(state, prevState, res) {
if(state === 'PREPARED') {
client.once("sync", function (state, prevState, res) {
if (state === "PREPARED") {
console.log("prepared");
} else {
console.log(state);
@@ -75,8 +84,8 @@ To send a message:
```javascript
const content = {
"body": "message text",
"msgtype": "m.text"
body: "message text",
msgtype: "m.text",
};
client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
console.log(err);
@@ -86,11 +95,11 @@ client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
To listen for message events:
```javascript
client.on("Room.timeline", function(event, room, toStartOfTimeline) {
if (event.getType() !== "m.room.message") {
return; // only use messages
}
console.log(event.event.content.body);
client.on("Room.timeline", function (event, room, toStartOfTimeline) {
if (event.getType() !== "m.room.message") {
return; // only use messages
}
console.log(event.event.content.body);
});
```
@@ -98,73 +107,70 @@ By default, the `matrix-js-sdk` client uses the `MemoryStore` to store events as
```javascript
Object.keys(client.store.rooms).forEach((roomId) => {
client.getRoom(roomId).timeline.forEach(t => {
console.log(t.event);
});
client.getRoom(roomId).timeline.forEach((t) => {
console.log(t.event);
});
});
```
What does this SDK do?
----------------------
## What does this SDK do?
This SDK provides a full object model around the Matrix Client-Server API and emits
events for incoming data and state changes. Aside from wrapping the HTTP API, it:
- Handles syncing (via `/initialSync` and `/events`)
- Handles the generation of "friendly" room and member names.
- Handles historical `RoomMember` information (e.g. display names).
- Manages room member state across multiple events (e.g. it handles typing, power
levels and membership changes).
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
which can be listened to for things like name changes, new messages, membership
changes, presence changes, and more.
- Handle "local echo" of messages sent using the SDK. This means that messages
that have just been sent will appear in the timeline as 'sending', until it
completes. This is beneficial because it prevents there being a gap between
hitting the send button and having the "remote echo" arrive.
- Mark messages which failed to send as not sent.
- Automatically retry requests to send messages due to network errors.
- Automatically retry requests to send messages due to rate limiting errors.
- Handle queueing of messages.
- Handles pagination.
- Handle assigning push actions for events.
- Handles room initial sync on accepting invites.
- Handles WebRTC calling.
- Handles syncing (via `/initialSync` and `/events`)
- Handles the generation of "friendly" room and member names.
- Handles historical `RoomMember` information (e.g. display names).
- Manages room member state across multiple events (e.g. it handles typing, power
levels and membership changes).
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
which can be listened to for things like name changes, new messages, membership
changes, presence changes, and more.
- Handle "local echo" of messages sent using the SDK. This means that messages
that have just been sent will appear in the timeline as 'sending', until it
completes. This is beneficial because it prevents there being a gap between
hitting the send button and having the "remote echo" arrive.
- Mark messages which failed to send as not sent.
- Automatically retry requests to send messages due to network errors.
- Automatically retry requests to send messages due to rate limiting errors.
- Handle queueing of messages.
- Handles pagination.
- Handle assigning push actions for events.
- Handles room initial sync on accepting invites.
- Handles WebRTC calling.
Later versions of the SDK will:
- Expose a `RoomSummary` which would be suitable for a recents page.
- Provide different pluggable storage layers (e.g. local storage, database-backed)
Usage
=====
- Expose a `RoomSummary` which would be suitable for a recents page.
- Provide different pluggable storage layers (e.g. local storage, database-backed)
# Usage
Conventions
-----------
## Conventions
### Emitted events
The SDK will emit events using an ``EventEmitter``. It also
emits object models (e.g. ``Rooms``, ``RoomMembers``) when they
The SDK will emit events using an `EventEmitter`. It also
emits object models (e.g. `Rooms`, `RoomMembers`) when they
are updated.
```javascript
// Listen for low-level MatrixEvents
client.on("event", function(event) {
// Listen for low-level MatrixEvents
client.on("event", function (event) {
console.log(event.getType());
});
});
// Listen for typing changes
client.on("RoomMember.typing", function(event, member) {
// Listen for typing changes
client.on("RoomMember.typing", function (event, member) {
if (member.typing) {
console.log(member.name + " is typing...");
console.log(member.name + " is typing...");
} else {
console.log(member.name + " stopped typing.");
}
else {
console.log(member.name + " stopped typing.");
}
});
});
// start the client to setup the connection to the server
client.startClient();
// start the client to setup the connection to the server
client.startClient();
```
### Promises and Callbacks
@@ -181,11 +187,11 @@ The typical usage is something like:
});
```
Alternatively, if you have a Node.js-style ``callback(err, result)`` function,
Alternatively, if you have a Node.js-style `callback(err, result)` function,
you can pass the result of the promise into it with something like:
```javascript
matrixClient.someMethod(arg1, arg2).nodeify(callback);
matrixClient.someMethod(arg1, arg2).nodeify(callback);
```
The main thing to note is that it is problematic to discard the result of a
@@ -193,61 +199,65 @@ promise-returning function, as that will cause exceptions to go unobserved.
Methods which return a promise show this in their documentation.
Many methods in the SDK support *both* Node.js-style callbacks *and* Promises,
via an optional ``callback`` argument. The callback support is now deprecated:
new methods do not include a ``callback`` argument, and in the future it may be
Many methods in the SDK support _both_ Node.js-style callbacks _and_ Promises,
via an optional `callback` argument. The callback support is now deprecated:
new methods do not include a `callback` argument, and in the future it may be
removed from existing methods.
Examples
--------
## Examples
This section provides some useful code snippets which demonstrate the
core functionality of the SDK. These examples assume the SDK is setup like this:
```javascript
import * as sdk from "matrix-js-sdk";
const myUserId = "@example:localhost";
const myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
const matrixClient = sdk.createClient({
baseUrl: "http://localhost:8008",
accessToken: myAccessToken,
userId: myUserId
});
import * as sdk from "matrix-js-sdk";
const myUserId = "@example:localhost";
const myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
const matrixClient = sdk.createClient({
baseUrl: "http://localhost:8008",
accessToken: myAccessToken,
userId: myUserId,
});
```
### Automatically join rooms when invited
```javascript
matrixClient.on("RoomMember.membership", function(event, member) {
if (member.membership === "invite" && member.userId === myUserId) {
matrixClient.joinRoom(member.roomId).then(function() {
console.log("Auto-joined %s", member.roomId);
});
}
});
matrixClient.on("RoomMember.membership", function (event, member) {
if (member.membership === "invite" && member.userId === myUserId) {
matrixClient.joinRoom(member.roomId).then(function () {
console.log("Auto-joined %s", member.roomId);
});
}
});
matrixClient.startClient();
matrixClient.startClient();
```
### Print out messages for all rooms
```javascript
matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) {
if (toStartOfTimeline) {
return; // don't print paginated results
}
if (event.getType() !== "m.room.message") {
return; // only print messages
}
console.log(
// the room name will update with m.room.name events automatically
"(%s) %s :: %s", room.name, event.getSender(), event.getContent().body
);
});
matrixClient.on("Room.timeline", function (event, room, toStartOfTimeline) {
if (toStartOfTimeline) {
return; // don't print paginated results
}
if (event.getType() !== "m.room.message") {
return; // only print messages
}
console.log(
// the room name will update with m.room.name events automatically
"(%s) %s :: %s",
room.name,
event.getSender(),
event.getContent().body,
);
});
matrixClient.startClient();
matrixClient.startClient();
```
Output:
```
(My Room) @megan:localhost :: Hello world
(My Room) @megan:localhost :: how are you?
@@ -259,27 +269,24 @@ Output:
### Print out membership lists whenever they are changed
```javascript
matrixClient.on("RoomState.members", function(event, state, member) {
const room = matrixClient.getRoom(state.roomId);
if (!room) {
return;
}
const memberList = state.getMembers();
console.log(room.name);
console.log(Array(room.name.length + 1).join("=")); // underline
for (var i = 0; i < memberList.length; i++) {
console.log(
"(%s) %s",
memberList[i].membership,
memberList[i].name
);
}
});
matrixClient.on("RoomState.members", function (event, state, member) {
const room = matrixClient.getRoom(state.roomId);
if (!room) {
return;
}
const memberList = state.getMembers();
console.log(room.name);
console.log(Array(room.name.length + 1).join("=")); // underline
for (var i = 0; i < memberList.length; i++) {
console.log("(%s) %s", memberList[i].membership, memberList[i].name);
}
});
matrixClient.startClient();
matrixClient.startClient();
```
Output:
```
My Room
=======
@@ -289,36 +296,34 @@ Output:
(invite) @charlie:localhost
```
API Reference
=============
# 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
```
Then visit ``http://localhost:8005`` to see the API docs.
Then visit `http://localhost:8005` to see the API docs.
End-to-end encryption support
=============================
# End-to-end encryption support
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
application to make libolm available, via the ``Olm`` global.
application to make libolm available, via the `Olm` global.
It is also necessary to call ``await matrixClient.initCrypto()`` after creating a new
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
It is also necessary to call `await matrixClient.initCrypto()` after creating a new
`MatrixClient` (but **before** calling `matrixClient.startClient()`) to
initialise the crypto layer.
If the ``Olm`` global is not available, the SDK will show a warning, as shown
below; ``initCrypto()`` will also fail.
If the `Olm` global is not available, the SDK will show a warning, as shown
below; `initCrypto()` will also fail.
```
Unable to load crypto module: crypto will be disabled: Error: global.Olm is not defined
@@ -330,46 +335,51 @@ specification.
To provide the Olm library in a browser application:
* download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
* load ``olm.js`` as a ``<script>`` *before* ``browser-matrix.js``.
- download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
- load `olm.js` as a `<script>` _before_ `browser-matrix.js`.
To provide the Olm library in a node.js application:
* ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``
(replace the URL with the latest version you want to use from
- `yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz`
(replace the URL with the latest version you want to use from
https://packages.matrix.org/npm/olm/)
* ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``.
- `global.Olm = require('olm');` _before_ loading `matrix-js-sdk`.
If you want to package Olm as dependency for your node.js application, you can
use ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``. If your
application also works without e2e crypto enabled, add ``--optional`` to mark it
use `yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz`. If your
application also works without e2e crypto enabled, add `--optional` to mark it
as an optional dependency.
# Contributing
Contributing
============
*This section is for people who want to modify the SDK. If you just
want to use this SDK, skip this section.*
_This section is for people who want to modify the SDK. If you just
want to use this SDK, skip this section._
First, you need to pull in the right build tools:
```
$ yarn install
```
Building
--------
## Building
To build a browser version from scratch when developing::
```
$ yarn build
```
To run tests (Jasmine)::
To run tests (Jest):
```
$ yarn test
```
> **Note**
> The `sync-browserify.spec.ts` requires a browser build (`yarn build`) in order to pass
To run linting:
```
$ yarn lint
```
+27 -27
View File
@@ -20,19 +20,19 @@ blurrier.
When we are low on disk space overall or near the group limit / origin quota:
* Chrome
* Log database may fail to start with AbortError
* IndexedDB fails to start for crypto: AbortError in connect from
indexeddb-store-worker
* When near the quota, QuotaExceededError is used more consistently
* Firefox
* The first error will be QuotaExceededError
* Future write attempts will fail with various errors when space is low,
including nonsense like "InvalidStateError: A mutation operation was
attempted on a database that did not allow mutations."
* Once you start getting errors, the DB is effectively wedged in read-only
mode
* Can revive access if you reopen the DB
- Chrome
- Log database may fail to start with AbortError
- IndexedDB fails to start for crypto: AbortError in connect from
indexeddb-store-worker
- When near the quota, QuotaExceededError is used more consistently
- Firefox
- The first error will be QuotaExceededError
- Future write attempts will fail with various errors when space is low,
including nonsense like "InvalidStateError: A mutation operation was
attempted on a database that did not allow mutations."
- Once you start getting errors, the DB is effectively wedged in read-only
mode
- Can revive access if you reopen the DB
## Cache Eviction
@@ -41,9 +41,9 @@ limited by a single quota, in practice, browsers appear to handle `localStorage`
separately from the others, so it has a separate quota limit and isn't evicted
when low on space.
* Chrome, Firefox
* IndexedDB for origin deleted
* Local Storage remains in place
- Chrome, Firefox
- IndexedDB for origin deleted
- Local Storage remains in place
## Persistent Storage
@@ -51,20 +51,20 @@ Storage Standard offers a `navigator.storage.persist` API that can be used to
request persistent storage that won't be deleted by the browser because of low
space.
* Chrome
* Chrome 75 seems to grant this without any prompt based on [interaction
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
* Firefox
* Firefox 67 shows a prompt to grant
* Reverting persistent seems to require revoking permission _and_ clearing
site data
- Chrome
- Chrome 75 seems to grant this without any prompt based on [interaction
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
- Firefox
- Firefox 67 shows a prompt to grant
- Reverting persistent seems to require revoking permission _and_ clearing
site data
## Storage Estimation
Storage Standard offers a `navigator.storage.estimate` API to get some clue of
how much space remains.
* Chrome, Firefox
* Can run this at any time to request an estimate of space remaining
* Firefox
* Returns `0` for `usage` if a site is persisted
- Chrome, Firefox
- Can run this at any time to request an estimate of space remaining
- Firefox
- Returns `0` for `usage` if a site is persisted
+4 -3
View File
@@ -1,9 +1,10 @@
To try it out, **you must build the SDK first** and then host this folder:
```
$ npm run build
$ yarn install
$ yarn build
$ cd examples/browser
$ python -m SimpleHTTPServer 8003
$ python -m http.server 8003
```
Then visit ``http://localhost:8003``.
Then visit `http://localhost:8003`.
+2 -6
View File
@@ -1,11 +1,7 @@
console.log("Loading browser sdk");
var client = matrixcs.createClient("https://matrix.org");
client.publicRooms(function (err, data) {
if (err) {
console.error("err %s", JSON.stringify(err));
return;
}
var client = matrixcs.createClient({ baseUrl: "https://matrix.org" });
client.publicRooms().then(function (data) {
console.log("data %s [...]", JSON.stringify(data).substring(0, 100));
console.log("Congratulations! The SDK is working on the browser!");
var result = document.getElementById("result");
+15 -16
View File
@@ -1,18 +1,17 @@
<html lang="en">
<head>
<title>Test</title>
<meta charset="utf-8"/>
<link rel="icon" href="data:,">
<script src="lib/matrix.js"></script>
<script src="browserTest.js"></script>
</head>
<body>
Sanity Testing (check the console) : This example is here to make sure that
the SDK works inside a browser. It simply does a GET /publicRooms on
matrix.org
<br/>
You should see a message confirming that the SDK works below:
<br/>
<div id="result"></div>
</body>
<head>
<title>Test</title>
<meta charset="utf-8" />
<link rel="icon" href="data:," />
<script src="lib/matrix.js"></script>
<script src="browserTest.js"></script>
</head>
<body>
Sanity Testing (check the console) : This example is here to make sure that the SDK works inside a browser. It
simply does a GET /publicRooms on matrix.org
<br />
You should see a message confirming that the SDK works below:
<br />
<div id="result"></div>
</body>
</html>
@@ -1,59 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Test Crypto in Browser</title>
<script src="lib/olm.js"></script>
<script src="lib/matrix.js"></script>
</head>
<body>
<h1>Testing export/import of Olm devices in the browser</h1>
<ul>
<li>
Make sure you built the current version of the Matrix JS SDK
(<code>yarn build</code>)
</li>
<li>
copy <code>olm.js</code> and <code>olm.wasm</code>
from a recent release of Olm (was tested with version 3.1.4)
in directory <code>lib/</code>
</li>
<li>start a local Matrix homeserver (on port 8008, or change the port in the code)</li>
<li>Serve this HTML file (e.g. <code>python3 -m http.server</code>) and go to it through your browser</li>
<li>
in the JS console, do:
<pre>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Test Crypto in Browser</title>
<script src="lib/olm.js"></script>
<script src="lib/matrix.js"></script>
</head>
<body>
<h1>Testing export/import of Olm devices in the browser</h1>
<ul>
<li>Make sure you built the current version of the Matrix JS SDK (<code>yarn build</code>)</li>
<li>
copy <code>olm.js</code> and <code>olm.wasm</code> from a recent release of Olm (was tested with version
3.1.4) in directory <code>lib/</code>
</li>
<li>start a local Matrix homeserver (on port 8008, or change the port in the code)</li>
<li>Serve this HTML file (e.g. <code>python3 -m http.server</code>) and go to it through your browser</li>
<li>
in the JS console, do:
<pre>
aliceMatrixClient = await newMatrixClient("alice-"+randomHex());
await aliceMatrixClient.exportDevice();
await aliceMatrixClient.getAccessToken();
</pre>
</li>
<li>
copy the result of <code>exportDevice</code> and <code>getAccessToken</code> somewhere
(<strong>not</strong> in a JS variable as it will be destroyed when you refresh the page)
</li>
<li><strong>refresh the page (F5)</strong> to make sure the client is destroyed</li>
<li>
Do the following, replacing <code>ALICE_ID</code>
with the user ID of Alice (you can find it in the exported data)
<pre>
</pre
>
</li>
<li>
copy the result of <code>exportDevice</code> and <code>getAccessToken</code> somewhere (<strong
>not</strong
>
in a JS variable as it will be destroyed when you refresh the page)
</li>
<li><strong>refresh the page (F5)</strong> to make sure the client is destroyed</li>
<li>
Do the following, replacing <code>ALICE_ID</code>
with the user ID of Alice (you can find it in the exported data)
<pre>
bobMatrixClient = await newMatrixClient("bob-"+randomHex());
roomId = await bobMatrixClient.createEncryptedRoom([ALICE_ID]);
await bobMatrixClient.sendTextMessage('Hi Alice!', roomId);
</pre>
</li>
<li>Again, <strong>refresh the page (F5)</strong>. You may want to clear your console as well.</li>
<li>
Now do the following, using the exported data and the access token you saved previously:
<pre>
</pre
>
</li>
<li>Again, <strong>refresh the page (F5)</strong>. You may want to clear your console as well.</li>
<li>
Now do the following, using the exported data and the access token you saved previously:
<pre>
aliceMatrixClient = await importMatrixClient(EXPORTED_DATA, ACCESS_TOKEN);
</pre>
</li>
<li>You should see the message sent by Bob printed in the console.</li>
</ul>
</pre
>
</li>
<li>You should see the message sent by Bob printed in the console.</li>
</ul>
<script src="olm-device-export-import.js"></script>
</body>
</html>
<script src="olm-device-export-import.js"></script>
</body>
</html>
@@ -1,34 +1,26 @@
if (!Olm) {
console.error(
"global.Olm does not seem to be present."
+ " Did you forget to add olm in the lib/ directory?"
);
console.error("global.Olm does not seem to be present." + " Did you forget to add olm in the lib/ directory?");
}
const BASE_URL = 'http://localhost:8008';
const ROOM_CRYPTO_CONFIG = { algorithm: 'm.megolm.v1.aes-sha2' };
const PASSWORD = 'password';
const BASE_URL = "http://localhost:8008";
const ROOM_CRYPTO_CONFIG = { algorithm: "m.megolm.v1.aes-sha2" };
const PASSWORD = "password";
// useful to create new usernames
window.randomHex = () => Math.floor(Math.random() * (10**6)).toString(16);
window.randomHex = () => Math.floor(Math.random() * 10 ** 6).toString(16);
window.newMatrixClient = async function (username) {
const registrationClient = matrixcs.createClient(BASE_URL);
const userRegisterResult = await registrationClient.register(
username,
PASSWORD,
null,
{ type: 'm.login.dummy' }
);
const userRegisterResult = await registrationClient.register(username, PASSWORD, null, { type: "m.login.dummy" });
const matrixClient = matrixcs.createClient({
baseUrl: BASE_URL,
userId: userRegisterResult.user_id,
accessToken: userRegisterResult.access_token,
deviceId: userRegisterResult.device_id,
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
cryptoStore: new matrixcs.MemoryCryptoStore(),
baseUrl: BASE_URL,
userId: userRegisterResult.user_id,
accessToken: userRegisterResult.access_token,
deviceId: userRegisterResult.device_id,
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
cryptoStore: new matrixcs.MemoryCryptoStore(),
});
extendMatrixClient(matrixClient);
@@ -36,15 +28,15 @@ window.newMatrixClient = async function (username) {
await matrixClient.initCrypto();
await matrixClient.startClient();
return matrixClient;
}
};
window.importMatrixClient = async function (exportedDevice, accessToken) {
const matrixClient = matrixcs.createClient({
baseUrl: BASE_URL,
deviceToImport: exportedDevice,
accessToken,
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
cryptoStore: new matrixcs.MemoryCryptoStore(),
baseUrl: BASE_URL,
deviceToImport: exportedDevice,
accessToken,
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
cryptoStore: new matrixcs.MemoryCryptoStore(),
});
extendMatrixClient(matrixClient);
@@ -52,71 +44,62 @@ window.importMatrixClient = async function (exportedDevice, accessToken) {
await matrixClient.initCrypto();
await matrixClient.startClient();
return matrixClient;
}
};
function extendMatrixClient(matrixClient) {
// automatic join
matrixClient.on('RoomMember.membership', async (event, member) => {
if (member.membership === 'invite' && member.userId === matrixClient.getUserId()) {
matrixClient.on("RoomMember.membership", async (event, member) => {
if (member.membership === "invite" && member.userId === matrixClient.getUserId()) {
await matrixClient.joinRoom(member.roomId);
// setting up of room encryption seems to be triggered automatically
// but if we don't wait for it the first messages we send are unencrypted
await matrixClient.setRoomEncryption(member.roomId, { algorithm: 'm.megolm.v1.aes-sha2' })
await matrixClient.setRoomEncryption(member.roomId, { algorithm: "m.megolm.v1.aes-sha2" });
}
});
matrixClient.onDecryptedMessage = message => {
console.log('Got encrypted message: ', message);
}
matrixClient.onDecryptedMessage = (message) => {
console.log("Got encrypted message: ", message);
};
matrixClient.on('Event.decrypted', (event) => {
if (event.getType() === 'm.room.message'){
matrixClient.on("Event.decrypted", (event) => {
if (event.getType() === "m.room.message") {
matrixClient.onDecryptedMessage(event.getContent().body);
} else {
console.log('decrypted an event of type', event.getType());
console.log("decrypted an event of type", event.getType());
console.log(event);
}
});
matrixClient.createEncryptedRoom = async function(usersToInvite) {
const {
room_id: roomId,
} = await this.createRoom({
visibility: 'private',
invite: usersToInvite,
matrixClient.createEncryptedRoom = async function (usersToInvite) {
const { room_id: roomId } = await this.createRoom({
visibility: "private",
invite: usersToInvite,
});
// matrixClient.setRoomEncryption() only updates local state
// but does not send anything to the server
// (see https://github.com/matrix-org/matrix-js-sdk/issues/905)
// so we do it ourselves with 'sendStateEvent'
await this.sendStateEvent(
roomId, 'm.room.encryption', ROOM_CRYPTO_CONFIG,
);
await this.setRoomEncryption(
roomId, ROOM_CRYPTO_CONFIG,
);
await this.sendStateEvent(roomId, "m.room.encryption", ROOM_CRYPTO_CONFIG);
await this.setRoomEncryption(roomId, ROOM_CRYPTO_CONFIG);
// Marking all devices as verified
let room = this.getRoom(roomId);
let members = (await room.getEncryptionTargetMembers()).map(x => x["userId"])
let members = (await room.getEncryptionTargetMembers()).map((x) => x["userId"]);
let memberkeys = await this.downloadKeys(members);
for (const userId in memberkeys) {
for (const deviceId in memberkeys[userId]) {
await this.setDeviceVerified(userId, deviceId);
}
for (const deviceId in memberkeys[userId]) {
await this.setDeviceVerified(userId, deviceId);
}
}
return roomId;
}
};
matrixClient.sendTextMessage = async function(message, roomId) {
return matrixClient.sendMessage(
roomId,
{
body: message,
msgtype: 'm.text',
}
)
}
}
matrixClient.sendTextMessage = async function (message, roomId) {
return matrixClient.sendMessage(roomId, {
body: message,
msgtype: "m.text",
});
};
}
+1 -2
View File
@@ -1,6 +1,5 @@
This is a functional terminal app which allows you to see the room list for a user, join rooms, send messages and view room membership lists.
To try it out, you will need to edit `app.js` to configure it for your `homeserver`, `access_token` and `user_id`. Then run:
```
@@ -24,7 +23,7 @@ Room list index commands:
Room commands:
'/exit' Return to the room list index.
'/members' Show the room member list.
$ /enter 2
[2015-06-12 15:14:54] Megan2 <<< herro
+140 -152
View File
@@ -5,7 +5,7 @@ var clc = require("cli-color");
var matrixClient = sdk.createClient({
baseUrl: "http://localhost:8008",
accessToken: myAccessToken,
userId: myUserId
userId: myUserId,
});
// Data structures
@@ -14,15 +14,15 @@ var viewingRoom = null;
var numMessagesToShow = 20;
// Reading from stdin
var CLEAR_CONSOLE = '\x1B[2J';
var CLEAR_CONSOLE = "\x1B[2J";
var readline = require("readline");
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
completer: completer
completer: completer,
});
rl.setPrompt("$ ");
rl.on('line', function(line) {
rl.on("line", function (line) {
if (line.trim().length === 0) {
rl.prompt();
return;
@@ -37,14 +37,11 @@ rl.on('line', function(line) {
if (line === "/exit") {
viewingRoom = null;
printRoomList();
}
else if (line === "/members") {
} else if (line === "/members") {
printMemberList(viewingRoom);
}
else if (line === "/roominfo") {
} else if (line === "/roominfo") {
printRoomInfo(viewingRoom);
}
else if (line === "/resend") {
} else if (line === "/resend") {
// get the oldest not sent event.
var notSentEvent;
for (var i = 0; i < viewingRoom.timeline.length; i++) {
@@ -54,76 +51,84 @@ rl.on('line', function(line) {
}
}
if (notSentEvent) {
matrixClient.resendEvent(notSentEvent, viewingRoom).then(function() {
printMessages();
rl.prompt();
}, function(err) {
printMessages();
print("/resend Error: %s", err);
rl.prompt();
});
matrixClient.resendEvent(notSentEvent, viewingRoom).then(
function () {
printMessages();
rl.prompt();
},
function (err) {
printMessages();
print("/resend Error: %s", err);
rl.prompt();
},
);
printMessages();
rl.prompt();
}
}
else if (line.indexOf("/more ") === 0) {
} else if (line.indexOf("/more ") === 0) {
var amount = parseInt(line.split(" ")[1]) || 20;
matrixClient.scrollback(viewingRoom, amount).then(function(room) {
printMessages();
rl.prompt();
}, function(err) {
print("/more Error: %s", err);
});
}
else if (line.indexOf("/invite ") === 0) {
matrixClient.scrollback(viewingRoom, amount).then(
function (room) {
printMessages();
rl.prompt();
},
function (err) {
print("/more Error: %s", err);
},
);
} else if (line.indexOf("/invite ") === 0) {
var userId = line.split(" ")[1].trim();
matrixClient.invite(viewingRoom.roomId, userId).then(function() {
printMessages();
rl.prompt();
}, function(err) {
print("/invite Error: %s", err);
});
}
else if (line.indexOf("/file ") === 0) {
matrixClient.invite(viewingRoom.roomId, userId).then(
function () {
printMessages();
rl.prompt();
},
function (err) {
print("/invite Error: %s", err);
},
);
} else if (line.indexOf("/file ") === 0) {
var filename = line.split(" ")[1].trim();
var stream = fs.createReadStream(filename);
matrixClient.uploadContent({
stream: stream,
name: filename
}).then(function(url) {
var content = {
msgtype: "m.file",
body: filename,
url: JSON.parse(url).content_uri
};
matrixClient.sendMessage(viewingRoom.roomId, content);
});
}
else {
matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function() {
matrixClient
.uploadContent({
stream: stream,
name: filename,
})
.then(function (url) {
var content = {
msgtype: "m.file",
body: filename,
url: JSON.parse(url).content_uri,
};
matrixClient.sendMessage(viewingRoom.roomId, content);
});
} else {
matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function () {
printMessages();
rl.prompt();
});
// print local echo immediately
printMessages();
}
}
else {
} else {
if (line.indexOf("/join ") === 0) {
var roomIndex = line.split(" ")[1];
viewingRoom = roomList[roomIndex];
if (viewingRoom.getMember(myUserId).membership === "invite") {
// join the room first
matrixClient.joinRoom(viewingRoom.roomId).then(function(room) {
setRoomList();
viewingRoom = room;
printMessages();
rl.prompt();
}, function(err) {
print("/join Error: %s", err);
});
}
else {
matrixClient.joinRoom(viewingRoom.roomId).then(
function (room) {
setRoomList();
viewingRoom = room;
printMessages();
rl.prompt();
},
function (err) {
print("/join Error: %s", err);
},
);
} else {
printMessages();
}
}
@@ -133,18 +138,18 @@ rl.on('line', function(line) {
// ==== END User input
// show the room list after syncing.
matrixClient.on("sync", function(state, prevState, data) {
matrixClient.on("sync", function (state, prevState, data) {
switch (state) {
case "PREPARED":
setRoomList();
printRoomList();
printHelp();
rl.prompt();
break;
}
setRoomList();
printRoomList();
printHelp();
rl.prompt();
break;
}
});
matrixClient.on("Room", function() {
matrixClient.on("Room", function () {
setRoomList();
if (!viewingRoom) {
printRoomList();
@@ -153,7 +158,7 @@ matrixClient.on("Room", function() {
});
// print incoming messages.
matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) {
matrixClient.on("Room.timeline", function (event, room, toStartOfTimeline) {
if (toStartOfTimeline) {
return; // don't print paginated results
}
@@ -165,20 +170,19 @@ matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) {
function setRoomList() {
roomList = matrixClient.getRooms();
roomList.sort(function(a,b) {
roomList.sort(function (a, b) {
// < 0 = a comes first (lower index) - we want high indexes = newer
var aMsg = a.timeline[a.timeline.length-1];
var aMsg = a.timeline[a.timeline.length - 1];
if (!aMsg) {
return -1;
}
var bMsg = b.timeline[b.timeline.length-1];
var bMsg = b.timeline[b.timeline.length - 1];
if (!bMsg) {
return 1;
}
if (aMsg.getTs() > bMsg.getTs()) {
return 1;
}
else if (aMsg.getTs() < bMsg.getTs()) {
} else if (aMsg.getTs() < bMsg.getTs()) {
return -1;
}
return 0;
@@ -189,16 +193,15 @@ function printRoomList() {
print(CLEAR_CONSOLE);
print("Room List:");
var fmts = {
"invite": clc.cyanBright,
"leave": clc.blackBright
invite: clc.cyanBright,
leave: clc.blackBright,
};
for (var i = 0; i < roomList.length; i++) {
var msg = roomList[i].timeline[roomList[i].timeline.length-1];
var msg = roomList[i].timeline[roomList[i].timeline.length - 1];
var dateStr = "---";
var fmt;
if (msg) {
dateStr = new Date(msg.getTs()).toISOString().replace(
/T/, ' ').replace(/\..+/, '');
dateStr = new Date(msg.getTs()).toISOString().replace(/T/, " ").replace(/\..+/, "");
}
var myMembership = roomList[i].getMyMembership();
if (myMembership) {
@@ -207,9 +210,10 @@ function printRoomList() {
var roomName = fixWidth(roomList[i].name, 25);
print(
"[%s] %s (%s members) %s",
i, fmt ? fmt(roomName) : roomName,
i,
fmt ? fmt(roomName) : roomName,
roomList[i].getJoinedMembers().length,
dateStr
dateStr,
);
}
}
@@ -230,12 +234,12 @@ function printHelp() {
}
function completer(line) {
var completions = [
"/help", "/join ", "/exit", "/members", "/more ", "/resend", "/invite"
];
var hits = completions.filter(function(c) { return c.indexOf(line) == 0 });
var completions = ["/help", "/join ", "/exit", "/members", "/more ", "/resend", "/invite"];
var hits = completions.filter(function (c) {
return c.indexOf(line) == 0;
});
// show all completions if none found
return [hits.length ? hits : completions, line]
return [hits.length ? hits : completions, line];
}
function printMessages() {
@@ -252,14 +256,14 @@ function printMessages() {
function printMemberList(room) {
var fmts = {
"join": clc.green,
"ban": clc.red,
"invite": clc.blue,
"leave": clc.blackBright
join: clc.green,
ban: clc.red,
invite: clc.blue,
leave: clc.blackBright,
};
var members = room.currentState.getMembers();
// sorted based on name.
members.sort(function(a, b) {
members.sort(function (a, b) {
if (a.name > b.name) {
return -1;
}
@@ -268,21 +272,24 @@ function printMemberList(room) {
}
return 0;
});
print("Membership list for room \"%s\"", room.name);
print('Membership list for room "%s"', room.name);
print(new Array(room.name.length + 28).join("-"));
room.currentState.getMembers().forEach(function(member) {
room.currentState.getMembers().forEach(function (member) {
if (!member.membership) {
return;
}
var fmt = fmts[member.membership] || function(a){return a;};
var membershipWithPadding = (
member.membership + new Array(10 - member.membership.length).join(" ")
);
var fmt =
fmts[member.membership] ||
function (a) {
return a;
};
var membershipWithPadding = member.membership + new Array(10 - member.membership.length).join(" ");
print(
"%s"+fmt(" :: ")+"%s"+fmt(" (")+"%s"+fmt(")"),
membershipWithPadding, member.name,
(member.userId === myUserId ? "Me" : member.userId),
fmt
"%s" + fmt(" :: ") + "%s" + fmt(" (") + "%s" + fmt(")"),
membershipWithPadding,
member.name,
member.userId === myUserId ? "Me" : member.userId,
fmt,
);
});
}
@@ -292,38 +299,31 @@ function printRoomInfo(room) {
var eTypeHeader = " Event Type(state_key) ";
var sendHeader = " Sender ";
// pad content to 100
var restCount = (
100 - "Content".length - " | ".length - " | ".length -
eTypeHeader.length - sendHeader.length
);
var padSide = new Array(Math.floor(restCount/2)).join(" ");
var restCount = 100 - "Content".length - " | ".length - " | ".length - eTypeHeader.length - sendHeader.length;
var padSide = new Array(Math.floor(restCount / 2)).join(" ");
var contentHeader = padSide + "Content" + padSide;
print(eTypeHeader+sendHeader+contentHeader);
print(eTypeHeader + sendHeader + contentHeader);
print(new Array(100).join("-"));
eventMap.keys().forEach(function(eventType) {
if (eventType === "m.room.member") { return; } // use /members instead.
eventMap.keys().forEach(function (eventType) {
if (eventType === "m.room.member") {
return;
} // use /members instead.
var eventEventMap = eventMap.get(eventType);
eventEventMap.keys().forEach(function(stateKey) {
var typeAndKey = eventType + (
stateKey.length > 0 ? "("+stateKey+")" : ""
);
eventEventMap.keys().forEach(function (stateKey) {
var typeAndKey = eventType + (stateKey.length > 0 ? "(" + stateKey + ")" : "");
var typeStr = fixWidth(typeAndKey, eTypeHeader.length);
var event = eventEventMap.get(stateKey);
var sendStr = fixWidth(event.getSender(), sendHeader.length);
var contentStr = fixWidth(
JSON.stringify(event.getContent()), contentHeader.length
);
print(typeStr+" | "+sendStr+" | "+contentStr);
var contentStr = fixWidth(JSON.stringify(event.getContent()), contentHeader.length);
print(typeStr + " | " + sendStr + " | " + contentStr);
});
})
});
}
function printLine(event) {
var fmt;
var name = event.sender ? event.sender.name : event.getSender();
var time = new Date(
event.getTs()
).toISOString().replace(/T/, ' ').replace(/\..+/, '');
var time = new Date(event.getTs()).toISOString().replace(/T/, " ").replace(/\..+/, "");
var separator = "<<<";
if (event.getSender() === myUserId) {
name = "Me";
@@ -331,8 +331,7 @@ function printLine(event) {
if (event.status === sdk.EventStatus.SENDING) {
separator = "...";
fmt = clc.xterm(8);
}
else if (event.status === sdk.EventStatus.NOT_SENT) {
} else if (event.status === sdk.EventStatus.NOT_SENT) {
separator = " x ";
fmt = clc.redBright;
}
@@ -341,69 +340,58 @@ function printLine(event) {
var maxNameWidth = 15;
if (name.length > maxNameWidth) {
name = name.slice(0, maxNameWidth-1) + "\u2026";
name = name.slice(0, maxNameWidth - 1) + "\u2026";
}
if (event.getType() === "m.room.message") {
body = event.getContent().body;
}
else if (event.isState()) {
} else if (event.isState()) {
var stateName = event.getType();
if (event.getStateKey().length > 0) {
stateName += " ("+event.getStateKey()+")";
stateName += " (" + event.getStateKey() + ")";
}
body = (
"[State: "+stateName+" updated to: "+JSON.stringify(event.getContent())+"]"
);
body = "[State: " + stateName + " updated to: " + JSON.stringify(event.getContent()) + "]";
separator = "---";
fmt = clc.xterm(249).italic;
}
else {
} else {
// random message event
body = (
"[Message: "+event.getType()+" Content: "+JSON.stringify(event.getContent())+"]"
);
body = "[Message: " + event.getType() + " Content: " + JSON.stringify(event.getContent()) + "]";
separator = "---";
fmt = clc.xterm(249).italic;
}
if (fmt) {
print(
"[%s] %s %s %s", time, name, separator, body, fmt
);
}
else {
print("[%s] %s %s %s", time, name, separator, body, fmt);
} else {
print("[%s] %s %s %s", time, name, separator, body);
}
}
function print(str, formatter) {
if (typeof arguments[arguments.length-1] === "function") {
if (typeof arguments[arguments.length - 1] === "function") {
// last arg is the formatter so get rid of it and use it on each
// param passed in but not the template string.
var newArgs = [];
var i = 0;
for (i=0; i<arguments.length-1; i++) {
for (i = 0; i < arguments.length - 1; i++) {
newArgs.push(arguments[i]);
}
var fmt = arguments[arguments.length-1];
for (i=0; i<newArgs.length; i++) {
var fmt = arguments[arguments.length - 1];
for (i = 0; i < newArgs.length; i++) {
newArgs[i] = fmt(newArgs[i]);
}
console.log.apply(console.log, newArgs);
}
else {
} else {
console.log.apply(console.log, arguments);
}
}
function fixWidth(str, len) {
if (str.length > len) {
return str.substring(0, len-2) + "\u2026";
}
else if (str.length < len) {
return str.substring(0, len - 2) + "\u2026";
} else if (str.length < len) {
return str + new Array(len - str.length).join(" ");
}
return str;
}
matrixClient.startClient(numMessagesToShow); // messages for each room.
matrixClient.startClient(numMessagesToShow); // messages for each room.
+12 -12
View File
@@ -1,14 +1,14 @@
{
"name": "example-app",
"version": "0.0.0",
"description": "",
"main": "app.js",
"scripts": {
"preinstall": "npm install ../.."
},
"author": "",
"license": "Apache 2.0",
"dependencies": {
"cli-color": "^1.0.0"
}
"name": "example-app",
"version": "0.0.0",
"description": "",
"main": "app.js",
"scripts": {
"preinstall": "npm install ../.."
},
"author": "",
"license": "Apache 2.0",
"dependencies": {
"cli-color": "^1.0.0"
}
}
+1 -1
View File
@@ -6,4 +6,4 @@ To try it out, **you must build the SDK first** and then host this folder:
$ python -m SimpleHTTPServer 8003
```
Then visit ``http://localhost:8003``.
Then visit `http://localhost:8003`.
+26 -23
View File
@@ -9,7 +9,7 @@ const client = matrixcs.createClient({
baseUrl: BASE_URL,
accessToken: TOKEN,
userId: USER_ID,
deviceId: DEVICE_ID
deviceId: DEVICE_ID,
});
let call;
@@ -21,18 +21,16 @@ function disableButtons(place, answer, hangup) {
function addListeners(call) {
let lastError = "";
call.on("hangup", function() {
call.on("hangup", function () {
disableButtons(false, true, true);
document.getElementById("result").innerHTML = (
"<p>Call ended. Last error: "+lastError+"</p>"
);
document.getElementById("result").innerHTML = "<p>Call ended. Last error: " + lastError + "</p>";
});
call.on("error", function(err) {
call.on("error", function (err) {
lastError = err.message;
call.hangup();
disableButtons(false, true, true);
});
call.on("feeds_changed", function(feeds) {
call.on("feeds_changed", function (feeds) {
const localFeed = feeds.find((feed) => feed.isLocal());
const remoteFeed = feeds.find((feed) => !feed.isLocal());
@@ -51,33 +49,38 @@ function addListeners(call) {
});
}
window.onload = function() {
window.onload = function () {
document.getElementById("result").innerHTML = "<p>Please wait. Syncing...</p>";
document.getElementById("config").innerHTML = "<p>" +
"Homeserver: <code>"+BASE_URL+"</code><br/>"+
"Room: <code>"+ROOM_ID+"</code><br/>"+
"User: <code>"+USER_ID+"</code><br/>"+
document.getElementById("config").innerHTML =
"<p>" +
"Homeserver: <code>" +
BASE_URL +
"</code><br/>" +
"Room: <code>" +
ROOM_ID +
"</code><br/>" +
"User: <code>" +
USER_ID +
"</code><br/>" +
"</p>";
disableButtons(true, true, true);
};
client.on("sync", function(state, prevState, data) {
client.on("sync", function (state, prevState, data) {
switch (state) {
case "PREPARED":
syncComplete();
break;
}
syncComplete();
break;
}
});
function syncComplete() {
document.getElementById("result").innerHTML = "<p>Ready for calls.</p>";
disableButtons(false, true, true);
document.getElementById("call").onclick = function() {
document.getElementById("call").onclick = function () {
console.log("Placing call...");
call = matrixcs.createNewMatrixCall(
client, ROOM_ID
);
call = matrixcs.createNewMatrixCall(client, ROOM_ID);
console.log("Call => %s", call);
addListeners(call);
call.placeVideoCall();
@@ -85,14 +88,14 @@ function syncComplete() {
disableButtons(true, true, false);
};
document.getElementById("hangup").onclick = function() {
document.getElementById("hangup").onclick = function () {
console.log("Hanging up call...");
console.log("Call => %s", call);
call.hangup();
document.getElementById("result").innerHTML = "<p>Hungup call.</p>";
};
document.getElementById("answer").onclick = function() {
document.getElementById("answer").onclick = function () {
console.log("Answering call...");
console.log("Call => %s", call);
call.answer();
@@ -100,7 +103,7 @@ function syncComplete() {
document.getElementById("result").innerHTML = "<p>Answered call.</p>";
};
client.on("Call.incoming", function(c) {
client.on("Call.incoming", function (c) {
console.log("Call ringing");
disableButtons(true, false, false);
document.getElementById("result").innerHTML = "<p>Incoming call...</p>";
+19 -21
View File
@@ -1,25 +1,23 @@
<html>
<head>
<title>VoIP Test</title>
<script src="lib/matrix.js"></script>
<script src="browserTest.js"></script>
</head>
<head>
<title>VoIP Test</title>
<script src="lib/matrix.js"></script>
<script src="browserTest.js"></script>
</head>
<body>
You can place and receive calls with this example. Make sure to edit the
constants in <code>browserTest.js</code> first.
<div id="config"></div>
<div id="result"></div>
<button id="call">Place Call</button>
<button id="answer">Answer Call</button>
<button id="hangup">Hangup Call</button>
<div id="videoBackground" class="video-background">
<video class="video-element" id="local"></video>
<video class="video-element" id="remote"></video>
</div>
</body>
<body>
You can place and receive calls with this example. Make sure to edit the constants in
<code>browserTest.js</code> first.
<div id="config"></div>
<div id="result"></div>
<button id="call">Place Call</button>
<button id="answer">Answer Call</button>
<button id="hangup">Hangup Call</button>
<div id="videoBackground" class="video-background">
<video class="video-element" id="local"></video>
<video class="video-element" id="remote"></video>
</div>
</body>
</html>
<style>
@@ -31,4 +29,4 @@
.video-element {
height: 100%;
}
</style>
</style>
+47
View File
@@ -0,0 +1,47 @@
/* Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import type { Config } from "jest";
import { env } from "process";
const config: Config = {
testEnvironment: "node",
testMatch: ["<rootDir>/spec/**/*.spec.{js,ts}"],
setupFilesAfterEnv: ["<rootDir>/spec/setupTests.ts"],
collectCoverageFrom: ["<rootDir>/src/**/*.{js,ts}"],
coverageReporters: ["text-summary", "lcov"],
testResultsProcessor: "@casualbot/jest-sonar-reporter",
// Always print out a summary if there are any failing tests. Normally
// a summary is only printed if there are more than 20 test *suites*.
reporters: [["default", { summaryThreshold: 0 }]],
};
// if we're running under GHA, enable the GHA reporter
if (env["GITHUB_ACTIONS"] !== undefined) {
const reporters: Config["reporters"] = [
["github-actions", { silent: false }],
// as above: always show a summary if there were any failing tests.
["summary", { summaryThreshold: 0 }],
];
// if we're running against the develop branch, also enable the slow test reporter
if (env["GITHUB_REF"] == "refs/heads/develop") {
reporters.push("<rootDir>/spec/slowReporter.js");
}
config.reporters = reporters;
}
export default config;
-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
}
}
+157 -131
View File
@@ -1,135 +1,161 @@
{
"name": "matrix-js-sdk",
"version": "21.1.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=16.0.0"
},
"scripts": {
"prepublishOnly": "yarn build",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"dist": "echo 'This is for the release script so it can make assets (browser bundle).' && yarn build",
"clean": "rimraf lib dist",
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser",
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
"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",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 0 src spec",
"lint:js-fix": "eslint --fix src spec",
"lint:types": "tsc --noEmit",
"test": "jest",
"test:watch": "jest --watch",
"coverage": "yarn test --coverage"
},
"repository": {
"type": "git",
"url": "https://github.com/matrix-org/matrix-js-sdk"
},
"keywords": [
"matrix-org"
],
"main": "./lib/index.js",
"browser": "./lib/browser-index.js",
"matrix_src_main": "./src/index.ts",
"matrix_src_browser": "./src/browser-index.js",
"matrix_lib_main": "./lib/index.js",
"matrix_lib_typings": "./lib/index.d.ts",
"author": "matrix.org",
"license": "Apache-2.0",
"files": [
"dist",
"lib",
"src",
"git-revision.txt",
"CHANGELOG.md",
"CONTRIBUTING.rst",
"LICENSE",
"README.md",
"package.json",
"release.sh"
],
"dependencies": {
"@babel/runtime": "^7.12.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",
"p-retry": "4",
"qs": "^6.9.6",
"unhomoglyph": "^1.0.6"
},
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.10",
"@babel/eslint-plugin": "^7.12.10",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@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",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"allchange": "^1.0.6",
"babel-jest": "^29.0.0",
"babelify": "^10.0.0",
"better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0",
"docdash": "^1.2.0",
"domexception": "^4.0.0",
"eslint": "8.25.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",
"exorcist": "^2.0.0",
"fake-indexeddb": "^4.0.0",
"jest": "^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",
"typescript": "^4.5.3"
},
"jest": {
"testEnvironment": "node",
"testMatch": [
"<rootDir>/spec/**/*.spec.{js,ts}"
"name": "matrix-js-sdk",
"version": "28.2.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=18.0.0"
},
"scripts": {
"prepublishOnly": "yarn build",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"dist": "echo 'This is for the release script so it can make assets (browser bundle).' && yarn build",
"clean": "rimraf lib dist",
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser",
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "mkdir dist && BROWSERIFYSWAP_ENV='no-rust-crypto' browserify -d src/browser-index.ts -p [ tsify -p ./tsconfig-build.json ] | 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": "typedoc",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 0 src spec && prettier --check .",
"lint:js-fix": "prettier --loglevel=warn --write . && eslint --fix src spec",
"lint:types": "tsc --noEmit",
"test": "jest",
"test:watch": "jest --watch",
"coverage": "yarn test --coverage"
},
"repository": {
"type": "git",
"url": "https://github.com/matrix-org/matrix-js-sdk"
},
"keywords": [
"matrix-org"
],
"setupFilesAfterEnv": [
"<rootDir>/spec/setupTests.ts"
"main": "./lib/index.js",
"browser": "./lib/browser-index.js",
"matrix_src_main": "./src/index.ts",
"matrix_src_browser": "./src/browser-index.ts",
"matrix_lib_main": "./lib/index.js",
"matrix_lib_browser": "./lib/browser-index.js",
"matrix_lib_typings": "./lib/index.d.ts",
"author": "matrix.org",
"license": "Apache-2.0",
"files": [
"dist",
"lib",
"src",
"git-revision.txt",
"CHANGELOG.md",
"CONTRIBUTING.rst",
"LICENSE",
"README.md",
"package.json",
"release.sh"
],
"collectCoverageFrom": [
"<rootDir>/src/**/*.{js,ts}"
],
"coverageReporters": [
"text-summary",
"lcov"
],
"testResultsProcessor": "jest-sonar-reporter"
},
"jestSonar": {
"reportPath": "coverage",
"sonar56x": true
},
"typings": "./lib/index.d.ts"
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^1.2.3-alpha.0",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
"jwt-decode": "^3.1.2",
"loglevel": "^1.7.1",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.6.0",
"oidc-client-ts": "^2.2.4",
"p-retry": "4",
"sdp-transform": "^2.14.1",
"unhomoglyph": "^1.0.6",
"uuid": "9"
},
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.10",
"@babel/eslint-plugin": "^7.12.10",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.12.10",
"@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.14.tgz",
"@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5",
"@types/debug": "^4.1.7",
"@types/domexception": "^4.0.0",
"@types/jest": "^29.0.0",
"@types/node": "18",
"@types/sdp-transform": "^2.4.5",
"@types/uuid": "9",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"allchange": "^1.0.6",
"babel-jest": "^29.0.0",
"babelify": "^10.0.0",
"browserify": "^17.0.0",
"browserify-swap": "^0.2.2",
"debug": "^4.3.4",
"domexception": "^4.0.0",
"eslint": "8.48.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^27.1.6",
"eslint-plugin-jsdoc": "^46.0.0",
"eslint-plugin-matrix-org": "^1.0.0",
"eslint-plugin-tsdoc": "^0.2.17",
"eslint-plugin-unicorn": "^48.0.0",
"exorcist": "^2.0.0",
"fake-indexeddb": "^4.0.0",
"fetch-mock-jest": "^1.5.1",
"jest": "^29.0.0",
"jest-environment-jsdom": "^29.0.0",
"jest-localstorage-mock": "^2.4.6",
"jest-mock": "^29.0.0",
"matrix-mock-request": "^2.5.0",
"prettier": "2.8.8",
"rimraf": "^5.0.0",
"terser": "^5.5.1",
"ts-node": "^10.9.1",
"tsify": "^5.0.2",
"typedoc": "^0.24.0",
"typedoc-plugin-coverage": "^2.1.0",
"typedoc-plugin-mdn-links": "^3.0.3",
"typedoc-plugin-missing-exports": "^2.0.0",
"typedoc-plugin-versions": "^0.2.3",
"typedoc-plugin-versions-cli": "^0.1.12",
"typescript": "^5.0.0"
},
"@casualbot/jest-sonar-reporter": {
"outputDirectory": "coverage",
"outputName": "jest-sonar-report.xml",
"relativePaths": true
},
"browserify": {
"transform": [
"browserify-swap",
[
"babelify",
{
"sourceMaps": "inline",
"presets": [
"@babel/preset-env",
"@babel/preset-typescript"
]
}
]
]
},
"browserify-swap": {
"no-rust-crypto": {
"src/rust-crypto/index.ts$": "./src/rust-crypto/browserify-index.ts"
}
},
"typings": "./lib/index.d.ts"
}
+3 -3
View File
@@ -11,7 +11,7 @@ jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
# When merging to develop, we need revert the `main` and `typings` fields if we adjusted them previously.
for i in main typings
for i in main typings browser
do
# If a `lib` prefixed value is present, it means we adjusted the field
# earlier at publish time, so we should revert it now.
@@ -21,9 +21,9 @@ if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
# to the TypeScript source.
src_value=$(jq -r ".matrix_src_$i" package.json)
if [ "$src_value" != "null" ]; then
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
else
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
fi
fi
done
+4 -4
View File
@@ -130,7 +130,7 @@ fi
# global cache here to ensure we get the right thing.
yarn cache clean
# Ensure all dependencies are updated
yarn install --ignore-scripts --pure-lockfile
yarn install --ignore-scripts --frozen-lockfile
# ignore leading v on release
release="${1#v}"
@@ -180,11 +180,11 @@ yarn version --no-git-tag-version --new-version "$release"
# they exist). This small bit of gymnastics allows us to use the TypeScript
# source directly for development without needing to build before linting or
# testing.
for i in main typings
for i in main typings browser
do
lib_value=$(jq -r ".matrix_lib_$i" package.json)
if [ "$lib_value" != "null" ]; then
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
fi
done
@@ -225,7 +225,7 @@ if [ $dodist -eq 0 ]; then
pushd "$builddir"
git clone "$projdir" .
git checkout "$rel_branch"
yarn install --pure-lockfile
yarn install --frozen-lockfile
# We haven't tagged yet, so tell the dist script what version
# it's building
DIST_VERSION="$tag" yarn dist
+1 -1
View File
@@ -15,4 +15,4 @@ for line in sys.stdin:
break
found_first_header = True
elif not re.match(r"^=+$", line) and len(line) > 0:
print line
print(line)
+6 -6
View File
@@ -1,14 +1,14 @@
#!/usr/bin/env node
const fsProm = require('fs/promises');
const fsProm = require("fs/promises");
const PKGJSON = 'package.json';
const PKGJSON = "package.json";
async function main() {
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, 'utf8'));
for (const field of ['main', 'typings']) {
if (pkgJson["matrix_lib_"+field] !== undefined) {
pkgJson[field] = pkgJson["matrix_lib_"+field];
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, "utf8"));
for (const field of ["main", "typings"]) {
if (pkgJson["matrix_lib_" + field] !== undefined) {
pkgJson[field] = pkgJson["matrix_lib_" + field];
}
}
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
+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
+86 -62
View File
@@ -16,26 +16,33 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// `expect` is allowed in helper functions which are called within `test`/`it` blocks
/* eslint-disable jest/no-standalone-expect */
// load olm before the sdk if possible
import './olm-loader';
import "./olm-loader";
import MockHttpBackend from 'matrix-mock-request';
import MockHttpBackend from "matrix-mock-request";
import { LocalStorageCryptoStore } from '../src/crypto/store/localStorage-crypto-store';
import { logger } from '../src/logger';
import type { IDeviceKeys, IOneTimeKey } from "../src/@types/crypto";
import type { IE2EKeyReceiver } from "./test-utils/E2EKeyReceiver";
import { LocalStorageCryptoStore } from "../src/crypto/store/localStorage-crypto-store";
import { logger } from "../src/logger";
import { syncPromise } from "./test-utils/test-utils";
import { createClient } from "../src/matrix";
import { createClient, IStartClientOpts } from "../src/matrix";
import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client";
import { MockStorageApi } from "./MockStorageApi";
import { encodeUri } from "../src/utils";
import { IDeviceKeys, IOneTimeKey } from "../src/crypto/dehydration";
import { IKeyBackupSession } from "../src/crypto/keybackup";
import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client';
import { IKeysUploadResponse, IUploadKeysRequest } from "../src/client";
import { ISyncResponder } from "./test-utils/SyncResponder";
/**
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
*
* @deprecated Avoid using this; it is tied too tightly to matrix-mock-request and is generally inconvenient to use.
* Instead, construct a MatrixClient manually, use fetch-mock-jest to intercept the HTTP requests, and
* use things like {@link E2EKeyReceiver} and {@link SyncResponder} to manage the requests.
*/
export class TestClient {
export class TestClient implements IE2EKeyReceiver, ISyncResponder {
public readonly httpBackend: MockHttpBackend;
public readonly client: MatrixClient;
public deviceKeys?: IDeviceKeys | null;
@@ -73,15 +80,18 @@ export class TestClient {
}
public toString(): string {
return 'TestClient[' + this.userId + ']';
return "TestClient[" + this.userId + "]";
}
/**
* start the client, and wait for it to initialise.
*/
public start(): Promise<void> {
logger.log(this + ': starting');
this.httpBackend.when("GET", "/versions").respond(200, {});
public start(opts: IStartClientOpts = {}): Promise<void> {
logger.log(this + ": starting");
this.httpBackend.when("GET", "/versions").respond(200, {
// we have tests that rely on support for lazy-loading members
versions: ["v1.1"],
});
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.expectDeviceKeyUpload();
@@ -93,19 +103,18 @@ export class TestClient {
this.client.startClient({
// set this so that we can get hold of failed events
pendingEventOrdering: PendingEventOrdering.Detached,
...opts,
});
return Promise.all([
this.httpBackend.flushAllExpected(),
syncPromise(this.client),
]).then(() => {
logger.log(this + ': started');
return Promise.all([this.httpBackend.flushAllExpected(), syncPromise(this.client)]).then(() => {
logger.log(this + ": started");
});
}
/**
* stop the client
* @return {Promise} Resolves once the mock http backend has finished all pending flushes
* @returns Promise which resolves once the mock http backend has finished all pending flushes
*/
public async stop(): Promise<void> {
this.client.stopClient();
@@ -113,20 +122,30 @@ export class TestClient {
}
/**
* Set up expectations that the client will upload device keys.
* Set up expectations that the client will upload device keys (and possibly one-time keys)
*/
public expectDeviceKeyUpload() {
this.httpBackend.when("POST", "/keys/upload")
this.httpBackend
.when("POST", "/keys/upload")
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content) => {
expect(content.one_time_keys).toBe(undefined);
expect(content.device_keys).toBeTruthy();
logger.log(this + ': received device keys');
logger.log(this + ": received device keys");
// we expect this to happen before any one-time keys are uploaded.
expect(Object.keys(this.oneTimeKeys!).length).toEqual(0);
this.deviceKeys = content.device_keys;
return { one_time_key_counts: { signed_curve25519: 0 } };
// the first batch of one-time keys may be uploaded at the same time.
if (content.one_time_keys) {
logger.log(`${this}: received ${Object.keys(content.one_time_keys).length} one-time keys`);
this.oneTimeKeys = content.one_time_keys;
}
return {
one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
},
};
});
}
@@ -135,7 +154,7 @@ export class TestClient {
* set up an expectation that the keys will be uploaded, and wait for
* that to happen.
*
* @returns {Promise} for the one-time keys
* @returns Promise for the one-time keys
*/
public awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
if (Object.keys(this.oneTimeKeys!).length != 0) {
@@ -143,30 +162,35 @@ export class TestClient {
return Promise.resolve(this.oneTimeKeys!);
}
this.httpBackend.when("POST", "/keys/upload")
this.httpBackend
.when("POST", "/keys/upload")
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content: IUploadKeysRequest) => {
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBe(undefined);
return { one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
} };
return {
one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
},
};
});
this.httpBackend.when("POST", "/keys/upload")
this.httpBackend
.when("POST", "/keys/upload")
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content: IUploadKeysRequest) => {
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).not.toEqual({});
logger.log('%s: received %i one-time keys', this,
Object.keys(content.one_time_keys!).length);
logger.log("%s: received %i one-time keys", this, Object.keys(content.one_time_keys!).length);
this.oneTimeKeys = content.one_time_keys;
return { one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
} };
return {
one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
},
};
});
// this can take ages
return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => {
return this.httpBackend.flush("/keys/upload", 2, 1000).then((flushed) => {
expect(flushed).toEqual(2);
return this.oneTimeKeys!;
});
@@ -177,57 +201,57 @@ export class TestClient {
*
* We check that the query contains each of the users in `response`.
*
* @param {Object} response response to the query.
* @param response - response to the query.
*/
public expectKeyQuery(response: IDownloadKeyResult) {
this.httpBackend.when('POST', '/keys/query').respond<IDownloadKeyResult>(
200, (_path, content) => {
Object.keys(response.device_keys).forEach((userId) => {
expect(content.device_keys![userId]).toEqual([]);
});
return response;
this.httpBackend.when("POST", "/keys/query").respond<IDownloadKeyResult>(200, (_path, content) => {
Object.keys(response.device_keys).forEach((userId) => {
expect((content.device_keys! as Record<string, any>)[userId]).toEqual([]);
});
}
/**
* Set up expectations that the client will query key backups for a particular session
*/
public expectKeyBackupQuery(roomId: string, sessionId: string, status: number, response: IKeyBackupSession) {
this.httpBackend.when('GET', encodeUri("/room_keys/keys/$roomId/$sessionId", {
$roomId: roomId,
$sessionId: sessionId,
})).respond(status, response);
return response;
});
}
/**
* get the uploaded curve25519 device key
*
* @return {string} base64 device key
* @returns base64 device key
*/
public getDeviceKey(): string {
const keyId = 'curve25519:' + this.deviceId;
const keyId = "curve25519:" + this.deviceId;
return this.deviceKeys!.keys[keyId];
}
/**
* get the uploaded ed25519 device key
*
* @return {string} base64 device key
* @returns base64 device key
*/
public getSigningKey(): string {
const keyId = 'ed25519:' + this.deviceId;
const keyId = "ed25519:" + this.deviceId;
return this.deviceKeys!.keys[keyId];
}
/** Next time we see a sync request (or immediately, if there is one waiting), send the given response
*
* Calling this will register a response for `/sync`, and then, in the background, flush a single `/sync` request.
* Try calling {@link syncPromise} to wait for the sync to complete.
*
* @param response - response to /sync request
*/
public sendOrQueueSyncResponse(syncResponse: object): void {
this.httpBackend.when("GET", "/sync").respond(200, syncResponse);
this.httpBackend.flush("/sync", 1);
}
/**
* flush a single /sync request, and wait for the syncing event
*
* @deprecated: prefer to use {@link #sendOrQueueSyncResponse} followed by {@link syncPromise}.
*/
public flushSync(): Promise<void> {
logger.log(`${this}: flushSync`);
return Promise.all([
this.httpBackend.flush('/sync', 1),
syncPromise(this.client),
]).then(() => {
return Promise.all([this.httpBackend.flush("/sync", 1), syncPromise(this.client)]).then(() => {
logger.log(`${this}: flushSync completed`);
});
}
+9
View File
@@ -14,6 +14,9 @@ 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 { default as BrowserMatrix } from "../../src/browser-index";
// stub for browser-matrix browserify tests
// @ts-ignore
global.XMLHttpRequest = jest.fn();
@@ -23,3 +26,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,
} as typeof BrowserMatrix;
+12 -26
View File
@@ -16,28 +16,15 @@ 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() {
describe("Browserify Test", function () {
let client: MatrixClient;
let httpBackend: HttpBackend;
@@ -78,22 +65,21 @@ describe("Browserify Test", function() {
const syncData = {
next_batch: "batch1",
rooms: {
join: {},
},
};
syncData.rooms.join[ROOM_ID] = {
timeline: {
events: [
event,
],
limited: false,
join: {
[ROOM_ID]: {
timeline: {
events: [event],
limited: false,
},
},
},
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, syncData);
const syncPromise = new Promise(r => client.once(global.matrixcs.ClientEvent.Sync, r));
const syncPromise = new Promise((r) => client.once(global.matrixcs.ClientEvent.Sync, r));
const unexpectedErrorFn = jest.fn();
client.once(global.matrixcs.ClientEvent.SyncUnexpectedError, unexpectedErrorFn);
+342
View File
@@ -0,0 +1,342 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { AuthDict, createClient, CryptoEvent, MatrixClient } from "../../../src";
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints";
import { encryptAES } from "../../../src/crypto/aes";
import { CryptoCallbacks, CrossSigningKey } from "../../../src/crypto-api";
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import {
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64,
SIGNED_CROSS_SIGNING_KEYS_DATA,
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
} from "../../test-utils/test-data";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
// eslint-disable-next-line no-global-assign
indexedDB = new IDBFactory();
});
const TEST_USER_ID = "@alice:localhost";
const TEST_DEVICE_ID = "xzcvb";
/**
* Integration tests for cross-signing functionality.
*
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
* to provide the most effective integration tests possible.
*/
describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: string, initCrypto: InitCrypto) => {
// newBackendOnly is the opposite to `oldBackendOnly`: it will skip the test if we are running against the legacy
// backend. Once we drop support for legacy crypto, it will go away.
const newBackendOnly = backend === "rust-sdk" ? test : test.skip;
let aliceClient: MatrixClient;
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
let syncResponder: ISyncResponder;
/** an object which intercepts `/keys/query` requests on the test homeserver */
let e2eKeyResponder: E2EKeyResponder;
// Encryption key used to encrypt cross signing keys
const encryptionKey = new Uint8Array(32);
/**
* Create the {@link CryptoCallbacks}
*/
function createCryptoCallbacks(): CryptoCallbacks {
return {
getSecretStorageKey: (keys, name) => {
return Promise.resolve<[string, Uint8Array]>(["key_id", encryptionKey]);
},
};
}
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
const homeserverUrl = "https://alice-server.com";
aliceClient = createClient({
baseUrl: homeserverUrl,
userId: TEST_USER_ID,
accessToken: "akjgkrgjs",
deviceId: TEST_DEVICE_ID,
cryptoCallbacks: createCryptoCallbacks(),
});
syncResponder = new SyncResponder(homeserverUrl);
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
/** an object which intercepts `/keys/upload` requests on the test homeserver */
new E2EKeyReceiver(homeserverUrl);
await initCrypto(aliceClient);
});
afterEach(async () => {
await aliceClient.stopClient();
fetchMock.mockReset();
});
/**
* Create cross-signing keys and publish the keys
*
* @param authDict - The parameters to as the `auth` dict in the key upload request.
* @see https://spec.matrix.org/v1.6/client-server-api/#authentication-types
*/
async function bootstrapCrossSigning(authDict: AuthDict): Promise<void> {
await aliceClient.getCrypto()?.bootstrapCrossSigning({
authUploadDeviceSigningKeys: (makeRequest) => makeRequest(authDict).then(() => undefined),
});
}
describe("bootstrapCrossSigning (before initialsync completes)", () => {
it("publishes keys if none were yet published", async () => {
mockSetupCrossSigningRequests();
// provide a UIA callback, so that the cross-signing keys are uploaded
const authDict = { type: "test" };
await bootstrapCrossSigning(authDict);
// check the cross-signing keys upload
expect(fetchMock.called("upload-keys")).toBeTruthy();
const [, keysOpts] = fetchMock.lastCall("upload-keys")!;
const keysBody = JSON.parse(keysOpts!.body as string);
expect(keysBody.auth).toEqual(authDict); // check uia dict was passed
// there should be a key of each type
// master key is signed by the device
expect(keysBody).toHaveProperty(`master_key.signatures.[${TEST_USER_ID}].[ed25519:${TEST_DEVICE_ID}]`);
const masterKeyId = Object.keys(keysBody.master_key.keys)[0];
// ssk and usk are signed by the master key
expect(keysBody).toHaveProperty(`self_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
expect(keysBody).toHaveProperty(`user_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
const sskId = Object.keys(keysBody.self_signing_key.keys)[0];
// check the publish call
expect(fetchMock.called("upload-sigs")).toBeTruthy();
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
const body = JSON.parse(sigsOpts!.body as string);
// there should be a signature for our device, by our self-signing key.
expect(body).toHaveProperty(
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[${sskId}]`,
);
});
newBackendOnly("get cross signing keys from secret storage and import them", async () => {
// Return public cross signing keys
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
mockInitialApiRequests(aliceClient.getHomeserverUrl());
// Encrypt the private keys and return them in the /sync response as if they are in Secret Storage
const masterKey = await encryptAES(
MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.master",
);
const selfSigningKey = await encryptAES(
SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.self_signing",
);
const userSigningKey = await encryptAES(
USER_CROSS_SIGNING_PRIVATE_KEY_BASE64,
encryptionKey,
"m.cross_signing.user_signing",
);
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
account_data: {
events: [
{
type: "m.cross_signing.master",
content: {
encrypted: {
key_id: masterKey,
},
},
},
{
type: "m.cross_signing.self_signing",
content: {
encrypted: {
key_id: selfSigningKey,
},
},
},
{
type: "m.cross_signing.user_signing",
content: {
encrypted: {
key_id: userSigningKey,
},
},
},
{
type: "m.secret_storage.key.key_id",
content: {
key: "key_id",
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
},
},
],
},
});
await aliceClient.startClient();
await syncPromise(aliceClient);
// we expect a request to upload signatures for our device ...
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
// we expect the UserTrustStatusChanged event to be fired after the cross signing keys import
const userTrustStatusChangedPromise = new Promise<string>((resolve) =>
aliceClient.on(CryptoEvent.UserTrustStatusChanged, resolve),
);
const authDict = { type: "test" };
await bootstrapCrossSigning(authDict);
// Check if the UserTrustStatusChanged event was fired
expect(await userTrustStatusChangedPromise).toBe(aliceClient.getUserId());
// Expect the signature to be uploaded
expect(fetchMock.called("upload-sigs")).toBeTruthy();
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
const body = JSON.parse(sigsOpts!.body as string);
// the device should have a signature with the public self cross signing keys.
expect(body).toHaveProperty(
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[ed25519:${SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64}]`,
);
});
});
describe("getCrossSigningStatus()", () => {
it("should return correct values without bootstrapping cross-signing", async () => {
mockSetupCrossSigningRequests();
const crossSigningStatus = await aliceClient.getCrypto()!.getCrossSigningStatus();
// Expect the cross signing keys to be unavailable
expect(crossSigningStatus).toStrictEqual({
publicKeysOnDevice: false,
privateKeysInSecretStorage: false,
privateKeysCachedLocally: { masterKey: false, userSigningKey: false, selfSigningKey: false },
});
});
it("should return correct values after bootstrapping cross-signing", async () => {
mockSetupCrossSigningRequests();
// provide a UIA callback, so that the cross-signing keys are uploaded
const authDict = { type: "test" };
await bootstrapCrossSigning(authDict);
const crossSigningStatus = await aliceClient.getCrypto()!.getCrossSigningStatus();
// Expect the cross signing keys to be available
expect(crossSigningStatus).toStrictEqual({
publicKeysOnDevice: true,
privateKeysInSecretStorage: false,
privateKeysCachedLocally: { masterKey: true, userSigningKey: true, selfSigningKey: true },
});
});
});
describe("isCrossSigningReady()", () => {
it("should return false if cross-signing is not bootstrapped", async () => {
mockSetupCrossSigningRequests();
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
expect(isCrossSigningReady).toBeFalsy();
});
it("should return true after bootstrapping cross-signing", async () => {
mockSetupCrossSigningRequests();
await bootstrapCrossSigning({ type: "test" });
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
expect(isCrossSigningReady).toBeTruthy();
});
});
describe("getCrossSigningKeyId", () => {
/**
* Intercept /keys/device_signing/upload request and return the cross signing keys
* https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3keysdevice_signingupload
*
* @returns the cross signing keys
*/
function awaitCrossSigningKeysUpload() {
return new Promise<any>((resolve) => {
fetchMock.post(
// legacy crypto uses /unstable/; /v3/ is correct
{
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
name: "upload-keys",
},
(url, options) => {
const content = JSON.parse(options.body as string);
resolve(content);
return {};
},
// Override the routes define in `mockSetupCrossSigningRequests`
{ overwriteRoutes: true },
);
});
}
it("should return the cross signing key id for each cross signing key", async () => {
mockSetupCrossSigningRequests();
// Intercept cross signing keys upload
const crossSigningKeysPromise = awaitCrossSigningKeysUpload();
// provide a UIA callback, so that the cross-signing keys are uploaded
const authDict = { type: "test" };
await bootstrapCrossSigning(authDict);
// Get the cross signing keys
const crossSigningKeys = await crossSigningKeysPromise;
const getPubKey = (crossSigningKey: any) => Object.values(crossSigningKey!.keys)[0];
const masterKeyId = await aliceClient.getCrypto()!.getCrossSigningKeyId();
expect(masterKeyId).toBe(getPubKey(crossSigningKeys.master_key));
const selfSigningKeyId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.SelfSigning);
expect(selfSigningKeyId).toBe(getPubKey(crossSigningKeys.self_signing_key));
const userSigningKeyId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.UserSigning);
expect(userSigningKeyId).toBe(getPubKey(crossSigningKeys.user_signing_key));
});
});
});
File diff suppressed because it is too large Load Diff
+799
View File
@@ -0,0 +1,799 @@
/*
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 fetchMock from "fetch-mock-jest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { createClient, CryptoEvent, ICreateClientOpts, MatrixClient, TypedEventEmitter } from "../../../src";
import { SyncResponder } from "../../test-utils/SyncResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
import { awaitDecryption, CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import * as testData from "../../test-utils/test-data";
import { KeyBackupInfo } from "../../../src/crypto-api/keybackup";
import { IKeyBackup } from "../../../src/crypto/backup";
const ROOM_ID = testData.TEST_ROOM_ID;
/** The homeserver url that we give to the test client, and where we intercept /sync, /keys, etc requests. */
const TEST_HOMESERVER_URL = "https://alice-server.com";
const TEST_USER_ID = "@alice:localhost";
const TEST_DEVICE_ID = "xzcvb";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
// eslint-disable-next-line no-global-assign
indexedDB = new IDBFactory();
});
enum MockKeyUploadEvent {
KeyUploaded = "KeyUploaded",
}
type MockKeyUploadEventHandlerMap = {
[MockKeyUploadEvent.KeyUploaded]: (roomId: string, sessionId: string, backupVersion: string) => void;
};
/*
* Test helper. Returns an event emitter that will emit an event every time fetchmock sees a request to backup a key.
*/
function mockUploadEmitter(
expectedVersion: string,
): TypedEventEmitter<MockKeyUploadEvent, MockKeyUploadEventHandlerMap> {
const emitter = new TypedEventEmitter();
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
(url, request) => {
const version = new URLSearchParams(new URL(url).search).get("version");
if (version != expectedVersion) {
return {
status: 403,
body: {
current_version: expectedVersion,
errcode: "M_WRONG_ROOM_KEYS_VERSION",
error: "Wrong backup version.",
},
};
}
const uploadPayload: IKeyBackup = JSON.parse(request.body?.toString() ?? "{}");
let count = 0;
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
for (const sessionId of Object.keys(value.sessions)) {
emitter.emit(MockKeyUploadEvent.KeyUploaded, roomId, sessionId, version);
count++;
}
}
return {
status: 200,
body: {
count: count,
etag: "abcdefg",
},
};
},
{
overwriteRoutes: true,
},
);
return emitter;
}
describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backend: string, initCrypto: InitCrypto) => {
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
// Rust backend. Once we have full support in the rust sdk, it will go away.
// const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
// const newBackendOnly = backend === "libolm" ? test.skip : test;
let aliceClient: MatrixClient;
/** an object which intercepts `/sync` requests on the test homeserver */
let syncResponder: SyncResponder;
/** an object which intercepts `/keys/upload` requests on the test homeserver */
let e2eKeyReceiver: E2EKeyReceiver;
/** an object which intercepts `/keys/query` requests on the test homeserver */
let e2eKeyResponder: E2EKeyResponder;
jest.useFakeTimers();
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
mockInitialApiRequests(TEST_HOMESERVER_URL);
syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL);
e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL);
e2eKeyResponder.addDeviceKeys(testData.SIGNED_TEST_DEVICE_DATA);
e2eKeyResponder.addKeyReceiver(TEST_USER_ID, e2eKeyReceiver);
});
afterEach(async () => {
if (aliceClient !== undefined) {
await aliceClient.stopClient();
}
// Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync();
fetchMock.mockReset();
});
async function initTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
const client = createClient({
baseUrl: TEST_HOMESERVER_URL,
userId: TEST_USER_ID,
accessToken: "akjgkrgjs",
deviceId: TEST_DEVICE_ID,
...opts,
});
await initCrypto(client);
return client;
}
it("Alice checks key backups when receiving a message she can't decrypt", async function () {
const syncResponse = {
next_batch: 1,
rooms: {
join: {
[ROOM_ID]: {
timeline: {
events: [testData.ENCRYPTED_EVENT],
},
},
},
},
};
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
testData.CURVE25519_KEY_BACKUP_DATA,
);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceCrypto.storeSessionBackupPrivateKey(Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"));
// start after saving the private key
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup, and re-check the backup.
// XXX: should we automatically re-check after a device becomes verified?
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
// Now, send Alice a message that she won't be able to decrypt, and check that she fetches the key from the backup.
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
const room = aliceClient.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
await awaitDecryption(event, { waitOnDecryptionFailure: true });
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
});
describe("recover from backup", () => {
it("can restore from backup (Curve25519 version)", async function () {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
const fullBackup = {
rooms: {
[ROOM_ID]: {
sessions: {
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
},
},
},
};
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
const check = await aliceCrypto.checkKeyBackupAndEnable();
let onKeyCached: () => void;
const awaitKeyCached = new Promise<void>((resolve) => {
onKeyCached = resolve;
});
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
cacheCompleteCallback: () => onKeyCached(),
},
);
expect(result.imported).toStrictEqual(1);
await awaitKeyCached;
});
it("recover specific session from backup", async function () {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
testData.CURVE25519_KEY_BACKUP_DATA,
);
const check = await aliceCrypto.checkKeyBackupAndEnable();
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
ROOM_ID,
testData.MEGOLM_SESSION_DATA.session_id,
check!.backupInfo!,
);
expect(result.imported).toStrictEqual(1);
});
it("Fails on bad recovery key", async function () {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
const fullBackup = {
rooms: {
[ROOM_ID]: {
sessions: {
[testData.MEGOLM_SESSION_DATA.session_id]: testData.CURVE25519_KEY_BACKUP_DATA,
},
},
},
};
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", fullBackup);
const check = await aliceCrypto.checkKeyBackupAndEnable();
await expect(
aliceClient.restoreKeyBackupWithRecoveryKey(
"EsTx A7Xn aNFF k3jH zpV3 MQoN LJEg mscC HecF 982L wC77 mYQD",
undefined,
undefined,
check!.backupInfo!,
),
).rejects.toThrow();
});
});
describe("backupLoop", () => {
it("Alice should upload known keys when backup is enabled", async function () {
// 404 means that there is no active backup
fetchMock.get("path:/_matrix/client/v3/room_keys/version", 404);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
// check that signalling is working
const remainingZeroPromise = new Promise<void>((resolve, reject) => {
aliceClient.on(CryptoEvent.KeyBackupSessionsRemaining, (remaining) => {
if (remaining == 0) {
resolve();
}
});
});
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
const uploadMockEmitter = mockUploadEmitter(testData.SIGNED_BACKUP_DATA.version!);
const uploadPromises = someRoomKeys.map((data) => {
new Promise<void>((resolve) => {
uploadMockEmitter.on(MockKeyUploadEvent.KeyUploaded, (roomId, sessionId, version) => {
if (
data.room_id == roomId &&
data.session_id == sessionId &&
version == testData.SIGNED_BACKUP_DATA.version
) {
resolve();
}
});
});
});
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
});
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
await aliceCrypto.importRoomKeys(someRoomKeys);
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
jest.runAllTimers();
await Promise.all(uploadPromises);
// Wait until all keys are backed up to ensure that when a new key is received the loop is restarted
await remainingZeroPromise;
// A new key import should trigger a new upload.
const newKey = testData.MEGOLM_SESSION_DATA;
const newKeyUploadPromise = new Promise<void>((resolve) => {
uploadMockEmitter.on(MockKeyUploadEvent.KeyUploaded, (roomId, sessionId, version) => {
if (
newKey.room_id == roomId &&
newKey.session_id == sessionId &&
version == testData.SIGNED_BACKUP_DATA.version
) {
resolve();
}
});
});
await aliceCrypto.importRoomKeys([newKey]);
jest.runAllTimers();
await newKeyUploadPromise;
});
it("Alice should re-upload all keys if a new trusted backup is available", async function () {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
// check that signalling is working
const remainingZeroPromise = new Promise<void>((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupSessionsRemaining, (remaining) => {
if (remaining == 0) {
resolve();
}
});
});
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
});
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
mockUploadEmitter(testData.SIGNED_BACKUP_DATA.version!);
await aliceCrypto.importRoomKeys(someRoomKeys);
// The backup loop is waiting a random amount of time to avoid different clients firing at the same time.
jest.runAllTimers();
// wait for all keys to be backed up
await remainingZeroPromise;
const newBackupVersion = "2";
const uploadMockEmitter = mockUploadEmitter(newBackupVersion);
const newBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
newBackup.version = newBackupVersion;
// Let's simulate that a new backup is available by returning error code on key upload
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
overwriteRoutes: true,
});
// If we import a new key the loop will try to upload to old version, it will
// fail then check the current version and switch if trusted
const uploadPromises = someRoomKeys.map((data) => {
new Promise<void>((resolve) => {
uploadMockEmitter.on(MockKeyUploadEvent.KeyUploaded, (roomId, sessionId, version) => {
if (data.room_id == roomId && data.session_id == sessionId && version == newBackupVersion) {
resolve();
}
});
});
});
const disableOldBackup = new Promise<void>((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupFailed, (errCode) => {
if (errCode == "M_WRONG_ROOM_KEYS_VERSION") {
resolve();
}
});
});
const enableNewBackup = new Promise<void>((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
if (enabled) {
resolve();
}
});
});
// A new key import should trigger a new upload.
const newKey = testData.MEGOLM_SESSION_DATA;
const newKeyUploadPromise = new Promise<void>((resolve) => {
uploadMockEmitter.on(MockKeyUploadEvent.KeyUploaded, (roomId, sessionId, version) => {
if (newKey.room_id == roomId && newKey.session_id == sessionId && version == newBackupVersion) {
resolve();
}
});
});
await aliceCrypto.importRoomKeys([newKey]);
jest.runAllTimers();
await disableOldBackup;
await enableNewBackup;
jest.runAllTimers();
await Promise.all(uploadPromises);
await newKeyUploadPromise;
});
it("Backup loop should be resistant to network failures", async function () {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
});
// on the first key upload attempt, simulate a network failure
const failurePromise = new Promise((resolve) => {
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
() => {
resolve(undefined);
throw new TypeError(`Failed to fetch`);
},
{
overwriteRoutes: true,
},
);
});
// kick the import loop off and wait for the failed request
const someRoomKeys = testData.MEGOLM_SESSION_DATA_ARRAY;
await aliceCrypto.importRoomKeys(someRoomKeys);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
jest.runAllTimers();
await failurePromise;
// Fix the endpoint to do successful uploads
const successPromise = new Promise((resolve) => {
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
() => {
resolve(undefined);
return {
status: 200,
body: {
count: 2,
etag: "abcdefg",
},
};
},
{
overwriteRoutes: true,
},
);
});
// check that a `KeyBackupSessionsRemaining` event is emitted with `remaining == 0`
const allKeysUploadedPromise = new Promise((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupSessionsRemaining, (remaining) => {
if (remaining == 0) {
resolve(undefined);
}
});
});
// run the timers, which will make the backup loop redo the request
await jest.runAllTimersAsync();
await successPromise;
await allKeysUploadedPromise;
});
});
it("getActiveSessionBackupVersion() should give correct result", async function () {
// 404 means that there is no active backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", 404);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
// tell Alice to trust the dummy device that signed the backup
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
await aliceCrypto.checkKeyBackupAndEnable();
// At this point there is no backup
let backupStatus: string | null;
backupStatus = await aliceCrypto.getActiveSessionBackupVersion();
expect(backupStatus).toBeNull();
// Serve a backup with no trusted signature
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
delete unsignedBackup.auth_data.signatures;
fetchMock.get("express:/_matrix/client/v3/room_keys/version", unsignedBackup, {
overwriteRoutes: true,
});
const checked = await aliceCrypto.checkKeyBackupAndEnable();
expect(checked?.backupInfo?.version).toStrictEqual(unsignedBackup.version);
expect(checked?.trustInfo?.trusted).toBeFalsy();
backupStatus = await aliceCrypto.getActiveSessionBackupVersion();
expect(backupStatus).toBeNull();
// Add a valid signature to the backup
fetchMock.get("express:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA, {
overwriteRoutes: true,
});
// check that signalling is working
const backupPromise = new Promise<void>((resolve, reject) => {
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
if (enabled) {
resolve();
}
});
});
const validCheck = await aliceCrypto.checkKeyBackupAndEnable();
expect(validCheck?.trustInfo?.trusted).toStrictEqual(true);
await backupPromise;
backupStatus = await aliceCrypto.getActiveSessionBackupVersion();
expect(backupStatus).toStrictEqual(testData.SIGNED_BACKUP_DATA.version);
});
describe("isKeyBackupTrusted", () => {
it("does not trust a backup signed by an untrusted device", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// download the device list, to match the trusted case
await aliceClient.startClient();
await waitForDeviceList();
const result = await aliceCrypto.isKeyBackupTrusted(testData.SIGNED_BACKUP_DATA);
expect(result).toEqual({ trusted: false, matchesDecryptionKey: false });
});
it("trusts a backup signed by a trusted device", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// tell Alice to trust the dummy device that signed the backup
await aliceClient.startClient();
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
const result = await aliceCrypto.isKeyBackupTrusted(testData.SIGNED_BACKUP_DATA);
expect(result).toEqual({ trusted: true, matchesDecryptionKey: false });
});
it("recognises a backup which matches the decryption key", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
await aliceCrypto.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
);
const result = await aliceCrypto.isKeyBackupTrusted(testData.SIGNED_BACKUP_DATA);
expect(result).toEqual({ trusted: false, matchesDecryptionKey: true });
});
it("is not fooled by a backup which matches the decryption key but uses a different algorithm", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceClient.startClient();
await aliceCrypto.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
);
const backup: KeyBackupInfo = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
backup.algorithm = "m.megolm_backup.v1.aes-hmac-sha2";
const result = await aliceCrypto.isKeyBackupTrusted(backup);
expect(result).toEqual({ trusted: false, matchesDecryptionKey: false });
});
});
describe("checkKeyBackupAndEnable", () => {
it("enables a backup signed by a trusted device", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// tell Alice to trust the dummy device that signed the backup
await aliceClient.startClient();
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
expect(result!.trustInfo).toEqual({ trusted: true, matchesDecryptionKey: false });
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
});
it("does not enable a backup signed by an untrusted device", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// download the device list, to match the trusted case
await aliceClient.startClient();
await waitForDeviceList();
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
expect(result!.trustInfo).toEqual({ trusted: false, matchesDecryptionKey: false });
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
});
it("disables backup when a new untrusted backup is available", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// tell Alice to trust the dummy device that signed the backup
await aliceClient.startClient();
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
const unsignedBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
delete unsignedBackup.auth_data.signatures;
unsignedBackup.version = "2";
fetchMock.get("path:/_matrix/client/v3/room_keys/version", unsignedBackup, {
overwriteRoutes: true,
});
await aliceCrypto.checkKeyBackupAndEnable();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
});
it("switches backup when a new trusted backup is available", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// tell Alice to trust the dummy device that signed the backup
await aliceClient.startClient();
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
const newBackupVersion = "2";
const newBackup = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
newBackup.version = newBackupVersion;
fetchMock.get("path:/_matrix/client/v3/room_keys/version", newBackup, {
overwriteRoutes: true,
});
await aliceCrypto.checkKeyBackupAndEnable();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(newBackupVersion);
});
it("Disables when backup is deleted", async () => {
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
// tell Alice to trust the dummy device that signed the backup
await aliceClient.startClient();
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toEqual(testData.SIGNED_BACKUP_DATA.version);
fetchMock.get(
"path:/_matrix/client/v3/room_keys/version",
{
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No backup found",
},
},
{
overwriteRoutes: true,
},
);
const noResult = await aliceCrypto.checkKeyBackupAndEnable();
expect(noResult).toBeNull();
expect(await aliceCrypto.getActiveSessionBackupVersion()).toBeNull();
});
});
/** make sure that the client knows about the dummy device */
async function waitForDeviceList(): Promise<void> {
// Completing the initial sync will make the device list download outdated device lists (of which our own
// user will be one).
syncResponder.sendOrQueueSyncResponse({});
// DeviceList has a sleep(5) which we need to make happen
await jest.advanceTimersByTimeAsync(10);
// The client should now know about the dummy device
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
expect(devices.get(TEST_USER_ID)!.keys()).toContain(TEST_DEVICE_ID);
}
});
@@ -22,18 +22,20 @@ limitations under the License.
*
* Note that megolm (group) conversation is not tested here.
*
* See also `megolm.spec.js`.
* See also `crypto.spec.js`.
*/
// load olm before the sdk if possible
import '../olm-loader';
import "../../olm-loader";
import { logger } from '../../src/logger';
import * as testUtils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { CRYPTO_ENABLED, IUploadKeysRequest } from "../../src/client";
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
import { DeviceInfo } from '../../src/crypto/deviceinfo';
import type { Session } from "@matrix-org/olm";
import type { IDeviceKeys, IOneTimeKey } from "../../../src/@types/crypto";
import { logger } from "../../../src/logger";
import * as testUtils from "../../test-utils/test-utils";
import { TestClient } from "../../TestClient";
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../../src/client";
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../../src/matrix";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
let aliTestClient: TestClient;
const roomId = "!room:localhost";
@@ -47,39 +49,31 @@ const bobAccessToken = "fewgfkuesa";
let aliMessages: IContent[];
let bobMessages: IContent[];
// IMessage isn't exported by src/crypto/algorithms/olm.ts
interface OlmPayload {
type: number;
body: string;
}
type OlmPayload = ReturnType<Session["encrypt"]>;
async function bobUploadsDeviceKeys(): Promise<void> {
bobTestClient.expectDeviceKeyUpload();
await Promise.all([
bobTestClient.client.uploadKeys(),
bobTestClient.httpBackend.flushAllExpected(),
]);
await bobTestClient.httpBackend.flushAllExpected();
expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0);
}
/**
* Set an expectation that querier will query uploader's keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
* @returns resolves once the http request has completed.
*/
function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise<number> {
// can't query keys before bob has uploaded them
expect(uploader.deviceKeys).toBeTruthy();
const uploaderKeys = {};
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys;
querier.httpBackend.when("POST", "/keys/query")
.respond(200, function(_path, content: IUploadKeysRequest) {
expect(content.device_keys![uploader.userId!]).toEqual([]);
const result = {};
result[uploader.userId!] = uploaderKeys;
return { device_keys: result };
});
const uploaderKeys: Record<string, IDeviceKeys> = {};
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys!;
querier.httpBackend.when("POST", "/keys/query").respond(200, function (_path, content: IQueryKeysRequest) {
expect(content.device_keys![uploader.userId!]).toEqual([]);
const result: Record<string, Record<string, IDeviceKeys>> = {};
result[uploader.userId!] = uploaderKeys;
return { device_keys: result };
});
return querier.httpBackend.flush("/keys/query", 1);
}
const expectAliQueryKeys = () => expectQueryKeys(aliTestClient, bobTestClient);
@@ -88,16 +82,14 @@ const expectBobQueryKeys = () => expectQueryKeys(bobTestClient, aliTestClient);
/**
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
* @returns resolves once the http request has completed.
*/
async function expectAliClaimKeys(): Promise<void> {
const keys = await bobTestClient.awaitOneTimeKeyUpload();
aliTestClient.httpBackend.when(
"POST", "/keys/claim",
).respond(200, function(_path, content: IUploadKeysRequest) {
aliTestClient.httpBackend.when("POST", "/keys/claim").respond(200, function (_path, content: IClaimKeysRequest) {
const claimType = content.one_time_keys![bobUserId][bobDeviceId];
expect(claimType).toEqual("signed_curve25519");
let keyId = '';
let keyId = "";
for (keyId in keys) {
if (bobTestClient.oneTimeKeys!.hasOwnProperty(keyId)) {
if (keyId.indexOf(claimType + ":") === 0) {
@@ -105,7 +97,7 @@ async function expectAliClaimKeys(): Promise<void> {
}
}
}
const result = {};
const result: Record<string, Record<string, Record<string, IOneTimeKey>>> = {};
result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
@@ -138,8 +130,7 @@ async function aliDownloadsKeys(): Promise<void> {
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const devices = data!.devices[bobUserId]!;
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys!.keys);
expect(devices[bobDeviceId].verified).
toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
expect(devices[bobDeviceId].verified).toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
});
}
@@ -156,15 +147,13 @@ const bobEnablesEncryption = () => clientEnablesEncryption(bobTestClient.client)
* Ali sends a message, first claiming e2e keys. Set the expectations and
* check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
* @returns which resolves to the ciphertext for Bob's device.
*/
async function aliSendsFirstMessage(): Promise<OlmPayload> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, ciphertext] = await Promise.all([
sendMessage(aliTestClient.client),
expectAliQueryKeys()
.then(expectAliClaimKeys)
.then(expectAliSendMessageRequest),
expectAliQueryKeys().then(expectAliClaimKeys).then(expectAliSendMessageRequest),
]);
return ciphertext;
}
@@ -173,14 +162,11 @@ async function aliSendsFirstMessage(): Promise<OlmPayload> {
* Ali sends a message without first claiming e2e keys. Set the expectations
* and check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
* @returns which resolves to the ciphertext for Bob's device.
*/
async function aliSendsMessage(): Promise<OlmPayload> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, ciphertext] = await Promise.all([
sendMessage(aliTestClient.client),
expectAliSendMessageRequest(),
]);
const [_, ciphertext] = await Promise.all([sendMessage(aliTestClient.client), expectAliSendMessageRequest()]);
return ciphertext;
}
@@ -188,14 +174,13 @@ async function aliSendsMessage(): Promise<OlmPayload> {
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
* expectations and check the results.
*
* @return {promise} which resolves to the ciphertext for Ali's device.
* @returns which resolves to the ciphertext for Ali's device.
*/
async function bobSendsReplyMessage(): Promise<OlmPayload> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, ciphertext] = await Promise.all([
sendMessage(bobTestClient.client),
expectBobQueryKeys()
.then(expectBobSendMessageRequest),
expectBobQueryKeys().then(expectBobSendMessageRequest),
]);
return ciphertext;
}
@@ -203,7 +188,7 @@ async function bobSendsReplyMessage(): Promise<OlmPayload> {
/**
* Set an expectation that Ali will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
* @returns which resolves to the ciphertext for Bob's device.
*/
async function expectAliSendMessageRequest(): Promise<OlmPayload> {
const content = await expectSendMessageRequest(aliTestClient.httpBackend);
@@ -217,7 +202,7 @@ async function expectAliSendMessageRequest(): Promise<OlmPayload> {
/**
* Set an expectation that Bob will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
* @returns which resolves to the ciphertext for Bob's device.
*/
async function expectBobSendMessageRequest(): Promise<OlmPayload> {
const content = await expectSendMessageRequest(bobTestClient.httpBackend);
@@ -231,15 +216,13 @@ async function expectBobSendMessageRequest(): Promise<OlmPayload> {
}
function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
return client.sendMessage(
roomId, { msgtype: "m.text", body: "Hello, World" },
);
return client.sendMessage(roomId, { msgtype: "m.text", body: "Hello, World" });
}
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
const path = "/send/m.room.encrypted/";
const prom = new Promise<IContent>((resolve) => {
httpBackend.when("PUT", path).respond(200, function(_path, content) {
httpBackend.when("PUT", path).respond(200, function (_path, content) {
resolve(content);
return {
event_id: "asdfgh",
@@ -254,16 +237,12 @@ async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]):
function aliRecvMessage(): Promise<void> {
const message = bobMessages.shift()!;
return recvMessage(
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
);
return recvMessage(aliTestClient.httpBackend, aliTestClient.client, bobUserId, message);
}
function bobRecvMessage(): Promise<void> {
const message = aliMessages.shift()!;
return recvMessage(
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
);
return recvMessage(bobTestClient.httpBackend, bobTestClient.client, aliUserId, message);
}
async function recvMessage(
@@ -276,32 +255,30 @@ async function recvMessage(
next_batch: "x",
rooms: {
join: {
[roomId]: {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: sender,
}),
],
},
},
},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: sender,
}),
],
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
const eventPromise = new Promise<MatrixEvent>((resolve) => {
const onEvent = function(event: MatrixEvent) {
const onEvent = function (event: MatrixEvent) {
// ignore the m.room.member events
if (event.getType() == "m.room.member") {
return;
}
logger.log(client.credentials.userId + " received event",
event);
logger.log(client.credentials.userId + " received event", event);
client.removeListener(ClientEvent.Event, onEvent);
resolve(event);
@@ -327,32 +304,32 @@ async function recvMessage(
* Send an initial sync response to the client (which just includes the member
* list for our test room).
*
* @param {TestClient} testClient
* @returns {Promise} which resolves when the sync has been flushed.
* @returns which resolves when the sync has been flushed.
*/
function firstSync(testClient: TestClient): Promise<void> {
// send a sync response including our test room.
const syncData = {
next_batch: "x",
rooms: {
join: { },
},
};
syncData.rooms.join[roomId] = {
state: {
events: [
testUtils.mkMembership({
mship: "join",
user: aliUserId,
}),
testUtils.mkMembership({
mship: "join",
user: bobUserId,
}),
],
},
timeline: {
events: [],
join: {
[roomId]: {
state: {
events: [
testUtils.mkMembership({
mship: "join",
user: aliUserId,
}),
testUtils.mkMembership({
mship: "join",
user: bobUserId,
}),
],
},
timeline: {
events: [],
},
},
},
},
};
@@ -385,6 +362,13 @@ describe("MatrixClient crypto", () => {
it("Bob uploads device keys", bobUploadsDeviceKeys);
it("handles failures to upload device keys", async () => {
// since device keys are uploaded asynchronously, there's not really much to do here other than fail the
// upload.
bobTestClient.httpBackend.when("POST", "/keys/upload").fail(0, new Error("bleh"));
await bobTestClient.httpBackend.flushAllExpected();
});
it("Ali downloads Bobs device keys", async () => {
await bobUploadsDeviceKeys();
await aliDownloadsKeys();
@@ -396,10 +380,7 @@ describe("MatrixClient crypto", () => {
const bobDeviceKeys = bobTestClient.deviceKeys!;
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
await Promise.all([
aliTestClient.client.downloadKeys([bobUserId]),
expectAliQueryKeys(),
]);
await Promise.all([aliTestClient.client.downloadKeys([bobUserId]), expectAliQueryKeys()]);
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
// should get an empty list
expect(devices).toEqual([]);
@@ -409,26 +390,24 @@ describe("MatrixClient crypto", () => {
const eveUserId = "@eve:localhost";
const bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'bvcxz',
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "bvcxz",
keys: {
'ed25519:bvcxz': 'pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q',
'curve25519:bvcxz': '7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ',
"ed25519:bvcxz": "pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q",
"curve25519:bvcxz": "7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ",
},
user_id: '@eve:localhost',
user_id: "@eve:localhost",
signatures: {
'@eve:localhost': {
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
"@eve:localhost": {
"ed25519:bvcxz":
"CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG" + "0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg",
},
},
};
const bobKeys = {};
const bobKeys: Record<string, typeof bobDeviceKeys> = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when(
"POST", "/keys/query",
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
aliTestClient.httpBackend.when("POST", "/keys/query").respond(200, { device_keys: { [bobUserId]: bobKeys } });
await Promise.all([
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
@@ -445,26 +424,24 @@ describe("MatrixClient crypto", () => {
it("Ali gets keys with an incorrect deviceId", async () => {
const bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'bad_device',
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "bad_device",
keys: {
'ed25519:bad_device': 'e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0',
'curve25519:bad_device': 'YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc',
"ed25519:bad_device": "e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0",
"curve25519:bad_device": "YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc",
},
user_id: '@bob:localhost',
user_id: "@bob:localhost",
signatures: {
'@bob:localhost': {
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
"@bob:localhost": {
"ed25519:bad_device":
"fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A" + "me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ",
},
},
};
const bobKeys = {};
const bobKeys: Record<string, typeof bobDeviceKeys> = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliTestClient.httpBackend.when(
"POST", "/keys/query",
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
aliTestClient.httpBackend.when("POST", "/keys/query").respond(200, { device_keys: { [bobUserId]: bobKeys } });
await Promise.all([
aliTestClient.client.downloadKeys([bobUserId]),
@@ -495,7 +472,7 @@ describe("MatrixClient crypto", () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
@@ -506,7 +483,7 @@ describe("MatrixClient crypto", () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
@@ -515,26 +492,25 @@ describe("MatrixClient crypto", () => {
next_batch: "x",
rooms: {
join: {
[roomId]: {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: "@bogus:sender",
}),
],
},
},
},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: "@bogus:sender",
}),
],
},
};
bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
const eventPromise = new Promise<MatrixEvent>((resolve) => {
const onEvent = function(event: MatrixEvent) {
const onEvent = function (event: MatrixEvent) {
logger.log(bobUserId + " received event", event);
resolve(event);
};
@@ -558,11 +534,10 @@ describe("MatrixClient crypto", () => {
await aliDownloadsKeys();
aliTestClient.client.setDeviceBlocked(bobUserId, bobDeviceId, true);
const p1 = sendMessage(aliTestClient.client);
const p2 = expectSendMessageRequest(aliTestClient.httpBackend)
.then(function(sentContent) {
// no unblocked devices, so the ciphertext should be empty
expect(sentContent.ciphertext).toEqual({});
});
const p2 = expectSendMessageRequest(aliTestClient.httpBackend).then(function (sentContent) {
// no unblocked devices, so the ciphertext should be empty
expect(sentContent.ciphertext).toEqual({});
});
await Promise.all([p1, p2]);
});
@@ -570,7 +545,7 @@ describe("MatrixClient crypto", () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start();
await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
await firstSync(aliTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
@@ -588,9 +563,7 @@ describe("MatrixClient crypto", () => {
await firstSync(bobTestClient);
await aliEnablesEncryption();
await aliSendsFirstMessage();
bobTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {},
);
bobTestClient.httpBackend.when("POST", "/keys/query").respond(200, {});
await bobRecvMessage();
await bobEnablesEncryption();
const ciphertext = await bobSendsReplyMessage();
@@ -605,28 +578,28 @@ describe("MatrixClient crypto", () => {
await aliTestClient.start();
await firstSync(aliTestClient);
const syncData = {
next_batch: '2',
next_batch: "2",
rooms: {
join: {},
},
};
syncData.rooms.join[roomId] = {
state: {
events: [
testUtils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.olm.v1.curve25519-aes-sha2',
join: {
[roomId]: {
state: {
events: [
testUtils.mkEvent({
type: "m.room.encryption",
skey: "",
content: {
algorithm: "m.olm.v1.curve25519-aes-sha2",
},
}),
],
},
}),
],
},
},
},
};
aliTestClient.httpBackend.when('GET', '/sync').respond(
200, syncData);
await aliTestClient.httpBackend.flush('/sync', 1);
aliTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
await aliTestClient.httpBackend.flush("/sync", 1);
aliTestClient.expectKeyQuery({
device_keys: {
[bobUserId]: {},
@@ -649,7 +622,7 @@ describe("MatrixClient crypto", () => {
// enqueue expectations:
// * Sync with empty one_time_keys => upload keys
logger.log(aliTestClient + ': starting');
logger.log(aliTestClient + ": starting");
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
@@ -659,24 +632,61 @@ describe("MatrixClient crypto", () => {
// it will upload one-time keys.
httpBackend.when("GET", "/sync").respond(200, syncDataEmpty);
await Promise.all([
aliTestClient.client.startClient({}),
httpBackend.flushAllExpected(),
]);
logger.log(aliTestClient + ': started');
httpBackend.when("POST", "/keys/upload")
.respond(200, (_path, content: IUploadKeysRequest) => {
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).not.toEqual({});
expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1);
// cancel futher calls by telling the client
// we have more than we need
return {
one_time_key_counts: {
signed_curve25519: 70,
},
};
});
await Promise.all([aliTestClient.client.startClient({}), httpBackend.flushAllExpected()]);
logger.log(aliTestClient + ": started");
httpBackend.when("POST", "/keys/upload").respond(200, (_path, content: IUploadKeysRequest) => {
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).not.toEqual({});
expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1);
// cancel futher calls by telling the client
// we have more than we need
return {
one_time_key_counts: {
signed_curve25519: 70,
},
};
});
await httpBackend.flushAllExpected();
});
it("Checks for outgoing room key requests for a given event's session", async () => {
const eventA0 = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: "m.megolm.v1.aes-sha2",
session_id: "sessionid",
sender_key: "senderkey",
},
});
const eventA1 = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: "m.megolm.v1.aes-sha2",
session_id: "sessionid",
sender_key: "senderkey",
},
});
const eventB = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: "m.megolm.v1.aes-sha2",
session_id: "othersessionid",
sender_key: "senderkey",
},
});
const nonEncryptedEvent = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {},
});
aliTestClient.client.crypto?.onSyncCompleted({});
await aliTestClient.client.cancelAndResendEventRoomKeyRequest(eventA0);
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventA1)).not.toBeNull();
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(eventB)).toBeNull();
expect(await aliTestClient.client.getOutgoingRoomKeyRequest(nonEncryptedEvent)).toBeNull();
});
});
+105
View File
@@ -0,0 +1,105 @@
/*
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 "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { createClient } from "../../../src";
afterEach(() => {
// reset fake-indexeddb after each test, to make sure we don't leak connections
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
// eslint-disable-next-line no-global-assign
indexedDB = new IDBFactory();
});
describe("MatrixClient.initRustCrypto", () => {
it("should raise if userId or deviceId is unknown", async () => {
const unknownUserClient = createClient({
baseUrl: "http://test.server",
deviceId: "aliceDevice",
});
await expect(() => unknownUserClient.initRustCrypto()).rejects.toThrow("unknown userId");
const unknownDeviceClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:test",
});
await expect(() => unknownDeviceClient.initRustCrypto()).rejects.toThrow("unknown deviceId");
});
it("should create the indexed dbs", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
});
// No databases.
expect(await indexedDB.databases()).toHaveLength(0);
await matrixClient.initRustCrypto();
// should have two dbs now
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
expect(databaseNames).toEqual(
expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto", "matrix-js-sdk::matrix-sdk-crypto-meta"]),
);
});
it("should ignore a second call", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
});
await matrixClient.initRustCrypto();
await matrixClient.initRustCrypto();
});
});
describe("MatrixClient.clearStores", () => {
it("should clear the indexeddbs", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
});
await matrixClient.initRustCrypto();
expect(await indexedDB.databases()).toHaveLength(2);
await matrixClient.stopClient();
await matrixClient.clearStores();
expect(await indexedDB.databases()).toHaveLength(0);
});
it("should not fail in environments without indexedDB", async () => {
// eslint-disable-next-line no-global-assign
indexedDB = undefined!;
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
});
await matrixClient.stopClient();
await matrixClient.clearStores();
// No error thrown in clearStores
});
});
File diff suppressed because it is too large Load Diff
+264 -259
View File
@@ -16,9 +16,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { TestClient } from '../TestClient';
import * as testUtils from '../test-utils/test-utils';
import { logger } from '../../src/logger';
import { TestClient } from "../TestClient";
import * as testUtils from "../test-utils/test-utils";
import { logger } from "../../src/logger";
const ROOM_ID = "!room:id";
@@ -26,26 +26,24 @@ const ROOM_ID = "!room:id";
* get a /sync response which contains a single e2e room (ROOM_ID), with the
* members given
*
* @param {string[]} roomMembers
*
* @return {object} sync response
* @returns sync response
*/
function getSyncResponse(roomMembers) {
function getSyncResponse(roomMembers: string[]) {
const stateEvents = [
testUtils.mkEvent({
type: 'm.room.encryption',
skey: '',
type: "m.room.encryption",
skey: "",
content: {
algorithm: 'm.megolm.v1.aes-sha2',
algorithm: "m.megolm.v1.aes-sha2",
},
}),
];
Array.prototype.push.apply(
stateEvents,
roomMembers.map(
(m) => testUtils.mkMembership({
mship: 'join',
roomMembers.map((m) =>
testUtils.mkMembership({
mship: "join",
sender: m,
}),
),
@@ -67,24 +65,22 @@ function getSyncResponse(roomMembers) {
return syncResponse;
}
describe("DeviceList management:", function() {
describe("DeviceList management:", function () {
if (!global.Olm) {
logger.warn('not running deviceList tests: Olm not present');
logger.warn("not running deviceList tests: Olm not present");
return;
}
let sessionStoreBackend;
let aliceTestClient;
let aliceTestClient: TestClient;
let sessionStoreBackend: Storage;
async function createTestClient() {
const testClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend,
);
const testClient = new TestClient("@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend);
await testClient.client.initCrypto();
return testClient;
}
beforeEach(async function() {
beforeEach(async function () {
// we create our own sessionStoreBackend so that we can use it for
// another TestClient.
sessionStoreBackend = new testUtils.MockStorageApi();
@@ -92,305 +88,314 @@ describe("DeviceList management:", function() {
aliceTestClient = await createTestClient();
});
afterEach(function() {
afterEach(function () {
return aliceTestClient.stop();
});
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
return aliceTestClient.start().then(function() {
const syncResponse = getSyncResponse(['@bob:xyz']);
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
logger.log("Forcing alice to download our device keys");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:xyz': {},
},
});
return Promise.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
logger.log("Telling alice to send a megolm message");
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, {
event_id: '$event_id',
});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
it("Alice shouldn't do a second /query for non-e2e-capable devices", function () {
aliceTestClient.expectKeyQuery({
device_keys: { "@alice:localhost": {} },
failures: {},
});
return aliceTestClient
.start()
.then(function () {
const syncResponse = getSyncResponse(["@bob:xyz"]);
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.flushSync();
})
.then(function () {
logger.log("Forcing alice to download our device keys");
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
device_keys: {
"@bob:xyz": {},
},
});
return Promise.all([
aliceTestClient.client.downloadKeys(["@bob:xyz"]),
aliceTestClient.httpBackend.flush("/keys/query", 1),
]);
})
.then(function () {
logger.log("Telling alice to send a megolm message");
aliceTestClient.httpBackend.when("PUT", "/send/").respond(200, {
event_id: "$event_id",
});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, "test"),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
});
});
it.skip("We should not get confused by out-of-order device query responses", () => {
// https://github.com/vector-im/element-web/issues/3126
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
return aliceTestClient.start().then(() => {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
return aliceTestClient.flushSync();
}).then(() => {
// to make sure the initial device queries are flushed out, we
// attempt to send a message.
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {
device_keys: {
'@bob:xyz': {},
'@chris:abc': {},
},
},
);
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
200, { event_id: '$event1' });
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
() => aliceTestClient.httpBackend.flush('/send/', 1),
),
aliceTestClient.client.crypto.deviceList.saveIfDirty(),
]);
}).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
expect(data.syncToken).toEqual(1);
});
// invalidate bob's and chris's device lists in separate syncs
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
next_batch: '2',
device_lists: {
changed: ['@bob:xyz'],
},
});
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
next_batch: '3',
device_lists: {
changed: ['@chris:abc'],
},
});
// flush both syncs
return aliceTestClient.flushSync().then(() => {
return aliceTestClient.flushSync();
});
}).then(() => {
// check that we don't yet have a request for chris's devices.
aliceTestClient.httpBackend.when('POST', '/keys/query', {
device_keys: {
'@chris:abc': {},
},
token: '3',
}).respond(200, {
device_keys: { '@chris:abc': {} },
});
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(0);
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
if (bobStat != 1 && bobStat != 2) {
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
bobStat);
}
const chrisStat = data.trackingStatus['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error(
'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
);
}
});
// now add an expectation for a query for bob's devices, and let
// it complete.
aliceTestClient.httpBackend.when('POST', '/keys/query', {
device_keys: {
'@bob:xyz': {},
},
token: '2',
}).respond(200, {
device_keys: { '@bob:xyz': {} },
});
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
}).then(() => {
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(3);
const chrisStat = data.trackingStatus['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error(
'Unexpected status for chris: wanted 1 or 2, got ' + bobStat,
);
}
});
// now let the query for chris's devices complete.
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@chris:abc']);
}).then(() => {
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
const chrisStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(3);
expect(chrisStat).toEqual(3);
expect(data.syncToken).toEqual(3);
});
aliceTestClient.expectKeyQuery({
device_keys: { "@alice:localhost": {} },
failures: {},
});
return aliceTestClient
.start()
.then(() => {
aliceTestClient.httpBackend
.when("GET", "/sync")
.respond(200, getSyncResponse(["@bob:xyz", "@chris:abc"]));
return aliceTestClient.flushSync();
})
.then(() => {
// to make sure the initial device queries are flushed out, we
// attempt to send a message.
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
device_keys: {
"@bob:xyz": {},
"@chris:abc": {},
},
});
aliceTestClient.httpBackend.when("PUT", "/send/").respond(200, { event_id: "$event1" });
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, "test"),
aliceTestClient.httpBackend
.flush("/keys/query", 1)
.then(() => aliceTestClient.httpBackend.flush("/send/", 1)),
aliceTestClient.client.crypto!.deviceList.saveIfDirty(),
]);
})
.then(() => {
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
expect(data!.syncToken).toEqual(1);
});
// invalidate bob's and chris's device lists in separate syncs
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: "2",
device_lists: {
changed: ["@bob:xyz"],
},
});
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: "3",
device_lists: {
changed: ["@chris:abc"],
},
});
// flush both syncs
return aliceTestClient.flushSync().then(() => {
return aliceTestClient.flushSync();
});
})
.then(() => {
// check that we don't yet have a request for chris's devices.
aliceTestClient.httpBackend
.when("POST", "/keys/query", {
device_keys: {
"@chris:abc": {},
},
token: "3",
})
.respond(200, {
device_keys: { "@chris:abc": {} },
});
return aliceTestClient.httpBackend.flush("/keys/query", 1);
})
.then((flushed) => {
expect(flushed).toEqual(0);
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
})
.then(() => {
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus["@bob:xyz"];
if (bobStat != 1 && bobStat != 2) {
throw new Error("Unexpected status for bob: wanted 1 or 2, got " + bobStat);
}
const chrisStat = data!.trackingStatus["@chris:abc"];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error("Unexpected status for chris: wanted 1 or 2, got " + chrisStat);
}
});
// now add an expectation for a query for bob's devices, and let
// it complete.
aliceTestClient.httpBackend
.when("POST", "/keys/query", {
device_keys: {
"@bob:xyz": {},
},
token: "2",
})
.respond(200, {
device_keys: { "@bob:xyz": {} },
});
return aliceTestClient.httpBackend.flush("/keys/query", 1);
})
.then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(["@bob:xyz"]);
})
.then(() => {
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
})
.then(() => {
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus["@bob:xyz"];
expect(bobStat).toEqual(3);
const chrisStat = data!.trackingStatus["@chris:abc"];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error("Unexpected status for chris: wanted 1 or 2, got " + bobStat);
}
});
// now let the query for chris's devices complete.
return aliceTestClient.httpBackend.flush("/keys/query", 1);
})
.then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(["@chris:abc"]);
})
.then(() => {
return aliceTestClient.client.crypto!.deviceList.saveIfDirty();
})
.then(() => {
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus["@bob:xyz"];
const chrisStat = data!.trackingStatus["@bob:xyz"];
expect(bobStat).toEqual(3);
expect(chrisStat).toEqual(3);
expect(data!.syncToken).toEqual(3);
});
});
});
// https://github.com/vector-im/element-web/issues/4983
describe("Alice should know she has stale device lists", () => {
beforeEach(async function() {
beforeEach(async function () {
await aliceTestClient.start();
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz']));
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse(["@bob:xyz"]));
await aliceTestClient.flushSync();
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {
device_keys: {
'@bob:xyz': {},
},
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, {
device_keys: {
"@bob:xyz": {},
},
);
await aliceTestClient.httpBackend.flush('/keys/query', 1);
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
});
await aliceTestClient.httpBackend.flush("/keys/query", 1);
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus["@bob:xyz"];
// Alice should be tracking bob's device list
expect(bobStat).toBeGreaterThan(
0,
);
expect(bobStat).toBeGreaterThan(0);
});
});
it("when Bob leaves", async function() {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, {
next_batch: 2,
device_lists: {
left: ['@bob:xyz'],
},
rooms: {
join: {
[ROOM_ID]: {
timeline: {
events: [
testUtils.mkMembership({
mship: 'leave',
sender: '@bob:xyz',
}),
],
},
it("when Bob leaves", async function () {
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 2,
device_lists: {
left: ["@bob:xyz"],
},
rooms: {
join: {
[ROOM_ID]: {
timeline: {
events: [
testUtils.mkMembership({
mship: "leave",
sender: "@bob:xyz",
}),
],
},
},
},
},
);
});
await aliceTestClient.flushSync();
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus["@bob:xyz"];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(
0,
);
expect(bobStat).toEqual(0);
});
});
it("when Alice leaves", async function() {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, {
next_batch: 2,
device_lists: {
left: ['@bob:xyz'],
},
rooms: {
leave: {
[ROOM_ID]: {
timeline: {
events: [
testUtils.mkMembership({
mship: 'leave',
sender: '@bob:xyz',
}),
],
},
it("when Alice leaves", async function () {
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 2,
device_lists: {
left: ["@bob:xyz"],
},
rooms: {
leave: {
[ROOM_ID]: {
timeline: {
events: [
testUtils.mkMembership({
mship: "leave",
sender: "@bob:xyz",
}),
],
},
},
},
},
);
});
await aliceTestClient.flushSync();
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
await aliceTestClient.client.crypto!.deviceList.saveIfDirty();
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
// @ts-ignore accessing a protected field
aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus["@bob:xyz"];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(
0,
);
expect(bobStat).toEqual(0);
});
});
it("when Bob leaves whilst Alice is offline", async function() {
it("when Bob leaves whilst Alice is offline", async function () {
aliceTestClient.stop();
const anotherTestClient = await createTestClient();
try {
await anotherTestClient.start();
anotherTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse([]));
anotherTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse([]));
await anotherTestClient.flushSync();
await anotherTestClient.client?.crypto?.deviceList?.saveIfDirty();
// @ts-ignore accessing private property
anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data!.trackingStatus['@bob:xyz'];
const bobStat = data!.trackingStatus["@bob:xyz"];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual(
0,
);
expect(bobStat).toEqual(0);
});
} finally {
anotherTestClient.stop();
+87 -93
View File
@@ -29,7 +29,7 @@ import {
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
describe("MatrixClient events", function() {
describe("MatrixClient events", function () {
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
let client: MatrixClient | undefined;
@@ -46,23 +46,25 @@ describe("MatrixClient events", function() {
return [client!, httpBackend];
};
beforeEach(function() {
beforeEach(function () {
[client!, httpBackend] = setupTests();
});
afterEach(function() {
afterEach(function () {
httpBackend?.verifyNoOutstandingExpectation();
client?.stopClient();
return httpBackend?.stop();
});
describe("emissions", function() {
describe("emissions", function () {
const SYNC_DATA = {
next_batch: "s_5_3",
presence: {
events: [
utils.mkPresence({
user: "@foo:bar", name: "Foo Bar", presence: "online",
user: "@foo:bar",
name: "Foo Bar",
presence: "online",
}),
],
},
@@ -72,7 +74,9 @@ describe("MatrixClient events", function() {
timeline: {
events: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm",
room: "!erufh:bar",
user: "@foo:bar",
msg: "hmmm",
}),
],
prev_batch: "s",
@@ -80,14 +84,15 @@ describe("MatrixClient events", function() {
state: {
events: [
utils.mkMembership({
room: "!erufh:bar", mship: "join", user: "@foo:bar",
room: "!erufh:bar",
mship: "join",
user: "@foo:bar",
}),
utils.mkEvent({
type: "m.room.create", room: "!erufh:bar",
type: "m.room.create",
room: "!erufh:bar",
user: "@foo:bar",
content: {
creator: "@foo:bar",
},
content: {},
}),
],
},
@@ -103,18 +108,23 @@ describe("MatrixClient events", function() {
timeline: {
events: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar",
room: "!erufh:bar",
user: "@foo:bar",
msg: "ello ello",
}),
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: ":D",
room: "!erufh:bar",
user: "@foo:bar",
msg: ":D",
}),
],
},
ephemeral: {
events: [
utils.mkEvent({
type: "m.typing", room: "!erufh:bar", content: {
type: "m.typing",
room: "!erufh:bar",
content: {
user_ids: ["@foo:bar"],
},
}),
@@ -125,50 +135,49 @@ describe("MatrixClient events", function() {
},
};
it("should emit events from both the first and subsequent /sync calls",
function() {
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
it("should emit events from both the first and subsequent /sync calls", function () {
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let expectedEvents: Partial<IEvent>[] = [];
expectedEvents = expectedEvents.concat(
SYNC_DATA.presence.events,
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
);
let expectedEvents: Partial<IEvent>[] = [];
expectedEvents = expectedEvents.concat(
SYNC_DATA.presence.events,
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
);
client!.on(ClientEvent.Event, function(event) {
let found = false;
for (let i = 0; i < expectedEvents.length; i++) {
if (expectedEvents[i].event_id === event.getId()) {
expectedEvents.splice(i, 1);
found = true;
break;
}
client!.on(ClientEvent.Event, function (event) {
let found = false;
for (let i = 0; i < expectedEvents.length; i++) {
if (expectedEvents[i].event_id === event.getId()) {
expectedEvents.splice(i, 1);
found = true;
break;
}
expect(found).toBe(true);
});
client!.startClient();
return Promise.all([
// wait for two SYNCING events
utils.syncPromise(client!).then(() => {
return utils.syncPromise(client!);
}),
httpBackend!.flushAllExpected(),
]).then(() => {
expect(expectedEvents.length).toEqual(0);
});
}
expect(found).toBe(true);
});
it("should emit User events", function(done) {
client!.startClient();
return Promise.all([
// wait for two SYNCING events
utils.syncPromise(client!).then(() => {
return utils.syncPromise(client!);
}),
httpBackend!.flushAllExpected(),
]).then(() => {
expect(expectedEvents.length).toEqual(0);
});
});
it("should emit User events", async () => {
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let fired = false;
client!.on(UserEvent.Presence, function(event, user) {
client!.on(UserEvent.Presence, function (event, user) {
fired = true;
expect(user).toBeTruthy();
expect(event).toBeTruthy();
@@ -177,59 +186,50 @@ describe("MatrixClient events", function() {
}
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
expect(user.presence).toEqual(
SYNC_DATA.presence.events[0]?.content?.presence,
);
expect(user.presence).toEqual(SYNC_DATA.presence.events[0]?.content?.presence);
});
client!.startClient();
httpBackend!.flushAllExpected().then(function() {
expect(fired).toBe(true);
done();
});
await httpBackend!.flushAllExpected();
expect(fired).toBe(true);
});
it("should emit Room events", function() {
it("should emit Room events", function () {
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let roomInvokeCount = 0;
let roomNameInvokeCount = 0;
let timelineFireCount = 0;
client!.on(ClientEvent.Room, function(room) {
client!.on(ClientEvent.Room, function (room) {
roomInvokeCount++;
expect(room.roomId).toEqual("!erufh:bar");
});
client!.on(RoomEvent.Timeline, function(event, room) {
client!.on(RoomEvent.Timeline, function (event, room) {
timelineFireCount++;
expect(room?.roomId).toEqual("!erufh:bar");
});
client!.on(RoomEvent.Name, function(room) {
client!.on(RoomEvent.Name, function (room) {
roomNameInvokeCount++;
});
client!.startClient();
return Promise.all([
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 2),
]).then(function() {
return Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 2)]).then(function () {
expect(roomInvokeCount).toEqual(1);
expect(roomNameInvokeCount).toEqual(1);
expect(timelineFireCount).toEqual(3);
});
});
it("should emit RoomState events", function() {
it("should emit RoomState events", function () {
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
const roomStateEventTypes = [
"m.room.member", "m.room.create",
];
const roomStateEventTypes = ["m.room.member", "m.room.create"];
let eventsInvokeCount = 0;
let membersInvokeCount = 0;
let newMemberInvokeCount = 0;
client!.on(RoomStateEvent.Events, function(event, state) {
client!.on(RoomStateEvent.Events, function (event, state) {
eventsInvokeCount++;
const index = roomStateEventTypes.indexOf(event.getType());
expect(index).not.toEqual(-1);
@@ -237,13 +237,13 @@ describe("MatrixClient events", function() {
roomStateEventTypes.splice(index, 1);
}
});
client!.on(RoomStateEvent.Members, function(event, state, member) {
client!.on(RoomStateEvent.Members, function (event, state, member) {
membersInvokeCount++;
expect(member.roomId).toEqual("!erufh:bar");
expect(member.userId).toEqual("@foo:bar");
expect(member.membership).toEqual("join");
});
client!.on(RoomStateEvent.NewMember, function(event, state, member) {
client!.on(RoomStateEvent.NewMember, function (event, state, member) {
newMemberInvokeCount++;
expect(member.roomId).toEqual("!erufh:bar");
expect(member.userId).toEqual("@foo:bar");
@@ -252,17 +252,14 @@ describe("MatrixClient events", function() {
client!.startClient();
return Promise.all([
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 2),
]).then(function() {
return Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 2)]).then(function () {
expect(membersInvokeCount).toEqual(1);
expect(newMemberInvokeCount).toEqual(1);
expect(eventsInvokeCount).toEqual(2);
});
});
it("should emit RoomMember events", function() {
it("should emit RoomMember events", function () {
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
@@ -270,27 +267,24 @@ describe("MatrixClient events", function() {
let powerLevelInvokeCount = 0;
let nameInvokeCount = 0;
let membershipInvokeCount = 0;
client!.on(RoomMemberEvent.Name, function(event, member) {
client!.on(RoomMemberEvent.Name, function (event, member) {
nameInvokeCount++;
});
client!.on(RoomMemberEvent.Typing, function(event, member) {
client!.on(RoomMemberEvent.Typing, function (event, member) {
typingInvokeCount++;
expect(member.typing).toBe(true);
});
client!.on(RoomMemberEvent.PowerLevel, function(event, member) {
client!.on(RoomMemberEvent.PowerLevel, function (event, member) {
powerLevelInvokeCount++;
});
client!.on(RoomMemberEvent.Membership, function(event, member) {
client!.on(RoomMemberEvent.Membership, function (event, member) {
membershipInvokeCount++;
expect(member.membership).toEqual("join");
});
client!.startClient();
return Promise.all([
httpBackend!.flushAllExpected(),
utils.syncPromise(client!, 2),
]).then(function() {
return Promise.all([httpBackend!.flushAllExpected(), utils.syncPromise(client!, 2)]).then(function () {
expect(typingInvokeCount).toEqual(1);
expect(powerLevelInvokeCount).toEqual(0);
expect(nameInvokeCount).toEqual(0);
@@ -298,36 +292,36 @@ describe("MatrixClient events", function() {
});
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
const error = { errcode: 'M_UNKNOWN_TOKEN' };
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function () {
const error = { errcode: "M_UNKNOWN_TOKEN" };
httpBackend!.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0;
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
client!.on(HttpApiEvent.SessionLoggedOut, function (errObj) {
sessionLoggedOutCount++;
expect(errObj.data).toEqual(error);
});
client!.startClient();
return httpBackend!.flushAllExpected().then(function() {
return httpBackend!.flushAllExpected().then(function () {
expect(sessionLoggedOutCount).toEqual(1);
});
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() {
const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true };
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function () {
const error = { errcode: "M_UNKNOWN_TOKEN", soft_logout: true };
httpBackend!.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0;
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
client!.on(HttpApiEvent.SessionLoggedOut, function (errObj) {
sessionLoggedOutCount++;
expect(errObj.data).toEqual(error);
});
client!.startClient();
return httpBackend!.flushAllExpected().then(function() {
return httpBackend!.flushAllExpected().then(function () {
expect(sessionLoggedOutCount).toEqual(1);
});
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+84 -70
View File
@@ -1,13 +1,13 @@
import HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils/test-utils";
import { MatrixClient } from "../../src/matrix";
import { ClientEvent, MatrixClient } from "../../src/matrix";
import { MatrixScheduler } from "../../src/scheduler";
import { MemoryStore } from "../../src/store/memory";
import { MatrixError } from "../../src/http-api";
import { IStore } from "../../src/store";
describe("MatrixClient opts", function() {
describe("MatrixClient opts", function () {
const baseUrl = "http://localhost.or.something";
let httpBackend = new HttpBackend();
const userId = "@alice:localhost";
@@ -19,11 +19,14 @@ describe("MatrixClient opts", function() {
presence: {},
rooms: {
join: {
"!foo:bar": { // roomId
"!foo:bar": {
// roomId
timeline: {
events: [
utils.mkMessage({
room: roomId, user: userB, msg: "hello",
room: roomId,
user: userB,
msg: "hello",
}),
],
prev_batch: "f_1_1",
@@ -31,22 +34,30 @@ describe("MatrixClient opts", function() {
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB,
type: "m.room.name",
room: roomId,
user: userB,
content: {
name: "Old room name",
},
}),
utils.mkMembership({
room: roomId, mship: "join", user: userB, name: "Bob",
room: roomId,
mship: "join",
user: userB,
name: "Bob",
}),
utils.mkMembership({
room: roomId, mship: "join", user: userId, name: "Alice",
room: roomId,
mship: "join",
user: userId,
name: "Alice",
}),
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId,
},
type: "m.room.create",
room: roomId,
user: userId,
content: {},
}),
],
},
@@ -55,18 +66,18 @@ describe("MatrixClient opts", function() {
},
};
beforeEach(function() {
beforeEach(function () {
httpBackend = new HttpBackend();
});
afterEach(function() {
afterEach(function () {
httpBackend.verifyNoOutstandingExpectation();
return httpBackend.stop();
});
describe("without opts.store", function() {
let client;
beforeEach(function() {
describe("without opts.store", function () {
let client: MatrixClient;
beforeEach(function () {
client = new MatrixClient({
fetchFn: httpBackend.fetchFn as typeof global.fetch,
store: undefined,
@@ -77,34 +88,34 @@ describe("MatrixClient opts", function() {
});
});
afterEach(function() {
afterEach(function () {
client.stopClient();
});
it("should be able to send messages", function(done) {
it("should be able to send messages", async () => {
const eventId = "$flibble:wibble";
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId,
});
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
expect(res.event_id).toEqual(eventId);
done();
});
httpBackend.flush("/txn1", 1);
const [res] = await Promise.all([
client.sendTextMessage("!foo:bar", "a body", "txn1"),
httpBackend.flush("/txn1", 1),
]);
expect(res.event_id).toEqual(eventId);
});
it("should be able to sync / get new events", async function() {
const expectedEventTypes = [ // from /initialSync
"m.room.message", "m.room.name", "m.room.member", "m.room.member",
it("should be able to sync / get new events", async function () {
const expectedEventTypes = [
// from /initialSync
"m.room.message",
"m.room.name",
"m.room.member",
"m.room.member",
"m.room.create",
];
client.on("event", function(event) {
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
-1,
);
expectedEventTypes.splice(
expectedEventTypes.indexOf(event.getType()), 1,
);
client.on(ClientEvent.Event, function (event) {
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(-1);
expectedEventTypes.splice(expectedEventTypes.indexOf(event.getType()), 1);
});
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
@@ -114,19 +125,14 @@ describe("MatrixClient opts", function() {
await httpBackend.flush("/versions", 1);
await httpBackend.flush("/pushrules", 1);
await httpBackend.flush("/filter", 1);
await Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]);
expect(expectedEventTypes.length).toEqual(
0,
);
await Promise.all([httpBackend.flush("/sync", 1), utils.syncPromise(client)]);
expect(expectedEventTypes.length).toEqual(0);
});
});
describe("without opts.scheduler", function() {
let client;
beforeEach(function() {
describe("without opts.scheduler", function () {
let client: MatrixClient;
beforeEach(function () {
client = new MatrixClient({
fetchFn: httpBackend.fetchFn as typeof global.fetch,
store: new MemoryStore() as IStore,
@@ -137,25 +143,25 @@ describe("MatrixClient opts", function() {
});
});
afterEach(function() {
afterEach(function () {
client.stopClient();
});
it("shouldn't retry sending events", function(done) {
httpBackend.when("PUT", "/txn1").respond(500, new MatrixError({
errcode: "M_SOMETHING",
error: "Ruh roh",
}));
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
expect(false).toBe(true);
}, function(err) {
expect(err.errcode).toEqual("M_SOMETHING");
done();
});
httpBackend.flush("/txn1", 1);
it("shouldn't retry sending events", async () => {
httpBackend.when("PUT", "/txn1").respond(
500,
new MatrixError({
errcode: "M_SOMETHING",
error: "Ruh roh",
}),
);
await expect(
Promise.all([client.sendTextMessage("!foo:bar", "a body", "txn1"), httpBackend.flush("/txn1", 1)]),
).rejects.toThrow("MatrixError: [500] Unknown message");
});
it("shouldn't queue events", function(done) {
it("shouldn't queue events", async () => {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: "AAA",
});
@@ -164,30 +170,38 @@ describe("MatrixClient opts", function() {
});
let sentA = false;
let sentB = false;
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
const messageASendPromise = client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
sentA = true;
// We expect messageB to be sent before messageA to ensure as we're
// testing that there is no queueing that blocks each other
expect(sentB).toBe(true);
});
client.sendTextMessage("!foo:bar", "b body", "txn2").then(function(res) {
const messageBSendPromise = client.sendTextMessage("!foo:bar", "b body", "txn2").then(function (res) {
sentB = true;
// We expect messageB to be sent before messageA to ensure as we're
// testing that there is no queueing that blocks each other
expect(sentA).toBe(false);
});
httpBackend.flush("/txn2", 1).then(function() {
httpBackend.flush("/txn1", 1).then(function() {
done();
});
});
// Allow messageB to succeed first
await httpBackend.flush("/txn2", 1);
// Then allow messageA to succeed
await httpBackend.flush("/txn1", 1);
// Now await the message send promises to
await messageBSendPromise;
await messageASendPromise;
});
it("should be able to send messages", function(done) {
it("should be able to send messages", async () => {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: "foo",
});
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
expect(res.event_id).toEqual("foo");
done();
});
httpBackend.flush("/txn1", 1);
const [res] = await Promise.all([
client.sendTextMessage("!foo:bar", "a body", "txn1"),
httpBackend.flush("/txn1", 1),
]);
expect(res.event_id).toEqual("foo");
});
});
});
+28 -39
View File
@@ -29,13 +29,7 @@ describe("MatrixClient relations", () => {
const setupTests = (): [MatrixClient, HttpBackend] => {
const scheduler = new MatrixScheduler();
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{ scheduler },
);
const testClient = new TestClient(userId, "DEVICE", accessToken, undefined, { scheduler });
const httpBackend = testClient.httpBackend;
const client = testClient.client;
@@ -52,76 +46,71 @@ describe("MatrixClient relations", () => {
});
it("should read related events with the default options", async () => {
const response = client!.relations(roomId, '$event-0', null, null);
const response = client!.relations(roomId, "$event-0", null, null);
httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
httpBackend!
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0?dir=b")
.respond(200, { chunk: [], next_batch: 'NEXT' });
.when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0?dir=b")
.respond(200, { chunk: [], next_batch: "NEXT" });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
expect(await response).toEqual({ events: [], nextBatch: "NEXT", originalEvent: null, prevBatch: null });
});
it("should read related events with relation type", async () => {
const response = client!.relations(roomId, '$event-0', 'm.reference', null);
const response = client!.relations(roomId, "$event-0", "m.reference", null);
httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
httpBackend!
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b")
.respond(200, { chunk: [], next_batch: 'NEXT' });
.when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b")
.respond(200, { chunk: [], next_batch: "NEXT" });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
expect(await response).toEqual({ events: [], nextBatch: "NEXT", originalEvent: null, prevBatch: null });
});
it("should read related events with relation type and event type", async () => {
const response = client!.relations(roomId, '$event-0', 'm.reference', 'm.room.message');
const response = client!.relations(roomId, "$event-0", "m.reference", "m.room.message");
httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
httpBackend!
.when(
"GET",
"/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b",
)
.respond(200, { chunk: [], next_batch: 'NEXT' });
.when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b")
.respond(200, { chunk: [], next_batch: "NEXT" });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
expect(await response).toEqual({ events: [], nextBatch: "NEXT", originalEvent: null, prevBatch: null });
});
it("should read related events with custom options", async () => {
const response = client!.relations(roomId, '$event-0', null, null, {
const response = client!.relations(roomId, "$event-0", null, null, {
dir: Direction.Forward,
from: 'FROM',
from: "FROM",
limit: 10,
to: 'TO',
to: "TO",
});
httpBackend!.when("GET", "/rooms/!room%3Ahere/event/%24event-0").respond(200, null);
httpBackend!
.when(
"GET",
"/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO",
)
.respond(200, { chunk: [], next_batch: 'NEXT' });
.when("GET", "/_matrix/client/v1/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO")
.respond(200, { chunk: [], next_batch: "NEXT" });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT", "originalEvent": null, "prevBatch": null });
expect(await response).toEqual({ events: [], nextBatch: "NEXT", originalEvent: null, prevBatch: null });
});
it('should use default direction in the fetchRelations endpoint', async () => {
const response = client!.fetchRelations(roomId, '$event-0', null, null);
it("should use default direction in the fetchRelations endpoint", async () => {
const response = client!.fetchRelations(roomId, "$event-0", null, null);
httpBackend!
.when(
"GET",
"/rooms/!room%3Ahere/relations/%24event-0?dir=b",
)
.respond(200, { chunk: [], next_batch: 'NEXT' });
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0?dir=b")
.respond(200, { chunk: [], next_batch: "NEXT" });
await httpBackend!.flushAllExpected();
expect(await response).toEqual({ "chunk": [], "next_batch": "NEXT" });
expect(await response).toEqual({ chunk: [], next_batch: "NEXT" });
});
});
+45 -59
View File
@@ -20,7 +20,7 @@ import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src
import { Room } from "../../src/models/room";
import { TestClient } from "../TestClient";
describe("MatrixClient retrying", function() {
describe("MatrixClient retrying", function () {
const userId = "@alice:localhost";
const accessToken = "aseukfgwef";
const roomId = "!room:here";
@@ -30,13 +30,7 @@ describe("MatrixClient retrying", function() {
const setupTests = (): [MatrixClient, HttpBackend, Room] => {
const scheduler = new MatrixScheduler();
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{ scheduler },
);
const testClient = new TestClient(userId, "DEVICE", accessToken, undefined, { scheduler });
const httpBackend = testClient.httpBackend;
const client = testClient.client;
const room = new Room(roomId, client, userId);
@@ -45,49 +39,46 @@ describe("MatrixClient retrying", function() {
return [client, httpBackend, room];
};
beforeEach(function() {
beforeEach(function () {
[client, httpBackend, room] = setupTests();
});
afterEach(function() {
afterEach(function () {
httpBackend!.verifyNoOutstandingExpectation();
return httpBackend!.stop();
});
xit("should retry according to MatrixScheduler.retryFn", function() {
it.skip("should retry according to MatrixScheduler.retryFn", function () {});
});
it.skip("should queue according to MatrixScheduler.queueFn", function () {});
xit("should queue according to MatrixScheduler.queueFn", function() {
it.skip("should mark events as EventStatus.NOT_SENT when giving up", function () {});
});
it.skip("should mark events as EventStatus.QUEUED when queued", function () {});
xit("should mark events as EventStatus.NOT_SENT when giving up", function() {
});
xit("should mark events as EventStatus.QUEUED when queued", function() {
});
it("should mark events as EventStatus.CANCELLED when cancelled", function() {
it("should mark events as EventStatus.CANCELLED when cancelled", function () {
// send a couple of events; the second will be queued
const p1 = client!.sendMessage(roomId, {
"msgtype": "m.text",
"body": "m1",
}).then(function() {
// we expect the first message to fail
throw new Error('Message 1 unexpectedly sent successfully');
}, () => {
// this is expected
});
const p1 = client!
.sendMessage(roomId, {
msgtype: "m.text",
body: "m1",
})
.then(
function () {
// we expect the first message to fail
throw new Error("Message 1 unexpectedly sent successfully");
},
() => {
// this is expected
},
);
// XXX: it turns out that the promise returned by this message
// never gets resolved.
// https://github.com/matrix-org/matrix-js-sdk/issues/496
client!.sendMessage(roomId, {
"msgtype": "m.text",
"body": "m2",
msgtype: "m.text",
body: "m2",
});
// both events should be in the timeline at this point
@@ -100,20 +91,23 @@ describe("MatrixClient retrying", function() {
expect(ev2.status).toEqual(EventStatus.SENDING);
// the first message should get sent, and the second should get queued
httpBackend!.when("PUT", "/send/m.room.message/").check(function() {
// ev2 should now have been queued
expect(ev2.status).toEqual(EventStatus.QUEUED);
httpBackend!
.when("PUT", "/send/m.room.message/")
.check(function () {
// ev2 should now have been queued
expect(ev2.status).toEqual(EventStatus.QUEUED);
// now we can cancel the second and check everything looks sane
client!.cancelPendingEvent(ev2);
expect(ev2.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(1);
// now we can cancel the second and check everything looks sane
client!.cancelPendingEvent(ev2);
expect(ev2.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(1);
// shouldn't be able to cancel the first message yet
expect(function() {
client!.cancelPendingEvent(ev1);
}).toThrow();
}).respond(400); // fail the first message
// shouldn't be able to cancel the first message yet
expect(function () {
client!.cancelPendingEvent(ev1);
}).toThrow();
})
.respond(400); // fail the first message
// wait for the localecho of ev1 to be updated
const p3 = new Promise<void>((resolve, reject) => {
@@ -122,7 +116,7 @@ describe("MatrixClient retrying", function() {
resolve();
}
});
}).then(function() {
}).then(function () {
expect(ev1.status).toEqual(EventStatus.NOT_SENT);
expect(tl.length).toEqual(1);
@@ -132,19 +126,11 @@ describe("MatrixClient retrying", function() {
expect(tl.length).toEqual(0);
});
return Promise.all([
p1,
p3,
httpBackend!.flushAllExpected(),
]);
return Promise.all([p1, p3, httpBackend!.flushAllExpected()]);
});
describe("resending", function() {
xit("should be able to resend a NOT_SENT event", function() {
});
xit("should be able to resend a sent event", function() {
});
describe("resending", function () {
it.skip("should be able to resend a NOT_SENT event", function () {});
it.skip("should be able to resend a sent event", function () {});
});
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,383 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import "fake-indexeddb/auto";
import HttpBackend from "matrix-mock-request";
import {
Category,
ClientEvent,
EventType,
ISyncResponse,
MatrixClient,
MatrixEvent,
NotificationCountType,
RelationType,
Room,
} from "../../src";
import { TestClient } from "../TestClient";
import { ReceiptType } from "../../src/@types/read_receipts";
import { mkThread } from "../test-utils/thread";
import { SyncState } from "../../src/sync";
describe("MatrixClient syncing", () => {
const userA = "@alice:localhost";
const userB = "@bob:localhost";
const selfUserId = userA;
const selfAccessToken = "aseukfgwef";
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
const setupTestClient = (): [MatrixClient, HttpBackend] => {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
const httpBackend = testClient.httpBackend;
const client = testClient.client;
httpBackend!.when("GET", "/versions").respond(200, {});
httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
return [client, httpBackend];
};
beforeEach(() => {
[client, httpBackend] = setupTestClient();
});
afterEach(() => {
httpBackend!.verifyNoOutstandingExpectation();
client!.stopClient();
return httpBackend!.stop();
});
it("reactions in thread set the correct timeline to unread", async () => {
const roomId = "!room:localhost";
// start the client, and wait for it to initialise
httpBackend!.when("GET", "/sync").respond(200, {
next_batch: "s_5_3",
rooms: {
[Category.Join]: {},
[Category.Leave]: {},
[Category.Invite]: {},
},
});
client!.startClient({ threadSupport: true });
await Promise.all([
httpBackend?.flushAllExpected(),
new Promise<void>((resolve) => {
client!.on(ClientEvent.Sync, (state) => state === SyncState.Syncing && resolve());
}),
]);
const room = new Room(roomId, client!, selfUserId);
jest.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
const threadReply = thread.events.at(-1)!;
await room.addLiveEvents([thread.rootEvent]);
// Initialize read receipt datastructure before testing the reaction
room.addReceiptToStructure(thread.rootEvent.getId()!, ReceiptType.Read, selfUserId, { ts: 1 }, false);
thread.thread.addReceiptToStructure(
threadReply.getId()!,
ReceiptType.Read,
selfUserId,
{ thread_id: thread.thread.id, ts: 1 },
false,
);
expect(room.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(thread.rootEvent.getId());
expect(thread.thread.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(threadReply.getId());
const reactionEventId = `$9-${Math.random()}-${Math.random()}`;
let lastEvent: MatrixEvent | null = null;
jest.spyOn(client! as any, "sendEventHttpRequest").mockImplementation((event) => {
lastEvent = event as MatrixEvent;
return { event_id: reactionEventId };
});
await client!.sendEvent(roomId, EventType.Reaction, {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: threadReply.getId(),
key: "",
},
});
expect(lastEvent!.getId()).toEqual(reactionEventId);
room.handleRemoteEcho(new MatrixEvent(lastEvent!.event), lastEvent!);
// Our ideal state after this is the following:
//
// Room: [synthetic: threadroot, actual: threadroot]
// Thread: [synthetic: threadreaction, actual: threadreply]
//
// The reaction and reply are both in the thread, and their receipts should be isolated to the thread.
// The reaction has not been acknowledged in a dedicated read receipt message, so only the synthetic receipt
// should be updated.
// Ensure the synthetic receipt for the room has not been updated
expect(room.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(thread.rootEvent.getId());
expect(room.getEventReadUpTo(selfUserId, false)).toEqual(thread.rootEvent.getId());
// Ensure the actual receipt for the room has not been updated
expect(room.getReadReceiptForUserId(selfUserId, true)?.eventId).toEqual(thread.rootEvent.getId());
expect(room.getEventReadUpTo(selfUserId, true)).toEqual(thread.rootEvent.getId());
// Ensure the synthetic receipt for the thread has been updated
expect(thread.thread.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(reactionEventId);
expect(thread.thread.getEventReadUpTo(selfUserId, false)).toEqual(reactionEventId);
// Ensure the actual receipt for the thread has not been updated
expect(thread.thread.getReadReceiptForUserId(selfUserId, true)?.eventId).toEqual(threadReply.getId());
expect(thread.thread.getEventReadUpTo(selfUserId, true)).toEqual(threadReply.getId());
});
describe("Stuck unread notifications integration tests", () => {
const ROOM_ID = "!room:localhost";
const syncData = getSampleStuckNotificationSyncResponse(ROOM_ID);
it("resets notifications if the last event originates from the logged in user", async () => {
httpBackend!
.when("GET", "/sync")
.check((req) => {
expect(req.queryParams!.filter).toEqual("a filter id");
})
.respond(200, syncData);
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
client!.startClient({ initialSyncLimit: 1 });
await httpBackend!.flushAllExpected();
const room = client?.getRoom(ROOM_ID);
expect(room).toBeInstanceOf(Room);
expect(room?.getUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
});
});
function getSampleStuckNotificationSyncResponse(roomId: string): Partial<ISyncResponse> {
return {
next_batch: "batch_token",
rooms: {
[Category.Join]: {
[roomId]: {
timeline: {
events: [
{
content: {
room_version: "9",
},
origin_server_ts: 1,
sender: userB,
state_key: "",
type: "m.room.create",
event_id: "$event1",
},
{
content: {
avatar_url: "",
displayname: userB,
membership: "join",
},
origin_server_ts: 2,
sender: userB,
state_key: userB,
type: "m.room.member",
event_id: "$event2",
},
{
content: {
ban: 50,
events: {
"m.room.avatar": 50,
"m.room.canonical_alias": 50,
"m.room.encryption": 100,
"m.room.history_visibility": 100,
"m.room.name": 50,
"m.room.power_levels": 100,
"m.room.server_acl": 100,
"m.room.tombstone": 100,
},
events_default: 0,
historical: 100,
invite: 0,
kick: 50,
redact: 50,
state_default: 50,
users: {
[userA]: 100,
[userB]: 100,
},
users_default: 0,
},
origin_server_ts: 3,
sender: userB,
state_key: "",
type: "m.room.power_levels",
event_id: "$event3",
},
{
content: {
join_rule: "invite",
},
origin_server_ts: 4,
sender: userB,
state_key: "",
type: "m.room.join_rules",
event_id: "$event4",
},
{
content: {
history_visibility: "shared",
},
origin_server_ts: 5,
sender: userB,
state_key: "",
type: "m.room.history_visibility",
event_id: "$event5",
},
{
content: {
guest_access: "can_join",
},
origin_server_ts: 6,
sender: userB,
state_key: "",
type: "m.room.guest_access",
unsigned: {
age: 1651569,
},
event_id: "$event6",
},
{
content: {
algorithm: "m.megolm.v1.aes-sha2",
},
origin_server_ts: 7,
sender: userB,
state_key: "",
type: "m.room.encryption",
event_id: "$event7",
},
{
content: {
avatar_url: "",
displayname: userA,
is_direct: true,
membership: "invite",
},
origin_server_ts: 8,
sender: userB,
state_key: userA,
type: "m.room.member",
event_id: "$event8",
},
{
content: {
msgtype: "m.text",
body: "hello",
},
origin_server_ts: 9,
sender: userB,
type: "m.room.message",
event_id: "$event9",
},
{
content: {
avatar_url: "",
displayname: userA,
membership: "join",
},
origin_server_ts: 10,
sender: userA,
state_key: userA,
type: "m.room.member",
event_id: "$event10",
},
{
content: {
msgtype: "m.text",
body: "world",
},
origin_server_ts: 11,
sender: userA,
type: "m.room.message",
event_id: "$event11",
},
],
prev_batch: "123",
limited: false,
},
state: {
events: [],
},
account_data: {
events: [
{
type: "m.fully_read",
content: {
event_id: "$dER5V1RCMxzAhHXQJoMjqyuoxpPtK2X6hCb9T8Jg2wU",
},
},
],
},
ephemeral: {
events: [
{
type: "m.receipt",
content: {
$event9: {
"m.read": {
[userA]: {
ts: 100,
},
},
"m.read.private": {
[userA]: {
ts: 100,
},
},
},
dER5V1RCMxzAhHXQJoMjqyuoxpPtK2X6hCb9T8Jg2wU: {
"m.read": {
[userB]: {
ts: 666,
},
},
},
},
},
],
},
unread_notifications: {
notification_count: 1,
highlight_count: 0,
},
summary: {
"m.joined_member_count": 2,
"m.invited_member_count": 0,
"m.heroes": [userB],
},
},
},
[Category.Leave]: {},
[Category.Invite]: {},
[Category.Knock]: {},
},
};
}
});
-170
View File
@@ -1,170 +0,0 @@
/*
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 { Account } from "@matrix-org/olm";
import { logger } from "../../src/logger";
import { decodeRecoveryKey } from "../../src/crypto/recoverykey";
import { IKeyBackupInfo, IKeyBackupSession } from "../../src/crypto/keybackup";
import { TestClient } from "../TestClient";
import { IEvent } from "../../src";
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
const ROOM_ID = '!ROOM:ID';
const SESSION_ID = 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc';
const ENCRYPTED_EVENT: Partial<IEvent> = {
type: 'm.room.encrypted',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: 'SENDER_CURVE25519',
session_id: SESSION_ID,
ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N'
+ 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl'
+ 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs',
},
room_id: '!ROOM:ID',
event_id: '$event1',
origin_server_ts: 1507753886000,
};
const CURVE25519_KEY_BACKUP_DATA: IKeyBackupSession = {
first_message_index: 0,
forwarded_count: 0,
is_verified: false,
session_data: {
ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw'
+ '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ'
+ 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9'
+ 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy'
+ 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF'
+ 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV'
+ '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv'
+ 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe'
+ 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf'
+ 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy'
+ 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg',
mac: '5lxYBHQU80M',
ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14',
},
};
const CURVE25519_BACKUP_INFO: IKeyBackupInfo = {
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
version: "1",
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
};
const RECOVERY_KEY = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
/**
* start an Olm session with a given recipient
*/
function createOlmSession(olmAccount: Olm.Account, recipientTestClient: TestClient): Promise<Olm.Session> {
return recipientTestClient.awaitOneTimeKeyUpload().then((keys) => {
const otkId = Object.keys(keys)[0];
const otk = keys[otkId];
const session = new global.Olm.Session();
session.create_outbound(
olmAccount, recipientTestClient.getDeviceKey(), otk.key,
);
return session;
});
}
describe("megolm key backups", function() {
if (!global.Olm) {
logger.warn('not running megolm tests: Olm not present');
return;
}
const Olm = global.Olm;
let testOlmAccount: Olm.Account;
let aliceTestClient: TestClient;
const setupTestClient = (): [Account, TestClient] => {
const aliceTestClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs",
);
const testOlmAccount = new Olm.Account();
testOlmAccount!.create();
return [testOlmAccount, aliceTestClient];
};
beforeAll(function() {
return Olm.init();
});
beforeEach(async function() {
[testOlmAccount, aliceTestClient] = setupTestClient();
await aliceTestClient!.client.initCrypto();
aliceTestClient!.client.crypto!.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
});
afterEach(function() {
return aliceTestClient!.stop();
});
it("Alice checks key backups when receiving a message she can't decrypt", function() {
const syncResponse = {
next_batch: 1,
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
timeline: {
events: [ENCRYPTED_EVENT],
},
};
return aliceTestClient!.start().then(() => {
return createOlmSession(testOlmAccount, aliceTestClient);
}).then(() => {
const privkey = decodeRecoveryKey(RECOVERY_KEY);
return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey);
}).then(() => {
aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse);
aliceTestClient!.expectKeyBackupQuery(
ROOM_ID,
SESSION_ID,
200,
CURVE25519_KEY_BACKUP_DATA,
);
return aliceTestClient!.httpBackend.flushAllExpected();
}).then(function(): Promise<MatrixEvent> {
const room = aliceTestClient!.client.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
if (event.getContent()) {
return Promise.resolve(event);
}
return new Promise((resolve, reject) => {
event.once(MatrixEventEvent.Decrypted, (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
}).then((event) => {
expect(event.getContent()).toEqual('testytest');
});
});
});
File diff suppressed because it is too large Load Diff
+436 -161
View File
@@ -20,16 +20,29 @@ import { fail } from "assert";
import { SlidingSync, SlidingSyncEvent, MSC3575RoomData, SlidingSyncState, Extension } from "../../src/sliding-sync";
import { TestClient } from "../TestClient";
import { IRoomEvent, IStateEvent } from "../../src/sync-accumulator";
import { IRoomEvent, IStateEvent } from "../../src";
import {
MatrixClient, MatrixEvent, NotificationCountType, JoinRule, MatrixError,
EventType, IPushRules, PushRuleKind, TweakName, ClientEvent, RoomMemberEvent,
MatrixClient,
MatrixEvent,
NotificationCountType,
JoinRule,
MatrixError,
EventType,
IPushRules,
PushRuleKind,
TweakName,
ClientEvent,
RoomMemberEvent,
RoomEvent,
Room,
IRoomTimelineData,
} from "../../src";
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
import { SyncState } from "../../src/sync";
import { IStoredClientOpts } from "../../src/client";
import { SyncApiOptions, SyncState } from "../../src/sync";
import { IStoredClientOpts } from "../../src";
import { logger } from "../../src/logger";
import { emitPromise } from "../test-utils/test-utils";
import { defer } from "../../src/utils";
describe("SlidingSyncSdk", () => {
let client: MatrixClient | undefined;
@@ -40,10 +53,9 @@ describe("SlidingSyncSdk", () => {
const selfAccessToken = "aseukfgwef";
const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
s.getList = jest.fn();
s.getListParams = jest.fn();
s.getListData = jest.fn();
s.getRoomSubscriptions = jest.fn();
s.listLength = jest.fn();
s.modifyRoomSubscriptionInfo = jest.fn();
s.modifyRoomSubscriptions = jest.fn();
s.registerExtension = jest.fn();
@@ -67,7 +79,7 @@ describe("SlidingSyncSdk", () => {
event_id: "$" + eventIdCounter,
};
};
const mkOwnStateEvent = (evType: string, content: object, stateKey = ''): IStateEvent => {
const mkOwnStateEvent = (evType: string, content: object, stateKey = ""): IStateEvent => {
eventIdCounter++;
return {
type: evType,
@@ -97,19 +109,20 @@ describe("SlidingSyncSdk", () => {
};
// assign client/httpBackend globals
const setupClient = async (testOpts?: Partial<IStoredClientOpts&{withCrypto: boolean}>) => {
const setupClient = async (testOpts?: Partial<IStoredClientOpts & { withCrypto: boolean }>) => {
testOpts = testOpts || {};
const syncOpts: SyncApiOptions = {};
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
httpBackend = testClient.httpBackend;
client = testClient.client;
mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0));
mockSlidingSync = mockifySlidingSync(new SlidingSync("", new Map(), {}, client, 0));
if (testOpts.withCrypto) {
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
await client!.initCrypto();
testOpts.crypto = client!.crypto;
syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto;
}
httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts);
httpBackend!.when("GET", "/_matrix/client/v3/pushrules").respond(200, {});
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts, syncOpts);
};
// tear down client/httpBackend globals
@@ -119,13 +132,13 @@ describe("SlidingSyncSdk", () => {
};
// find an extension on a SlidingSyncSdk instance
const findExtension = (name: string): Extension => {
const findExtension = (name: string): Extension<any, any> => {
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
// find the extension
for (let i = 0; i < mockFn.mock.calls.length; i++) {
const calledExtension = mockFn.mock.calls[i][0] as Extension;
if (calledExtension && calledExtension.name() === name) {
const calledExtension = mockFn.mock.calls[i][0] as Extension<any, any>;
if (calledExtension?.name() === name) {
return calledExtension;
}
}
@@ -141,11 +154,11 @@ describe("SlidingSyncSdk", () => {
const hasSynced = sdk!.sync();
await httpBackend!.flushAllExpected();
await hasSynced;
expect(mockSlidingSync!.start).toBeCalled();
expect(mockSlidingSync!.start).toHaveBeenCalled();
});
it("can stop()", async () => {
sdk!.stop();
expect(mockSlidingSync!.stop).toBeCalled();
expect(mockSlidingSync!.stop).toHaveBeenCalled();
});
});
@@ -170,11 +183,12 @@ describe("SlidingSyncSdk", () => {
const roomE = "!e_with_invite:localhost";
const roomF = "!f_calc_room_name:localhost";
const roomG = "!g_join_invite_counts:localhost";
const roomH = "!g_num_live:localhost";
const data: Record<string, MSC3575RoomData> = {
[roomA]: {
name: "A",
required_state: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomName, { name: "A" }, ""),
@@ -189,12 +203,11 @@ describe("SlidingSyncSdk", () => {
name: "B",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello B" }),
mkOwnEvent(EventType.RoomMessage, { body: "world B" }),
],
initial: true,
},
@@ -202,7 +215,7 @@ describe("SlidingSyncSdk", () => {
name: "C",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello C" }),
@@ -215,7 +228,7 @@ describe("SlidingSyncSdk", () => {
name: "D",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello D" }),
@@ -251,7 +264,7 @@ describe("SlidingSyncSdk", () => {
[roomF]: {
name: "#foo:localhost",
required_state: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomCanonicalAlias, { alias: "#foo:localhost" }, ""),
@@ -267,7 +280,7 @@ describe("SlidingSyncSdk", () => {
name: "G",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
],
@@ -275,74 +288,111 @@ describe("SlidingSyncSdk", () => {
invited_count: 2,
initial: true,
},
[roomH]: {
name: "H",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "live event" }),
],
initial: true,
num_live: 1,
},
};
it("can be created with required_state and timeline", () => {
it("can be created with required_state and timeline", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.name).toEqual(data[roomA].name);
expect(gotRoom.getMyMembership()).toEqual("join");
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.name).toEqual(data[roomA].name);
expect(gotRoom!.getMyMembership()).toEqual("join");
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
});
it("can be created with timeline only", () => {
it("can be created with timeline only", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.name).toEqual(data[roomB].name);
expect(gotRoom.getMyMembership()).toEqual("join");
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.name).toEqual(data[roomB].name);
expect(gotRoom!.getMyMembership()).toEqual("join");
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
});
it("can be created with a highlight_count", () => {
it("can be created with a highlight_count", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomC);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
).toEqual(data[roomC].highlight_count);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(
data[roomC].highlight_count,
);
});
it("can be created with a notification_count", () => {
it("can be created with a notification_count", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomD);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
).toEqual(data[roomD].notification_count);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(
data[roomD].notification_count,
);
});
it("can be created with an invited/joined_count", () => {
it("can be created with an invited/joined_count", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomG);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
expect(gotRoom!.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
});
it("can be created with invite_state", () => {
it("can be created with live events", async () => {
const seenLiveEventDeferred = defer<boolean>();
const listener = (
ev: MatrixEvent,
room?: Room,
toStartOfTimeline?: boolean,
deleted?: boolean,
timelineData?: IRoomTimelineData,
) => {
if (timelineData?.liveEvent) {
assertTimelineEvents([ev], data[roomH].timeline.slice(-1));
seenLiveEventDeferred.resolve(true);
}
};
client!.on(RoomEvent.Timeline, listener);
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomH, data[roomH]);
await emitPromise(client!, ClientEvent.Room);
client!.off(RoomEvent.Timeline, listener);
const gotRoom = client!.getRoom(roomH);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.name).toEqual(data[roomH].name);
expect(gotRoom!.getMyMembership()).toEqual("join");
// check the entire timeline is correct
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents(), data[roomH].timeline);
await expect(seenLiveEventDeferred.promise).resolves.toBeTruthy();
});
it("can be created with invite_state", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomE);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getMyMembership()).toEqual("invite");
expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.getMyMembership()).toEqual("invite");
expect(gotRoom!.currentState.getJoinRule()).toEqual(JoinRule.Invite);
});
it("uses the 'name' field to caluclate the room name", () => {
it("uses the 'name' field to caluclate the room name", async () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomF);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.name,
).toEqual(data[roomF].name);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.name).toEqual(data[roomF].name);
});
describe("updating", () => {
@@ -354,29 +404,33 @@ describe("SlidingSyncSdk", () => {
name: data[roomA].name,
});
const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom).toBeTruthy();
if (gotRoom == null) {
return;
}
const newTimeline = data[roomA].timeline;
newTimeline.push(newEvent);
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline);
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-3), newTimeline);
});
it("can update with a new required_state event", async () => {
let gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
expect(gotRoom).toBeTruthy();
if (gotRoom == null) {
return;
}
expect(gotRoom!.getJoinRule()).toEqual(JoinRule.Invite); // default
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, {
required_state: [
mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, ""),
],
required_state: [mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, "")],
timeline: [],
name: data[roomB].name,
});
gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
expect(gotRoom).toBeTruthy();
if (gotRoom == null) {
return;
}
expect(gotRoom!.getJoinRule()).toEqual(JoinRule.Restricted);
});
it("can update with a new highlight_count", async () => {
@@ -387,11 +441,11 @@ describe("SlidingSyncSdk", () => {
highlight_count: 1,
});
const gotRoom = client!.getRoom(roomC);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
).toEqual(1);
expect(gotRoom).toBeTruthy();
if (gotRoom == null) {
return;
}
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Highlight)).toEqual(1);
});
it("can update with a new notification_count", async () => {
@@ -402,11 +456,11 @@ describe("SlidingSyncSdk", () => {
notification_count: 1,
});
const gotRoom = client!.getRoom(roomD);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
).toEqual(1);
expect(gotRoom).toBeTruthy();
if (gotRoom == null) {
return;
}
expect(gotRoom!.getUnreadNotificationCount(NotificationCountType.Total)).toEqual(1);
});
it("can update with a new joined_count", () => {
@@ -417,9 +471,11 @@ describe("SlidingSyncSdk", () => {
joined_count: 1,
});
const gotRoom = client!.getRoom(roomG);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinedMemberCount()).toEqual(1);
expect(gotRoom).toBeTruthy();
if (gotRoom == null) {
return;
}
expect(gotRoom!.getJoinedMemberCount()).toEqual(1);
});
// Regression test for a bug which caused the timeline entries to be out-of-order
@@ -441,16 +497,25 @@ describe("SlidingSyncSdk", () => {
initial: true, // e.g requested via room subscription
});
const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom).toBeTruthy();
if (gotRoom == null) {
return;
}
logger.log("want:", oldTimeline.map((e) => (e.type + " : " + (e.content || {}).body)));
logger.log("got:", gotRoom.getLiveTimeline().getEvents().map(
(e) => (e.getType() + " : " + e.getContent().body)),
logger.log(
"want:",
oldTimeline.map((e) => e.type + " : " + (e.content || {}).body),
);
logger.log(
"got:",
gotRoom
.getLiveTimeline()
.getEvents()
.map((e) => e.getType() + " : " + e.getContent().body),
);
// we expect the timeline now to be oldTimeline (so the old events are in fact old)
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents(), oldTimeline);
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents(), oldTimeline);
});
});
});
@@ -466,42 +531,56 @@ describe("SlidingSyncSdk", () => {
const FAILED_SYNC_ERROR_THRESHOLD = 3; // would be nice to export the const in the actual class...
it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => {
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete,
{ pos: "h", lists: [], rooms: {}, extensions: {} },
);
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, {
pos: "h",
lists: {},
rooms: {},
extensions: {},
});
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
SlidingSyncEvent.Lifecycle,
SlidingSyncState.RequestFinished,
null,
new Error("generic"),
);
expect(sdk!.getSyncState()).toEqual(SyncState.Reconnecting);
for (let i = 0; i < FAILED_SYNC_ERROR_THRESHOLD; i++) {
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
SlidingSyncEvent.Lifecycle,
SlidingSyncState.RequestFinished,
null,
new Error("generic"),
);
}
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
});
it("emits SyncState.Syncing after a previous SyncState.Error", async () => {
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle,
SlidingSyncState.Complete,
{ pos: "i", lists: [], rooms: {}, extensions: {} },
);
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, {
pos: "i",
lists: {},
rooms: {},
extensions: {},
});
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
});
it("emits SyncState.Error immediately when receiving M_UNKNOWN_TOKEN and stops syncing", async () => {
expect(mockSlidingSync!.stop).not.toBeCalled();
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({
errcode: "M_UNKNOWN_TOKEN",
message: "Oh no your access token is no longer valid",
}));
expect(mockSlidingSync!.stop).not.toHaveBeenCalled();
mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle,
SlidingSyncState.RequestFinished,
null,
new MatrixError({
errcode: "M_UNKNOWN_TOKEN",
message: "Oh no your access token is no longer valid",
}),
);
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
expect(mockSlidingSync!.stop).toBeCalled();
expect(mockSlidingSync!.stop).toHaveBeenCalled();
});
});
@@ -523,7 +602,7 @@ describe("SlidingSyncSdk", () => {
name: "Room with Invite",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee),
@@ -532,16 +611,17 @@ describe("SlidingSyncSdk", () => {
await httpBackend!.flush("/profile", 1, 1000);
await emitPromise(client!, RoomMemberEvent.Name);
const room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
expect(room).toBeTruthy();
const inviteeMember = room.getMember(invitee)!;
expect(inviteeMember).toBeDefined();
expect(inviteeMember).toBeTruthy();
expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url);
expect(inviteeMember.name).toEqual(inviteeProfile.displayname);
});
});
describe("ExtensionE2EE", () => {
let ext: Extension;
let ext: Extension<any, any>;
beforeAll(async () => {
await setupClient({
withCrypto: true,
@@ -551,56 +631,52 @@ 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", () => {
client!.crypto!.processDeviceLists = jest.fn();
ext.onResponse({
device_lists: {
changed: ["@alice:localhost"],
left: ["@bob:localhost"],
},
});
// TODO: more assertions?
expect(client!.crypto!.processDeviceLists).toHaveBeenCalledWith({
changed: ["@alice:localhost"],
left: ["@bob:localhost"],
});
});
it("can update OTK counts", () => {
client!.crypto!.updateOneTimeKeyCount = jest.fn();
it("can update OTK counts and unused fallback keys", () => {
client!.crypto!.processKeyCounts = jest.fn();
ext.onResponse({
device_one_time_keys_count: {
signed_curve25519: 42,
},
});
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(42);
ext.onResponse({
device_one_time_keys_count: {
not_signed_curve25519: 42,
// missing field -> default to 0
},
});
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
});
it("can update fallback keys", () => {
ext.onResponse({
device_unused_fallback_key_types: ["signed_curve25519"],
});
expect(client!.crypto!.getNeedsNewFallback()).toEqual(false);
ext.onResponse({
device_unused_fallback_key_types: ["not_signed_curve25519"],
});
expect(client!.crypto!.getNeedsNewFallback()).toEqual(true);
expect(client!.crypto!.processKeyCounts).toHaveBeenCalledWith({ signed_curve25519: 42 }, [
"signed_curve25519",
]);
});
});
describe("ExtensionAccountData", () => {
let ext: Extension;
let ext: Extension<any, any>;
beforeAll(async () => {
await setupClient();
const hasSynced = sdk!.sync();
@@ -608,12 +684,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 = {
@@ -630,20 +708,20 @@ describe("SlidingSyncSdk", () => {
],
});
globalData = client!.getAccountData(globalType)!;
expect(globalData).toBeDefined();
expect(globalData).toBeTruthy();
expect(globalData.getContent()).toEqual(globalContent);
});
it("processes rooms account data", async () => {
const roomId = "!room:id";
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with account data",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
initial: true,
});
@@ -651,6 +729,7 @@ describe("SlidingSyncSdk", () => {
foo: "bar",
};
const roomType = "test";
await emitPromise(client!, ClientEvent.Room);
ext.onResponse({
rooms: {
[roomId]: [
@@ -662,11 +741,12 @@ describe("SlidingSyncSdk", () => {
},
});
const room = client!.getRoom(roomId)!;
expect(room).toBeDefined();
expect(room).toBeTruthy();
const event = room.getAccountData(roomType)!;
expect(event).toBeDefined();
expect(event).toBeTruthy();
expect(event.getContent()).toEqual(roomContent);
});
it("doesn't crash for unknown room account data", async () => {
const unknownRoomId = "!unknown:id";
const roomType = "tester";
@@ -686,22 +766,25 @@ 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 = {
global: {
[PushRuleKind.RoomSpecific]: [{
enabled: true,
default: true,
pattern: "monkey",
actions: [
{
set_tweak: TweakName.Sound,
value: "default",
},
],
rule_id: roomId,
}],
[PushRuleKind.RoomSpecific]: [
{
enabled: true,
default: true,
pattern: "monkey",
actions: [
{
set_tweak: TweakName.Sound,
value: "default",
},
],
rule_id: roomId,
},
],
},
};
let pushRule = client!.getRoomPushRule("global", roomId);
@@ -718,8 +801,10 @@ describe("SlidingSyncSdk", () => {
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific]![0]);
});
});
describe("ExtensionToDevice", () => {
let ext: Extension;
let ext: Extension<any, any>;
beforeAll(async () => {
await setupClient();
const hasSynced = sdk!.sync();
@@ -727,12 +812,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 +829,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,17 +859,16 @@ describe("SlidingSyncSdk", () => {
});
expect(called).toBe(true);
});
it("can cancel key verification requests", async () => {
const seen: Record<string, boolean> = {};
client!.on(ClientEvent.ToDeviceEvent, (ev) => {
const evType = ev.getType();
expect(seen[evType]).toBeFalsy();
seen[evType] = true;
if (evType === "m.key.verification.start" || evType === "m.key.verification.request") {
expect(ev.isCancelled()).toEqual(true);
} else {
expect(ev.isCancelled()).toEqual(false);
}
expect(ev.isCancelled()).toEqual(
evType === "m.key.verification.start" || evType === "m.key.verification.request",
);
});
ext.onResponse({
next_batch: "45678",
@@ -809,4 +897,191 @@ describe("SlidingSyncSdk", () => {
});
});
});
describe("ExtensionTyping", () => {
let ext: Extension<any, any>;
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, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
initial: true,
});
await emitPromise(client!, ClientEvent.Room);
const room = client!.getRoom(roomId)!;
expect(room).toBeTruthy();
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, {}, ""),
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).toBeTruthy();
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<any, any>;
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, {}, ""),
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,
});
await emitPromise(client!, ClientEvent.Room);
const room = client!.getRoom(roomId)!;
expect(room).toBeTruthy();
expect(room.getReadReceiptForUserId(alice, true)).toBeNull();
ext.onResponse(generateReceiptResponse(alice, roomId, lastEvent.event_id, "m.read", 1234567));
const receipt = room.getReadReceiptForUserId(alice);
expect(receipt).toBeTruthy();
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
});
});
});
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from '../src/logger';
import { logger } from "../src/logger";
// try to load the olm library.
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
global.Olm = require('@matrix-org/olm');
logger.log('loaded libolm');
global.Olm = require("@matrix-org/olm");
logger.log("loaded libolm");
} catch (e) {
logger.warn("unable to run crypto tests: libolm not available");
logger.warn("unable to run crypto tests: libolm not available", e);
}
+7 -1
View File
@@ -16,4 +16,10 @@ limitations under the License.
import DOMException from "domexception";
global.DOMException = DOMException;
global.DOMException = DOMException as typeof global.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;
+164
View File
@@ -0,0 +1,164 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import debugFunc from "debug";
import { Debugger } from "debug";
import fetchMock from "fetch-mock-jest";
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
/** Interface implemented by classes that intercept `/keys/upload` requests from test clients to catch the uploaded keys
*
* Common interface implemented by {@link TestClient} and {@link E2EKeyReceiver}
*/
export interface IE2EKeyReceiver {
/**
* get the uploaded ed25519 device key
*
* @returns base64 device key
*/
getSigningKey(): string;
/**
* get the uploaded curve25519 device key
*
* @returns base64 device key
*/
getDeviceKey(): string;
/**
* Wait for one-time-keys to be uploaded, then return them.
*
* @returns Promise for the one-time keys
*/
awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>>;
}
/** E2EKeyReceiver: An object which intercepts `/keys/uploads` fetches via fetch-mock.
*
* It stashes the uploaded keys for use elsewhere in the tests.
*/
export class E2EKeyReceiver implements IE2EKeyReceiver {
private readonly debug: Debugger;
private deviceKeys: IDeviceKeys | null = null;
private oneTimeKeys: Record<string, IOneTimeKey> = {};
private readonly oneTimeKeysPromise: Promise<void>;
/**
* Construct a new E2EKeyReceiver.
*
* It will immediately register an intercept of `/keys/uploads` requests for the given homeserverUrl.
* Only /upload requests made to this server will be intercepted: this allows a single test to use more than one
* client and have the keys collected separately.
*
* @param homeserverUrl - the Homeserver Url of the client under test.
*/
public constructor(homeserverUrl: string) {
this.debug = debugFunc(`e2e-key-receiver:[${homeserverUrl}]`);
// set up a listener for /keys/upload.
this.oneTimeKeysPromise = new Promise((resolveOneTimeKeys) => {
const listener = (url: string, options: RequestInit) =>
this.onKeyUploadRequest(resolveOneTimeKeys, options);
fetchMock.post(new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(), listener);
});
}
private async onKeyUploadRequest(onOnTimeKeysUploaded: () => void, options: RequestInit): Promise<object> {
const content = JSON.parse(options.body as string);
// device keys may only be uploaded once
if (content.device_keys && Object.keys(content.device_keys).length > 0) {
if (this.deviceKeys) {
throw new Error("Application attempted to upload E2E device keys multiple times");
}
this.debug(`received device keys`);
this.deviceKeys = content.device_keys;
}
if (content.one_time_keys && Object.keys(content.one_time_keys).length > 0) {
// this is a one-time-key upload
// if we already have a batch of one-time keys, then slow-roll the response,
// otherwise the client ends up tight-looping one-time-key-uploads and filling the logs with junk.
if (Object.keys(this.oneTimeKeys).length > 0) {
this.debug(`received second batch of one-time keys: blocking response`);
await new Promise(() => {});
}
this.debug(`received ${Object.keys(content.one_time_keys).length} one-time keys`);
Object.assign(this.oneTimeKeys, content.one_time_keys);
onOnTimeKeysUploaded();
}
return {
one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length,
},
};
}
/** Get the uploaded Ed25519 key
*
* If device keys have not yet been uploaded, throws an error
*/
public getSigningKey(): string {
if (!this.deviceKeys) {
throw new Error("Device keys not yet uploaded");
}
const keyIds = Object.keys(this.deviceKeys.keys).filter((v) => v.startsWith("ed25519:"));
if (keyIds.length != 1) {
throw new Error(`Expected exactly 1 ed25519 key uploaded, got ${keyIds}`);
}
return this.deviceKeys.keys[keyIds[0]];
}
/** Get the uploaded Curve25519 key
*
* If device keys have not yet been uploaded, throws an error
*/
public getDeviceKey(): string {
if (!this.deviceKeys) {
throw new Error("Device keys not yet uploaded");
}
const keyIds = Object.keys(this.deviceKeys.keys).filter((v) => v.startsWith("curve25519:"));
if (keyIds.length != 1) {
throw new Error(`Expected exactly 1 curve25519 key uploaded, got ${keyIds}`);
}
return this.deviceKeys.keys[keyIds[0]];
}
/**
* If the device keys have already been uploaded, return them. Else return null.
*/
public getUploadedDeviceKeys(): IDeviceKeys | null {
return this.deviceKeys;
}
/**
* If one-time keys have already been uploaded, return them. Otherwise,
* set up an expectation that the keys will be uploaded, and wait for
* that to happen.
*
* @returns Promise for the one-time keys
*/
public async awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
await this.oneTimeKeysPromise;
return this.oneTimeKeys;
}
}
+119
View File
@@ -0,0 +1,119 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import { MapWithDefault } from "../../src/utils";
import { IDownloadKeyResult } from "../../src";
import { IDeviceKeys } from "../../src/@types/crypto";
import { E2EKeyReceiver } from "./E2EKeyReceiver";
/**
* An object which intercepts `/keys/query` fetches via fetch-mock.
*/
export class E2EKeyResponder {
private deviceKeysByUserByDevice = new MapWithDefault<string, Map<string, any>>(() => new Map());
private e2eKeyReceiversByUser = new Map<string, E2EKeyReceiver>();
private masterKeysByUser: Record<string, any> = {};
private selfSigningKeysByUser: Record<string, any> = {};
private userSigningKeysByUser: Record<string, any> = {};
/**
* Construct a new E2EKeyResponder.
*
* It will immediately register an intercept of `/keys/query` requests for the given homeserverUrl.
* Only /query requests made to this server will be intercepted: this allows a single test to use more than one
* client and have the keys collected separately.
*
* @param homeserverUrl - the Homeserver Url of the client under test.
*/
public constructor(homeserverUrl: string) {
// set up a listener for /keys/query.
const listener = (url: string, options: RequestInit) => this.onKeyQueryRequest(options);
fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), listener);
}
private onKeyQueryRequest(options: RequestInit) {
const content = JSON.parse(options.body as string);
const usersToReturn = Object.keys(content["device_keys"]);
const response = {
device_keys: {} as { [userId: string]: any },
master_keys: {} as { [userId: string]: any },
self_signing_keys: {} as { [userId: string]: any },
user_signing_keys: {} as { [userId: string]: any },
failures: {} as { [serverName: string]: any },
};
for (const user of usersToReturn) {
const userKeys = this.deviceKeysByUserByDevice.get(user);
if (userKeys !== undefined) {
response.device_keys[user] = Object.fromEntries(userKeys.entries());
}
const e2eKeyReceiver = this.e2eKeyReceiversByUser.get(user);
if (e2eKeyReceiver !== undefined) {
const deviceKeys = e2eKeyReceiver.getUploadedDeviceKeys();
if (deviceKeys !== null) {
response.device_keys[user] ??= {};
response.device_keys[user][deviceKeys.device_id] = deviceKeys;
}
}
if (this.masterKeysByUser.hasOwnProperty(user)) {
response.master_keys[user] = this.masterKeysByUser[user];
}
if (this.selfSigningKeysByUser.hasOwnProperty(user)) {
response.self_signing_keys[user] = this.selfSigningKeysByUser[user];
}
if (this.userSigningKeysByUser.hasOwnProperty(user)) {
response.user_signing_keys[user] = this.userSigningKeysByUser[user];
}
}
return response;
}
/**
* Add a set of device keys for return by a future `/keys/query`, as if they had been `/upload`ed
*
* @param keys - device keys for this device.
*/
public addDeviceKeys(keys: IDeviceKeys) {
this.deviceKeysByUserByDevice.getOrCreate(keys.user_id).set(keys.device_id, keys);
}
/** Add a set of cross-signing keys for return by a future `/keys/query`, as if they had been `/keys/device_signing/upload`ed
*
* @param data cross-signing data
*/
public addCrossSigningData(
data: Pick<IDownloadKeyResult, "master_keys" | "self_signing_keys" | "user_signing_keys">,
) {
Object.assign(this.masterKeysByUser, data.master_keys);
Object.assign(this.selfSigningKeysByUser, data.self_signing_keys);
Object.assign(this.userSigningKeysByUser, data.user_signing_keys);
}
/**
* Add an E2EKeyReceiver to poll for uploaded keys
*
* Any keys which have been uploaded to the given `E2EKeyReceiver` at the time of the `/keys/query` request will
* be added to the response.
*
* @param e2eKeyReceiver
*/
public addKeyReceiver(userId: string, e2eKeyReceiver: E2EKeyReceiver) {
this.e2eKeyReceiversByUser.set(userId, e2eKeyReceiver);
}
}
+131
View File
@@ -0,0 +1,131 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import debugFunc from "debug";
import { Debugger } from "debug";
import fetchMock from "fetch-mock-jest";
import { MockResponse } from "fetch-mock";
/** Interface implemented by classes that intercept `/sync` requests from test clients
*
* Common interface implemented by {@link TestClient} and {@link SyncResponder}
*/
export interface ISyncResponder {
/** Next time we see a sync request (or immediately, if there is one waiting), send the given response
*
* @param response - response to /sync request
*/
sendOrQueueSyncResponse(response: object): void;
}
enum SyncResponderState {
IDLE,
WAITING_FOR_REQUEST,
WAITING_FOR_RESPONSE,
}
/** SyncResponder: An object which intercepts `/sync` fetches via fetch-mock.
*
* Two modes are possible:
* * A response can be queued up; the next call to `/sync` will return it.
* * If a call to `/sync` arrives before a response is queued, it will block until a call to {@link #sendOrQueueSyncResponse}.
*/
export class SyncResponder implements ISyncResponder {
private readonly debug: Debugger;
private state: SyncResponderState = SyncResponderState.IDLE;
/*
* properties that are only valid in WAITING_FOR_REQUEST
*/
/** the response to be sent when the request is made */
private pendingResponse: object | null = null;
/*
* properties that are only valid in WAITING_FOR_RESPONSE
*/
/** a callback to be called with a response once one is registered.
*
* It will release the /sync request and update the state.
*/
private onResponseReceived: ((response: object) => void) | null = null;
/**
* Construct a new SyncResponder.
*
* It will immediately register an intercept of `/sync` requests for the given homeserverUrl.
* Only /sync requests made to this server will be intercepted: this allows a single test to use more than one
* client and have overlapping /sync requests.
*
* @param homeserverUrl - the Homeserver Url of the client under test.
*/
public constructor(homeserverUrl: string) {
this.debug = debugFunc(`sync-responder:[${homeserverUrl}]`);
fetchMock.get("begin:" + new URL("/_matrix/client/v3/sync?", homeserverUrl).toString(), (_url, _options) =>
this.onSyncRequest(),
);
}
private async onSyncRequest(): Promise<MockResponse> {
switch (this.state) {
case SyncResponderState.IDLE: {
this.debug("Got /sync request: waiting for response to be ready");
const res = await new Promise<object>((resolve) => {
this.onResponseReceived = resolve;
this.state = SyncResponderState.WAITING_FOR_RESPONSE;
});
this.debug("Responding to /sync");
this.state = SyncResponderState.IDLE;
this.onResponseReceived = null;
return res;
}
case SyncResponderState.WAITING_FOR_REQUEST: {
this.debug("Got /sync request: responding immediately with queued response");
const res = this.pendingResponse!;
this.state = SyncResponderState.IDLE;
this.pendingResponse = null;
return res;
}
default:
// we must already be in WAITING_FOR_RESPONSE, ie we already have a /sync request in progress
throw new Error(`Got unexpected /sync request in state ${this.state}`);
}
}
/** Next time we see a sync request (or immediately, if there is one waiting), send the given response
*
* @param response - response to /sync request
*/
public sendOrQueueSyncResponse(response: object): void {
switch (this.state) {
case SyncResponderState.IDLE:
this.pendingResponse = response;
this.state = SyncResponderState.WAITING_FOR_REQUEST;
break;
case SyncResponderState.WAITING_FOR_RESPONSE:
this.onResponseReceived!(response);
break;
default:
// we already have a response queued
throw new Error(`Cannot queue more than one /sync response`);
}
}
}
+12 -21
View File
@@ -17,10 +17,7 @@ limitations under the License.
import { MatrixEvent } from "../../src";
import { M_BEACON, M_BEACON_INFO } from "../../src/@types/beacon";
import { LocationAssetType } from "../../src/@types/location";
import {
makeBeaconContent,
makeBeaconInfoContent,
} from "../../src/content-helpers";
import { makeBeaconContent, makeBeaconInfoContent } from "../../src/content-helpers";
type InfoContentProps = {
timeout: number;
@@ -44,13 +41,7 @@ export const makeBeaconInfoEvent = (
contentProps: Partial<InfoContentProps> = {},
eventId?: string,
): MatrixEvent => {
const {
timeout,
isLive,
description,
assetType,
timestamp,
} = {
const { timeout, isLive, description, assetType, timestamp } = {
...DEFAULT_INFO_CONTENT_PROPS,
...contentProps,
};
@@ -77,9 +68,9 @@ type ContentProps = {
description?: string;
};
const DEFAULT_CONTENT_PROPS: ContentProps = {
uri: 'geo:-36.24484561954707,175.46884959563613;u=10',
uri: "geo:-36.24484561954707,175.46884959563613;u=10",
timestamp: 123,
beaconInfoId: '$123',
beaconInfoId: "$123",
};
/**
@@ -87,10 +78,7 @@ const DEFAULT_CONTENT_PROPS: ContentProps = {
* all required properties are mocked
* override with contentProps
*/
export const makeBeaconEvent = (
sender: string,
contentProps: Partial<ContentProps> = {},
): MatrixEvent => {
export const makeBeaconEvent = (sender: string, contentProps: Partial<ContentProps> = {}): MatrixEvent => {
const { uri, timestamp, beaconInfoId, description } = {
...DEFAULT_CONTENT_PROPS,
...contentProps,
@@ -107,10 +95,13 @@ export const makeBeaconEvent = (
* Create a mock geolocation position
* defaults all required properties
*/
export const makeGeolocationPosition = (
{ timestamp, coords }:
{ timestamp?: number, coords: Partial<GeolocationCoordinates> },
): GeolocationPosition => ({
export const makeGeolocationPosition = ({
timestamp,
coords,
}: {
timestamp?: number;
coords: Partial<GeolocationCoordinates>;
}): GeolocationPosition => ({
timestamp: timestamp ?? 1647256791840,
coords: {
accuracy: 1,
+3 -4
View File
@@ -58,11 +58,12 @@ export const getMockClientWithEventEmitter = (
});
* ```
*/
export const mockClientMethodsUser = (userId = '@alice:domain') => ({
export const mockClientMethodsUser = (userId = "@alice:domain") => ({
getUserId: jest.fn().mockReturnValue(userId),
getSafeUserId: jest.fn().mockReturnValue(userId),
getUser: jest.fn().mockReturnValue(new User(userId)),
isGuest: jest.fn().mockReturnValue(false),
mxcUrlToHttp: jest.fn().mockReturnValue('mock-mxcUrlToHttp'),
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
credentials: { userId },
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
getAccessToken: jest.fn(),
@@ -85,10 +86,8 @@ export const mockClientMethodsEvents = () => ({
* Returns basic mocked client methods related to server support
*/
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
doesServerSupportSeparateAddAndBind: jest.fn(),
getIdentityServerUrl: jest.fn(),
getHomeserverUrl: jest.fn(),
getCapabilities: jest.fn().mockReturnValue({}),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
});
+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);
});
}
+92
View File
@@ -0,0 +1,92 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import { KeyBackupInfo } from "../../src/crypto-api";
/**
* Mock out the endpoints that the js-sdk calls when we call `MatrixClient.start()`.
*
* @param homeserverUrl - the homeserver url for the client under test
*/
export function mockInitialApiRequests(homeserverUrl: string) {
fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["v1.1"] });
fetchMock.getOnce(new URL("/_matrix/client/v3/pushrules/", homeserverUrl).toString(), {});
fetchMock.postOnce(new URL("/_matrix/client/v3/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(), {
filter_id: "fid",
});
}
/**
* Mock the requests needed to set up cross signing
*
* Return 404 error for `GET _matrix/client/v3/user/:userId/account_data/:type` request
* Return `{}` for `POST _matrix/client/v3/keys/signatures/upload` request (named `upload-sigs` for fetchMock check)
* Return `{}` for `POST /_matrix/client/(unstable|v3)/keys/device_signing/upload` request (named `upload-keys` for fetchMock check)
*/
export function mockSetupCrossSigningRequests(): void {
// have account_data requests return an empty object
fetchMock.get("express:/_matrix/client/v3/user/:userId/account_data/:type", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "Account data not found." },
});
// we expect a request to upload signatures for our device ...
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
// ... and one to upload the cross-signing keys (with UIA)
fetchMock.post(
// legacy crypto uses /unstable/; /v3/ is correct
{
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
name: "upload-keys",
},
{},
);
}
/**
* Mock out requests to `/room_keys/version`.
*
* Returns `404 M_NOT_FOUND` for GET requests until `POST room_keys/version` is called.
* Once the POST is done, `GET /room_keys/version` will return the posted backup
* instead of 404.
*
* @param backupVersion - The backup version that will be returned by `POST room_keys/version`.
*/
export function mockSetupMegolmBackupRequests(backupVersion: string): void {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No current backup version",
},
});
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (url, request) => {
const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}");
backupData.version = backupVersion;
backupData.count = 0;
backupData.etag = "zer";
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupData, {
overwriteRoutes: true,
});
return {
version: backupVersion,
};
});
}
+53
View File
@@ -0,0 +1,53 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { OidcClientConfig } from "../../src";
import { ValidatedIssuerMetadata } from "../../src/oidc/validate";
/**
* Makes a valid OidcClientConfig with minimum valid values
* @param issuer used as the base for all other urls
* @returns OidcClientConfig
*/
export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClientConfig => {
const metadata = mockOpenIdConfiguration(issuer);
return {
issuer,
account: issuer + "account",
registrationEndpoint: metadata.registration_endpoint,
authorizationEndpoint: metadata.authorization_endpoint,
tokenEndpoint: metadata.token_endpoint,
metadata,
};
};
/**
* Useful for mocking <issuer>/.well-known/openid-configuration
* @param issuer used as the base for all other urls
* @returns ValidatedIssuerMetadata
*/
export const mockOpenIdConfiguration = (issuer = "https://auth.org/"): ValidatedIssuerMetadata => ({
issuer,
revocation_endpoint: issuer + "revoke",
token_endpoint: issuer + "token",
authorization_endpoint: issuer + "auth",
registration_endpoint: issuer + "registration",
jwks_uri: issuer + "jwks",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token"],
code_challenge_methods_supported: ["S256"],
});
+1
View File
@@ -0,0 +1 @@
/env
+654
View File
@@ -0,0 +1,654 @@
#!/bin/env python
#
# Copyright 2023 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This file is a Python script to generate test data for crypto tests.
To run it:
python -m venv env
./env/bin/pip install cryptography canonicaljson
./env/bin/python generate-test-data.py > index.ts
"""
import base64
import json
import base58
from canonicaljson import encode_canonical_json
from cryptography.hazmat.primitives.asymmetric import ed25519, x25519
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from cryptography.hazmat.primitives import hashes, padding, hmac
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from random import randbytes, seed
ALICE_DATA = {
"TEST_USER_ID": "@alice:localhost",
"TEST_DEVICE_ID": "test_device",
"TEST_ROOM_ID": "!room:id",
# any 32-byte string can be an ed25519 private key.
"TEST_DEVICE_PRIVATE_KEY_BYTES": b"deadbeefdeadbeefdeadbeefdeadbeef",
# any 32-byte string can be an curve25519 private key.
"TEST_DEVICE_CURVE_PRIVATE_KEY_BYTES": b"deadmuledeadmuledeadmuledeadmule",
"MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"doyouspeakwhaaaaaaaaaaaaaaaaaale",
"USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"useruseruseruseruseruseruseruser",
"SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"selfselfselfselfselfselfselfself",
# Private key for secure key backup. There are some sessions encrypted with this key in megolm-backup.spec.ts
"B64_BACKUP_DECRYPTION_KEY": "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=",
"OTK": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw"
}
BOB_DATA = {
"TEST_USER_ID": "@bob:xyz",
"TEST_DEVICE_ID": "bob_device",
"TEST_ROOM_ID": "!room:id",
# any 32-byte string can be an ed25519 private key.
"TEST_DEVICE_PRIVATE_KEY_BYTES": b"Deadbeefdeadbeefdeadbeefdeadbeef",
# any 32-byte string can be an curve25519 private key.
"TEST_DEVICE_CURVE_PRIVATE_KEY_BYTES": b"Deadmuledeadmuledeadmuledeadmule",
"MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Doyouspeakwhaaaaaaaaaaaaaaaaaale",
"USER_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Useruseruseruseruseruseruseruser",
"SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES": b"Selfselfselfselfselfselfselfself",
# Private key for secure key backup. There are some sessions encrypted with this key in megolm-backup.spec.ts
"B64_BACKUP_DECRYPTION_KEY": "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=",
"OTK": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw"
}
def main() -> None:
print(
f"""\
/* Test data for cryptography tests
*
* Do not edit by hand! This file is generated by `./generate-test-data.py`
*/
import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
import {{ IDownloadKeyResult, IEvent }} from "../../../src";
import {{ KeyBackupSession, KeyBackupInfo }} from "../../../src/crypto-api/keybackup";
/* eslint-disable comma-dangle */
// Alice data
{build_test_data(ALICE_DATA)}
// Bob data
{build_test_data(BOB_DATA, "BOB_")}
""",
end="",
)
# Use static seed to have stable random test data upon new generation
seed(10)
def build_test_data(user_data, prefix = "") -> str:
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["TEST_DEVICE_PRIVATE_KEY_BYTES"]
)
device_curve_key = x25519.X25519PrivateKey.from_private_bytes(
user_data["TEST_DEVICE_CURVE_PRIVATE_KEY_BYTES"]
)
b64_public_key = encode_base64(
private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
)
device_data = {
"algorithms": ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
"device_id": user_data["TEST_DEVICE_ID"],
"keys": {
f"curve25519:{user_data['TEST_DEVICE_ID']}": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
f"ed25519:{user_data['TEST_DEVICE_ID']}": b64_public_key,
},
"signatures": {user_data['TEST_USER_ID']: {}},
"user_id": user_data["TEST_USER_ID"],
}
device_data["signatures"][user_data["TEST_USER_ID"]][f"ed25519:{user_data['TEST_DEVICE_ID']}"] = sign_json(
device_data, private_key
)
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
)
b64_master_public_key = encode_base64(
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
)
b64_master_private_key = encode_base64(user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"])
self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
)
b64_self_signing_public_key = encode_base64(
self_signing_private_key.public_key().public_bytes(
Encoding.Raw, PublicFormat.Raw
)
)
b64_self_signing_private_key = encode_base64( user_data["SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES"])
user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["USER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
)
b64_user_signing_public_key = encode_base64(
user_signing_private_key.public_key().public_bytes(
Encoding.Raw, PublicFormat.Raw
)
)
b64_user_signing_private_key = encode_base64(user_data["USER_CROSS_SIGNING_PRIVATE_KEY_BYTES"])
backup_decryption_key = x25519.X25519PrivateKey.from_private_bytes(
base64.b64decode(user_data["B64_BACKUP_DECRYPTION_KEY"])
)
b64_backup_public_key = encode_base64(
backup_decryption_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
)
backup_data = {
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
"version": "1",
"auth_data": {
"public_key": b64_backup_public_key,
},
}
# sign with our device key
sig = sign_json(backup_data["auth_data"], private_key)
backup_data["auth_data"]["signatures"] = {
user_data["TEST_USER_ID"]: {f"ed25519:{user_data['TEST_DEVICE_ID']}": sig}
}
set_of_exported_room_keys = [build_exported_megolm_key(device_curve_key)[0], build_exported_megolm_key(device_curve_key)[0]]
additional_exported_room_key, additional_exported_ed_key = build_exported_megolm_key(device_curve_key)
ratcheted_exported_room_key = symetric_ratchet_step_of_megolm_key(additional_exported_room_key, additional_exported_ed_key)
otk_to_sign = {
"key": user_data['OTK']
}
# sign our public otk key with our device key
otk = sign_json(otk_to_sign, private_key)
otks = {
user_data["TEST_USER_ID"]: {
user_data['TEST_DEVICE_ID']: {
"signed_curve25519:AAAAHQ": {
"key": user_data["OTK"],
"signatures": {
user_data["TEST_USER_ID"]: {f"ed25519:{user_data['TEST_DEVICE_ID']}": otk}
}
}
}
}
}
backed_up_room_key = encrypt_megolm_key_for_backup(additional_exported_room_key, backup_decryption_key.public_key())
clear_event, encrypted_event = generate_encrypted_event_content(additional_exported_room_key, additional_exported_ed_key, device_curve_key)
backup_recovery_key = export_recovery_key(user_data["B64_BACKUP_DECRYPTION_KEY"])
return f"""\
export const {prefix}TEST_USER_ID = "{user_data['TEST_USER_ID']}";
export const {prefix}TEST_DEVICE_ID = "{user_data['TEST_DEVICE_ID']}";
export const {prefix}TEST_ROOM_ID = "{user_data['TEST_ROOM_ID']}";
/** The base64-encoded public ed25519 key for this device */
export const {prefix}TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "{b64_public_key}";
/** Signed device data, suitable for returning from a `/keys/query` call */
export const {prefix}SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {json.dumps(device_data, indent=4)};
/** base64-encoded public master cross-signing key */
export const {prefix}MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_master_public_key}";
/** base64-encoded private master cross-signing key */
export const {prefix}MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_master_private_key}";
/** base64-encoded public self cross-signing key */
export const {prefix}SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_self_signing_public_key}";
/** base64-encoded private self signing cross-signing key */
export const {prefix}SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_self_signing_private_key}";
/** base64-encoded public user cross-signing key */
export const {prefix}USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_user_signing_public_key}";
/** base64-encoded private user signing cross-signing key */
export const {prefix}USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "{b64_user_signing_private_key}";
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
json.dumps(build_cross_signing_keys_data(user_data), indent=4)
};
/** Signed OTKs, returned by `POST /keys/claim` */
export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) };
/** base64-encoded backup decryption (private) key */
export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }";
/** Backup decryption key in export format */
export const {prefix}BACKUP_DECRYPTION_KEY_BASE58 = "{ backup_recovery_key }";
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
export const {prefix}MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = {
json.dumps(set_of_exported_room_keys, indent=4)
};
/** An exported megolm session */
export const {prefix}MEGOLM_SESSION_DATA: IMegolmSessionData = {
json.dumps(additional_exported_room_key, indent=4)
};
/** A ratcheted version of {prefix}MEGOLM_SESSION_DATA */
export const {prefix}RATCHTED_MEGOLM_SESSION_DATA: IMegolmSessionData = {
json.dumps(ratcheted_exported_room_key, indent=4)
};
/** The key from {prefix}MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/
export const {prefix}CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {json.dumps(backed_up_room_key, indent=4)};
/** A test clear event */
export const {prefix}CLEAR_EVENT: Partial<IEvent> = {json.dumps(clear_event, indent=4)};
/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */
export const {prefix}ENCRYPTED_EVENT: Partial<IEvent> = {json.dumps(encrypted_event, indent=4)};
"""
def build_cross_signing_keys_data(user_data) -> dict:
"""Build the signed cross-signing-keys data for return from /keys/query"""
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
)
b64_master_public_key = encode_base64(
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
)
self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
)
b64_self_signing_public_key = encode_base64(
self_signing_private_key.public_key().public_bytes(
Encoding.Raw, PublicFormat.Raw
)
)
user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
user_data["USER_CROSS_SIGNING_PRIVATE_KEY_BYTES"]
)
b64_user_signing_public_key = encode_base64(
user_signing_private_key.public_key().public_bytes(
Encoding.Raw, PublicFormat.Raw
)
)
# create without signatures initially
cross_signing_keys_data = {
"master_keys": {
user_data["TEST_USER_ID"]: {
"keys": {
f"ed25519:{b64_master_public_key}": b64_master_public_key,
},
"user_id": user_data["TEST_USER_ID"],
"usage": ["master"],
}
},
"self_signing_keys": {
user_data["TEST_USER_ID"]: {
"keys": {
f"ed25519:{b64_self_signing_public_key}": b64_self_signing_public_key,
},
"user_id": user_data["TEST_USER_ID"],
"usage": ["self_signing"],
},
},
"user_signing_keys": {
user_data["TEST_USER_ID"]: {
"keys": {
f"ed25519:{b64_user_signing_public_key}": b64_user_signing_public_key,
},
"user_id": user_data["TEST_USER_ID"],
"usage": ["user_signing"],
},
},
}
# sign the sub-keys with the master
for k in ["self_signing_keys", "user_signing_keys"]:
to_sign = cross_signing_keys_data[k][user_data["TEST_USER_ID"]]
sig = sign_json(to_sign, master_private_key)
to_sign["signatures"] = {
user_data["TEST_USER_ID"]: {f"ed25519:{b64_master_public_key}": sig}
}
return cross_signing_keys_data
def encode_base64(input_bytes: bytes) -> str:
"""Encode with unpadded base64"""
output_bytes = base64.b64encode(input_bytes)
output_string = output_bytes.decode("ascii")
return output_string.rstrip("=")
def sign_json(json_object: dict, private_key: ed25519.Ed25519PrivateKey) -> str:
"""
Sign the given json object
Returns the base64-encoded signature of signing `input` following the Matrix
JSON signature algorithm [1]
[1]: https://spec.matrix.org/v1.7/appendices/#signing-details
"""
signatures = json_object.pop("signatures", {})
unsigned = json_object.pop("unsigned", None)
signature = private_key.sign(encode_canonical_json(json_object))
signature_base64 = encode_base64(signature)
json_object["signatures"] = signatures
if unsigned is not None:
json_object["unsigned"] = unsigned
return signature_base64
def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tuple[dict, ed25519.Ed25519PrivateKey]:
"""
Creates an exported megolm room key, as per https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md#session-export-format
that can be imported via importRoomKeys API.
Returns the exported key, the matching privat edKey (needed to encrypt)
"""
index = 0
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32))
# Just use radom bytes for the ratchet parts
ratchet = randbytes(32 * 4)
# exported key, start with version byte
exported_key = bytearray(b'\x01')
exported_key += index.to_bytes(4, 'big')
exported_key += ratchet
# KPub
exported_key += private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
megolm_export = {
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": encode_base64(
device_curve_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
),
"session_id": encode_base64(
private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
),
"session_key": encode_base64(exported_key),
"sender_claimed_keys": {
"ed25519": encode_base64(ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
},
"forwarding_curve25519_key_chain": [],
}
return megolm_export, private_key
def symetric_ratchet_step_of_megolm_key(previous: dict , megolm_private_key: ed25519.Ed25519PrivateKey) -> dict:
"""
Very simple ratchet step from 0 to 1
Used to generate a ratcheted key to test unknown message index.
"""
session_key: str = previous["session_key"]
# Get the megolm R0 from the export format
decoded = base64.b64decode(session_key.encode("ascii"))
ri = decoded[5:133]
ri0 = ri[0:32]
ri1 = ri[32:64]
ri2 = ri[64:96]
ri3 = ri[96:128]
h = hmac.HMAC(ri3, hashes.SHA256())
h.update(b'x\03')
ri1_3 = h.finalize()
index = 1
private_key = megolm_private_key
# exported key, start with version byte
exported_key = bytearray(b'\x01')
exported_key += index.to_bytes(4, 'big')
exported_key += ri0
exported_key += ri1
exported_key += ri2
exported_key += ri1_3
# KPub
exported_key += private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
megolm_export = {
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": previous["sender_key"],
"session_id": previous["session_id"],
"session_key": encode_base64(exported_key),
"sender_claimed_keys": previous["sender_claimed_keys"],
"forwarding_curve25519_key_chain": [],
}
return megolm_export
def encrypt_megolm_key_for_backup(session_data: dict, backup_public_key: x25519.X25519PublicKey) -> dict:
"""
Encrypts an exported megolm key for key backup, using the m.megolm_backup.v1.curve25519-aes-sha2 algorithm.
"""
data = encode_canonical_json(session_data)
# Generate an ephemeral curve25519 key, and perform an ECDH with the ephemeral key
# and the backups public key to generate a shared secret.
# The public half of the ephemeral key, encoded using unpadded base64,
# becomes the ephemeral property of the session_data.
ephemeral_keypair = x25519.X25519PrivateKey.from_private_bytes(randbytes(32))
shared_secret = ephemeral_keypair.exchange(backup_public_key)
ephemeral = encode_base64(ephemeral_keypair.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw))
# Using the shared secret, generate 80 bytes by performing an HKDF using SHA-256 as the hash,
# with a salt of 32 bytes of 0, and with the empty string as the info.
# The first 32 bytes are used as the AES key, the next 32 bytes are used as the MAC key,
# and the last 16 bytes are used as the AES initialization vector.
salt = bytes(32)
info = b""
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=80,
salt=salt,
info=info,
)
raw_key = hkdf.derive(shared_secret)
aes_key = raw_key[:32]
mac = raw_key[32:64]
iv = raw_key[64:80]
# Stringify the JSON object, and encrypt it using AES-CBC-256 with PKCS#7 padding.
# This encrypted data, encoded using unpadded base64, becomes the ciphertext property of the session_data.
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv))
encryptor = cipher.encryptor()
padder = padding.PKCS7(128).padder()
padded_data = padder.update(data) + padder.finalize()
ct = encryptor.update(padded_data) + encryptor.finalize()
cipher_text = encode_base64(ct)
# Pass the raw encrypted data (prior to base64 encoding) through HMAC-SHA-256 using the MAC key generated above.
# The first 8 bytes of the resulting MAC are base64-encoded, and become the mac property of the session_data.
h = hmac.HMAC(mac, hashes.SHA256())
# h.update(ct)
signature = h.finalize()
mac = encode_base64(signature[:8])
encrypted_key = {
"first_message_index": 1,
"forwarded_count": 0,
"is_verified": False,
"session_data": {
"ciphertext": cipher_text,
"ephemeral": ephemeral,
"mac": mac
}
}
return encrypted_key
def generate_encrypted_event_content(exported_key: dict, ed_key: ed25519.Ed25519PrivateKey, curve_key: x25519.X25519PrivateKey) -> tuple[dict, dict]:
"""
Encrypts an event using the given key in session export format.
Will not do any ratcheting, just encrypt at index 0.
"""
clear_event = {
"type": "m.room.message",
"room_id": "!room:id",
"sender": "@alice:localhost",
"content": {
"msgtype": "m.text",
"body": "Hello world"
}
}
session_key: str = exported_key["session_key"]
# Get the megolm R0 from the export format
decoded = base64.b64decode(session_key.encode("ascii"))
r0 = decoded[5:133]
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=80,
salt=bytes(32),
info=b"MEGOLM_KEYS",
)
raw_key = hkdf.derive(r0)
aes_key = raw_key[:32]
mac = raw_key[32:64]
aes_iv = raw_key[64:80]
payload_json = {
"room_id": clear_event["room_id"],
"type": clear_event["type"],
"content": clear_event["content"]
}
payload_string = encode_canonical_json(payload_json)
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(aes_iv))
encryptor = cipher.encryptor()
padder = padding.PKCS7(128).padder()
padded_data = padder.update(payload_string)
padded_data += padder.finalize()
ct = encryptor.update(padded_data) + encryptor.finalize()
# The ratchet index i, and the cipher-text, are then packed
# into a message as described in Message format. Then the entire message
# (including the version bytes and all payload bytes) are passed through
# HMAC-SHA-256. The first 8 bytes of the MAC are appended to the message.
message = bytearray()
message += b'\x03'
# int tag for index
message += b'\x08'
# index is 0
message += b'\x00'
message += b'\x12'
# probably works only for short messages
message += len(ct).to_bytes(1, 'big')
# encrypted data
message += ct
h = hmac.HMAC(mac, hashes.SHA256())
h.update(message)
signature = h.finalize()
mac = signature[:8]
message += mac
# Finally, the authenticated message is signed using the Ed25519 keypair;
# the 64 byte signature is appended to the message
signature = ed_key.sign(bytes(message))
message += signature
cipher_text = encode_base64(message)
encrypted_payload = {
"algorithm" : "m.megolm.v1.aes-sha2",
"sender_key" : encode_base64(curve_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
"ciphertext" : cipher_text,
"session_id" : exported_key["session_id"],
"device_id" : "TEST_DEVICE"
}
encrypted_event = {
"type": "m.room.encrypted",
"room_id": "!room:id",
"sender": "@alice:localhost",
"content": encrypted_payload,
"event_id": "$event1",
"origin_server_ts": 1507753886000,
}
return clear_event, encrypted_event
def export_recovery_key(key_b64: str) -> str:
"""
Export a private recovery key as a recovery key that can be presented to users.
As per spec https://spec.matrix.org/v1.8/client-server-api/#recovery-key
"""
private_key_bytes = base64.b64decode(key_b64)
# The 256-bit curve25519 private key is prepended by the bytes 0x8B and 0x01
export_bytes = bytearray()
export_bytes += b'\x8b'
export_bytes += b'\x01'
export_bytes += private_key_bytes
# All the bytes in the string above, including the two header bytes,
# are XORed together to form a parity byte. This parity byte is appended to the byte string.
parity_byte = 0 #b'\x8b' ^ b'\x01'
[parity_byte := parity_byte ^ x for x in export_bytes]
export_bytes += parity_byte.to_bytes(1, 'big')
# The byte string is encoded using base58
recovery_key = base58.b58encode(export_bytes).decode('utf-8')
split = [recovery_key[i:i + 4] for i in range(0, len(recovery_key), 4)]
return ' '.join(split)
if __name__ == "__main__":
main()
+451
View File
@@ -0,0 +1,451 @@
/* Test data for cryptography tests
*
* Do not edit by hand! This file is generated by `./generate-test-data.py`
*/
import { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto";
import { IDownloadKeyResult, IEvent } from "../../../src";
import { KeyBackupSession, KeyBackupInfo } from "../../../src/crypto-api/keybackup";
/* eslint-disable comma-dangle */
// Alice data
export const TEST_USER_ID = "@alice:localhost";
export const TEST_DEVICE_ID = "test_device";
export const TEST_ROOM_ID = "!room:id";
/** The base64-encoded public ed25519 key for this device */
export const TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "YI/7vbGVLpGdYtuceQR8MSsKB/QjgfMXM1xqnn+0NWU";
/** Signed device data, suitable for returning from a `/keys/query` call */
export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": "test_device",
"keys": {
"curve25519:test_device": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
"ed25519:test_device": "YI/7vbGVLpGdYtuceQR8MSsKB/QjgfMXM1xqnn+0NWU"
},
"user_id": "@alice:localhost",
"signatures": {
"@alice:localhost": {
"ed25519:test_device": "LmQC/yAUZJmkxZ+3L0nEwvtVWOzjqQqADWBhk+C47SPaFYHeV+E291mgXaSCJVeGltX+HC49Aw7nb6ga7sw0Aw"
}
}
};
/** base64-encoded public master cross-signing key */
export const MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY";
/** base64-encoded private master cross-signing key */
export const MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "ZG95b3VzcGVha3doYWFhYWFhYWFhYWFhYWFhYWFhbGU";
/** base64-encoded public self cross-signing key */
export const SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "aU2+2CyXQTCuDcmWW0EL2bhJ6PdjFW2LbAsbHqf02AY";
/** base64-encoded private self signing cross-signing key */
export const SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "c2VsZnNlbGZzZWxmc2VsZnNlbGZzZWxmc2VsZnNlbGY";
/** base64-encoded public user cross-signing key */
export const USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "g5TC/zjQXyZYuDLZv7a41z5fFVrXpYPypG//AFQj8hY";
/** base64-encoded private user signing cross-signing key */
export const USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "dXNlcnVzZXJ1c2VydXNlcnVzZXJ1c2VydXNlcnVzZXI";
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
"master_keys": {
"@alice:localhost": {
"keys": {
"ed25519:J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY": "J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY"
},
"user_id": "@alice:localhost",
"usage": [
"master"
]
}
},
"self_signing_keys": {
"@alice:localhost": {
"keys": {
"ed25519:aU2+2CyXQTCuDcmWW0EL2bhJ6PdjFW2LbAsbHqf02AY": "aU2+2CyXQTCuDcmWW0EL2bhJ6PdjFW2LbAsbHqf02AY"
},
"user_id": "@alice:localhost",
"usage": [
"self_signing"
],
"signatures": {
"@alice:localhost": {
"ed25519:J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY": "XfhYEhZmOs8BJdb3viatILBZ/bElsHXEW28V4tIaY5CxrBR0YOym3yZHWmRmypXessHZAKOhZn3yBMXzdajyCw"
}
}
}
},
"user_signing_keys": {
"@alice:localhost": {
"keys": {
"ed25519:g5TC/zjQXyZYuDLZv7a41z5fFVrXpYPypG//AFQj8hY": "g5TC/zjQXyZYuDLZv7a41z5fFVrXpYPypG//AFQj8hY"
},
"user_id": "@alice:localhost",
"usage": [
"user_signing"
],
"signatures": {
"@alice:localhost": {
"ed25519:J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY": "6AkD1XM2H0/ebgP9oBdMKNeft7uxsrb0XN1CsjjHgeZCvCTMmv3BHlLiT/Hzy4fe8H+S1tr484dcXN/PIdnfDA"
}
}
}
}
};
/** Signed OTKs, returned by `POST /keys/claim` */
export const ONE_TIME_KEYS = {
"@alice:localhost": {
"test_device": {
"signed_curve25519:AAAAHQ": {
"key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw",
"signatures": {
"@alice:localhost": {
"ed25519:test_device": "25djC6Rk6gIgFBMVawY9X9LnY8XMMziey6lKqL8Q5Bbp7T1vw9uk0RE7eKO2a/jNLcYroO2xRztGhBrKz5sOCQ"
}
}
}
}
}
};
/** base64-encoded backup decryption (private) key */
export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
/** Backup decryption key in export format */
export const BACKUP_DECRYPTION_KEY_BASE58 = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
export const SIGNED_BACKUP_DATA: KeyBackupInfo = {
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
"version": "1",
"auth_data": {
"public_key": "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
"signatures": {
"@alice:localhost": {
"ed25519:test_device": "KDSNeumirTsd8piI0oVfv/wzg4J4HlEc7rs5XhODFcJ/YAcUdg65ajsZG+rLI0TQOSSGjorJqcrSiSB1HRSCAA"
}
}
}
};
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
{
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc",
"session_id": "FYOoKQSwe4d9jhTZ/LQCZFJINjPEqZ7Or4Z08reP92M",
"session_key": "AQAAAABZ0jXQOprFfXe41tIFmAtHxflJp4O2hM/vzQQpOazOCFeWSoW5P3Z9Q+voU3eXehMwyP8/hm/Q8xLP6/PmJdy+71se/17kdFwcDGgLxBWfa4ODM9zlI4EjKbNqmiii5loJ7rBhA/XXaw80m0hfU6zTDX/KrO55J0Pt4vJ0LDa3LBWDqCkEsHuHfY4U2fy0AmRSSDYzxKmezq+GdPK3j/dj",
"sender_claimed_keys": {
"ed25519": "QdgHgdpDgihgovpPzUiThXur1fbErTFh7paFvNKSgN0"
},
"forwarding_curve25519_key_chain": []
},
{
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc",
"session_id": "mPYSGA2l1tOQiipEDEVYhDSdTSFh2lDW1qpGKYZRxTc",
"session_key": "AQAAAAAHwgkB49BTPAEGTCK6degxUIbl8GPG2ugPRYhNtOpNic63u11+baXFfjDw5fmVfD1gJXpQQjGsqrIYioxrB1xzl7mfb942UHhYdaMQZowpp1fSpJVsxR5TddUU2EWifYD9EQsoz8mY1zqoazm4vUP4v9yxaTcUBj2c6HMJCY0gCJj2EhgNpdbTkIoqRAxFWIQ0nU0hYdpQ1taqRimGUcU3",
"sender_claimed_keys": {
"ed25519": "IrkbT6H+0urDf6wKDSyVC1fh1t84Vz6T62snni86Cog"
},
"forwarding_curve25519_key_chain": []
}
];
/** An exported megolm session */
export const MEGOLM_SESSION_DATA: IMegolmSessionData = {
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc",
"session_id": "ipdI6Zs/7DzFTEhiA2iGaMDfHkIYCleqXT6L+5e1/co",
"session_key": "AQAAAABXGO+Z9jlQJhIL6ByhXrv2BwCIxkhh7MXpKLsYmXkJcWrQlirmXmD79ga1zo+I4DCtEZzyGSpDWXBC6G7ez3H4gDMBam1RE3Jm5tc+oTlIri32UkYgSL0kBkcEnttqmIXBlK8tAfJo3cJnlh7F4ltEOAqrdME6dU0zXTkqXmURqYqXSOmbP+w8xUxIYgNohmjA3x5CGApXql0+i/uXtf3K",
"sender_claimed_keys": {
"ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI"
},
"forwarding_curve25519_key_chain": []
};
/** A ratcheted version of MEGOLM_SESSION_DATA */
export const RATCHTED_MEGOLM_SESSION_DATA: IMegolmSessionData = {
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc",
"session_id": "ipdI6Zs/7DzFTEhiA2iGaMDfHkIYCleqXT6L+5e1/co",
"session_key": "AQAAAAFXGO+Z9jlQJhIL6ByhXrv2BwCIxkhh7MXpKLsYmXkJcWrQlirmXmD79ga1zo+I4DCtEZzyGSpDWXBC6G7ez3H4gDMBam1RE3Jm5tc+oTlIri32UkYgSL0kBkcEnttqmIUWvpwC7by/yg231+gyzu9lDHAU4ivCj48pt7WGiORWmIqXSOmbP+w8xUxIYgNohmjA3x5CGApXql0+i/uXtf3K",
"sender_claimed_keys": {
"ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI"
},
"forwarding_curve25519_key_chain": []
};
/** The key from MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/
export const CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
"first_message_index": 1,
"forwarded_count": 0,
"is_verified": false,
"session_data": {
"ciphertext": "r6HRk2/Im2yJe5cLP8R81aVjFWjYWPHpw7TVxphiSK1cdIDZTTK57r6MfU+0i/mTPn+/PosT74OvYwCnehy2d1BPGxhDl8AhPcBu3//Kzlq2o5CssPsw+88gRehkAsPg9Zp5G9sL9to6giltvTWTbsaQpmvv3HLmBOYSFIxvyZrOT/Ffqu325f0IEsKcyV2BdIkw8Ob9Xt+VWoe4MYEGG6y1T8W125zeFgKWI4Ow76uput64H9zZjIo+Cc+hCTO9Ea4EnosSjizCotevkNck7C/zGgfhBikiohROb6SbaZgxicSsEDZ+f7brnri9yP3iXS3PMDHHpa1+XzG2VOG/Y9OQZpkPq+pbLrCC+NWJeJPslDAK5i+RURwzjnPmaHKCRHTq86CwhFyiCDf61MGwCY3xjrmBJg44BCdxWqCx0YJvwsvVqqnl4vTieUfrwThNPsQ81aVkDHvlmrgrTt8icDa8jTJhu34jem+pbRSEM5aJikV4B+zYiLz+dH/v6UpYA2eG8ReOvwpPXp6CAcIlplRPpWbMBeLFVcPkT4KAXTp9exFpB4on4pf8OsaDomlt4qAA0rhAZmhPWPKcU/A0Tz4gyMu54OivVtw1SPj+5Iq+YDQ8jB6Po3ApzMf6fwF9x/FjevbboFB05X2Jr0NrbFqXMOUwXHMgDAGiIWX8+gkmmbaiNWqg2etjN94pobQSGZelb18XGN7kuwMk+Zwk7A",
"ephemeral": "q+P1WdRtEiPIEtNuuGrRcueZxUbLnSKdsuTAkxewXgU",
"mac": "OibmACbORhI"
}
};
/** A test clear event */
export const CLEAR_EVENT: Partial<IEvent> = {
"type": "m.room.message",
"room_id": "!room:id",
"sender": "@alice:localhost",
"content": {
"msgtype": "m.text",
"body": "Hello world"
}
};
/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */
export const ENCRYPTED_EVENT: Partial<IEvent> = {
"type": "m.room.encrypted",
"room_id": "!room:id",
"sender": "@alice:localhost",
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"sender_key": "WimPd2udAU/1S/+YBpPbmr9L+0H5H+BnAVHSwDxlPGc",
"ciphertext": "AwgAEnAkBmciEAyhh1j6DCk29UXJ7kv/kvayUNfuNT0iAioLxcXjFXOZ5ho3jF1/wrytlt0Lb298uMM67OxdVMi+/mMfYpwlvi07P9cIH6CMSj8tyhYoWl0SrKY6tkPf5GWOlRSRRKbziXa96FHXvnA3V2FCAIGtAe3G4ei5RPbhkmKAFBLAen33/D6MjJVqU8Ojr5vTkgls5eyirarlVpsmnH06alDaxO8avrU0NL+Vsw26xvlUQgEMOnUJ",
"session_id": "ipdI6Zs/7DzFTEhiA2iGaMDfHkIYCleqXT6L+5e1/co",
"device_id": "TEST_DEVICE"
},
"event_id": "$event1",
"origin_server_ts": 1507753886000
};
// Bob data
export const BOB_TEST_USER_ID = "@bob:xyz";
export const BOB_TEST_DEVICE_ID = "bob_device";
export const BOB_TEST_ROOM_ID = "!room:id";
/** The base64-encoded public ed25519 key for this device */
export const BOB_TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "jmY0h8QS6Te6gxyjOmMc0eKOqmbAtXpVo4CCWFubk50";
/** Signed device data, suitable for returning from a `/keys/query` call */
export const BOB_SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": "bob_device",
"keys": {
"curve25519:bob_device": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
"ed25519:bob_device": "jmY0h8QS6Te6gxyjOmMc0eKOqmbAtXpVo4CCWFubk50"
},
"user_id": "@bob:xyz",
"signatures": {
"@bob:xyz": {
"ed25519:bob_device": "4ApBs9jaeGyfdYaWRUdBvQAkDyXjACJ9KJ0xLHMgiFT/1yo6VqPTx2iziKGnrBiGhbtKNxEhDPOvZZkBU73cDQ"
}
}
};
/** base64-encoded public master cross-signing key */
export const BOB_MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA";
/** base64-encoded private master cross-signing key */
export const BOB_MASTER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "RG95b3VzcGVha3doYWFhYWFhYWFhYWFhYWFhYWFhbGU";
/** base64-encoded public self cross-signing key */
export const BOB_SELF_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A";
/** base64-encoded private self signing cross-signing key */
export const BOB_SELF_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "U2VsZnNlbGZzZWxmc2VsZnNlbGZzZWxmc2VsZnNlbGY";
/** base64-encoded public user cross-signing key */
export const BOB_USER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw";
/** base64-encoded private user signing cross-signing key */
export const BOB_USER_CROSS_SIGNING_PRIVATE_KEY_BASE64 = "VXNlcnVzZXJ1c2VydXNlcnVzZXJ1c2VydXNlcnVzZXI";
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
export const BOB_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
"master_keys": {
"@bob:xyz": {
"keys": {
"ed25519:KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA": "KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA"
},
"user_id": "@bob:xyz",
"usage": [
"master"
]
}
},
"self_signing_keys": {
"@bob:xyz": {
"keys": {
"ed25519:DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A": "DaScI3WulBvDjf/d2vdyP5Cgjdypn1c/PSDX23MgN+A"
},
"user_id": "@bob:xyz",
"usage": [
"self_signing"
],
"signatures": {
"@bob:xyz": {
"ed25519:KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA": "RxM8iJU6ZkyzQSVtNnXIJMPyEahVsN+fQQTBNKAs+kqySFyXBgchx+8czZaAhJCpXh9gD1nskT4yyFd2eyUXBw"
}
}
}
},
"user_signing_keys": {
"@bob:xyz": {
"keys": {
"ed25519:lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw": "lXP89FP6zvFH9TSbU1S8uSdAsVawm1NmV6z+Rfr3lEw"
},
"user_id": "@bob:xyz",
"usage": [
"user_signing"
],
"signatures": {
"@bob:xyz": {
"ed25519:KKVOHOB2LsW7hFJwqyzXpA+vp7u5+gaMWUJvBS7mjuA": "jF8fvnPZulrPyh/4E8dNDVBP3iHHl9bRc+rRArVyGzoom+uVrokOck7BN2YmPyCRFZJJx7fgRA1Bveyu+mTVAg"
}
}
}
}
};
/** Signed OTKs, returned by `POST /keys/claim` */
export const BOB_ONE_TIME_KEYS = {
"@bob:xyz": {
"bob_device": {
"signed_curve25519:AAAAHQ": {
"key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw",
"signatures": {
"@bob:xyz": {
"ed25519:bob_device": "dlZc9VA/hP980Mxvu9qwi0qJx8VK7sADGOM48CE01YM7K/Mbty9lis/QjtQAWqDg371QyynVRjEzt9qj7eSFCg"
}
}
}
}
}
};
/** base64-encoded backup decryption (private) key */
export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
/** Backup decryption key in export format */
export const BOB_BACKUP_DECRYPTION_KEY_BASE58 = "EsT5 Sd5m mEXs NQYE ibRe 3q9E 4aXW rHih 5f9J 6rU6 AfwY mASR";
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = {
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
"version": "1",
"auth_data": {
"public_key": "ZRuVWcWlDuvOwZRygccUCD4Avtnt130800I+WQNwwRY",
"signatures": {
"@bob:xyz": {
"ed25519:bob_device": "lDIMj3VC0WazE2FamGHpmbiqKf9Z4pO4qapZ5TL5BnD3c+dvb+2waOEd6pgay/pmrQ6MW4Eu2KDEpe1fnHc3BA"
}
}
}
};
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
{
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
"session_id": "/2K+V777vipCxPZ0gpY9qcpz1DYaXwuMRIu0UEP0Wa0",
"session_key": "AQAAAAAclzWVMeWBKH+B/WMowa3rb4ma3jEl6n5W4GCs9ue65CruzD3ihX+85pZ9hsV9Bf6fvhjp76WNRajoJYX0UIt7aosjmu0i+H+07hEQ0zqTKpVoSH0ykJ6stAMhdr6Q4uW5crBmdTTBIsqmoWsNJZKKoE2+ldYrZ1lrFeaJbjBIY/9ivle++74qQsT2dIKWPanKc9Q2Gl8LjESLtFBD9Fmt",
"sender_claimed_keys": {
"ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
},
"forwarding_curve25519_key_chain": []
},
{
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
"session_id": "+07YOpSgdZ1X9le3n3NMByw0V1B0H0Djnbm76jgmWoo",
"session_key": "AQAAAAAjWfIMo9+BWS8IvhfsQuomxXXXGy11tJs0ej505xxd1RzOIP4ftq3MbZYsfH8kqSMBc2l1Ym2u3Dksv2/nR0zGQeNIgOxeMuwHU3Ry7+DdV1I96blPylVCCn/f5RAy6smKoaeylptPdXgVXmw3YBBUVYpHpm+xCIUUp9foAdb8hftO2DqUoHWdV/ZXt59zTAcsNFdQdB9A4525u+o4JlqK",
"sender_claimed_keys": {
"ed25519": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM"
},
"forwarding_curve25519_key_chain": []
}
];
/** An exported megolm session */
export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = {
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
"session_id": "gywydBrIJcJWktC/ic3tunKZM1XZm1MpYiYtdbj8Rpc",
"session_key": "AQAAAADZJL7OdM/KHfPzXPZ3CtlLBIlzbwk06dnZTd3bvkcdP5u73rdmThBKdqGA4xzCyxZsHdYLZRrlmD3VwOmNfvWMqYdPxA1X0vs3d172y9EIG8i+N/skJxTRypcVSV9XoinBNIWr/gkyepuAKiQqemlc8J5amD9OkmbVkmnrxP1uyYMsMnQayCXCVpLQv4nN7bpymTNV2ZtTKWImLXW4/EaX",
"sender_claimed_keys": {
"ed25519": "zBdpQwWYyz1MkZuEUhXqcdMfUNN/B9psLFDDDTJOg64"
},
"forwarding_curve25519_key_chain": []
};
/** A ratcheted version of BOB_MEGOLM_SESSION_DATA */
export const BOB_RATCHTED_MEGOLM_SESSION_DATA: IMegolmSessionData = {
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!room:id",
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
"session_id": "gywydBrIJcJWktC/ic3tunKZM1XZm1MpYiYtdbj8Rpc",
"session_key": "AQAAAAHZJL7OdM/KHfPzXPZ3CtlLBIlzbwk06dnZTd3bvkcdP5u73rdmThBKdqGA4xzCyxZsHdYLZRrlmD3VwOmNfvWMqYdPxA1X0vs3d172y9EIG8i+N/skJxTRypcVSV9Xoil2JdGx9oPqR0dFVh661Aqs86rJRbQ4IeRiuEm35VMxboMsMnQayCXCVpLQv4nN7bpymTNV2ZtTKWImLXW4/EaX",
"sender_claimed_keys": {
"ed25519": "zBdpQwWYyz1MkZuEUhXqcdMfUNN/B9psLFDDDTJOg64"
},
"forwarding_curve25519_key_chain": []
};
/** The key from BOB_MEGOLM_SESSION_DATA, encrypted for backup using `m.megolm_backup.v1.curve25519-aes-sha2` algorithm*/
export const BOB_CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
"first_message_index": 1,
"forwarded_count": 0,
"is_verified": false,
"session_data": {
"ciphertext": "d7UVOK17WEVky/8hK0h3HsTQrFMEbKbfqMcl2KtyTWcI9S5gGFWK9Git5BzVRxRggvxQ0c8PDfqL+dr3zHytAMW+71BJqIPQW910vV7SX3IcGylnoUcS3doVkJZiprXytXMP89AKcgv5Dj7mS2ZdvNGE+Atro74bzZ5yot5BrE0ZE5SjoUBPLaLMMu9HopLIV+qx01Rc3F0wmkocSPo51N0nv6wvO5Cst0FiOGHDK6r1pFlgDEJLmBkOyC4e8oMVbKTJzsSQVbJ8tJ37xuhI+T5P0ZlmiqKDqYRp8uh50w+txLEixYhEUunFgCTt1DAmiS9pLNYhLyl1ggwuQjzZe+AV6timbRxNJy18/AEcPomJw7z/pxYIiNLHRKOC13Wp8kGWx9cOgfMQ5KmBuLS8psGiLTBkfWPLOfNYqjbeqAR+OGZQoS6hUjbBYU7QuFa4FOYBHkNB2UqNsdsMb9qB/qs7QGTSb8Lok5YjW1c81BUpmIyKvuqnKma0MZskrpTYGQD2eJDABFCZwLFm+LgDyUTeSiV5xguYztLrHOk8LHKo9M8dIZgoBjeFVJxyjbcXKsVS3aQkMXKCrRlKLqhZTws/ZJwVfW9DbktZ9dT+tRZQvI7tjJofojcLX61AGJDnqUf5+2Gv1tEnmUI953gIzc8NlcFabPOsDsZEODt7MdOCTPT3w29umyhKbCsslpb64LoS/AB2QRPRCgkJS7snRA",
"ephemeral": "oO0VX84OUIzm2i/12zAhTWOZT5IFRH5mXaKZ8fXkCgU",
"mac": "lEfHlqfJQwU"
}
};
/** A test clear event */
export const BOB_CLEAR_EVENT: Partial<IEvent> = {
"type": "m.room.message",
"room_id": "!room:id",
"sender": "@alice:localhost",
"content": {
"msgtype": "m.text",
"body": "Hello world"
}
};
/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */
export const BOB_ENCRYPTED_EVENT: Partial<IEvent> = {
"type": "m.room.encrypted",
"room_id": "!room:id",
"sender": "@alice:localhost",
"content": {
"algorithm": "m.megolm.v1.aes-sha2",
"sender_key": "FOvlmz18LLI3k/llCpqRoKT90+gFF8YhuL+v1YBXHlw",
"ciphertext": "AwgAEnA/mEqZm2lSrhoG11OpDqsohGSBJWsudbuoItLlivmpFZQHrKMbE6z/dhCTwUi76vwfRCtf4tyPMD845cqZH1nL0bowq3/awyzZ8Q263Y3WrLfkUTFBU6oPF/IULUFZZuw6kLdfd5g5+uigvqUhFFpICoj7KNHznv4sFNssd00/WgJquZ6PRt6e1v6ANFNiZPAwghIL+kBc6pb8i6MUWt9JnXilJhTqFDHdXiY4qkaKBWbwebC26PYM",
"session_id": "gywydBrIJcJWktC/ic3tunKZM1XZm1MpYiYtdbj8Rpc",
"device_id": "TEST_DEVICE"
},
"event_id": "$event1",
"origin_server_ts": 1507753886000
};
+253 -76
View File
@@ -2,20 +2,30 @@
import EventEmitter from "events";
// load olm before the sdk if possible
import '../olm-loader';
import "../olm-loader";
import { logger } from '../../src/logger';
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src";
import { logger } from "../../src/logger";
import { IContent, IEvent, IEventRelation, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import {
ClientEvent,
EventType,
IJoinedRoom,
IPusher,
ISyncResponse,
MatrixClient,
MsgType,
RelationType,
} from "../../src";
import { SyncState } from "../../src/sync";
import { eventMapperFor } from "../../src/event-mapper";
import { TEST_ROOM_ID } from "./test-data";
/**
* Return a promise that is resolved when the client next emits a
* SYNCING event.
* @param {Object} client The client
* @param {Number=} count Number of syncs to wait for (default 1)
* @return {Promise} Resolves once the client has emitted a SYNCING event
* @param client - The client
* @param count - Number of syncs to wait for (default 1)
* @returns Promise which resolves once the client has emitted a SYNCING event
*/
export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
if (count <= 0) {
@@ -40,21 +50,78 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
}
/**
* Create a spy for an object and automatically spy its methods.
* @param {*} constr The class constructor (used with 'new')
* @param {string} name The name of the class
* @return {Object} An instantiated object with spied methods/properties.
* Return a sync response which contains a single room (by default TEST_ROOM_ID), with the members given
* @param roomMembers
* @param roomId
*
* @returns the sync response
*/
export function mock<T>(constr: { new(...args: any[]): T }, name: string): T {
export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): ISyncResponse {
const roomResponse: IJoinedRoom = {
summary: {
"m.heroes": [],
"m.joined_member_count": roomMembers.length,
"m.invited_member_count": roomMembers.length,
},
state: {
events: [
mkEventCustom({
sender: roomMembers[0],
type: "m.room.encryption",
state_key: "",
content: {
algorithm: "m.megolm.v1.aes-sha2",
},
}),
],
},
timeline: {
events: [],
prev_batch: "",
},
ephemeral: { events: [] },
account_data: { events: [] },
unread_notifications: {},
};
for (let i = 0; i < roomMembers.length; i++) {
roomResponse.state.events.push(
mkMembershipCustom({
membership: "join",
sender: roomMembers[i],
}),
);
}
return {
next_batch: "1",
rooms: {
join: { [roomId]: roomResponse },
invite: {},
leave: {},
knock: {},
},
account_data: { events: [] },
};
}
/**
* Create a spy for an object and automatically spy its methods.
* @param constr - The class constructor (used with 'new')
* @param name - The name of the class
* @returns An instantiated object with spied methods/properties.
*/
export function mock<T>(constr: { new (...args: any[]): T }, name: string): T {
// Based on http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
const HelperConstr = new Function(); // jshint ignore:line
HelperConstr.prototype = constr.prototype;
// @ts-ignore
const result = new HelperConstr();
result.toString = function() {
result.toString = function () {
return "mock" + (name ? " of " + name : "");
};
for (const key of Object.getOwnPropertyNames(constr.prototype)) { // eslint-disable-line guard-for-in
for (const key of Object.getOwnPropertyNames(constr.prototype)) {
// eslint-disable-line guard-for-in
try {
if (constr.prototype[key] instanceof Function) {
result[key] = jest.fn();
@@ -78,20 +145,21 @@ interface IEventOpts {
user?: string;
unsigned?: IUnsigned;
redacts?: string;
ts?: number;
}
let testEventIndex = 1; // counter for events, easier for comparison of randomly generated events
/**
* Create an Event.
* @param {Object} opts Values for the event.
* @param {string} opts.type The event.type
* @param {string} opts.room The event.room_id
* @param {string} opts.sender The event.sender
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
* @param {Object} opts.content The event.content
* @param {boolean} opts.event True to make a MatrixEvent.
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
* @return {Object} a JSON object representing this event.
* @param opts - Values for the event.
* @param opts.type - The event.type
* @param opts.room - The event.room_id
* @param opts.sender - The event.sender
* @param opts.skey - Optional. The state key (auto inserts empty string)
* @param opts.content - The event.content
* @param opts.event - True to make a MatrixEvent.
* @param client - If passed along with opts.event=true will be used to set up re-emitters.
* @returns a JSON object representing this event.
*/
export function mkEvent(opts: IEventOpts & { event: true }, client?: MatrixClient): MatrixEvent;
export function mkEvent(opts: IEventOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
@@ -109,18 +177,21 @@ 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;
} else if ([
EventType.RoomName,
EventType.RoomTopic,
EventType.RoomCreate,
EventType.RoomJoinRules,
EventType.RoomPowerLevels,
EventType.RoomTopic,
"com.example.state",
].includes(opts.type)) {
} else if (
[
EventType.RoomName,
EventType.RoomTopic,
EventType.RoomCreate,
EventType.RoomJoinRules,
EventType.RoomPowerLevels,
EventType.RoomTopic,
"com.example.state",
].includes(opts.type)
) {
event.state_key = "";
}
@@ -158,8 +229,8 @@ interface IPresenceOpts {
/**
* Create an m.presence event.
* @param {Object} opts Values for the presence.
* @return {Object|MatrixEvent} The event
* @param opts - Values for the presence.
* @returns The event
*/
export function mkPresence(opts: IPresenceOpts & { event: true }): MatrixEvent;
export function mkPresence(opts: IPresenceOpts & { event?: false }): Partial<IEvent>;
@@ -191,16 +262,16 @@ interface IMembershipOpts {
/**
* Create an m.room.member event.
* @param {Object} opts Values for the membership.
* @param {string} opts.room The room ID for the event.
* @param {string} opts.mship The content.membership for the event.
* @param {string} opts.sender The sender user ID for the event.
* @param {string} opts.skey The target user ID for the event if applicable
* @param opts - Values for the membership.
* @param opts.room - The room ID for the event.
* @param opts.mship - The content.membership for the event.
* @param opts.sender - The sender user ID for the event.
* @param opts.skey - The target user ID for the event if applicable
* e.g. for invites/bans.
* @param {string} opts.name The content.displayname for the event.
* @param {string} opts.url The content.avatar_url for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
* @param opts.name - The content.displayname for the event.
* @param opts.url - The content.avatar_url for the event.
* @param opts.event - True to make a MatrixEvent.
* @returns The event
*/
export function mkMembership(opts: IMembershipOpts & { event: true }): MatrixEvent;
export function mkMembership(opts: IMembershipOpts & { event?: false }): Partial<IEvent>;
@@ -226,8 +297,8 @@ export function mkMembership(opts: IMembershipOpts & { event?: boolean }): Parti
}
export function mkMembershipCustom<T>(
base: T & { membership: string, sender: string, content?: IContent },
): T & { type: EventType, sender: string, state_key: string, content: IContent } & GeneratedMetadata {
base: T & { membership: string; sender: string; content?: IContent },
): T & { type: EventType; sender: string; state_key: string; content: IContent } & GeneratedMetadata {
const content = base.content || {};
return mkEventCustom({
...base,
@@ -237,22 +308,27 @@ export function mkMembershipCustom<T>(
});
}
interface IMessageOpts {
export interface IMessageOpts {
room?: string;
user: string;
msg?: string;
event?: boolean;
relatesTo?: IEventRelation;
ts?: number;
}
/**
* Create an m.room.message event.
* @param {Object} opts Values for the message
* @param {string} opts.room The room ID for the event.
* @param {string} opts.user The user ID for the event.
* @param {string} opts.msg Optional. The content.body for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
* @return {Object|MatrixEvent} The event
* @param opts - Values for the message
* @param opts.room - The room ID for the event.
* @param opts.user - The user ID for the event.
* @param opts.msg - Optional. The content.body for the event.
* @param opts.event - True to make a MatrixEvent.
* @param opts.relatesTo - An IEventRelation relating this to another event.
* @param opts.ts - The timestamp of the event.
* @param opts.event - True to make a MatrixEvent.
* @param client - If passed along with opts.event=true will be used to set up re-emitters.
* @returns The event
*/
export function mkMessage(opts: IMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent;
export function mkMessage(opts: IMessageOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
@@ -269,6 +345,10 @@ export function mkMessage(
},
};
if (opts.relatesTo) {
eventOpts.content["m.relates_to"] = opts.relatesTo;
}
if (!eventOpts.content.body) {
eventOpts.content.body = "Random->" + Math.random();
}
@@ -282,14 +362,15 @@ interface IReplyMessageOpts extends IMessageOpts {
/**
* Create a reply message.
*
* @param {Object} opts Values for the message
* @param {string} opts.room The room ID for the event.
* @param {string} opts.user The user ID for the event.
* @param {string} opts.msg Optional. The content.body for the event.
* @param {MatrixEvent} opts.replyToMessage The replied message
* @param {boolean} opts.event True to make a MatrixEvent.
* @param {MatrixClient} client If passed along with opts.event=true will be used to set up re-emitters.
* @return {Object|MatrixEvent} The event
* @param opts - Values for the message
* @param opts.room - The room ID for the event.
* @param opts.user - The user ID for the event.
* @param opts.msg - Optional. The content.body for the event.
* @param opts.ts - The timestamp of the event.
* @param opts.replyToMessage - The replied message
* @param opts.event - True to make a MatrixEvent.
* @param client - If passed along with opts.event=true will be used to set up re-emitters.
* @returns The event
*/
export function mkReplyMessage(opts: IReplyMessageOpts & { event: true }, client?: MatrixClient): MatrixEvent;
export function mkReplyMessage(opts: IReplyMessageOpts & { event?: false }, client?: MatrixClient): Partial<IEvent>;
@@ -307,7 +388,7 @@ export function mkReplyMessage(
"rel_type": "m.in_reply_to",
"event_id": opts.replyToMessage.getId(),
"m.in_reply_to": {
"event_id": opts.replyToMessage.getId()!,
event_id: opts.replyToMessage.getId()!,
},
},
},
@@ -320,11 +401,76 @@ export function mkReplyMessage(
}
/**
* A mock implementation of webstorage
* Create a reaction event.
*
* @constructor
* @param target - the event we are reacting to.
* @param client - the MatrixClient
* @param userId - the userId of the sender
* @param roomId - the id of the room we are in
* @param ts - The timestamp of the event.
* @returns The event
*/
export class MockStorageApi {
export function mkReaction(
target: MatrixEvent,
client: MatrixClient,
userId: string,
roomId: string,
ts?: number,
): MatrixEvent {
return mkEvent(
{
event: true,
type: EventType.Reaction,
user: userId,
room: roomId,
content: {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: target.getId()!,
key: Math.random().toString(),
},
},
ts,
},
client,
);
}
export function mkEdit(
target: MatrixEvent,
client: MatrixClient,
userId: string,
roomId: string,
msg?: string,
ts?: number,
) {
msg = msg ?? `Edit of ${target.getId()}`;
return mkEvent(
{
event: true,
type: EventType.RoomMessage,
user: userId,
room: roomId,
content: {
"body": `* ${msg}`,
"m.new_content": {
body: msg,
},
"m.relates_to": {
rel_type: RelationType.Replace,
event_id: target.getId()!,
},
},
ts,
},
client,
);
}
/**
* A mock implementation of webstorage
*/
export class MockStorageApi implements Storage {
private data: Record<string, any> = {};
public get length() {
@@ -346,33 +492,52 @@ export class MockStorageApi {
public removeItem(k: string): void {
delete this.data[k];
}
public clear(): void {
this.data = {};
}
}
/**
* If an event is being decrypted, wait for it to finish being decrypted.
*
* @param {MatrixEvent} event
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
* @returns promise which resolves (to `event`) when the event has been decrypted
*/
export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent> {
export async function awaitDecryption(
event: MatrixEvent,
{ waitOnDecryptionFailure = false } = {},
): Promise<MatrixEvent> {
// An event is not always decrypted ahead of time
// getClearContent is a good signal to know whether an event has been decrypted
// already
if (event.getClearContent() !== null) {
return event;
if (waitOnDecryptionFailure && event.isDecryptionFailure()) {
logger.log(`${Date.now()}: event ${event.getId()} got decryption error; waiting`);
} else {
return event;
}
} else {
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
logger.log(`${Date.now()}: event ${event.getId()} is not yet decrypted; waiting`);
}
return new Promise((resolve) => {
event.once(MatrixEventEvent.Decrypted, (ev) => {
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
return new Promise((resolve) => {
if (waitOnDecryptionFailure) {
event.on(MatrixEventEvent.Decrypted, (ev, err) => {
logger.log(`${Date.now()}: MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
if (!err) {
resolve(ev);
}
});
} else {
event.once(MatrixEventEvent.Decrypted, (ev, err) => {
logger.log(`${Date.now()}: MatrixEventEvent.Decrypted for event ${event.getId()}: ${err ?? "success"}`);
resolve(ev);
});
});
}
}
});
}
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise((r) => e.once(k, r));
export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
app_display_name: "app",
@@ -384,3 +549,15 @@ export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
pushkey: "pushpush",
...extra,
});
/**
* a list of the supported crypto implementations, each with a callback to initialise that implementation
* for the given client
*/
export const CRYPTO_BACKENDS: Record<string, InitCrypto> = {};
export type InitCrypto = (_: MatrixClient) => Promise<void>;
CRYPTO_BACKENDS["rust-sdk"] = (client: MatrixClient) => client.initRustCrypto();
if (global.Olm) {
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initCrypto();
}
+163
View File
@@ -0,0 +1,163 @@
/*
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, THREAD_RELATION_TYPE } 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: THREAD_RELATION_TYPE.name,
["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;
};
type MakeThreadResult = {
/**
* Thread model
*/
thread: Thread;
/**
* Thread root event
*/
rootEvent: MatrixEvent;
/**
* Events added to the thread
*/
events: MatrixEvent[];
};
/**
* Starts a new thread in a room by creating a message as thread root.
* Also creates a Thread model and adds it to the room.
* Does not insert the messages into a timeline.
*/
export const mkThread = ({
room,
client,
authorId,
participantUserIds,
length = 2,
ts = 1,
}: MakeThreadProps): MakeThreadResult => {
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, [rootEvent, ...events], true);
return { thread, rootEvent, events };
};
+724 -47
View File
@@ -1,5 +1,5 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Copyright 2022 - 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,7 +14,35 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export const DUMMY_SDP = (
import {
ClientEvent,
ClientEventHandlerMap,
EventType,
GroupCall,
GroupCallIntent,
GroupCallType,
IContent,
ISendEventResponse,
MatrixClient,
MatrixEvent,
Room,
RoomMember,
RoomState,
RoomStateEvent,
RoomStateEventHandlerMap,
SendToDeviceContentMap,
} from "../../src";
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
import { ReEmitter } from "../../src/ReEmitter";
import { SyncState } from "../../src/sync";
import { CallEvent, CallEventHandlerMap, CallState, 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" +
"s=-\r\nt=0 0\r\na=group:BUNDLE 0\r\n" +
@@ -51,96 +79,745 @@ export const DUMMY_SDP = (
"a=rtpmap:112 telephone-event/32000\r\n" +
"a=rtpmap:113 telephone-event/16000\r\n" +
"a=rtpmap:126 telephone-event/8000\r\n" +
"a=ssrc:3619738545 cname:2RWtmqhXLdoF4sOi\r\n"
);
"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";
export const FAKE_ROOM_ID = "!fake:test.dummy";
export const FAKE_CONF_ID = "fakegroupcallid";
export const FAKE_USER_ID_1 = "@alice:test.dummy";
export const FAKE_DEVICE_ID_1 = "@AAAAAA";
export const FAKE_SESSION_ID_1 = "alice1";
export const FAKE_USER_ID_2 = "@bob:test.dummy";
export const FAKE_DEVICE_ID_2 = "@BBBBBB";
export const FAKE_SESSION_ID_2 = "bob1";
export const FAKE_USER_ID_3 = "@charlie:test.dummy";
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 iceConnectionStateChangeListener?: () => void;
public onTrackListener?: (e: RTCTrackEvent) => void;
public onDataChannelListener?: (ev: RTCDataChannelEvent) => void;
public needsNegotiation = false;
public readyToNegotiate: Promise<void>;
private onReadyToNegotiate?: () => void;
public localDescription: RTCSessionDescription;
public signalingState: RTCSignalingState = "stable";
public iceConnectionState: RTCIceConnectionState = "connected";
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 = {
sdp: DUMMY_SDP,
type: 'offer',
toJSON: function() { },
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 === "iceconnectionstatechange") {
this.iceConnectionStateChangeListener = listener;
} else if (type == "track") {
this.onTrackListener = listener;
} else if (type == "datachannel") {
this.onDataChannelListener = 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();
}
}
public triggerIncomingDataChannel(): void {
this.onDataChannelListener?.({ channel: {} } as RTCDataChannelEvent);
}
public restartIce(): void {}
}
export class MockRTCRtpSender {
constructor(public track: MockMediaStreamTrack) { }
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) { }
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) {}
stop() { }
}
public stop = jest.fn<void, []>();
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
// implementation
export class MockMediaStream {
constructor(
public id: string,
private tracks: MockMediaStreamTrack[] = [],
) {}
public listeners: [string, (...args: any[]) => any][] = [];
public isStopped = false;
public settings?: MediaTrackSettings;
listeners: [string, (...args: any[]) => any][] = [];
public getSettings(): MediaTrackSettings {
return this.settings!;
}
dispatchEvent(eventType: string) {
// 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();
});
}
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 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 typed(): MediaStreamTrack {
return this as unknown as MediaStreamTrack;
}
}
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
// implementation
export class MockMediaStream {
constructor(public id: string, private tracks: MockMediaStreamTrack[] = []) {}
public listeners: [string, (...args: any[]) => any][] = [];
public isStopped = false;
public dispatchEvent(eventType: string) {
this.listeners.forEach(([t, c]) => {
if (t !== eventType) return;
c();
});
}
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]);
}
public removeEventListener(eventType: string, callback: (...args: any[]) => any) {
this.listeners.filter(([t, c]) => {
return t !== eventType || c !== callback;
});
}
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",
) { }
constructor(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;
}
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;
}
stopUserMediaStream() { }
hasAudioDevice() { return true; }
}
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: SendToDeviceContentMap, txnId?: string]
>();
public isInitialSyncComplete(): boolean {
return false;
}
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 getFoci = jest.fn();
public supportsThreads(): 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 MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap> {
constructor(public roomId: string, public groupCallId?: string) {
super();
}
public state = CallState.Ringing;
public opponentUserId = FAKE_USER_ID_1;
public opponentDeviceId = FAKE_DEVICE_ID_1;
public opponentSessionId = FAKE_SESSION_ID_1;
public opponentMember = { userId: this.opponentUserId };
public callId = "1";
public localUsermediaFeed = {
setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(),
isAudioMuted: jest.fn().mockReturnValue(false),
isVideoMuted: jest.fn().mockReturnValue(false),
stream: new MockMediaStream("stream"),
} as unknown as CallFeed;
public remoteUsermediaFeed?: CallFeed;
public remoteScreensharingFeed?: CallFeed;
public reject = jest.fn<void, []>();
public answerWithCallFeeds = jest.fn<void, [CallFeed[]]>();
public hangup = jest.fn<void, []>();
public initStats = jest.fn<void, []>();
public sendMetadataUpdate = jest.fn<void, []>();
public getOpponentMember(): Partial<RoomMember> {
return this.opponentMember;
}
public getOpponentDeviceId(): string | undefined {
return this.opponentDeviceId;
}
public getOpponentSessionId(): string | undefined {
return this.opponentSessionId;
}
public getLocalFeeds(): CallFeed[] {
return [this.localUsermediaFeed];
}
public typed(): MatrixCall {
return this as unknown as MatrixCall;
}
}
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,
},
redacted?: boolean,
): 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),
isRedacted: jest.fn().mockReturnValue(redacted ?? false),
} 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;
}
export const REMOTE_SFU_DESCRIPTION =
"v=0\n" +
"o=- 3242942315779688438 1678878001 IN IP4 0.0.0.0\n" +
"s=-\n" +
"t=0 0\n" +
"a=fingerprint:sha-256 EA:30:B2:7F:49:B5:46:D6:40:72:BF:79:95:C1:65:08:6E:35:09:FB:90:89:DA:EF:6B:82:D1:38:8C:25:39:B2\n" +
"a=group:BUNDLE 0 1 2\n" +
"m=audio 9 UDP/TLS/RTP/SAVPF 111 9 0 8\n" +
"c=IN IP4 0.0.0.0\n" +
"a=setup:actpass\n" +
"a=mid:0\n" +
"a=ice-ufrag:obZwzAcRtxwuozPZ\n" +
"a=ice-pwd:TWXNaPeyKTTvRLyIQhWHfHlZHJjtcoKs\n" +
"a=rtcp-mux\n" +
"a=rtcp-rsize\n" +
"a=rtpmap:111 opus/48000/2\n" +
"a=fmtp:111 minptime=10;usedtx=1;useinbandfec=1\n" +
"a=rtcp-fb:111 transport-cc \n" +
"a=rtpmap:9 G722/8000\n" +
"a=rtpmap:0 PCMU/8000\n" +
"a=rtpmap:8 PCMA/8000\n" +
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\n" +
"a=ssrc:2963372119 cname:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
"a=ssrc:2963372119 msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 4b811ab6-6926-473d-8ca5-ac45f268c507\n" +
"a=ssrc:2963372119 mslabel:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
"a=ssrc:2963372119 label:4b811ab6-6926-473d-8ca5-ac45f268c507\n" +
"a=msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 4b811ab6-6926-473d-8ca5-ac45f268c507\n" +
"a=sendrecv\n" +
"a=candidate:1155505470 1 udp 2130706431 13.41.173.213 41385 typ host\n" +
"a=candidate:1155505470 2 udp 2130706431 13.41.173.213 41385 typ host\n" +
"a=candidate:1155505470 1 udp 2130706431 13.41.173.213 40026 typ host\n" +
"a=candidate:1155505470 2 udp 2130706431 13.41.173.213 40026 typ host\n" +
"a=end-of-candidates\n" +
"m=video 9 UDP/TLS/RTP/SAVPF 96 97 102 103 104 106 108 109 98 99 112 116\n" +
"c=IN IP4 0.0.0.0\n" +
"a=setup:actpass\n" +
"a=mid:1\n" +
"a=ice-ufrag:obZwzAcRtxwuozPZ\n" +
"a=ice-pwd:TWXNaPeyKTTvRLyIQhWHfHlZHJjtcoKs\n" +
"a=rtcp-mux\n" +
"a=rtcp-rsize\n" +
"a=rtpmap:96 VP8/90000\n" +
"a=rtcp-fb:96 goog-remb \n" +
"a=rtcp-fb:96 transport-cc \n" +
"a=rtcp-fb:96 ccm fir\n" +
"a=rtcp-fb:96 nack \n" +
"a=rtcp-fb:96 nack pli\n" +
"a=rtpmap:97 rtx/90000\n" +
"a=fmtp:97 apt=96\n" +
"a=rtpmap:102 H264/90000\n" +
"a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\n" +
"a=rtcp-fb:102 goog-remb \n" +
"a=rtcp-fb:102 transport-cc \n" +
"a=rtcp-fb:102 ccm fir\n" +
"a=rtcp-fb:102 nack \n" +
"a=rtcp-fb:102 nack pli\n" +
"a=rtpmap:103 rtx/90000\n" +
"a=fmtp:103 apt=102\n" +
"a=rtpmap:104 H264/90000\n" +
"a=fmtp:104 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f\n" +
"a=rtcp-fb:104 goog-remb \n" +
"a=rtcp-fb:104 transport-cc \n" +
"a=rtcp-fb:104 ccm fir\n" +
"a=rtcp-fb:104 nack \n" +
"a=rtcp-fb:104 nack pli\n" +
"a=rtpmap:106 H264/90000\n" +
"a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\n" +
"a=rtcp-fb:106 goog-remb \n" +
"a=rtcp-fb:106 transport-cc \n" +
"a=rtcp-fb:106 ccm fir\n" +
"a=rtcp-fb:106 nack \n" +
"a=rtcp-fb:106 nack pli\n" +
"a=rtpmap:108 H264/90000\n" +
"a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f\n" +
"a=rtcp-fb:108 goog-remb \n" +
"a=rtcp-fb:108 transport-cc \n" +
"a=rtcp-fb:108 ccm fir\n" +
"a=rtcp-fb:108 nack \n" +
"a=rtcp-fb:108 nack pli\n" +
"a=rtpmap:109 rtx/90000\n" +
"a=fmtp:109 apt=108\n" +
"a=rtpmap:98 VP9/90000\n" +
"a=fmtp:98 profile-id=0\n" +
"a=rtcp-fb:98 goog-remb \n" +
"a=rtcp-fb:98 transport-cc \n" +
"a=rtcp-fb:98 ccm fir\n" +
"a=rtcp-fb:98 nack \n" +
"a=rtcp-fb:98 nack pli\n" +
"a=rtpmap:99 rtx/90000\n" +
"a=fmtp:99 apt=98\n" +
"a=rtpmap:112 H264/90000\n" +
"a=fmtp:112 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f\n" +
"a=rtcp-fb:112 goog-remb \n" +
"a=rtcp-fb:112 transport-cc \n" +
"a=rtcp-fb:112 ccm fir\n" +
"a=rtcp-fb:112 nack \n" +
"a=rtcp-fb:112 nack pli\n" +
"a=rtpmap:116 ulpfec/90000\n" +
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\n" +
"a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\n" +
"a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\n" +
"a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\n" +
"a=rid:f recv\n" +
"a=rid:h recv\n" +
"a=rid:q recv\n" +
"a=simulcast:recv f;h;q\n" +
"a=ssrc:1212931603 cname:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
"a=ssrc:1212931603 msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 12905f48-75b9-499f-ba50-fc00f56a86c6\n" +
"a=ssrc:1212931603 mslabel:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
"a=ssrc:1212931603 label:12905f48-75b9-499f-ba50-fc00f56a86c6\n" +
"a=msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 12905f48-75b9-499f-ba50-fc00f56a86c6\n" +
"a=sendrecv\n" +
"m=application 9 UDP/DTLS/SCTP webrtc-datachannel\n" +
"c=IN IP4 0.0.0.0\n" +
"a=setup:actpass\n" +
"a=mid:2\n" +
"a=sendrecv\n" +
"a=sctp-port:5000\n" +
"a=ice-ufrag:obZwzAcRtxwuozPZ\n" +
"a=ice-pwd:TWXNaPeyKTTvRLyIQhWHfHlZHJjtcoKs";
export const groupCallParticipantsFourOtherDevices = new Map([
[
new RoomMember("roomId0", "user1"),
new Map([
[
"deviceId0",
{
sessionId: "0",
screensharing: false,
},
],
[
"deviceId1",
{
sessionId: "1",
screensharing: false,
},
],
[
"deviceId2",
{
sessionId: "2",
screensharing: false,
},
],
]),
],
[
new RoomMember("roomId0", "user2"),
new Map([
[
"deviceId3",
{
sessionId: "0",
screensharing: false,
},
],
[
"deviceId4",
{
sessionId: "1",
screensharing: false,
},
],
]),
],
]);
export const groupCallParticipantsOneOtherDevice = new Map([
[
new RoomMember("roomId1", "thisMember"),
new Map([
[
"deviceId0",
{
sessionId: "0",
screensharing: false,
},
],
]),
],
[
new RoomMember("roomId1", "opponentMember"),
new Map([
[
"deviceId1",
{
sessionId: "1",
screensharing: false,
},
],
]),
],
]);
File diff suppressed because it is too large Load Diff
+2 -14
View File
@@ -46,13 +46,7 @@ describe("NamespacedValue", () => {
});
it("should not permit falsey values for both parts", () => {
try {
new UnstableValue(null!, null!);
// noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail");
} catch (e) {
expect((<Error>e).message).toBe("One of stable or unstable values must be supplied");
}
expect(() => new UnstableValue(null!, null!)).toThrow("One of stable or unstable values must be supplied");
});
});
@@ -72,12 +66,6 @@ describe("UnstableValue", () => {
});
it("should not permit falsey unstable values", () => {
try {
new UnstableValue("stable", null!);
// noinspection ExceptionCaughtLocallyJS
throw new Error("Failed to fail");
} catch (e) {
expect((<Error>e).message).toBe("Unstable value must be supplied");
}
expect(() => new UnstableValue("stable", null!)).toThrow("Unstable value must be supplied");
});
});
+7 -9
View File
@@ -27,16 +27,14 @@ class EventSource extends EventEmitter {
}
doAnError() {
this.emit('error');
this.emit("error");
}
}
class EventTarget extends EventEmitter {
class EventTarget extends EventEmitter {}
}
describe("ReEmitter", function() {
it("Re-Emits events with the same args", function() {
describe("ReEmitter", function () {
it("Re-Emits events with the same args", function () {
const src = new EventSource();
const tgt = new EventTarget();
@@ -53,18 +51,18 @@ describe("ReEmitter", function() {
expect(handler).toHaveBeenCalledWith("foo", "bar", src);
});
it("Doesn't throw if no handler for 'error' event", function() {
it("Doesn't throw if no handler for 'error' event", function () {
const src = new EventSource();
const tgt = new EventTarget();
const reEmitter = new ReEmitter(tgt);
reEmitter.reEmit(src, ['error']);
reEmitter.reEmit(src, ["error"]);
// without the workaround in ReEmitter, this would throw
src.doAnError();
const handler = jest.fn();
tgt.on('error', handler);
tgt.on("error", handler);
src.doAnError();
+116
View File
@@ -0,0 +1,116 @@
import { ConnectionError } from "../../src/http-api/errors";
import { ClientEvent, MatrixClient, Store } from "../../src/client";
import { ToDeviceMessageQueue } from "../../src/ToDeviceMessageQueue";
import { getMockClientWithEventEmitter } from "../test-utils/client";
import { StubStore } from "../../src/store/stub";
import { IndexedToDeviceBatch } from "../../src/models/ToDeviceMessage";
import { SyncState } from "../../src/sync";
import { defer } from "../../src/utils";
describe("onResumedSync", () => {
let batch: IndexedToDeviceBatch | null;
let shouldFailSendToDevice: Boolean;
let onSendToDeviceFailure: () => void;
let onSendToDeviceSuccess: () => void;
let resumeSync: (newState: SyncState, oldState: SyncState) => void;
let store: Store;
let mockClient: MatrixClient;
let queue: ToDeviceMessageQueue;
beforeEach(() => {
batch = {
id: 0,
txnId: "123",
eventType: "m.dummy",
batch: [],
};
shouldFailSendToDevice = true;
onSendToDeviceFailure = () => {};
onSendToDeviceSuccess = () => {};
resumeSync = (newState, oldState) => {
shouldFailSendToDevice = false;
mockClient.emit(ClientEvent.Sync, newState, oldState);
};
store = new StubStore();
store.getOldestToDeviceBatch = jest.fn().mockImplementation(() => {
return batch;
});
store.removeToDeviceBatch = jest.fn().mockImplementation(() => {
batch = null;
});
mockClient = getMockClientWithEventEmitter({});
mockClient.store = store;
mockClient.sendToDevice = jest.fn().mockImplementation(async () => {
if (shouldFailSendToDevice) {
await Promise.reject(new ConnectionError("")).finally(() => {
setTimeout(onSendToDeviceFailure, 0);
});
} else {
await Promise.resolve({}).finally(() => {
setTimeout(onSendToDeviceSuccess, 0);
});
}
});
queue = new ToDeviceMessageQueue(mockClient);
});
it("resends queue after connectivity restored", async () => {
const deferred = defer();
onSendToDeviceFailure = () => {
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
resumeSync(SyncState.Syncing, SyncState.Catchup);
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(2);
};
onSendToDeviceSuccess = () => {
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(3);
expect(store.removeToDeviceBatch).toHaveBeenCalled();
deferred.resolve();
};
queue.start();
return deferred.promise;
});
it("does not resend queue if client sync still catching up", async () => {
const deferred = defer();
onSendToDeviceFailure = () => {
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
resumeSync(SyncState.Catchup, SyncState.Catchup);
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
deferred.resolve();
};
queue.start();
return deferred.promise;
});
it("does not resend queue if connectivity restored after queue stopped", async () => {
const deferred = defer();
onSendToDeviceFailure = () => {
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
queue.stop();
resumeSync(SyncState.Syncing, SyncState.Catchup);
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
deferred.resolve();
};
queue.start();
return deferred.promise;
});
});
File diff suppressed because it is too large Load Diff
+108 -103
View File
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { REFERENCE_RELATION } from "matrix-events-sdk";
import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location";
import { M_TOPIC } from "../../src/@types/topic";
import {
@@ -25,24 +23,20 @@ import {
parseBeaconContent,
parseTopicContent,
} from "../../src/content-helpers";
import { REFERENCE_RELATION } from "../../src/@types/extensible_events";
describe('Beacon content helpers', () => {
describe('makeBeaconInfoContent()', () => {
describe("Beacon content helpers", () => {
describe("makeBeaconInfoContent()", () => {
const mockDateNow = 123456789;
beforeEach(() => {
jest.spyOn(global.Date, 'now').mockReturnValue(mockDateNow);
jest.spyOn(global.Date, "now").mockReturnValue(mockDateNow);
});
afterAll(() => {
jest.spyOn(global.Date, 'now').mockRestore();
jest.spyOn(global.Date, "now").mockRestore();
});
it('create fully defined event content', () => {
expect(makeBeaconInfoContent(
1234,
true,
'nice beacon_info',
LocationAssetType.Pin,
)).toEqual({
description: 'nice beacon_info',
it("create fully defined event content", () => {
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual({
description: "nice beacon_info",
timeout: 1234,
live: true,
[M_TIMESTAMP.name]: mockDateNow,
@@ -52,78 +46,72 @@ describe('Beacon content helpers', () => {
});
});
it('defaults timestamp to current time', () => {
expect(makeBeaconInfoContent(
1234,
true,
'nice beacon_info',
LocationAssetType.Pin,
)).toEqual(expect.objectContaining({
[M_TIMESTAMP.name]: mockDateNow,
}));
it("defaults timestamp to current time", () => {
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual(
expect.objectContaining({
[M_TIMESTAMP.name]: mockDateNow,
}),
);
});
it('uses timestamp when provided', () => {
expect(makeBeaconInfoContent(
1234,
true,
'nice beacon_info',
LocationAssetType.Pin,
99999,
)).toEqual(expect.objectContaining({
[M_TIMESTAMP.name]: 99999,
}));
it("uses timestamp when provided", () => {
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin, 99999)).toEqual(
expect.objectContaining({
[M_TIMESTAMP.name]: 99999,
}),
);
});
it('defaults asset type to self when not set', () => {
expect(makeBeaconInfoContent(
1234,
true,
'nice beacon_info',
// no assetType passed
)).toEqual(expect.objectContaining({
[M_ASSET.name]: {
type: LocationAssetType.Self,
},
}));
it("defaults asset type to self when not set", () => {
expect(
makeBeaconInfoContent(
1234,
true,
"nice beacon_info",
// no assetType passed
),
).toEqual(
expect.objectContaining({
[M_ASSET.name]: {
type: LocationAssetType.Self,
},
}),
);
});
});
describe('makeBeaconContent()', () => {
it('creates event content without description', () => {
expect(makeBeaconContent(
'geo:foo',
123,
'$1234',
// no description
)).toEqual({
describe("makeBeaconContent()", () => {
it("creates event content without description", () => {
expect(
makeBeaconContent(
"geo:foo",
123,
"$1234",
// no description
),
).toEqual({
[M_LOCATION.name]: {
description: undefined,
uri: 'geo:foo',
uri: "geo:foo",
},
[M_TIMESTAMP.name]: 123,
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: '$1234',
event_id: "$1234",
},
});
});
it('creates event content with description', () => {
expect(makeBeaconContent(
'geo:foo',
123,
'$1234',
'test description',
)).toEqual({
it("creates event content with description", () => {
expect(makeBeaconContent("geo:foo", 123, "$1234", "test description")).toEqual({
[M_LOCATION.name]: {
description: 'test description',
uri: 'geo:foo',
description: "test description",
uri: "geo:foo",
},
[M_TIMESTAMP.name]: 123,
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: '$1234',
event_id: "$1234",
},
});
});
@@ -190,64 +178,81 @@ describe('Beacon content helpers', () => {
});
});
describe('Topic content helpers', () => {
describe('makeTopicContent()', () => {
it('creates fully defined event content without html', () => {
describe("Topic content helpers", () => {
describe("makeTopicContent()", () => {
it("creates fully defined event content without html", () => {
expect(makeTopicContent("pizza")).toEqual({
topic: "pizza",
[M_TOPIC.name]: [{
body: "pizza",
mimetype: "text/plain",
}],
[M_TOPIC.name]: [
{
body: "pizza",
mimetype: "text/plain",
},
],
});
});
it('creates fully defined event content with html', () => {
it("creates fully defined event content with html", () => {
expect(makeTopicContent("pizza", "<b>pizza</b>")).toEqual({
topic: "pizza",
[M_TOPIC.name]: [{
body: "pizza",
mimetype: "text/plain",
}, {
body: "<b>pizza</b>",
mimetype: "text/html",
}],
[M_TOPIC.name]: [
{
body: "pizza",
mimetype: "text/plain",
},
{
body: "<b>pizza</b>",
mimetype: "text/html",
},
],
});
});
});
describe('parseTopicContent()', () => {
it('parses event content with plain text topic without mimetype', () => {
expect(parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [{
body: "pizza",
}],
})).toEqual({
describe("parseTopicContent()", () => {
it("parses event content with plain text topic without mimetype", () => {
expect(
parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [
{
body: "pizza",
},
],
}),
).toEqual({
text: "pizza",
});
});
it('parses event content with plain text topic', () => {
expect(parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [{
body: "pizza",
mimetype: "text/plain",
}],
})).toEqual({
it("parses event content with plain text topic", () => {
expect(
parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [
{
body: "pizza",
mimetype: "text/plain",
},
],
}),
).toEqual({
text: "pizza",
});
});
it('parses event content with html topic', () => {
expect(parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [{
body: "<b>pizza</b>",
mimetype: "text/html",
}],
})).toEqual({
it("parses event content with html topic", () => {
expect(
parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [
{
body: "<b>pizza</b>",
mimetype: "text/html",
},
],
}),
).toEqual({
text: "pizza",
html: "<b>pizza</b>",
});
+32 -42
View File
@@ -16,60 +16,50 @@ limitations under the License.
import { getHttpUriForMxc } from "../../src/content-repo";
describe("ContentRepo", function() {
describe("ContentRepo", function () {
const baseUrl = "https://my.home.server";
describe("getHttpUriForMxc", function() {
it("should do nothing to HTTP URLs when allowing direct links", function() {
describe("getHttpUriForMxc", function () {
it("should do nothing to HTTP URLs when allowing direct links", function () {
const httpUrl = "http://example.com/image.jpeg";
expect(
getHttpUriForMxc(
baseUrl, httpUrl, undefined, undefined, undefined, true,
),
).toEqual(httpUrl);
expect(getHttpUriForMxc(baseUrl, httpUrl, undefined, undefined, undefined, true)).toEqual(httpUrl);
});
it("should return the empty string HTTP URLs by default", function() {
it("should return the empty string HTTP URLs by default", function () {
const httpUrl = "http://example.com/image.jpeg";
expect(getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
});
it("should return a download URL if no width/height/resize are specified",
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
);
});
it("should return the empty string for null input", function() {
expect(getHttpUriForMxc(null as any, '')).toEqual("");
it("should return a download URL if no width/height/resize are specified", function () {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v3/download/server.name/resourceid",
);
});
it("should return a thumbnail URL if a width/height/resize is specified",
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
"?width=32&height=64&method=crop",
);
});
it("should return the empty string for null input", function () {
expect(getHttpUriForMxc(null as any, "")).toEqual("");
});
it("should put fragments from mxc:// URIs after any query parameters",
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
"?width=32#automade",
);
});
it("should return a thumbnail URL if a width/height/resize is specified", function () {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
baseUrl + "/_matrix/media/v3/thumbnail/server.name/resourceid" + "?width=32&height=64&method=crop",
);
});
it("should put fragments from mxc:// URIs at the end of the HTTP URI",
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
);
});
it("should put fragments from mxc:// URIs after any query parameters", function () {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
baseUrl + "/_matrix/media/v3/thumbnail/server.name/resourceid" + "?width=32#automade",
);
});
it("should put fragments from mxc:// URIs at the end of the HTTP URI", function () {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v3/download/server.name/resourceid#automade",
);
});
});
});
+649 -414
View File
File diff suppressed because it is too large Load Diff
+121 -138
View File
@@ -14,28 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import '../../olm-loader';
import {
CrossSigningInfo,
createCryptoStoreCacheCallbacks,
} from '../../../src/crypto/CrossSigning';
import {
IndexedDBCryptoStore,
} from '../../../src/crypto/store/indexeddb-crypto-store';
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
import 'fake-indexeddb/auto';
import 'jest-localstorage-mock';
import "../../olm-loader";
import { CrossSigningInfo, createCryptoStoreCacheCallbacks } from "../../../src/crypto/CrossSigning";
import { IndexedDBCryptoStore } from "../../../src/crypto/store/indexeddb-crypto-store";
import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
import "fake-indexeddb/auto";
import "jest-localstorage-mock";
import { OlmDevice } from "../../../src/crypto/OlmDevice";
import { logger } from '../../../src/logger';
import { logger } from "../../../src/logger";
const userId = "@alice:example.com";
// Private key for tests only
const testKey = new Uint8Array([
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82, 0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef, 0xae, 0xb1, 0x05,
0xc1, 0xe7, 0x62, 0x78, 0xa6, 0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
]);
const types = [
@@ -50,13 +43,13 @@ badKey[0] ^= 1;
const masterKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
describe("CrossSigningInfo.getCrossSigningKey", function() {
describe("CrossSigningInfo.getCrossSigningKey", function () {
if (!global.Olm) {
logger.warn('Not running megolm backup unit tests: libolm not present');
logger.warn("Not running megolm backup unit tests: libolm not present");
return;
}
beforeAll(function() {
beforeAll(function () {
return global.Olm.init();
});
@@ -65,13 +58,12 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
await expect(info.getCrossSigningKey("master")).rejects.toThrow();
});
it.each(types)("should throw if the callback returns falsey",
async ({ type, shouldCache }) => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: async () => false as unknown as Uint8Array,
});
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
it.each(types)("should throw if the callback returns falsey", async ({ type, shouldCache }) => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: async () => false as unknown as Uint8Array,
});
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
});
it("should throw if the expected key doesn't come back", async () => {
const info = new CrossSigningInfo(userId, {
@@ -96,63 +88,8 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
}
});
it.each(types)("should request a key from the cache callback (if set)" +
" and does not call app if one is found" +
" %o",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockImplementation(() => {
if (shouldCache) {
return Promise.reject(new Error("Regular callback called"));
} else {
return Promise.resolve(testKey);
}
});
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
if (shouldCache) {
expect(getCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
}
});
it.each(types)("should store a key with the cache callback (if set)",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(shouldCache ? 1 : 0);
if (shouldCache) {
expect(storeCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
expect(storeCrossSigningKeyCache.mock.calls[0][1]).toBe(testKey);
}
});
it.each(types)("does not store a bad key to the cache",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(badKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ storeCrossSigningKeyCache },
);
await expect(info.getCrossSigningKey(type, masterKeyPub)).rejects.toThrow();
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(0);
});
it.each(types)("does not store a value to the cache if it came from the cache",
it.each(types)(
"should request a key from the cache callback (if set)" + " and does not call app if one is found" + " %o",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockImplementation(() => {
if (shouldCache) {
@@ -162,56 +99,99 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
}
});
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockRejectedValue(
new Error("Tried to store a value from cache"),
);
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { getCrossSigningKeyCache });
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKeyCache).toHaveBeenCalledTimes(shouldCache ? 1 : 0);
if (shouldCache) {
// eslint-disable-next-line jest/no-conditional-expect
expect(getCrossSigningKeyCache).toHaveBeenLastCalledWith(type, expect.any(String));
}
},
);
it.each(types)("should store a key with the cache callback (if set)", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { storeCrossSigningKeyCache });
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(storeCrossSigningKeyCache).toHaveBeenCalledTimes(shouldCache ? 1 : 0);
if (shouldCache) {
// eslint-disable-next-line jest/no-conditional-expect
expect(storeCrossSigningKeyCache).toHaveBeenLastCalledWith(type, testKey);
}
});
it.each(types)("does not store a bad key to the cache", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(badKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { storeCrossSigningKeyCache });
await expect(info.getCrossSigningKey(type, masterKeyPub)).rejects.toThrow();
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(0);
});
it.each(types)("does not store a value to the cache if it came from the cache", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockImplementation(() => {
if (shouldCache) {
return Promise.reject(new Error("Regular callback called"));
} else {
return Promise.resolve(testKey);
}
});
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockRejectedValue(new Error("Tried to store a value from cache"));
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(0);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
});
it.each(types)(
"requests a key from the cache callback (if set) and then calls app" + " if one is not found",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const storeCrossSigningKeyCache = jest.fn();
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(0);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
});
expect(getCrossSigningKey.mock.calls.length).toBe(1);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
it.each(types)("requests a key from the cache callback (if set) and then calls app" +
" if one is not found", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const storeCrossSigningKeyCache = jest.fn();
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKey.mock.calls.length).toBe(1);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
/* Also expect that the cache gets updated */
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
},
);
/* Also expect that the cache gets updated */
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
});
it.each(types)(
"requests a key from the cache callback (if set) and then" + " calls app if that key doesn't match",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(badKey);
const storeCrossSigningKeyCache = jest.fn();
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKey.mock.calls.length).toBe(1);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
it.each(types)("requests a key from the cache callback (if set) and then" +
" calls app if that key doesn't match", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(badKey);
const storeCrossSigningKeyCache = jest.fn();
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKey.mock.calls.length).toBe(1);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
/* Also expect that the cache gets updated */
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
});
/* Also expect that the cache gets updated */
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
},
);
});
/*
@@ -219,20 +199,21 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
* it's not possible to get one in normal execution unless you hack as we do here.
*/
describe.each([
["IndexedDBCryptoStore",
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["LocalStorageCryptoStore",
() => new IndexedDBCryptoStore(undefined!, "tests")],
["MemoryCryptoStore", () => {
const store = new IndexedDBCryptoStore(undefined!, "tests");
// @ts-ignore set private properties
store._backend = new MemoryCryptoStore();
// @ts-ignore
store._backendPromise = Promise.resolve(store._backend);
return store;
}],
])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function(name, dbFactory) {
let store;
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["LocalStorageCryptoStore", () => new IndexedDBCryptoStore(undefined!, "tests")],
[
"MemoryCryptoStore",
() => {
const store = new IndexedDBCryptoStore(undefined!, "tests");
// @ts-ignore set private properties
store._backend = new MemoryCryptoStore();
// @ts-ignore
store._backendPromise = Promise.resolve(store._backend);
return store;
},
],
])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function (name, dbFactory) {
let store: IndexedDBCryptoStore;
beforeAll(() => {
store = dbFactory();
@@ -245,8 +226,10 @@ describe.each([
it("should cache data to the store and retrieve it", async () => {
await store.startup();
const olmDevice = new OlmDevice(store);
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } =
createCryptoStoreCacheCallbacks(store, olmDevice);
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } = createCryptoStoreCacheCallbacks(
store,
olmDevice,
);
await storeCrossSigningKeyCache!("self_signing", testKey);
// If we've not saved anything, don't expect anything
+87 -92
View File
@@ -22,33 +22,29 @@ import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store
import { DeviceList } from "../../../src/crypto/DeviceList";
import { IDownloadKeyResult, MatrixClient } from "../../../src";
import { OlmDevice } from "../../../src/crypto/OlmDevice";
import { CryptoStore } from "../../../src/crypto/store/base";
const signedDeviceList: IDownloadKeyResult = {
"failures": {},
"device_keys": {
failures: {},
device_keys: {
"@test1:sw1v.org": {
"HGKAWHRVJQ": {
"signatures": {
HGKAWHRVJQ: {
signatures: {
"@test1:sw1v.org": {
"ed25519:HGKAWHRVJQ":
"8PB450fxKDn5s8IiRZ2N2t6MiueQYVRLHFEzqIi1eLdxx1w" +
"XEPC1/1Uz9T4gwnKlMVAKkhB5hXQA/3kjaeLABw",
},
},
"user_id": "@test1:sw1v.org",
"keys": {
"ed25519:HGKAWHRVJQ":
"0gI/T6C+mn1pjtvnnW2yB2l1IIBb/5ULlBXi/LXFSEQ",
"curve25519:HGKAWHRVJQ":
"mbIZED1dBsgIgkgzxDpxKkJmsr4hiWlGzQTvUnQe3RY",
user_id: "@test1:sw1v.org",
keys: {
"ed25519:HGKAWHRVJQ": "0gI/T6C+mn1pjtvnnW2yB2l1IIBb/5ULlBXi/LXFSEQ",
"curve25519:HGKAWHRVJQ": "mbIZED1dBsgIgkgzxDpxKkJmsr4hiWlGzQTvUnQe3RY",
},
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2",
],
"device_id": "HGKAWHRVJQ",
"unsigned": {
"device_display_name": "",
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "HGKAWHRVJQ",
unsigned: {
device_display_name: "",
},
},
},
@@ -56,50 +52,45 @@ const signedDeviceList: IDownloadKeyResult = {
};
const signedDeviceList2: IDownloadKeyResult = {
"failures": {},
"device_keys": {
failures: {},
device_keys: {
"@test2:sw1v.org": {
"QJVRHWAKGH": {
"signatures": {
QJVRHWAKGH: {
signatures: {
"@test2:sw1v.org": {
"ed25519:QJVRHWAKGH":
"w1xxdLe1iIqzEFHLRVYQeuiM6t2N2ZRiI8s5nDKxf054BP8" +
"1CPEX/AQXh5BhkKAVMlKnwg4T9zU1/wBALeajk3",
},
},
"user_id": "@test2:sw1v.org",
"keys": {
"ed25519:QJVRHWAKGH":
"Ig0/C6T+bBII1l2By2Wnnvtjp1nm/iXBlLU5/QESFXL",
"curve25519:QJVRHWAKGH":
"YR3eQnUvTQzGlWih4rsmJkKxpDxzgkgIgsBd1DEZIbm",
user_id: "@test2:sw1v.org",
keys: {
"ed25519:QJVRHWAKGH": "Ig0/C6T+bBII1l2By2Wnnvtjp1nm/iXBlLU5/QESFXL",
"curve25519:QJVRHWAKGH": "YR3eQnUvTQzGlWih4rsmJkKxpDxzgkgIgsBd1DEZIbm",
},
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2",
],
"device_id": "QJVRHWAKGH",
"unsigned": {
"device_display_name": "",
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "QJVRHWAKGH",
unsigned: {
device_display_name: "",
},
},
},
},
};
describe('DeviceList', function() {
let downloadSpy;
let cryptoStore;
describe("DeviceList", function () {
let downloadSpy: jest.Mock;
let cryptoStore: CryptoStore;
let deviceLists: DeviceList[] = [];
beforeEach(function() {
beforeEach(function () {
deviceLists = [];
downloadSpy = jest.fn();
cryptoStore = new MemoryCryptoStore();
});
afterEach(function() {
afterEach(function () {
for (const dl of deviceLists) {
dl.stop();
}
@@ -108,94 +99,98 @@ describe('DeviceList', function() {
function createTestDeviceList(keyDownloadChunkSize = 250) {
const baseApis = {
downloadKeysForUsers: downloadSpy,
getUserId: () => '@test1:sw1v.org',
deviceId: 'HGKAWHRVJQ',
getUserId: () => "@test1:sw1v.org",
deviceId: "HGKAWHRVJQ",
} as unknown as MatrixClient;
const mockOlm = {
verifySignature: function(key, message, signature) {},
verifySignature: function (key: string, message: string, signature: string) {},
} as unknown as OlmDevice;
const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize);
deviceLists.push(dl);
return dl;
}
it("should successfully download and store device keys", function() {
it("should successfully download and store device keys", function () {
const dl = createTestDeviceList();
dl.startTrackingDeviceList('@test1:sw1v.org');
dl.startTrackingDeviceList("@test1:sw1v.org");
const queryDefer1 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValue(queryDefer1.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
expect(downloadSpy).toHaveBeenCalledWith(["@test1:sw1v.org"], {});
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
return prom1.then(() => {
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
const storedKeys = dl.getRawStoredDevicesForUser("@test1:sw1v.org");
expect(Object.keys(storedKeys)).toEqual(["HGKAWHRVJQ"]);
dl.stop();
});
});
it("should have an outdated devicelist on an invalidation while an " +
"update is in progress", function() {
it("should have an outdated devicelist on an invalidation while an update is in progress", async function () {
const dl = createTestDeviceList();
dl.startTrackingDeviceList('@test1:sw1v.org');
dl.startTrackingDeviceList("@test1:sw1v.org");
const queryDefer1 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValue(queryDefer1.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
expect(downloadSpy).toHaveBeenCalledWith(["@test1:sw1v.org"], {});
downloadSpy.mockReset();
// outdated notif arrives while the request is in flight.
const queryDefer2 = utils.defer();
downloadSpy.mockReturnValue(queryDefer2.promise);
dl.invalidateUserDeviceList('@test1:sw1v.org');
dl.invalidateUserDeviceList("@test1:sw1v.org");
dl.refreshOutdatedDeviceLists();
dl.saveIfDirty().then(() => {
// the first request completes
queryDefer1.resolve({
failures: {},
device_keys: {
'@test1:sw1v.org': {},
},
await dl
.saveIfDirty()
.then(() => {
// the first request completes
queryDefer1.resolve({
failures: {},
device_keys: {
"@test1:sw1v.org": {},
},
});
return prom1;
})
.then(async () => {
// uh-oh; user restarts before second request completes. The new instance
// should know we never got a complete device list.
logger.log("Creating new devicelist to simulate app reload");
downloadSpy.mockReset();
const dl2 = createTestDeviceList();
await dl2.load();
const queryDefer3 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValue(queryDefer3.promise);
const prom3 = dl2.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(["@test1:sw1v.org"], {});
dl2.stop();
queryDefer3.resolve(utils.deepCopy(signedDeviceList));
// allow promise chain to complete
return prom3;
})
.then(() => {
const storedKeys = dl.getRawStoredDevicesForUser("@test1:sw1v.org");
expect(Object.keys(storedKeys)).toEqual(["HGKAWHRVJQ"]);
dl.stop();
});
return prom1;
}).then(() => {
// uh-oh; user restarts before second request completes. The new instance
// should know we never got a complete device list.
logger.log("Creating new devicelist to simulate app reload");
downloadSpy.mockReset();
const dl2 = createTestDeviceList();
const queryDefer3 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValue(queryDefer3.promise);
const prom3 = dl2.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
dl2.stop();
queryDefer3.resolve(utils.deepCopy(signedDeviceList));
// allow promise chain to complete
return prom3;
}).then(() => {
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
dl.stop();
});
});
it("should download device keys in batches", function() {
it("should download device keys in batches", function () {
const dl = createTestDeviceList(1);
dl.startTrackingDeviceList('@test1:sw1v.org');
dl.startTrackingDeviceList('@test2:sw1v.org');
dl.startTrackingDeviceList("@test1:sw1v.org");
dl.startTrackingDeviceList("@test2:sw1v.org");
const queryDefer1 = utils.defer<IDownloadKeyResult>();
downloadSpy.mockReturnValueOnce(queryDefer1.promise);
@@ -203,17 +198,17 @@ describe('DeviceList', function() {
downloadSpy.mockReturnValueOnce(queryDefer2.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toBeCalledTimes(2);
expect(downloadSpy).toHaveBeenNthCalledWith(1, ['@test1:sw1v.org'], {});
expect(downloadSpy).toHaveBeenNthCalledWith(2, ['@test2:sw1v.org'], {});
expect(downloadSpy).toHaveBeenCalledTimes(2);
expect(downloadSpy).toHaveBeenNthCalledWith(1, ["@test1:sw1v.org"], {});
expect(downloadSpy).toHaveBeenNthCalledWith(2, ["@test2:sw1v.org"], {});
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
queryDefer2.resolve(utils.deepCopy(signedDeviceList2));
return prom1.then(() => {
const storedKeys1 = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys1)).toEqual(['HGKAWHRVJQ']);
const storedKeys2 = dl.getRawStoredDevicesForUser('@test2:sw1v.org');
expect(Object.keys(storedKeys2)).toEqual(['QJVRHWAKGH']);
const storedKeys1 = dl.getRawStoredDevicesForUser("@test1:sw1v.org");
expect(Object.keys(storedKeys1)).toEqual(["HGKAWHRVJQ"]);
const storedKeys2 = dl.getRawStoredDevicesForUser("@test2:sw1v.org");
expect(Object.keys(storedKeys2)).toEqual(["QJVRHWAKGH"]);
dl.stop();
});
});
File diff suppressed because it is too large Load Diff
+65 -89
View File
@@ -15,15 +15,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MockedObject } from 'jest-mock';
import { MockedObject } from "jest-mock";
import '../../../olm-loader';
import "../../../olm-loader";
import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
import { logger } from "../../../../src/logger";
import { OlmDevice } from "../../../../src/crypto/OlmDevice";
import * as olmlib from "../../../../src/crypto/olmlib";
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
import { MatrixClient } from '../../../../src';
import { MatrixClient } from "../../../../src";
function makeOlmDevice() {
const cryptoStore = new MemoryCryptoStore();
@@ -31,73 +31,72 @@ function makeOlmDevice() {
return olmDevice;
}
async function setupSession(initiator, opponent) {
async function setupSession(initiator: OlmDevice, opponent: OlmDevice) {
await opponent.generateOneTimeKeys(1);
const keys = await opponent.getOneTimeKeys();
const firstKey = Object.values(keys['curve25519'])[0];
const firstKey = Object.values(keys["curve25519"])[0];
const sid = await initiator.createOutboundSession(
opponent.deviceCurve25519Key, firstKey,
);
const sid = await initiator.createOutboundSession(opponent.deviceCurve25519Key!, firstKey);
return sid;
}
describe("OlmDevice", function() {
function alwaysSucceed<T>(promise: Promise<T>): Promise<T | void> {
// swallow any exception thrown by a promise, so that
// Promise.all doesn't abort
return promise.catch(() => {});
}
describe("OlmDevice", function () {
if (!global.Olm) {
logger.warn('Not running megolm unit tests: libolm not present');
logger.warn("Not running megolm unit tests: libolm not present");
return;
}
beforeAll(function() {
beforeAll(function () {
return global.Olm.init();
});
let aliceOlmDevice: OlmDevice;
let bobOlmDevice: OlmDevice;
beforeEach(async function() {
beforeEach(async function () {
aliceOlmDevice = makeOlmDevice();
bobOlmDevice = makeOlmDevice();
await aliceOlmDevice.init();
await bobOlmDevice.init();
});
describe('olm', function() {
it("can decrypt messages", async function() {
describe("olm", function () {
it("can decrypt messages", async function () {
const sid = await setupSession(aliceOlmDevice, bobOlmDevice);
const ciphertext = await aliceOlmDevice.encryptMessage(
const ciphertext = (await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key!,
sid,
"The olm or proteus is an aquatic salamander in the family Proteidae",
) as any; // OlmDevice.encryptMessage has incorrect return type
)) as any; // OlmDevice.encryptMessage has incorrect return type
const result = await bobOlmDevice.createInboundSession(
aliceOlmDevice.deviceCurve25519Key!,
ciphertext.type,
ciphertext.body,
);
expect(result.payload).toEqual(
"The olm or proteus is an aquatic salamander in the family Proteidae",
);
expect(result.payload).toEqual("The olm or proteus is an aquatic salamander in the family Proteidae");
});
it('exports picked account and olm sessions', async function() {
it("exports picked account and olm sessions", async function () {
const sessionId = await setupSession(aliceOlmDevice, bobOlmDevice);
const exported = await bobOlmDevice.export();
// At this moment only Alice (the “initiator” in setupSession) has a session
expect(exported.sessions).toEqual([]);
const MESSAGE = (
"The olm or proteus is an aquatic salamander"
+ " in the family Proteidae"
);
const ciphertext = await aliceOlmDevice.encryptMessage(
const MESSAGE = "The olm or proteus is an aquatic salamander" + " in the family Proteidae";
const ciphertext = (await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key!,
sessionId,
MESSAGE,
) as any; // OlmDevice.encryptMessage has incorrect return type
)) as any; // OlmDevice.encryptMessage has incorrect return type
const bobRecreatedOlmDevice = makeOlmDevice();
bobRecreatedOlmDevice.init({ fromExportedDevice: exported });
@@ -113,15 +112,12 @@ describe("OlmDevice", function() {
// this time we expect Bob to have a session to export
expect(exportedAgain.sessions).toHaveLength(1);
const MESSAGE_2 = (
"In contrast to most amphibians,"
+ " the olm is entirely aquatic"
);
const ciphertext2 = await aliceOlmDevice.encryptMessage(
const MESSAGE_2 = "In contrast to most amphibians," + " the olm is entirely aquatic";
const ciphertext2 = (await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key!,
sessionId,
MESSAGE_2,
) as any; // OlmDevice.encryptMessage has incorrect return type
)) as any; // OlmDevice.encryptMessage has incorrect return type
const bobRecreatedAgainOlmDevice = makeOlmDevice();
bobRecreatedAgainOlmDevice.init({ fromExportedDevice: exportedAgain });
@@ -136,7 +132,7 @@ describe("OlmDevice", function() {
expect(decrypted2).toEqual(MESSAGE_2);
});
it("creates only one session at a time", async function() {
it("creates only one session at a time", async function () {
// if we call ensureOlmSessionsForDevices multiple times, it should
// only try to create one session at a time, even if the server is
// slow
@@ -150,29 +146,26 @@ describe("OlmDevice", function() {
});
},
} as unknown as MockedObject<MatrixClient>;
const devicesByUser = {
"@bob:example.com": [
DeviceInfo.fromStorage({
keys: {
"curve25519:ABCDEFG": "akey",
},
}, "ABCDEFG"),
const devicesByUser = new Map([
[
"@bob:example.com",
[
DeviceInfo.fromStorage(
{
keys: {
"curve25519:ABCDEFG": "akey",
},
},
"ABCDEFG",
),
],
],
};
function alwaysSucceed(promise) {
// swallow any exception thrown by a promise, so that
// Promise.all doesn't abort
return promise.catch(() => {});
}
]);
// start two tasks that try to ensure that there's an olm session
const promises = Promise.all([
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUser,
)),
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUser,
)),
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUser)),
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUser)),
]);
await new Promise((resolve) => {
@@ -192,7 +185,7 @@ describe("OlmDevice", function() {
expect(count).toBe(2);
});
it("avoids deadlocks when two tasks are ensuring the same devices", async function() {
it("avoids deadlocks when two tasks are ensuring the same devices", async function () {
// This test checks whether `ensureOlmSessionsForDevices` properly
// handles multiple tasks in flight ensuring some set of devices in
// common without deadlocks.
@@ -208,60 +201,43 @@ describe("OlmDevice", function() {
},
} as unknown as MockedObject<MatrixClient>;
const deviceBobA = DeviceInfo.fromStorage({
keys: {
"curve25519:BOB-A": "akey",
const deviceBobA = DeviceInfo.fromStorage(
{
keys: {
"curve25519:BOB-A": "akey",
},
},
}, "BOB-A");
const deviceBobB = DeviceInfo.fromStorage({
keys: {
"curve25519:BOB-B": "bkey",
"BOB-A",
);
const deviceBobB = DeviceInfo.fromStorage(
{
keys: {
"curve25519:BOB-B": "bkey",
},
},
}, "BOB-B");
"BOB-B",
);
// There's no required ordering of devices per user, so here we
// create two different orderings so that each task reserves a
// device the other task needs before continuing.
const devicesByUserAB = {
"@bob:example.com": [
deviceBobA,
deviceBobB,
],
};
const devicesByUserBA = {
"@bob:example.com": [
deviceBobB,
deviceBobA,
],
};
const devicesByUserAB = new Map([["@bob:example.com", [deviceBobA, deviceBobB]]]);
const devicesByUserBA = new Map([["@bob:example.com", [deviceBobB, deviceBobA]]]);
function alwaysSucceed(promise) {
// swallow any exception thrown by a promise, so that
// Promise.all doesn't abort
return promise.catch(() => {});
}
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUserAB,
));
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUserAB));
// After a single tick through the first task, it should have
// claimed ownership of all devices to avoid deadlocking others.
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUserBA,
));
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUserBA));
// The second task should not have changed the ownership count, as
// it's waiting on the first task.
expect(Object.keys(aliceOlmDevice.sessionsInProgress).length).toBe(2);
// Track the tasks, but don't await them yet.
const promises = Promise.all([
task1,
task2,
]);
const promises = Promise.all([task1, task2]);
await new Promise((resolve) => {
setTimeout(resolve, 200);
+284 -259
View File
@@ -15,9 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MockedObject } from "jest-mock";
import '../../olm-loader';
import "../../olm-loader";
import { logger } from "../../../src/logger";
import * as olmlib from "../../../src/crypto/olmlib";
import { MatrixClient } from "../../../src/client";
@@ -30,27 +28,31 @@ import { Crypto } from "../../../src/crypto";
import { resetCrossSigningKeys } from "./crypto-utils";
import { BackupManager } from "../../../src/crypto/backup";
import { StubStore } from "../../../src/store/stub";
import { MatrixScheduler } from '../../../src';
import { IndexedDBCryptoStore, MatrixScheduler } from "../../../src";
import { CryptoStore } from "../../../src/crypto/store/base";
import { MegolmDecryption as MegolmDecryptionClass } from "../../../src/crypto/algorithms/megolm";
import { IKeyBackupInfo } from "../../../src/crypto/keybackup";
const Olm = global.Olm;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
const ROOM_ID = '!ROOM:ID';
const ROOM_ID = "!ROOM:ID";
const SESSION_ID = 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc';
const SESSION_ID = "o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc";
const ENCRYPTED_EVENT = new MatrixEvent({
type: 'm.room.encrypted',
room_id: '!ROOM:ID',
type: "m.room.encrypted",
room_id: "!ROOM:ID",
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: 'SENDER_CURVE25519',
algorithm: "m.megolm.v1.aes-sha2",
sender_key: "SENDER_CURVE25519",
session_id: SESSION_ID,
ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N'
+ 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl'
+ 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs',
ciphertext:
"AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N" +
"CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl" +
"mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs",
},
event_id: '$event1',
event_id: "$event1",
origin_server_ts: 1507753886000,
});
@@ -59,19 +61,20 @@ const CURVE25519_KEY_BACKUP_DATA = {
forwarded_count: 0,
is_verified: false,
session_data: {
ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw'
+ '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ'
+ 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9'
+ 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy'
+ 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF'
+ 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV'
+ '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv'
+ 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe'
+ 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf'
+ 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy'
+ 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg',
mac: '5lxYBHQU80M',
ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14',
ciphertext:
"2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw" +
"6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ" +
"Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9" +
"SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy" +
"Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF" +
"ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV" +
"4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv" +
"C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe" +
"Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf" +
"QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy" +
"iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg",
mac: "5lxYBHQU80M",
ephemeral: "/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14",
},
};
@@ -80,54 +83,60 @@ const AES256_KEY_BACKUP_DATA = {
forwarded_count: 0,
is_verified: false,
session_data: {
iv: 'b3Jqqvm5S9QdmXrzssspLQ',
ciphertext: 'GOOASO3E9ThogkG0zMjEduGLM3u9jHZTkS7AvNNbNj3q1znwk4OlaVKXce'
+ '7ynofiiYIiS865VlOqrKEEXv96XzRyUpgn68e3WsicwYl96EtjIEh/iY003PG2Qd'
+ 'EluT899Ax7PydpUHxEktbWckMppYomUR5q8x1KI1SsOQIiJaIGThmIMPANRCFiK0'
+ 'WQj+q+dnhzx4lt9AFqU5bKov8qKnw2qGYP7/+6RmJ0Kpvs8tG6lrcNDEHtFc2r0r'
+ 'KKubDypo0Vc8EWSwsAHdKa36ewRavpreOuE8Z9RLfY0QIR1ecXrMqW0CdGFr7H3P'
+ 'vcjF8sjwvQAavzxEKT1WMGizSMLeKWo2mgZ5cKnwV5HGUAw596JQvKs9laG2U89K'
+ 'YrT0sH30vi62HKzcBLcDkWkUSNYPz7UiZ1MM0L380UA+1ZOXSOmtBA9xxzzbc8Xd'
+ 'fRimVgklGdxrxjzuNLYhL2BvVH4oPWonD9j0bvRwE6XkimdbGQA8HB7UmXXjE8WA'
+ 'RgaDHkfzoA3g3aeQ',
mac: 'uR988UYgGL99jrvLLPX3V1ows+UYbktTmMxPAo2kxnU',
iv: "b3Jqqvm5S9QdmXrzssspLQ",
ciphertext:
"GOOASO3E9ThogkG0zMjEduGLM3u9jHZTkS7AvNNbNj3q1znwk4OlaVKXce" +
"7ynofiiYIiS865VlOqrKEEXv96XzRyUpgn68e3WsicwYl96EtjIEh/iY003PG2Qd" +
"EluT899Ax7PydpUHxEktbWckMppYomUR5q8x1KI1SsOQIiJaIGThmIMPANRCFiK0" +
"WQj+q+dnhzx4lt9AFqU5bKov8qKnw2qGYP7/+6RmJ0Kpvs8tG6lrcNDEHtFc2r0r" +
"KKubDypo0Vc8EWSwsAHdKa36ewRavpreOuE8Z9RLfY0QIR1ecXrMqW0CdGFr7H3P" +
"vcjF8sjwvQAavzxEKT1WMGizSMLeKWo2mgZ5cKnwV5HGUAw596JQvKs9laG2U89K" +
"YrT0sH30vi62HKzcBLcDkWkUSNYPz7UiZ1MM0L380UA+1ZOXSOmtBA9xxzzbc8Xd" +
"fRimVgklGdxrxjzuNLYhL2BvVH4oPWonD9j0bvRwE6XkimdbGQA8HB7UmXXjE8WA" +
"RgaDHkfzoA3g3aeQ",
mac: "uR988UYgGL99jrvLLPX3V1ows+UYbktTmMxPAo2kxnU",
},
};
const CURVE25519_BACKUP_INFO = {
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
version: '1',
version: "1",
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
};
const AES256_BACKUP_INFO = {
const AES256_BACKUP_INFO: IKeyBackupInfo = {
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
version: '1',
auth_data: {
// FIXME: add iv and mac
},
version: "1",
auth_data: {} as IKeyBackupInfo["auth_data"],
};
const keys = {};
const keys: Record<string, Uint8Array> = {};
function getCrossSigningKey(type) {
return keys[type];
function getCrossSigningKey(type: string) {
return Promise.resolve(keys[type]);
}
function saveCrossSigningKeys(k) {
function saveCrossSigningKeys(k: Record<string, Uint8Array>) {
Object.assign(keys, k);
}
function makeTestClient(cryptoStore) {
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
function makeTestScheduler(): MatrixScheduler {
return (["getQueueForEvent", "queueEvent", "removeEventFromQueue", "setProcessFunction"] as const).reduce(
(r, k) => {
r[k] = jest.fn();
return r;
},
{} as MatrixScheduler,
);
}
function makeTestClient(cryptoStore: CryptoStore) {
const scheduler = makeTestScheduler();
const store = new StubStore();
return new MatrixClient({
const client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
@@ -139,80 +148,81 @@ function makeTestClient(cryptoStore) {
cryptoStore: cryptoStore,
cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys },
});
// initialising the crypto library will trigger a key upload request, which we can stub out
client.uploadKeysRequest = jest.fn();
return client;
}
describe("MegolmBackup", function() {
describe("MegolmBackup", function () {
if (!global.Olm) {
logger.warn('Not running megolm backup unit tests: libolm not present');
logger.warn("Not running megolm backup unit tests: libolm not present");
return;
}
beforeAll(function() {
beforeAll(function () {
return Olm.init();
});
let olmDevice;
let mockOlmLib;
let mockCrypto;
let cryptoStore;
let megolmDecryption;
beforeEach(async function() {
mockCrypto = testUtils.mock(Crypto, 'Crypto');
let olmDevice: OlmDevice;
let mockOlmLib: typeof olmlib;
let mockCrypto: Crypto;
let cryptoStore: CryptoStore;
let megolmDecryption: MegolmDecryptionClass;
beforeEach(async function () {
mockCrypto = testUtils.mock(Crypto, "Crypto");
// @ts-ignore making mock
mockCrypto.backupManager = testUtils.mock(BackupManager, "BackupManager");
mockCrypto.backupKey = new Olm.PkEncryption();
mockCrypto.backupKey.set_recipient_key(
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
);
mockCrypto.backupInfo = CURVE25519_BACKUP_INFO;
mockCrypto.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
cryptoStore = new MemoryCryptoStore();
olmDevice = new OlmDevice(cryptoStore);
// we stub out the olm encryption bits
mockOlmLib = {};
mockOlmLib = {} as unknown as typeof olmlib;
mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
mockOlmLib.encryptMessageForDevice =
jest.fn().mockResolvedValue(undefined);
mockOlmLib.encryptMessageForDevice = jest.fn().mockResolvedValue(undefined);
});
describe("backup", function() {
let mockBaseApis;
describe("backup", function () {
let mockBaseApis: MatrixClient;
beforeEach(function() {
mockBaseApis = {};
beforeEach(function () {
mockBaseApis = {} as unknown as MatrixClient;
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
userId: "@user:id",
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
});
}) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
// clobber the setTimeout function to run 100x faster.
// ideally we would use lolex, but we have no oportunity
// to tick the clock between the first try and the retry.
const realSetTimeout = global.setTimeout;
jest.spyOn(global, 'setTimeout').mockImplementation(function(f, n) {
return realSetTimeout(f!, n!/100);
jest.spyOn(global, "setTimeout").mockImplementation(function (f, n) {
return realSetTimeout(f!, n! / 100);
});
});
afterEach(function() {
jest.spyOn(global, 'setTimeout').mockRestore();
afterEach(function () {
jest.spyOn(global, "setTimeout").mockRestore();
});
it('automatically calls the key back up', function() {
it("automatically calls the key back up", function () {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// construct a fake decrypted key event via the use of a mocked
// 'crypto' implementation.
const event = new MatrixEvent({
type: 'm.room.encrypted',
type: "m.room.encrypted",
});
event.getWireType = () => "m.room.encrypted";
event.getWireContent = () => {
@@ -222,9 +232,9 @@ describe("MegolmBackup", function() {
};
const decryptedData = {
clearEvent: {
type: 'm.room_key',
type: "m.room_key",
content: {
algorithm: 'm.megolm.v1.aes-sha2',
algorithm: "m.megolm.v1.aes-sha2",
room_id: ROOM_ID,
session_id: groupSession.session_id(),
session_key: groupSession.session_key(),
@@ -234,23 +244,27 @@ describe("MegolmBackup", function() {
claimedEd25519Key: "SENDER_ED25519",
};
mockCrypto.decryptEvent = function() {
mockCrypto.decryptEvent = function () {
return Promise.resolve(decryptedData);
};
mockCrypto.cancelRoomKeyRequest = function() {};
mockCrypto.cancelRoomKeyRequest = function () {};
// @ts-ignore readonly field write
mockCrypto.backupManager = {
backupGroupSession: jest.fn(),
};
return event.attemptDecryption(mockCrypto).then(() => {
return megolmDecryption.onRoomKeyEvent(event);
}).then(() => {
expect(mockCrypto.backupManager.backupGroupSession).toHaveBeenCalled();
});
return event
.attemptDecryption(mockCrypto)
.then(() => {
return megolmDecryption.onRoomKeyEvent(event);
})
.then(() => {
expect(mockCrypto.backupManager.backupGroupSession).toHaveBeenCalled();
});
});
it('sends backups to the server (Curve25519 version)', function() {
it("sends backups to the server (Curve25519 version)", function () {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
@@ -259,64 +273,62 @@ describe("MegolmBackup", function() {
const client = makeTestClient(cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
userId: "@user:id",
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
}) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto()
return client
.initCrypto()
.then(() => {
return cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice.pickleKey),
return cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined!,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
txn);
});
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn,
);
});
})
.then(async () => {
await client.enableKeyBackup({
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
version: '1',
version: "1",
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
});
let numCalls = 0;
return new Promise<void>((resolve, reject) => {
client.http.authedRequest = function<T>(
method, path, queryParams, data, opts,
): Promise<T> {
client.http.authedRequest = function (method, path, queryParams, data, opts): any {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({} as T);
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect(queryParams?.version).toBe("1");
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
resolve();
return Promise.resolve({} as T);
return Promise.resolve({});
};
client.crypto!.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
@@ -329,7 +341,7 @@ describe("MegolmBackup", function() {
});
});
it('sends backups to the server (AES-256 version)', function() {
it("sends backups to the server (AES-256 version)", function () {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
@@ -338,42 +350,42 @@ describe("MegolmBackup", function() {
const client = makeTestClient(cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
userId: "@user:id",
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
}) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto()
return client
.initCrypto()
.then(() => {
return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32));
})
.then(() => {
return cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice.pickleKey),
return cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined!,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
txn);
});
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn,
);
});
})
.then(async () => {
await client.enableKeyBackup({
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
version: '1',
version: "1",
auth_data: {
iv: "PsCAtR7gMc4xBd9YS3A9Ow",
mac: "ZSDsTFEZK7QzlauCLMleUcX96GQZZM7UNtk4sripSqQ",
@@ -381,25 +393,23 @@ describe("MegolmBackup", function() {
});
let numCalls = 0;
return new Promise<void>((resolve, reject) => {
client.http.authedRequest = function<T>(
method, path, queryParams, data, opts,
): Promise<T> {
client.http.authedRequest = function (method, path, queryParams, data, opts): any {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({} as T);
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect(queryParams?.version).toBe("1");
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
resolve();
return Promise.resolve({} as T);
return Promise.resolve({});
};
client.crypto!.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
@@ -412,7 +422,7 @@ describe("MegolmBackup", function() {
});
});
it('signs backups with the cross-signing master key', async function() {
it("signs backups with the cross-signing master key", async function () {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
@@ -421,35 +431,41 @@ describe("MegolmBackup", function() {
const client = makeTestClient(cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
userId: "@user:id",
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
}) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
await client.initCrypto();
client.uploadDeviceSigningKeys = async function(e) {return {};};
client.uploadKeySignatures = async function(e) {return { failures: {} };};
client.uploadDeviceSigningKeys = async function (e) {
return {};
};
client.uploadKeySignatures = async function (e) {
return { failures: {} };
};
await resetCrossSigningKeys(client);
let numCalls = 0;
await Promise.all([
new Promise<void>((resolve, reject) => {
let backupInfo;
client.http.authedRequest = function(
method, path, queryParams, data, opts,
) {
let backupInfo: Record<string, any> | BodyInit | undefined;
client.http.authedRequest = function (method, path, queryParams, data, opts): any {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(2);
/* eslint-disable jest/no-conditional-expect */
if (numCalls === 1) {
expect(method).toBe("POST");
expect(path).toBe("/room_keys/version");
try {
// make sure auth_data is signed by the master key
olmlib.pkVerify(
(data as Record<string, any>).auth_data, client.getCrossSigningId()!, "@alice:bar",
(data as Record<string, any>).auth_data,
client.getCrossSigningId()!,
"@alice:bar",
);
} catch (e) {
reject(e);
@@ -467,6 +483,7 @@ describe("MegolmBackup", function() {
reject(new Error("authedRequest called too many times"));
return Promise.resolve({});
}
/* eslint-enable jest/no-conditional-expect */
};
}),
client.createKeyBackupVersion({
@@ -480,16 +497,13 @@ describe("MegolmBackup", function() {
client.stopClient();
});
it('retries when a backup fails', async function() {
it("retries when a backup fails", async function () {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key());
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
const scheduler = makeTestScheduler();
const store = new StubStore();
const client = new MatrixClient({
baseUrl: "https://my.home.server",
@@ -502,39 +516,40 @@ describe("MegolmBackup", function() {
deviceId: "device",
cryptoStore: cryptoStore,
});
// initialising the crypto library will trigger a key upload request, which we can stub out
client.uploadKeysRequest = jest.fn();
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
userId: "@user:id",
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
}) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
await client.initCrypto();
await cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice.pickleKey),
await cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined!,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
txn);
});
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice.pickleKey),
},
txn,
);
});
await client.enableKeyBackup({
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
version: '1',
version: "1",
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
@@ -542,30 +557,26 @@ describe("MegolmBackup", function() {
let numCalls = 0;
await new Promise<void>((resolve, reject) => {
client.http.authedRequest = function<T>(
method, path, queryParams, data, opts,
): Promise<T> {
client.http.authedRequest = function (method, path, queryParams, data, opts): any {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(2);
if (numCalls >= 3) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({} as T);
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe('1');
expect(queryParams?.version).toBe("1");
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toBeDefined();
expect((data as Record<string, any>).rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
if (numCalls > 1) {
resolve();
return Promise.resolve({} as T);
return Promise.resolve({});
} else {
return Promise.reject(
new Error("this is an expected failure"),
);
return Promise.reject(new Error("this is an expected failure"));
}
};
return client.crypto!.backupManager.backupGroupSession(
@@ -578,66 +589,73 @@ describe("MegolmBackup", function() {
});
});
describe("restore", function() {
let client;
describe("restore", function () {
let client: MatrixClient;
beforeEach(function() {
beforeEach(function () {
client = makeTestClient(cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
userId: "@user:id",
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
}) as MegolmDecryptionClass;
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto();
});
afterEach(function() {
afterEach(function () {
client.stopClient();
});
it('can restore from backup (Curve25519 version)', function() {
client.http.authedRequest = function() {
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
it("can restore from backup (Curve25519 version)", function () {
client.http.authedRequest = function () {
return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA);
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
CURVE25519_BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
expect(res.untrusted).toBeTruthy(); // keys from Curve25519 backup are untrusted
});
return client
.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
CURVE25519_BACKUP_INFO,
)
.then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
})
.then((res) => {
expect(res.clearEvent.content).toEqual("testytest");
expect(res.untrusted).toBeTruthy(); // keys from Curve25519 backup are untrusted
});
});
it('can restore from backup (AES-256 version)', function() {
client.http.authedRequest = function() {
return Promise.resolve(AES256_KEY_BACKUP_DATA);
it("can restore from backup (AES-256 version)", function () {
client.http.authedRequest = function () {
return Promise.resolve<any>(AES256_KEY_BACKUP_DATA);
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
AES256_BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
expect(res.untrusted).toBeFalsy(); // keys from AES backup are trusted
});
return client
.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
AES256_BACKUP_INFO,
)
.then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
})
.then((res) => {
expect(res.clearEvent.content).toEqual("testytest");
expect(res.untrusted).toBeFalsy(); // keys from AES backup are trusted
});
});
it('can restore backup by room (Curve25519 version)', function() {
client.http.authedRequest = function() {
return Promise.resolve({
it("can restore backup by room (Curve25519 version)", function () {
client.http.authedRequest = function () {
return Promise.resolve<any>({
rooms: {
[ROOM_ID]: {
sessions: {
@@ -647,30 +665,35 @@ describe("MegolmBackup", function() {
},
});
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
null, null, CURVE25519_BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
});
return client
.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
null!,
null!,
CURVE25519_BACKUP_INFO,
)
.then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
})
.then((res) => {
expect(res.clearEvent.content).toEqual("testytest");
});
});
it('has working cache functions', async function() {
it("has working cache functions", async function () {
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
await client.crypto.storeSessionBackupPrivateKey(key);
const result = await client.crypto.getSessionBackupPrivateKey();
expect(new Uint8Array(result)).toEqual(key);
await client.crypto!.storeSessionBackupPrivateKey(key);
const result = await client.crypto!.getSessionBackupPrivateKey();
expect(new Uint8Array(result!)).toEqual(key);
});
it('caches session backup keys as it encounters them', async function() {
const cachedNull = await client.crypto.getSessionBackupPrivateKey();
it("caches session backup keys as it encounters them", async function () {
const cachedNull = await client.crypto!.getSessionBackupPrivateKey();
expect(cachedNull).toBeNull();
client.http.authedRequest = function() {
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
client.http.authedRequest = function () {
return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA);
};
await new Promise((resolve) => {
await new Promise<void>((resolve) => {
client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
@@ -679,33 +702,32 @@ describe("MegolmBackup", function() {
{ cacheCompleteCallback: resolve },
);
});
const cachedKey = await client.crypto.getSessionBackupPrivateKey();
const cachedKey = await client.crypto!.getSessionBackupPrivateKey();
expect(cachedKey).not.toBeNull();
});
it("fails if an known algorithm is used", async function() {
it("fails if an known algorithm is used", async function () {
const BAD_BACKUP_INFO = Object.assign({}, CURVE25519_BACKUP_INFO, {
algorithm: "this.algorithm.does.not.exist",
});
client.http.authedRequest = function() {
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
client.http.authedRequest = function () {
return Promise.resolve<any>(CURVE25519_KEY_BACKUP_DATA);
};
await expect(client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
BAD_BACKUP_INFO,
)).rejects.toThrow();
await expect(
client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
BAD_BACKUP_INFO,
),
).rejects.toThrow();
});
});
describe("flagAllGroupSessionsForBackup", () => {
it("should return number of sesions needing backup", async () => {
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject<MatrixScheduler>;
const scheduler = makeTestScheduler();
const store = new StubStore();
const client = new MatrixClient({
baseUrl: "https://my.home.server",
@@ -718,6 +740,9 @@ describe("MegolmBackup", function() {
deviceId: "device",
cryptoStore,
});
// initialising the crypto library will trigger a key upload request, which we can stub out
client.uploadKeysRequest = jest.fn();
await client.initCrypto();
cryptoStore.countSessionsNeedingBackup = jest.fn().mockReturnValue(6);
+52
View File
@@ -0,0 +1,52 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { TextEncoder, TextDecoder } from "util";
import { decodeBase64, encodeBase64, encodeUnpaddedBase64 } from "../../../src/common-crypto/base64";
describe("Crypto Base64 encoding", () => {
it("Should decode properly encoded data", async () => {
const toEncode = "encoding hello world";
const encoded = encodeBase64(new TextEncoder().encode(toEncode));
const decoded = new TextDecoder().decode(decodeBase64(encoded));
expect(decoded).toStrictEqual(toEncode);
});
it("Encode unpadded should not have padding", async () => {
const toEncode = "encoding hello world";
const data = new TextEncoder().encode(toEncode);
const paddedEncoded = encodeBase64(data);
const unpaddedEncoded = encodeUnpaddedBase64(data);
expect(paddedEncoded).not.toEqual(unpaddedEncoded);
const padding = paddedEncoded.charAt(paddedEncoded.length - 1);
expect(padding).toStrictEqual("=");
});
it("Decode should be indifferent to padding", async () => {
const withPadding = "ZW5jb2RpbmcgaGVsbG8gd29ybGQ=";
const withoutPadding = "ZW5jb2RpbmcgaGVsbG8gd29ybGQ";
const decodedPad = decodeBase64(withPadding);
const decodedNoPad = decodeBase64(withoutPadding);
expect(decodedPad).toStrictEqual(decodedNoPad);
});
});

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