Compare commits

...

170 Commits

Author SHA1 Message Date
RiotRobot 9d6210b3f9 v33.0.0-rc.0 2024-05-29 12:54:25 +00:00
Andy Balaam 909caab74e Don't run migration for Rust crypto if the legacy store is empty (#4218)
* Don't run migration for Rust crypto if the legacy store is empty

Fixes https://github.com/element-hq/element-web/issues/27447

* Add copyright for the TypeScript files in legacy DB dumps

* Provide a type for the accountPickle we check for before migration

* Remove redundant backup response

This is unused

* Simplify keys response

* Downgrade log message.

---------

Co-authored-by: Richard van der Hoff <richard@matrix.org>
2024-05-28 17:43:25 +00:00
Michael Telatynski 7c87625157 Remove more deprecated methods, fields, and exports (#4217) 2024-05-28 09:12:55 +01:00
Richard van der Hoff b19817bb73 Bump matrix-sdk-crypto-wasm to 5.0.0 (#4216)
Slightly more involved than normal because it requires us to pass a backup version into OlmMachine.importBackedUpRoomKeys.

On the other hand we can now re-enable the test that was disabled in #4214 due to matrix-org/matrix-rust-sdk#3447

Fixes: element-hq/element-web#27165
2024-05-24 12:10:52 +01:00
Richard van der Hoff 36196ea422 initRustCrypto: allow app to pass in the store key directly (#4210)
* `initRustCrypto`: allow app to pass in the store key directly

... instead of using the pickleKey. This allows us to avoid a slow PBKDF
operation.

* Fix link in doc-comment
2024-05-24 09:52:34 +00:00
renovate[bot] a81adf542e Update dependency @matrix-org/matrix-sdk-crypto-wasm to v4.10.0 (#4214)
* Update dependency @matrix-org/matrix-sdk-crypto-wasm to v4.10.0

* Disable affected test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-23 11:19:41 +00:00
RiotRobot a49bc3ddf4 Merge branch 'master' into develop 2024-05-22 12:04:33 +00:00
RiotRobot a86d4ceb49 v32.4.0 2024-05-22 12:04:07 +00:00
R Midhun Suresh 8c3be2a56a Update organization (#4212) 2024-05-21 15:55:49 +00:00
RiotRobot 38898a60c7 v32.3.0 2024-05-21 12:24:52 +00:00
Bayyr Oorjak fd3a4d4403 Preserve ESM for async imports to work correctly (#4187)
* fix: fix lazy rust crypto import

* test: use "commonjs" for tests because of circular deps

* chore: revert commonjs for "module"

* refactor: remove unnecessary example

* refactor: add comments

Signed-off-by: Bayyr Oorjak <the.bayyr.oorjak@gmail.com>

* refactor: improve comment

Signed-off-by: Bayyr Oorjak <the.bayyr.oorjak@gmail.com>

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

* Update babel.config.js

---------

Signed-off-by: Bayyr Oorjak <the.bayyr.oorjak@gmail.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-05-21 11:48:00 +00:00
RiotRobot 93d96281fd Resetting package fields for development 2024-05-21 12:25:26 +00:00
RiotRobot 944dc51c58 Merge branch 'master' into develop 2024-05-21 12:25:25 +00:00
renovate[bot] c6b43dd176 Update dependency eslint-plugin-unicorn to v53 (#4209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 16:56:22 +00:00
Michael Telatynski f03dd7b7bc Remove deprecated methods and fields (#4201)
* Remove legacy `threepidCreds` field

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove deprecated shouldUpgradeToVersion

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove `added` legacy login request field

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove deprecated re-exports

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove `home_server` field

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update imports in tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-15 15:23:04 +00:00
Michael Telatynski 51fa1866a9 Wire up verification cancel & mismatch for rust crypto (#4202)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-15 14:58:24 +00:00
renovate[bot] d76fb2baa0 Update all non-major dependencies (#4207)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 11:51:51 +00:00
renovate[bot] 3feafc9c17 Update dependency typedoc-plugin-mdn-links to v3.1.25 (#4206)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 11:28:29 +00:00
Michael Telatynski c9075b3dba Only pass id_server if we had one to begin with (#4200)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-15 10:59:42 +00:00
renovate[bot] 69c474dda7 Update dependency @types/node to v18.19.33 (#4205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 10:54:21 +00:00
renovate[bot] 73ce51065f Update mheap/github-action-required-labels digest to 5847eef (#4204)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 10:26:32 +00:00
renovate[bot] 5d0407d0a6 Update actions/checkout digest to 0ad4b8f (#4203)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 10:23:58 +00:00
RiotRobot 3a4b02d8e6 v32.3.0-rc.0 2024-05-15 09:04:25 +00:00
Kegan Dougal d421e7f829 Run complement-crypto in CI (#4197)
* Maybe run complement-crypto

* Use existing checkout

* Test that things fail if crypto breaks

* Fix test; run only on merge queue

* Prettier

* Maybe get it working in a merge queue
2024-05-13 10:26:27 +00:00
Michael Telatynski 9fd051af33 Update downstream-end-to-end-tests.yml 2024-05-13 11:20:47 +01:00
Michael Telatynski b78a1ad889 Hotfix types export point
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-10 13:26:07 +01:00
Michael Telatynski a25cdcecaa Fix state_events.ts types (#4196)
* Fix state_events.ts types

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-10 10:55:44 +00:00
RiotRobot 2a716bd076 Resetting package fields for development 2024-05-07 12:16:24 +00:00
RiotRobot ef1db8d664 Merge branch 'master' into develop 2024-05-07 12:16:23 +00:00
RiotRobot 22865fd834 v32.2.0 2024-05-07 12:15:48 +00:00
Michael Telatynski c4fe564855 Simplify OIDC types & export decodeIdToken (#4193)
* Fix types

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Export `decodeIdToken`

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-07 10:21:57 +00:00
Michael Telatynski 9ecb1a0381 Fix sendEventHttpRequest for m.room.redaction events without redacts (#4192)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-07 08:50:37 +00:00
Travis Ralston ef9490c7b1 Add missing (and common-ish) HTTP method verbs to types (#4188) 2024-05-02 23:09:15 +00:00
Travis Ralston 402adfbe8a Add helpers for authenticated media, and associated documentation (#4185)
* Add helpers for authenticated media, and associated documentation

* Appease the linter
2024-05-02 21:11:09 +00:00
renovate[bot] 41e8c2af34 Update dependency typedoc-plugin-coverage to v3.1.1 (#4186)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-01 10:23:17 +00:00
renovate[bot] 4843b40296 Update dependency typedoc-plugin-mdn-links to v3.1.22 (#4182)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-30 13:18:45 +00:00
renovate[bot] bc2c870152 Update babel monorepo to v7.24.5 (#4181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-30 13:17:37 +00:00
renovate[bot] 7c7b2817d3 Update all non-major dependencies (#4184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-30 13:16:31 +00:00
renovate[bot] 9f78202ecd Update typescript-eslint monorepo to v7.7.1 (#4183)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-04-30 14:40:57 +01:00
RiotRobot abc9911e95 v32.2.0-rc.0 2024-04-30 12:02:20 +00:00
David Baker efdae0d66f Remove spammy RTCSession log line (#4180)
This can get quite spammy in rooms with legacy calls and isn't really
neccesary: remove it to cut down on the log spam.
2024-04-30 10:43:20 +00:00
David Baker 2bf554761c Add some comments to the release drafter workflows (#4140)
* Add some comments to the release drafter workflows

* Rename component workflow so they have different names

* Fix comment

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-04-30 09:38:07 +00:00
Michael Telatynski c2687643b5 Update downstream-end-to-end-tests.yml 2024-04-30 10:30:19 +01:00
Michael Telatynski a33758eda6 Update downstream-end-to-end-tests.yml 2024-04-30 10:28:44 +01:00
Michael Telatynski 8faed02cc5 Update sonarcloud.yml 2024-04-29 17:22:32 +01:00
Michael Telatynski 3ae0dab47a Update sonarcloud.yml 2024-04-29 16:59:55 +01:00
Michael Telatynski 95394e4cbe Update sonarcloud.yml 2024-04-29 16:52:54 +01:00
Michael Telatynski 8c9bbc01fc Update downstream-end-to-end-tests.yml 2024-04-29 16:20:24 +01:00
Michael Telatynski eb888791a3 Pass all args to Sonar via sonar-project.properties file (#4179)
* Pass all args to Sonar via sonar-project.properties file

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update Sonarcloud action to v3.1

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Apply suggestions from code review

Co-authored-by: davidegirardi <16451191+davidegirardi@users.noreply.github.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: davidegirardi <16451191+davidegirardi@users.noreply.github.com>
2024-04-29 15:12:00 +00:00
Michael Telatynski 6c0b2f55e1 Delete .github/workflows/downstream-artifacts.yml 2024-04-29 10:53:12 +01:00
Michael Telatynski c9a5eaece3 Simplify Playwright CI (#4178) 2024-04-29 08:49:00 +01:00
Hubert Chathi 64505de36b Use a different error code for UTDs when user was not in the room (#4172)
* use a different error code for UTDs when user was not in the room

* if user is invited, treat it as unexpected UTD
2024-04-26 13:38:10 +00:00
Valere 65d858f9a3 Fix rust migration when ssss secret not encrypted (#4168) 2024-04-26 12:59:17 +00:00
RiotRobot 1da5e8f56a Resetting package fields for development 2024-04-23 12:34:26 +00:00
RiotRobot 5efd4c2915 Merge branch 'master' into develop 2024-04-23 12:34:25 +00:00
RiotRobot bc03950f8a v32.1.0 2024-04-23 12:33:56 +00:00
Travis Ralston c09da9a23f Modernize window.crypto access constants (#4169)
* Force service worker-safe crypto when operating under a service worker

* Fix tests/unsafe execution

* Further fix tests?

* Docs would probably be good

* Define a type guard function

https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards

* Use `@types` repo

* Maybe don't modify tsconfig, I guess

* Revert "Use `@types` repo"

This reverts commit db46bcf1db4b94fbc7e0c97a20d5d800fcb2768b.

* Use a different type for Window

* Simplify the crypto accessors
2024-04-22 19:44:01 +00:00
Andrew Ferrazzutti e874468ba3 Improve compliance with MSC3266 (#4155)
* Fix fields of MSC 3266 summary object

Also remove redundant room_type field which is inherited from elsewhere

* Export the MSC 3266 summary type

* Use proper endpoint for MSC 3266 summary lookup

Use the endpoint recommended by the MSC

* Rename newly-exported symbol to not start with I

* Use "export type"

* Lint

* Fix type of "encryption" field

* Add TSDoc documentation

* Add basic integration test for getRoomSummary

* Lint

* Use fallback endpoint for MSC3266

* Improve test coverage

* Lint

* Refactor async catch to satisfy linter

* Increase test coverage
2024-04-22 17:39:10 +00:00
Johannes Marbach 6fedda91f9 Use encoded URI components when storing sessions in memory crypto store (#4170)
* Use encoded URI components when storing sessions in memory crypto store

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>

* Add URI en-/decoding to missing methods

* Extract convenience functions

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-04-22 08:12:54 +00:00
Valere d22a39f5d7 Element R: fix isCrossSigningReady not checking identity trust (#4156)
* Fix inconsistency between rust and legacy

* Add tests

* Review: better comment

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

* review: Better doc

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

* Simplify test data and some comments

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-04-17 13:36:52 +00:00
Timo 4fc6ba884e add comment to make clear that RoomStateEvent.Events does not update related objects in the js-sdk (#4152)
Signed-off-by: Timo K <toger5@hotmail.de>
2024-04-17 10:28:40 +00:00
Richard van der Hoff c30e498013 Crypto: use a new error code for UTDs from device-relative historical events (#4139)
* Add `PerSessionKeyBackupDownloader.isKeyBackupDownloadConfigured()`

* Add new `RustBackupManager.getServerBackupInfo`

... and a convenience method in PerSessionKeyBackupDownloader to access it.

* Crypto.spec: move `useRealTimers` to global `afterEach`

... so that we don't need to remember to do it everywhere.

* Use fake timers for UTD error code tests

This doesn't have any effect on the tests, but *does* stop jest from hanging
when you run the tests in in-band mode. It shouldn't *really* be needed, but
using fake timers gives more reproducible tests, and I don't have the
time/patience to debug why it is needed.

* Use new error codes for UTDs from historical events
2024-04-17 10:26:41 +00:00
renovate[bot] 8240bf0ae7 Update shogo82148/actions-upload-release-asset digest to 8f032ef (#4157)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 15:59:04 +00:00
renovate[bot] 2321c44687 Update typedoc (#4162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 15:22:17 +00:00
renovate[bot] 0137e9d5a8 Update dependency eslint-plugin-unicorn to v52 (#4166)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 14:29:48 +00:00
renovate[bot] 28bbc51752 Update dependency @types/node to v18.19.31 (#4160)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 14:06:49 +00:00
renovate[bot] 0db3ac9b43 Update dependency eslint-plugin-jest to v28 (#4165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 13:56:50 +00:00
renovate[bot] 53039b78ee Update all non-major dependencies (#4158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 13:25:06 +00:00
renovate[bot] 2a06d19431 Update babel monorepo to v7.24.4 (#4159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 13:24:46 +00:00
renovate[bot] a747eef04c Update dependency typescript to v5.4.5 (#4161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 13:24:27 +00:00
renovate[bot] 583823c2ef Update dependency knip to v5 (#4167)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 13:21:14 +00:00
renovate[bot] 26d13c15c3 Update typescript-eslint monorepo to v7.6.0 (#4163)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 13:20:45 +00:00
RiotRobot c850ca3179 v32.1.0-rc.0 2024-04-16 12:16:13 +00:00
Valere 8438533532 Validate backup private key before migrating it (#4114)
* Migrate own identity trust to rust crypto

* Fix gendoc not happy if msk of IDownloadKeyResult has a signature

* add missing mock

* code review

* Code review

* Review gh suggestion

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

* Review gh suggestion

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

* Review gh suggestion

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

* Review gh suggestion

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

* review move function down in file

* Review gh suggestion

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

* Review gh suggestion

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

* Review: Cleaning tests, renaming

* Review: better comment

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

* Comment paragraphs

* retry until initial  key query is successfull

* Validate backup private key before migrating it

* post merge fix

* Fix test, missing mock

* Use crypto wasm instead of lib olm to check backup key

* typo

* code review

* quick lint

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-04-15 07:17:14 +00:00
David Baker 475f82c5ce Update git-get-release action (#4148)
which appears to be breaking renovate somehow, and could probably use
and update anyway.
2024-04-11 09:36:07 +00:00
Hubert Chathi 936e7c3072 Add support for device dehydration v2 (Element R) (#4062)
* initial implementation of device dehydration

* add dehydrated flag for devices

* add missing dehydration.ts file, add test, add function to schedule dehydration

* add more dehydration utility functions

* stop scheduled dehydration when crypto stops

* bump matrix-crypto-sdk-wasm version, and fix tests

* adding dehydratedDevices member to mock OlmDevice isn't necessary any more

* fix yarn lock file

* more tests

* fix test

* more tests

* fix typo

* fix logic for checking if dehydration supported

* make changes from review

* add missing file

* move setup into another function

* apply changes from review

* implement simpler API

* fix type and move the code to the right spot

* apply suggestions from review

* make sure that cross-signing and secret storage are set up
2024-04-11 04:01:47 +00:00
RiotRobot 82ed7bd86a Resetting package fields for development 2024-04-09 10:07:09 +00:00
RiotRobot cb67eae858 Merge branch 'master' into develop 2024-04-09 10:07:07 +00:00
RiotRobot e4937e6222 v32.0.0 2024-04-09 10:06:41 +00:00
Michael Telatynski 5cdd524da7 OIDC improvements in prep of OIDC-QR reciprocation (#4149)
* Add `device_authorization_endpoint` field to OIDC issuer well-known metadata

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Allow `validateIdToken` to skip handling nonce when none is present

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tweak registerOidcClient to check OIDC grant_types_supported before registration

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-04-08 14:07:00 +00:00
Michael Telatynski 0ff0093380 Add knip unused code & dependency analyser (#4013) 2024-04-08 11:04:40 +01:00
Valere b352405c89 ElementR| Retry query backup until it works during migration to avoid spurious correption error popup (#4113)
* retry query backup until it works during migration

* Add log line when fails to get backup during migration
2024-04-08 07:11:06 +00:00
renovate[bot] 0d73d0c6c7 Update typescript-eslint monorepo to v7.4.0 (#4146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-03 09:49:35 +00:00
renovate[bot] d2f76d4956 Update dependency typescript to v5.4.3 (#4145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-03 09:23:09 +00:00
renovate[bot] c680dd7eb2 Update dependency @types/node to v18.19.28 (#4144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-03 08:37:32 +00:00
renovate[bot] e24bb0f50c Update babel monorepo to v7.24.3 (#4143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-03 08:36:51 +00:00
renovate[bot] 1ed3b13f0d Update all non-major dependencies (#4142)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 17:58:13 +00:00
renovate[bot] 4f628bf64c Update mheap/github-action-required-labels digest to 132879b (#4141)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 17:08:08 +00:00
RiotRobot 7d5c003716 v32.0.0-rc.0 2024-04-02 16:15:04 +00:00
Richard van der Hoff dbab185f9d Refactoring and simplification in decryption error handling (#4138)
* Clean up decryption failure integ tests

* Fix the names
* Stop waiting as soon as the event is decrypted, even if code is wrong (so
  tests fail rather than time out if the code is wrong)

* Bump timeouts on some tests

These tend to fail due to slow init of wasm artifacts

* Factor out `onDecryptionKeyMissingError` call

* Factor out `onMegolmDecryptionError`
2024-04-02 13:39:49 +00:00
Richard van der Hoff cfcd191cbf Update matrix-rust-sdk-crypto-wasm to 4.9.0 (#4137) 2024-04-02 13:09:59 +00:00
RiotRobot 514633c5fa Merge branch 'master' into develop 2024-03-28 16:45:10 +00:00
RiotRobot 5bffb7df4f v31.6.1 2024-03-28 16:44:30 +00:00
David Baker 9e1897dcd0 Merge pull request #4136 from matrix-org/backport-4135-to-staging
Fix merging of default push rules (backport)
2024-03-28 16:35:36 +00:00
David Baker 5f3ddc37a1 Merge pull request #4135 from matrix-org/t3chguy/fix/27173
Fix merging of default push rules

(cherry picked from commit 78a225795b)
2024-03-28 16:28:33 +00:00
David Baker 78a225795b Merge pull request #4135 from matrix-org/t3chguy/fix/27173
Fix merging of default push rules
2024-03-28 16:23:58 +00:00
Michael Telatynski 467b49a0dc Add test
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-28 15:52:12 +00:00
Michael Telatynski 06e083874a Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-28 14:59:52 +00:00
Michael Telatynski 0f25429849 Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-28 13:41:31 +00:00
Michael Telatynski 32ddf2813d Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-28 12:55:44 +00:00
Michael Telatynski 1ed082f3d4 Fix merging of default push rules
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-28 11:58:52 +00:00
RiotRobot 706002cdcb Resetting package fields for development 2024-03-26 16:26:09 +00:00
RiotRobot 731de1108c Merge branch 'master' into develop 2024-03-26 16:26:07 +00:00
RiotRobot 2da6c0c605 v31.6.0 2024-03-26 16:25:15 +00:00
Richard van der Hoff 9f1d0c3896 Add new decryptExistingEvent test helper (#4133)
* grammar fix

* IEncryptionResult -> EncryptionResult

These are the same thing; the former is the old name.

* Support setting event IDs

* Helper for decrypting existing decryption failures
2024-03-25 14:10:58 +00:00
Gabri 0b290fffa1 Improve types for IPowerLevelsContent and hasSufficientPowerLevelFor (#4128)
Signed-off-by: Gabriele Messina <56839513+galash13@users.noreply.github.com>
2024-03-25 13:58:38 +00:00
Michael Telatynski 97844f0e47 Improve types for sendEvent (#4108) 2024-03-25 12:48:49 +00:00
Michael Telatynski 85a55c79cd Remove various deprecated methods & re-exports (#4125) 2024-03-25 12:21:11 +00:00
Johannes Marbach 63d4195453 Use RoomEvent.MyMembership in auto-join example (#4130)
This seems easier / more efficient than listening to all membership events and then filtering by user ID.
2024-03-25 09:10:42 +00:00
Richard van der Hoff d5a35f8a99 Add new enum for verification methods. (#4129)
* Define constants for the verification methods.

* Remove some confusing references to the *old* `VerificationMethod`
2024-03-22 17:17:31 +00:00
Richard van der Hoff d1259b241c Clean up code for handling decryption failures (#4126)
Various improvements, including:

* Defining an enum for decryption failure reasons
* Exposing the reason code as a property on Event
2024-03-22 17:15:27 +00:00
David Langley a573727662 Remove the logic that throws when the lazy loading options has changed. (#4124)
* remove InvalidStoreState and the logic that checks for the change in the lazyLoading client option

* lint
2024-03-22 16:36:23 +00:00
Richard van der Hoff dce8acbf17 Add some test utils in a new entrypoint (#4127)
* Clean up README a little

This just removes some of the most egregious lies and outdated stuff. There's a
*lot* more that can be done here.

* Add some test utils in a new entrypoint

* Fix comment

* Update src/testing.ts
2024-03-22 14:10:55 +00:00
David Baker 4ba1341f8f Fix highlights from threads disappearing on new messages (#4106)
* Fix highlights from threads disappearing on new messages

This changes interface of Room, so this is a BREAKING CHANGE.

Correctly mirrors the logic we use for room notifications for thread
notifications, ie. set only the total notifications count from the
server if it's zero.

I'm not delighted with this since it ends up with function on room
whose contract is to do something frankly, deeply weird and
unintuitive. However, this is the hack we use for room notifications
and it, empirically, works well enough. To do better, we'd need much
more complex logic to overlay notification counts for decrypted messages.

Fixes https://github.com/element-hq/element-web/issues/25523

* Add tests for the special notification behaviour in syncing

* Correctly copy the room logic for reseting notifications

We were always ignoring the highlight count, even for encrypted rooms,
which was broken because we don't do the local calculation for unencrypted
rooms.
2024-03-21 16:29:00 +00:00
David Baker e517d009bf Extend logic for local notification processing to threads (#4111)
* Move code for processing our own receipts to Room

This is some code to process our own receipts and recalculate our
notification counts.

There was no reason for this to be in client. Room is still rather
large, but at least it makes somewhat more sense there.

Moving as a refactor before I start work on it.

* Add test for the client-side e2e notifications code

* Extend logic for local notification processing to threads

There's collection of logic for for processing receipts and recomputing
notifications for encrypted rooms, but we didn't do the same for threads.
As a reasult, when I tried pulling some of the logic over in
https://github.com/matrix-org/matrix-js-sdk/pull/4106
clearing notifications on threads just broke.

This extends the logic of reprocessing local notifications when a receipt
arrives to threads.

Based on https://github.com/matrix-org/matrix-js-sdk/pull/4109

* simplify object literal

* Add tests & null guard

* Remove unused imports

* Add another skipped test

* Unused import

* enable tests

* Fix thread support nightmare

* Try this way

* Unused import

* Comment the bear trap

* expand comment
2024-03-21 12:22:19 +00:00
Ajay Bura dc2d03dea5 fix public rooms post request search params and body (#4110) 2024-03-21 10:29:51 +00:00
David Baker d5bb9e7600 Move code for processing our own receipts to Room (#4109)
* Move code for processing our own receipts to Room

This is some code to process our own receipts and recalculate our
notification counts.

There was no reason for this to be in client. Room is still rather
large, but at least it makes somewhat more sense there.

Moving as a refactor before I start work on it.

* Add test for the client-side e2e notifications code

* simplify object literal
2024-03-20 15:20:47 +00:00
Michael Telatynski d908036f50 Improve types for sendStateEvent (#4105) 2024-03-20 14:27:27 +00:00
David Baker afc3c6213b Fix bugs with the first reply to a thread (#4104)
* WIP fix for bugs first-thread-reply bugs

* Add re-emitter before we start adding events, as per comment

* Add test for notification bug

* Test for the bug that caused the dot to be the wrong colour

* Add comment

* elaborate

* Fix outdated comment

* Also fix this comment

* Fix another comment

* Fix typo

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

* Clarify comment

* More comment

* so much comment

also reformat (the bit that's actually added is s/it/this.addEvents/)

* The comments

* Maybe make comment clearer.

* Add comment about potential race

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-03-20 11:14:25 +00:00
Michael Telatynski 7884c22e41 Fix permissions for deploying docs to github pages (#4122)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-19 17:57:33 +00:00
renovate[bot] 887d8a7663 Update dependency typescript to v5.4.2 (#4123)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 17:25:31 +00:00
renovate[bot] 2c68ee2254 Update babel monorepo to v7.24.1 (#4119)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 16:33:19 +00:00
renovate[bot] d445823d0b Update typescript-eslint monorepo to v7.2.0 (#4121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 16:25:50 +00:00
renovate[bot] abe4630687 Update typedoc (#4118)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 16:25:07 +00:00
renovate[bot] 8664b66238 Update dependency @matrix-org/matrix-sdk-crypto-wasm to v4.7.0 (#4120)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 16:24:49 +00:00
renovate[bot] 596826ab4d Update all non-major dependencies (#4116)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 16:22:03 +00:00
renovate[bot] c8ec5421c7 Update dependency @types/node to v18.19.24 (#4117)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 16:21:55 +00:00
RiotRobot b8078f9916 v31.6.0-rc.0 2024-03-19 15:05:53 +00:00
Andy Balaam 92342c07ed Introduce Membership TS type (take 2) (#4107)
* Introduce Membership TS type

* Adapt the Membership TS type to be an enum

* Add docstrings for KnownMembership and Membership

* Move Membership types into a separate file, exported from types.ts

---------

Co-authored-by: Stanislav Demydiuk <s.demydiuk@gmail.com>
2024-03-18 12:47:23 +00:00
Valere 3e989006aa Migrate own identity local trust to rust crypto (#4090)
* Migrate own identity trust to rust crypto

* Fix gendoc not happy if msk of IDownloadKeyResult has a signature

* add missing mock

* code review

* Code review

* Review gh suggestion

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

* Review gh suggestion

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

* Review gh suggestion

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

* Review gh suggestion

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

* review move function down in file

* Review gh suggestion

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

* Review gh suggestion

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

* Review: Cleaning tests, renaming

* Review: better comment

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

* Comment paragraphs

* retry until initial  key query is successfull

* review quick nits

* missing mock in test

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-03-18 08:57:53 +00:00
Kim Brose 8e0ef5ff2c fix automatic DM avatar with functional members (#4017)
* fix automatic DM avatar with functional members

* update comments

* lint

* add tests for functional members

* keep functional members out of the public API

- remove public API for functional members, reverting most of 0ce2d82, f9b41f6, e65fb24
- remove tests for functional members public API c114bf5
- add shared functional members getter for both room name and avatar fallback generation

* filter functional members from more candidates

- remove from hero(es)
- remove from previous members

* add tests for fallback avatars with functional members

* Add docstring for getFunctionalMembers

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

* inline getInvitedAndJoinedFunctionalMemberCount

* update comments for getAvatarFallbackMember

* use correct list of heroes in getAvatarFallbackMember

* remove redundant type annotation

* optimize performance of invitedAndJoinedFunctionalMemberCount

* calculate nonFunctionalMemberCount in one step

instead of iterating redundantly

* clean up functional member tests with review feedback

* lint

* Update src/models/room.ts

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

* apply feedback about comments

* non-functional per review, lint

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-03-13 17:01:11 +00:00
Michael Telatynski 78d05942a3 Merge remote-tracking branch 'origin/develop' into develop 2024-03-12 18:53:54 +00:00
Michael Telatynski c22a6858c8 Temporarily disable broken step in the release process
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-12 18:53:44 +00:00
RiotRobot 461aeae281 Resetting package fields for development 2024-03-12 18:34:34 +00:00
RiotRobot 0511e313d3 Merge branch 'master' into develop 2024-03-12 18:34:33 +00:00
RiotRobot da3d5c4a43 v31.5.0 2024-03-12 18:33:38 +00:00
renovate[bot] 4c26b55c9a Update dependency typedoc to v0.25.11 (#4102)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-12 13:38:32 +00:00
Michael Telatynski 3711ad7e61 Export types describing all specced media event formats (#4092)
* Export types describing all specced media event formats

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate PR

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Move types to a dedicated export

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add readme entry

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-08 21:07:26 +00:00
Michael Telatynski 3031152444 Add .m.rule.is_room_mention push rule to DEFAULT_OVERRIDE_RULES (#4100)
* Add intentional mentions push rules to DEFAULT_OVERRIDE_RULES

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-08 20:21:12 +00:00
Daniel Salinas 51ebd2fcde Fix race condition with sliding sync extensions (#4089)
* Fix race condition with sliding sync extensions

* Fix types on sliding-sync spec test

* Prettier fixes
2024-03-07 23:56:36 +00:00
David Baker 27dd856778 Don't re-fetch thread root if we already have it (#4088)
The root event of a thread used to arrive with the pagination request, but this was unspecced and so got changed to simply fetch the root event. In many (almost all) cases this shouldn't be necessary because the thread should already have its root event: re-use it if it's already there. This is only in pagination, so there's no reason to believe that the root event would have changed and needs to be re-fetched.

This removes a number of duplicate calls to the /event/ endpoint from the tests.
2024-03-06 14:10:35 +00:00
renovate[bot] 7fee37680f Update typedoc (#4098)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-06 08:49:16 +00:00
renovate[bot] 2541ca04c2 Update all non-major dependencies (#4096)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-05 17:54:30 +00:00
Richard van der Hoff d55c6a36df PR template: reminder to document your stuff (#4081)
* PR template: reminder to document your stuff

* link to tsdoc, not typedoc

* add full stops
2024-03-05 17:10:33 +00:00
Timo 8c0736a719 Make sending ContentLoaded optional for a widgetClient (#4086)
* add sendContentLoaded option to widgetClient

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

* review

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

* add tests

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

* another try to get the coverage up

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

* self review

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

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2024-03-05 16:59:07 +00:00
renovate[bot] 50b042d1ff Update tspascoal/get-user-teams-membership digest to 57e9f42 (#4094)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 16:14:15 +00:00
renovate[bot] a818dc1e9d Update shogo82148/actions-upload-release-asset digest to 5bd52f0 (#4093)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 15:56:46 +00:00
renovate[bot] d8dae65a4d Update typescript-eslint monorepo to v7.1.0 (#4099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 15:43:37 +00:00
renovate[bot] 8be286308c Update dependency @types/node to v18.19.21 (#4095)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 15:16:30 +00:00
renovate[bot] 84498bf77d Update babel monorepo to v7.24.0 (#4097)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 14:50:28 +00:00
Michael Telatynski a1f4b07b7d Fix bad string split destructuring
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-05 14:31:57 +00:00
RiotRobot ee8413beff v31.5.0-rc.0 2024-03-05 14:03:49 +00:00
Michael Telatynski e4d4628cc8 When merging release notes, allow considering later versions in the same release cycle (#4085)
* When merging release notes, allow considering later versions in the same major.minor.patch set

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tweak comments

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-03-05 09:34:09 +00:00
David Langley b2e09250d9 Add job to automate adding new issues to the new project (#4087)
* add job to automate adding new issues to the new project

* missing jobs:
2024-02-29 12:20:53 +00:00
RiotRobot 6176faef48 Resetting package fields for development 2024-02-27 12:51:20 +00:00
RiotRobot 453cdd9eda Merge branch 'master' into develop 2024-02-27 12:51:18 +00:00
Valere d3dfcd9242 Add basic retry for rust crypto outgoing requests (#4061)
* Add basic retry for outgoing requests

* Update doc

* Remove 504 from retryable

* Retry all 5xx and clarify client timeouts

* code review cleaning

* do not retry rust request if M_TOO_LARGE

* refactor use common retry alg between scheduler and rust requests

* Code review, cleaning and doc
2024-02-26 14:07:28 +00:00
Michael Telatynski a26fc46ed4 Update MSC2965 OIDC Discovery implementation (#4064) 2024-02-23 16:43:11 +00:00
renovate[bot] be3913e8a5 Update typedoc (#3958)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-22 17:30:14 +00:00
renovate[bot] c1e0192baf Update dependency eslint-plugin-unicorn to v51 (#4083)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-22 17:06:12 +00:00
Andy Balaam 8123e9a3f1 Bump matrix-react-sdk-crypto-wasm to v4.6.0 (#4082)
To reduce memory usage during export - see https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm/pull/105
2024-02-22 11:55:27 +00:00
Michael Telatynski 0425f4e5c8 Merge remote-tracking branch 'origin/develop' into develop 2024-02-22 10:49:07 +00:00
Michael Telatynski 5b74b446d4 Prune broken docs symlinks
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-22 10:48:50 +00:00
renovate[bot] 624914a565 Update dependency husky to v9 (#4051)
* Update dependency husky to v9

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-22 09:46:30 +00:00
renovate[bot] 4da9627727 Update all non-major dependencies (#4076)
* Update all non-major dependencies

* Prettier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-02-22 09:35:20 +00:00
renovate[bot] 1cb30bfe9b Update typescript-eslint monorepo to v7 (#4078)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-22 09:04:17 +00:00
renovate[bot] 12308b4c07 Update dependency @types/node to v18.19.17 (#4075)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-22 09:02:45 +00:00
Michael Telatynski 7f25162725 Update pull_request.yaml 2024-02-21 21:31:43 +00:00
David Baker 4826868a8f Update stale comment (#4080)
And remove line that set it for it to just get overwritten
2024-02-21 19:01:16 +00:00
renovate[bot] 91bde6afa1 Update dependency typedoc-plugin-coverage to v3 (#4077)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-21 18:45:39 +00:00
183 changed files with 80412 additions and 75958 deletions
-22
View File
@@ -1,22 +0,0 @@
{
"sourceMaps": true,
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": 10
},
"modules": "commonjs"
}
],
"@babel/preset-typescript"
],
"plugins": [
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-runtime"
]
}
+1
View File
@@ -0,0 +1 @@
_docs
-3
View File
@@ -103,11 +103,8 @@ module.exports = {
},
},
{
// 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",
+4 -3
View File
@@ -2,6 +2,7 @@
## 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).
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
- [ ] 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)).
@@ -22,7 +22,7 @@ runs:
- name: Upload tarball signature
if: ${{ inputs.upload-url }}
uses: shogo82148/actions-upload-release-asset@dccd6d23e64fd6a746dce6814c0bde0a04886085 # v1
uses: shogo82148/actions-upload-release-asset@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ env.VERSION }}.tar.gz.asc
@@ -29,13 +29,13 @@ runs:
- name: Upload asset signatures
if: inputs.gpg-fingerprint
uses: shogo82148/actions-upload-release-asset@dccd6d23e64fd6a746dce6814c0bde0a04886085 # v1
uses: shogo82148/actions-upload-release-asset@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}.asc
- name: Upload assets
uses: shogo82148/actions-upload-release-asset@dccd6d23e64fd6a746dce6814c0bde0a04886085 # v1
uses: shogo82148/actions-upload-release-asset@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}
@@ -1,22 +0,0 @@
name: Build downstream artifacts
on:
merge_group:
types: [checks_requested]
pull_request: {}
# For now at least, we don't run this or the downstream-end-to-end-tests against pushes
# to develop or master.
#
#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.91.0
with:
matrix-js-sdk-sha: ${{ github.sha }}
react-sdk-repository: matrix-org/matrix-react-sdk
@@ -3,10 +3,16 @@
name: matrix-react-sdk End to End Tests
on:
workflow_run:
workflows: ["Build downstream artifacts"]
types:
- completed
merge_group:
types: [checks_requested]
pull_request: {}
# For now at least, we don't run this or the downstream-end-to-end-tests against pushes
# to develop or master.
#
#push:
# branches: [develop, master]
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
@@ -15,44 +21,14 @@ concurrency:
jobs:
playwright:
name: Playwright
# We only want to run the playwright tests on merge queue to prevent regressions
# from creeping in. They take a long time to run and consume multiple concurrent runners.
if: github.event.workflow_run.event == 'merge_group'
uses: matrix-org/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml@develop
permissions:
actions: read
issues: read
statuses: write
pull-requests: read
deployments: write
with:
matrix-js-sdk-sha: ${{ github.sha }}
react-sdk-repository: matrix-org/matrix-react-sdk
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
# We want to make the Playwright tests a required check for the merge queue.
#
# Unfortunately, GitHub doesn't distinguish between "checks needed for branch
# protection" (ie, the things that must pass before the PR will even be added
# to the merge queue) and "checks needed in the merge queue". We just have to add
# the check to the branch protection list.
#
# Ergo, if we know we're not going to run the Playwright tests, we need to add a
# passing status check manually.
mark_skipped:
if: github.event.workflow_run.event != 'merge_group'
permissions:
statuses: write
runs-on: ubuntu-latest
steps:
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
with:
authToken: "${{ secrets.GITHUB_TOKEN }}"
state: success
description: Playwright skipped
# Keep in step with the `context` that is updated by `Sibz/github-status-action`
# in matrix-org/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml.
context: "${{ github.workflow }} / end-to-end-tests"
sha: "${{ github.event.workflow_run.head_sha }}"
# We only want to run the playwright tests on merge queue to prevent regressions
# from creeping in. They take a long time to run and consume multiple concurrent runners.
skip: ${{ github.event_name != 'merge_group' }}
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
fail-fast: false
matrix:
include:
- repo: vector-im/element-web
- repo: element-hq/element-web
event: element-web-notify
- repo: matrix-org/matrix-react-sdk
event: upstream-sdk-notify
+4 -3
View File
@@ -14,7 +14,7 @@ jobs:
name: Preview Changelog
runs-on: ubuntu-latest
steps:
- uses: mheap/github-action-required-labels@80a96a4863886addcbc9f681b5b295ba7f5424e1 # v5
- uses: mheap/github-action-required-labels@5847eef68201219cf0a4643ea7be61e77837bbce # v5
if: github.event_name != 'merge_group'
with:
labels: |
@@ -46,7 +46,8 @@ jobs:
if: github.event.action == 'opened'
steps:
- name: Check membership
uses: tspascoal/get-user-teams-membership@ba78054988f58bea69b7c6136d563236f8ed2fc0 # v3
if: github.event.pull_request.user.login != 'renovate[bot]'
uses: tspascoal/get-user-teams-membership@57e9f42acd78f4d0f496b3be4368fc5f62696662 # v3
id: teams
with:
username: ${{ github.event.pull_request.user.login }}
@@ -55,7 +56,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
- name: Add label
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
if: steps.teams.outputs.isTeamMember == 'false'
uses: actions/github-script@v7
with:
script: |
@@ -1,4 +1,5 @@
name: Release Drafter
# Workflow used by other workflows to generate draft releases.
name: Release Drafter Reusable
on:
workflow_call:
inputs:
+4
View File
@@ -1,3 +1,7 @@
# Generates the draft release for the js-sdk
# Normally triggered whenever anything is merged to the staging branch, but
# also has a workflow dispatch trigger in case it needs running manually due
# to failures / workflow updates etc.
name: Release Drafter
on:
push:
+10 -10
View File
@@ -59,7 +59,7 @@ jobs:
- name: Get draft release
id: draft-release
uses: cardinalby/git-get-release-action@cedef2faf69cb7c55b285bad07688d04430b7ada # v1
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
@@ -294,15 +294,15 @@ jobs:
remove-labels: X-Upcoming-Release-Blocker
add-labels: X-Release-Blocker
- name: Wait for master->develop gitflow merge
if: inputs.final
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
with:
ref: master
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
check-name: merge
allowed-conclusions: success
# - name: Wait for master->develop gitflow merge
# if: inputs.final
# uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
# with:
# ref: master
# repo-token: ${{ secrets.GITHUB_TOKEN }}
# wait-interval: 10
# check-name: merge
# allowed-conclusions: success
bump-downstreams:
name: Update npm dependency in downstream projects
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
- name: 🚀 Publish to npm
id: npm-publish
uses: JS-DevTools/npm-publish@4b07b26a2f6e0a51846e1870223e545bae91c552 # v3.0.1
uses: JS-DevTools/npm-publish@19c28f1ef146469e409470805ea4279d47c3d35c # v3.1.1
with:
token: ${{ secrets.NPM_TOKEN }}
access: public
+20 -25
View File
@@ -39,12 +39,6 @@ jobs:
- name: 🧮 Checkout code
uses: actions/checkout@v4
- name: 🧮 Checkout gh-pages
uses: actions/checkout@v4
with:
ref: gh-pages
path: _docs
- name: 🔧 Yarn cache
uses: actions/setup-node@v4
with:
@@ -53,25 +47,26 @@ jobs:
- name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile"
- name: 🔨 Install symlinks
run: |
sudo apt-get update
sudo apt-get install -y symlinks
- name: 📖 Generate docs
run: |
yarn tpv purge --yes --out _docs --stale --major 10
yarn gendoc
symlinks -rc _docs
run: yarn gendoc
- name: 🔨 Set up git
run: |
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: _docs
- name: 🚀 Deploy
run: |
git add . --all
git commit -m "Update docs"
git push
working-directory: _docs
docs-deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: docs
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+6 -14
View File
@@ -12,10 +12,6 @@ on:
type: boolean
required: false
description: "Whether to combine multiple LCOV and jest-sonar-report files in coverage artifact"
extra_args:
type: string
required: false
description: "Extra args to pass to SonarCloud"
jobs:
sonarqube:
runs-on: ubuntu-latest
@@ -34,7 +30,7 @@ jobs:
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🧮 Checkout code"
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
@@ -60,14 +56,14 @@ jobs:
- 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
coverage=$(find coverage -type f -name '*lcov.info' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
echo "sonar.javascript.lcov.reportPaths=$coverage" >> sonar-project.properties
reports=$(find coverage -type f -name 'jest-sonar-report*.xml' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
echo "sonar.testExecutionReportPaths=$reports" >> sonar-project.properties
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v2.7
uses: matrix-org/sonarcloud-workflow-action@v3.2
# 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:
@@ -78,10 +74,6 @@ jobs:
branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }}
extra_args: |
${{ inputs.extra_args }}
-Dsonar.javascript.lcov.reportPaths=${{ steps.extra_args.outputs.reportPaths }}
-Dsonar.testExecutionReportPaths=${{ steps.extra_args.outputs.testExecutionReportPaths }}
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
if: always()
+16 -7
View File
@@ -83,13 +83,6 @@ jobs:
- name: Generate Docs
run: "yarn run gendoc --treatWarningsAsErrors"
# 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: Upload Artifact
uses: actions/upload-artifact@v4
with:
@@ -97,3 +90,19 @@ jobs:
path: _docs
# We'll only use this in a workflow_run, then we're done with it
retention-days: 1
analyse_dead_code:
name: "Analyse Dead Code"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
cache: "yarn"
- name: Install Deps
run: "yarn install --frozen-lockfile"
- name: Run linter
run: "yarn run lint:knip"
+18
View File
@@ -71,6 +71,24 @@ jobs:
disable_coverage: true
matrix-js-sdk-sha: ${{ github.sha }}
complement-crypto:
name: "Run Complement Crypto tests"
if: github.event_name == 'merge_group'
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
with:
use_js_sdk: "."
# we need this so the job is reported properly when run in a merge queue
downstream-complement-crypto:
name: Downstream Complement Crypto tests
runs-on: ubuntu-latest
if: always()
needs:
- complement-crypto
steps:
- if: needs.complement-crypto.result != 'skipped' && needs.complement-crypto.result != 'success'
run: exit 1
# Hook for branch protection to skip downstream testing outside of merge queues
# and skip sonarcloud coverage within merge queues
downstream:
+14
View File
@@ -0,0 +1,14 @@
name: Move new issues into Issue triage board
on:
issues:
types: [opened]
jobs:
automate-project-columns-next:
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@main
with:
project-url: https://github.com/orgs/element-hq/projects/120
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+1 -1
View File
@@ -6,6 +6,6 @@ on:
jobs:
call-triage-labelled:
uses: vector-im/element-web/.github/workflows/triage-labelled.yml@develop
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@develop
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
+107
View File
@@ -1,3 +1,110 @@
Changes in [32.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.4.0) (2024-05-22)
==================================================================================================
* No changes
Changes in [32.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.3.0) (2024-05-21)
==================================================================================================
## ✨ Features
* Simplify OIDC types \& export `decodeIdToken` ([#4193](https://github.com/matrix-org/matrix-js-sdk/pull/4193)). Contributed by @t3chguy.
* Add helpers for authenticated media, and associated documentation ([#4185](https://github.com/matrix-org/matrix-js-sdk/pull/4185)). Contributed by @turt2live.
## 🐛 Bug Fixes
* Fix state\_events.ts types ([#4196](https://github.com/matrix-org/matrix-js-sdk/pull/4196)). Contributed by @t3chguy.
* Fix sendEventHttpRequest for `m.room.redaction` events without `redacts` ([#4192](https://github.com/matrix-org/matrix-js-sdk/pull/4192)). Contributed by @t3chguy.
Changes in [32.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.2.0) (2024-05-07)
==================================================================================================
## ✨ Features
* Use a different error code for UTDs when user was not in the room ([#4172](https://github.com/matrix-org/matrix-js-sdk/pull/4172)). Contributed by @uhoreg.
* Modernize window.crypto access constants ([#4169](https://github.com/matrix-org/matrix-js-sdk/pull/4169)). Contributed by @turt2live.
* Improve compliance with MSC3266 ([#4155](https://github.com/matrix-org/matrix-js-sdk/pull/4155)). Contributed by @AndrewFerr.
* Add comment to make clear that RoomStateEvent.Events does not update related objects in the js-sdk ([#4152](https://github.com/matrix-org/matrix-js-sdk/pull/4152)). Contributed by @toger5.
* Crypto: use a new error code for UTDs from device-relative historical events ([#4139](https://github.com/matrix-org/matrix-js-sdk/pull/4139)). Contributed by @richvdh.
## 🐛 Bug Fixes
* Element-R: Fix rust migration when ssss secret are stored not encryted in cache (old legacy behavior) ([#4168](https://github.com/matrix-org/matrix-js-sdk/pull/4168)). Contributed by @BillCarsonFr.
Changes in [32.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.1.0) (2024-04-23)
==================================================================================================
## ✨ Features
* Add support for device dehydration v2 (Element R) ([#4062](https://github.com/matrix-org/matrix-js-sdk/pull/4062)). Contributed by @uhoreg.
* OIDC improvements in prep of OIDC-QR reciprocation ([#4149](https://github.com/matrix-org/matrix-js-sdk/pull/4149)). Contributed by @t3chguy.
## 🐛 Bug Fixes
* Validate backup private key before migrating it ([#4114](https://github.com/matrix-org/matrix-js-sdk/pull/4114)). Contributed by @BillCarsonFr.
* ElementR| Retry query backup until it works during migration to avoid spurious correption error popup ([#4113](https://github.com/matrix-org/matrix-js-sdk/pull/4113)). Contributed by @BillCarsonFr.
Changes in [32.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.0.0) (2024-04-09)
==================================================================================================
## 🚨 BREAKING CHANGES
* Remove various deprecated methods \& re-exports ([#4125](https://github.com/matrix-org/matrix-js-sdk/pull/4125)). Contributed by @t3chguy.
* Remove the logic that throws when the lazy loading options has changed. ([#4124](https://github.com/matrix-org/matrix-js-sdk/pull/4124)). Contributed by @langleyd.
* Fix highlights from threads disappearing on new messages ([#4106](https://github.com/matrix-org/matrix-js-sdk/pull/4106)). Contributed by @dbkr.
## ✨ Features
* Add new `decryptExistingEvent` test helper ([#4133](https://github.com/matrix-org/matrix-js-sdk/pull/4133)). Contributed by @richvdh.
* Improve types for `sendEvent` ([#4108](https://github.com/matrix-org/matrix-js-sdk/pull/4108)). Contributed by @t3chguy.
* Remove various deprecated methods \& re-exports ([#4125](https://github.com/matrix-org/matrix-js-sdk/pull/4125)). Contributed by @t3chguy.
* Add new enum for verification methods. ([#4129](https://github.com/matrix-org/matrix-js-sdk/pull/4129)). Contributed by @richvdh.
* Add some test utils in a new entrypoint ([#4127](https://github.com/matrix-org/matrix-js-sdk/pull/4127)). Contributed by @richvdh.
* Improve types for `sendStateEvent` ([#4105](https://github.com/matrix-org/matrix-js-sdk/pull/4105)). Contributed by @t3chguy.
## 🐛 Bug Fixes
* Improve types for `IPowerLevelsContent` and `hasSufficientPowerLevelFor` ([#4128](https://github.com/matrix-org/matrix-js-sdk/pull/4128)). Contributed by @galash13.
* Remove the logic that throws when the lazy loading options has changed. ([#4124](https://github.com/matrix-org/matrix-js-sdk/pull/4124)). Contributed by @langleyd.
* Fix highlights from threads disappearing on new messages ([#4106](https://github.com/matrix-org/matrix-js-sdk/pull/4106)). Contributed by @dbkr.
* Extend logic for local notification processing to threads ([#4111](https://github.com/matrix-org/matrix-js-sdk/pull/4111)). Contributed by @dbkr.
* Fix public rooms post request search params and body ([#4110](https://github.com/matrix-org/matrix-js-sdk/pull/4110)). Contributed by @ajbura.
* Fix bugs with the first reply to a thread ([#4104](https://github.com/matrix-org/matrix-js-sdk/pull/4104)). Contributed by @dbkr.
Changes in [31.6.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.6.1) (2024-03-28)
==================================================================================================
## 🐛 Bug Fixes
* Fix merging of default push rules ([#4136](https://github.com/matrix-org/matrix-js-sdk/pull/4136)).
Changes in [31.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.6.0) (2024-03-26)
==================================================================================================
## ✨ Features
* Introduce Membership TS type (take 2) ([#4107](https://github.com/matrix-org/matrix-js-sdk/pull/4107)). Contributed by @andybalaam.
* fix automatic DM avatar with functional members ([#4017](https://github.com/matrix-org/matrix-js-sdk/pull/4017)). Contributed by @HarHarLinks.
* Export types describing all specced media event formats ([#4092](https://github.com/matrix-org/matrix-js-sdk/pull/4092)). Contributed by @t3chguy.
* Add `.m.rule.is_room_mention` push rule to DEFAULT\_OVERRIDE\_RULES ([#4100](https://github.com/matrix-org/matrix-js-sdk/pull/4100)). Contributed by @t3chguy.
* Make sending ContentLoaded optional for a widgetClient ([#4086](https://github.com/matrix-org/matrix-js-sdk/pull/4086)). Contributed by @toger5.
## 🐛 Bug Fixes
* Migrate own identity local trust to rust crypto ([#4090](https://github.com/matrix-org/matrix-js-sdk/pull/4090)). Contributed by @BillCarsonFr.
* Fix race condition with sliding sync extensions ([#4089](https://github.com/matrix-org/matrix-js-sdk/pull/4089)). Contributed by @zzorba.
Changes in [31.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.5.0) (2024-03-12)
==================================================================================================
## ✨ Features
* Update MSC2965 OIDC Discovery implementation ([#4064](https://github.com/matrix-org/matrix-js-sdk/pull/4064)). Contributed by @t3chguy.
## 🐛 Bug Fixes
* Add basic retry for rust crypto outgoing requests ([#4061](https://github.com/matrix-org/matrix-js-sdk/pull/4061)). Contributed by @BillCarsonFr.
Changes in [31.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v31.4.0) (2024-02-27)
==================================================================================================
## ✨ Features
+59 -56
View File
@@ -21,15 +21,9 @@ endpoints from before Matrix 1.1, for example.
# Quickstart
## In a browser
### Note, the browserify build has been removed. Please use a bundler like webpack or vite instead.
## 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.
If you wish to use a ponyfill or adapter of some sort then pass it as `fetchFn` to the MatrixClient constructor options.
> [!IMPORTANT]
> Servers may require or use authenticated endpoints for media (images, files, avatars, etc). See the
> [Authenticated Media](#authenticated-media) section for information on how to enable support for this.
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.
@@ -47,8 +41,6 @@ client.publicRooms(function (err, 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
@@ -101,12 +93,40 @@ Object.keys(client.store.rooms).forEach((roomId) => {
});
```
## Authenticated media
Servers supporting [MSC3916](https://github.com/matrix-org/matrix-spec-proposals/pull/3916) will require clients, like
yours, to include an `Authorization` header when `/download`ing or `/thumbnail`ing media. For NodeJS environments this
may be as easy as the following code snippet, though web browsers may need to use [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)
to append the header when using the endpoints in `<img />` elements and similar.
```javascript
const downloadUrl = client.mxcUrlToHttp(
/*mxcUrl=*/ "mxc://example.org/abc123", // the MXC URI to download/thumbnail, typically from an event or profile
/*width=*/ undefined, // part of the thumbnail API. Use as required.
/*height=*/ undefined, // part of the thumbnail API. Use as required.
/*resizeMethod=*/ undefined, // part of the thumbnail API. Use as required.
/*allowDirectLinks=*/ false, // should generally be left `false`.
/*allowRedirects=*/ true, // implied supported with authentication
/*useAuthentication=*/ true, // the flag we're after in this example
);
const img = await fetch(downloadUrl, {
headers: {
Authorization: `Bearer ${client.getAccessToken()}`,
},
});
// Do something with `img`.
```
> [!WARNING]
> In future the js-sdk will _only_ return authentication-required URLs, mandating population of the `Authorization` header.
## 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 syncing (via `/sync`)
- 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
@@ -127,20 +147,20 @@ events for incoming data and state changes. Aside from wrapping the HTTP API, it
- 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
## Conventions
## Supported platforms
### Emitted events
`matrix-js-sdk` can be used in either Node.js applications (ensure you have the latest LTS version of Node.js installed),
or in browser applications, via a bundler such as Webpack or Vite.
The SDK will emit events using an `EventEmitter`. It also
emits object models (e.g. `Rooms`, `RoomMembers`) when they
are updated.
You can also use the sdk with [Deno](https://deno.land/) (`import npm:matrix-js-sdk`) but its not officialy supported.
## Emitted events
The SDK raises notifications to the application using
[`EventEmitter`s](https://nodejs.org/api/events.html#class-eventemitter). The `MatrixClient` itself
implements `EventEmitter`, as do many of the high-level abstractions such as `Room` and `RoomMember`.
```javascript
// Listen for low-level MatrixEvents
@@ -161,41 +181,21 @@ client.on(RoomMemberEvent.Typing, function (event, member) {
client.startClient();
```
### Promises and Callbacks
## Entry points
Most of the methods in the SDK are asynchronous: they do not directly return a
result, but instead return a [Promise](http://documentup.com/kriskowal/q/)
which will be fulfilled in the future.
As well as the primary entry point (`matrix-js-sdk`), there are several other entry points which may be useful:
The typical usage is something like:
```javascript
matrixClient.someMethod(arg1, arg2).then(function(result) {
...
});
```
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);
```
The main thing to note is that it is problematic to discard the result of a
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
removed from existing methods.
| Entry point | Description |
| ------------------------------ | --------------------------------------------------------------------------------------------------- |
| `matrix-js-sdk` | Primary entry point. High-level functionality, and lots of historical clutter in need of a cleanup. |
| `matrix-js-sdk/lib/crypto-api` | Cryptography functionality. |
| `matrix-js-sdk/lib/types` | Low-level types, reflecting data structures defined in the Matrix spec. |
| `matrix-js-sdk/lib/testing` | Test utilities, which may be useful in test code but should not be used in production code. |
## 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:
core functionality of the SDK. These examples assume the SDK is set up like this:
```javascript
import * as sdk from "matrix-js-sdk";
@@ -211,10 +211,10 @@ const matrixClient = sdk.createClient({
### Automatically join rooms when invited
```javascript
matrixClient.on(RoomMemberEvent.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(RoomEvent.MyMembership, function (room, membership, prevMembership) {
if (membership === KnownMembership.Invite) {
matrixClient.joinRoom(room.roomId).then(function () {
console.log("Auto-joined %s", room.roomId);
});
}
});
@@ -294,7 +294,7 @@ host the API reference from the source files like this:
```
$ yarn gendoc
$ cd _docs
$ cd docs
$ python -m http.server 8005
```
@@ -302,6 +302,9 @@ Then visit `http://localhost:8005` to see the API docs.
# End-to-end encryption support
**This section is outdated.** Use of `libolm` is deprecated and we are replacing it with support
from the matrix-rust-sdk (https://github.com/element-hq/element-web/issues/21972).
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.
+26
View File
@@ -0,0 +1,26 @@
module.exports = {
sourceMaps: true,
presets: [
[
"@babel/preset-env",
{
targets: {
esmodules: true,
},
// We want to output ES modules for the final build (mostly to ensure that
// async imports work correctly). However, jest doesn't support ES modules very
// well yet (see https://github.com/jestjs/jest/issues/9430), so we use commonjs
// when testing.
modules: process.env.NODE_ENV === "test" ? "commonjs" : false,
},
],
"@babel/preset-typescript",
],
plugins: [
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-runtime",
],
};
+1 -1
View File
@@ -115,7 +115,7 @@ rl.on("line", function (line) {
if (line.indexOf("/join ") === 0) {
var roomIndex = line.split(" ")[1];
viewingRoom = roomList[roomIndex];
if (viewingRoom.getMember(myUserId).membership === "invite") {
if (viewingRoom.getMember(myUserId).membership === KnownMembership.Invite) {
// join the room first
matrixClient.joinRoom(viewingRoom.roomId).then(
function (room) {
+36
View File
@@ -0,0 +1,36 @@
import { KnipConfig } from "knip";
export default {
entry: [
"src/index.ts",
"src/types.ts",
"src/browser-index.ts",
"src/indexeddb-worker.ts",
"scripts/**",
"spec/**",
"release.sh",
// For now, we include all source files as entrypoints as we have been bad about gutwrenched imports
"src/**",
],
project: ["**/*.{js,ts}"],
ignore: ["examples/**"],
ignoreDependencies: [
// Required for `action-validator`
"@action-validator/*",
// Used for git pre-commit hooks
"husky",
// Used in script which only runs in environment with `@octokit/rest` installed
"@octokit/rest",
// Used by jest
"jest-environment-jsdom",
"babel-jest",
"ts-node",
// Used by `@babel/plugin-transform-runtime`
"@babel/runtime",
],
ignoreBinaries: [
// Used when available by reusable workflow `.github/workflows/release-make.yml`
"dist",
],
ignoreExportsUsedInFile: true,
} satisfies KnipConfig;
+17 -18
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "31.4.0",
"version": "33.0.0-rc.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=18.0.0"
@@ -19,6 +19,7 @@
"lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src spec",
"lint:types": "tsc --noEmit",
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
"lint:knip": "knip",
"test": "jest",
"test:watch": "jest --watch",
"coverage": "yarn test --coverage"
@@ -52,7 +53,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^4.5.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^5.0.0",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
@@ -67,8 +68,8 @@
"uuid": "9"
},
"devDependencies": {
"@action-validator/cli": "^0.5.3",
"@action-validator/core": "^0.5.3",
"@action-validator/cli": "^0.6.0",
"@action-validator/core": "^0.6.0",
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.10",
@@ -80,7 +81,6 @@
"@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.7",
"@matrix-org/olm": "3.2.15",
"@peculiar/webcrypto": "^1.4.5",
@@ -92,42 +92,41 @@
"@types/node": "18",
"@types/sdp-transform": "^2.4.5",
"@types/uuid": "9",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"babel-jest": "^29.0.0",
"debug": "^4.3.4",
"domexception": "^4.0.0",
"eslint": "8.56.0",
"eslint": "8.57.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-jest": "^28.0.0",
"eslint-plugin-jsdoc": "^48.0.0",
"eslint-plugin-matrix-org": "^1.0.0",
"eslint-plugin-tsdoc": "^0.2.17",
"eslint-plugin-unicorn": "^50.0.0",
"eslint-plugin-unicorn": "^53.0.0",
"fake-indexeddb": "^5.0.2",
"fetch-mock": "9.11.0",
"fetch-mock-jest": "^1.5.1",
"husky": "^8.0.3",
"husky": "^9.0.0",
"jest": "^29.0.0",
"jest-environment-jsdom": "^29.0.0",
"jest-localstorage-mock": "^2.4.6",
"jest-mock": "^29.0.0",
"knip": "^5.0.0",
"lint-staged": "^15.0.2",
"matrix-mock-request": "^2.5.0",
"node-fetch": "^2.7.0",
"prettier": "3.2.4",
"prettier": "3.2.5",
"rimraf": "^5.0.0",
"ts-node": "^10.9.1",
"typedoc": "^0.24.0",
"typedoc-plugin-coverage": "^2.1.0",
"ts-node": "^10.9.2",
"typedoc": "^0.25.10",
"typedoc-plugin-coverage": "^3.0.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"
"typescript": "^5.3.3"
},
"@casualbot/jest-sonar-reporter": {
"outputDirectory": "coverage",
+41 -15
View File
@@ -2,23 +2,28 @@
const fs = require("fs");
// Dependency can be the name of an entry in package.json, in which case the owner, repo & version will be looked up in its own package.json
// Or it can be a string in the form owner/repo@tag
// Or it can be a tuple of dependency, from version, to version, in which case a list of releases in that range (to inclusive) will be returned
async function listReleases(github, owner, repo) {
const response = await github.rest.repos.listReleases({
owner,
repo,
per_page: 100,
});
// Filters out draft releases
return response.data.filter((release) => !release.draft);
}
// Dependency can be a tuple of dependency, from version, to version, in which case a list of releases in that range (to inclusive) will be returned
// Or it can be a string in the form accepted by `getRelease`
async function getReleases(github, dependency) {
if (Array.isArray(dependency)) {
const [dep, fromVersion, toVersion] = dependency;
const upstreamPackageJson = getDependencyPackageJson(dep);
const [owner, repo] = upstreamPackageJson.repository.url.split("/").slice(-2);
const response = await github.rest.repos.listReleases({
owner,
repo,
per_page: 100,
});
const unfilteredReleases = await listReleases(github, owner, repo);
// Only include non-draft & non-prerelease releases, unless the to-release is a pre-release, include that one
const releases = response.data.filter(
(release) => !release.draft && (!release.prerelease || release.tag_name === `v${toVersion}`),
const releases = unfilteredReleases.filter(
(release) => !release.prerelease || release.tag_name === `v${toVersion}`,
);
const fromVersionIndex = releases.findIndex((release) => release.tag_name === `v${fromVersion}`);
@@ -30,14 +35,35 @@ async function getReleases(github, dependency) {
return [await getRelease(github, dependency)];
}
// Dependency can be the name of an entry in package.json, in which case the owner, repo & version will be looked up in its own package.json
// Or it can be a string in the form owner/repo@tag - in this case the tag is used exactly to find the release
// Or it can be a string in the form owner/repo~tag - in this case the latest tag in the same major.minor.patch set is used to find the release
async function getRelease(github, dependency) {
let owner;
let repo;
let tag;
if (dependency.includes("/") && dependency.includes("@")) {
owner = dependency.split("/")[0];
repo = dependency.split("/")[1].split("@")[0];
tag = dependency.split("@")[1];
if (dependency.includes("/")) {
let rest;
[owner, rest] = dependency.split("/");
if (dependency.includes("@")) {
[repo, tag] = rest.split("@");
} else if (dependency.includes("~")) {
[repo, tag] = rest.split("~");
if (tag.includes("-rc.")) {
// If the tag is an RC, find the latest matching RC in the set
try {
const releases = await listReleases(github, owner, repo);
const baseVersion = tag.split("-rc.")[0];
const release = releases.find((release) => release.tag_name.startsWith(baseVersion));
if (release) return release;
} catch (e) {
// Fall back to getReleaseByTag
}
}
}
} else {
const upstreamPackageJson = getDependencyPackageJson(dependency);
[owner, repo] = upstreamPackageJson.repository.url.split("/").slice(-2);
@@ -129,7 +155,7 @@ if (require.main === module) {
process.exit(1);
}
const [releaseId, ...dependencies] = process.argv.slice(2);
main({ github, releaseId, dependencies: ["matrix-react-sdk"] }).then((output) => {
main({ github, releaseId, dependencies }).then((output) => {
// eslint-disable-next-line no-console
console.log(output);
});
+88 -23
View File
@@ -81,33 +81,37 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
};
}
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
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(),
});
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);
syncResponder = new SyncResponder(homeserverUrl);
e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
/** an object which intercepts `/keys/upload` requests on the test homeserver */
new E2EKeyReceiver(homeserverUrl);
// Silence warnings from the backup manager
fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), {
status: 404,
body: { errcode: "M_NOT_FOUND" },
});
// Silence warnings from the backup manager
fetchMock.getOnce(new URL("/_matrix/client/v3/room_keys/version", homeserverUrl).toString(), {
status: 404,
body: { errcode: "M_NOT_FOUND" },
});
await initCrypto(aliceClient);
});
await initCrypto(aliceClient);
},
/* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */
10000,
);
afterEach(async () => {
await aliceClient.stopClient();
@@ -343,6 +347,67 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
expect(isCrossSigningReady).toBeTruthy();
});
it("should return false if identity is not trusted, even if the secrets are in 4S", async () => {
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
// Complete initial sync, to get the 4S account_data events stored
mockInitialApiRequests(aliceClient.getHomeserverUrl());
// For this test we need to have a well-formed 4S setup.
const mockSecretInfo = {
encrypted: {
// Don't care about the actual values here, just need to be present for validation
KeyId: {
iv: "IVIVIVIVIVIVIV",
ciphertext: "CIPHERTEXTB64",
mac: "MACMACMAC",
},
},
};
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
account_data: {
events: [
{
type: "m.secret_storage.key.KeyId",
content: {
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
// iv and mac not relevant for this test
},
},
{
type: "m.secret_storage.default_key",
content: {
key: "KeyId",
},
},
{
type: "m.cross_signing.master",
content: mockSecretInfo,
},
{
type: "m.cross_signing.user_signing",
content: mockSecretInfo,
},
{
type: "m.cross_signing.self_signing",
content: mockSecretInfo,
},
],
},
});
await aliceClient.startClient();
await syncPromise(aliceClient);
// Sanity: ensure that the secrets are in 4S
const status = await aliceClient.getCrypto()!.getCrossSigningStatus();
expect(status.privateKeysInSecretStorage).toBeTruthy();
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
expect(isCrossSigningReady).toBeFalsy();
});
});
describe("getCrossSigningKeyId", () => {
+196 -56
View File
@@ -26,6 +26,7 @@ import * as testUtils from "../../test-utils/test-utils";
import {
advanceTimersUntil,
CRYPTO_BACKENDS,
emitPromise,
getSyncResponse,
InitCrypto,
mkEventCustom,
@@ -50,6 +51,7 @@ import {
ClientEvent,
createClient,
CryptoEvent,
HistoryVisibility,
IClaimOTKsResult,
IContent,
IDownloadKeyResult,
@@ -59,11 +61,11 @@ import {
MatrixClient,
MatrixEvent,
MatrixEventEvent,
MsgType,
PendingEventOrdering,
Room,
RoomMember,
RoomStateEvent,
HistoryVisibility,
} from "../../../src/matrix";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
@@ -80,12 +82,12 @@ import { SecretStorageKeyDescription } from "../../../src/secret-storage";
import {
CrossSigningKey,
CryptoCallbacks,
DecryptionFailureCode,
EventShieldColour,
EventShieldReason,
KeyBackupInfo,
} from "../../../src/crypto-api";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { DecryptionError } from "../../../src/crypto/algorithms";
import { IKeyBackup } from "../../../src/crypto/backup";
import {
createOlmAccount,
@@ -99,12 +101,16 @@ import {
} from "./olm-utils";
import { ToDevicePayload } from "../../../src/models/ToDeviceMessage";
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
import { UNSIGNED_MEMBERSHIP_FIELD } from "../../../src/@types/event";
import { KnownMembership } from "../../../src/@types/membership";
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();
jest.useRealTimers();
});
/**
@@ -464,56 +470,58 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
describe("Unable to decrypt error codes", function () {
it("Encryption fails with expected UISI error", async () => {
beforeEach(() => {
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
});
it("Decryption fails with UISI error", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const awaitUISI = new Promise<void>((resolve) => {
aliceClient.on(MatrixEventEvent.Decrypted, (ev, err) => {
const error = err as DecryptionError;
if (error.code == "MEGOLM_UNKNOWN_INBOUND_SESSION_ID") {
resolve();
}
});
});
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
const awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
// Ensure that the timestamp post-dates the creation of our device
const encryptedEvent = {
...testData.ENCRYPTED_EVENT,
origin_server_ts: Date.now(),
};
// Alice gets both the events in a single sync
const syncResponse = {
next_batch: 1,
rooms: {
join: {
[testData.TEST_ROOM_ID]: { timeline: { events: [testData.ENCRYPTED_EVENT] } },
[testData.TEST_ROOM_ID]: { timeline: { events: [encryptedEvent] } },
},
},
};
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
await awaitUISI;
const ev = await awaitDecryption;
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID);
});
it("Encryption fails with expected Unknown Index error", async () => {
it("Decryption fails with Unknown Index error", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const awaitUnknownIndex = new Promise<void>((resolve) => {
aliceClient.on(MatrixEventEvent.Decrypted, (ev, err) => {
const error = err as DecryptionError;
if (error.code == "OLM_UNKNOWN_MESSAGE_INDEX") {
resolve();
}
});
});
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
const awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
await aliceClient.getCrypto()!.importRoomKeys([testData.RATCHTED_MEGOLM_SESSION_DATA]);
// Alice gets both the events in a single sync
// Ensure that the timestamp post-dates the creation of our device
const encryptedEvent = {
...testData.ENCRYPTED_EVENT,
origin_server_ts: Date.now(),
};
const syncResponse = {
next_batch: 1,
rooms: {
join: {
[testData.TEST_ROOM_ID]: { timeline: { events: [testData.ENCRYPTED_EVENT] } },
[testData.TEST_ROOM_ID]: { timeline: { events: [encryptedEvent] } },
},
},
};
@@ -521,23 +529,168 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
await awaitUnknownIndex;
const ev = await awaitDecryption;
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX);
});
it("Encryption fails with Unable to decrypt for other errors", async () => {
describe("Historical events", () => {
async function sendEventAndAwaitDecryption(props: Partial<IEvent> = {}): Promise<MatrixEvent> {
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
const awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
// Ensure that the timestamp pre-dates the creation of our device: set it to 24 hours ago
const encryptedEvent = {
...testData.ENCRYPTED_EVENT,
origin_server_ts: Date.now() - 24 * 3600 * 1000,
...props,
};
const syncResponse = {
next_batch: 1,
rooms: {
join: {
[testData.TEST_ROOM_ID]: { timeline: { events: [encryptedEvent] } },
},
},
};
syncResponder.sendOrQueueSyncResponse(syncResponse);
return await awaitDecryption;
}
newBackendOnly("fails with HISTORICAL_MESSAGE_BACKUP_NO_BACKUP when there is no backup", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const ev = await sendEventAndAwaitDecryption();
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
});
newBackendOnly("fails with HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED when the backup is broken", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const ev = await sendEventAndAwaitDecryption();
expect(ev.decryptionFailureReason).toEqual(
DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED,
);
});
newBackendOnly("fails with HISTORICAL_MESSAGE_WORKING_BACKUP when backup is working", async () => {
// The test backup data is signed by a dummy device. We'll need to tell Alice about the device, and
// later, tell her to trust it, so that she trusts the backup.
const e2eResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
e2eResponder.addDeviceKeys(testData.SIGNED_TEST_DEVICE_DATA);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
await startClientAndAwaitFirstSync();
await aliceClient
.getCrypto()!
.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
testData.SIGNED_BACKUP_DATA.version!,
);
// Tell Alice to trust the dummy device that signed the backup
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
expect(devices.get(TEST_USER_ID)!.keys()).toContain(testData.TEST_DEVICE_ID);
await aliceClient.getCrypto()!.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
// Tell Alice to check and enable backup
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
// Sanity: Alice should now have working backup.
expect(await aliceClient.getCrypto()!.getActiveSessionBackupVersion()).toEqual(
testData.SIGNED_BACKUP_DATA.version,
);
// Finally! we can check what happens when we get an event.
const ev = await sendEventAndAwaitDecryption();
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP);
});
newBackendOnly("fails with NOT_JOINED if user is not member of room", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const ev = await sendEventAndAwaitDecryption({
unsigned: {
[UNSIGNED_MEMBERSHIP_FIELD.name]: "leave",
},
});
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED);
});
newBackendOnly(
"fails with another error when the server reports user was a member of the room",
async () => {
// This tests that when the server reports that the user
// was invited at the time the event was sent, then we
// don't get a HISTORICAL_MESSAGE_USER_NOT_JOINED error,
// and instead get some other error, since the user should
// have gotten the key for the event.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const ev = await sendEventAndAwaitDecryption({
unsigned: {
[UNSIGNED_MEMBERSHIP_FIELD.name]: "invite",
},
});
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
},
);
newBackendOnly(
"fails with another error when the server reports user was a member of the room",
async () => {
// This tests that when the server reports the user's
// membership, and reports that the user was joined, then we
// don't get a HISTORICAL_MESSAGE_USER_NOT_JOINED error, and
// instead get some other error.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const ev = await sendEventAndAwaitDecryption({
unsigned: {
[UNSIGNED_MEMBERSHIP_FIELD.name]: "join",
},
});
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
},
);
});
it("Decryption fails with Unable to decrypt for other errors", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
await aliceClient.getCrypto()!.importRoomKeys([testData.MEGOLM_SESSION_DATA]);
const awaitDecryptionError = new Promise<void>((resolve) => {
aliceClient.on(MatrixEventEvent.Decrypted, (ev, err) => {
const error = err as DecryptionError;
aliceClient.on(MatrixEventEvent.Decrypted, (ev) => {
// rust and libolm can't have an exact 1:1 mapping for all errors,
// but some errors are part of API and should match
if (
error.code != "MEGOLM_UNKNOWN_INBOUND_SESSION_ID" &&
error.code != "OLM_UNKNOWN_MESSAGE_INDEX"
ev.decryptionFailureReason !== DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID &&
ev.decryptionFailureReason !== DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX
) {
resolve();
}
@@ -1007,10 +1160,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
return encryptedMessage;
}
afterEach(() => {
jest.useRealTimers();
});
newBackendOnly("should rotate the session after 2 messages", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
@@ -1242,7 +1391,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
content: { algorithm: "m.megolm.v1.aes-sha2" },
}),
testUtils.mkMembership({
mship: "join",
mship: KnownMembership.Join,
sender: aliceClient.getUserId()!,
}),
],
@@ -1661,7 +1810,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
type: "m.room.member",
state_key: "@alice:localhost",
content: {
membership: "invite",
membership: KnownMembership.Invite,
},
},
],
@@ -1810,7 +1959,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
type: "m.room.member",
state_key: "@alice:localhost",
content: {
membership: "invite",
membership: KnownMembership.Invite,
},
},
],
@@ -1886,7 +2035,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
{
type: "m.room.member",
state_key: aliceClient.getUserId(),
content: { membership: "join" },
content: { membership: KnownMembership.Join },
event_id: "$alijoin",
},
],
@@ -1913,7 +2062,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
{
type: "m.room.member",
state_key: "@other:user",
content: { membership: "invite" },
content: { membership: KnownMembership.Invite },
event_id: "$otherinvite",
},
],
@@ -1927,7 +2076,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
expectAliceKeyQuery({ device_keys: { "@other:user": {} }, failures: {} });
aliceClient.on(RoomStateEvent.NewMember, (_e, _s, member: RoomMember) => {
if (member.userId == "@other:user") {
aliceClient.sendMessage(testRoomId, { msgtype: "m.text", body: "Hello, World" });
aliceClient.sendMessage(testRoomId, { msgtype: MsgType.Text, body: "Hello, World" });
}
});
@@ -2061,7 +2210,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
fetchMock.getOnce(new RegExp(membersPath), {
chunk: [
testUtils.mkMembershipCustom({
membership: "join",
membership: KnownMembership.Join,
sender: "@bob:xyz",
}),
],
@@ -2194,10 +2343,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
});
afterEach(() => {
jest.useRealTimers();
});
function awaitKeyUploadRequest(): Promise<{ keysCount: number; fallbackKeysCount: number }> {
return new Promise((resolve) => {
const listener = (url: string, options: RequestInit) => {
@@ -2260,10 +2405,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
describe("getUserDeviceInfo", () => {
afterEach(() => {
jest.useRealTimers();
});
// From https://spec.matrix.org/v1.6/client-server-api/#post_matrixclientv3keysquery
// Using extracted response from matrix.org, it needs to have real keys etc to pass old crypto verification
const queryResponseBody = {
@@ -2752,10 +2893,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
});
afterEach(() => {
jest.useRealTimers();
});
it("Should be able to restore from 4S after bootstrap", async () => {
const backupVersion = "1";
await bootstrapSecurity(backupVersion);
@@ -3090,7 +3227,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const roomResponse = {
state: {
events: [
mkMembershipCustom({ membership: "join", sender: persistentStoreClient.getSafeUserId() }),
mkMembershipCustom({
membership: KnownMembership.Join,
sender: persistentStoreClient.getSafeUserId(),
}),
...stateEvents,
],
},
@@ -0,0 +1,181 @@
/*
Copyright 2024 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 fetchMock from "fetch-mock-jest";
import { createClient, ClientEvent, MatrixClient, MatrixEvent } from "../../../src";
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
import { AddSecretStorageKeyOpts } from "../../../src/secret-storage";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
describe("Device dehydration", () => {
it("should rehydrate and dehydrate a device", async () => {
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
cryptoCallbacks: {
getSecretStorageKey: async (keys: any, name: string) => {
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
},
},
});
await initializeSecretStorage(matrixClient, "@alice:localhost", "http://test.server");
// count the number of times the dehydration key gets set
let setDehydrationCount = 0;
matrixClient.on(ClientEvent.AccountData, (event: MatrixEvent) => {
if (event.getType() === "org.matrix.msc3814") {
setDehydrationCount++;
}
});
const crypto = matrixClient.getCrypto()!;
fetchMock.config.overwriteRoutes = true;
// start dehydration -- we start with no dehydrated device, and we
// store the dehydrated device that we create
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
});
let dehydratedDeviceBody: any;
let dehydrationCount = 0;
let resolveDehydrationPromise: () => void;
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
dehydratedDeviceBody = JSON.parse(opts.body as string);
dehydrationCount++;
if (resolveDehydrationPromise) {
resolveDehydrationPromise();
}
return {};
});
await crypto.startDehydration();
expect(dehydrationCount).toEqual(1);
// a week later, we should have created another dehydrated device
const dehydrationPromise = new Promise<void>((resolve, reject) => {
resolveDehydrationPromise = resolve;
});
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
await dehydrationPromise;
expect(dehydrationCount).toEqual(2);
// restart dehydration -- rehydrate the device that we created above,
// and create a new dehydrated device. We also set `createNewKey`, so
// a new dehydration key will be set
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
device_id: dehydratedDeviceBody.device_id,
device_data: dehydratedDeviceBody.device_data,
});
const eventsResponse = jest.fn((url, opts) => {
// rehydrating should make two calls to the /events endpoint.
// The first time will return a single event, and the second
// time will return no events (which will signal to the
// rehydration function that it can stop)
const body = JSON.parse(opts.body as string);
const nextBatch = body.next_batch ?? "0";
const events = nextBatch === "0" ? [{ sender: "@alice:localhost", type: "m.dummy", content: {} }] : [];
return {
events,
next_batch: nextBatch + "1",
};
});
fetchMock.post(
`path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device/${encodeURIComponent(dehydratedDeviceBody.device_id)}/events`,
eventsResponse,
);
await crypto.startDehydration(true);
expect(dehydrationCount).toEqual(3);
expect(setDehydrationCount).toEqual(2);
expect(eventsResponse.mock.calls).toHaveLength(2);
matrixClient.stopClient();
});
});
/** create a new secret storage and cross-signing keys */
async function initializeSecretStorage(
matrixClient: MatrixClient,
userId: string,
homeserverUrl: string,
): Promise<void> {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
});
const e2eKeyReceiver = new E2EKeyReceiver(homeserverUrl);
const e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
e2eKeyResponder.addKeyReceiver(userId, e2eKeyReceiver);
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {});
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {});
const accountData: Map<string, object> = new Map();
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
const name = url.split("/").pop()!;
const value = accountData.get(name);
if (value) {
return value;
} else {
return {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
};
}
});
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
const name = url.split("/").pop()!;
const value = JSON.parse(opts.body as string);
accountData.set(name, value);
matrixClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: name, content: value }));
return {};
});
await matrixClient.initRustCrypto();
const crypto = matrixClient.getCrypto()! as RustCrypto;
// we need to process a sync so that the OlmMachine will upload keys
await crypto.preprocessToDeviceMessages([]);
await crypto.onSyncCompleted({});
// create initial secret storage
async function createSecretStorageKey() {
return {
keyInfo: {} as AddSecretStorageKeyOpts,
privateKey: new Uint8Array(32),
};
}
await matrixClient.bootstrapCrossSigning({ setupNewCrossSigning: true });
await matrixClient.bootstrapSecretStorage({
createSecretStorageKey,
setupNewSecretStorage: true,
setupNewKeyBackup: false,
});
}
+34 -21
View File
@@ -21,7 +21,7 @@ import { Mocked } from "jest-mock";
import {
createClient,
CryptoApi,
Crypto,
CryptoEvent,
ICreateClientOpts,
IEvent,
@@ -45,6 +45,7 @@ import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keyback
import { IKeyBackup } from "../../../src/crypto/backup";
import { flushPromises } from "../../test-utils/flushPromises";
import { defer, IDeferred } from "../../../src/utils";
import { DecryptionFailureCode } from "../../../src/crypto-api";
const ROOM_ID = testData.TEST_ROOM_ID;
@@ -192,28 +193,31 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
}
}
beforeEach(async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
beforeEach(
async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
// ignore requests to send room key requests
fetchMock.put("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
// ignore requests to send room key requests
fetchMock.put("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceCrypto.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
testData.SIGNED_BACKUP_DATA.version!,
);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceCrypto.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
testData.SIGNED_BACKUP_DATA.version!,
);
// start after saving the private key
await aliceClient.startClient();
// 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 aliceClient.getCrypto()!.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
});
// 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 aliceClient.getCrypto()!.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
} /* it can take a while to initialise the crypto library on the first pass, so bump up the timeout. */,
10000,
);
it("Alice checks key backups when receiving a message she can't decrypt", async () => {
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", (url, request) => {
@@ -239,8 +243,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
const room = aliceClient.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
await advanceTimersUntil(awaitDecryption(event, { waitOnDecryptionFailure: true }));
// On the first decryption attempt, decryption fails.
await awaitDecryption(event);
expect(event.decryptionFailureReason).toEqual(
backend === "libolm"
? DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID
: DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP,
);
// Eventually, decryption succeeds.
await awaitDecryption(event, { waitOnDecryptionFailure: true });
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
});
@@ -297,7 +310,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
});
describe("recover from backup", () => {
let aliceCrypto: CryptoApi;
let aliceCrypto: Crypto.CryptoApi;
beforeEach(async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
+5 -4
View File
@@ -34,8 +34,9 @@ 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 { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent, MsgType } from "../../../src/matrix";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
import { KnownMembership } from "../../../src/@types/membership";
let aliTestClient: TestClient;
const roomId = "!room:localhost";
@@ -216,7 +217,7 @@ 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: MsgType.Text, body: "Hello, World" });
}
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
@@ -316,11 +317,11 @@ function firstSync(testClient: TestClient): Promise<void> {
state: {
events: [
testUtils.mkMembership({
mship: "join",
mship: KnownMembership.Join,
user: aliUserId,
}),
testUtils.mkMembership({
mship: "join",
mship: KnownMembership.Join,
user: bobUserId,
}),
],
+362 -137
View File
@@ -20,6 +20,10 @@ import fetchMock from "fetch-mock-jest";
import { createClient, CryptoEvent, IndexedDBCryptoStore } from "../../../src";
import { populateStore } from "../../test-utils/test_indexeddb_cryptostore_dump";
import { MSK_NOT_CACHED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/no_cached_msk_dump";
import { IDENTITY_NOT_TRUSTED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/unverified";
import { FULL_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/full_account";
import { EMPTY_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/empty_account";
jest.setTimeout(15000);
@@ -82,6 +86,44 @@ describe("MatrixClient.initRustCrypto", () => {
);
});
it("should create the meta db if given a storageKey", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
});
// No databases.
expect(await indexedDB.databases()).toHaveLength(0);
await matrixClient.initRustCrypto({ storageKey: new Uint8Array(32) });
// should have two indexed 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 create the meta db if given a storagePassword", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
});
// No databases.
expect(await indexedDB.databases()).toHaveLength(0);
await matrixClient.initRustCrypto({ storagePassword: "the cow is on the moon" });
// should have two indexed 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",
@@ -93,150 +135,333 @@ describe("MatrixClient.initRustCrypto", () => {
await matrixClient.initRustCrypto();
});
it("should migrate from libolm", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
auth_data: {
public_key: "q+HZiJdHl2Yopv9GGvv7EYSzDMrAiRknK4glSdoaomI",
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
"reDp6Mu+j+tfUL3/T6f5OBT3N825Lzpc43vvG+RvjX6V+KxXzodBQArgCoeEHLtL9OgSBmNrhTkSOX87MWCKAw",
"ed25519:KMFSTJSMLB":
"F8tyV5W6wNi0GXTdSg+gxSCULQi0EYxdAAqfkyNq58KzssZMw5i+PRA0aI2b+D7NH/aZaJrtiYNHJ0gWLSQvAw",
},
},
},
version: "7",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
etag: "1",
count: 79,
describe("Libolm Migration", () => {
beforeEach(() => {
fetchMock.reset();
});
fetchMock.post("path:/_matrix/client/v3/keys/query", {
device_keys: {
"@vdhtest200713:matrix.org": {
KMFSTJSMLB: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "KMFSTJSMLB",
keys: {
"curve25519:KMFSTJSMLB": "LKv0bKbc0EC4h0jknbemv3QalEkeYvuNeUXVRgVVTTU",
"ed25519:KMFSTJSMLB": "qK70DEqIXq7T+UU3v/al47Ab4JkMEBLpNrTBMbS5rrw",
},
user_id: "@vdhtest200713:matrix.org",
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:KMFSTJSMLB":
"aE+PdxLAdwQ/xfJwLmqebvt/lrT97fZas2SQFFrM+dPmHxQtjyS8csm88BLfGRjJKK1B/vWev3AaKqQZwLTUAw",
"ed25519:lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k":
"lCd4SA/JT1nnxsgN9yQaLJQhH5hkLMVVx6ba5JAjL1wpWVqyPxzMJHImX6vTztk6S8rybcdfYkea5W/Ii+4HCQ",
},
},
it("should migrate from libolm", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", FULL_ACCOUNT_DATASET.backupResponse);
fetchMock.post("path:/_matrix/client/v3/keys/query", FULL_ACCOUNT_DATASET.keyQueryResponse);
const testStoreName = "test-store";
await populateStore(testStoreName, FULL_ACCOUNT_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: FULL_ACCOUNT_DATASET.userId,
deviceId: FULL_ACCOUNT_DATASET.deviceId,
cryptoStore,
pickleKey: FULL_ACCOUNT_DATASET.pickleKey,
});
const progressListener = jest.fn();
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
await matrixClient.initRustCrypto();
const verificationStatus = await matrixClient
.getCrypto()!
.getDeviceVerificationStatus(FULL_ACCOUNT_DATASET.userId, FULL_ACCOUNT_DATASET.deviceId);
// Check that the current device and identity trust is migrated correctly just after migration
expect(verificationStatus).toBeDefined();
expect(verificationStatus!.crossSigningVerified).toEqual(true);
expect(verificationStatus!.signedByOwner).toEqual(true);
// Do some basic checks on the imported data
const deviceKeys = await matrixClient.getCrypto()!.getOwnDeviceKeys();
expect(deviceKeys.curve25519).toEqual("LKv0bKbc0EC4h0jknbemv3QalEkeYvuNeUXVRgVVTTU");
expect(deviceKeys.ed25519).toEqual("qK70DEqIXq7T+UU3v/al47Ab4JkMEBLpNrTBMbS5rrw");
expect(await matrixClient.getCrypto()!.getActiveSessionBackupVersion()).toEqual("7");
expect(await matrixClient.getCrypto()!.isEncryptionEnabledInRoom("!CWLUCoEWXSFyTCOtfL:matrix.org")).toBe(
true,
);
// check the progress callback
expect(progressListener.mock.calls.length).toBeGreaterThan(50);
// The first call should have progress == 0
const [firstProgress, totalSteps] = progressListener.mock.calls[0];
expect(totalSteps).toBeGreaterThan(3000);
expect(firstProgress).toEqual(0);
for (let i = 1; i < progressListener.mock.calls.length - 1; i++) {
const [progress, total] = progressListener.mock.calls[i];
expect(total).toEqual(totalSteps);
expect(progress).toBeGreaterThan(progressListener.mock.calls[i - 1][0]);
expect(progress).toBeLessThanOrEqual(totalSteps);
}
// The final call should have progress == total == -1
expect(progressListener).toHaveBeenLastCalledWith(-1, -1);
}, 60000);
describe("Private key backup migration", () => {
it("should not migrate the backup private key if backup has changed", async () => {
// Here we have a new backup server side, and the migrated account has the previous backup key.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.newBackupResponse);
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
expect(privateBackupKey).toBeNull();
});
it("should not migrate the backup private key if backup has unknown algorithm", async () => {
// Here we have a new backup server side, and the migrated account has the previous backup key.
const backupResponse = {
...MSK_NOT_CACHED_DATASET.backupResponse,
algorithm: "m.megolm_backup.v8",
};
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupResponse);
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
expect(privateBackupKey).toBeNull();
});
it("should not migrate the backup private key if the backup has been deleted", async () => {
// The old backup has been deleted server side.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No backup found",
},
},
},
master_keys: {
"@vdhtest200713:matrix.org": {
user_id: "@vdhtest200713:matrix.org",
usage: ["master"],
keys: {
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
"gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI",
},
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:MWOGVUTXZN":
"stOu1aHbhsWB/Aj5M/HqBR83QzME+682C995Uc8JxSmmyrlWmgG8QrnoUDG2OFR1t6zNQ+QLEilU4WNEOV73DQ",
},
},
},
},
self_signing_keys: {
"@vdhtest200713:matrix.org": {
user_id: "@vdhtest200713:matrix.org",
usage: ["self_signing"],
keys: {
"ed25519:lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k":
"lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k",
},
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
"HKTC7NoBhAkfJtmemmkn/HvCCgBQViWZ0uH7aGPRaWMDFgD8T7Q+y1j3FKZv4mhSopR85Fq3FRyXsG8OVvGeBA",
},
},
},
},
user_signing_keys: {
"@vdhtest200713:matrix.org": {
user_id: "@vdhtest200713:matrix.org",
usage: ["user_signing"],
keys: {
"ed25519:YShqO/3u5vQ0uucojraWrtoLrek0CYrurN/vH/YPMg8":
"YShqO/3u5vQ0uucojraWrtoLrek0CYrurN/vH/YPMg8",
},
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
"u8VOi4IaeRJwDgy2ftK02NJQPdBijy8f/0+WnHGG72yfOvMthwWzEw8SrRSNG8glBNrfHinKwCyJJzAJwyepCQ",
},
},
},
},
});
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
expect(privateBackupKey).toBeNull();
});
it("should migrate the backup private key if the backup matches", async () => {
// The old backup has been deleted server side.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.backupResponse);
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
expect(privateBackupKey).toBeDefined();
});
});
const testStoreName = "test-store";
await populateStore(testStoreName);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
it("should not migrate if account data is missing", async () => {
// See https://github.com/element-hq/element-web/issues/27447
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@vdhtest200713:matrix.org",
deviceId: "KMFSTJSMLB",
cryptoStore,
pickleKey: "+1k2Ppd7HIisUY824v7JtV3/oEE4yX0TqtmNPyhaD7o",
// Given we have an almost-empty legacy account in the database
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No backup found" },
});
fetchMock.post("path:/_matrix/client/v3/keys/query", EMPTY_ACCOUNT_DATASET.keyQueryResponse);
const testStoreName = "test-store";
await populateStore(testStoreName, EMPTY_ACCOUNT_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: EMPTY_ACCOUNT_DATASET.userId,
deviceId: EMPTY_ACCOUNT_DATASET.deviceId,
cryptoStore,
pickleKey: EMPTY_ACCOUNT_DATASET.pickleKey,
});
// When we start Rust crypto, potentially triggering an upgrade
const progressListener = jest.fn();
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
await matrixClient.initRustCrypto();
// Then no error occurs, and no upgrade happens
expect(progressListener.mock.calls.length).toBe(0);
}, 60000);
describe("Legacy trust migration", () => {
async function populateAndStartLegacyCryptoStore(dumpPath: string): Promise<IndexedDBCryptoStore> {
const testStoreName = "test-store";
await populateStore(testStoreName, dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
await cryptoStore.startup();
return cryptoStore;
}
it("should not revert to untrusted if legacy was trusted but msk not in cache, big account", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No backup found",
},
});
fetchMock.post("path:/_matrix/client/v3/keys/query", FULL_ACCOUNT_DATASET.keyQueryResponse);
const cryptoStore = await populateAndStartLegacyCryptoStore(FULL_ACCOUNT_DATASET.dumpPath);
// Remove the master key from the cache
await cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
const objectStore = txn.objectStore("account");
objectStore.delete(`ssss_cache:master`);
});
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: FULL_ACCOUNT_DATASET.userId,
deviceId: FULL_ACCOUNT_DATASET.deviceId,
cryptoStore,
pickleKey: FULL_ACCOUNT_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const verificationStatus = await matrixClient
.getCrypto()!
.getUserVerificationStatus(FULL_ACCOUNT_DATASET.userId);
expect(verificationStatus.isCrossSigningVerified()).toBe(true);
}, 60000);
it("should not revert to untrusted if legacy was trusted but msk not in cache", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.backupResponse);
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
const cryptoStore = await populateAndStartLegacyCryptoStore(MSK_NOT_CACHED_DATASET.dumpPath);
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const verificationStatus = await matrixClient
.getCrypto()!
.getUserVerificationStatus("@migration:localhost");
expect(verificationStatus.isCrossSigningVerified()).toBe(true);
});
it("should not migrate local trust if key has changed", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.backupResponse);
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.rotatedKeyQueryResponse);
const cryptoStore = await populateAndStartLegacyCryptoStore(MSK_NOT_CACHED_DATASET.dumpPath);
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const verificationStatus = await matrixClient
.getCrypto()!
.getUserVerificationStatus("@migration:localhost");
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
});
it("should not migrate local trust if was not trusted in legacy", async () => {
// Just 404 here for the test
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No backup found",
},
});
fetchMock.post("path:/_matrix/client/v3/keys/query", IDENTITY_NOT_TRUSTED_DATASET.keyQueryResponse);
const cryptoStore = await populateAndStartLegacyCryptoStore(IDENTITY_NOT_TRUSTED_DATASET.dumpPath);
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: IDENTITY_NOT_TRUSTED_DATASET.userId,
deviceId: IDENTITY_NOT_TRUSTED_DATASET.deviceId,
cryptoStore,
pickleKey: IDENTITY_NOT_TRUSTED_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const verificationStatus = await matrixClient
.getCrypto()!
.getUserVerificationStatus("@untrusted:localhost");
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
});
});
const progressListener = jest.fn();
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
await matrixClient.initRustCrypto();
const verificationStatus = await matrixClient
.getCrypto()!
.getDeviceVerificationStatus("@vdhtest200713:matrix.org", "KMFSTJSMLB");
// Check that the current device and identity trust is migrated correctly just after migration
expect(verificationStatus).toBeDefined();
expect(verificationStatus!.crossSigningVerified).toEqual(true);
expect(verificationStatus!.signedByOwner).toEqual(true);
// Do some basic checks on the imported data
const deviceKeys = await matrixClient.getCrypto()!.getOwnDeviceKeys();
expect(deviceKeys.curve25519).toEqual("LKv0bKbc0EC4h0jknbemv3QalEkeYvuNeUXVRgVVTTU");
expect(deviceKeys.ed25519).toEqual("qK70DEqIXq7T+UU3v/al47Ab4JkMEBLpNrTBMbS5rrw");
expect(await matrixClient.getCrypto()!.getActiveSessionBackupVersion()).toEqual("7");
expect(await matrixClient.getCrypto()!.isEncryptionEnabledInRoom("!CWLUCoEWXSFyTCOtfL:matrix.org")).toBe(true);
// check the progress callback
expect(progressListener.mock.calls.length).toBeGreaterThan(50);
// The first call should have progress == 0
const [firstProgress, totalSteps] = progressListener.mock.calls[0];
expect(totalSteps).toBeGreaterThan(3000);
expect(firstProgress).toEqual(0);
for (let i = 1; i < progressListener.mock.calls.length - 1; i++) {
const [progress, total] = progressListener.mock.calls[i];
expect(total).toEqual(totalSteps);
expect(progress).toBeGreaterThan(progressListener.mock.calls[i - 1][0]);
expect(progress).toBeLessThanOrEqual(totalSteps);
}
// The final call should have progress == total == -1
expect(progressListener).toHaveBeenLastCalledWith(-1, -1);
}, 60000);
});
});
describe("MatrixClient.clearStores", () => {
+4 -3
View File
@@ -19,6 +19,7 @@ limitations under the License.
import { TestClient } from "../TestClient";
import * as testUtils from "../test-utils/test-utils";
import { logger } from "../../src/logger";
import { KnownMembership } from "../../src/@types/membership";
const ROOM_ID = "!room:id";
@@ -43,7 +44,7 @@ function getSyncResponse(roomMembers: string[]) {
stateEvents,
roomMembers.map((m) =>
testUtils.mkMembership({
mship: "join",
mship: KnownMembership.Join,
sender: m,
}),
),
@@ -323,7 +324,7 @@ describe("DeviceList management:", function () {
timeline: {
events: [
testUtils.mkMembership({
mship: "leave",
mship: KnownMembership.Leave,
sender: "@bob:xyz",
}),
],
@@ -357,7 +358,7 @@ describe("DeviceList management:", function () {
timeline: {
events: [
testUtils.mkMembership({
mship: "leave",
mship: KnownMembership.Leave,
sender: "@bob:xyz",
}),
],
@@ -28,6 +28,7 @@ import {
} from "../../src";
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { KnownMembership } from "../../src/@types/membership";
describe("MatrixClient events", function () {
const selfUserId = "@alice:localhost";
@@ -85,7 +86,7 @@ describe("MatrixClient events", function () {
events: [
utils.mkMembership({
room: "!erufh:bar",
mship: "join",
mship: KnownMembership.Join,
user: "@foo:bar",
}),
utils.mkEvent({
@@ -272,7 +273,7 @@ describe("MatrixClient events", function () {
membersInvokeCount++;
expect(member.roomId).toEqual("!erufh:bar");
expect(member.userId).toEqual("@foo:bar");
expect(member.membership).toEqual("join");
expect(member.membership).toEqual(KnownMembership.Join);
});
client!.on(RoomStateEvent.NewMember, function (event, state, member) {
newMemberInvokeCount++;
@@ -310,7 +311,7 @@ describe("MatrixClient events", function () {
});
client!.on(RoomMemberEvent.Membership, function (event, member) {
membershipInvokeCount++;
expect(member.membership).toEqual("join");
expect(member.membership).toEqual(KnownMembership.Join);
});
client!.startClient();
@@ -36,6 +36,7 @@ import { TestClient } from "../TestClient";
import { FeatureSupport, Thread, ThreadEvent } from "../../src/models/thread";
import { emitPromise } from "../test-utils/test-utils";
import { Feature, ServerSupport } from "../../src/feature";
import { KnownMembership } from "../../src/@types/membership";
const userId = "@alice:localhost";
const userName = "Alice";
@@ -63,7 +64,7 @@ const buildRelationPaginationQuery = (params: QueryDict): string => {
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId,
mship: "join",
mship: KnownMembership.Join,
user: userId,
name: userName,
event: false,
@@ -98,7 +99,7 @@ const INITIAL_SYNC_DATA = {
events: [
withoutRoomId(ROOM_NAME_EVENT),
utils.mkMembership({
mship: "join",
mship: KnownMembership.Join,
user: otherUserId,
name: "Bob",
event: false,
@@ -607,11 +608,6 @@ describe("MatrixClient event timelines", function () {
await client.stopClient(); // we don't need the client to be syncing at this time
const room = client.getRoom(roomId)!;
httpBackend
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
return THREAD_ROOT;
});
httpBackend
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
@@ -1545,9 +1541,7 @@ describe("MatrixClient event timelines", function () {
expect(timelineSets).not.toBeNull();
respondToThreads(threadsResponse);
respondToThreads(threadsResponse);
respondToEvent(THREAD_ROOT);
respondToEvent(THREAD2_ROOT);
respondToThread(THREAD_ROOT, [THREAD_REPLY]);
respondToThread(THREAD2_ROOT, [THREAD2_REPLY]);
await flushHttp(room.fetchRoomThreads());
const threadIds = room.getThreads().map((thread) => thread.id);
@@ -1566,7 +1560,6 @@ describe("MatrixClient event timelines", function () {
thread.initialEventsFetched = true;
const prom = emitPromise(room, ThreadEvent.NewReply);
respondToEvent(THREAD_ROOT_UPDATED);
respondToEvent(THREAD2_ROOT);
await room.addLiveEvents([THREAD_REPLY2]);
await httpBackend.flushAllExpected();
await prom;
@@ -1643,7 +1636,7 @@ describe("MatrixClient event timelines", function () {
...THREAD_ROOT.unsigned!["m.relations"],
"io.element.thread": {
...THREAD_ROOT.unsigned!["m.relations"]!["io.element.thread"],
count: 2,
count: 1,
latest_event: THREAD_REPLY,
},
},
@@ -1692,7 +1685,6 @@ describe("MatrixClient event timelines", function () {
thread.initialEventsFetched = true;
const prom = emitPromise(room, ThreadEvent.Update);
respondToEvent(THREAD_ROOT_UPDATED);
respondToEvent(THREAD2_ROOT);
await room.addLiveEvents([THREAD_REPLY_REACTION]);
await httpBackend.flushAllExpected();
await prom;
@@ -1930,7 +1922,7 @@ describe("MatrixClient event timelines", function () {
// a state event, followed by a redaction thereof
const event = utils.mkMembership({
mship: "join",
mship: KnownMembership.Join,
user: otherUserId,
});
const redaction = utils.mkEvent({
@@ -2007,11 +1999,6 @@ describe("MatrixClient event timelines", function () {
},
},
});
httpBackend
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
return THREAD_ROOT;
});
httpBackend
.when("GET", "/rooms/!foo%3Abar/event/" + encodeURIComponent(THREAD_ROOT.event_id!))
.respond(200, function () {
+112 -30
View File
@@ -19,7 +19,16 @@ import { Mocked } from "jest-mock";
import * as utils from "../test-utils/test-utils";
import { CRYPTO_ENABLED, IStoredClientOpts, MatrixClient } from "../../src/client";
import { MatrixEvent } from "../../src/models/event";
import { Filter, KnockRoomOpts, MemoryStore, Method, Room, SERVICE_TYPES } from "../../src/matrix";
import {
Filter,
JoinRule,
KnockRoomOpts,
MemoryStore,
Method,
Room,
RoomSummary,
SERVICE_TYPES,
} from "../../src/matrix";
import { TestClient } from "../TestClient";
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
import { IFilterDefinition } from "../../src/filter";
@@ -27,6 +36,7 @@ import { ISearchResults } from "../../src/@types/search";
import { IStore } from "../../src/store";
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
import { SetPresence } from "../../src/sync";
import { KnownMembership } from "../../src/@types/membership";
describe("MatrixClient", function () {
const userId = "@alice:localhost";
@@ -162,7 +172,7 @@ describe("MatrixClient", function () {
utils.mkMembership({
user: userId,
room: roomId,
mship: "join",
mship: KnownMembership.Join,
event: true,
}),
]);
@@ -182,7 +192,7 @@ describe("MatrixClient", function () {
utils.mkMembership({
user: userId,
room: roomId,
mship: "join",
mship: KnownMembership.Join,
event: true,
}),
]);
@@ -269,7 +279,7 @@ describe("MatrixClient", function () {
utils.mkMembership({
user: userId,
room: roomId,
mship: "knock",
mship: KnownMembership.Knock,
event: true,
}),
]);
@@ -1709,6 +1719,102 @@ describe("MatrixClient", function () {
await Promise.all([client.unbindThreePid("email", "alice@server.com"), httpBackend.flushAllExpected()]);
});
});
describe("getRoomSummary", () => {
const roomId = "!foo:bar";
const encodedRoomId = encodeURIComponent(roomId);
const roomSummary: RoomSummary = {
"room_id": roomId,
"name": "My Room",
"avatar_url": "",
"topic": "My room topic",
"world_readable": false,
"guest_can_join": false,
"num_joined_members": 1,
"room_type": "",
"join_rule": JoinRule.Public,
"membership": "leave",
"im.nheko.summary.room_version": "6",
"im.nheko.summary.encryption": "algo",
};
const prefix = "/_matrix/client/unstable/im.nheko.summary/";
const suffix = `summary/${encodedRoomId}`;
const deprecatedSuffix = `rooms/${encodedRoomId}/summary`;
const errorUnrecogStatus = 404;
const errorUnrecogBody = {
errcode: "M_UNRECOGNIZED",
error: "Unsupported endpoint",
};
const errorBadreqStatus = 400;
const errorBadreqBody = {
errcode: "M_UNKNOWN",
error: "Invalid request",
};
it("should respond with a valid room summary object", () => {
httpBackend.when("GET", prefix + suffix).respond(200, roomSummary);
const prom = client.getRoomSummary(roomId).then((response) => {
expect(response).toEqual(roomSummary);
});
httpBackend.flush("");
return prom;
});
it("should allow fallback to the deprecated endpoint", () => {
httpBackend.when("GET", prefix + suffix).respond(errorUnrecogStatus, errorUnrecogBody);
httpBackend.when("GET", prefix + deprecatedSuffix).respond(200, roomSummary);
const prom = client.getRoomSummary(roomId).then((response) => {
expect(response).toEqual(roomSummary);
});
httpBackend.flush("");
return prom;
});
it("should respond to unsupported path with error", () => {
httpBackend.when("GET", prefix + suffix).respond(errorUnrecogStatus, errorUnrecogBody);
httpBackend.when("GET", prefix + deprecatedSuffix).respond(errorUnrecogStatus, errorUnrecogBody);
const prom = client.getRoomSummary(roomId).then(
function (response) {
throw Error("request not failed");
},
function (error) {
expect(error.httpStatus).toEqual(errorUnrecogStatus);
expect(error.errcode).toEqual(errorUnrecogBody.errcode);
expect(error.message).toEqual(`MatrixError: [${errorUnrecogStatus}] ${errorUnrecogBody.error}`);
},
);
httpBackend.flush("");
return prom;
});
it("should respond to invalid path arguments with error", () => {
httpBackend.when("GET", prefix).respond(errorBadreqStatus, errorBadreqBody);
const prom = client.getRoomSummary("notAroom").then(
function (response) {
throw Error("request not failed");
},
function (error) {
expect(error.httpStatus).toEqual(errorBadreqStatus);
expect(error.errcode).toEqual(errorBadreqBody.errcode);
expect(error.message).toEqual(`MatrixError: [${errorBadreqStatus}] ${errorBadreqBody.error}`);
},
);
httpBackend.flush("");
return prom;
});
});
});
function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
@@ -1719,7 +1825,6 @@ function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
const buildEventMessageInThread = (root: MatrixEvent) =>
new MatrixEvent({
age: 80098509,
content: {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
@@ -1740,12 +1845,10 @@ const buildEventMessageInThread = (root: MatrixEvent) =>
sender: "@andybalaam-test1:matrix.org",
type: "m.room.encrypted",
unsigned: { age: 80098509 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventPollResponseReference = () =>
new MatrixEvent({
age: 80098509,
content: {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
@@ -1763,7 +1866,6 @@ const buildEventPollResponseReference = () =>
sender: "@andybalaam-test1:matrix.org",
type: "m.room.encrypted",
unsigned: { age: 80106237 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventReaction = (event: MatrixEvent) =>
@@ -1803,7 +1905,6 @@ const buildEventRedaction = (event: MatrixEvent) =>
const buildEventPollStartThreadRoot = () =>
new MatrixEvent({
age: 80108647,
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: "ENCRYPTEDSTUFF",
@@ -1817,12 +1918,10 @@ const buildEventPollStartThreadRoot = () =>
sender: "@andybalaam-test1:matrix.org",
type: "m.room.encrypted",
unsigned: { age: 80108647 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventReply = (target: MatrixEvent) =>
new MatrixEvent({
age: 80098509,
content: {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
@@ -1841,12 +1940,10 @@ const buildEventReply = (target: MatrixEvent) =>
sender: "@andybalaam-test1:matrix.org",
type: "m.room.encrypted",
unsigned: { age: 80098509 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventRoomName = () =>
new MatrixEvent({
age: 80123249,
content: {
name: "1 poll, 1 vote, 1 thread",
},
@@ -1857,12 +1954,10 @@ const buildEventRoomName = () =>
state_key: "",
type: "m.room.name",
unsigned: { age: 80123249 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventEncryption = () =>
new MatrixEvent({
age: 80123383,
content: {
algorithm: "m.megolm.v1.aes-sha2",
},
@@ -1873,12 +1968,10 @@ const buildEventEncryption = () =>
state_key: "",
type: "m.room.encryption",
unsigned: { age: 80123383 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventGuestAccess = () =>
new MatrixEvent({
age: 80123473,
content: {
guest_access: "can_join",
},
@@ -1889,12 +1982,10 @@ const buildEventGuestAccess = () =>
state_key: "",
type: "m.room.guest_access",
unsigned: { age: 80123473 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventHistoryVisibility = () =>
new MatrixEvent({
age: 80123556,
content: {
history_visibility: "shared",
},
@@ -1905,14 +1996,12 @@ const buildEventHistoryVisibility = () =>
state_key: "",
type: "m.room.history_visibility",
unsigned: { age: 80123556 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventJoinRules = () =>
new MatrixEvent({
age: 80123696,
content: {
join_rule: "invite",
join_rule: KnownMembership.Invite,
},
event_id: "$6JDDeDp7fEc0F6YnTWMruNcKWFltR3e9wk7wWDDJrAU",
origin_server_ts: 1643815441191,
@@ -1921,12 +2010,10 @@ const buildEventJoinRules = () =>
state_key: "",
type: "m.room.join_rules",
unsigned: { age: 80123696 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventPowerLevels = () =>
new MatrixEvent({
age: 80124105,
content: {
ban: 50,
events: {
@@ -1957,16 +2044,14 @@ const buildEventPowerLevels = () =>
state_key: "",
type: "m.room.power_levels",
unsigned: { age: 80124105 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventMember = () =>
new MatrixEvent({
age: 80125279,
content: {
avatar_url: "mxc://matrix.org/aNtbVcFfwotudypZcHsIcPOc",
displayname: "andybalaam-test1",
membership: "join",
membership: KnownMembership.Join,
},
event_id: "$Ex5eVmMs_ti784mo8bgddynbwLvy6231lCycJr7Cl9M",
origin_server_ts: 1643815439608,
@@ -1975,12 +2060,10 @@ const buildEventMember = () =>
state_key: "@andybalaam-test1:matrix.org",
type: "m.room.member",
unsigned: { age: 80125279 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventCreate = () =>
new MatrixEvent({
age: 80126105,
content: {
room_version: "6",
},
@@ -1991,7 +2074,6 @@ const buildEventCreate = () =>
state_key: "",
type: "m.room.create",
unsigned: { age: 80126105 },
user_id: "@andybalaam-test1:matrix.org",
});
function assertObjectContains(obj: Record<string, any>, expected: any): void {
+3 -2
View File
@@ -6,6 +6,7 @@ import { MatrixScheduler } from "../../src/scheduler";
import { MemoryStore } from "../../src/store/memory";
import { MatrixError } from "../../src/http-api";
import { IStore } from "../../src/store";
import { KnownMembership } from "../../src/@types/membership";
describe("MatrixClient opts", function () {
const baseUrl = "http://localhost.or.something";
@@ -43,13 +44,13 @@ describe("MatrixClient opts", function () {
}),
utils.mkMembership({
room: roomId,
mship: "join",
mship: KnownMembership.Join,
user: userB,
name: "Bob",
}),
utils.mkMembership({
room: roomId,
mship: "join",
mship: KnownMembership.Join,
user: userId,
name: "Alice",
}),
+3 -3
View File
@@ -16,7 +16,7 @@ limitations under the License.
import HttpBackend from "matrix-mock-request";
import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src/matrix";
import { EventStatus, MatrixClient, MatrixScheduler, MsgType, RoomEvent } from "../../src/matrix";
import { Room } from "../../src/models/room";
import { TestClient } from "../TestClient";
@@ -60,7 +60,7 @@ describe("MatrixClient retrying", function () {
// send a couple of events; the second will be queued
const p1 = client!
.sendMessage(roomId, {
msgtype: "m.text",
msgtype: MsgType.Text,
body: "m1",
})
.then(
@@ -77,7 +77,7 @@ describe("MatrixClient retrying", function () {
// never gets resolved.
// https://github.com/matrix-org/matrix-js-sdk/issues/496
client!.sendMessage(roomId, {
msgtype: "m.text",
msgtype: MsgType.Text,
body: "m2",
});
+14 -13
View File
@@ -30,6 +30,7 @@ import {
Room,
} from "../../src";
import { TestClient } from "../TestClient";
import { KnownMembership } from "../../src/@types/membership";
describe("MatrixClient room timelines", function () {
const userId = "@alice:localhost";
@@ -42,7 +43,7 @@ describe("MatrixClient room timelines", function () {
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId,
mship: "join",
mship: KnownMembership.Join,
user: userId,
name: userName,
});
@@ -76,7 +77,7 @@ describe("MatrixClient room timelines", function () {
ROOM_NAME_EVENT,
utils.mkMembership({
room: roomId,
mship: "join",
mship: KnownMembership.Join,
user: otherUserId,
name: "Bob",
}),
@@ -316,7 +317,7 @@ describe("MatrixClient room timelines", function () {
// make an m.room.member event for alice's join
const joinMshipEvent = utils.mkMembership({
mship: "join",
mship: KnownMembership.Join,
user: userId,
room: roomId,
name: "Old Alice",
@@ -326,16 +327,16 @@ describe("MatrixClient room timelines", function () {
// make an m.room.member event with prev_content for alice's nick
// change
const oldMshipEvent = utils.mkMembership({
mship: "join",
mship: KnownMembership.Join,
user: userId,
room: roomId,
name: userName,
url: "mxc://some/url",
});
oldMshipEvent.prev_content = {
oldMshipEvent.unsigned!.prev_content = {
displayname: "Old Alice",
avatar_url: undefined,
membership: "join",
membership: KnownMembership.Join,
};
// set the list of events to return on scrollback (/messages)
@@ -487,7 +488,7 @@ describe("MatrixClient room timelines", function () {
utils.mkMembership({
user: userId,
room: roomId,
mship: "join",
mship: KnownMembership.Join,
name: "New Name",
}),
utils.mkMessage({ user: userId, room: roomId }),
@@ -554,13 +555,13 @@ describe("MatrixClient room timelines", function () {
utils.mkMembership({
user: userC,
room: roomId,
mship: "join",
mship: KnownMembership.Join,
name: "C",
}),
utils.mkMembership({
user: userC,
room: roomId,
mship: "invite",
mship: KnownMembership.Invite,
skey: userD,
}),
];
@@ -571,9 +572,9 @@ describe("MatrixClient room timelines", function () {
return Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]).then(function () {
expect(room.currentState.getMembers().length).toEqual(4);
expect(room.currentState.getMember(userC)!.name).toEqual("C");
expect(room.currentState.getMember(userC)!.membership).toEqual("join");
expect(room.currentState.getMember(userC)!.membership).toEqual(KnownMembership.Join);
expect(room.currentState.getMember(userD)!.name).toEqual(userD);
expect(room.currentState.getMember(userD)!.membership).toEqual("invite");
expect(room.currentState.getMember(userD)!.membership).toEqual(KnownMembership.Invite);
});
});
});
@@ -598,9 +599,9 @@ describe("MatrixClient room timelines", function () {
expect(room.timeline[0].event).toEqual(eventData[0]);
expect(room.currentState.getMembers().length).toEqual(2);
expect(room.currentState.getMember(userId)!.name).toEqual(userName);
expect(room.currentState.getMember(userId)!.membership).toEqual("join");
expect(room.currentState.getMember(userId)!.membership).toEqual(KnownMembership.Join);
expect(room.currentState.getMember(otherUserId)!.name).toEqual("Bob");
expect(room.currentState.getMember(otherUserId)!.membership).toEqual("join");
expect(room.currentState.getMember(otherUserId)!.membership).toEqual(KnownMembership.Join);
});
});
});
+475 -92
View File
@@ -39,6 +39,7 @@ import {
IndexedDBStore,
RelationType,
EventType,
MatrixEventEvent,
} from "../../src";
import { ReceiptType } from "../../src/@types/read_receipts";
import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
@@ -46,6 +47,8 @@ import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
import { emitPromise, mkEvent, mkMessage } from "../test-utils/test-utils";
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
import { IActionsObject } from "../../src/pushprocessor";
import { KnownMembership } from "../../src/@types/membership";
describe("MatrixClient syncing", () => {
const selfUserId = "@alice:localhost";
@@ -123,7 +126,7 @@ describe("MatrixClient syncing", () => {
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "invite",
membership: KnownMembership.Invite,
},
},
],
@@ -151,10 +154,10 @@ describe("MatrixClient syncing", () => {
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "leave",
membership: KnownMembership.Leave,
},
prev_content: {
membership: "invite",
membership: KnownMembership.Invite,
},
// XXX: And other fields required on an event
},
@@ -167,10 +170,10 @@ describe("MatrixClient syncing", () => {
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "leave",
membership: KnownMembership.Leave,
},
prev_content: {
membership: "invite",
membership: KnownMembership.Invite,
},
// XXX: And other fields required on an event
},
@@ -193,22 +196,22 @@ describe("MatrixClient syncing", () => {
// Room, string, string
fires++;
expect(room.roomId).toBe(roomId);
expect(membership).toBe("invite");
expect(membership).toBe(KnownMembership.Invite);
expect(oldMembership).toBeFalsy();
// Second fire: a leave
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
fires++;
expect(room.roomId).toBe(roomId);
expect(membership).toBe("leave");
expect(oldMembership).toBe("invite");
expect(membership).toBe(KnownMembership.Leave);
expect(oldMembership).toBe(KnownMembership.Invite);
// Third/final fire: a second invite
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
fires++;
expect(room.roomId).toBe(roomId);
expect(membership).toBe("invite");
expect(oldMembership).toBe("leave");
expect(membership).toBe(KnownMembership.Invite);
expect(oldMembership).toBe(KnownMembership.Leave);
});
});
@@ -238,7 +241,7 @@ describe("MatrixClient syncing", () => {
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "knock",
membership: KnownMembership.Knock,
},
},
],
@@ -266,10 +269,10 @@ describe("MatrixClient syncing", () => {
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "leave",
membership: KnownMembership.Leave,
},
prev_content: {
membership: "knock",
membership: KnownMembership.Knock,
},
// XXX: And other fields required on an event
},
@@ -282,10 +285,10 @@ describe("MatrixClient syncing", () => {
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "leave",
membership: KnownMembership.Leave,
},
prev_content: {
membership: "knock",
membership: KnownMembership.Knock,
},
// XXX: And other fields required on an event
},
@@ -308,22 +311,22 @@ describe("MatrixClient syncing", () => {
// Room, string, string
fires++;
expect(room.roomId).toBe(roomId);
expect(membership).toBe("knock");
expect(membership).toBe(KnownMembership.Knock);
expect(oldMembership).toBeFalsy();
// Second fire: a leave
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
fires++;
expect(room.roomId).toBe(roomId);
expect(membership).toBe("leave");
expect(oldMembership).toBe("knock");
expect(membership).toBe(KnownMembership.Leave);
expect(oldMembership).toBe(KnownMembership.Knock);
// Third/final fire: a second knock
client!.once(RoomEvent.MyMembership, (room, membership, oldMembership) => {
fires++;
expect(room.roomId).toBe(roomId);
expect(membership).toBe("knock");
expect(oldMembership).toBe("leave");
expect(membership).toBe(KnownMembership.Knock);
expect(oldMembership).toBe(KnownMembership.Leave);
});
});
@@ -381,7 +384,7 @@ describe("MatrixClient syncing", () => {
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "invite",
membership: KnownMembership.Invite,
},
},
],
@@ -421,7 +424,7 @@ describe("MatrixClient syncing", () => {
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "knock",
membership: KnownMembership.Knock,
},
},
],
@@ -533,12 +536,12 @@ describe("MatrixClient syncing", () => {
events: [
utils.mkMembership({
room: roomOne,
mship: "join",
mship: KnownMembership.Join,
user: otherUserId,
}),
utils.mkMembership({
room: roomOne,
mship: "join",
mship: KnownMembership.Join,
user: selfUserId,
}),
utils.mkEvent({
@@ -556,7 +559,7 @@ describe("MatrixClient syncing", () => {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne,
mship: "invite",
mship: KnownMembership.Invite,
user: userC,
}) as IStateEvent,
);
@@ -589,7 +592,7 @@ describe("MatrixClient syncing", () => {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne,
mship: "invite",
mship: KnownMembership.Invite,
user: userC,
}) as IStateEvent,
);
@@ -617,7 +620,7 @@ describe("MatrixClient syncing", () => {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne,
mship: "invite",
mship: KnownMembership.Invite,
user: userC,
}) as IStateEvent,
);
@@ -644,7 +647,7 @@ describe("MatrixClient syncing", () => {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne,
mship: "invite",
mship: KnownMembership.Invite,
user: userC,
}) as IStateEvent,
);
@@ -719,12 +722,12 @@ describe("MatrixClient syncing", () => {
}),
utils.mkMembership({
room: roomOne,
mship: "join",
mship: KnownMembership.Join,
user: otherUserId,
}),
utils.mkMembership({
room: roomOne,
mship: "join",
mship: KnownMembership.Join,
user: selfUserId,
}),
utils.mkEvent({
@@ -750,13 +753,13 @@ describe("MatrixClient syncing", () => {
events: [
utils.mkMembership({
room: roomTwo,
mship: "join",
mship: KnownMembership.Join,
user: otherUserId,
name: otherDisplayName,
}),
utils.mkMembership({
room: roomTwo,
mship: "join",
mship: KnownMembership.Join,
user: selfUserId,
}),
utils.mkEvent({
@@ -1247,7 +1250,7 @@ describe("MatrixClient syncing", () => {
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomOne,
mship: "join",
mship: KnownMembership.Join,
user: userA,
});
@@ -1508,12 +1511,12 @@ describe("MatrixClient syncing", () => {
}),
utils.mkMembership({
room: roomOne,
mship: "join",
mship: KnownMembership.Join,
user: otherUserId,
}),
utils.mkMembership({
room: roomOne,
mship: "join",
mship: KnownMembership.Join,
user: selfUserId,
}),
utils.mkEvent({
@@ -1605,12 +1608,12 @@ describe("MatrixClient syncing", () => {
}),
utils.mkMembership({
room: roomOne,
mship: "join",
mship: KnownMembership.Join,
user: otherUserId,
}),
utils.mkMembership({
room: roomOne,
mship: "join",
mship: KnownMembership.Join,
user: selfUserId,
}),
utils.mkEvent({
@@ -1645,6 +1648,99 @@ describe("MatrixClient syncing", () => {
});
});
it("should zero total notifications for threads when absent from the notifications object", async () => {
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
[THREAD_ID]: {
highlight_count: 2,
notification_count: 5,
},
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomOne);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {};
httpBackend!.when("GET", "/sync").respond(200, syncData);
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
});
it("should zero highlight notifications for threads in encrypted rooms", async () => {
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
[THREAD_ID]: {
highlight_count: 2,
notification_count: 5,
},
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomOne);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
[THREAD_ID]: {
highlight_count: 0,
notification_count: 0,
},
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
});
it("should not zero highlight notifications for threads in encrypted rooms", async () => {
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
[THREAD_ID]: {
highlight_count: 2,
notification_count: 5,
},
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomOne);
room!.hasEncryptionStateEvent = jest.fn().mockReturnValue(true);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(5);
syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = {
[THREAD_ID]: {
highlight_count: 0,
notification_count: 0,
},
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
expect(room!.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(2);
});
it("caches unknown threads receipts and replay them when the thread is created", async () => {
const THREAD_ID = "$unknownthread:localhost";
@@ -1732,64 +1828,351 @@ describe("MatrixClient syncing", () => {
});
});
it("should apply encrypted notification logic for events within the same sync blob", async () => {
const roomId = "!room123:server";
const syncData = {
rooms: {
join: {
[roomId]: {
ephemeral: {
events: [],
},
timeline: {
events: [
utils.mkEvent({
room: roomId,
event: true,
skey: "",
type: EventType.RoomEncryption,
content: {},
}),
utils.mkMessage({
room: roomId,
user: otherUserId,
msg: "hello",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomId,
mship: "join",
user: otherUserId,
}),
utils.mkMembership({
room: roomId,
mship: "join",
user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create",
room: roomId,
user: selfUserId,
content: {},
}),
],
describe("encrypted notification logic", () => {
let roomId: string;
let syncData: ISyncResponse;
beforeEach(() => {
roomId = "!room123:server";
syncData = {
rooms: {
join: {
[roomId]: {
ephemeral: {
events: [],
},
timeline: {
events: [
utils.mkEvent({
room: roomId,
event: true,
skey: "",
type: EventType.RoomEncryption,
content: {},
}),
utils.mkMessage({
room: roomId,
user: otherUserId,
msg: "hello",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomId,
mship: KnownMembership.Join,
user: otherUserId,
}),
utils.mkMembership({
room: roomId,
mship: KnownMembership.Join,
user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create",
room: roomId,
user: selfUserId,
content: {},
}),
],
},
},
},
},
},
} as unknown as ISyncResponse;
} as unknown as ISyncResponse;
});
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
it("should apply encrypted notification logic for events within the same sync blob", async () => {
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomId)!;
expect(room).toBeInstanceOf(Room);
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
const room = client!.getRoom(roomId)!;
expect(room).toBeInstanceOf(Room);
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
});
it("should recalculate highlights on unthreaded receipt for encrypted rooms", async () => {
const myUserId = client!.getUserId()!;
const firstEventId = syncData.rooms.join[roomId].timeline.events[1].event_id;
// add a receipt for the first event in the room (let's say the user has already read that one)
syncData.rooms.join[roomId].ephemeral.events = [
{
content: {
[firstEventId]: {
"m.read": {
[myUserId]: { ts: 1 },
},
},
},
type: "m.receipt",
},
];
// Now add a highlighting event after that receipt
const pingEvent = utils.mkMessage({
room: roomId,
user: otherUserId,
msg: client?.getUserId() + " ping",
}) as IRoomEvent;
syncData.rooms.join[roomId].timeline.events.push(pingEvent);
// fudge this to make it a highlight
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
if (ev.getId() === pingEvent.event_id) {
return {
notify: true,
tweaks: {
highlight: true,
},
};
}
return null;
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomId)!;
expect(room).toBeInstanceOf(Room);
// the room should now have one highlight since our receipt was before the ping message
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
});
it("should recalculate highlights on main thread receipt for encrypted rooms", async () => {
const myUserId = client!.getUserId()!;
const firstEventId = syncData.rooms.join[roomId].timeline.events[1].event_id;
// add a receipt for the first event in the room (let's say the user has already read that one)
syncData.rooms.join[roomId].ephemeral.events = [
{
content: {
[firstEventId]: {
"m.read": {
[myUserId]: { ts: 1, thread_id: "main" },
},
},
},
type: "m.receipt",
},
];
// Now add a highlighting event after that receipt
const pingEvent = utils.mkMessage({
room: roomId,
user: otherUserId,
msg: client?.getUserId() + " ping",
}) as IRoomEvent;
syncData.rooms.join[roomId].timeline.events.push(pingEvent);
// fudge this to make it a highlight
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
if (ev.getId() === pingEvent.event_id) {
return {
notify: true,
tweaks: {
highlight: true,
},
};
}
return null;
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient();
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomId)!;
expect(room).toBeInstanceOf(Room);
// the room should now have one highlight since our receipt was before the ping message
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
});
describe("notification processing in threads", () => {
let threadEvent1: IRoomEvent;
let threadEvent2: IRoomEvent;
let firstEventId: string;
beforeEach(() => {
firstEventId = syncData.rooms.join[roomId].timeline.events[1].event_id;
// Add a threaded event off of the first event
threadEvent1 = utils.mkEvent({
type: EventType.RoomMessage,
user: otherUserId,
room: roomId,
ts: 500,
content: {
"body": "first thread response",
"m.relates_to": {
"event_id": firstEventId,
"m.in_reply_to": {
event_id: firstEventId,
},
"rel_type": "io.element.thread",
},
},
}) as IRoomEvent;
syncData.rooms.join[roomId].timeline.events.push(threadEvent1);
// ...and another
threadEvent2 = utils.mkEvent({
type: EventType.RoomMessage,
user: otherUserId,
room: roomId,
ts: 1500,
content: {
"body": "second thread response",
"m.relates_to": {
"event_id": firstEventId,
"m.in_reply_to": {
event_id: firstEventId,
},
"rel_type": "io.element.thread",
},
},
}) as IRoomEvent;
syncData.rooms.join[roomId].timeline.events.push(threadEvent2);
// fudge to make these highlights
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
if ([threadEvent1.event_id, threadEvent2.event_id].includes(ev.getId()!)) {
return {
notify: true,
tweaks: {
highlight: true,
},
};
}
return null;
};
});
it("checks threads with notifications on unthreaded receipts", async () => {
const myUserId = client!.getUserId()!;
// add a receipt for a random, ficticious thread, otherwise the client will
// think that the thread is before any threaded receipts and ignore it.
syncData.rooms.join[roomId].ephemeral.events = [
{
content: {
[firstEventId]: {
"m.read": {
[myUserId]: { ts: 1, thread_id: "some_other_thread" },
},
},
},
type: "m.receipt",
},
];
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient({ threadSupport: true });
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomId)!;
// pretend that the client has decrypted an event to trigger it to compute
// local notifications
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(firstEventId)!);
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(threadEvent1.event_id)!);
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(threadEvent2.event_id)!);
expect(room).toBeInstanceOf(Room);
// we should now have one highlight: the unread message that pings
expect(
room.getThreadUnreadNotificationCount(firstEventId, NotificationCountType.Highlight),
).toEqual(2);
const syncData2 = {
rooms: {
join: {
[roomId]: {
ephemeral: {
events: [
{
content: {
[firstEventId]: {
"m.read": {
[myUserId]: { ts: 1 },
},
},
},
type: "m.receipt",
},
],
},
},
},
},
} as unknown as ISyncResponse;
httpBackend!.when("GET", "/sync").respond(200, syncData2);
await Promise.all([httpBackend!.flush("/sync", 1), utils.syncPromise(client!)]);
expect(room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0);
});
it("should recalculate highlights on threaded receipt for encrypted rooms", async () => {
const myUserId = client!.getUserId()!;
// add a receipt for the first message in the threadm leaving the second one unread
syncData.rooms.join[roomId].ephemeral.events = [
{
content: {
[threadEvent1.event_id]: {
"m.read": {
[myUserId]: { ts: 1, thread_id: firstEventId },
},
},
},
type: "m.receipt",
},
];
// fudge to make both thread replies highlights
client!.getPushActionsForEvent = (ev: MatrixEvent): IActionsObject | null => {
if ([threadEvent1.event_id, threadEvent2.event_id].includes(ev.getId()!)) {
return {
notify: true,
tweaks: {
highlight: true,
},
};
}
return null;
};
httpBackend!.when("GET", "/sync").respond(200, syncData);
client!.startClient({ threadSupport: true });
await Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]);
const room = client!.getRoom(roomId)!;
expect(room).toBeInstanceOf(Room);
// pretend that the client has decrypted an event to trigger it to compute
// local notifications
client?.emit(MatrixEventEvent.Decrypted, room.findEventById(firstEventId)!);
// the room should now have one highlight: the second thread message
expect(room.getThreadUnreadNotificationCount(firstEventId, NotificationCountType.Highlight)).toBe(
1,
);
});
});
});
});
@@ -1901,7 +2284,7 @@ describe("MatrixClient syncing", () => {
it("should return a room based on the room initialSync API", async () => {
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomOne)}/initialSync`).respond(200, {
room_id: roomOne,
membership: "leave",
membership: KnownMembership.Leave,
messages: {
start: "start",
end: "end",
@@ -1950,7 +2333,7 @@ describe("MatrixClient syncing", () => {
const room = await prom;
expect(room.roomId).toBe(roomOne);
expect(room.getMyMembership()).toBe("leave");
expect(room.getMyMembership()).toBe(KnownMembership.Leave);
expect(room.name).toBe("Room Name");
expect(room.currentState.getStateEvents("m.room.name", "")?.getId()).toBe("$eventId");
expect(room.timeline[0].getContent().body).toBe("Message 1");
@@ -2042,7 +2425,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
type: "m.room.member",
state_key: selfUserId,
content: {
membership: "invite",
membership: KnownMembership.Invite,
},
},
],
@@ -34,6 +34,7 @@ import { TestClient } from "../TestClient";
import { ReceiptType } from "../../src/@types/read_receipts";
import { mkThread } from "../test-utils/thread";
import { SyncState } from "../../src/sync";
import { KnownMembership } from "../../src/@types/membership";
const userA = "@alice:localhost";
const userB = "@bob:localhost";
@@ -151,7 +152,7 @@ describe("MatrixClient syncing", () => {
await client!.sendEvent(roomId, EventType.Reaction, {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: threadReply.getId(),
event_id: threadReply.getId()!,
key: "",
},
});
@@ -229,7 +230,7 @@ describe("MatrixClient syncing", () => {
content: {
avatar_url: "",
displayname: userB,
membership: "join",
membership: KnownMembership.Join,
},
origin_server_ts: 2,
sender: userB,
@@ -270,7 +271,7 @@ describe("MatrixClient syncing", () => {
},
{
content: {
join_rule: "invite",
join_rule: KnownMembership.Invite,
},
origin_server_ts: 4,
sender: userB,
@@ -316,7 +317,7 @@ describe("MatrixClient syncing", () => {
avatar_url: "",
displayname: userA,
is_direct: true,
membership: "invite",
membership: KnownMembership.Invite,
},
origin_server_ts: 8,
sender: userB,
@@ -338,7 +339,7 @@ describe("MatrixClient syncing", () => {
content: {
avatar_url: "",
displayname: userA,
membership: "join",
membership: KnownMembership.Join,
},
origin_server_ts: 10,
sender: userA,
+20 -19
View File
@@ -43,6 +43,7 @@ import { IStoredClientOpts } from "../../src";
import { logger } from "../../src/logger";
import { emitPromise } from "../test-utils/test-utils";
import { defer } from "../../src/utils";
import { KnownMembership } from "../../src/@types/membership";
describe("SlidingSyncSdk", () => {
let client: MatrixClient | undefined;
@@ -189,7 +190,7 @@ describe("SlidingSyncSdk", () => {
name: "A",
required_state: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomName, { name: "A" }, ""),
],
@@ -204,7 +205,7 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello B" }),
mkOwnEvent(EventType.RoomMessage, { body: "world B" }),
@@ -216,7 +217,7 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello C" }),
mkOwnEvent(EventType.RoomMessage, { body: "world C" }),
@@ -229,7 +230,7 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello D" }),
mkOwnEvent(EventType.RoomMessage, { body: "world D" }),
@@ -244,7 +245,7 @@ describe("SlidingSyncSdk", () => {
invite_state: [
{
type: EventType.RoomMember,
content: { membership: "invite" },
content: { membership: KnownMembership.Invite },
state_key: selfUserId,
sender: "@bob:localhost",
event_id: "$room_e_invite",
@@ -265,7 +266,7 @@ describe("SlidingSyncSdk", () => {
name: "#foo:localhost",
required_state: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomCanonicalAlias, { alias: "#foo:localhost" }, ""),
mkOwnStateEvent(EventType.RoomName, { name: "This should be ignored" }, ""),
@@ -281,7 +282,7 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
],
joined_count: 5,
@@ -293,7 +294,7 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "live event" }),
],
@@ -308,7 +309,7 @@ describe("SlidingSyncSdk", () => {
const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.name).toEqual(data[roomA].name);
expect(gotRoom!.getMyMembership()).toEqual("join");
expect(gotRoom!.getMyMembership()).toEqual(KnownMembership.Join);
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
});
@@ -318,7 +319,7 @@ describe("SlidingSyncSdk", () => {
const gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.name).toEqual(data[roomB].name);
expect(gotRoom!.getMyMembership()).toEqual("join");
expect(gotRoom!.getMyMembership()).toEqual(KnownMembership.Join);
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
});
@@ -372,7 +373,7 @@ describe("SlidingSyncSdk", () => {
const gotRoom = client!.getRoom(roomH);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.name).toEqual(data[roomH].name);
expect(gotRoom!.getMyMembership()).toEqual("join");
expect(gotRoom!.getMyMembership()).toEqual(KnownMembership.Join);
// check the entire timeline is correct
assertTimelineEvents(gotRoom!.getLiveTimeline().getEvents(), data[roomH].timeline);
await expect(seenLiveEventDeferred.promise).resolves.toBeTruthy();
@@ -383,7 +384,7 @@ describe("SlidingSyncSdk", () => {
await emitPromise(client!, ClientEvent.Room);
const gotRoom = client!.getRoom(roomE);
expect(gotRoom).toBeTruthy();
expect(gotRoom!.getMyMembership()).toEqual("invite");
expect(gotRoom!.getMyMembership()).toEqual(KnownMembership.Invite);
expect(gotRoom!.currentState.getJoinRule()).toEqual(JoinRule.Invite);
});
@@ -603,9 +604,9 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Invite }, invitee),
],
});
await httpBackend!.flush("/profile", 1, 1000);
@@ -719,7 +720,7 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
@@ -923,7 +924,7 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
@@ -964,7 +965,7 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
@@ -1050,12 +1051,12 @@ describe("SlidingSyncSdk", () => {
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
{
type: EventType.RoomMember,
state_key: alice,
content: { membership: "join" },
content: { membership: KnownMembership.Join },
sender: alice,
origin_server_ts: Date.now(),
event_id: "$alice",
+5 -5
View File
@@ -107,8 +107,8 @@ describe("SlidingSync", () => {
onRequest: (initial) => {
return { initial: initial };
},
onResponse: (res) => {
return {};
onResponse: async (res) => {
return;
},
when: () => ExtensionState.PreProcess,
};
@@ -1572,7 +1572,7 @@ describe("SlidingSync", () => {
onPreExtensionRequest = () => {
return extReq;
};
onPreExtensionResponse = (resp) => {
onPreExtensionResponse = async (resp) => {
extensionOnResponseCalled = true;
callbackOrder.push("onPreExtensionResponse");
expect(resp).toEqual(extResp);
@@ -1613,7 +1613,7 @@ describe("SlidingSync", () => {
return undefined;
};
let responseCalled = false;
onPreExtensionResponse = (resp) => {
onPreExtensionResponse = async (resp) => {
responseCalled = true;
};
httpBackend!
@@ -1649,7 +1649,7 @@ describe("SlidingSync", () => {
};
let responseCalled = false;
const callbackOrder: string[] = [];
onPostExtensionResponse = (resp) => {
onPostExtensionResponse = async (resp) => {
expect(resp).toEqual(extResp);
responseCalled = true;
callbackOrder.push("onPostExtensionResponse");
+3 -4
View File
@@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { OidcClientConfig } from "../../src";
import { ValidatedIssuerMetadata } from "../../src/oidc/validate";
import { OidcClientConfig, ValidatedIssuerMetadata } from "../../src";
/**
* Makes a valid OidcClientConfig with minimum valid values
@@ -26,8 +25,7 @@ export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClien
const metadata = mockOpenIdConfiguration(issuer);
return {
issuer,
account: issuer + "account",
accountManagementEndpoint: issuer + "account",
registrationEndpoint: metadata.registration_endpoint,
authorizationEndpoint: metadata.authorization_endpoint,
tokenEndpoint: metadata.token_endpoint,
@@ -46,6 +44,7 @@ export const mockOpenIdConfiguration = (issuer = "https://auth.org/"): Validated
token_endpoint: issuer + "token",
authorization_endpoint: issuer + "auth",
registration_endpoint: issuer + "registration",
device_authorization_endpoint: issuer + "device",
jwks_uri: issuer + "jwks",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token"],
+8 -5
View File
@@ -19,6 +19,7 @@ import {
import { SyncState } from "../../src/sync";
import { eventMapperFor } from "../../src/event-mapper";
import { TEST_ROOM_ID } from "./test-data";
import { KnownMembership, Membership } from "../../src/@types/membership";
/**
* Return a promise that is resolved when the client next emits a
@@ -87,7 +88,7 @@ export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): I
for (let i = 0; i < roomMembers.length; i++) {
roomResponse.state.events.push(
mkMembershipCustom({
membership: "join",
membership: KnownMembership.Join,
sender: roomMembers[i],
}),
);
@@ -172,8 +173,10 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
room_id: opts.room,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
prev_content: opts.prev_content,
unsigned: opts.unsigned || {},
unsigned: {
...opts.unsigned,
prev_content: opts.prev_content,
},
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
txn_id: "~" + Math.random(),
redacts: opts.redacts,
@@ -251,7 +254,7 @@ export function mkPresence(opts: IPresenceOpts & { event?: boolean }): Partial<I
interface IMembershipOpts {
room?: string;
mship: string;
mship: Membership;
sender?: string;
user?: string;
skey?: string;
@@ -297,7 +300,7 @@ export function mkMembership(opts: IMembershipOpts & { event?: boolean }): Parti
}
export function mkMembershipCustom<T>(
base: T & { membership: string; sender: string; content?: IContent },
base: T & { membership: Membership; sender: string; content?: IContent },
): T & { type: EventType; sender: string; state_key: string; content: IContent } & GeneratedMetadata {
const content = base.content || {};
return mkEventCustom({
@@ -1,11 +1,13 @@
## Dump of libolm indexeddb cryptostore
## Dumps of libolm indexeddb cryptostore
This directory contains a dump of a real indexeddb store from a session using
This directory contains several dumps of real indexeddb stores from a session using
libolm crypto.
The corresponding pickle key is `+1k2Ppd7HIisUY824v7JtV3/oEE4yX0TqtmNPyhaD7o`.
Each directory contains, in dump.json, a dump of data created by pasting the following
code into the browser console; and in index.ts, details of the user, pickle key,
and corresponding key query and backup responses (`DumpDataSetInfo`).
It was created by pasting the following into the browser console:
The dump is created by pasting the following into the browser console:
```javascript
async function exportIndexedDb(name) {
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
## Dump of an empty libolm indexeddb cryptostore to test skipping migration
A dump of an account which is almost completely empty, and totally unsuitable
for use as a real account.
This dump was manually created by copying and editing full_account.
Created to test
["Unable to restore session" error due due to half-initialised legacy indexeddb crypto store #27447](https://github.com/element-hq/element-web/issues/27447).
We should not launch the Rust migration code when we find a DB in this state.
@@ -0,0 +1,14 @@
{
"account": [],
"device_data": [],
"inbound_group_sessions": [],
"inbound_group_sessions_withheld": [],
"notified_error_devices": [],
"outgoingRoomKeyRequests": [],
"parked_shared_history": [],
"rooms": [],
"session_problems": [],
"sessions": [],
"sessions_needing_backup": [],
"shared_history_inbound_group_sessions": []
}
@@ -0,0 +1,35 @@
/*
Copyright 2024 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 { DumpDataSetInfo } from "../index";
/**
* A key query response containing the current keys of the tested user.
* To be used during tests with fetchmock.
*/
const KEYS_QUERY_RESPONSE = { device_keys: { "@emptyuser:example.com": {} } };
/**
* A dataset containing the information for the tested user.
* To be used during tests.
*/
export const EMPTY_ACCOUNT_DATASET: DumpDataSetInfo = {
userId: "@emptyuser:example.com",
deviceId: "EMPTYDEVIC",
pickleKey: "+/bcdefghijklmnopqrstu1/zyxvutsrqponmlkjih2",
keyQueryResponse: KEYS_QUERY_RESPONSE,
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/empty_account/dump.json",
};
@@ -0,0 +1,4 @@
## Dump of a libolm indexeddb cryptostore to test migration of a full account
A dump of an account containing a complete set of data to migrate.
The data set is substantial enough to allow for testing of chunking mechanisms and progress reporting during the migration process.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,125 @@
/*
Copyright 2024 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 { DumpDataSetInfo } from "../index";
/**
* A key query response containing the current keys of the tested user.
* To be used during tests with fetchmock.
*/
const KEYS_QUERY_RESPONSE: any = {
device_keys: {
"@vdhtest200713:matrix.org": {
KMFSTJSMLB: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "KMFSTJSMLB",
keys: {
"curve25519:KMFSTJSMLB": "LKv0bKbc0EC4h0jknbemv3QalEkeYvuNeUXVRgVVTTU",
"ed25519:KMFSTJSMLB": "qK70DEqIXq7T+UU3v/al47Ab4JkMEBLpNrTBMbS5rrw",
},
user_id: "@vdhtest200713:matrix.org",
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:KMFSTJSMLB":
"aE+PdxLAdwQ/xfJwLmqebvt/lrT97fZas2SQFFrM+dPmHxQtjyS8csm88BLfGRjJKK1B/vWev3AaKqQZwLTUAw",
"ed25519:lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k":
"lCd4SA/JT1nnxsgN9yQaLJQhH5hkLMVVx6ba5JAjL1wpWVqyPxzMJHImX6vTztk6S8rybcdfYkea5W/Ii+4HCQ",
},
},
},
},
},
master_keys: {
"@vdhtest200713:matrix.org": {
user_id: "@vdhtest200713:matrix.org",
usage: ["master"],
keys: {
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI": "gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI",
},
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:MWOGVUTXZN":
"stOu1aHbhsWB/Aj5M/HqBR83QzME+682C995Uc8JxSmmyrlWmgG8QrnoUDG2OFR1t6zNQ+QLEilU4WNEOV73DQ",
},
},
},
},
self_signing_keys: {
"@vdhtest200713:matrix.org": {
user_id: "@vdhtest200713:matrix.org",
usage: ["self_signing"],
keys: {
"ed25519:lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k": "lDvg6vi3P80L9XFNpUSU+5Y87m3p6yHcC83jhSU4Q5k",
},
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
"HKTC7NoBhAkfJtmemmkn/HvCCgBQViWZ0uH7aGPRaWMDFgD8T7Q+y1j3FKZv4mhSopR85Fq3FRyXsG8OVvGeBA",
},
},
},
},
user_signing_keys: {
"@vdhtest200713:matrix.org": {
user_id: "@vdhtest200713:matrix.org",
usage: ["user_signing"],
keys: {
"ed25519:YShqO/3u5vQ0uucojraWrtoLrek0CYrurN/vH/YPMg8": "YShqO/3u5vQ0uucojraWrtoLrek0CYrurN/vH/YPMg8",
},
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
"u8VOi4IaeRJwDgy2ftK02NJQPdBijy8f/0+WnHGG72yfOvMthwWzEw8SrRSNG8glBNrfHinKwCyJJzAJwyepCQ",
},
},
},
},
};
/**
* A `/room_keys/version` response containing the current server-side backup info.
* To be used during tests with fetchmock.
*/
const BACKUP_RESPONSE: any = {
auth_data: {
public_key: "q+HZiJdHl2Yopv9GGvv7EYSzDMrAiRknK4glSdoaomI",
signatures: {
"@vdhtest200713:matrix.org": {
"ed25519:gh9fGr39eNZUdWynEMJ/q/WZq/Pk/foFxHXFBFm18ZI":
"reDp6Mu+j+tfUL3/T6f5OBT3N825Lzpc43vvG+RvjX6V+KxXzodBQArgCoeEHLtL9OgSBmNrhTkSOX87MWCKAw",
"ed25519:KMFSTJSMLB":
"F8tyV5W6wNi0GXTdSg+gxSCULQi0EYxdAAqfkyNq58KzssZMw5i+PRA0aI2b+D7NH/aZaJrtiYNHJ0gWLSQvAw",
},
},
},
version: "7",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
etag: "1",
count: 79,
};
/**
* A dataset containing the information for the tested user.
* To be used during tests.
*/
export const FULL_ACCOUNT_DATASET: DumpDataSetInfo = {
userId: "@vdhtest200713:matrix.org",
deviceId: "KMFSTJSMLB",
pickleKey: "+1k2Ppd7HIisUY824v7JtV3/oEE4yX0TqtmNPyhaD7o",
backupResponse: BACKUP_RESPONSE,
keyQueryResponse: KEYS_QUERY_RESPONSE,
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/full_account/dump.json",
};
@@ -18,11 +18,12 @@ import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
/**
* Populate an IndexedDB store with the test data from this directory.
* Populate an IndexedDB store with a set of test data.
*
* @param name - Name of the IndexedDB database to create.
* @param dumpPath - The path to the dump file to import.
*/
export async function populateStore(name: string): Promise<IDBDatabase> {
export async function populateStore(name: string, dumpPath: string): Promise<IDBDatabase> {
const req = indexedDB.open(name, 11);
const db = await new Promise<IDBDatabase>((resolve, reject) => {
@@ -42,7 +43,7 @@ export async function populateStore(name: string): Promise<IDBDatabase> {
};
});
await importData(db);
await importData(db, dumpPath);
return db;
}
@@ -100,8 +101,8 @@ function upgradeDatabase(oldVersion: number, db: IDBDatabase) {
}
}
async function importData(db: IDBDatabase) {
const path = resolve("spec/test-utils/test_indexeddb_cryptostore_dump/dump.json");
async function importData(db: IDBDatabase, dumpPath: string) {
const path = resolve(dumpPath);
const json: Record<string, Array<{ key?: any; value: any }>> = JSON.parse(
await readFile(path, { encoding: "utf8" }),
);
@@ -134,3 +135,20 @@ async function importData(db: IDBDatabase) {
});
}
}
export interface DumpDataSetInfo {
/** The user ID to use for the test.*/
userId: string;
/** The device ID to use for the test.*/
deviceId: string;
/** The path to the dump file to import via {@link populateStore}.*/
dumpPath: string;
/** The pickle key to use for the dumped account.*/
pickleKey: string;
/** The response to use for the keys query. */
keyQueryResponse: any;
/** The response to use for the backup query.*/
backupResponse?: any;
/** Additional dump info specific for some tests.*/
[key: string]: any;
}
@@ -0,0 +1,4 @@
## Dump of a libolm indexeddb cryptostore where the msk is not cached
A dump simulating an account where the identity was verified, but the msk was not in cache.
Used to test that the owner identity local trust is migrated correctly.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,299 @@
/*
Copyright 2024 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 { KeyBackupInfo } from "../../../../src/crypto-api/keybackup";
import { DumpDataSetInfo } from "../index";
/**
* A key query response containing the current keys of the tested user.
* To be used during tests with fetchmock.
*/
const KEY_QUERY_RESPONSE: any = {
device_keys: {
"@migration:localhost": {
CBGTADUILV: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "CBGTADUILV",
keys: {
"curve25519:CBGTADUILV": "gqhFlc7Wzc1wmmmAu3ySIEe4LtDcBK/bdzrtZg+mMSg",
"ed25519:CBGTADUILV": "q1q3L1Il4l61c/6TmI4fYWMsseNMJJYE2Y0r+5ajKQI",
},
signatures: {
"@migration:localhost": {
"ed25519:CBGTADUILV":
"ppSmA0slyQ7RJOFn+qZSLCGeHN6/jAmqKvUZo5Q1hWk0ugkKycRoSUi9TOfbfAVSf8xvFirXy2VGXQbEVPJqAA",
"ed25519:d+4HhsodR2Zqv4Z5V0VxPfy8zbjLjUCdCyv5qme5Ygc":
"cFLWl1fjehLrzrEn3UnmZMIgy3C23WMgGRsn4e6Z/55vmen4KMs8bLpgZaDoWhIdn/8siHRWafA5sFdzK2NsBQ",
"ed25519:bmFmNcVPvaqrlNzmyKn9uU+QRHyx2QRbn/bUAlTH760":
"C6EeqNPcaQyuZgo8+HOUywc/TMkW5IMjg7aoxyu93X//KcNNXKRfj1banYP6XqyPuQITLamBYc1089Jpt9g4Cw",
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4":
"YwBN/SbCxO8hPgv1B9JY2WVFK4LNK9vq1UNVrkF2j0ZDw9LrvaOws72mbmzZ0nbD3ohcEZ8rXsEosxEVr5r7AQ",
},
},
user_id: "@migration:localhost",
unsigned: {
device_display_name: "localhost:8080: Chrome on macOS",
},
},
TMWBMDZPFT: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "TMWBMDZPFT",
keys: {
"curve25519:TMWBMDZPFT": "oYP9EXvHMbliFdfk8jPvUw0KhAd0+PBqdMslJAt/ZGQ",
"ed25519:TMWBMDZPFT": "IyfPT67JutFWJsUxrxSqEWxgRjKn9B/w78uKU4OBj1E",
},
signatures: {
"@migration:localhost": {
"ed25519:TMWBMDZPFT":
"IWIuuDag4ZMDhMObYV63X7dBYEUYNHYR0Yu/bwLvQh5ieDjQSrZSLOzDrgCyPCM4hkc4JlhneQpJsYo1lUH7DA",
"ed25519:d+4HhsodR2Zqv4Z5V0VxPfy8zbjLjUCdCyv5qme5Ygc":
"iEcTKElQu4CAsQIXmBaZmXwfB6Diut+4ZXakP1ob7OIDMrCYBcgXsBFYg6GuxwL0LCTVcUgbUw7VuPKSvM8UAA",
"ed25519:MYgcP5P7P6KucWjLvTRofY5PWxsf+WDj2BiXtqOO5Gw":
"KcBLDWkCwZyIzlBkC29PNzHxx7Br14TYlhBfREEEQo/Rd34ZZUYwbQ8iPhB8S1GVq3YwgAV6piYIcxpQin+dBA",
"ed25519:HGN9m99VprMuQBDA3o+KZKcEYTaGmiaujrkygjScMnY":
"VqrvA148Uxib9TNFI1rc9r8qpwTojCkqLofEz9dMLc/XV3U14WD5/LDEhMuCwNu6wsu/uO+dS4AmJlJnN/iAAg",
"ed25519:Nt0L/p+UVHMx603sYHXwXja+VyQIUVFvu0vDBYn56Zk":
"D1COHzROOTNlCn8b1zI9+6phUtF0OVqWxLfOLnX5t14H2oENYV2ASgaxsdmXcSZPrGzaJkmSOginHHzsabe5CA",
"ed25519:bmFmNcVPvaqrlNzmyKn9uU+QRHyx2QRbn/bUAlTH760":
"SFSDrsi3GQ9jjBYUc2aUSzf777/0NfQWrOBi2CK+v5VQY3FkyHBln3K4YzvxIKSVIhOaQtBlEDtfQb33kwTgDg",
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4":
"BtJkzQe0YFAa8gJiYXYtzGtktl9vZMNYl5jd4DA8Toi4VxgosJNZQE7lT5qpYU0BrlFn46QIs/38X8JhSt+wAQ",
},
},
user_id: "@migration:localhost",
unsigned: {
device_display_name: "localhost:8080: Chrome on macOS",
},
},
},
},
failures: {},
master_keys: {
"@migration:localhost": {
keys: {
"ed25519:cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4": "cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4",
},
signatures: {
"@migration:localhost": {
"ed25519:TMWBMDZPFT":
"RrPUnYoekK7wZGrLNXshgoupF8v53S/vJyvkBJi+q9THh4Qrf3CieuVJFx8mwtmEZgGoA2tSroAVnRqvEQ+IBQ",
"ed25519:cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4":
"o4CbtdU3IqJK90UXAEBtxps2m4XBYvWJI2nbVlzBaGRr+Xt/3vtwDMlc5G970kPQWBbs/koYJh8MSaE7Fm1mAg",
"ed25519:CBGTADUILV":
"AgZoG+ix8aW3FAW6v+/Xu+QJpxzvsx5itbB8RyqMet9YlNqX90vYIbBV7IoV2WFY2WdANYEffX2CE0FpR6NnCg",
},
},
usage: ["master"],
user_id: "@migration:localhost",
},
},
self_signing_keys: {
"@migration:localhost": {
keys: {
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4": "RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4",
},
signatures: {
"@migration:localhost": {
"ed25519:cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4":
"hs8VqoTfipDjC2pzFdmzb1aENhDjVV+gc86fuYftczaCcsXUWop/NPwoF51Ie6Nb3YL0N7ZZAUrycuJP5hFbDg",
},
},
usage: ["self_signing"],
user_id: "@migration:localhost",
},
},
user_signing_keys: {
"@migration:localhost": {
keys: {
"ed25519:WNJ2G3Ig5EdC4wYiRKcK7bhLP2+I4wI6V7SKgJTXdw8": "WNJ2G3Ig5EdC4wYiRKcK7bhLP2+I4wI6V7SKgJTXdw8",
},
signatures: {
"@migration:localhost": {
"ed25519:cFjUBAhAZ2tjYF1TpQtYNA3x9XRzTiIdP2N2EvRaOH4":
"Vlba5rJQxG+ussVLoycvHcin7Ghv0uUeClDqDbM+RPF+jx9w4ozbcuEOTJdyzyPA+GxN9Kzh2lmVFMMQGyvNAw",
},
},
usage: ["user_signing"],
user_id: "@migration:localhost",
},
},
};
/**
* A new key query response for the same user simulating a cross-signing key reset.
* To be used during tests with fetchmock.
*/
const ROTATED_KEY_QUERY_RESPONSE: any = {
device_keys: {
"@migration:localhost": {
TMWBMDZPFT: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "TMWBMDZPFT",
keys: {
"curve25519:TMWBMDZPFT": "oYP9EXvHMbliFdfk8jPvUw0KhAd0+PBqdMslJAt/ZGQ",
"ed25519:TMWBMDZPFT": "IyfPT67JutFWJsUxrxSqEWxgRjKn9B/w78uKU4OBj1E",
},
signatures: {
"@migration:localhost": {
"ed25519:TMWBMDZPFT":
"IWIuuDag4ZMDhMObYV63X7dBYEUYNHYR0Yu/bwLvQh5ieDjQSrZSLOzDrgCyPCM4hkc4JlhneQpJsYo1lUH7DA",
"ed25519:d+4HhsodR2Zqv4Z5V0VxPfy8zbjLjUCdCyv5qme5Ygc":
"iEcTKElQu4CAsQIXmBaZmXwfB6Diut+4ZXakP1ob7OIDMrCYBcgXsBFYg6GuxwL0LCTVcUgbUw7VuPKSvM8UAA",
"ed25519:MYgcP5P7P6KucWjLvTRofY5PWxsf+WDj2BiXtqOO5Gw":
"KcBLDWkCwZyIzlBkC29PNzHxx7Br14TYlhBfREEEQo/Rd34ZZUYwbQ8iPhB8S1GVq3YwgAV6piYIcxpQin+dBA",
"ed25519:HGN9m99VprMuQBDA3o+KZKcEYTaGmiaujrkygjScMnY":
"VqrvA148Uxib9TNFI1rc9r8qpwTojCkqLofEz9dMLc/XV3U14WD5/LDEhMuCwNu6wsu/uO+dS4AmJlJnN/iAAg",
"ed25519:Nt0L/p+UVHMx603sYHXwXja+VyQIUVFvu0vDBYn56Zk":
"D1COHzROOTNlCn8b1zI9+6phUtF0OVqWxLfOLnX5t14H2oENYV2ASgaxsdmXcSZPrGzaJkmSOginHHzsabe5CA",
"ed25519:bmFmNcVPvaqrlNzmyKn9uU+QRHyx2QRbn/bUAlTH760":
"SFSDrsi3GQ9jjBYUc2aUSzf777/0NfQWrOBi2CK+v5VQY3FkyHBln3K4YzvxIKSVIhOaQtBlEDtfQb33kwTgDg",
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4":
"BtJkzQe0YFAa8gJiYXYtzGtktl9vZMNYl5jd4DA8Toi4VxgosJNZQE7lT5qpYU0BrlFn46QIs/38X8JhSt+wAQ",
},
},
user_id: "@migration:localhost",
unsigned: {
device_display_name: "localhost:8080: Chrome on macOS",
},
},
XFZFSCUOFL: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "XFZFSCUOFL",
keys: {
"curve25519:XFZFSCUOFL": "aN2Ty+0rutNkrRtxhV+ciI8GhF4epSxzL7bAOr8zfkc",
"ed25519:XFZFSCUOFL": "V7CPhXdfLFk+qAOFivrpFskmunVTeuM+EOM3DMlDxkI",
},
signatures: {
"@migration:localhost": {
"ed25519:XFZFSCUOFL":
"4Pqc2FWJ5p/L/tSlfUBIlcQzLmN5CksJriAibY8LSDAXdGYiQJ7hvKqneEuVhrMYwqyIxb4bAad+r6wnY0/7Cg",
"ed25519:RkQzi0+aKIL9Y+GzsN23xMz3i3QRkH03G5aqqEbbuy4":
"yH8pKnD+E8YaawS+1NCjwy0cf2WzBRff9BBNX4YnAuTyc6s5b1QqNfu9DP5qblw8TZ7hZmaziePZKsjRiqJLBg",
"ed25519:OEv0wHLusJx7zTCc0h3HbNIHLIxlGZKh63tc2ptKb+Y":
"M8SfAiEUzd7AsWp8InS7BxV3cRqV3MjMxks4DwSxsVxvkCco2JWybKgev+vTZyM6XDg930o0FObQOxWm4+CkBw",
},
},
user_id: "@migration:localhost",
unsigned: {
device_display_name: "localhost:8080: Chrome on macOS",
},
},
},
},
failures: {},
master_keys: {
"@migration:localhost": {
user_id: "@migration:localhost",
usage: ["master"],
keys: {
"ed25519:rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack": "rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack",
},
signatures: {
"@migration:localhost": {
"ed25519:XFZFSCUOFL":
"C8aswtyUABWvj2DInehVoh2P/EDbwRhlIk51LtV3L71POUCh7pZuyXRMMWKZeyRvHRmEllXBtRkH1iol/p56Bg",
},
},
},
},
self_signing_keys: {
"@migration:localhost": {
user_id: "@migration:localhost",
usage: ["self_signing"],
keys: {
"ed25519:OEv0wHLusJx7zTCc0h3HbNIHLIxlGZKh63tc2ptKb+Y": "OEv0wHLusJx7zTCc0h3HbNIHLIxlGZKh63tc2ptKb+Y",
},
signatures: {
"@migration:localhost": {
"ed25519:rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack":
"dH596pGp8+f8dlwd81UrKDWoRDd24yAqqMSLqR4fJHyfszbn7qCvQA6LYZ023TLmk33FKcJqRtd2v/ykTmS3Bg",
},
},
},
},
user_signing_keys: {
"@migration:localhost": {
user_id: "@migration:localhost",
usage: ["user_signing"],
keys: {
"ed25519:8XHpC3MeMReIfYneWIRX8c4ANgJuQ1+oFrktBcLka4o": "8XHpC3MeMReIfYneWIRX8c4ANgJuQ1+oFrktBcLka4o",
},
signatures: {
"@migration:localhost": {
"ed25519:rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack":
"FX6ylagvx3IG1zMf/ayYgDb/1+x0/F28pHQqzQMGGssAmc15nat/R6AF0QO7Qg7uqTAf04ohuZtWax3dTwjNDQ",
},
},
},
},
};
/**
* A `/room_keys/version` response containing the current server-side backup info.
* To be used during tests with fetchmock.
*/
const BACKUP_RESPONSE: KeyBackupInfo = {
auth_data: {
public_key: "2ffIfIB4oryqZpsJQjQNUaxgCzxliC6A4PJvnrN+XAA",
signatures: {
"@migration:localhost": {
"ed25519:TMWBMDZPFT":
"qBvalid/G4hnSF3hAeX4TtRN6/BqprgiYnLEtDuatyQ5WxWr0s4uSOyvHSglsRdpoo32FDBHfTIZkCOVxSLwAA",
},
},
},
version: "2",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
etag: "0",
count: 0,
};
/**
* This was generated by doing a backup reset on the account.
* This is a new valid backup for this account.
*/
const NEW_BACKUP_RESPONSE: KeyBackupInfo = {
auth_data: {
public_key: "CkDxWALi3lcChgjEZFEM6clYq5x768XBwsL++eaOzTI",
signatures: {
"@migration:localhost": {
"ed25519:YVEGEYPYWX":
"ZSYuQDdwgB9WKXQ+z5aWWfqSolBCGRw53kur1Vy956gFefgzCBkMbw5M0I2UgfU2Cukri7jZ4ig201zmLNmaAA",
"ed25519:rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack":
"+UQ8EA507LoIqgK9rPsqPoGrj+iRBJeY2Oz0mMtXmVf8c1y8G0KWJNUWqvOysnOhsoJf1bt8ey48CxjjtSQ2AA",
},
},
},
version: "3",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
etag: "0",
count: 0,
};
/**
* A dataset containing the information for the tested user.
* To be used during tests.
*/
export const MSK_NOT_CACHED_DATASET: DumpDataSetInfo = {
userId: "@migration:localhost",
deviceId: "CBGTADUILV",
pickleKey: "qEURMepfkMvoBQGaWlI9MZKYnDMsSAiW8aFTKXaeDV0",
keyQueryResponse: KEY_QUERY_RESPONSE,
rotatedKeyQueryResponse: ROTATED_KEY_QUERY_RESPONSE,
backupResponse: BACKUP_RESPONSE,
newBackupResponse: NEW_BACKUP_RESPONSE,
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/no_cached_msk_dump/dump.json",
};
@@ -0,0 +1,4 @@
## Dump of a libolm indexeddb cryptostore where the identity is not trusted.
A dump of an account where the identity was not verified.
Used as a test case for migration of the identity local trust.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,126 @@
/*
Copyright 2024 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 { DumpDataSetInfo } from "../index";
/**
* A key query response containing the current keys of the tested user.
* To be used during tests with fetchmock.
*/
const KEY_QUERY_RESPONSE = {
device_keys: {
"@untrusted:localhost": {
IXNYALOZWU: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "IXNYALOZWU",
keys: {
"curve25519:IXNYALOZWU": "EHMQEtJd9INJg28HwKK8Te1EX8obR3VTtyNwf/rcczM",
"ed25519:IXNYALOZWU": "OxMfZHsYJvroTp1RtjUOejpWbRBryN6VsojC5dKR74U",
},
signatures: {
"@untrusted:localhost": {
"ed25519:IXNYALOZWU":
"tWaTiRKc95ZCqM2qrKTdq1sQ3DPFgw3vdrOVmWIHQwj92DCgJtnQ9uymLMOq+MSb80bdBBjXwrNeOufgaL/6CQ",
"ed25519:+ik0n/QnBPq8H/48wAT+54slKk1SL7NIk/HtiN/cNEg":
"+QXZFLiAv+k7UXgAP6AXLk/PdZ3TlJ77M23m73v8qvavAlnkLBAjKNA3BG39JTQET5UhW5DnCohwsbGP+aY1Cw",
},
},
user_id: "@untrusted:localhost",
unsigned: {
device_display_name: "localhost:8080: Chrome on macOS",
},
},
VJPSPVPWZT: {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: "VJPSPVPWZT",
keys: {
"curve25519:VJPSPVPWZT": "+RxCNRFPqBZJm6PLjEJsSdFixGWQJygD5Os11/+6PC0",
"ed25519:VJPSPVPWZT": "wqH7xK/DQya8m05Vy4rnacjugGNBiY+7Ml6wyRVkM9U",
},
signatures: {
"@untrusted:localhost": {
"ed25519:VJPSPVPWZT":
"XC+RoKL/zVZOIwk/bGEQJlJu49QicY1v6vSDMHA2y0/fpX/MD4KiWGD7+W5DFD54E8FrFVTsIgkzat561qdTBQ",
},
},
user_id: "@untrusted:localhost",
unsigned: {
device_display_name: "localhost:8080: Chrome on macOS",
},
},
},
},
failures: {},
master_keys: {
"@untrusted:localhost": {
keys: {
"ed25519:Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g": "Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g",
},
signatures: {
"@untrusted:localhost": {
"ed25519:IXNYALOZWU":
"KdAdyKO2sb3Di3bdK+oxf+gjMSmW/sisRNvpKZORPKwmy2SGaKGYkecBtslunoFjnb+hjIESgweQu6cHoNX4AA",
"ed25519:Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g":
"b0R9Id5HxHYo+MA22Vlq0OckTrWnSWhgHLvF8Wr4e154JdtOyK7N0aXPQPkrLB0fmyVmGdbDa9xs9jsfINGmDw",
},
},
usage: ["master"],
user_id: "@untrusted:localhost",
},
},
self_signing_keys: {
"@untrusted:localhost": {
keys: {
"ed25519:+ik0n/QnBPq8H/48wAT+54slKk1SL7NIk/HtiN/cNEg": "+ik0n/QnBPq8H/48wAT+54slKk1SL7NIk/HtiN/cNEg",
},
signatures: {
"@untrusted:localhost": {
"ed25519:Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g":
"z/5z51jbRpyDQhYnfUHhhb5fUbzRDlfjD8mZA2ZGStpE/F41lDyxjlvF2W/E2CJ27bmJFdk7nC+ZCwriYfYxBw",
},
},
usage: ["self_signing"],
user_id: "@untrusted:localhost",
},
},
user_signing_keys: {
"@untrusted:localhost": {
keys: {
"ed25519:L/8HbQWnK9OidAcDVB+Az9b0Mx3OdBtIMFsUjV6qgSQ": "L/8HbQWnK9OidAcDVB+Az9b0Mx3OdBtIMFsUjV6qgSQ",
},
signatures: {
"@untrusted:localhost": {
"ed25519:Uahbc3+Rk65y0ku6T2RL/29fEA9Bum6+OaqptG6df3g":
"UuNvzebLQn31LYGbx+ADe60BB25kWy4SVVyd9BXlY/tAZMoA8Tmq1e2R2tJJtPdJxC/Oogktj2+iikZV/YMjAQ",
},
},
usage: ["user_signing"],
user_id: "@untrusted:localhost",
},
},
};
/**
* A dataset containing the information for the tested user.
* To be used during tests.
*/
export const IDENTITY_NOT_TRUSTED_DATASET: DumpDataSetInfo = {
userId: "@untrusted:localhost",
deviceId: "VJPSPVPWZT",
pickleKey: "WVllQb4Lk/WwP4Q7iBfeTUHpgydZm9YqXI1B5bTvnIM",
keyQueryResponse: KEY_QUERY_RESPONSE,
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/unverified/dump.json",
};
+2 -99
View File
@@ -15,13 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import MockHttpBackend from "matrix-mock-request";
import { AutoDiscoveryAction, M_AUTHENTICATION } from "../../src";
import { AutoDiscoveryAction } from "../../src";
import { AutoDiscovery } from "../../src/autodiscovery";
import { OidcError } from "../../src/oidc/error";
import { makeDelegatedAuthConfig } from "../test-utils/oidc";
// keep to reset the fetch function after using MockHttpBackend
// @ts-ignore private property
@@ -409,10 +406,6 @@ describe("AutoDiscovery", function () {
error: null,
base_url: null,
},
"m.authentication": {
state: "IGNORE",
error: OidcError.NotSupported,
},
};
expect(conf).toEqual(expected);
@@ -450,10 +443,6 @@ describe("AutoDiscovery", function () {
error: null,
base_url: null,
},
"m.authentication": {
state: "IGNORE",
error: OidcError.NotSupported,
},
};
expect(conf).toEqual(expected);
@@ -476,9 +465,6 @@ describe("AutoDiscovery", function () {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.authentication": {
invalid: true,
},
});
return Promise.all([
httpBackend.flushAllExpected(),
@@ -494,10 +480,6 @@ describe("AutoDiscovery", function () {
error: null,
base_url: null,
},
"m.authentication": {
state: "FAIL_ERROR",
error: OidcError.Misconfigured,
},
};
expect(conf).toEqual(expected);
@@ -728,10 +710,6 @@ describe("AutoDiscovery", function () {
error: null,
base_url: "https://identity.example.org",
},
"m.authentication": {
state: "IGNORE",
error: OidcError.NotSupported,
},
};
expect(conf).toEqual(expected);
@@ -784,10 +762,6 @@ describe("AutoDiscovery", function () {
"org.example.custom.property": {
cupcakes: "yes",
},
"m.authentication": {
state: "IGNORE",
error: OidcError.NotSupported,
},
};
expect(conf).toEqual(expected);
@@ -883,7 +857,7 @@ describe("AutoDiscovery", function () {
const expected = {
"m.homeserver": {
state: AutoDiscoveryAction.FAIL_ERROR,
error: AutoDiscovery.ERROR_HOMESERVER_TOO_OLD,
error: AutoDiscovery.ERROR_UNSUPPORTED_HOMESERVER_SPEC_VERSION,
base_url: "https://example.org",
},
"m.identity_server": {
@@ -897,75 +871,4 @@ describe("AutoDiscovery", function () {
}),
]);
});
describe("m.authentication", () => {
const homeserverName = "example.org";
const homeserverUrl = "https://chat.example.org/";
const issuer = "https://auth.org/";
beforeAll(() => {
// make these tests independent from fetch mocking above
AutoDiscovery.setFetchFn(realAutoDiscoveryFetch);
});
beforeEach(() => {
fetchMock.resetBehavior();
fetchMock.get(`${homeserverUrl}_matrix/client/versions`, { versions: ["v1.5"] });
fetchMock.get("https://example.org/.well-known/matrix/client", {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.authentication": {
issuer,
},
});
});
it("should return valid authentication configuration", async () => {
const config = makeDelegatedAuthConfig(issuer);
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
fetchMock.get(`${config.metadata.issuer}jwks`, {
status: 200,
headers: {
"Content-Type": "application/json",
},
keys: [],
});
const result = await AutoDiscovery.findClientConfig(homeserverName);
expect(result[M_AUTHENTICATION.stable!]).toEqual({
state: AutoDiscovery.SUCCESS,
...config,
signingKeys: [],
account: undefined,
error: null,
});
});
it("should set state to error for invalid authentication configuration", async () => {
const config = makeDelegatedAuthConfig(issuer);
// authorization_code is required
config.metadata.grant_types_supported = ["openid"];
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
fetchMock.get(`${config.metadata.issuer}jwks`, {
status: 200,
headers: {
"Content-Type": "application/json",
},
keys: [],
});
const result = await AutoDiscovery.findClientConfig(homeserverName);
expect(result[M_AUTHENTICATION.stable!]).toEqual({
state: AutoDiscovery.FAIL_ERROR,
error: OidcError.OpSupport,
});
});
});
});
+24
View File
@@ -76,5 +76,29 @@ describe("ContentRepo", function () {
baseUrl + "/_matrix/media/v3/download/server.name/resourceid#automade",
);
});
it("should return an authenticated URL when requested", function () {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri, undefined, undefined, undefined, undefined, true, true)).toEqual(
baseUrl +
"/_matrix/client/unstable/org.matrix.msc3916/media/download/server.name/resourceid?allow_redirect=true",
);
expect(getHttpUriForMxc(baseUrl, mxcUri, 64, 64, "scale", undefined, true, true)).toEqual(
baseUrl +
"/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/server.name/resourceid?width=64&height=64&method=scale&allow_redirect=true",
);
});
it("should force-enable allow_redirects when useAuthentication is set true", function () {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri, undefined, undefined, undefined, undefined, false, true)).toEqual(
baseUrl +
"/_matrix/client/unstable/org.matrix.msc3916/media/download/server.name/resourceid?allow_redirect=true",
);
expect(getHttpUriForMxc(baseUrl, mxcUri, 64, 64, "scale", undefined, false, true)).toEqual(
baseUrl +
"/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/server.name/resourceid?width=64&height=64&method=scale&allow_redirect=true",
);
});
});
});
+3 -2
View File
@@ -25,6 +25,7 @@ import { UserTrustLevel } from "../../src/crypto/CrossSigning";
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
import { EventDecryptionResult } from "../../src/common-crypto/CryptoBackend";
import * as testData from "../test-utils/test-data";
import { KnownMembership } from "../../src/@types/membership";
const Olm = global.Olm;
@@ -463,7 +464,7 @@ describe("Crypto", function () {
type: "m.room.member",
sender: "@alice:example.com",
room_id: roomId,
content: { membership: "invite" },
content: { membership: KnownMembership.Invite },
state_key: "@bob:example.com",
}),
]);
@@ -795,7 +796,7 @@ describe("Crypto", function () {
type: "m.room.member",
sender: "@clara:example.com",
room_id: roomId,
content: { membership: "invite" },
content: { membership: KnownMembership.Invite },
state_key: "@bob:example.com",
}),
]);
+3 -2
View File
@@ -36,6 +36,7 @@ import { DeviceTrustLevel } from "../../../../src/crypto/CrossSigning";
import { MegolmEncryption as MegolmEncryptionClass } from "../../../../src/crypto/algorithms/megolm";
import { recursiveMapToObject } from "../../../../src/utils";
import { sleep } from "../../../../src/utils";
import { KnownMembership } from "../../../../src/@types/membership";
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
@@ -806,11 +807,11 @@ describe("MegolmDecryption", function () {
aliceRoom.getEncryptionTargetMembers = jest.fn().mockResolvedValue([
{
userId: "@alice:example.com",
membership: "join",
membership: KnownMembership.Join,
},
{
userId: "@bob:example.com",
membership: "join",
membership: KnownMembership.Join,
},
]);
const BOB_DEVICES = {
+10 -10
View File
@@ -23,12 +23,12 @@ import HttpBackend from "matrix-mock-request";
import * as olmlib from "../../../src/crypto/olmlib";
import { MatrixError } from "../../../src/http-api";
import { logger } from "../../../src/logger";
import { ICrossSigningKey, ICreateClientOpts, ISignedKey, MatrixClient } from "../../../src/client";
import { ICreateClientOpts, ISignedKey, MatrixClient } from "../../../src/client";
import { CryptoEvent } from "../../../src/crypto";
import { IDevice } from "../../../src/crypto/deviceinfo";
import { TestClient } from "../../TestClient";
import { resetCrossSigningKeys } from "./crypto-utils";
import { BootstrapCrossSigningOpts } from "../../../src/crypto-api";
import { BootstrapCrossSigningOpts, CrossSigningKeyInfo } from "../../../src/crypto-api";
const PUSH_RULES_RESPONSE: Response = {
method: "GET",
@@ -377,7 +377,7 @@ describe("Cross Signing", function () {
const bobSigning = new global.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK: ICrossSigningKey = {
const bobSSK: CrossSigningKeyInfo = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
@@ -515,7 +515,7 @@ describe("Cross Signing", function () {
};
olmlib.pkSign(bobDevice, selfSigningKey as unknown as PkSigning, "@bob:example.com", "");
const bobMaster: ICrossSigningKey = {
const bobMaster: CrossSigningKeyInfo = {
user_id: "@bob:example.com",
usage: ["master"],
keys: {
@@ -630,7 +630,7 @@ describe("Cross Signing", function () {
const bobSigning = new global.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK: ICrossSigningKey = {
const bobSSK: CrossSigningKeyInfo = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
@@ -696,7 +696,7 @@ describe("Cross Signing", function () {
const bobSigning = new global.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK: ICrossSigningKey = {
const bobSSK: CrossSigningKeyInfo = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
@@ -763,7 +763,7 @@ describe("Cross Signing", function () {
const bobSigning2 = new global.Olm.PkSigning();
const bobPrivkey2 = bobSigning2.generate_seed();
const bobPubkey2 = bobSigning2.init_with_seed(bobPrivkey2);
const bobSSK2: ICrossSigningKey = {
const bobSSK2: CrossSigningKeyInfo = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
@@ -913,7 +913,7 @@ describe("Cross Signing", function () {
const aliceSigning = new global.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: ICrossSigningKey = {
const aliceSSK: CrossSigningKeyInfo = {
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
@@ -988,7 +988,7 @@ describe("Cross Signing", function () {
const aliceSigning = new global.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: ICrossSigningKey = {
const aliceSSK: CrossSigningKeyInfo = {
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
@@ -1048,7 +1048,7 @@ describe("Cross Signing", function () {
const aliceSigning = new global.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: ICrossSigningKey = {
const aliceSSK: CrossSigningKeyInfo = {
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
+6 -5
View File
@@ -23,12 +23,13 @@ import { makeTestClients } from "./verification/util";
import { encryptAES } from "../../../src/crypto/aes";
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
import { logger } from "../../../src/logger";
import { ClientEvent, ICreateClientOpts, ICrossSigningKey, MatrixClient } from "../../../src/client";
import { ClientEvent, ICreateClientOpts, MatrixClient } from "../../../src/client";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
import { ISignatures } from "../../../src/@types/signed";
import { ICurve25519AuthData } from "../../../src/crypto/keybackup";
import { SecretStorageKeyDescription, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { decodeBase64 } from "../../../src/base64";
import { CrossSigningKeyInfo } from "../../../src/crypto-api";
async function makeTestClient(
userInfo: { userId: string; deviceId: string },
@@ -475,7 +476,7 @@ describe("Secrets", function () {
[`ed25519:${XSPubKey}`]: XSPubKey,
},
},
self_signing: sign<ICrossSigningKey>(
self_signing: sign<CrossSigningKeyInfo>(
{
user_id: "@alice:example.com",
usage: ["self_signing"],
@@ -486,7 +487,7 @@ describe("Secrets", function () {
XSK,
"@alice:example.com",
),
user_signing: sign<ICrossSigningKey>(
user_signing: sign<CrossSigningKeyInfo>(
{
user_id: "@alice:example.com",
usage: ["user_signing"],
@@ -631,7 +632,7 @@ describe("Secrets", function () {
[`ed25519:${XSPubKey}`]: XSPubKey,
},
},
self_signing: sign<ICrossSigningKey>(
self_signing: sign<CrossSigningKeyInfo>(
{
user_id: "@alice:example.com",
usage: ["self_signing"],
@@ -642,7 +643,7 @@ describe("Secrets", function () {
XSK,
"@alice:example.com",
),
user_signing: sign<ICrossSigningKey>(
user_signing: sign<CrossSigningKeyInfo>(
{
user_id: "@alice:example.com",
usage: ["user_signing"],
+31 -3
View File
@@ -75,6 +75,20 @@ class MockWidgetApi extends EventEmitter {
public transport = { reply: jest.fn() };
}
declare module "../../src/types" {
interface StateEvents {
"org.example.foo": {
hello: string;
};
}
interface TimelineEvents {
"org.matrix.rageshake_request": {
request_id: number;
};
}
}
describe("RoomWidgetClient", () => {
let widgetApi: MockedObject<WidgetApi>;
let client: MatrixClient;
@@ -87,9 +101,12 @@ describe("RoomWidgetClient", () => {
client.stopClient();
});
const makeClient = async (capabilities: ICapabilities): Promise<void> => {
const makeClient = async (
capabilities: ICapabilities,
sendContentLoaded: boolean | undefined = undefined,
): Promise<void> => {
const baseUrl = "https://example.org";
client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl });
client = createRoomWidgetClient(widgetApi, capabilities, "!1:example.org", { baseUrl }, sendContentLoaded);
expect(widgetApi.start).toHaveBeenCalled(); // needs to have been called early in order to not miss messages
widgetApi.emit("ready");
await client.startClient();
@@ -143,7 +160,7 @@ describe("RoomWidgetClient", () => {
});
});
describe("messages", () => {
describe("initialization", () => {
it("requests permissions for specific message types", async () => {
await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
@@ -158,6 +175,15 @@ describe("RoomWidgetClient", () => {
expect(widgetApi.requestCapabilityToReceiveMessage).toHaveBeenCalledWith();
});
it("sends content loaded when configured", async () => {
await makeClient({});
expect(widgetApi.sendContentLoaded).toHaveBeenCalled();
});
it("does not sent content loaded when configured", async () => {
await makeClient({}, false);
expect(widgetApi.sendContentLoaded).not.toHaveBeenCalled();
});
// No point in testing sending and receiving since it's done exactly the
// same way as non-message events
});
@@ -305,12 +331,14 @@ describe("RoomWidgetClient", () => {
expect(await emittedSync).toEqual(SyncState.Syncing);
});
});
describe("oidc token", () => {
it("requests an oidc token", async () => {
await makeClient({});
expect(await client.getOpenIdToken()).toStrictEqual(testOIDCToken);
});
});
it("gets TURN servers", async () => {
const server1: ITurnServer = {
uris: [
-5
View File
@@ -136,11 +136,6 @@ describe("EventTimelineSet", () => {
expect(eventsInLiveTimeline.length).toStrictEqual(1);
expect(eventsInLiveTimeline[0]).toStrictEqual(duplicateMessageEvent);
});
it("Make sure legacy overload passing options directly as parameters still works", () => {
expect(() => eventTimelineSet.addLiveEvent(messageEvent, DuplicateStrategy.Replace, false)).not.toThrow();
expect(() => eventTimelineSet.addLiveEvent(messageEvent, DuplicateStrategy.Ignore, true)).not.toThrow();
});
});
describe("addEventToTimeline", () => {
+11 -10
View File
@@ -7,6 +7,7 @@ import { MatrixClient } from "../../src/matrix";
import { Room } from "../../src/models/room";
import { RoomMember } from "../../src/models/room-member";
import { EventTimelineSet } from "../../src/models/event-timeline-set";
import { KnownMembership } from "../../src/@types/membership";
describe("EventTimeline", function () {
const roomId = "!foo:bar";
@@ -50,7 +51,7 @@ describe("EventTimeline", function () {
const events = [
utils.mkMembership({
room: roomId,
mship: "invite",
mship: KnownMembership.Invite,
user: userB,
skey: userA,
event: true,
@@ -87,7 +88,7 @@ describe("EventTimeline", function () {
const state = [
utils.mkMembership({
room: roomId,
mship: "invite",
mship: KnownMembership.Invite,
user: userB,
skey: userA,
event: true,
@@ -203,11 +204,11 @@ describe("EventTimeline", function () {
it("should set event.sender for new and old events", function () {
const sentinel = new RoomMember(roomId, userA);
sentinel.name = "Alice";
sentinel.membership = "join";
sentinel.membership = KnownMembership.Join;
const oldSentinel = new RoomMember(roomId, userA);
sentinel.name = "Old Alice";
sentinel.membership = "join";
sentinel.membership = KnownMembership.Join;
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
if (uid === userA) {
@@ -246,11 +247,11 @@ describe("EventTimeline", function () {
it("should set event.target for new and old m.room.member events", function () {
const sentinel = new RoomMember(roomId, userA);
sentinel.name = "Alice";
sentinel.membership = "join";
sentinel.membership = KnownMembership.Join;
const oldSentinel = new RoomMember(roomId, userA);
sentinel.name = "Old Alice";
sentinel.membership = "join";
sentinel.membership = KnownMembership.Join;
mocked(timeline.getState(EventTimeline.FORWARDS)!).getSentinelMember.mockImplementation(function (uid) {
if (uid === userA) {
@@ -267,14 +268,14 @@ describe("EventTimeline", function () {
const newEv = utils.mkMembership({
room: roomId,
mship: "invite",
mship: KnownMembership.Invite,
user: userB,
skey: userA,
event: true,
});
const oldEv = utils.mkMembership({
room: roomId,
mship: "ban",
mship: KnownMembership.Ban,
user: userB,
skey: userA,
event: true,
@@ -291,7 +292,7 @@ describe("EventTimeline", function () {
const events = [
utils.mkMembership({
room: roomId,
mship: "invite",
mship: KnownMembership.Invite,
user: userB,
skey: userA,
event: true,
@@ -330,7 +331,7 @@ describe("EventTimeline", function () {
const events = [
utils.mkMembership({
room: roomId,
mship: "invite",
mship: KnownMembership.Invite,
user: userB,
skey: userA,
event: true,
+27 -22
View File
@@ -22,6 +22,7 @@ import { Filter } from "../../src/filter";
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
import {
EventType,
MsgType,
RelationType,
RoomCreateTypeField,
RoomType,
@@ -38,30 +39,31 @@ import * as testUtils from "../test-utils/test-utils";
import { makeBeaconInfoContent } from "../../src/content-helpers";
import { M_BEACON_INFO } from "../../src/@types/beacon";
import {
ContentHelpers,
ClientPrefix,
ConditionKind,
ContentHelpers,
Direction,
EventTimeline,
EventTimelineSet,
getHttpUriForMxc,
ICreateRoomOpts,
IPushRule,
IRequestOpts,
MatrixError,
MatrixHttpApi,
MatrixScheduler,
Method,
Room,
EventTimelineSet,
PushRuleActionName,
TweakName,
Room,
RuleId,
IPushRule,
ConditionKind,
getHttpUriForMxc,
TweakName,
} from "../../src";
import { supportsMatrixCall } from "../../src/webrtc/call";
import { makeBeaconEvent } from "../test-utils/beacon";
import {
IGNORE_INVITES_ACCOUNT_EVENT_KEY,
POLICIES_ACCOUNT_EVENT_TYPE,
PolicyRecommendation,
PolicyScope,
} from "../../src/models/invites-ignorer";
import { IOlmDevice } from "../../src/crypto/algorithms/megolm";
@@ -71,6 +73,8 @@ import * as featureUtils from "../../src/feature";
import { StubStore } from "../../src/store/stub";
import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorageImpl } from "../../src/secret-storage";
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
import { KnownMembership } from "../../src/@types/membership";
import { RoomMessageEventContent } from "../../src/@types/events";
jest.useFakeTimers();
@@ -382,6 +386,9 @@ describe("MatrixClient", function () {
expect(client.mxcUrlToHttp(mxc, 32, 46, "scale", false, true)).toBe(
getHttpUriForMxc(client.baseUrl, mxc, 32, 46, "scale", false, true),
);
expect(client.mxcUrlToHttp(mxc, 32, 46, "scale", false, true, true)).toBe(
getHttpUriForMxc(client.baseUrl, mxc, 32, 46, "scale", false, true, true),
);
});
});
@@ -565,7 +572,7 @@ describe("MatrixClient", function () {
describe("sendEvent", () => {
const roomId = "!room:example.org";
const body = "This is the body";
const content = { body };
const content = { body, msgtype: MsgType.Text } satisfies RoomMessageEventContent;
it("overload without threadId works", async () => {
const eventId = "$eventId:example.org";
@@ -660,12 +667,13 @@ describe("MatrixClient", function () {
const content = {
body,
"msgtype": MsgType.Text,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$other:event",
},
},
};
} satisfies RoomMessageEventContent;
const room = new Room(roomId, client, userId);
mocked(store.getRoom).mockReturnValue(room);
@@ -750,7 +758,7 @@ describe("MatrixClient", function () {
it("should get (unstable) file trees with valid state", async () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "join",
getMyMembership: () => KnownMembership.Join,
currentState: {
getStateEvents: (eventType, stateKey) => {
/* eslint-disable jest/no-conditional-expect */
@@ -789,7 +797,7 @@ describe("MatrixClient", function () {
it("should not get (unstable) file trees if not joined", async () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "leave", // "not join"
getMyMembership: () => KnownMembership.Leave, // "not join"
} as unknown as Room;
client.getRoom = (getRoomId) => {
expect(getRoomId).toEqual(roomId);
@@ -812,7 +820,7 @@ describe("MatrixClient", function () {
it("should not get (unstable) file trees with invalid create contents", async () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "join",
getMyMembership: () => KnownMembership.Join,
currentState: {
getStateEvents: (eventType, stateKey) => {
/* eslint-disable jest/no-conditional-expect */
@@ -849,7 +857,7 @@ describe("MatrixClient", function () {
it("should not get (unstable) file trees with invalid purpose/subtype contents", async () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "join",
getMyMembership: () => KnownMembership.Join,
currentState: {
getStateEvents: (eventType, stateKey) => {
/* eslint-disable jest/no-conditional-expect */
@@ -1309,7 +1317,7 @@ describe("MatrixClient", function () {
describe("redactEvent", () => {
const roomId = "!room:example.org";
const mockRoom = {
getMyMembership: () => "join",
getMyMembership: () => KnownMembership.Join,
currentState: {
getStateEvents: (eventType, stateKey) => {
if (eventType === EventType.RoomEncryption) {
@@ -1448,7 +1456,7 @@ describe("MatrixClient", function () {
const txnId = "m12345";
const mockRoom = {
getMyMembership: () => "join",
getMyMembership: () => KnownMembership.Join,
updatePendingEvent: (event: MatrixEvent, status: EventStatus) => event.setStatus(status),
hasEncryptionStateEvent: jest.fn().mockReturnValue(true),
} as unknown as Room;
@@ -1526,8 +1534,6 @@ describe("MatrixClient", function () {
{ startOpts: {}, hasThreadSupport: false },
{ startOpts: { threadSupport: true }, hasThreadSupport: true },
{ startOpts: { threadSupport: false }, hasThreadSupport: false },
{ startOpts: { experimentalThreadSupport: true }, hasThreadSupport: true },
{ startOpts: { experimentalThreadSupport: true, threadSupport: false }, hasThreadSupport: false },
])("enabled thread support for the SDK instance", async ({ startOpts, hasThreadSupport }) => {
await client.startClient(startOpts);
expect(client.supportsThreads()).toBe(hasThreadSupport);
@@ -1554,7 +1560,6 @@ describe("MatrixClient", function () {
},
},
event_id: "$ev1",
user_id: "@alice:matrix.org",
});
expect(rootEvent.isThreadRoot).toBe(true);
@@ -2081,10 +2086,10 @@ describe("MatrixClient", function () {
await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
// Add a rule in the new source room.
await client.sendStateEvent(NEW_SOURCE_ROOM_ID, PolicyScope.User, {
await client.sendStateEvent(NEW_SOURCE_ROOM_ID, EventType.PolicyRuleUser, {
entity: "*:example.org",
reason: "just a test",
recommendation: "m.ban",
recommendation: PolicyRecommendation.Ban,
});
// We should reject this invite.
@@ -2171,8 +2176,8 @@ describe("MatrixClient", function () {
// Check where it shows up.
const targetRoomId = ignoreInvites2.target;
const targetRoom = client.getRoom(targetRoomId) as WrappedRoom;
expect(targetRoom._state.get(PolicyScope.User)[eventId]).toBeTruthy();
expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy();
expect(targetRoom._state.get(EventType.PolicyRuleUser)[eventId]).toBeTruthy();
expect(newSourceRoom._state.get(EventType.PolicyRuleUser)?.[eventId]).toBeFalsy();
});
});
+2 -1
View File
@@ -15,6 +15,7 @@ limitations under the License.
*/
import { EventTimeline, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
import { KnownMembership } from "../../../src/@types/membership";
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
@@ -76,7 +77,7 @@ describe("MatrixRTCSession", () => {
it("ignores memberships events of members not in the room", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
mockRoom.hasMembershipState = (state) => state === "join";
mockRoom.hasMembershipState = (state) => state === KnownMembership.Join;
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess?.memberships.length).toEqual(0);
});
+17 -18
View File
@@ -16,11 +16,12 @@ limitations under the License.
import { IContent, MatrixClient, MatrixEvent } from "../../../src";
import { Room } from "../../../src/models/room";
import { IEncryptedFile, RelationType, UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
import { RelationType, UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
import { EventTimelineSet } from "../../../src/models/event-timeline-set";
import { EventTimeline } from "../../../src/models/event-timeline";
import { MSC3089Branch } from "../../../src/models/MSC3089Branch";
import { MSC3089TreeSpace } from "../../../src/models/MSC3089TreeSpace";
import { EncryptedFile } from "../../../src/@types/media";
describe("MSC3089Branch", () => {
let client: MatrixClient;
@@ -254,7 +255,7 @@ describe("MSC3089Branch", () => {
it("should create new versions of itself", async () => {
const canaryName = "canary";
const canaryContents = "contents go here";
const canaryFile = {} as IEncryptedFile;
const canaryFile = {} as EncryptedFile;
const canaryAddl = { canary: true };
indexEvent.getContent = () => ({ active: true, retained: true });
const stateKeyOrder = [fileEventId2, fileEventId];
@@ -287,23 +288,21 @@ describe("MSC3089Branch", () => {
const createFn = jest
.fn()
.mockImplementation(
(name: string, contents: ArrayBuffer, info: Partial<IEncryptedFile>, addl: IContent) => {
expect(name).toEqual(canaryName);
expect(contents).toBe(canaryContents);
expect(info).toBe(canaryFile);
expect(addl).toMatchObject({
...canaryAddl,
"m.new_content": true,
"m.relates_to": {
rel_type: RelationType.Replace,
event_id: fileEventId,
},
});
.mockImplementation((name: string, contents: ArrayBuffer, info: Partial<EncryptedFile>, addl: IContent) => {
expect(name).toEqual(canaryName);
expect(contents).toBe(canaryContents);
expect(info).toBe(canaryFile);
expect(addl).toMatchObject({
...canaryAddl,
"m.new_content": true,
"m.relates_to": {
rel_type: RelationType.Replace,
event_id: fileEventId,
},
});
return Promise.resolve({ event_id: fileEventId2 });
},
);
return Promise.resolve({ event_id: fileEventId2 });
});
directory.createFile = createFn;
await branch.createNewVersion(canaryName, canaryContents, canaryFile, canaryAddl);
+10 -8
View File
@@ -25,6 +25,8 @@ import {
} from "../../../src/models/MSC3089TreeSpace";
import { DEFAULT_ALPHABET } from "../../../src/utils";
import { MatrixError } from "../../../src/http-api";
import { KnownMembership } from "../../../src/@types/membership";
import { EncryptedFile } from "../../../src/@types/media";
describe("MSC3089TreeSpace", () => {
let client: MatrixClient;
@@ -458,14 +460,14 @@ describe("MSC3089TreeSpace", () => {
expect(stateKey).toBeUndefined();
return [
// Partial implementations
{ getContent: () => ({ membership: "join" }), getStateKey: () => joinMemberId },
{ getContent: () => ({ membership: "knock" }), getStateKey: () => knockMemberId },
{ getContent: () => ({ membership: "invite" }), getStateKey: () => inviteMemberId },
{ getContent: () => ({ membership: "leave" }), getStateKey: () => leaveMemberId },
{ getContent: () => ({ membership: "ban" }), getStateKey: () => banMemberId },
{ getContent: () => ({ membership: KnownMembership.Join }), getStateKey: () => joinMemberId },
{ getContent: () => ({ membership: KnownMembership.Knock }), getStateKey: () => knockMemberId },
{ getContent: () => ({ membership: KnownMembership.Invite }), getStateKey: () => inviteMemberId },
{ getContent: () => ({ membership: KnownMembership.Leave }), getStateKey: () => leaveMemberId },
{ getContent: () => ({ membership: KnownMembership.Ban }), getStateKey: () => banMemberId },
// ensure we don't kick ourselves
{ getContent: () => ({ membership: "join" }), getStateKey: () => selfUserId },
{ getContent: () => ({ membership: KnownMembership.Join }), getStateKey: () => selfUserId },
];
},
};
@@ -946,7 +948,7 @@ describe("MSC3089TreeSpace", () => {
const fileInfo = {
mimetype: "text/plain",
// other fields as required by encryption, but ignored here
};
} as unknown as EncryptedFile;
const fileEventId = "$file";
const fileName = "My File.txt";
const fileContents = "This is a test file";
@@ -1006,7 +1008,7 @@ describe("MSC3089TreeSpace", () => {
const fileInfo = {
mimetype: "text/plain",
// other fields as required by encryption, but ignored here
};
} as unknown as EncryptedFile;
const fileEventId = "$file";
const fileName = "My File.txt";
const fileContents = "This is a test file";
+37 -49
View File
@@ -27,6 +27,8 @@ import {
THREAD_RELATION_TYPE,
TweakName,
} from "../../../src";
import { DecryptionFailureCode } from "../../../src/crypto-api";
import { DecryptionError } from "../../../src/common-crypto/CryptoBackend";
describe("MatrixEvent", () => {
it("should create copies of itself", () => {
@@ -360,20 +362,50 @@ describe("MatrixEvent", () => {
});
});
it("should report decryption errors", async () => {
it("should report unknown decryption errors", async () => {
const decryptionListener = jest.fn();
encryptedEvent.addListener(MatrixEventEvent.Decrypted, decryptionListener);
const testError = new Error("test error");
const crypto = {
decryptEvent: jest.fn().mockRejectedValue(new Error("test error")),
decryptEvent: jest.fn().mockRejectedValue(testError),
} as unknown as Crypto;
await encryptedEvent.attemptDecryption(crypto);
expect(encryptedEvent.isEncrypted()).toBeTruthy();
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
expect(encryptedEvent.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_ERROR);
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeFalsy();
expect(encryptedEvent.getContent()).toEqual({
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: Error: test error **",
});
expect(decryptionListener).toHaveBeenCalledWith(encryptedEvent, testError);
});
it("should report known decryption errors", async () => {
const decryptionListener = jest.fn();
encryptedEvent.addListener(MatrixEventEvent.Decrypted, decryptionListener);
const testError = new DecryptionError(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID, "uisi");
const crypto = {
decryptEvent: jest.fn().mockRejectedValue(testError),
} as unknown as Crypto;
await encryptedEvent.attemptDecryption(crypto);
expect(encryptedEvent.isEncrypted()).toBeTruthy();
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
expect(encryptedEvent.decryptionFailureReason).toEqual(
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
);
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeFalsy();
expect(encryptedEvent.getContent()).toEqual({
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: DecryptionError: uisi **",
});
expect(decryptionListener).toHaveBeenCalledWith(encryptedEvent, testError);
});
it(`should report "DecryptionError: The sender has disabled encrypting to unverified devices."`, async () => {
@@ -423,6 +455,8 @@ describe("MatrixEvent", () => {
expect(eventAttemptDecryptionSpy).toHaveBeenCalledTimes(2);
expect(crypto.decryptEvent).toHaveBeenCalledTimes(2);
expect(encryptedEvent.getType()).toEqual("m.room.message");
expect(encryptedEvent.isDecryptionFailure()).toBe(false);
expect(encryptedEvent.decryptionFailureReason).toBe(null);
});
});
@@ -469,52 +503,6 @@ describe("MatrixEvent", () => {
default: false,
enabled: true,
} as IAnnotatedPushRule;
describe("setPushActions()", () => {
it("sets actions on event", () => {
const actions = { notify: false, tweaks: {} };
const event = new MatrixEvent({
type: "com.example.test",
content: {
isTest: true,
},
});
event.setPushActions(actions);
expect(event.getPushActions()).toBe(actions);
});
it("sets actions to undefined", () => {
const event = new MatrixEvent({
type: "com.example.test",
content: {
isTest: true,
},
});
event.setPushActions(null);
// undefined is set on state
expect(event.getPushDetails().actions).toBe(undefined);
// but pushActions getter returns null when falsy
expect(event.getPushActions()).toBe(null);
});
it("clears existing push rule", () => {
const prevActions = { notify: true, tweaks: { highlight: true } };
const actions = { notify: false, tweaks: {} };
const event = new MatrixEvent({
type: "com.example.test",
content: {
isTest: true,
},
});
event.setPushDetails(prevActions, pushRule);
event.setPushActions(actions);
// rule is not in event push cache
expect(event.getPushDetails()).toEqual({ actions });
});
});
describe("setPushDetails()", () => {
it("sets actions and rule on event", () => {
@@ -543,7 +531,7 @@ describe("MatrixEvent", () => {
});
event.setPushDetails(prevActions, pushRule);
event.setPushActions(actions);
event.setPushDetails(actions);
// rule is not in event push cache
expect(event.getPushDetails()).toEqual({ actions });
+6 -3
View File
@@ -46,8 +46,8 @@ const realSubtleCrypto = crypto.subtleCrypto;
describe("oidc authorization", () => {
const delegatedAuthConfig = makeDelegatedAuthConfig();
const authorizationEndpoint = delegatedAuthConfig.metadata.authorization_endpoint;
const tokenEndpoint = delegatedAuthConfig.metadata.token_endpoint;
const authorizationEndpoint = delegatedAuthConfig.authorizationEndpoint;
const tokenEndpoint = delegatedAuthConfig.tokenEndpoint;
const clientId = "xyz789";
const baseUrl = "https://test.com";
@@ -58,7 +58,10 @@ describe("oidc authorization", () => {
jest.spyOn(logger, "warn");
jest.setSystemTime(now);
fetchMock.get(delegatedAuthConfig.issuer + ".well-known/openid-configuration", mockOpenIdConfiguration());
fetchMock.get(
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
mockOpenIdConfiguration(),
);
Object.defineProperty(window, "crypto", {
value: {
+33 -11
View File
@@ -18,10 +18,10 @@ import fetchMockJest from "fetch-mock-jest";
import { OidcError } from "../../../src/oidc/error";
import { OidcRegistrationClientMetadata, registerOidcClient } from "../../../src/oidc/register";
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
describe("registerOidcClient()", () => {
const issuer = "https://auth.com/";
const registrationEndpoint = "https://auth.com/register";
const clientName = "Element";
const baseUrl = "https://just.testing";
const metadata: OidcRegistrationClientMetadata = {
@@ -35,25 +35,20 @@ describe("registerOidcClient()", () => {
};
const dynamicClientId = "xyz789";
const delegatedAuthConfig = {
issuer,
registrationEndpoint,
authorizationEndpoint: issuer + "auth",
tokenEndpoint: issuer + "token",
};
const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
beforeEach(() => {
fetchMockJest.mockClear();
fetchMockJest.resetBehavior();
});
it("should make correct request to register client", async () => {
fetchMockJest.post(registrationEndpoint, {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
status: 200,
body: JSON.stringify({ client_id: dynamicClientId }),
});
expect(await registerOidcClient(delegatedAuthConfig, metadata)).toEqual(dynamicClientId);
expect(fetchMockJest).toHaveBeenCalledWith(
registrationEndpoint,
delegatedAuthConfig.registrationEndpoint!,
expect.objectContaining({
headers: {
"Accept": "application/json",
@@ -77,7 +72,7 @@ describe("registerOidcClient()", () => {
});
it("should throw when registration request fails", async () => {
fetchMockJest.post(registrationEndpoint, {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
status: 500,
});
await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow(
@@ -86,7 +81,7 @@ describe("registerOidcClient()", () => {
});
it("should throw when registration response is invalid", async () => {
fetchMockJest.post(registrationEndpoint, {
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
status: 200,
// no clientId in response
body: "{}",
@@ -95,4 +90,31 @@ describe("registerOidcClient()", () => {
OidcError.DynamicRegistrationInvalid,
);
});
it("should throw when required endpoints are unavailable", async () => {
await expect(() =>
registerOidcClient(
{
...delegatedAuthConfig,
registrationEndpoint: undefined,
},
metadata,
),
).rejects.toThrow(OidcError.DynamicRegistrationNotSupported);
});
it("should throw when required scopes are unavailable", async () => {
await expect(() =>
registerOidcClient(
{
...delegatedAuthConfig,
metadata: {
...delegatedAuthConfig.metadata,
grant_types_supported: [delegatedAuthConfig.metadata.grant_types_supported[0]],
},
},
metadata,
),
).rejects.toThrow(OidcError.DynamicRegistrationNotSupported);
});
});
+16 -16
View File
@@ -64,7 +64,7 @@ describe("OidcTokenRefresher", () => {
keys: [],
});
fetchMock.post(config.metadata.token_endpoint, {
fetchMock.post(config.tokenEndpoint, {
status: 200,
headers: {
"Content-Type": "application/json",
@@ -88,7 +88,7 @@ describe("OidcTokenRefresher", () => {
},
{ overwriteRoutes: true },
);
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await expect(refresher.oidcClientReady).rejects.toThrow();
expect(logger.error).toHaveBeenCalledWith(
"Failed to initialise OIDC client.",
@@ -98,7 +98,7 @@ describe("OidcTokenRefresher", () => {
});
it("initialises oidc client", async () => {
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
// @ts-ignore peek at private property to see we initialised the client correctly
@@ -114,19 +114,19 @@ describe("OidcTokenRefresher", () => {
describe("doRefreshAccessToken()", () => {
it("should throw when oidcClient has not been initialised", async () => {
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await expect(refresher.doRefreshAccessToken("token")).rejects.toThrow(
"Cannot get new token before OIDC client is initialised.",
);
});
it("should refresh the tokens", async () => {
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
const result = await refresher.doRefreshAccessToken("refresh-token");
expect(fetchMock).toHaveFetched(config.metadata.token_endpoint, {
expect(fetchMock).toHaveFetched(config.tokenEndpoint, {
method: "POST",
});
@@ -137,7 +137,7 @@ describe("OidcTokenRefresher", () => {
});
it("should persist the new tokens", async () => {
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
// spy on our stub
jest.spyOn(refresher, "persistTokens");
@@ -153,7 +153,7 @@ describe("OidcTokenRefresher", () => {
it("should only have one inflight refresh request at once", async () => {
fetchMock
.postOnce(
config.metadata.token_endpoint,
config.tokenEndpoint,
{
status: 200,
headers: {
@@ -164,7 +164,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: true },
)
.postOnce(
config.metadata.token_endpoint,
config.tokenEndpoint,
{
status: 200,
headers: {
@@ -175,7 +175,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: false },
);
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
// reset call counts
fetchMock.resetHistory();
@@ -188,7 +188,7 @@ describe("OidcTokenRefresher", () => {
const result2 = await first;
// only one call to token endpoint
expect(fetchMock).toHaveFetchedTimes(1, config.metadata.token_endpoint);
expect(fetchMock).toHaveFetchedTimes(1, config.tokenEndpoint);
expect(result1).toEqual({
accessToken: "first-new-access-token",
refreshToken: "first-new-refresh-token",
@@ -208,7 +208,7 @@ describe("OidcTokenRefresher", () => {
it("should log and rethrow when token refresh fails", async () => {
fetchMock.post(
config.metadata.token_endpoint,
config.tokenEndpoint,
{
status: 503,
headers: {
@@ -218,7 +218,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: true },
);
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
await expect(refresher.doRefreshAccessToken("refresh-token")).rejects.toThrow();
@@ -228,7 +228,7 @@ describe("OidcTokenRefresher", () => {
// make sure inflight request is cleared after a failure
fetchMock
.postOnce(
config.metadata.token_endpoint,
config.tokenEndpoint,
{
status: 503,
headers: {
@@ -238,7 +238,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: true },
)
.postOnce(
config.metadata.token_endpoint,
config.tokenEndpoint,
{
status: 200,
headers: {
@@ -249,7 +249,7 @@ describe("OidcTokenRefresher", () => {
{ overwriteRoutes: false },
);
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
const refresher = new OidcTokenRefresher(authConfig.issuer, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
// reset call counts
fetchMock.resetHistory();
+1 -94
View File
@@ -17,105 +17,12 @@ limitations under the License.
import { mocked } from "jest-mock";
import { jwtDecode } from "jwt-decode";
import { M_AUTHENTICATION } from "../../../src";
import { logger } from "../../../src/logger";
import {
validateIdToken,
validateOIDCIssuerWellKnown,
validateWellKnownAuthentication,
} from "../../../src/oidc/validate";
import { validateIdToken, validateOIDCIssuerWellKnown } from "../../../src/oidc/validate";
import { OidcError } from "../../../src/oidc/error";
jest.mock("jwt-decode");
describe("validateWellKnownAuthentication()", () => {
const baseWk = {
"m.homeserver": {
base_url: "https://hs.org",
},
};
it("should throw not supported error when wellKnown has no m.authentication section", () => {
expect(() => validateWellKnownAuthentication(undefined)).toThrow(OidcError.NotSupported);
});
it("should throw misconfigured error when authentication issuer is not a string", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: { url: "test.com" },
},
};
expect(() => validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!] as any)).toThrow(
OidcError.Misconfigured,
);
});
it("should throw misconfigured error when authentication account is not a string", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
account: { url: "test" },
},
};
expect(() => validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!] as any)).toThrow(
OidcError.Misconfigured,
);
});
it("should throw misconfigured error when authentication account is false", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
account: false,
},
};
expect(() => validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!] as any)).toThrow(
OidcError.Misconfigured,
);
});
it("should return valid config when wk uses stable m.authentication", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
account: "account.com",
},
};
expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({
issuer: "test.com",
account: "account.com",
});
});
it("should return valid config when m.authentication account is missing", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
},
};
expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({
issuer: "test.com",
});
});
it("should remove unexpected properties", () => {
const wk = {
...baseWk,
[M_AUTHENTICATION.stable!]: {
issuer: "test.com",
somethingElse: "test",
},
};
expect(validateWellKnownAuthentication(wk[M_AUTHENTICATION.stable!])).toEqual({
issuer: "test.com",
});
});
});
describe("validateOIDCIssuerWellKnown", () => {
const validWk: any = {
authorization_endpoint: "https://test.org/authorize",
+462 -157
View File
@@ -1,8 +1,35 @@
import * as utils from "../test-utils/test-utils";
import { IActionsObject, PushProcessor } from "../../src/pushprocessor";
import { ConditionKind, EventType, IContent, MatrixClient, MatrixEvent, PushRuleActionName, RuleId } from "../../src";
import {
ConditionKind,
EventType,
IContent,
IPushRule,
MatrixClient,
MatrixEvent,
PushRuleActionName,
RuleId,
TweakName,
} from "../../src";
import { mockClientMethodsUser } from "../test-utils/client";
const msc3914RoomCallRule: IPushRule = {
rule_id: ".org.matrix.msc3914.rule.room.call",
default: true,
enabled: true,
conditions: [
{
kind: ConditionKind.EventMatch,
key: "type",
pattern: "org.matrix.msc3401.call",
},
{
kind: ConditionKind.CallStarted,
},
],
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Sound, value: "default" }],
};
describe("NotificationService", function () {
const testUserId = "@ali:matrix.org";
const testDisplayName = "Alice M";
@@ -12,164 +39,150 @@ describe("NotificationService", function () {
let pushProcessor: PushProcessor;
const msc3914RoomCallRule = {
rule_id: ".org.matrix.msc3914.rule.room.call",
default: true,
enabled: true,
conditions: [
{
kind: "event_match",
key: "type",
pattern: "org.matrix.msc3401.call",
},
{
kind: "call_started",
},
],
actions: ["notify", { set_tweak: "sound", value: "default" }],
};
// These would be better if individual rules were configured in the tests themselves.
const matrixClient = {
getRoom: function () {
return {
currentState: {
getMember: function () {
return {
name: testDisplayName,
};
},
getJoinedMemberCount: function () {
return 0;
},
members: {},
},
};
},
...mockClientMethodsUser(testUserId),
supportsIntentionalMentions: () => true,
pushRules: {
device: {},
global: {
content: [
{
actions: [
"notify",
{
set_tweak: "sound",
value: "default",
},
{
set_tweak: "highlight",
},
],
enabled: true,
pattern: "ali",
rule_id: ".m.rule.contains_user_name",
},
{
actions: [
"notify",
{
set_tweak: "sound",
value: "default",
},
{
set_tweak: "highlight",
},
],
enabled: true,
pattern: "coffee",
rule_id: "coffee",
},
{
actions: [
"notify",
{
set_tweak: "sound",
value: "default",
},
{
set_tweak: "highlight",
},
],
enabled: true,
pattern: "foo*bar",
rule_id: "foobar",
},
],
override: [
{
actions: [
"notify",
{
set_tweak: "sound",
value: "default",
},
{
set_tweak: "highlight",
},
],
conditions: [
{
kind: "contains_display_name",
},
],
enabled: true,
rule_id: ".m.rule.contains_display_name",
},
{
actions: [
"notify",
{
set_tweak: "sound",
value: "default",
},
],
conditions: [
{
is: "2",
kind: "room_member_count",
},
],
enabled: true,
rule_id: ".m.rule.room_one_to_one",
},
],
room: [],
sender: [],
underride: [
msc3914RoomCallRule,
{
actions: ["dont-notify"],
conditions: [
{
key: "content.msgtype",
kind: "event_match",
pattern: "m.notice",
},
],
enabled: true,
rule_id: ".m.rule.suppress_notices",
},
{
actions: [
"notify",
{
set_tweak: "highlight",
value: false,
},
],
conditions: [],
enabled: true,
rule_id: ".m.rule.fallback",
},
],
},
},
} as unknown as MatrixClient;
let matrixClient: MatrixClient;
beforeEach(function () {
// These would be better if individual rules were configured in the tests themselves.
matrixClient = {
getRoom: function () {
return {
currentState: {
getMember: function () {
return {
name: testDisplayName,
};
},
getJoinedMemberCount: function () {
return 0;
},
members: {},
},
};
},
...mockClientMethodsUser(testUserId),
supportsIntentionalMentions: () => true,
pushRules: {
device: {},
global: {
content: [
{
actions: [
"notify",
{
set_tweak: "sound",
value: "default",
},
{
set_tweak: "highlight",
},
],
enabled: true,
pattern: "ali",
rule_id: ".m.rule.contains_user_name",
},
{
actions: [
"notify",
{
set_tweak: "sound",
value: "default",
},
{
set_tweak: "highlight",
},
],
enabled: true,
pattern: "coffee",
rule_id: "coffee",
},
{
actions: [
"notify",
{
set_tweak: "sound",
value: "default",
},
{
set_tweak: "highlight",
},
],
enabled: true,
pattern: "foo*bar",
rule_id: "foobar",
},
],
override: [
{
actions: [
"notify",
{
set_tweak: "sound",
value: "default",
},
{
set_tweak: "highlight",
},
],
conditions: [
{
kind: "contains_display_name",
},
],
enabled: true,
default: true,
rule_id: ".m.rule.contains_display_name",
},
{
actions: [
"notify",
{
set_tweak: "sound",
value: "default",
},
],
conditions: [
{
is: "2",
kind: "room_member_count",
},
],
enabled: true,
rule_id: ".m.rule.room_one_to_one",
},
],
room: [],
sender: [],
underride: [
msc3914RoomCallRule,
{
actions: ["dont-notify"],
conditions: [
{
key: "content.msgtype",
kind: "event_match",
pattern: "m.notice",
},
],
enabled: true,
rule_id: ".m.rule.suppress_notices",
},
{
actions: [
"notify",
{
set_tweak: "highlight",
value: false,
},
],
conditions: [],
enabled: true,
rule_id: ".m.rule.fallback",
},
],
},
},
} as unknown as MatrixClient;
testEvent = utils.mkEvent({
type: "m.room.message",
room: testRoomId,
@@ -699,3 +712,295 @@ describe("Test PushProcessor.partsForDottedKey", function () {
expect(PushProcessor.partsForDottedKey(path)).toStrictEqual(expected);
});
});
describe("rewriteDefaultRules", () => {
it("should add default rules in the correct order", () => {
const pushRules = PushProcessor.rewriteDefaultRules({
device: {},
global: {
content: [],
override: [
// Include user-defined push rules inbetween .m.rule.master and other default rules to assert they are maintained in-order.
{
rule_id: ".m.rule.master",
default: true,
enabled: false,
conditions: [],
actions: [],
},
{
actions: [
PushRuleActionName.Notify,
{
set_tweak: TweakName.Sound,
value: "default",
},
{
set_tweak: TweakName.Highlight,
},
],
enabled: true,
pattern: "coffee",
rule_id: "coffee",
default: false,
},
{
actions: [
PushRuleActionName.Notify,
{
set_tweak: TweakName.Sound,
value: "default",
},
{
set_tweak: TweakName.Highlight,
},
],
conditions: [
{
kind: ConditionKind.ContainsDisplayName,
},
],
enabled: true,
default: true,
rule_id: ".m.rule.contains_display_name",
},
{
actions: [
PushRuleActionName.Notify,
{
set_tweak: TweakName.Sound,
value: "default",
},
],
conditions: [
{
is: "2",
kind: ConditionKind.RoomMemberCount,
},
],
enabled: true,
rule_id: ".m.rule.room_one_to_one",
default: true,
},
],
room: [],
sender: [],
underride: [
{
actions: [
PushRuleActionName.Notify,
{
set_tweak: TweakName.Highlight,
value: false,
},
],
conditions: [],
enabled: true,
rule_id: "user-defined",
default: false,
},
msc3914RoomCallRule,
{
actions: [
PushRuleActionName.Notify,
{
set_tweak: TweakName.Highlight,
value: false,
},
],
conditions: [],
enabled: true,
rule_id: ".m.rule.fallback",
default: true,
},
],
},
});
// By the time we get here, we expect the PushProcessor to have merged the new .m.rule.is_room_mention rule into the existing list of rules.
// Check that has happened, and that it is in the right place.
const containsDisplayNameRuleIdx = pushRules.global.override?.findIndex(
(rule) => rule.rule_id === RuleId.ContainsDisplayName,
);
expect(containsDisplayNameRuleIdx).toBeGreaterThan(-1);
const isRoomMentionRuleIdx = pushRules.global.override?.findIndex(
(rule) => rule.rule_id === RuleId.IsRoomMention,
);
expect(isRoomMentionRuleIdx).toBeGreaterThan(-1);
const mReactionRuleIdx = pushRules.global.override?.findIndex((rule) => rule.rule_id === ".m.rule.reaction");
expect(mReactionRuleIdx).toBeGreaterThan(-1);
expect(containsDisplayNameRuleIdx).toBeLessThan(isRoomMentionRuleIdx!);
expect(isRoomMentionRuleIdx).toBeLessThan(mReactionRuleIdx!);
expect(pushRules.global.override?.map((r) => r.rule_id)).toEqual([
".m.rule.master",
"coffee",
".m.rule.contains_display_name",
".m.rule.room_one_to_one",
".m.rule.is_room_mention",
".m.rule.reaction",
".org.matrix.msc3786.rule.room.server_acl",
]);
expect(pushRules.global.underride?.map((r) => r.rule_id)).toEqual([
"user-defined",
".org.matrix.msc3914.rule.room.call",
// Assert that unknown default rules are maintained
".m.rule.fallback",
]);
});
it("should add missing msc3914 rule in correct place", () => {
const pushRules = PushProcessor.rewriteDefaultRules({
device: {},
global: {
// Sample push rules from a Synapse user.
// Note that rules 2 and 3 are backwards, this will trigger a warning in the console.
underride: [
{
conditions: [
{
kind: "event_match",
key: "type",
pattern: "m.call.invite",
},
],
actions: [
"notify",
{
set_tweak: "sound",
value: "ring",
},
{
set_tweak: "highlight",
value: false,
},
],
rule_id: ".m.rule.call",
default: true,
enabled: true,
},
{
conditions: [
{
kind: "event_match",
key: "type",
pattern: "m.room.message",
},
{
kind: "room_member_count",
is: "2",
},
],
actions: [
"notify",
{
set_tweak: "sound",
value: "TEST1",
},
{
set_tweak: "highlight",
value: false,
},
],
rule_id: ".m.rule.room_one_to_one",
default: true,
enabled: true,
},
{
conditions: [
{
kind: "event_match",
key: "type",
pattern: "m.room.encrypted",
},
{
kind: "room_member_count",
is: "2",
},
],
actions: [
"notify",
{
set_tweak: "sound",
value: "TEST2",
},
{
set_tweak: "highlight",
value: false,
},
],
rule_id: ".m.rule.encrypted_room_one_to_one",
default: true,
enabled: true,
},
{
conditions: [
{
kind: "event_match",
key: "type",
pattern: "m.room.message",
},
],
actions: ["dont_notify"],
rule_id: ".m.rule.message",
default: true,
enabled: true,
},
{
conditions: [
{
kind: "event_match",
key: "type",
pattern: "m.room.encrypted",
},
],
actions: ["dont_notify"],
rule_id: ".m.rule.encrypted",
default: true,
enabled: true,
},
{
conditions: [
{
kind: "event_match",
key: "type",
pattern: "im.vector.modular.widgets",
},
{
kind: "event_match",
key: "content.type",
pattern: "jitsi",
},
{
kind: "event_match",
key: "state_key",
pattern: "*",
},
],
actions: [
"notify",
{
set_tweak: "highlight",
value: false,
},
],
rule_id: ".im.vector.jitsi",
default: true,
enabled: true,
},
] as IPushRule[],
},
});
expect(pushRules.global.underride?.map((r) => r.rule_id)).toEqual([
".m.rule.call",
".org.matrix.msc3914.rule.room.call",
".m.rule.room_one_to_one",
".m.rule.encrypted_room_one_to_one",
".m.rule.message",
".m.rule.encrypted",
".im.vector.jitsi",
]);
});
});
+22 -21
View File
@@ -27,6 +27,7 @@ import {
UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS,
UNSTABLE_MSC2666_SHARED_ROOMS,
} from "../../src";
import { KnownMembership } from "../../src/@types/membership";
describe("RoomMember", function () {
const roomId = "!foo:bar";
@@ -50,7 +51,7 @@ describe("RoomMember", function () {
room: roomId,
user: userA,
content: {
membership: "join",
membership: KnownMembership.Join,
avatar_url: "mxc://flibble/wibble",
},
});
@@ -282,11 +283,11 @@ describe("RoomMember", function () {
describe("isKicked", () => {
it("should return false if membership is not `leave`", () => {
const member1 = new RoomMember(roomId, userA);
member1.membership = "join";
member1.membership = KnownMembership.Join;
expect(member1.isKicked()).toBeFalsy();
const member2 = new RoomMember(roomId, userA);
member2.membership = "invite";
member2.membership = KnownMembership.Invite;
expect(member2.isKicked()).toBeFalsy();
const member3 = new RoomMember(roomId, userA);
@@ -295,17 +296,17 @@ describe("RoomMember", function () {
it("should return false if the membership event is unknown", () => {
const member = new RoomMember(roomId, userA);
member.membership = "leave";
member.membership = KnownMembership.Leave;
expect(member.isKicked()).toBeFalsy();
});
it("should return false if the member left of their own accord", () => {
const member = new RoomMember(roomId, userA);
member.membership = "leave";
member.membership = KnownMembership.Leave;
member.events.member = utils.mkMembership({
event: true,
sender: userA,
mship: "leave",
mship: KnownMembership.Leave,
skey: userA,
});
expect(member.isKicked()).toBeFalsy();
@@ -313,11 +314,11 @@ describe("RoomMember", function () {
it("should return true if the member's leave was sent by another user", () => {
const member = new RoomMember(roomId, userA);
member.membership = "leave";
member.membership = KnownMembership.Leave;
member.events.member = utils.mkMembership({
event: true,
sender: userB,
mship: "leave",
mship: KnownMembership.Leave,
skey: userA,
});
expect(member.isKicked()).toBeTruthy();
@@ -327,11 +328,11 @@ describe("RoomMember", function () {
describe("getDMInviter", () => {
it("should return userId of the sender of the invite if is_direct=true", () => {
const member = new RoomMember(roomId, userA);
member.membership = "invite";
member.membership = KnownMembership.Invite;
member.events.member = utils.mkMembership({
event: true,
sender: userB,
mship: "invite",
mship: KnownMembership.Invite,
skey: userA,
});
member.events.member.event.content!.is_direct = true;
@@ -340,11 +341,11 @@ describe("RoomMember", function () {
it("should not return userId of the sender of the invite if is_direct=false", () => {
const member = new RoomMember(roomId, userA);
member.membership = "invite";
member.membership = KnownMembership.Invite;
member.events.member = utils.mkMembership({
event: true,
sender: userB,
mship: "invite",
mship: KnownMembership.Invite,
skey: userA,
});
member.events.member.event.content!.is_direct = false;
@@ -355,7 +356,7 @@ describe("RoomMember", function () {
describe("setMembershipEvent", function () {
const joinEvent = utils.mkMembership({
event: true,
mship: "join",
mship: KnownMembership.Join,
user: userA,
room: roomId,
name: "Alice",
@@ -363,7 +364,7 @@ describe("RoomMember", function () {
const inviteEvent = utils.mkMembership({
event: true,
mship: "invite",
mship: KnownMembership.Invite,
user: userB,
skey: userA,
room: roomId,
@@ -371,10 +372,10 @@ describe("RoomMember", function () {
it("should set 'membership' and assign the event to 'events.member'.", function () {
member.setMembershipEvent(inviteEvent);
expect(member.membership).toEqual("invite");
expect(member.membership).toEqual(KnownMembership.Invite);
expect(member.events.member).toEqual(inviteEvent);
member.setMembershipEvent(joinEvent);
expect(member.membership).toEqual("join");
expect(member.membership).toEqual(KnownMembership.Join);
expect(member.events.member).toEqual(joinEvent);
});
@@ -387,13 +388,13 @@ describe("RoomMember", function () {
return [
utils.mkMembership({
event: true,
mship: "join",
mship: KnownMembership.Join,
room: roomId,
user: userB,
}),
utils.mkMembership({
event: true,
mship: "join",
mship: KnownMembership.Join,
room: roomId,
user: userC,
name: "Alice",
@@ -443,7 +444,7 @@ describe("RoomMember", function () {
it("should set 'name' to user_id if it is just whitespace", function () {
const joinEvent = utils.mkMembership({
event: true,
mship: "join",
mship: KnownMembership.Join,
user: userA,
room: roomId,
name: " \u200b ",
@@ -457,7 +458,7 @@ describe("RoomMember", function () {
it("should disambiguate users on a fuzzy displayname match", function () {
const joinEvent = utils.mkMembership({
event: true,
mship: "join",
mship: KnownMembership.Join,
user: userA,
room: roomId,
name: "Alíce\u200b", // note diacritic and zero width char
@@ -471,7 +472,7 @@ describe("RoomMember", function () {
return [
utils.mkMembership({
event: true,
mship: "join",
mship: KnownMembership.Join,
room: roomId,
user: userC,
name: "Alice",
+67 -39
View File
@@ -25,9 +25,11 @@ import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@typ
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { M_BEACON } from "../../src/@types/beacon";
import { MatrixClient } from "../../src/client";
import { DecryptionError } from "../../src/crypto/algorithms";
import { defer } from "../../src/utils";
import { Room } from "../../src/models/room";
import { KnownMembership } from "../../src/@types/membership";
import { DecryptionFailureCode } from "../../src/crypto-api";
import { DecryptionError } from "../../src/common-crypto/CryptoBackend";
describe("RoomState", function () {
const roomId = "!foo:bar";
@@ -44,14 +46,14 @@ describe("RoomState", function () {
utils.mkMembership({
// userA joined
event: true,
mship: "join",
mship: KnownMembership.Join,
user: userA,
room: roomId,
}),
utils.mkMembership({
// userB joined
event: true,
mship: "join",
mship: KnownMembership.Join,
user: userB,
room: roomId,
}),
@@ -102,20 +104,20 @@ describe("RoomState", function () {
it("should return a member which changes as state changes", function () {
const member = state.getMember(userB);
expect(member?.membership).toEqual("join");
expect(member?.membership).toEqual(KnownMembership.Join);
expect(member?.name).toEqual(userB);
state.setStateEvents([
utils.mkMembership({
room: roomId,
user: userB,
mship: "leave",
mship: KnownMembership.Leave,
event: true,
name: "BobGone",
}),
]);
expect(member?.membership).toEqual("leave");
expect(member?.membership).toEqual(KnownMembership.Leave);
expect(member?.name).toEqual("BobGone");
});
});
@@ -131,17 +133,17 @@ describe("RoomState", function () {
utils.mkMembership({
room: roomId,
user: userA,
mship: "leave",
mship: KnownMembership.Leave,
event: true,
name: "AliceIsGone",
}),
]);
const postLeaveUser = state.getSentinelMember(userA);
expect(preLeaveUser?.membership).toEqual("join");
expect(preLeaveUser?.membership).toEqual(KnownMembership.Join);
expect(preLeaveUser?.name).toEqual(userA);
expect(postLeaveUser?.membership).toEqual("leave");
expect(postLeaveUser?.membership).toEqual(KnownMembership.Leave);
expect(postLeaveUser?.name).toEqual("AliceIsGone");
});
});
@@ -166,7 +168,7 @@ describe("RoomState", function () {
it("should return a single MatrixEvent if a state_key was specified", function () {
const event = state.getStateEvents("m.room.member", userA);
expect(event?.getContent()).toMatchObject({
membership: "join",
membership: KnownMembership.Join,
});
});
});
@@ -176,13 +178,13 @@ describe("RoomState", function () {
const memberEvents = [
utils.mkMembership({
user: "@cleo:bar",
mship: "invite",
mship: KnownMembership.Invite,
room: roomId,
event: true,
}),
utils.mkMembership({
user: "@daisy:bar",
mship: "join",
mship: KnownMembership.Join,
room: roomId,
event: true,
}),
@@ -202,13 +204,13 @@ describe("RoomState", function () {
const memberEvents = [
utils.mkMembership({
user: "@cleo:bar",
mship: "invite",
mship: KnownMembership.Invite,
room: roomId,
event: true,
}),
utils.mkMembership({
user: "@daisy:bar",
mship: "join",
mship: KnownMembership.Join,
room: roomId,
event: true,
}),
@@ -228,7 +230,7 @@ describe("RoomState", function () {
const events = [
utils.mkMembership({
user: "@cleo:bar",
mship: "invite",
mship: KnownMembership.Invite,
room: roomId,
event: true,
}),
@@ -282,7 +284,7 @@ describe("RoomState", function () {
it("should call setPowerLevelEvent on a new RoomMember if power levels exist", function () {
const memberEvent = utils.mkMembership({
mship: "join",
mship: KnownMembership.Join,
user: userC,
room: roomId,
event: true,
@@ -312,7 +314,7 @@ describe("RoomState", function () {
it("should call setMembershipEvent on the right RoomMember", function () {
const memberEvent = utils.mkMembership({
user: userB,
mship: "leave",
mship: KnownMembership.Leave,
room: roomId,
event: true,
});
@@ -447,7 +449,7 @@ describe("RoomState", function () {
it("should add a new member", function () {
const oobMemberEvent = utils.mkMembership({
user: userLazy,
mship: "join",
mship: KnownMembership.Join,
room: roomId,
event: true,
});
@@ -462,7 +464,7 @@ describe("RoomState", function () {
state.setOutOfBandMembers([
utils.mkMembership({
user: userLazy,
mship: "join",
mship: KnownMembership.Join,
room: roomId,
event: true,
}),
@@ -474,7 +476,7 @@ describe("RoomState", function () {
const userLazy = "@oob:hs";
const oobMemberEvent = utils.mkMembership({
user: userLazy,
mship: "join",
mship: KnownMembership.Join,
room: roomId,
event: true,
});
@@ -491,7 +493,7 @@ describe("RoomState", function () {
it("should never overwrite existing members", function () {
const oobMemberEvent = utils.mkMembership({
user: userA,
mship: "join",
mship: KnownMembership.Join,
room: roomId,
event: true,
});
@@ -506,7 +508,7 @@ describe("RoomState", function () {
const doesntExistYetUserId = "@doesntexistyet:hs";
const oobMemberEvent = utils.mkMembership({
user: doesntExistYetUserId,
mship: "join",
mship: KnownMembership.Join,
room: roomId,
event: true,
});
@@ -529,7 +531,7 @@ describe("RoomState", function () {
state.setOutOfBandMembers([
utils.mkMembership({
user: userLazy,
mship: "join",
mship: KnownMembership.Join,
room: roomId,
event: true,
}),
@@ -554,7 +556,7 @@ describe("RoomState", function () {
copy.setOutOfBandMembers([
utils.mkMembership({
user: userA,
mship: "join",
mship: KnownMembership.Join,
room: roomId,
event: true,
}),
@@ -563,7 +565,7 @@ describe("RoomState", function () {
state.setOutOfBandMembers([
utils.mkMembership({
user: userLazy,
mship: "join",
mship: KnownMembership.Join,
room: roomId,
event: true,
}),
@@ -576,7 +578,7 @@ describe("RoomState", function () {
copy.setStateEvents([
utils.mkMembership({
user: userLazy,
mship: "join",
mship: KnownMembership.Join,
room: roomId,
event: true,
}),
@@ -697,9 +699,13 @@ describe("RoomState", function () {
});
it("should update after adding joined member", function () {
state.setStateEvents([utils.mkMembership({ event: true, mship: "join", user: userA, room: roomId })]);
state.setStateEvents([
utils.mkMembership({ event: true, mship: KnownMembership.Join, user: userA, room: roomId }),
]);
expect(state.getJoinedMemberCount()).toEqual(1);
state.setStateEvents([utils.mkMembership({ event: true, mship: "join", user: userC, room: roomId })]);
state.setStateEvents([
utils.mkMembership({ event: true, mship: KnownMembership.Join, user: userC, room: roomId }),
]);
expect(state.getJoinedMemberCount()).toEqual(2);
});
});
@@ -710,9 +716,13 @@ describe("RoomState", function () {
});
it("should update after adding invited member", function () {
state.setStateEvents([utils.mkMembership({ event: true, mship: "invite", user: userA, room: roomId })]);
state.setStateEvents([
utils.mkMembership({ event: true, mship: KnownMembership.Invite, user: userA, room: roomId }),
]);
expect(state.getInvitedMemberCount()).toEqual(1);
state.setStateEvents([utils.mkMembership({ event: true, mship: "invite", user: userC, room: roomId })]);
state.setStateEvents([
utils.mkMembership({ event: true, mship: KnownMembership.Invite, user: userC, room: roomId }),
]);
expect(state.getInvitedMemberCount()).toEqual(2);
});
});
@@ -723,19 +733,27 @@ describe("RoomState", function () {
});
it("should, once used, override counting members from state", function () {
state.setStateEvents([utils.mkMembership({ event: true, mship: "join", user: userA, room: roomId })]);
state.setStateEvents([
utils.mkMembership({ event: true, mship: KnownMembership.Join, user: userA, room: roomId }),
]);
expect(state.getJoinedMemberCount()).toEqual(1);
state.setJoinedMemberCount(100);
expect(state.getJoinedMemberCount()).toEqual(100);
state.setStateEvents([utils.mkMembership({ event: true, mship: "join", user: userC, room: roomId })]);
state.setStateEvents([
utils.mkMembership({ event: true, mship: KnownMembership.Join, user: userC, room: roomId }),
]);
expect(state.getJoinedMemberCount()).toEqual(100);
});
it("should, once used, override counting members from state, " + "also after clone", function () {
state.setStateEvents([utils.mkMembership({ event: true, mship: "join", user: userA, room: roomId })]);
state.setStateEvents([
utils.mkMembership({ event: true, mship: KnownMembership.Join, user: userA, room: roomId }),
]);
state.setJoinedMemberCount(100);
const copy = state.clone();
copy.setStateEvents([utils.mkMembership({ event: true, mship: "join", user: userC, room: roomId })]);
copy.setStateEvents([
utils.mkMembership({ event: true, mship: KnownMembership.Join, user: userC, room: roomId }),
]);
expect(state.getJoinedMemberCount()).toEqual(100);
});
});
@@ -746,19 +764,27 @@ describe("RoomState", function () {
});
it("should, once used, override counting members from state", function () {
state.setStateEvents([utils.mkMembership({ event: true, mship: "invite", user: userB, room: roomId })]);
state.setStateEvents([
utils.mkMembership({ event: true, mship: KnownMembership.Invite, user: userB, room: roomId }),
]);
expect(state.getInvitedMemberCount()).toEqual(1);
state.setInvitedMemberCount(100);
expect(state.getInvitedMemberCount()).toEqual(100);
state.setStateEvents([utils.mkMembership({ event: true, mship: "invite", user: userC, room: roomId })]);
state.setStateEvents([
utils.mkMembership({ event: true, mship: KnownMembership.Invite, user: userC, room: roomId }),
]);
expect(state.getInvitedMemberCount()).toEqual(100);
});
it("should, once used, override counting members from state, " + "also after clone", function () {
state.setStateEvents([utils.mkMembership({ event: true, mship: "invite", user: userB, room: roomId })]);
state.setStateEvents([
utils.mkMembership({ event: true, mship: KnownMembership.Invite, user: userB, room: roomId }),
]);
state.setInvitedMemberCount(100);
const copy = state.clone();
copy.setStateEvents([utils.mkMembership({ event: true, mship: "invite", user: userC, room: roomId })]);
copy.setStateEvents([
utils.mkMembership({ event: true, mship: KnownMembership.Invite, user: userC, room: roomId }),
]);
expect(state.getInvitedMemberCount()).toEqual(100);
});
});
@@ -1015,7 +1041,9 @@ describe("RoomState", function () {
content: beacon1RelationContent,
});
jest.spyOn(failedDecryptionRelatedEvent, "isDecryptionFailure").mockReturnValue(true);
mockClient.decryptEventIfNeeded.mockRejectedValue(new DecryptionError("ERR", "msg"));
mockClient.decryptEventIfNeeded.mockRejectedValue(
new DecryptionError(DecryptionFailureCode.UNKNOWN_ERROR, "msg"),
);
// spy on event.once
const eventOnceSpy = jest.spyOn(failedDecryptionRelatedEvent, "once");
+472 -368
View File
File diff suppressed because it is too large Load Diff
@@ -22,13 +22,15 @@ import {
KeysClaimRequest,
KeysQueryRequest,
KeysUploadRequest,
PutDehydratedDeviceRequest,
RoomMessageRequest,
SignatureUploadRequest,
UploadSigningKeysRequest,
ToDeviceRequest,
} from "@matrix-org/matrix-sdk-crypto-wasm";
import fetchMock from "fetch-mock-jest";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
import { TypedEventEmitter } from "../../../src";
import { HttpApiEvent, HttpApiEventHandlerMap, IHttpOpts, MatrixHttpApi, UIAuthCallback } from "../../../src";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import { defer } from "../../../src/utils";
@@ -232,6 +234,35 @@ describe("OutgoingRequestProcessor", () => {
httpBackend.verifyNoOutstandingRequests();
});
it("should handle PutDehydratedDeviceRequest", async () => {
// first, mock up a request as we might expect to receive it from the Rust layer ...
const testReq = { foo: "bar" };
const outgoingRequest = new PutDehydratedDeviceRequest(JSON.stringify(testReq));
// ... then poke the request into the OutgoingRequestProcessor under test
const reqProm = processor.makeOutgoingRequest(outgoingRequest);
// Now: check that it makes a matching HTTP request.
const testResponse = '{"result":1}';
httpBackend
.when("PUT", "/_matrix")
.check((req) => {
expect(req.path).toEqual(
"https://example.com/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
);
expect(JSON.parse(req.rawData)).toEqual(testReq);
expect(req.headers["Accept"]).toEqual("application/json");
expect(req.headers["Content-Type"]).toEqual("application/json");
})
.respond(200, testResponse, true);
// PutDehydratedDeviceRequest does not need to be marked as sent, so no call to OlmMachine.markAsSent is expected.
await httpBackend.flushAllExpected();
await reqProm;
httpBackend.verifyNoOutstandingRequests();
});
it("does not explode with unknown requests", async () => {
const outgoingRequest = { id: "5678", type: 987 };
const markSentCallPromise = awaitCallToMarkAsSent();
@@ -274,4 +305,302 @@ describe("OutgoingRequestProcessor", () => {
// ... and `makeOutgoingRequest` resolves satisfactorily
await result;
});
describe("Should retry requests", () => {
beforeEach(() => {
jest.useFakeTimers();
// here we use another httpApi instance in order to use fetchMock
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
baseUrl: "https://example.com",
prefix: "/_matrix",
onlyData: true,
});
processor = new OutgoingRequestProcessor(olmMachine, httpApi);
});
afterEach(() => {
jest.useRealTimers();
fetchMock.reset();
});
describe("Should retry on retryable errors", () => {
const retryableErrors: Array<[number, { status: number; body: { error: string } }]> = [
[429, { status: 429, body: { error: "Too Many Requests" } }],
[500, { status: 500, body: { error: "Internal Server Error" } }],
[502, { status: 502, body: { error: "Bad Gateway" } }],
[503, { status: 503, body: { error: "Service Unavailable" } }],
[504, { status: 504, body: { error: "Gateway timeout" } }],
[505, { status: 505, body: { error: "HTTP Version Not Supported" } }],
[506, { status: 506, body: { error: "Variant Also Negotiates" } }],
[507, { status: 507, body: { error: "Insufficient Storage" } }],
[508, { status: 508, body: { error: "Loop Detected" } }],
[510, { status: 510, body: { error: "Not Extended" } }],
[511, { status: 511, body: { error: "Network Authentication Required" } }],
[525, { status: 525, body: { error: "SSL Handshake Failed" } }],
];
describe.each(retryableErrors)(`When status code is %s`, (_, error) => {
test.each(tests)(`for request of type %ss`, async (_, RequestClass, expectedMethod, expectedPath) => {
// first, mock up a request as we might expect to receive it from the Rust layer ...
const testBody = '{ "foo": "bar" }';
const outgoingRequest = new RequestClass("1234", testBody);
fetchMock.mock(expectedPath, error, { method: expectedMethod });
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
// Run all timers and wait for the request promise to resolve/reject
await Promise.all([jest.runAllTimersAsync(), requestPromise.catch(() => {})]);
await expect(requestPromise).rejects.toThrow();
// Should have ultimately made 5 requests (1 initial + 4 retries)
const calls = fetchMock.calls(expectedPath);
expect(calls).toHaveLength(5);
// The promise should have been rejected
await expect(requestPromise).rejects.toThrow();
});
});
});
it("should not retry if M_TOO_LARGE", async () => {
const testBody = '{ "messages": { "user": {"device": "bar" }}}';
const outgoingRequest = new ToDeviceRequest("1234", "custom.type", "12345", testBody);
fetchMock.put("express:/_matrix/client/v3/sendToDevice/:type/:txnId", {
status: 502,
body: {
errcode: "M_TOO_LARGE",
error: "Request too large",
},
});
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
await Promise.all([requestPromise.catch(() => {}), jest.runAllTimersAsync()]);
await expect(requestPromise).rejects.toThrow();
const calls = fetchMock.calls("express:/_matrix/client/v3/sendToDevice/:type/:txnId");
expect(calls).toHaveLength(1);
// The promise should have been rejected
await expect(requestPromise).rejects.toThrow();
});
it("should retry on Failed to fetch connection errors", async () => {
let callCount = 0;
fetchMock.post("path:/_matrix/client/v3/keys/upload", (url, opts) => {
callCount++;
if (callCount == 2) {
return {
status: 200,
body: "{}",
};
} else {
throw new Error("Failed to fetch");
}
});
const outgoingRequest = new KeysUploadRequest("1234", "{}");
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
await Promise.all([requestPromise, jest.runAllTimersAsync()]);
const calls = fetchMock.calls("path:/_matrix/client/v3/keys/upload");
expect(calls).toHaveLength(2);
expect(olmMachine.markRequestAsSent).toHaveBeenCalled();
});
it("should retry to send to-device", async () => {
let callCount = 0;
const testBody = '{ "messages": { "user": {"device": "bar" }}}';
const outgoingRequest = new ToDeviceRequest("1234", "custom.type", "12345", testBody);
fetchMock.put("express:/_matrix/client/v3/sendToDevice/:type/:txnId", (url, opts) => {
callCount++;
if (callCount == 2) {
return {
status: 200,
body: "{}",
};
} else {
throw new Error("Failed to fetch");
}
});
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
await Promise.all([requestPromise, jest.runAllTimersAsync()]);
const calls = fetchMock.calls("express:/_matrix/client/v3/sendToDevice/:type/:txnId");
expect(calls).toHaveLength(2);
expect(olmMachine.markRequestAsSent).toHaveBeenCalled();
});
it("should retry to call with UIA", async () => {
let callCount = 0;
const testBody = '{ "foo": "bar" }';
const outgoingRequest = new UploadSigningKeysRequest(testBody);
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", (url, opts) => {
callCount++;
if (callCount == 2) {
return {
status: 200,
body: "{}",
};
} else {
throw new Error("Failed to fetch");
}
});
const authCallback: UIAuthCallback<Object> = async (makeRequest) => {
return await makeRequest({ type: "test" });
};
const requestPromise = processor.makeOutgoingRequest(outgoingRequest, authCallback);
await Promise.all([requestPromise, jest.runAllTimersAsync()]);
const calls = fetchMock.calls("path:/_matrix/client/v3/keys/device_signing/upload");
expect(calls).toHaveLength(2);
// Will not mark as sent as it's a UIA request
});
it("should retry on respect server cool down on LIMIT_EXCEEDED", async () => {
const retryAfterMs = 5000;
let callCount = 0;
fetchMock.post("path:/_matrix/client/v3/keys/upload", (url, opts) => {
callCount++;
if (callCount == 2) {
return {
status: 200,
body: "{}",
};
} else {
return {
status: 429,
body: {
errcode: "M_LIMIT_EXCEEDED",
error: "Too many requests",
retry_after_ms: retryAfterMs,
},
};
}
});
const outgoingRequest = new KeysUploadRequest("1234", "{}");
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
// advanced by less than the retryAfterMs
await jest.advanceTimersByTimeAsync(retryAfterMs - 1000);
// should not have made a second request yet
{
const calls = fetchMock.calls("path:/_matrix/client/v3/keys/upload");
expect(calls).toHaveLength(1);
}
// advanced by the remaining time
await jest.advanceTimersByTimeAsync(retryAfterMs + 1000);
await requestPromise;
const calls = fetchMock.calls("path:/_matrix/client/v3/keys/upload");
expect(calls).toHaveLength(2);
expect(olmMachine.markRequestAsSent).toHaveBeenCalled();
});
const nonRetryableErrors: Array<[number, { status: number; body: { errcode: string } }]> = [
[400, { status: 400, body: { errcode: "Bad Request" } }],
[401, { status: 401, body: { errcode: "M_UNKNOWN_TOKEN" } }],
[403, { status: 403, body: { errcode: "M_FORBIDDEN" } }],
];
describe.each(nonRetryableErrors)("Should not retry all sort of errors", (_, error) => {
test.each(tests)("for %ss", async (_, RequestClass, expectedMethod, expectedPath) => {
const testBody = '{ "foo": "bar" }';
const outgoingRequest = new RequestClass("1234", testBody);
// @ts-ignore to avoid having to do if else to switch the method (.put/.post)
fetchMock[expectedMethod.toLowerCase()](expectedPath, {
status: error.status,
body: error.body,
});
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
// Run all timers and wait for the request promise to resolve/reject
await Promise.all([jest.runAllTimersAsync(), requestPromise.catch(() => {})]);
await expect(requestPromise).rejects.toThrow();
// Should have only tried once
const calls = fetchMock.calls(expectedPath);
expect(calls).toHaveLength(1);
await expect(requestPromise).rejects.toThrow();
});
});
describe("Should not retry client timeouts", () => {
test.each(tests)("for %ss", async (_, RequestClass, expectedMethod, expectedPath) => {
const testBody = '{ "foo": "bar" }';
const outgoingRequest = new RequestClass("1234", testBody);
// @ts-ignore to avoid having to do if else to switch the method (.put/.post)
fetchMock[expectedMethod.toLowerCase()](expectedPath, () => {
// This is what a client timeout error will throw
throw new DOMException("The user aborted a request.", "AbortError");
});
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
// Run all timers and wait for the request promise to resolve/reject
await Promise.all([jest.runAllTimersAsync(), requestPromise.catch(() => {})]);
await expect(requestPromise).rejects.toThrow();
// Should have only tried once
const calls = fetchMock.calls(expectedPath);
expect(calls).toHaveLength(1);
await expect(requestPromise).rejects.toThrow();
});
});
describe("Should retry until it works", () => {
it.each([1, 2, 3, 4])("should succeed if the call number %s is ok", async (successfulCall) => {
let callCount = 0;
fetchMock.post("path:/_matrix/client/v3/keys/upload", (url, opts) => {
callCount++;
if (callCount == successfulCall) {
return {
status: 200,
body: "{}",
};
} else {
return {
status: 500,
body: { error: "Internal server error" },
};
}
});
const outgoingRequest = new KeysUploadRequest("1234", "{}");
const requestPromise = processor.makeOutgoingRequest(outgoingRequest);
await Promise.all([requestPromise, jest.runAllTimersAsync()]);
const calls = fetchMock.calls("path:/_matrix/client/v3/keys/upload");
expect(calls).toHaveLength(successfulCall);
expect(olmMachine.markRequestAsSent).toHaveBeenCalled();
});
});
});
});
@@ -90,7 +90,7 @@ describe("PerSessionKeyBackupDownloader", () => {
mockRustBackupManager = {
getActiveBackupVersion: jest.fn(),
requestKeyBackupVersion: jest.fn(),
getServerBackupInfo: jest.fn(),
importBackedUpRoomKeys: jest.fn(),
createBackupDecryptor: jest.fn().mockReturnValue(mockBackupDecryptor),
on: jest.fn().mockImplementation((event, listener) => {
@@ -135,7 +135,7 @@ describe("PerSessionKeyBackupDownloader", () => {
decryptionKey: RustSdkCryptoJs.BackupDecryptionKey.fromBase64(TestData.BACKUP_DECRYPTION_KEY_BASE64),
} as unknown as RustSdkCryptoJs.BackupKeys);
mockRustBackupManager.requestKeyBackupVersion.mockResolvedValue(TestData.SIGNED_BACKUP_DATA);
mockRustBackupManager.getServerBackupInfo.mockResolvedValue(TestData.SIGNED_BACKUP_DATA);
});
it("Should download and import a missing key from backup", async () => {
@@ -155,8 +155,11 @@ describe("PerSessionKeyBackupDownloader", () => {
downloader.onDecryptionKeyMissingError(roomId, sessionId);
// `isKeyBackupDownloadConfigured` is false until the config is proven.
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
await expectAPICall;
await awaitKeyImported.promise;
expect(downloader.isKeyBackupDownloadConfigured()).toBe(true);
expect(mockRustBackupManager.createBackupDecryptor).toHaveBeenCalledTimes(1);
});
@@ -313,6 +316,9 @@ describe("PerSessionKeyBackupDownloader", () => {
expect(getConfigSpy).toHaveBeenCalledTimes(1);
expect(getConfigSpy).toHaveReturnedWith(Promise.resolve(null));
// isKeyBackupDownloadConfigured remains false
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
});
it("Should not query server if backup not active", async () => {
@@ -328,6 +334,9 @@ describe("PerSessionKeyBackupDownloader", () => {
expect(getConfigSpy).toHaveBeenCalledTimes(1);
expect(getConfigSpy).toHaveReturnedWith(Promise.resolve(null));
// isKeyBackupDownloadConfigured remains false
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
});
it("Should stop if backup key is not cached", async () => {
@@ -344,6 +353,9 @@ describe("PerSessionKeyBackupDownloader", () => {
expect(getConfigSpy).toHaveBeenCalledTimes(1);
expect(getConfigSpy).toHaveReturnedWith(Promise.resolve(null));
// isKeyBackupDownloadConfigured remains false
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
});
it("Should stop if backup key cached as wrong version", async () => {
@@ -363,6 +375,9 @@ describe("PerSessionKeyBackupDownloader", () => {
expect(getConfigSpy).toHaveBeenCalledTimes(1);
expect(getConfigSpy).toHaveReturnedWith(Promise.resolve(null));
// isKeyBackupDownloadConfigured remains false
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
});
it("Should stop if backup key version does not match the active one", async () => {
@@ -382,13 +397,16 @@ describe("PerSessionKeyBackupDownloader", () => {
expect(getConfigSpy).toHaveBeenCalledTimes(1);
expect(getConfigSpy).toHaveReturnedWith(Promise.resolve(null));
// isKeyBackupDownloadConfigured remains false
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
});
});
describe("Given Backup state update", () => {
it("After initial sync, when backup becomes trusted it should request keys for past requests", async () => {
// there is a backup
mockRustBackupManager.requestKeyBackupVersion.mockResolvedValue(TestData.SIGNED_BACKUP_DATA);
mockRustBackupManager.getServerBackupInfo.mockResolvedValue(TestData.SIGNED_BACKUP_DATA);
// but at this point it's not trusted and we don't have the key
mockRustBackupManager.getActiveBackupVersion.mockResolvedValue(null);
@@ -410,6 +428,7 @@ describe("PerSessionKeyBackupDownloader", () => {
// @ts-ignore access to private property
expect(downloader.hasConfigurationProblem).toEqual(true);
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
// Now the backup becomes trusted
mockRustBackupManager.getActiveBackupVersion.mockResolvedValue(TestData.SIGNED_BACKUP_DATA.version!);
@@ -423,6 +442,7 @@ describe("PerSessionKeyBackupDownloader", () => {
mockEmitter.emit(CryptoEvent.KeyBackupStatus, true);
await jest.runAllTimersAsync();
expect(downloader.isKeyBackupDownloadConfigured()).toBe(true);
await a0Imported;
await a1Imported;
@@ -434,7 +454,7 @@ describe("PerSessionKeyBackupDownloader", () => {
describe("Error cases", () => {
beforeEach(async () => {
// there is a backup
mockRustBackupManager.requestKeyBackupVersion.mockResolvedValue(TestData.SIGNED_BACKUP_DATA);
mockRustBackupManager.getServerBackupInfo.mockResolvedValue(TestData.SIGNED_BACKUP_DATA);
// It's trusted
mockRustBackupManager.getActiveBackupVersion.mockResolvedValue(TestData.SIGNED_BACKUP_DATA.version!);
// And we have the key in cache
+2 -1
View File
@@ -30,6 +30,7 @@ import { RoomEncryptor, toRustHistoryVisibility } from "../../../src/rust-crypto
import { KeyClaimManager } from "../../../src/rust-crypto/KeyClaimManager";
import { defer } from "../../../src/utils";
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
import { KnownMembership } from "../../../src/@types/membership";
describe("RoomEncryptor", () => {
describe("History Visibility", () => {
@@ -54,7 +55,7 @@ describe("RoomEncryptor", () => {
const mockRoomMember = {
userId: "@alice:example.org",
membership: "join",
membership: KnownMembership.Join,
} as unknown as Mocked<RoomMember>;
function createMockEvent(text: string): Mocked<MatrixEvent> {
+133 -10
View File
@@ -99,10 +99,12 @@ describe("initRustCrypto", () => {
isBackupEnabled: jest.fn().mockResolvedValue(false),
verifyBackup: jest.fn().mockResolvedValue({ trusted: jest.fn().mockReturnValue(false) }),
getBackupKeys: jest.fn(),
getIdentity: jest.fn().mockResolvedValue(null),
trackedUsers: jest.fn(),
} as unknown as Mocked<OlmMachine>;
}
it("passes through the store params", async () => {
it("passes through the store params (passphrase)", async () => {
const mockStore = { free: jest.fn() } as unknown as StoreHandle;
jest.spyOn(StoreHandle, "open").mockResolvedValue(mockStore);
@@ -124,7 +126,30 @@ describe("initRustCrypto", () => {
expect(OlmMachine.initFromStore).toHaveBeenCalledWith(expect.anything(), expect.anything(), mockStore);
});
it("suppresses the storePassphrase if storePrefix is unset", async () => {
it("passes through the store params (key)", async () => {
const mockStore = { free: jest.fn() } as unknown as StoreHandle;
jest.spyOn(StoreHandle, "openWithKey").mockResolvedValue(mockStore);
const testOlmMachine = makeTestOlmMachine();
jest.spyOn(OlmMachine, "initFromStore").mockResolvedValue(testOlmMachine);
const storeKey = new Uint8Array(32);
await initRustCrypto({
logger,
http: {} as MatrixClient["http"],
userId: TEST_USER,
deviceId: TEST_DEVICE_ID,
secretStorage: {} as ServerSideSecretStorage,
cryptoCallbacks: {} as CryptoCallbacks,
storePrefix: "storePrefix",
storeKey: storeKey,
});
expect(StoreHandle.openWithKey).toHaveBeenCalledWith("storePrefix", storeKey);
expect(OlmMachine.initFromStore).toHaveBeenCalledWith(expect.anything(), expect.anything(), mockStore);
});
it("suppresses the storePassphrase and storeKey if storePrefix is unset", async () => {
const mockStore = { free: jest.fn() } as unknown as StoreHandle;
jest.spyOn(StoreHandle, "open").mockResolvedValue(mockStore);
@@ -139,10 +164,11 @@ describe("initRustCrypto", () => {
secretStorage: {} as ServerSideSecretStorage,
cryptoCallbacks: {} as CryptoCallbacks,
storePrefix: null,
storeKey: new Uint8Array(),
storePassphrase: "storePassphrase",
});
expect(StoreHandle.open).toHaveBeenCalledWith(undefined, undefined);
expect(StoreHandle.open).toHaveBeenCalledWith();
expect(OlmMachine.initFromStore).toHaveBeenCalledWith(expect.anything(), expect.anything(), mockStore);
});
@@ -180,6 +206,7 @@ describe("initRustCrypto", () => {
jest.spyOn(Migration, "migrateMegolmSessions").mockResolvedValue(undefined);
const testOlmMachine = makeTestOlmMachine();
testOlmMachine.trackedUsers.mockResolvedValue([]);
jest.spyOn(OlmMachine, "initFromStore").mockResolvedValue(testOlmMachine);
});
@@ -202,7 +229,20 @@ describe("initRustCrypto", () => {
createMegolmSessions(legacyStore, nDevices, nSessionsPerDevice);
await legacyStore.markSessionsNeedingBackup([{ senderKey: pad43("device5"), sessionId: "session5" }]);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", { version: "45" });
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
auth_data: {
public_key: "backup_key_public",
},
version: "45",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
});
// The cached key should be valid for the backup
const mockBackupDecryptionKey: any = {
megolmV1PublicKey: {
publicKeyBase64: "backup_key_public",
},
};
jest.spyOn(RustSdkCryptoJs.BackupDecryptionKey, "fromBase64").mockReturnValue(mockBackupDecryptionKey);
function legacyMigrationProgressListener(progress: number, total: number): void {
logger.log(`migrated ${progress} of ${total}`);
@@ -288,6 +328,56 @@ describe("initRustCrypto", () => {
}
}, 10000);
it("migrates data from a legacy crypto store when secret are not encrypted", async () => {
const PICKLE_KEY = "pickle1234";
const legacyStore = new MemoryCryptoStore();
// It's possible for old sessions to directly store the secrets as raw UInt8Array,
// so we need to support that in the migration code.
// See https://github.com/matrix-org/matrix-js-sdk/commit/c81f11df0afd4d0da3b088892745ae2f8ba1c4a7
async function storeSecretKeyInClear(type: string, key: Uint8Array, store: CryptoStore) {
// @ts-ignore The API to store raw UInt8Array does not exist anymore, so we need that for this test.
store.privateKeys[type as keyof SecretStorePrivateKeys] = key;
}
// Populate the legacy store with some test data
const storeSecretKey = (type: string, key: string) =>
storeSecretKeyInClear(type, new TextEncoder().encode(key), legacyStore);
await legacyStore.storeAccount({}, "not a real account");
await storeSecretKey("master", "master key");
await storeSecretKey("self_signing", "ssk");
await storeSecretKey("user_signing", "usk");
fetchMock.get("path:/_matrix/client/v3/room_keys/version", 404);
function legacyMigrationProgressListener(progress: number, total: number): void {
logger.log(`migrated ${progress} of ${total}`);
}
await initRustCrypto({
logger,
http: makeMatrixHttpApi(),
userId: TEST_USER,
deviceId: TEST_DEVICE_ID,
secretStorage: {} as ServerSideSecretStorage,
cryptoCallbacks: {} as CryptoCallbacks,
storePrefix: "storePrefix",
storePassphrase: "storePassphrase",
legacyCryptoStore: legacyStore,
legacyPickleKey: PICKLE_KEY,
legacyMigrationProgressListener,
});
const data = mocked(Migration.migrateBaseData).mock.calls[0][0];
expect(data.pickledAccount).toEqual("not a real account");
expect(data.userId!.toString()).toEqual(TEST_USER);
expect(data.deviceId!.toString()).toEqual(TEST_DEVICE_ID);
expect(atob(data.privateCrossSigningMasterKey!)).toEqual("master key");
expect(atob(data.privateCrossSigningUserSigningKey!)).toEqual("usk");
expect(atob(data.privateCrossSigningSelfSigningKey!)).toEqual("ssk");
});
it("handles megolm sessions with no `keysClaimed`", async () => {
const legacyStore = new MemoryCryptoStore();
legacyStore.storeAccount({}, "not a real account");
@@ -759,8 +849,11 @@ describe("RustCrypto", () => {
},
},
};
} else if (request instanceof RustSdkCryptoJs.UploadSigningKeysRequest) {
// SigningKeysUploadRequest does not implement OutgoingRequest and does not need to be marked as sent.
} else if (
request instanceof RustSdkCryptoJs.UploadSigningKeysRequest ||
request instanceof RustSdkCryptoJs.PutDehydratedDeviceRequest
) {
// These request types do not implement OutgoingRequest and do not need to be marked as sent.
return;
}
if (request.id) {
@@ -1326,14 +1419,15 @@ describe("RustCrypto", () => {
const rustCrypto = await makeTestRustCrypto();
const olmMachine: OlmMachine = rustCrypto["olmMachine"];
const backupVersion = testData.SIGNED_BACKUP_DATA.version!;
await olmMachine.enableBackupV1(
(testData.SIGNED_BACKUP_DATA.auth_data as Curve25519AuthData).public_key,
testData.SIGNED_BACKUP_DATA.version!,
backupVersion,
);
// we import two keys: one "from backup", and one "from export"
const [backedUpRoomKey, exportedRoomKey] = testData.MEGOLM_SESSION_DATA_ARRAY;
await rustCrypto.importBackedUpRoomKeys([backedUpRoomKey]);
await rustCrypto.importBackedUpRoomKeys([backedUpRoomKey], backupVersion);
await rustCrypto.importRoomKeys([exportedRoomKey]);
// we ask for the keys that should be backed up
@@ -1368,16 +1462,17 @@ describe("RustCrypto", () => {
const rustCrypto = await makeTestRustCrypto();
const olmMachine: OlmMachine = rustCrypto["olmMachine"];
const backupVersion = testData.SIGNED_BACKUP_DATA.version!;
await olmMachine.enableBackupV1(
(testData.SIGNED_BACKUP_DATA.auth_data as Curve25519AuthData).public_key,
testData.SIGNED_BACKUP_DATA.version!,
backupVersion,
);
const backup = Array.from(testData.MEGOLM_SESSION_DATA_ARRAY);
// in addition to correct keys, we restore an invalid key
backup.push({ room_id: "!roomid", session_id: "sessionid" } as IMegolmSessionData);
const progressCallback = jest.fn();
await rustCrypto.importBackedUpRoomKeys(backup, { progressCallback });
await rustCrypto.importBackedUpRoomKeys(backup, backupVersion, { progressCallback });
expect(progressCallback).toHaveBeenCalledWith({
total: 3,
successes: 0,
@@ -1392,6 +1487,34 @@ describe("RustCrypto", () => {
});
});
});
describe("device dehydration", () => {
it("should detect if dehydration is supported", async () => {
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi());
fetchMock.config.overwriteRoutes = true;
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 404,
body: {
errcode: "M_UNRECOGNIZED",
error: "Unknown endpoint",
},
});
expect(await rustCrypto.isDehydrationSupported()).toBe(false);
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
});
expect(await rustCrypto.isDehydrationSupported()).toBe(true);
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
device_id: "DEVICE_ID",
device_data: "data",
});
expect(await rustCrypto.isDehydrationSupported()).toBe(true);
});
});
});
/** Build a MatrixHttpApi instance */
+2 -1
View File
@@ -6,6 +6,7 @@ import { MatrixError } from "../../src/http-api";
import { MatrixScheduler } from "../../src/scheduler";
import * as utils from "../test-utils/test-utils";
import { MatrixEvent } from "../../src";
import { KnownMembership } from "../../src/@types/membership";
jest.useFakeTimers();
@@ -336,7 +337,7 @@ describe("MatrixScheduler", function () {
utils.mkMembership({
user: "@alice:bar",
room: roomId,
mship: "join",
mship: KnownMembership.Join,
event: true,
}),
),
+16 -15
View File
@@ -31,6 +31,7 @@ import {
} from "../../src/sync-accumulator";
import { IRoomSummary } from "../../src";
import * as utils from "../test-utils/test-utils";
import { KnownMembership, Membership } from "../../src/@types/membership";
// The event body & unsigned object get frozen to assert that they don't get altered
// by the impl
@@ -95,7 +96,7 @@ describe("SyncAccumulator", function () {
ephemeral: { events: [] },
unread_notifications: {},
state: {
events: [member("alice", "join"), member("bob", "join")],
events: [member("alice", KnownMembership.Join), member("bob", KnownMembership.Join)],
},
summary: {
"m.heroes": undefined,
@@ -111,7 +112,7 @@ describe("SyncAccumulator", function () {
knock: {
"!knock": {
knock_state: {
events: [member("alice", "knock")],
events: [member("alice", KnownMembership.Knock)],
},
},
},
@@ -128,7 +129,7 @@ describe("SyncAccumulator", function () {
// 10 timeline messages with a prev_batch of "pinned_to_1"
sa.accumulate(
syncSkeleton({
state: { events: [member("alice", "join")] },
state: { events: [member("alice", KnownMembership.Join)] },
timeline: {
events: [
msg("alice", "1"),
@@ -204,7 +205,7 @@ describe("SyncAccumulator", function () {
it("should remove the stored timeline on limited syncs", () => {
sa.accumulate(
syncSkeleton({
state: { events: [member("alice", "join")] },
state: { events: [member("alice", KnownMembership.Join)] },
timeline: {
events: [msg("alice", "1"), msg("alice", "2"), msg("alice", "3")],
prev_batch: "pinned_to_1",
@@ -312,7 +313,7 @@ describe("SyncAccumulator", function () {
events: [
{
content: {
membership: "invite",
membership: KnownMembership.Invite,
},
state_key: "bob",
sender: "alice",
@@ -336,7 +337,7 @@ describe("SyncAccumulator", function () {
const rejectMemberEvent: IStateEvent = {
event_id: "$" + Math.random(),
content: {
membership: "leave",
membership: KnownMembership.Leave,
},
origin_server_ts: 123456789,
state_key: "bob",
@@ -344,7 +345,7 @@ describe("SyncAccumulator", function () {
type: "m.room.member",
unsigned: {
prev_content: {
membership: "invite",
membership: KnownMembership.Invite,
},
},
};
@@ -366,7 +367,7 @@ describe("SyncAccumulator", function () {
it("should accumulate knock state", () => {
const initKnockState = {
events: [member("alice", "knock")],
events: [member("alice", KnownMembership.Knock)],
};
sa.accumulate(
syncSkeleton(
@@ -453,7 +454,7 @@ describe("SyncAccumulator", function () {
const inviteStateEvents = [
{
content: {
membership: "invite",
membership: KnownMembership.Invite,
},
state_key: "bob",
sender: "alice",
@@ -482,7 +483,7 @@ describe("SyncAccumulator", function () {
const memberEvent: IStateEvent = {
event_id: "$" + Math.random(),
content: {
membership: "leave",
membership: KnownMembership.Leave,
},
origin_server_ts: 123456789,
state_key: "bob",
@@ -490,7 +491,7 @@ describe("SyncAccumulator", function () {
type: "m.room.member",
unsigned: {
prev_content: {
membership: "knock",
membership: KnownMembership.Knock,
},
},
};
@@ -528,7 +529,7 @@ describe("SyncAccumulator", function () {
const memberEvent: IStateEvent = {
event_id: "$" + Math.random(),
content: {
membership: "leave",
membership: KnownMembership.Leave,
},
origin_server_ts: 123456789,
state_key: "bob",
@@ -536,7 +537,7 @@ describe("SyncAccumulator", function () {
type: "m.room.member",
unsigned: {
prev_content: {
membership: "knock",
membership: KnownMembership.Knock,
},
},
};
@@ -933,7 +934,7 @@ function makeKnockState(): IKnockState {
name: "Room",
},
}) as IStrippedState,
member("bob", "knock"),
member("bob", KnownMembership.Knock),
],
};
}
@@ -950,7 +951,7 @@ function msg(localpart: string, text: string) {
};
}
function member(localpart: string, membership: string) {
function member(localpart: string, membership: Membership) {
return {
event_id: "$" + Math.random(),
content: {
+120
View File
@@ -0,0 +1,120 @@
/*
Copyright 2024 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 {
decryptExistingEvent,
mkDecryptionFailureMatrixEvent,
mkEncryptedMatrixEvent,
mkMatrixEvent,
} from "../../src/testing";
import { EventType } from "../../src";
import { DecryptionFailureCode } from "../../src/crypto-api";
describe("testing", () => {
describe("mkMatrixEvent", () => {
it("makes an event", () => {
const event = mkMatrixEvent({
content: { body: "blah" },
sender: "@alice:test",
type: EventType.RoomMessage,
roomId: "!test:room",
});
expect(event.getContent()).toEqual({ body: "blah" });
expect(event.sender?.userId).toEqual("@alice:test");
expect(event.isState()).toBe(false);
});
it("makes a state event", () => {
const event = mkMatrixEvent({
content: { body: "blah" },
sender: "@alice:test",
type: EventType.RoomTopic,
roomId: "!test:room",
stateKey: "",
});
expect(event.getContent()).toEqual({ body: "blah" });
expect(event.sender?.userId).toEqual("@alice:test");
expect(event.isState()).toBe(true);
expect(event.getStateKey()).toEqual("");
});
});
describe("mkEncryptedMatrixEvent", () => {
it("makes an event", async () => {
const event = await mkEncryptedMatrixEvent({
plainContent: { body: "blah" },
sender: "@alice:test",
plainType: EventType.RoomMessage,
roomId: "!test:room",
});
expect(event.sender?.userId).toEqual("@alice:test");
expect(event.isEncrypted()).toBe(true);
expect(event.isDecryptionFailure()).toBe(false);
expect(event.decryptionFailureReason).toBe(null);
expect(event.getContent()).toEqual({ body: "blah" });
expect(event.getType()).toEqual("m.room.message");
});
});
describe("mkDecryptionFailureMatrixEvent", () => {
it("makes an event", async () => {
const event = await mkDecryptionFailureMatrixEvent({
sender: "@alice:test",
roomId: "!test:room",
code: DecryptionFailureCode.UNKNOWN_ERROR,
msg: "blah",
});
expect(event.sender?.userId).toEqual("@alice:test");
expect(event.isEncrypted()).toBe(true);
expect(event.isDecryptionFailure()).toBe(true);
expect(event.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_ERROR);
expect(event.getContent()).toEqual({
body: "** Unable to decrypt: DecryptionError: blah **",
msgtype: "m.bad.encrypted",
});
expect(event.getType()).toEqual("m.room.message");
expect(event.isState()).toBe(false);
});
});
describe("decryptExistingEvent", () => {
it("decrypts an event", async () => {
const event = await mkDecryptionFailureMatrixEvent({
sender: "@alice:test",
roomId: "!test:room",
code: DecryptionFailureCode.UNKNOWN_ERROR,
msg: "blah",
});
expect(event.isEncrypted()).toBe(true);
expect(event.isDecryptionFailure()).toBe(true);
await decryptExistingEvent(event, {
plainContent: { body: "blah" },
plainType: "m.room.test",
});
expect(event.isEncrypted()).toBe(true);
expect(event.isDecryptionFailure()).toBe(false);
expect(event.decryptionFailureReason).toBe(null);
expect(event.getContent()).toEqual({ body: "blah" });
expect(event.getType()).toEqual("m.room.test");
});
});
});
+12 -11
View File
@@ -47,6 +47,7 @@ import { flushPromises } from "../../test-utils/flushPromises";
import { CallFeedReport } from "../../../src/webrtc/stats/statsReport";
import { CallFeedStatsReporter } from "../../../src/webrtc/stats/callFeedStatsReporter";
import { StatsReportEmitter } from "../../../src/webrtc/stats/statsReportEmitter";
import { KnownMembership } from "../../../src/@types/membership";
const FAKE_STATE_EVENTS = [
{
@@ -143,7 +144,7 @@ describe("Group Call", function () {
groupCall = new GroupCall(mockClient, room, GroupCallType.Video, false, GroupCallIntent.Prompt);
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
membership: "join",
membership: KnownMembership.Join,
} as unknown as RoomMember;
});
@@ -651,11 +652,11 @@ describe("Group Call", function () {
client1Room.currentState.members[FAKE_USER_ID_1] = client2Room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
membership: "join",
membership: KnownMembership.Join,
} as unknown as RoomMember;
client1Room.currentState.members[FAKE_USER_ID_2] = client2Room.currentState.members[FAKE_USER_ID_2] = {
userId: FAKE_USER_ID_2,
membership: "join",
membership: KnownMembership.Join,
} as unknown as RoomMember;
groupCall1 = new GroupCall(
@@ -831,11 +832,11 @@ describe("Group Call", function () {
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents());
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
membership: "join",
membership: KnownMembership.Join,
} as unknown as RoomMember;
room.currentState.members[FAKE_USER_ID_2] = {
userId: FAKE_USER_ID_2,
membership: "join",
membership: KnownMembership.Join,
} as unknown as RoomMember;
});
@@ -1026,11 +1027,11 @@ describe("Group Call", function () {
room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_2);
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
membership: "join",
membership: KnownMembership.Join,
} as unknown as RoomMember;
room.currentState.members[FAKE_USER_ID_2] = {
userId: FAKE_USER_ID_2,
membership: "join",
membership: KnownMembership.Join,
} as unknown as RoomMember;
groupCall = await createAndEnterGroupCall(mockClient, room);
@@ -1260,11 +1261,11 @@ describe("Group Call", function () {
room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
membership: "join",
membership: KnownMembership.Join,
} as unknown as RoomMember;
room.currentState.members[FAKE_USER_ID_2] = {
userId: FAKE_USER_ID_2,
membership: "join",
membership: KnownMembership.Join,
} as unknown as RoomMember;
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents());
@@ -1741,11 +1742,11 @@ describe("Group Call", function () {
const room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
membership: "join",
membership: KnownMembership.Join,
} as unknown as RoomMember;
room.currentState.members[FAKE_USER_ID_2] = {
userId: FAKE_USER_ID_2,
membership: "join",
membership: KnownMembership.Join,
} as unknown as RoomMember;
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents());
groupCall = await createAndEnterGroupCall(mockClient, room);
@@ -31,6 +31,7 @@ import { RoomState } from "../../../src/models/room-state";
import { GroupCallEventHandler, GroupCallEventHandlerEvent } from "../../../src/webrtc/groupCallEventHandler";
import { flushPromises } from "../../test-utils/flushPromises";
import { makeMockGroupCallStateEvent, MockCallMatrixClient } from "../../test-utils/webrtc";
import { KnownMembership } from "../../../src/@types/membership";
const FAKE_USER_ID = "@alice:test.dummy";
const FAKE_DEVICE_ID = "AAAAAAA";
@@ -50,7 +51,7 @@ describe("Group Call Event Handler", function () {
mockMember = {
userId: FAKE_USER_ID,
membership: "join",
membership: KnownMembership.Join,
} as unknown as RoomMember;
const mockEvent = makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID);
-5
View File
@@ -147,11 +147,6 @@ export interface LoginRequest {
* The login type being used.
*/
type: "m.login.password" | "m.login.token" | string;
/**
* Third-party identifier for the user.
* @deprecated in favour of `identifier`.
*/
address?: string;
/**
* ID of the client device.
* If this does not correspond to a known client device, a new device will be created.
+5
View File
@@ -15,3 +15,8 @@ limitations under the License.
*/
export type NonEmptyArray<T> = [T, ...T[]];
// Based on https://stackoverflow.com/a/53229857/3532235
export type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
export type XOR<T, U> = T | U extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
+117 -13
View File
@@ -15,6 +15,48 @@ limitations under the License.
*/
import { UnstableValue } from "../NamespacedValue";
import {
PolicyRuleEventContent,
RoomAvatarEventContent,
RoomCanonicalAliasEventContent,
RoomCreateEventContent,
RoomEncryptionEventContent,
RoomGuestAccessEventContent,
RoomHistoryVisibilityEventContent,
RoomJoinRulesEventContent,
RoomMemberEventContent,
RoomNameEventContent,
RoomPinnedEventsEventContent,
RoomPowerLevelsEventContent,
RoomServerAclEventContent,
RoomThirdPartyInviteEventContent,
RoomTombstoneEventContent,
RoomTopicEventContent,
SpaceChildEventContent,
SpaceParentEventContent,
} from "./state_events";
import {
ExperimentalGroupCallRoomMemberState,
IGroupCallRoomMemberState,
IGroupCallRoomState,
} from "../webrtc/groupCall";
import { MSC3089EventContent } from "../models/MSC3089Branch";
import { M_BEACON, M_BEACON_INFO, MBeaconEventContent, MBeaconInfoEventContent } from "./beacon";
import { XOR } from "./common";
import { ReactionEventContent, RoomMessageEventContent, StickerEventContent } from "./events";
import {
MCallAnswer,
MCallBase,
MCallCandidates,
MCallHangupReject,
MCallInviteNegotiate,
MCallReplacesEvent,
MCallSelectAnswer,
SDPStreamMetadata,
SDPStreamMetadataKey,
} from "../webrtc/callEventTypes";
import { EncryptionKeysEventContent, ICallNotifyContent } from "../matrixrtc/types";
import { M_POLL_END, M_POLL_START, PollEndEventContent, PollStartEventContent } from "./polls";
export enum EventType {
// Room state events
@@ -35,6 +77,11 @@ export enum EventType {
RoomTombstone = "m.room.tombstone",
RoomPredecessor = "org.matrix.msc3946.room_predecessor",
// Moderation policy lists
PolicyRuleUser = "m.policy.rule.user",
PolicyRuleRoom = "m.policy.rule.room",
PolicyRuleServer = "m.policy.rule.server",
SpaceChild = "m.space.child",
SpaceParent = "m.space.parent",
@@ -103,6 +150,10 @@ export enum RelationType {
Annotation = "m.annotation",
Replace = "m.replace",
Reference = "m.reference",
// Don't use this yet: it's only the stable version. The code still assumes we support the unstable prefix and,
// moreover, our tests currently use the unstable prefix. Use THREAD_RELATION_TYPE.name.
// Once we support *only* the stable prefix, THREAD_RELATION_TYPE can die and we can switch to this.
Thread = "m.thread",
}
@@ -246,17 +297,70 @@ export const LOCAL_NOTIFICATION_SETTINGS_PREFIX = new UnstableValue(
*/
export const UNSIGNED_THREAD_ID_FIELD = new UnstableValue("thread_id", "org.matrix.msc4023.thread_id");
export interface IEncryptedFile {
url: string;
mimetype?: string;
key: {
alg: string;
key_ops: string[]; // eslint-disable-line camelcase
kty: string;
k: string;
ext: boolean;
};
iv: string;
hashes: { [alg: string]: string };
v: string;
/**
* https://github.com/matrix-org/matrix-spec-proposals/pull/4115
*
* @experimental
*/
export const UNSIGNED_MEMBERSHIP_FIELD = new UnstableValue("membership", "io.element.msc4115.membership");
/**
* Mapped type from event type to content type for all specified non-state room events.
*/
export interface TimelineEvents {
[EventType.RoomMessage]: RoomMessageEventContent;
[EventType.Sticker]: StickerEventContent;
[EventType.Reaction]: ReactionEventContent;
[EventType.CallReplaces]: MCallReplacesEvent;
[EventType.CallAnswer]: MCallAnswer;
[EventType.CallSelectAnswer]: MCallSelectAnswer;
[EventType.CallNegotiate]: Omit<MCallInviteNegotiate, "offer">;
[EventType.CallInvite]: MCallInviteNegotiate;
[EventType.CallCandidates]: MCallCandidates;
[EventType.CallHangup]: MCallHangupReject;
[EventType.CallReject]: MCallHangupReject;
[EventType.CallSDPStreamMetadataChangedPrefix]: MCallBase & { [SDPStreamMetadataKey]: SDPStreamMetadata };
[EventType.CallEncryptionKeysPrefix]: EncryptionKeysEventContent;
[EventType.CallNotify]: ICallNotifyContent;
[M_BEACON.name]: MBeaconEventContent;
[M_POLL_START.name]: PollStartEventContent;
[M_POLL_END.name]: PollEndEventContent;
}
/**
* Mapped type from event type to content type for all specified room state events.
*/
export interface StateEvents {
[EventType.RoomCanonicalAlias]: RoomCanonicalAliasEventContent;
[EventType.RoomCreate]: RoomCreateEventContent;
[EventType.RoomJoinRules]: RoomJoinRulesEventContent;
[EventType.RoomMember]: RoomMemberEventContent;
// XXX: Spec says this event has 3 required fields but kicking such an invitation requires sending `{}`
[EventType.RoomThirdPartyInvite]: XOR<RoomThirdPartyInviteEventContent, {}>;
[EventType.RoomPowerLevels]: RoomPowerLevelsEventContent;
[EventType.RoomName]: RoomNameEventContent;
[EventType.RoomTopic]: RoomTopicEventContent;
[EventType.RoomAvatar]: RoomAvatarEventContent;
[EventType.RoomPinnedEvents]: RoomPinnedEventsEventContent;
[EventType.RoomEncryption]: RoomEncryptionEventContent;
[EventType.RoomHistoryVisibility]: RoomHistoryVisibilityEventContent;
[EventType.RoomGuestAccess]: RoomGuestAccessEventContent;
[EventType.RoomServerAcl]: RoomServerAclEventContent;
[EventType.RoomTombstone]: RoomTombstoneEventContent;
[EventType.SpaceChild]: SpaceChildEventContent;
[EventType.SpaceParent]: SpaceParentEventContent;
[EventType.PolicyRuleUser]: XOR<PolicyRuleEventContent, {}>;
[EventType.PolicyRuleRoom]: XOR<PolicyRuleEventContent, {}>;
[EventType.PolicyRuleServer]: XOR<PolicyRuleEventContent, {}>;
// MSC3401
[EventType.GroupCallPrefix]: IGroupCallRoomState;
[EventType.GroupCallMemberPrefix]: XOR<IGroupCallRoomMemberState, ExperimentalGroupCallRoomMemberState>;
// MSC3089
[UNSTABLE_MSC3089_BRANCH.name]: MSC3089EventContent;
// MSC3672
[M_BEACON_INFO.name]: MBeaconInfoEventContent;
}
+119
View File
@@ -0,0 +1,119 @@
/*
Copyright 2024 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 { MsgType, RelationType } from "./event";
import { FileInfo, ImageInfo, MediaEventContent } from "./media";
import { XOR } from "./common";
interface BaseTimelineEvent {
"body": string;
"m.mentions"?: {
user_ids?: string[];
room?: boolean;
};
}
interface ReplyEvent {
"m.relates_to"?: {
"m.in_reply_to"?: {
event_id: string;
};
};
}
interface NoRelationEvent {
"m.new_content"?: never;
"m.relates_to"?: never;
}
/**
* Partial content format of timeline events with rel_type `m.replace`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#event-replacements
*/
export interface ReplacementEvent<T> {
"m.new_content": T;
"m.relates_to": {
event_id: string;
rel_type: RelationType.Replace;
};
}
/**
* Partial content format of timeline events with rel_type other than `m.replace`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#forming-relationships-between-events
*/
export interface RelationEvent {
"m.new_content"?: never;
"m.relates_to": {
event_id: string;
rel_type: Exclude<RelationType, RelationType.Replace>;
};
}
/**
* Content format of timeline events with type `m.room.message` and `msgtype` `m.text`, `m.emote`, or `m.notice`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mroommessage
*/
export interface RoomMessageTextEventContent extends BaseTimelineEvent {
msgtype: MsgType.Text | MsgType.Emote | MsgType.Notice;
format?: "org.matrix.custom.html";
formatted_body?: string;
}
/**
* Content format of timeline events with type `m.room.message` and `msgtype` `m.location`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mlocation
*/
export interface RoomMessageLocationEventContent extends BaseTimelineEvent {
body: string;
geo_uri: string;
info: Pick<FileInfo, "thumbnail_info" | "thumbnail_file" | "thumbnail_url">;
msgtype: MsgType.Location;
}
type MessageEventContent = RoomMessageTextEventContent | RoomMessageLocationEventContent | MediaEventContent;
export type RoomMessageEventContent = BaseTimelineEvent &
XOR<XOR<ReplacementEvent<MessageEventContent>, RelationEvent>, XOR<ReplyEvent, NoRelationEvent>> &
MessageEventContent;
/**
* Content format of timeline events with type `m.sticker`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#msticker
*/
export interface StickerEventContent extends BaseTimelineEvent {
body: string;
info: ImageInfo;
url: string;
}
/**
* Content format of timeline events with type `m.reaction`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mreaction
*/
export interface ReactionEventContent {
"m.relates_to": {
event_id: string;
key: string;
rel_type: RelationType.Annotation;
};
}
+252
View File
@@ -0,0 +1,252 @@
/*
Copyright 2024 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 { MsgType } from "../@types/event";
/**
* Information on encrypted media attachments.
*
* Used within `m.room.message` events that reference files, such as `m.file` and `m.image`.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#extensions-to-mroommessage-msgtypes
*/
export interface EncryptedFile {
/**
* The URL to the file.
*/
url: string;
/**
* A JSON Web Key object.
*/
key: {
alg: string;
key_ops: string[]; // eslint-disable-line camelcase
kty: string;
k: string;
ext: boolean;
};
/**
* The 128-bit unique counter block used by AES-CTR, encoded as unpadded base64.
*/
iv: string;
/**
* A map from an algorithm name to a hash of the ciphertext, encoded as unpadded base64.
* Clients should support the SHA-256 hash, which uses the key `sha256`.
*/
hashes: { [alg: string]: string };
/**
* Version of the encrypted attachment's protocol. Must be `v2`.
*/
v: string;
}
interface ThumbnailInfo {
/**
* The mimetype of the image, e.g. image/jpeg.
*/
mimetype?: string;
/**
* The intended display width of the image in pixels.
* This may differ from the intrinsic dimensions of the image file.
*/
w?: number;
/**
* The intended display height of the image in pixels.
* This may differ from the intrinsic dimensions of the image file.
*/
h?: number;
/**
* Size of the image in bytes.
*/
size?: number;
}
interface BaseInfo {
mimetype?: string;
size?: number;
}
/**
* Information on media attachments of msgtype `m.file`
*
* Used within `m.room.message` events that reference files.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mfile
*/
export interface FileInfo extends BaseInfo {
/**
* Information on the encrypted thumbnail file, as specified in End-to-end encryption.
* Only present if the thumbnail is encrypted.
* @see https://spec.matrix.org/v1.9/client-server-api/#sending-encrypted-attachments
*/
thumbnail_file?: EncryptedFile;
/**
* Metadata about the image referred to in thumbnail_url.
*/
thumbnail_info?: ThumbnailInfo;
/**
* The URL to the thumbnail of the file. Only present if the thumbnail is unencrypted.
*/
thumbnail_url?: string;
}
/**
* Information on media attachments of msgtype `m.image`
*
* Used within `m.room.message` events that reference images.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mimage
*/
export interface ImageInfo extends FileInfo, ThumbnailInfo {}
/**
* Information on media attachments of msgtype `m.audio`
*
* Used within `m.room.message` events that reference audio files.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#maudio
*/
export interface AudioInfo extends BaseInfo {
/**
* The duration of the audio in milliseconds.
*/
duration?: number;
}
/**
* Information on media attachments of msgtype `m.video`
*
* Used within `m.room.message` events that reference video files.
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mvideo
*/
export interface VideoInfo extends AudioInfo, ImageInfo {
/**
* The duration of the video in milliseconds.
*/
duration?: number;
}
/**
* Union type representing the `content.info` field of all specified media events.
*/
export type MediaEventInfo = FileInfo | ImageInfo | AudioInfo | VideoInfo;
interface BaseContent {
/**
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
* @see https://spec.matrix.org/v1.9/client-server-api/#sending-encrypted-attachments
*/
file?: EncryptedFile;
/**
* Required if the file is unencrypted. The URL (typically mxc:// URI) to the file.
*/
url?: string;
}
/**
* Content format of media events with msgtype `m.file`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mfile
*/
export interface FileContent extends BaseContent {
/**
* A human-readable description of the file.
* This is recommended to be the filename of the original upload.
*/
body: string;
/**
* The original filename of the uploaded file.
*/
filename?: string;
/**
* Information about the file referred to in url.
*/
info?: FileInfo;
/**
* One of: [m.file].
*/
msgtype: MsgType.File;
}
/**
* Content format of media events with msgtype `m.image`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mimage
*/
export interface ImageContent extends BaseContent {
/**
* A textual representation of the image.
* This could be the alt text of the image, the filename of the image,
* or some kind of content description for accessibility e.g. image attachment.
*/
body: string;
/**
* Metadata about the image referred to in url.
*/
info?: ImageInfo;
/**
* One of: [m.image].
*/
msgtype: MsgType.Image;
}
/**
* Content format of media events with msgtype `m.audio`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#maudio
*/
export interface AudioContent extends BaseContent {
/**
* A description of the audio e.g. Bee Gees - Stayin Alive,
* or some kind of content description for accessibility e.g. audio attachment.
*/
body: string;
/**
* Metadata for the audio clip referred to in url.
*/
info?: AudioInfo;
/**
* One of: [m.audio].
*/
msgtype: MsgType.Audio;
}
/**
* Content format of media events with msgtype `m.video`
*
* @see https://spec.matrix.org/v1.9/client-server-api/#mvideo
*/
export interface VideoContent extends BaseContent {
/**
* A description of the video e.g. Gangnam style,
* or some kind of content description for accessibility e.g. video attachment.
*/
body: string;
/**
* Metadata about the video clip referred to in url.
*/
info?: VideoInfo;
/**
* One of: [m.video].
*/
msgtype: MsgType.Video;
}
/**
* Type representing media event contents for `m.room.message` events listed in the Matrix specification
*/
export type MediaEventContent = FileContent | ImageContent | AudioContent | VideoContent;
+57
View File
@@ -0,0 +1,57 @@
/*
Copyright 2024 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.
*/
/**
* Well-known values (from the spec or MSCs) that are allowed in the
* {@link Membership} type.
*/
export enum KnownMembership {
/**
* The user has been banned from the room, and is no longer allowed to join
* it until they are un-banned from the room (by having their membership
* state set to a value other than ban).
*/
Ban = "ban",
/**
* The user has been invited to join a room, but has not yet joined it.
* They may not participate in the room until they join.
* */
Invite = "invite",
/**
* The user has joined the room (possibly after accepting an invite), and
* may participate in it.
*/
Join = "join",
/**
* The user has knocked on the room, requesting permission to participate.
* They may not participate in the room until they join.
*/
Knock = "knock",
/**
* The user was once joined to the room, but has since left (possibly by
* choice, or possibly by being kicked).
*/
Leave = "leave",
}
/**
* The membership state for a user in a room [1]. A value from
* {@link KnownMembership} should be used where available, but all string values
* are allowed to provide flexibility for upcoming spec changes or proposals.
*
* [1] https://spec.matrix.org/latest/client-server-api/#mroommember
*/
export type Membership = KnownMembership | string;
+24
View File
@@ -0,0 +1,24 @@
/*
Copyright 2024 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 "oidc-client-ts";
declare module "oidc-client-ts" {
interface OidcMetadata {
// Add the missing device_authorization_endpoint field to the OidcMetadata interface
device_authorization_endpoint?: string;
}
}

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