Compare commits

...

113 Commits

Author SHA1 Message Date
RiotRobot 2f8b05b0da v34.4.0 2024-08-27 12:37:46 +00:00
RiotRobot 658563e2a7 v34.4.0-rc.1 2024-08-21 12:56:47 +00:00
David Baker 26d3033b17 Merge pull request #4358 from matrix-org/backport-4356-to-staging
[Backport staging] Rename slowReporter to reflect that it is CommonJS
2024-08-21 13:01:49 +01:00
David Baker a4bd7dc7d7 Merge branch 'staging' into backport-4356-to-staging 2024-08-21 12:53:16 +01:00
David Baker 1f48544b38 Merge pull request #4359 from matrix-org/dbkr/backport_4355
Move `type:module` declaration into package.json. (Backport 4355)
2024-08-21 12:46:40 +01:00
Richard van der Hoff 7ef4062f59 Move type:module declaration into package.json. (#4355)
* Rename `switch_package_to_release.js` to `.cjs`

Slightly surprisingly, the symlink is enough to make `node
switch_package_to_release.js` work.

* Rename .eslintrc.js to .cjs

Again, declare this as commonjs

* Move `type:module` declaration into package.json.

matrix-js-sdk is built into ECMAScript modules, and we should declare it as
such. See https://nodejs.org/api/packages.html#type. Failure to do so causes
problems for javascript projects attempting to build against matrix-js-sdk: see https://github.com/matrix-org/matrix-js-sdk/issues/4347.

Previously, we did this as part of the package.json switcheroo, but that is
unnecessarily fragile.

matrix-react-sdk, element-web, etc are unaffected by this, because they use the
typescript files directly, by importing `matrix-js-sdk/src/...`.
2024-08-21 12:04:59 +01:00
Richard van der Hoff 968bc51a35 Rename slowReporter to reflect that it is CommonJS (#4356)
Fix tests failing as a result of
https://github.com/matrix-org/matrix-js-sdk/pull/4355. This wasn't detected in
CI because the slowReporter is only enabled when building against develop.

(cherry picked from commit d413f5042e)
2024-08-21 09:39:52 +00:00
RiotRobot 43e58871de v34.4.0-rc.0 2024-08-20 13:40:49 +00:00
Richard van der Hoff 2544c14032 Unrevert prerelease fix, and fix release error (#4353)
* Reapply "Add "type" = "module" to ensure it is present (#4350)" (#4352)

This reverts commit 8214fd7156.

* Mark prettier config file as CommonJS

I *think* this will fix a problem with the release process in which we saw an
error:

```
Error:  Invalid configuration for file "/home/runner/work/matrix-js-sdk/matrix-js-sdk/package.json":
Error:  module is not defined in ES module scope
Error:  This file is being treated as an ES module because it has a '.js' file extension and '/home/runner/work/matrix-js-sdk/matrix-js-sdk/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
```
2024-08-20 13:16:44 +00:00
RiotRobot 8d19782c57 Merge branch 'master' into develop 2024-08-20 11:30:12 +00:00
RiotRobot 340bbe1a8f v34.3.1 2024-08-20 11:29:48 +00:00
David Baker 8214fd7156 Revert "Add "type" = "module" to ensure it is present (#4350)" (#4352)
This reverts commit d6080398db.
2024-08-20 11:10:15 +00:00
David Baker a0efed8b88 Merge commit from fork
Detect cycles when looking for predecessor rooms
2024-08-20 11:27:28 +01:00
Hugh Nimmo-Smith c408c0d1d5 Retry event decryption failures on first failure (#4346)
* Retry event decryption failures on first failure

* Suggestion from code review

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>

---------

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>
2024-08-19 13:35:45 +00:00
BLCK d6080398db Add "type" = "module" to ensure it is present (#4350) 2024-08-15 14:50:05 +00:00
Hugh Nimmo-Smith 467908703b Don't attempt to adjust for clock skews when calculating group call membership expiry (#4340)
* Use origin server timestamp for calculating group call membership expiry

* Fix tests

* Docs

* Refactor comments to reflect that the logic hasn't changed

* Make comment maintainable

* Fix up merge

* Fix test
2024-08-15 09:11:06 +00:00
Hugh Nimmo-Smith 87eddaf51a Handle MatrixRTC encryption keys arriving out of order (#4345)
* Handle MatrixRTC encryption keys arriving out of order

* Apply suggestions from code review

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>

* Suggestion from code review

---------

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>
2024-08-15 07:58:36 +00:00
Hugh Nimmo-Smith c65ef03567 Resend MatrixRTC encryption keys if a membership has changed (#4343)
* Resend MatrixRTC encryption keys if a membership has changed

* JSDoc

* Update src/matrixrtc/MatrixRTCSession.ts

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>

* Add note about using Set. symmetricDifference() when available

* Always store latest fingerprints

Should reduce unnecessary retransmits

* Refactor

---------

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>
2024-08-14 14:08:52 +00:00
RiotRobot 78cbf7cd28 Resetting package fields for development 2024-08-13 12:06:18 +00:00
RiotRobot dc7c1a4fef Merge branch 'master' into develop 2024-08-13 12:06:17 +00:00
RiotRobot 1ae0c2f3ee v34.3.0 2024-08-13 12:05:45 +00:00
Hugh Nimmo-Smith affaa95fb4 delayBeforeuse => delayBeforeUse for consistency (#4344) 2024-08-09 14:34:13 +00:00
Andrew Ferrazzutti 9176d3a671 Use non-legacy calls if any are found (#4337)
Akin to how legacy call events should be sent in rooms where there is
any ongoing legacy call, send non-legacy events in rooms where there are
only non-legacy calls; else fall back to the config preference.
2024-08-07 15:44:44 +00:00
RiotRobot de50129a53 v34.3.0-rc.1 2024-08-06 12:26:55 +00:00
Michael Telatynski 5568dfdd41 Move olm to dependencies as its types are needed downstream
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-08-06 13:25:55 +01:00
RiotRobot 39216d44ed v34.3.0-rc.0 2024-08-06 12:03:46 +00:00
Michael Telatynski 8c3b249567 Re-add olm dependency which is needed for types
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-08-06 12:00:30 +01:00
Michael Telatynski b8e40ad2a8 Resetting package fields for development 2024-08-06 11:46:18 +01:00
Michael Telatynski 4e2831764d Merge branch 'master' into develop 2024-08-06 11:46:05 +01:00
Michael Telatynski 09780672aa Fix release-gitflow.yml node version 2024-08-06 11:42:35 +01:00
Andrew Ferrazzutti 0fe53876ec Bump matrix-widget-api (#4336) 2024-08-02 12:10:24 +00:00
Michael Telatynski dfec3dc33c Make code tsc es2022 compliant (#4335)
* Remove redundant global.d.ts definitions

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

* Remove roomId overload

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

* Update base.ts

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-08-01 15:16:59 +00:00
Andrew Ferrazzutti fbdd78b428 Also check for MSC3757 for session state keys (#4334)
Do not prefix the non-legacy session membership state keys with an
underscore for rooms using MSC3757 (in addtion to MSC3779).
2024-08-01 14:57:29 +00:00
Andrew Ferrazzutti e10c362ef0 Support MSC4157: delayed events via Widget API (#4311) 2024-08-01 14:17:52 +00:00
David Baker 89a9a7fa38 Fix hashed ID server lookups with no Olm (#4333)
* Fix hashed ID server lookups with no Olm

It used the hash function from Olm (presumably to work cross-platform)
but subtle crypto is available on node nowadays so we can just use
that.

Refactor existing code that did this out to a common function, add
tests.

* Test the code when crypto is available

* Test case of no crypto available

* Move digest file to src to get it out of the way of the olm / e2e stuff

* Fix import

* Fix error string & doc

* subtle crypto, not webcrypto

* Extract the base64 part

* Fix test

* Move test file too

* Add more doc

* Fix imports
2024-08-01 10:55:23 +00:00
Andrew Ferrazzutti 687d08dc9d Support MSC4140: Delayed events (#4294)
and use them for more reliable MatrixRTC session membership events.

Also implement "parent" delayed events, which were in a previous version
of the MSC and may be reintroduced or be part of a new MSC later.

NOTE: Still missing is support for sending encrypted delayed events.
2024-07-30 12:43:25 +00:00
RiotRobot 7f91db83d0 v34.2.0 2024-07-30 12:37:58 +00:00
Michael Telatynski 0300d6343f Remove flaky test (#4332)
Fixes https://github.com/matrix-org/matrix-js-sdk/issues/4331
2024-07-29 13:42:07 +00:00
David Baker e0ef467d7d break instead of return 2024-07-29 13:55:08 +01:00
Richard van der Hoff dc1cccfecc Handle late-arriving m.room_key.withheld messages (#4310)
* Restructure eventsPendingKey to remove sender key

For withheld notices, we don't necessarily receive the sender key, so we'll
jhave to do without it.

* Re-decrypt events when we receive a withheld notice

* Extend test to cover late-arriving withheld notices

* update unit tests
2024-07-29 12:11:37 +00:00
David Baker 79299891fd Detect cycles when looking for predecessor rooms 2024-07-29 11:14:58 +01:00
Hubert Chathi d32f398345 Fix comment for useAuthorizationHeader config. (#4330) 2024-07-28 02:25:34 +00:00
Timo 0f08c00c07 Be specific about what is considered a MSC4143 call member event. (#4328)
* Be specific about what is considered a MSC4143 call member event.

* review

* check for empty event first

* Optimize for new session type events
If its a session type event we do not want to run two "key in" checks. We expect legacy events to be the less comment type going forward.

* awkward but necessary key count optimization
2024-07-25 10:57:27 +00:00
Timo 6b261b98c9 Add index.ts for matrixrtc module (#4314) 2024-07-25 10:00:11 +00:00
renovate[bot] 99f157a0f1 Update all non-major dependencies (#4323)
* Update all non-major dependencies

* Prettier

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

* Fix types for widget API update

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-07-24 16:40:51 +00:00
renovate[bot] f9f6d81346 Lock file maintenance (#4327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-24 14:42:05 +00:00
renovate[bot] 46604abe7b Update dependency matrix-widget-api to v1.7.0 (#4326)
* Update dependency matrix-widget-api to v1.7.0

* Fix types

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-07-24 13:26:04 +00:00
Valere 553758e713 Bump rust sdk wasm version to 7.0.0 (#4316) 2024-07-24 11:53:36 +00:00
renovate[bot] 509e64cfc1 Update dependency typescript to v5.5.4 (#4321)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-24 10:32:17 +00:00
renovate[bot] 60c2e9b3ed Update babel monorepo (#4318)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-24 09:45:56 +00:00
renovate[bot] cfb21fa80a Update typescript-eslint monorepo to v7.16.1 (#4322)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-24 09:27:51 +00:00
renovate[bot] c3d7f4e730 Update dependency typedoc-plugin-mdn-links to v3.2.5 (#4320)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-24 09:17:18 +00:00
renovate[bot] aa97beae44 Update dependency @types/node to v18.19.41 (#4319)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-24 09:10:23 +00:00
RiotRobot 5feab37166 v34.2.0-rc.0 2024-07-23 11:58:56 +00:00
Michael Telatynski 1a02835ab2 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into staging 2024-07-23 12:51:48 +01:00
David Baker 6f63ff1711 Remove the compare function from utils (#4315)
* Remove the compare function from utils

and change the one use of it to just intantiate a collator and use
it.

This was marked as internal module so this shouldn't be a breaking change.
Of course, react-sdk was using it.

Requires: https://github.com/matrix-org/matrix-react-sdk/pull/12782

* Add simple not-a-perf-test test

* recalculate repeatedly

otherwise we aren't testing anything different

* Use fewer members as it was making the test take a bit too long
2024-07-17 14:18:46 +00:00
RiotRobot 4d90fecb6a v34.1.0 2024-07-16 12:20:13 +00:00
Richard van der Hoff 30a26813ec Deprecate CreateSecretStorageOpts.getKeyBackupPassphrase (#4313)
it doesn't work in rust crypto, and we have no plans to fix it
(https://github.com/element-hq/element-web/issues/27455)
2024-07-13 10:19:14 +00:00
renovate[bot] f17a4fedb9 Lock file maintenance (#4199)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-10 16:31:06 +00:00
renovate[bot] 94e393c9a6 Update dependency typedoc to v0.26.4 (#4309)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-10 08:01:38 +00:00
Richard van der Hoff 53201688a6 Element-R: detect "withheld key" UTD errors, and mark them as such (#4302)
Partial fix to element-hq/element-web#27653
2024-07-09 21:42:58 +01:00
renovate[bot] 996663bf64 Update dependency rimraf to v6 (#4307)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-09 17:12:10 +00:00
renovate[bot] d6e4338a37 Update all non-major dependencies (#4305)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-09 16:48:22 +00:00
renovate[bot] b2665f2128 Update typedoc (#4304)
* Update typedoc

* Update README.md

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-07-09 15:30:34 +00:00
renovate[bot] af4b6bc126 Update typescript-eslint monorepo to v7.15.0 (#4306)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-09 14:16:29 +00:00
renovate[bot] 565bb0ef7c Update dependency typescript to v5.5.3 (#4303)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-09 14:15:37 +00:00
Michael Telatynski fe0edcd081 Stop testing on Node 21 as it is EOL (#4308)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-07-09 14:32:07 +00:00
RiotRobot 6520e0f54f v34.1.0-rc.3 2024-07-09 13:15:09 +00:00
Michael Telatynski ed7b314e6a Promote olm to a real dep given the types refer to it
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-07-09 14:13:50 +01:00
RiotRobot 24eff501e4 v34.1.0-rc.2 2024-07-09 12:17:59 +00:00
Michael Telatynski 51544f25a7 Fix bump-downstreams using incompatible Node version
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-07-09 13:17:11 +01:00
RiotRobot a0d73dfaca v34.1.0-rc.1 2024-07-09 12:12:53 +00:00
Michael Telatynski 5d2500b7a7 Fix bump-downstreams using incompatible Node version
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-07-09 13:11:25 +01:00
RiotRobot eff52b82e8 v34.1.0-rc.0 2024-07-09 12:04:20 +00:00
Michael Telatynski 2868308079 Prettier 2024-07-08 16:46:14 +01:00
RiotRobot a5ef569717 Resetting package fields for development 2024-07-08 12:18:34 +00:00
RiotRobot c06b22ae7c Merge branch 'master' into develop 2024-07-08 12:18:34 +00:00
RiotRobot 7a51798acb v34.0.0 2024-07-08 12:18:02 +00:00
Richard van der Hoff 712ba617de Remove crypto shims (#4292)
* Inline subtlecrypto shim

The presence of this thing just makes code more confusing.

* Remove pre-node-20 webcrypto hack

Until node 20.0, the webcrypto API lived at `crypto.webCrypto`. It's now
available at the same place as in web -- `globalThis.crypto`.

See: https://nodejs.org/docs/latest-v20.x/api/webcrypto.html#web-crypto-api

* oidc auth test: Clean up mocking

THe previous reset code wasn't really resetting the right thing. Let's just
re-init `window.crypto` on each test.

* Remove `crypto` shim

This isn't very useful any more.
2024-07-05 09:42:06 +00:00
Timo 957329b218 Fix room state being updated with old (now overwritten) state and emitting for those updates. (#4242)
* Fix room state being updated with old (now overwritten) state and emitting for those updates.

* remove timestamp condition

Add configuration for toStartOfTimeline

* fix timeline tests

* only skip event adding if event_id and replaces_state is set.

* fix room tests

* test skipping insertion

* rename back to lastStateEvent

* store if a state is at the start of a timeline in the RoomState class

* make `isStartTimelineState` a `public readonly` and fix condition.
2024-07-05 09:16:59 +00:00
Richard van der Hoff 1733ec7b7f Remove redundant checks on global.Olm (#4301)
These routines don't use Olm, and we shouldn't be checking for it.
2024-07-04 15:49:56 +00:00
RiotRobot 24c589923b v34.0.0-rc.1 2024-07-04 12:49:22 +00:00
Richard van der Hoff 03ed4f5dd7 Bump node.js requirement to 20. (#4293)
According to
https://github.com/matrix-org/matrix-js-sdk?tab=readme-ov-file#supported-platforms,
we *only* supprt the latest LTS release (which is currenly https://github.com/nodejs/release#release-schedule), so this should be safe.
2024-07-04 12:27:31 +00:00
Joel 6e641a28c0 Add ability to choose how many timeline events to sync when peeking (#4300)
* Add ability to choose how many timeline events to sync when peeking.

* Add a test that covers the new method parameter.

* Formatting.

---------

Co-authored-by: Joel <joel.garplind+github@gmail.com>
2024-07-04 12:14:14 +00:00
ElementRobot 1586de44bd [Backport staging] Fix "Unable to restore session" error (#4299)
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Fixes https://github.com/element-hq/element-web/issues/27666
2024-07-03 20:56:40 +01:00
Richard van der Hoff b36682cb99 Bump matrix-rust-sdk to 6.2.1 (#4298)
Fixes https://github.com/element-hq/element-web/issues/27666
2024-07-03 19:31:23 +00:00
David Baker 04ea2a4e5d Merge pull request #4297 from matrix-org/backport-4296-to-staging
[Backport staging] Fix error when sending encrypted messages in large rooms
2024-07-03 15:22:31 +01:00
Richard van der Hoff b71099d0f8 Bump matrix-rust-sdk to 6.2.0 (#4296)
Fixes https://github.com/element-hq/element-web/issues/27658

(cherry picked from commit ccc2fb5663)
2024-07-03 14:04:38 +00:00
Richard van der Hoff ccc2fb5663 Bump matrix-rust-sdk to 6.2.0 (#4296)
Fixes https://github.com/element-hq/element-web/issues/27658
2024-07-03 13:45:38 +00:00
David Langley 0a7f7efd9d Sync labels with element-meta and add local labels yml for others. (#4295) 2024-07-03 13:36:40 +00:00
Richard van der Hoff ae58d0c8eb Rust crypto: Clean up handling of verification events (#4288)
We had both an `onIncomingKeyVerificationRequest` and an
`onKeyVerificationRequest` which did different, but related, things.

Improve the documentation and reduce the duplication.
2024-07-01 11:32:09 +00:00
Richard van der Hoff 20a6704497 Remove redundant TextEncoder shim (#4290)
I believe the only reason we had this was that, before Node v11.0,
`TextEncoder` wasn't available in the global object. Nowadays it is (see
https://nodejs.org/api/util.html#class-utiltextencoder), so let's get rid of
it.
2024-07-01 09:15:01 +00:00
Kegan Dougal 3337bda752 Remove useless log line (#4289)
- it was spammy: https://github.com/element-hq/element-web/issues/27031
- it didn't actually log the duration, because the `block` function didn't
  `await` the inner promise.
2024-06-28 15:49:52 +00:00
Richard van der Hoff d90292bff5 Use prebuilt js-sdk for node example (#4286)
This example seems to have been broken by the switch to Typescript. We can't
just symlink in `../..` because that gives us the typescript version of the
source, which, obviously, doesn't work in node.

Instead, make sure we use a prebuilt version of the js-sdk.

It's actually even more broken as of js-sdk 33.0.0, thanks to the switch to ES
modules (#4187), but we'll get to that later.
2024-06-27 16:58:26 +00:00
Richard van der Hoff 3de0c02757 Remove redundant hack for using the old pickle key in rust crypto (#4282)
* Remove redundant hack for using the old pickle key in rust crypto

* Fix tests
2024-06-27 15:43:54 +00:00
Richard van der Hoff 65b9c31f9b Rename crypto-api.ts -> crypto-api/index.ts (#4283)
I found it quite confusing having `CryptoApi` be defined so far from the
`crypto-api` folder.
2024-06-26 21:02:40 +00:00
Richard van der Hoff d629a685c2 Declare matrix-js-sdk as an ES module (#4285)
* Declare matrix-js-sdk as an ES module

* Rename `babel.config.js` to show it is a CommonJS module

... otherwise it gets broken by `scripts/switch_package_to_release.js`
2024-06-26 17:11:29 +00:00
Timo 0210106be2 Add fetching the well known in embedded mode. (#4259)
* Add fetching the well known in embedded mode.

This is used to load the focus from the well known in elment-call.

* revert what we dont want in this PR.

* Update src/client.ts

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>

---------

Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>
2024-06-26 09:29:11 +00:00
renovate[bot] 3e05a71068 Update dependency typedoc-plugin-coverage to v3.3.0 (#4281)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 22:21:51 +00:00
renovate[bot] c755810d9c Update dependency typedoc-plugin-mdn-links to v3.2.0 (#4279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 17:06:31 +00:00
renovate[bot] 2d492f60a0 Update dependency typescript to v5.5.2 (#4280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 16:49:31 +00:00
renovate[bot] 16db2c5f9a Update dependency @types/node to v18.19.39 (#4278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 16:30:40 +00:00
renovate[bot] 29c02d8c37 Update all non-major dependencies (#4268)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 16:30:03 +00:00
Michael Telatynski 0f98df158c Fix ingest of release notes wiping out the parent notes (#4266)
* Fix ingest of release notes wiping out the parent notes

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

* Remove redundant reusable workflow input

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-06-25 16:24:13 +00:00
renovate[bot] 2ea4ce0bb6 Update dependency bs58 to v6 (#4274)
* Update dependency bs58 to v6

* Update import

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

* 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-06-25 16:02:43 +00:00
renovate[bot] 3e0017fecf Update actions/checkout digest to 692973e (#4267)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 15:25:04 +00:00
renovate[bot] a0073ddaaf Update typescript-eslint monorepo to v7.14.1 (#4272)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 14:36:52 +00:00
renovate[bot] c29e116c0c Update dependency typescript to v5.5.2 (#4271)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 14:36:11 +00:00
renovate[bot] d9f372ca79 Update dependency eslint-plugin-unicorn to v54 (#4275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 13:54:38 +00:00
renovate[bot] 6417f4fac7 Update dependency fetch-mock to v10 (#4276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-06-25 16:02:45 +01:00
renovate[bot] 4bae83f59f Update dependency @types/uuid to v10 (#4273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 13:53:11 +00:00
renovate[bot] b8c68eb102 Update dependency @types/node to v18.19.39 (#4269)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 13:50:33 +00:00
renovate[bot] 8790cde6d4 Update dependency typedoc to ^0.26.0 (#4270)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-06-25 15:21:27 +01:00
77 changed files with 3417 additions and 1953 deletions
View File
+43
View File
@@ -0,0 +1,43 @@
- name: "A-Element-R"
description: "Issues affecting the port of Element's crypto layer to Rust"
color: "bfd4f2"
- name: "A-Packaging"
description: "Packaging, signing, releasing"
color: "bfd4f2"
- name: "A-Technical-Debt"
color: "bfd4f2"
- name: "A-Testing"
description: "Testing, code coverage, etc."
color: "bfd4f2"
- name: "backport staging"
description: "Label to automatically backport PR to staging branch"
color: "B60205"
- name: "Dependencies"
description: "Pull requests that update a dependency file"
color: "0366d6"
- name: "Easy"
color: "5dc9f7"
- name: "Sponsored"
color: "ffc8f4"
- name: "T-Deprecation"
description: "A pull request that makes something deprecated"
color: "98e6ae"
- name: "T-Other"
description: "Questions, user support, anything else"
color: "98e6ae"
- name: "X-Blocked"
color: "ff7979"
- name: "X-Breaking-Change"
color: "ff7979"
- name: "X-Reverted"
description: "PR has been reverted"
color: "F68AA3"
- name: "X-Upcoming-Release-Blocker"
description: "This does not affect the current release cycle but will affect the next one"
color: "e99695"
- name: "Z-Community-PR"
description: "Issue is solved by a community member's PR"
color: "ededed"
- name: "Z-Flaky-Test"
description: "A test is raising false alarms"
color: "ededed"
+1
View File
@@ -34,6 +34,7 @@ jobs:
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install Deps
run: "yarn install --frozen-lockfile"
+6 -4
View File
@@ -24,10 +24,6 @@ on:
description: List of github projects (owner/repo) which should have their dependency bumped to the newly released version (in JSON string array string syntax)
type: string
required: false
include-changes:
description: Project to include changelog entries from in this release.
type: string
required: false
gpg-fingerprint:
description: Fingerprint of the GPG key to use for signing the git tag and assets, if any.
type: string
@@ -120,6 +116,7 @@ jobs:
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install dependencies
run: "yarn install --frozen-lockfile"
@@ -319,6 +316,11 @@ jobs:
ref: staging
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version: "lts/*"
- name: Bump dependency
env:
DEPENDENCY: ${{ needs.npm.outputs.id }}
+1
View File
@@ -25,6 +25,7 @@ jobs:
with:
cache: "yarn"
registry-url: "https://registry.npmjs.org"
node-version-file: package.json
- name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile"
+1
View File
@@ -43,6 +43,7 @@ jobs:
uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: 🔨 Install dependencies
run: "yarn install --frozen-lockfile"
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🧮 Checkout code"
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
+6 -1
View File
@@ -18,6 +18,7 @@ jobs:
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
@@ -44,6 +45,7 @@ jobs:
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
@@ -60,6 +62,7 @@ jobs:
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install Deps
run: "yarn install --frozen-lockfile"
@@ -76,12 +79,13 @@ jobs:
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
- name: Generate Docs
run: "yarn run gendoc --treatWarningsAsErrors"
run: "yarn run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
- name: Upload Artifact
uses: actions/upload-artifact@v4
@@ -100,6 +104,7 @@ jobs:
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install Deps
run: "yarn install --frozen-lockfile"
+21
View File
@@ -0,0 +1,21 @@
name: Sync labels
on:
workflow_dispatch: {}
schedule:
- cron: "0 1 * * *" # 1am every day
push:
branches:
- develop
paths:
- .github/labels.yml
jobs:
sync-labels:
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@develop
with:
LABELS: |
element-hq/element-meta
.github/labels.yml
DELETE: true
WET: true
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+11 -1
View File
@@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
specs: [integ, unit]
node: [18, "lts/*", 21]
node: ["lts/*", 22]
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -63,6 +63,16 @@ jobs:
coverage
!coverage/lcov-report
# Dummy completion job to simplify branch protections
jest-complete:
name: Jest tests
needs: jest
if: always()
runs-on: ubuntu-latest
steps:
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
run: exit 1
matrix-react-sdk:
name: Downstream test matrix-react-sdk
if: github.event_name == 'merge_group'
View File
+85
View File
@@ -1,3 +1,88 @@
Changes in [34.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.4.0) (2024-08-27)
==================================================================================================
## ✨ Features
* Use non-legacy calls if any are found ([#4337](https://github.com/matrix-org/matrix-js-sdk/pull/4337)). Contributed by @AndrewFerr.
## 🐛 Bug Fixes
* Retry event decryption failures on first failure ([#4346](https://github.com/matrix-org/matrix-js-sdk/pull/4346)). Contributed by @hughns.
* Ensure "type" = "module" ES declaration in pre-release.sh ([#4350](https://github.com/matrix-org/matrix-js-sdk/pull/4350)). Contributed by @BLCK-B.
* Handle MatrixRTC encryption keys arriving out of order ([#4345](https://github.com/matrix-org/matrix-js-sdk/pull/4345)). Contributed by @hughns.
* Resend MatrixRTC encryption keys if a membership has changed ([#4343](https://github.com/matrix-org/matrix-js-sdk/pull/4343)). Contributed by @hughns.
Changes in [34.3.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.3.1) (2024-08-20)
==================================================================================================
# Security
- Fixes for [CVE-2024-42369](https://nvd.nist.gov/vuln/detail/CVE-2024-42369) / [GHSA-vhr5-g3pm-49fm](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-vhr5-g3pm-49fm).
Changes in [34.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.3.0) (2024-08-13)
==================================================================================================
## ✨ Features
* Bump matrix-widget-api ([#4336](https://github.com/matrix-org/matrix-js-sdk/pull/4336)). Contributed by @AndrewFerr.
* Also check for MSC3757 for session state keys ([#4334](https://github.com/matrix-org/matrix-js-sdk/pull/4334)). Contributed by @AndrewFerr.
* Support Futures via widgets ([#4311](https://github.com/matrix-org/matrix-js-sdk/pull/4311)). Contributed by @AndrewFerr.
* Support MSC4140: Delayed events (Futures) ([#4294](https://github.com/matrix-org/matrix-js-sdk/pull/4294)). Contributed by @AndrewFerr.
* Handle late-arriving `m.room_key.withheld` messages ([#4310](https://github.com/matrix-org/matrix-js-sdk/pull/4310)). Contributed by @richvdh.
* Be specific about what is considered a MSC4143 call member event. ([#4328](https://github.com/matrix-org/matrix-js-sdk/pull/4328)). Contributed by @toger5.
* Add index.ts for matrixrtc module ([#4314](https://github.com/matrix-org/matrix-js-sdk/pull/4314)). Contributed by @toger5.
## 🐛 Bug Fixes
* Fix hashed ID server lookups with no Olm ([#4333](https://github.com/matrix-org/matrix-js-sdk/pull/4333)). Contributed by @dbkr.
Changes in [34.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.2.0) (2024-07-30)
==================================================================================================
## 🐛 Bug Fixes
* Element-R: detect "withheld key" UTD errors, and mark them as such ([#4302](https://github.com/matrix-org/matrix-js-sdk/pull/4302)). Contributed by @richvdh.
Changes in [34.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.1.0) (2024-07-16)
==================================================================================================
## ✨ Features
* Add ability to choose how many timeline events to sync when peeking ([#4300](https://github.com/matrix-org/matrix-js-sdk/pull/4300)). Contributed by @jgarplind.
* Remove redundant hack for using the old pickle key in rust crypto ([#4282](https://github.com/matrix-org/matrix-js-sdk/pull/4282)). Contributed by @richvdh.
* Add fetching the well known in embedded mode. ([#4259](https://github.com/matrix-org/matrix-js-sdk/pull/4259)). Contributed by @toger5.
## 🐛 Bug Fixes
* Fix room state being updated with old (now overwritten) state and emitting for those updates. ([#4242](https://github.com/matrix-org/matrix-js-sdk/pull/4242)). Contributed by @toger5.
* Fix incorrect "Olm is not available" errors ([#4301](https://github.com/matrix-org/matrix-js-sdk/pull/4301)). Contributed by @richvdh.
* Fix build for example script ([#4286](https://github.com/matrix-org/matrix-js-sdk/pull/4286)). Contributed by @richvdh.
* Declare matrix-js-sdk as an ES module ([#4285](https://github.com/matrix-org/matrix-js-sdk/pull/4285)). Contributed by @richvdh.
Changes in [34.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.0.0) (2024-07-08)
==================================================================================================
## 🚨 BREAKING CHANGES
* Fetch capabilities in the background ([#4246](https://github.com/matrix-org/matrix-js-sdk/pull/4246)). Contributed by @dbkr.
## ✨ Features
* Prefix the user+device state key if needed ([#4262](https://github.com/matrix-org/matrix-js-sdk/pull/4262)). Contributed by @AndrewFerr.
* Use legacy call membership if anyone else is ([#4260](https://github.com/matrix-org/matrix-js-sdk/pull/4260)). Contributed by @AndrewFerr.
* Fetch capabilities in the background ([#4246](https://github.com/matrix-org/matrix-js-sdk/pull/4246)). Contributed by @dbkr.
* Use server name instead of homeserver url to allow well-known lookups during QR OIDC reciprocation ([#4233](https://github.com/matrix-org/matrix-js-sdk/pull/4233)). Contributed by @t3chguy.
* Add via parameter for MSC4156 ([#4247](https://github.com/matrix-org/matrix-js-sdk/pull/4247)). Contributed by @Johennes.
* Make the js-sdk compatible with MSC preferred foci and active focus. ([#4195](https://github.com/matrix-org/matrix-js-sdk/pull/4195)). Contributed by @toger5.
* Replace usages of setImmediate with setTimeout for wider compatibility ([#4240](https://github.com/matrix-org/matrix-js-sdk/pull/4240)). Contributed by @t3chguy.
## 🐛 Bug Fixes
* [Backport staging] Fix "Unable to restore session" error ([#4299](https://github.com/matrix-org/matrix-js-sdk/pull/4299)). Contributed by @RiotRobot.
* [Backport staging] Fix error when sending encrypted messages in large rooms ([#4297](https://github.com/matrix-org/matrix-js-sdk/pull/4297)). Contributed by @RiotRobot.
* Element-R: Fix resource leaks in verification logic ([#4263](https://github.com/matrix-org/matrix-js-sdk/pull/4263)). Contributed by @richvdh.
* Upgrade Rust Crypto SDK to 6.1.0 ([#4261](https://github.com/matrix-org/matrix-js-sdk/pull/4261)). Contributed by @richvdh.
* Correctly transform base64 with multiple instances of + or / ([#4252](https://github.com/matrix-org/matrix-js-sdk/pull/4252)). Contributed by @robintown.
* Work around spec bug for m.room.avatar state event content type ([#4245](https://github.com/matrix-org/matrix-js-sdk/pull/4245)). Contributed by @t3chguy.
Changes in [33.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v33.1.0) (2024-06-18)
==================================================================================================
## ✨ Features
+1 -1
View File
@@ -39,7 +39,7 @@ 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.
[the Node.js terminal app](examples/node/README.md) for a more complex example.
To start the client:
+2 -4
View File
@@ -3,12 +3,10 @@
"version": "0.0.0",
"description": "",
"main": "app.js",
"scripts": {
"preinstall": "npm install ../.."
},
"author": "",
"license": "Apache 2.0",
"dependencies": {
"cli-color": "^1.0.0"
"cli-color": "^1.0.0",
"matrix-js-sdk": "^32.0.0"
}
}
+1 -1
View File
@@ -39,7 +39,7 @@ if (env["GITHUB_ACTIONS"] !== undefined) {
// if we're running against the develop branch, also enable the slow test reporter
if (env["GITHUB_REF"] == "refs/heads/develop") {
reporters.push("<rootDir>/spec/slowReporter.js");
reporters.push("<rootDir>/spec/slowReporter.cjs");
}
config.reporters = reporters;
}
+14 -13
View File
@@ -1,9 +1,9 @@
{
"name": "matrix-js-sdk",
"version": "34.0.0-rc.0",
"version": "34.4.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=18.0.0"
"node": ">=20.0.0"
},
"scripts": {
"prepack": "yarn build",
@@ -31,6 +31,7 @@
"keywords": [
"matrix-org"
],
"type": "module",
"main": "./lib/index.js",
"browser": "./lib/browser-index.js",
"matrix_src_main": "./src/index.ts",
@@ -53,14 +54,15 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^6.0.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^7.0.0",
"@matrix-org/olm": "3.2.15",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"bs58": "^6.0.0",
"content-type": "^1.0.4",
"jwt-decode": "^4.0.0",
"loglevel": "^1.7.1",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.6.0",
"matrix-widget-api": "^1.8.2",
"oidc-client-ts": "^3.0.1",
"p-retry": "4",
"sdp-transform": "^2.14.1",
@@ -82,7 +84,6 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@matrix-org/olm": "3.2.15",
"@peculiar/webcrypto": "^1.4.5",
"@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5",
@@ -90,7 +91,7 @@
"@types/jest": "^29.0.0",
"@types/node": "18",
"@types/sdp-transform": "^2.4.5",
"@types/uuid": "9",
"@types/uuid": "10",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"babel-jest": "^29.0.0",
@@ -104,9 +105,9 @@
"eslint-plugin-jsdoc": "^48.0.0",
"eslint-plugin-matrix-org": "^1.0.0",
"eslint-plugin-tsdoc": "^0.3.0",
"eslint-plugin-unicorn": "^53.0.0",
"eslint-plugin-unicorn": "^54.0.0",
"fake-indexeddb": "^5.0.2",
"fetch-mock": "9.11.0",
"fetch-mock": "10.1.0",
"fetch-mock-jest": "^1.5.1",
"husky": "^9.0.0",
"jest": "^29.0.0",
@@ -117,13 +118,13 @@
"lint-staged": "^15.0.2",
"matrix-mock-request": "^2.5.0",
"node-fetch": "^2.7.0",
"prettier": "3.3.2",
"rimraf": "^5.0.0",
"prettier": "3.3.3",
"rimraf": "^6.0.0",
"ts-node": "^10.9.2",
"typedoc": "^0.25.10",
"typedoc": "^0.26.0",
"typedoc-plugin-coverage": "^3.0.0",
"typedoc-plugin-mdn-links": "^3.0.3",
"typedoc-plugin-missing-exports": "^2.0.0",
"typedoc-plugin-missing-exports": "^3.0.0",
"typescript": "^5.3.3"
},
"@casualbot/jest-sonar-reporter": {
+7 -6
View File
@@ -112,7 +112,14 @@ const main = async ({ github, releaseId, dependencies }) => {
const { GITHUB_REPOSITORY } = process.env;
const [owner, repo] = GITHUB_REPOSITORY.split("/");
const { data: release } = await github.rest.repos.getRelease({
owner,
repo,
release_id: releaseId,
});
const sections = Object.fromEntries(categories.map((cat) => [cat, []]));
parseReleaseNotes(release.body, sections);
for (const dependency of dependencies) {
const releases = await getReleases(github, dependency);
for (const release of releases) {
@@ -120,12 +127,6 @@ const main = async ({ github, releaseId, dependencies }) => {
}
}
const { data: release } = await github.rest.repos.getRelease({
owner,
repo,
release_id: releaseId,
});
const intro = release.body.split(HEADING_PREFIX, 2)[0].trim();
let output = "";
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env node
const fsProm = require("fs/promises");
const PKGJSON = "package.json";
async function main() {
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, "utf8"));
for (const field of ["main", "typings"]) {
if (pkgJson["matrix_lib_" + field] !== undefined) {
pkgJson[field] = pkgJson["matrix_lib_" + field];
}
}
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
}
main();
-17
View File
@@ -1,17 +0,0 @@
#!/usr/bin/env node
const fsProm = require("fs/promises");
const PKGJSON = "package.json";
async function main() {
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, "utf8"));
for (const field of ["main", "typings"]) {
if (pkgJson["matrix_lib_" + field] !== undefined) {
pkgJson[field] = pkgJson["matrix_lib_" + field];
}
}
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
}
main();
+1
View File
@@ -0,0 +1 @@
switch_package_to_release.cjs
+85 -9
View File
@@ -19,7 +19,7 @@ import anotherjson from "another-json";
import fetchMock from "fetch-mock-jest";
import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { MockResponse, MockResponseFunction } from "fetch-mock";
import FetchMock from "fetch-mock";
import Olm from "@matrix-org/olm";
import * as testUtils from "../../test-utils/test-utils";
@@ -157,7 +157,7 @@ async function expectSendRoomKey(
return await new Promise<Olm.InboundGroupSession>((resolve) => {
fetchMock.putOnce(
new RegExp("/sendToDevice/m.room.encrypted/"),
(url: string, opts: RequestInit): MockResponse => {
(url: string, opts: RequestInit): FetchMock.MockResponse => {
const content = JSON.parse(opts.body as string);
resolve(onSendRoomKey(content));
return {};
@@ -291,7 +291,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
* @param response - the response to return from the request. Normally an {@link IClaimOTKsResult}
* (or a function that returns one).
*/
function expectAliceKeyClaim(response: MockResponse | MockResponseFunction) {
function expectAliceKeyClaim(response: FetchMock.MockResponse | FetchMock.MockResponseFunction) {
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/claim"), response);
}
@@ -1419,7 +1419,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
fetchMock.putOnce(
{ url: new RegExp("/send/"), name: "send-event" },
(url: string, opts: RequestInit): MockResponse => {
(url: string, opts: RequestInit): FetchMock.MockResponse => {
const content = JSON.parse(opts.body as string);
logger.log("/send:", content);
// make sure that a new session is used
@@ -1484,7 +1484,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// mark the device as known, and resend.
aliceClient.setDeviceKnown(aliceClient.getUserId()!, "DEVICE_ID");
expectAliceKeyClaim((url: string, opts: RequestInit): MockResponse => {
expectAliceKeyClaim((url: string, opts: RequestInit): FetchMock.MockResponse => {
const content = JSON.parse(opts.body as string);
expect(content.one_time_keys[aliceClient.getUserId()!].DEVICE_ID).toEqual("signed_curve25519");
return getTestKeysClaimResponse(aliceClient.getUserId()!);
@@ -2180,11 +2180,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
// ... and finally, send the room key. We block the response until `sendRoomMessageDefer` completes.
const sendRoomMessageDefer = defer<MockResponse>();
const sendRoomMessageDefer = defer<FetchMock.MockResponse>();
const reqProm = new Promise<IContent>((resolve) => {
fetchMock.putOnce(
new RegExp("/send/m.room.encrypted/"),
async (url: string, opts: RequestInit): Promise<MockResponse> => {
async (url: string, opts: RequestInit): Promise<FetchMock.MockResponse> => {
resolve(JSON.parse(opts.body as string));
return await sendRoomMessageDefer.promise;
},
@@ -2333,8 +2333,84 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
describe("m.room_key.withheld handling", () => {
// TODO: there are a bunch more tests for this sort of thing in spec/unit/crypto/algorithms/megolm.spec.ts.
// They should be converted to integ tests and moved.
describe.each([
["m.blacklisted", "The sender has blocked you.", DecryptionFailureCode.MEGOLM_KEY_WITHHELD],
[
"m.unverified",
"The sender has disabled encrypting to unverified devices.",
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
],
])(
"Decryption fails with withheld error if a withheld notice with code '%s' is received",
(withheldCode, expectedMessage, expectedErrorCode) => {
it.each(["before", "after"])("%s the event", async (when) => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
let awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
// Send Alice an encrypted room event which looks like it was encrypted with a megolm session
async function sendEncryptedEvent() {
const event = {
...testData.ENCRYPTED_EVENT,
origin_server_ts: Date.now(),
};
const syncResponse = {
next_batch: 1,
rooms: { join: { [ROOM_ID]: { timeline: { events: [event] } } } },
};
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
}
// Send Alice a withheld notice
async function sendWithheldMessage() {
const withheldMessage = {
type: "m.room_key.withheld",
sender: "@bob:example.com",
content: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: ROOM_ID,
sender_key: testData.ENCRYPTED_EVENT.content!.sender_key,
session_id: testData.ENCRYPTED_EVENT.content!.session_id,
code: withheldCode,
reason: "zzz",
},
};
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
to_device: { events: [withheldMessage] },
});
await syncPromise(aliceClient);
}
if (when === "before") {
await sendWithheldMessage();
await sendEncryptedEvent();
} else {
await sendEncryptedEvent();
// Make sure that the first attempt to decrypt has happened before the withheld arrives
await awaitDecryption;
awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
await sendWithheldMessage();
}
const ev = await awaitDecryption;
expect(ev.getContent()).toEqual({
body: `** Unable to decrypt: DecryptionError: ${expectedMessage} **`,
msgtype: "m.bad.encrypted",
});
expect(ev.decryptionFailureReason).toEqual(expectedErrorCode);
// `isEncryptedDisabledForUnverifiedDevices` should be true for `m.unverified` and false for other errors.
expect(ev.isEncryptedDisabledForUnverifiedDevices).toEqual(withheldCode === "m.unverified");
});
},
);
oldBackendOnly("does not block decryption on an 'm.unavailable' report", async function () {
// there may be a key downloads for alice
+1 -22
View File
@@ -66,26 +66,6 @@ describe("MatrixClient.initRustCrypto", () => {
expect(databaseNames).toEqual(expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto"]));
});
it("should create the meta db if given a pickleKey", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
pickleKey: "testKey",
});
// No databases.
expect(await indexedDB.databases()).toHaveLength(0);
await matrixClient.initRustCrypto();
// 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 storageKey", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
@@ -470,10 +450,9 @@ describe("MatrixClient.clearStores", () => {
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
pickleKey: "testKey",
});
await matrixClient.initRustCrypto();
await matrixClient.initRustCrypto({ storagePassword: "testKey" });
expect(await indexedDB.databases()).toHaveLength(2);
await matrixClient.stopClient();
+3 -3
View File
@@ -17,7 +17,7 @@ limitations under the License.
import "fake-indexeddb/auto";
import anotherjson from "another-json";
import { MockResponse } from "fetch-mock";
import FetchMock from "fetch-mock";
import fetchMock from "fetch-mock-jest";
import { IDBFactory } from "fake-indexeddb";
import { createHash } from "crypto";
@@ -1511,7 +1511,7 @@ function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }>
return new Promise((resolve) => {
fetchMock.putOnce(
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp(msgtype)}`),
(url: string, opts: RequestInit): MockResponse => {
(url: string, opts: RequestInit): FetchMock.MockResponse => {
resolve(JSON.parse(opts.body as string));
return {};
},
@@ -1535,7 +1535,7 @@ function mockSecretRequestAndGetPromises(): Map<string, Promise<string>> {
fetchMock.put(
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`),
(url: string, opts: RequestInit): MockResponse => {
(url: string, opts: RequestInit): FetchMock.MockResponse => {
const messages = JSON.parse(opts.body as string).messages[TEST_USER_ID];
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
const content = Object.values(messages)[0] as any;
+57 -54
View File
@@ -2281,67 +2281,70 @@ describe("MatrixClient syncing", () => {
httpBackend!.expectedRequests = [];
});
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: KnownMembership.Leave,
messages: {
start: "start",
end: "end",
chunk: [
it.each([undefined, 100])(
"should return a room based on the room initialSync API with limit %s",
async (limit) => {
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomOne)}/initialSync`).respond(200, {
room_id: roomOne,
membership: KnownMembership.Leave,
messages: {
start: "start",
end: "end",
chunk: [
{
content: { body: "Message 1" },
type: "m.room.message",
event_id: "$eventId1",
sender: userA,
origin_server_ts: 12313525,
room_id: roomOne,
},
{
content: { body: "Message 2" },
type: "m.room.message",
event_id: "$eventId2",
sender: userB,
origin_server_ts: 12315625,
room_id: roomOne,
},
],
},
state: [
{
content: { body: "Message 1" },
type: "m.room.message",
event_id: "$eventId1",
content: { name: "Room Name" },
type: "m.room.name",
event_id: "$eventId",
sender: userA,
origin_server_ts: 12313525,
room_id: roomOne,
},
{
content: { body: "Message 2" },
type: "m.room.message",
event_id: "$eventId2",
sender: userB,
origin_server_ts: 12315625,
origin_server_ts: 12314525,
state_key: "",
room_id: roomOne,
},
],
},
state: [
{
content: { name: "Room Name" },
type: "m.room.name",
event_id: "$eventId",
sender: userA,
origin_server_ts: 12314525,
state_key: "",
room_id: roomOne,
},
],
presence: [
{
content: {},
type: "m.presence",
sender: userA,
},
],
});
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
presence: [
{
content: {},
type: "m.presence",
sender: userA,
},
],
});
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
const prom = client!.peekInRoom(roomOne);
await httpBackend!.flushAllExpected();
const room = await prom;
const prom = client!.peekInRoom(roomOne, limit);
await httpBackend!.flushAllExpected();
const room = await prom;
expect(room.roomId).toBe(roomOne);
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");
expect(room.timeline[1].getContent().body).toBe("Message 2");
client?.stopPeeking();
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
await httpBackend!.flushAllExpected();
});
expect(room.roomId).toBe(roomOne);
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");
expect(room.timeline[1].getContent().body).toBe("Message 2");
client?.stopPeeking();
httpBackend!.when("GET", "/events").respond(200, { chunk: [] });
await httpBackend!.flushAllExpected();
},
);
});
describe("user account data", () => {
@@ -268,7 +268,8 @@ describe("MSC4108SignInWithQR", () => {
it("should abort if device doesn't come up by timeout", async () => {
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
(<Function>fn)();
return -1;
// TODO: mock timers properly
return -1 as any;
});
jest.spyOn(Date, "now").mockImplementation(() => {
return 12345678 + mocked(setTimeout).mock.calls.length * 1000;
@@ -320,7 +321,8 @@ describe("MSC4108SignInWithQR", () => {
it("should not send secrets if user cancels", async () => {
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
(<Function>fn)();
return -1;
// TODO: mock timers properly
return -1 as any;
});
await Promise.all([ourLogin.negotiateProtocols(), opponentLogin.negotiateProtocols()]);
+4 -2
View File
@@ -15,7 +15,6 @@ limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import { MockOptionsMethodPut } from "fetch-mock";
import { ISyncResponder } from "./SyncResponder";
@@ -40,7 +39,10 @@ export class AccountDataAccumulator {
* @param opts - options to pass to fetchMock
* @returns a Promise which will resolve (with the content of the account data) once it is set.
*/
public interceptSetAccountData(accountDataType: string, opts?: MockOptionsMethodPut): Promise<any> {
public interceptSetAccountData(
accountDataType: string,
opts?: Parameters<(typeof fetchMock)["put"]>[2],
): Promise<any> {
return new Promise((resolve) => {
// Called when the cross signing key is uploaded
fetchMock.put(
+2 -2
View File
@@ -17,7 +17,7 @@ limitations under the License.
import debugFunc from "debug";
import { Debugger } from "debug";
import fetchMock from "fetch-mock-jest";
import { MockResponse } from "fetch-mock";
import FetchMock from "fetch-mock";
/** Interface implemented by classes that intercept `/sync` requests from test clients
*
@@ -80,7 +80,7 @@ export class SyncResponder implements ISyncResponder {
);
}
private async onSyncRequest(): Promise<MockResponse> {
private async onSyncRequest(): Promise<FetchMock.MockResponse> {
switch (this.state) {
case SyncResponderState.IDLE: {
this.debug("Got /sync request: waiting for response to be ready");
+40
View File
@@ -0,0 +1,40 @@
/*
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 { encodeUnpaddedBase64Url } from "../../src";
import { sha256 } from "../../src/digest";
describe("sha256", () => {
it("should hash a string", async () => {
const hash = await sha256("test");
expect(encodeUnpaddedBase64Url(hash)).toBe("n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg");
});
it("should hash a string with emoji", async () => {
const hash = await sha256("test 🍱");
expect(encodeUnpaddedBase64Url(hash)).toBe("X2aDNrrwfq3nCTOl90R9qg9ynxhHnSzsMqtrdYX-SGw");
});
it("throws if webcrypto is not available", async () => {
const oldCrypto = global.crypto;
try {
global.crypto = {} as any;
await expect(sha256("test")).rejects.toThrow();
} finally {
global.crypto = oldCrypto;
}
});
});
+236 -3
View File
@@ -32,7 +32,7 @@ import {
IOpenIDCredentials,
} from "matrix-widget-api";
import { createRoomWidgetClient, MsgType } from "../../src/matrix";
import { createRoomWidgetClient, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
import { SyncState } from "../../src/sync";
import { ICapabilities } from "../../src/embedded";
@@ -59,8 +59,26 @@ class MockWidgetApi extends EventEmitter {
public requestCapabilityToReceiveState = jest.fn();
public requestCapabilityToSendToDevice = jest.fn();
public requestCapabilityToReceiveToDevice = jest.fn();
public sendRoomEvent = jest.fn(() => ({ event_id: `$${Math.random()}` }));
public sendStateEvent = jest.fn();
public sendRoomEvent = jest.fn(
(eventType: string, content: unknown, roomId?: string, delay?: number, parentDelayId?: string) =>
delay === undefined && parentDelayId === undefined
? { event_id: `$${Math.random()}` }
: { delay_id: `id-${Math.random()}` },
);
public sendStateEvent = jest.fn(
(
eventType: string,
stateKey: string,
content: unknown,
roomId?: string,
delay?: number,
parentDelayId?: string,
) =>
delay === undefined && parentDelayId === undefined
? { event_id: `$${Math.random()}` }
: { delay_id: `id-${Math.random()}` },
);
public updateDelayedEvent = jest.fn();
public sendToDevice = jest.fn();
public requestOpenIDConnectToken = jest.fn(() => {
return testOIDCToken;
@@ -125,6 +143,17 @@ describe("RoomWidgetClient", () => {
);
});
it("send handles wrong field in response", async () => {
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockResolvedValueOnce({
room_id: "!1:example.org",
delay_id: `id-${Math.random}`,
});
await expect(
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
).rejects.toThrow();
});
it("receives", async () => {
const event = new MatrixEvent({
type: "org.matrix.rageshake_request",
@@ -160,6 +189,199 @@ describe("RoomWidgetClient", () => {
});
});
describe("delayed events", () => {
describe("when supported", () => {
const doesServerSupportUnstableFeatureMock = jest.fn((feature) =>
Promise.resolve(feature === "org.matrix.msc4140"),
);
beforeAll(() => {
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
});
afterAll(() => {
doesServerSupportUnstableFeatureMock.mockReset();
});
it("sends delayed message events", async () => {
await makeClient({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
await client._unstable_sendDelayedEvent(
"!1:example.org",
{ delay: 2000 },
null,
"org.matrix.rageshake_request",
{ request_id: 123 },
);
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
"org.matrix.rageshake_request",
{ request_id: 123 },
"!1:example.org",
2000,
undefined,
);
});
it("sends child action delayed message events", async () => {
await makeClient({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
const parentDelayId = `id-${Math.random()}`;
await client._unstable_sendDelayedEvent(
"!1:example.org",
{ parent_delay_id: parentDelayId },
null,
"org.matrix.rageshake_request",
{ request_id: 123 },
);
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
"org.matrix.rageshake_request",
{ request_id: 123 },
"!1:example.org",
undefined,
parentDelayId,
);
});
it("sends delayed state events", async () => {
await makeClient({
sendDelayedEvents: true,
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
});
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
await client._unstable_sendDelayedStateEvent(
"!1:example.org",
{ delay: 2000 },
"org.example.foo",
{ hello: "world" },
"bar",
);
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
"org.example.foo",
"bar",
{ hello: "world" },
"!1:example.org",
2000,
undefined,
);
});
it("sends child action delayed state events", async () => {
await makeClient({
sendDelayedEvents: true,
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
});
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157SendDelayedEvent);
const parentDelayId = `fg-${Math.random()}`;
await client._unstable_sendDelayedStateEvent(
"!1:example.org",
{ parent_delay_id: parentDelayId },
"org.example.foo",
{ hello: "world" },
"bar",
);
expect(widgetApi.sendStateEvent).toHaveBeenCalledWith(
"org.example.foo",
"bar",
{ hello: "world" },
"!1:example.org",
undefined,
parentDelayId,
);
});
it("send delayed message events handles wrong field in response", async () => {
await makeClient({ sendDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
widgetApi.sendRoomEvent.mockResolvedValueOnce({
room_id: "!1:example.org",
event_id: `$${Math.random()}`,
});
await expect(
client._unstable_sendDelayedEvent(
"!1:example.org",
{ delay: 2000 },
null,
"org.matrix.rageshake_request",
{ request_id: 123 },
),
).rejects.toThrow();
});
it("send delayed state events handles wrong field in response", async () => {
await makeClient({
sendDelayedEvents: true,
sendState: [{ eventType: "org.example.foo", stateKey: "bar" }],
});
widgetApi.sendStateEvent.mockResolvedValueOnce({
room_id: "!1:example.org",
event_id: `$${Math.random()}`,
});
await expect(
client._unstable_sendDelayedStateEvent(
"!1:example.org",
{ delay: 2000 },
"org.example.foo",
{ hello: "world" },
"bar",
),
).rejects.toThrow();
});
it("updates delayed events", async () => {
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
for (const action of [
UpdateDelayedEventAction.Cancel,
UpdateDelayedEventAction.Restart,
UpdateDelayedEventAction.Send,
]) {
await client._unstable_updateDelayedEvent("id", action);
expect(widgetApi.updateDelayedEvent).toHaveBeenCalledWith("id", action);
}
});
});
describe("when unsupported", () => {
it("fails to send delayed message events", async () => {
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
await expect(
client._unstable_sendDelayedEvent(
"!1:example.org",
{ delay: 2000 },
null,
"org.matrix.rageshake_request",
{ request_id: 123 },
),
).rejects.toThrow("Server does not support");
});
it("fails to send delayed state events", async () => {
await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
await expect(
client._unstable_sendDelayedStateEvent(
"!1:example.org",
{ delay: 2000 },
"org.example.foo",
{ hello: "world" },
"bar",
),
).rejects.toThrow("Server does not support");
});
it("fails to update delayed state events", async () => {
await makeClient({});
for (const action of [
UpdateDelayedEventAction.Cancel,
UpdateDelayedEventAction.Restart,
UpdateDelayedEventAction.Send,
]) {
await expect(client._unstable_updateDelayedEvent("id", action)).rejects.toThrow(
"Server does not support",
);
}
});
});
});
describe("initialization", () => {
it("requests permissions for specific message types", async () => {
await makeClient({ sendMessage: [MsgType.Text], receiveMessage: [MsgType.Text] });
@@ -211,6 +433,17 @@ describe("RoomWidgetClient", () => {
);
});
it("send handles incorrect response", async () => {
await makeClient({ sendState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
widgetApi.sendStateEvent.mockResolvedValueOnce({
room_id: "!1:example.org",
delay_id: `id-${Math.random}`,
});
await expect(
client.sendStateEvent("!1:example.org", "org.example.foo", { hello: "world" }, "bar"),
).rejects.toThrow();
});
it("receives", async () => {
await makeClient({ receiveState: [{ eventType: "org.example.foo", stateKey: "bar" }] });
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
+369 -3
View File
@@ -57,6 +57,7 @@ import {
Room,
RuleId,
TweakName,
UpdateDelayedEventAction,
} from "../../src";
import { supportsMatrixCall } from "../../src/webrtc/call";
import { makeBeaconEvent } from "../test-utils/beacon";
@@ -97,7 +98,7 @@ type HttpLookup = {
method: string;
path: string;
prefix?: string;
data?: Record<string, any>;
data?: Record<string, any> | Record<string, any>[];
error?: object;
expectBody?: Record<string, any>;
expectQueryParams?: QueryDict;
@@ -298,7 +299,9 @@ describe("MatrixClient", function () {
...(opts || {}),
});
// FIXME: We shouldn't be yanking http like this.
client.http = (["authedRequest", "getContentUri", "request", "uploadContent"] as const).reduce((r, k) => {
client.http = (
["authedRequest", "getContentUri", "request", "uploadContent", "idServerRequest"] as const
).reduce((r, k) => {
r[k] = jest.fn();
return r;
}, {} as MatrixHttpApi<any>);
@@ -704,6 +707,328 @@ describe("MatrixClient", function () {
});
});
describe("_unstable_sendDelayedEvent", () => {
const unstableMSC4140Prefix = `${ClientPrefix.Unstable}/org.matrix.msc4140`;
const roomId = "!room:example.org";
const body = "This is the body";
const content = { body, msgtype: MsgType.Text } satisfies RoomMessageEventContent;
const timeoutDelayOpts = { delay: 2000 };
const realTimeoutDelayOpts = { "org.matrix.msc4140.delay": 2000 };
beforeEach(() => {
unstableFeatures["org.matrix.msc4140"] = true;
});
it("throws when unsupported by server", async () => {
unstableFeatures["org.matrix.msc4140"] = false;
const errorMessage = "Server does not support";
await expect(
client._unstable_sendDelayedEvent(
roomId,
timeoutDelayOpts,
null,
EventType.RoomMessage,
{ ...content },
client.makeTxnId(),
),
).rejects.toThrow(errorMessage);
await expect(
client._unstable_sendDelayedStateEvent(roomId, timeoutDelayOpts, EventType.RoomTopic, {
topic: "topic",
}),
).rejects.toThrow(errorMessage);
await expect(client._unstable_getDelayedEvents()).rejects.toThrow(errorMessage);
await expect(
client._unstable_updateDelayedEvent("anyDelayId", UpdateDelayedEventAction.Send),
).rejects.toThrow(errorMessage);
});
it("works with null threadId", async () => {
httpLookups = [];
const timeoutDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
expectQueryParams: realTimeoutDelayOpts,
data: { delay_id: "id1" },
expectBody: content,
});
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
roomId,
timeoutDelayOpts,
null,
EventType.RoomMessage,
{ ...content },
timeoutDelayTxnId,
);
const actionDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
data: { delay_id: "id2" },
expectBody: content,
});
await client._unstable_sendDelayedEvent(
roomId,
{ parent_delay_id: timeoutDelayId },
null,
EventType.RoomMessage,
{ ...content },
actionDelayTxnId,
);
});
it("works with non-null threadId", async () => {
httpLookups = [];
const threadId = "$threadId:server";
const expectBody = {
...content,
"m.relates_to": {
event_id: threadId,
is_falling_back: true,
rel_type: "m.thread",
},
};
const timeoutDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
expectQueryParams: realTimeoutDelayOpts,
data: { delay_id: "id1" },
expectBody,
});
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
roomId,
timeoutDelayOpts,
threadId,
EventType.RoomMessage,
{ ...content },
timeoutDelayTxnId,
);
const actionDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
data: { delay_id: "id2" },
expectBody,
});
await client._unstable_sendDelayedEvent(
roomId,
{ parent_delay_id: timeoutDelayId },
threadId,
EventType.RoomMessage,
{ ...content },
actionDelayTxnId,
);
});
it("should add thread relation if threadId is passed and the relation is missing", async () => {
httpLookups = [];
const threadId = "$threadId:server";
const expectBody = {
...content,
"m.relates_to": {
"m.in_reply_to": {
event_id: threadId,
},
"event_id": threadId,
"is_falling_back": true,
"rel_type": "m.thread",
},
};
const room = new Room(roomId, client, userId);
mocked(store.getRoom).mockReturnValue(room);
const rootEvent = new MatrixEvent({ event_id: threadId });
room.createThread(threadId, rootEvent, [rootEvent], false);
const timeoutDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
expectQueryParams: realTimeoutDelayOpts,
data: { delay_id: "id1" },
expectBody,
});
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
roomId,
timeoutDelayOpts,
threadId,
EventType.RoomMessage,
{ ...content },
timeoutDelayTxnId,
);
const actionDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
data: { delay_id: "id2" },
expectBody,
});
await client._unstable_sendDelayedEvent(
roomId,
{ parent_delay_id: timeoutDelayId },
threadId,
EventType.RoomMessage,
{ ...content },
actionDelayTxnId,
);
});
it("should add thread relation if threadId is passed and the relation is missing with reply", async () => {
httpLookups = [];
const threadId = "$threadId:server";
const content = {
body,
"msgtype": MsgType.Text,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$other:event",
},
},
} satisfies RoomMessageEventContent;
const expectBody = {
...content,
"m.relates_to": {
"m.in_reply_to": {
event_id: "$other:event",
},
"event_id": threadId,
"is_falling_back": false,
"rel_type": "m.thread",
},
};
const room = new Room(roomId, client, userId);
mocked(store.getRoom).mockReturnValue(room);
const rootEvent = new MatrixEvent({ event_id: threadId });
room.createThread(threadId, rootEvent, [rootEvent], false);
const timeoutDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${timeoutDelayTxnId}`,
expectQueryParams: realTimeoutDelayOpts,
data: { delay_id: "id1" },
expectBody,
});
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedEvent(
roomId,
timeoutDelayOpts,
threadId,
EventType.RoomMessage,
{ ...content },
timeoutDelayTxnId,
);
const actionDelayTxnId = client.makeTxnId();
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${actionDelayTxnId}`,
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
data: { delay_id: "id2" },
expectBody,
});
await client._unstable_sendDelayedEvent(
roomId,
{ parent_delay_id: timeoutDelayId },
threadId,
EventType.RoomMessage,
{ ...content },
actionDelayTxnId,
);
});
it("can send a delayed state event", async () => {
httpLookups = [];
const content = { topic: "The year 2000" };
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/state/m.room.topic/`,
expectQueryParams: realTimeoutDelayOpts,
data: { delay_id: "id1" },
expectBody: content,
});
const { delay_id: timeoutDelayId } = await client._unstable_sendDelayedStateEvent(
roomId,
timeoutDelayOpts,
EventType.RoomTopic,
{ ...content },
);
httpLookups.push({
method: "PUT",
path: `/rooms/${encodeURIComponent(roomId)}/state/m.room.topic/`,
expectQueryParams: { "org.matrix.msc4140.parent_delay_id": timeoutDelayId },
data: { delay_id: "id2" },
expectBody: content,
});
await client._unstable_sendDelayedStateEvent(
roomId,
{ parent_delay_id: timeoutDelayId },
EventType.RoomTopic,
{ ...content },
);
});
it("can look up delayed events", async () => {
httpLookups = [
{
method: "GET",
prefix: unstableMSC4140Prefix,
path: "/delayed_events",
data: [],
},
];
await client._unstable_getDelayedEvents();
});
it("can update delayed events", async () => {
const delayId = "id";
const action = UpdateDelayedEventAction.Restart;
httpLookups = [
{
method: "POST",
prefix: unstableMSC4140Prefix,
path: `/delayed_events/${encodeURIComponent(delayId)}`,
data: {
action,
},
},
];
await client._unstable_updateDelayedEvent(delayId, action);
});
});
it("should create (unstable) file trees", async () => {
const userId = "@test:example.org";
const roomId = "!room:example.org";
@@ -963,7 +1288,7 @@ describe("MatrixClient", function () {
const filter = new Filter(client.credentials.userId);
const filterId = await client.getOrCreateFilter(filterName, filter);
expect(filterId).toEqual(FILTER_RESPONSE.data?.filter_id);
expect(filterId).toEqual(!Array.isArray(FILTER_RESPONSE.data) && FILTER_RESPONSE.data?.filter_id);
});
});
@@ -3035,4 +3360,45 @@ describe("MatrixClient", function () {
expect(httpLookups.length).toEqual(0);
});
});
describe("identityHashedLookup", () => {
it("should return hashed lookup results", async () => {
const ID_ACCESS_TOKEN = "hello_id_server_please_let_me_make_a_request";
client.http.idServerRequest = jest.fn().mockImplementation((method, path, params) => {
if (method === "GET" && path === "/hash_details") {
return { algorithms: ["sha256"], lookup_pepper: "carrot" };
} else if (method === "POST" && path === "/lookup") {
return {
mappings: {
"WHA-MgrrsZACDI9F8OaVagpiyiV2sjZylGHJteT4OMU": "@bob:homeserver.dummy",
},
};
}
throw new Error("Test impl doesn't know about this request");
});
const lookupResult = await client.identityHashedLookup([["bob@email.dummy", "email"]], ID_ACCESS_TOKEN);
expect(client.http.idServerRequest).toHaveBeenCalledWith(
"GET",
"/hash_details",
undefined,
"/_matrix/identity/v2",
ID_ACCESS_TOKEN,
);
expect(client.http.idServerRequest).toHaveBeenCalledWith(
"POST",
"/lookup",
{ pepper: "carrot", algorithm: "sha256", addresses: ["WHA-MgrrsZACDI9F8OaVagpiyiV2sjZylGHJteT4OMU"] },
"/_matrix/identity/v2",
ID_ACCESS_TOKEN,
);
expect(lookupResult).toHaveLength(1);
expect(lookupResult[0]).toEqual({ address: "bob@email.dummy", mxid: "@bob:homeserver.dummy" });
});
});
});
+9 -22
View File
@@ -98,20 +98,6 @@ describe("CallMembership", () => {
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
});
it("considers memberships unexpired if local age low enough", () => {
const fakeEvent = makeMockEvent(1000);
fakeEvent.getLocalAge = jest.fn().mockReturnValue(3000);
const membership = new CallMembership(fakeEvent, membershipTemplate);
expect(membership.isExpired()).toEqual(false);
});
it("considers memberships expired when local age large", () => {
const fakeEvent = makeMockEvent(1000);
fakeEvent.localTimestamp = Date.now() - 6000;
const membership = new CallMembership(fakeEvent, membershipTemplate);
expect(membership.isExpired()).toEqual(true);
});
it("returns preferred foci", () => {
const fakeEvent = makeMockEvent();
const mockFocus = { type: "this_is_a_mock_focus" };
@@ -198,13 +184,6 @@ describe("CallMembership", () => {
beforeEach(() => {
// server origin timestamp for this event is 1000
fakeEvent = makeMockEvent(1000);
// our clock would have been at 2000 at the creation time (our clock at event receive time - age)
// (ie. the local clock is 1 second ahead of the servers' clocks)
fakeEvent.localTimestamp = 2000;
// for simplicity's sake, we say that the event's age is zero
fakeEvent.getLocalAge = jest.fn().mockReturnValue(0);
membership = new CallMembership(fakeEvent!, membershipTemplate);
jest.useFakeTimers();
@@ -215,6 +194,13 @@ describe("CallMembership", () => {
});
it("converts expiry time into local clock", () => {
// our clock would have been at 2000 at the creation time (our clock at event receive time - age)
// (ie. the local clock is 1 second ahead of the servers' clocks)
fakeEvent.localTimestamp = 2000;
// for simplicity's sake, we say that the event's age is zero
fakeEvent.getLocalAge = jest.fn().mockReturnValue(0);
// for sanity's sake, make sure the server-relative expiry time is what we expect
expect(membership.getAbsoluteExpiry()).toEqual(6000);
// therefore the expiry time converted to our clock should be 1 second later
@@ -223,7 +209,8 @@ describe("CallMembership", () => {
it("calculates time until expiry", () => {
jest.setSystemTime(2000);
expect(membership.getMsUntilExpiry()).toEqual(5000);
// should be using absolute expiry time
expect(membership.getMsUntilExpiry()).toEqual(4000);
});
});
});
+443 -30
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventTimeline, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
import { encodeBase64, EventTimeline, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
import { KnownMembership } from "../../../src/@types/membership";
import {
CallMembershipData,
@@ -46,6 +46,9 @@ describe("MatrixRTCSession", () => {
client = new MatrixClient({ baseUrl: "base_url" });
client.getUserId = jest.fn().mockReturnValue("@alice:example.org");
client.getDeviceId = jest.fn().mockReturnValue("AAAAAAA");
client.doesServerSupportUnstableFeature = jest.fn((feature) =>
Promise.resolve(feature === "org.matrix.msc4140"),
);
});
afterEach(() => {
@@ -70,14 +73,17 @@ describe("MatrixRTCSession", () => {
});
it("ignores expired memberships events", () => {
jest.useFakeTimers();
const expiredMembership = Object.assign({}, membershipTemplate);
expiredMembership.expires = 1000;
expiredMembership.device_id = "EXPIRED";
const mockRoom = makeMockRoom([membershipTemplate, expiredMembership], 10000);
const mockRoom = makeMockRoom([membershipTemplate, expiredMembership]);
jest.advanceTimersByTime(2000);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess?.memberships.length).toEqual(1);
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
jest.useRealTimers();
});
it("ignores memberships events of members not in the room", () => {
@@ -88,12 +94,15 @@ describe("MatrixRTCSession", () => {
});
it("honours created_ts", () => {
jest.useFakeTimers();
jest.setSystemTime(500);
const expiredMembership = Object.assign({}, membershipTemplate);
expiredMembership.created_ts = 500;
expiredMembership.expires = 1000;
const mockRoom = makeMockRoom([expiredMembership]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500);
jest.useRealTimers();
});
it("returns empty session if no membership events are present", () => {
@@ -241,40 +250,68 @@ describe("MatrixRTCSession", () => {
foci_preferred: [mockFocus],
};
function testSession(
let sendStateEventMock: jest.Mock;
let sendDelayedStateMock: jest.Mock;
let sentStateEvent: Promise<void>;
let sentDelayedState: Promise<void>;
beforeEach(() => {
sentStateEvent = new Promise((resolve) => {
sendStateEventMock = jest.fn(resolve);
});
sentDelayedState = new Promise((resolve) => {
sendDelayedStateMock = jest.fn(() => {
resolve();
return {
delay_id: "id",
};
});
});
client.sendStateEvent = sendStateEventMock;
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
});
async function testSession(
membershipData: CallMembershipData[] | SessionMembershipData,
shouldUseLegacy: boolean,
): void {
): Promise<void> {
sess = MatrixRTCSession.roomSessionForRoom(client, makeMockRoom(membershipData));
const makeNewLegacyMembershipsMock = jest.spyOn(sess as any, "makeNewLegacyMemberships");
const makeNewMembershipMock = jest.spyOn(sess as any, "makeNewMembership");
sess.joinRoomSession([mockFocus], mockFocus, joinSessionConfig);
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 500))]);
expect(makeNewLegacyMembershipsMock).toHaveBeenCalledTimes(shouldUseLegacy ? 1 : 0);
expect(makeNewMembershipMock).toHaveBeenCalledTimes(shouldUseLegacy ? 0 : 1);
await Promise.race([sentDelayedState, new Promise((resolve) => setTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(shouldUseLegacy ? 0 : 1);
}
it("uses legacy events if there are any active legacy calls", () => {
testSession([expiredLegacyMembershipData, legacyMembershipData, sessionMembershipData], true);
it("uses legacy events if there are any active legacy calls", async () => {
await testSession([expiredLegacyMembershipData, legacyMembershipData, sessionMembershipData], true);
});
it('uses legacy events if a non-legacy call is in a "memberships" array', () => {
testSession([sessionMembershipData], true);
it('uses legacy events if a non-legacy call is in a "memberships" array', async () => {
await testSession([sessionMembershipData], true);
});
it("uses non-legacy events if all legacy calls are expired", () => {
testSession([expiredLegacyMembershipData], false);
it("uses non-legacy events if all legacy calls are expired", async () => {
await testSession([expiredLegacyMembershipData], false);
});
it("uses non-legacy events if there are only non-legacy calls", () => {
testSession(sessionMembershipData, false);
it("uses non-legacy events if there are only non-legacy calls", async () => {
await testSession(sessionMembershipData, false);
});
});
describe("getOldestMembership", () => {
it("returns the oldest membership event", () => {
jest.useFakeTimers();
jest.setSystemTime(4000);
const mockRoom = makeMockRoom([
Object.assign({}, membershipTemplate, { device_id: "foo", created_ts: 3000 }),
Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }),
@@ -283,12 +320,15 @@ describe("MatrixRTCSession", () => {
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess.getOldestMembership()!.deviceId).toEqual("old");
jest.useRealTimers();
});
});
describe("getsActiveFocus", () => {
const activeFociConfig = { type: "livekit", livekit_service_url: "https://active.url" };
it("gets the correct active focus with oldest_membership", () => {
jest.useFakeTimers();
jest.setSystemTime(3000);
const mockRoom = makeMockRoom([
Object.assign({}, membershipTemplate, {
device_id: "foo",
@@ -306,6 +346,7 @@ describe("MatrixRTCSession", () => {
focus_selection: "oldest_membership",
});
expect(sess.getActiveFocus()).toBe(activeFociConfig);
jest.useRealTimers();
});
it("does not provide focus if the selction method is unknown", () => {
const mockRoom = makeMockRoom([
@@ -327,6 +368,8 @@ describe("MatrixRTCSession", () => {
expect(sess.getActiveFocus()).toBe(undefined);
});
it("gets the correct active focus legacy", () => {
jest.useFakeTimers();
jest.setSystemTime(3000);
const mockRoom = makeMockRoom([
Object.assign({}, membershipTemplate, {
device_id: "foo",
@@ -341,18 +384,34 @@ describe("MatrixRTCSession", () => {
sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }]);
expect(sess.getActiveFocus()).toBe(activeFociConfig);
jest.useRealTimers();
});
});
describe("joining", () => {
let mockRoom: Room;
let sendStateEventMock: jest.Mock;
let sendDelayedStateMock: jest.Mock;
let sendEventMock: jest.Mock;
let sentStateEvent: Promise<void>;
let sentDelayedState: Promise<void>;
beforeEach(() => {
sendStateEventMock = jest.fn();
sentStateEvent = new Promise((resolve) => {
sendStateEventMock = jest.fn(resolve);
});
sentDelayedState = new Promise((resolve) => {
sendDelayedStateMock = jest.fn(() => {
resolve();
return {
delay_id: "id",
};
});
});
sendEventMock = jest.fn();
client.sendStateEvent = sendStateEventMock;
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
client.sendEvent = sendEventMock;
mockRoom = makeMockRoom([]);
@@ -373,9 +432,11 @@ describe("MatrixRTCSession", () => {
expect(sess!.isJoined()).toEqual(true);
});
it("sends a membership event when joining a call", () => {
it("sends a membership event when joining a call", async () => {
const realSetTimeout = setTimeout;
jest.useFakeTimers();
sess!.joinRoomSession([mockFocus], mockFocus);
await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client.sendStateEvent).toHaveBeenCalledWith(
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
@@ -396,6 +457,8 @@ describe("MatrixRTCSession", () => {
},
"@alice:example.org",
);
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(0);
jest.useRealTimers();
});
@@ -403,13 +466,15 @@ describe("MatrixRTCSession", () => {
const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" };
const activeFocus = { type: "livekit", focus_selection: "oldest_membership" };
function testJoin(useOwnedStateEvents: boolean): void {
async function testJoin(useOwnedStateEvents: boolean): Promise<void> {
const realSetTimeout = setTimeout;
if (useOwnedStateEvents) {
mockRoom.getVersion = jest.fn().mockReturnValue("org.matrix.msc3779.default");
}
jest.useFakeTimers();
sess!.joinRoomSession([activeFocusConfig], activeFocus, { useLegacyMemberEvents: false });
await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client.sendStateEvent).toHaveBeenCalledWith(
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
@@ -423,15 +488,17 @@ describe("MatrixRTCSession", () => {
} satisfies SessionMembershipData,
`${!useOwnedStateEvents ? "_" : ""}@alice:example.org_AAAAAAA`,
);
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
jest.useRealTimers();
}
it("sends a membership event with session payload when joining a non-legacy call", () => {
testJoin(false);
it("sends a membership event with session payload when joining a non-legacy call", async () => {
await testJoin(false);
});
it("does not prefix the state key with _ for rooms that support user-owned state events", () => {
testJoin(true);
it("does not prefix the state key with _ for rooms that support user-owned state events", async () => {
await testJoin(true);
});
});
@@ -461,9 +528,8 @@ describe("MatrixRTCSession", () => {
const eventContent = await eventSentPromise;
// definitely should have renewed by 1 second before the expiry!
const timeElapsed = 60 * 60 * 1000 - 1000;
const event = mockRTCEvent(eventContent.memberships, mockRoom.roomId, timeElapsed);
jest.setSystemTime(1000);
const event = mockRTCEvent(eventContent.memberships, mockRoom.roomId);
const getState = mockRoom.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
getState.getStateEvents = jest.fn().mockReturnValue(event);
getState.events = new Map([
@@ -486,6 +552,8 @@ describe("MatrixRTCSession", () => {
sendStateEventMock.mockReset().mockImplementation(resolveFn);
// definitely should have renewed by 1 second before the expiry!
const timeElapsed = 60 * 60 * 1000 - 1000;
jest.setSystemTime(Date.now() + timeElapsed);
jest.advanceTimersByTime(timeElapsed);
await eventReSentPromise;
@@ -546,6 +614,17 @@ describe("MatrixRTCSession", () => {
});
});
it("does not send key if join called when already joined", () => {
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
expect(client.sendEvent).toHaveBeenCalledTimes(1);
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
expect(client.sendEvent).toHaveBeenCalledTimes(1);
});
it("retries key sends", async () => {
jest.useFakeTimers();
let firstEventSent = false;
@@ -622,7 +701,7 @@ describe("MatrixRTCSession", () => {
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId, undefined));
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
sess.onMembershipUpdate();
await keysSentPromise2;
@@ -633,6 +712,214 @@ describe("MatrixRTCSession", () => {
}
});
it("Does not re-send key if memberships stays same", async () => {
jest.useFakeTimers();
try {
const keysSentPromise1 = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
});
const member1 = membershipTemplate;
const member2 = Object.assign({}, membershipTemplate, {
device_id: "BBBBBBB",
});
const mockRoom = makeMockRoom([member1, member2]);
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([member1, member2], mockRoom.roomId));
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
await keysSentPromise1;
// make sure an encryption key was sent
expect(sendEventMock).toHaveBeenCalledWith(
expect.stringMatching(".*"),
"io.element.call.encryption_keys",
{
call_id: "",
device_id: "AAAAAAA",
keys: [
{
index: 0,
key: expect.stringMatching(".*"),
},
],
},
);
sendEventMock.mockClear();
// these should be a no-op:
sess.onMembershipUpdate();
expect(sendEventMock).toHaveBeenCalledTimes(0);
} finally {
jest.useRealTimers();
}
});
it("Re-sends key if a member changes membership ID", async () => {
jest.useFakeTimers();
try {
const keysSentPromise1 = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
});
const member1 = membershipTemplate;
const member2 = {
...membershipTemplate,
device_id: "BBBBBBB",
};
const mockRoom = makeMockRoom([member1, member2]);
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([member1, member2], mockRoom.roomId));
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
await keysSentPromise1;
// make sure an encryption key was sent
expect(sendEventMock).toHaveBeenCalledWith(
expect.stringMatching(".*"),
"io.element.call.encryption_keys",
{
call_id: "",
device_id: "AAAAAAA",
keys: [
{
index: 0,
key: expect.stringMatching(".*"),
},
],
},
);
sendEventMock.mockClear();
// this should be a no-op:
sess.onMembershipUpdate();
expect(sendEventMock).toHaveBeenCalledTimes(0);
// advance time to avoid key throttling
jest.advanceTimersByTime(10000);
// update membership ID
member2.membershipID = "newID";
const keysSentPromise2 = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
});
// this should re-send the key
sess.onMembershipUpdate();
await keysSentPromise2;
expect(sendEventMock).toHaveBeenCalledWith(
expect.stringMatching(".*"),
"io.element.call.encryption_keys",
{
call_id: "",
device_id: "AAAAAAA",
keys: [
{
index: 0,
key: expect.stringMatching(".*"),
},
],
},
);
} finally {
jest.useRealTimers();
}
});
it("Re-sends key if a member changes created_ts", async () => {
jest.useFakeTimers();
jest.setSystemTime(1000);
try {
const keysSentPromise1 = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
});
const member1 = { ...membershipTemplate, created_ts: 1000 };
const member2 = {
...membershipTemplate,
created_ts: 1000,
device_id: "BBBBBBB",
};
const mockRoom = makeMockRoom([member1, member2]);
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([member1, member2], mockRoom.roomId));
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
await keysSentPromise1;
// make sure an encryption key was sent
expect(sendEventMock).toHaveBeenCalledWith(
expect.stringMatching(".*"),
"io.element.call.encryption_keys",
{
call_id: "",
device_id: "AAAAAAA",
keys: [
{
index: 0,
key: expect.stringMatching(".*"),
},
],
},
);
sendEventMock.mockClear();
// this should be a no-op:
sess.onMembershipUpdate();
expect(sendEventMock).toHaveBeenCalledTimes(0);
// advance time to avoid key throttling
jest.advanceTimersByTime(10000);
// update created_ts
member2.created_ts = 5000;
const keysSentPromise2 = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
});
// this should re-send the key
sess.onMembershipUpdate();
await keysSentPromise2;
expect(sendEventMock).toHaveBeenCalledWith(
expect.stringMatching(".*"),
"io.element.call.encryption_keys",
{
call_id: "",
device_id: "AAAAAAA",
keys: [
{
index: 0,
key: expect.stringMatching(".*"),
},
],
},
);
} finally {
jest.useRealTimers();
}
});
it("Rotates key if a member leaves", async () => {
jest.useFakeTimers();
try {
@@ -668,7 +955,7 @@ describe("MatrixRTCSession", () => {
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId, undefined));
.mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId));
sess.onMembershipUpdate();
jest.advanceTimersByTime(10000);
@@ -707,7 +994,7 @@ describe("MatrixRTCSession", () => {
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId, undefined));
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
sess.onMembershipUpdate();
await new Promise((resolve) => {
@@ -739,9 +1026,7 @@ describe("MatrixRTCSession", () => {
const onMembershipsChanged = jest.fn();
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([], mockRoom.roomId, undefined));
mockRoom.getLiveTimeline().getState = jest.fn().mockReturnValue(makeMockRoomState([], mockRoom.roomId));
sess.onMembershipUpdate();
expect(onMembershipsChanged).toHaveBeenCalled();
@@ -751,7 +1036,7 @@ describe("MatrixRTCSession", () => {
jest.useFakeTimers();
try {
const membership = Object.assign({}, membershipTemplate);
const mockRoom = makeMockRoom([membership], 0);
const mockRoom = makeMockRoom([membership]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const membershipObject = sess.memberships[0];
@@ -779,7 +1064,7 @@ describe("MatrixRTCSession", () => {
expires: 1000,
}),
];
const mockRoomNoExpired = makeMockRoom(mockMemberships, 0);
const mockRoomNoExpired = makeMockRoom(mockMemberships);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoomNoExpired);
@@ -818,6 +1103,7 @@ describe("MatrixRTCSession", () => {
it("fills in created_ts for other memberships on update", () => {
client.sendStateEvent = jest.fn();
jest.useFakeTimers();
jest.setSystemTime(1000);
const mockRoom = makeMockRoom([
Object.assign({}, membershipTemplate, {
device_id: "OTHERDEVICE",
@@ -875,6 +1161,7 @@ describe("MatrixRTCSession", () => {
],
}),
getSender: jest.fn().mockReturnValue("@bob:example.org"),
getTs: jest.fn().mockReturnValue(Date.now()),
} as unknown as MatrixEvent);
const bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
@@ -898,6 +1185,7 @@ describe("MatrixRTCSession", () => {
],
}),
getSender: jest.fn().mockReturnValue("@bob:example.org"),
getTs: jest.fn().mockReturnValue(Date.now()),
} as unknown as MatrixEvent);
const bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
@@ -909,6 +1197,130 @@ describe("MatrixRTCSession", () => {
expect(bobKeys[4]).toEqual(Buffer.from("this is the key", "utf-8"));
});
it("collects keys by merging", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess.onCallEncryption({
getType: jest.fn().mockReturnValue("io.element.call.encryption_keys"),
getContent: jest.fn().mockReturnValue({
device_id: "bobsphone",
call_id: "",
keys: [
{
index: 0,
key: "dGhpcyBpcyB0aGUga2V5",
},
],
}),
getSender: jest.fn().mockReturnValue("@bob:example.org"),
getTs: jest.fn().mockReturnValue(Date.now()),
} as unknown as MatrixEvent);
let bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
expect(bobKeys).toHaveLength(1);
expect(bobKeys[0]).toEqual(Buffer.from("this is the key", "utf-8"));
sess.onCallEncryption({
getType: jest.fn().mockReturnValue("io.element.call.encryption_keys"),
getContent: jest.fn().mockReturnValue({
device_id: "bobsphone",
call_id: "",
keys: [
{
index: 4,
key: "dGhpcyBpcyB0aGUga2V5",
},
],
}),
getSender: jest.fn().mockReturnValue("@bob:example.org"),
getTs: jest.fn().mockReturnValue(Date.now()),
} as unknown as MatrixEvent);
bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
expect(bobKeys).toHaveLength(5);
expect(bobKeys[4]).toEqual(Buffer.from("this is the key", "utf-8"));
});
it("ignores older keys at same index", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess.onCallEncryption({
getType: jest.fn().mockReturnValue("io.element.call.encryption_keys"),
getContent: jest.fn().mockReturnValue({
device_id: "bobsphone",
call_id: "",
keys: [
{
index: 0,
key: encodeBase64(Buffer.from("newer key", "utf-8")),
},
],
}),
getSender: jest.fn().mockReturnValue("@bob:example.org"),
getTs: jest.fn().mockReturnValue(2000),
} as unknown as MatrixEvent);
sess.onCallEncryption({
getType: jest.fn().mockReturnValue("io.element.call.encryption_keys"),
getContent: jest.fn().mockReturnValue({
device_id: "bobsphone",
call_id: "",
keys: [
{
index: 0,
key: encodeBase64(Buffer.from("older key", "utf-8")),
},
],
}),
getSender: jest.fn().mockReturnValue("@bob:example.org"),
getTs: jest.fn().mockReturnValue(1000), // earlier timestamp than the newer key
} as unknown as MatrixEvent);
const bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
expect(bobKeys).toHaveLength(1);
expect(bobKeys[0]).toEqual(Buffer.from("newer key", "utf-8"));
});
it("key timestamps are treated as monotonic", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess.onCallEncryption({
getType: jest.fn().mockReturnValue("io.element.call.encryption_keys"),
getContent: jest.fn().mockReturnValue({
device_id: "bobsphone",
call_id: "",
keys: [
{
index: 0,
key: encodeBase64(Buffer.from("first key", "utf-8")),
},
],
}),
getSender: jest.fn().mockReturnValue("@bob:example.org"),
getTs: jest.fn().mockReturnValue(1000),
} as unknown as MatrixEvent);
sess.onCallEncryption({
getType: jest.fn().mockReturnValue("io.element.call.encryption_keys"),
getContent: jest.fn().mockReturnValue({
device_id: "bobsphone",
call_id: "",
keys: [
{
index: 0,
key: encodeBase64(Buffer.from("second key", "utf-8")),
},
],
}),
getSender: jest.fn().mockReturnValue("@bob:example.org"),
getTs: jest.fn().mockReturnValue(1000), // same timestamp as the first key
} as unknown as MatrixEvent);
const bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
expect(bobKeys).toHaveLength(1);
expect(bobKeys[0]).toEqual(Buffer.from("second key", "utf-8"));
});
it("ignores keys event for the local participant", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
@@ -925,6 +1337,7 @@ describe("MatrixRTCSession", () => {
],
}),
getSender: jest.fn().mockReturnValue(client.getUserId()),
getTs: jest.fn().mockReturnValue(Date.now()),
} as unknown as MatrixEvent);
const myKeys = sess.getKeysForParticipant(client.getUserId()!, client.getDeviceId()!)!;
@@ -102,6 +102,7 @@ describe("MatrixRTCSessionManager", () => {
getContent: jest.fn().mockReturnValue({}),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getRoomId: jest.fn().mockReturnValue("!room:id"),
isDecryptionFailure: jest.fn().mockReturnValue(false),
sender: {
userId: "@mock:user.example",
},
@@ -110,4 +111,93 @@ describe("MatrixRTCSessionManager", () => {
await new Promise(process.nextTick);
expect(onCallEncryptionMock).toHaveBeenCalled();
});
describe("event decryption", () => {
it("Retries decryption and processes success", async () => {
try {
jest.useFakeTimers();
const room1 = makeMockRoom([membershipTemplate]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);
client.emit(ClientEvent.Room, room1);
const onCallEncryptionMock = jest.fn();
client.matrixRTC.getRoomSession(room1).onCallEncryption = onCallEncryptionMock;
let isDecryptionFailure = true;
client.decryptEventIfNeeded = jest
.fn()
.mockReturnValueOnce(Promise.resolve())
.mockImplementation(() => {
isDecryptionFailure = false;
return Promise.resolve();
});
const timelineEvent = {
getType: jest.fn().mockReturnValue(EventType.CallEncryptionKeysPrefix),
getContent: jest.fn().mockReturnValue({}),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getRoomId: jest.fn().mockReturnValue("!room:id"),
isDecryptionFailure: jest.fn().mockImplementation(() => isDecryptionFailure),
getId: jest.fn().mockReturnValue("event_id"),
sender: {
userId: "@mock:user.example",
},
} as unknown as MatrixEvent;
client.emit(RoomEvent.Timeline, timelineEvent, undefined, undefined, false, {} as IRoomTimelineData);
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(1);
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
// should retry after one second:
await jest.advanceTimersByTimeAsync(1500);
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
expect(onCallEncryptionMock).toHaveBeenCalledTimes(1);
} finally {
jest.useRealTimers();
}
});
it("Retries decryption and processes failure", async () => {
try {
jest.useFakeTimers();
const room1 = makeMockRoom([membershipTemplate]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);
client.emit(ClientEvent.Room, room1);
const onCallEncryptionMock = jest.fn();
client.matrixRTC.getRoomSession(room1).onCallEncryption = onCallEncryptionMock;
client.decryptEventIfNeeded = jest.fn().mockReturnValue(Promise.resolve());
const timelineEvent = {
getType: jest.fn().mockReturnValue(EventType.CallEncryptionKeysPrefix),
getContent: jest.fn().mockReturnValue({}),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getRoomId: jest.fn().mockReturnValue("!room:id"),
isDecryptionFailure: jest.fn().mockReturnValue(true), // always fail
getId: jest.fn().mockReturnValue("event_id"),
sender: {
userId: "@mock:user.example",
},
} as unknown as MatrixEvent;
client.emit(RoomEvent.Timeline, timelineEvent, undefined, undefined, false, {} as IRoomTimelineData);
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(1);
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
// should retry after one second:
await jest.advanceTimersByTimeAsync(1500);
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
// doesn't retry again:
await jest.advanceTimersByTimeAsync(1500);
expect(client.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
expect(onCallEncryptionMock).toHaveBeenCalledTimes(0);
} finally {
jest.useRealTimers();
}
});
});
});
+7 -7
View File
@@ -20,10 +20,10 @@ import { randomString } from "../../../src/randomstring";
type MembershipData = CallMembershipData[] | SessionMembershipData;
export function makeMockRoom(membershipData: MembershipData, localAge: number | null = null): Room {
export function makeMockRoom(membershipData: MembershipData): Room {
const roomId = randomString(8);
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
const roomState = makeMockRoomState(membershipData, roomId, localAge);
const roomState = makeMockRoomState(membershipData, roomId);
return {
roomId: roomId,
hasMembershipState: jest.fn().mockReturnValue(true),
@@ -34,8 +34,8 @@ export function makeMockRoom(membershipData: MembershipData, localAge: number |
} as unknown as Room;
}
export function makeMockRoomState(membershipData: MembershipData, roomId: string, localAge: number | null = null) {
const event = mockRTCEvent(membershipData, roomId, localAge);
export function makeMockRoomState(membershipData: MembershipData, roomId: string) {
const event = mockRTCEvent(membershipData, roomId);
return {
on: jest.fn(),
off: jest.fn(),
@@ -57,7 +57,7 @@ export function makeMockRoomState(membershipData: MembershipData, roomId: string
};
}
export function mockRTCEvent(membershipData: MembershipData, roomId: string, localAge: number | null): MatrixEvent {
export function mockRTCEvent(membershipData: MembershipData, roomId: string): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue(
@@ -68,11 +68,11 @@ export function mockRTCEvent(membershipData: MembershipData, roomId: string, loc
},
),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getTs: jest.fn().mockReturnValue(1000),
localTimestamp: Date.now() - (localAge ?? 10),
getTs: jest.fn().mockReturnValue(Date.now()),
getRoomId: jest.fn().mockReturnValue(roomId),
sender: {
userId: "@mock:user.example",
},
isDecryptionFailure: jest.fn().mockReturnValue(false),
} as unknown as MatrixEvent;
}
+6 -1
View File
@@ -412,7 +412,12 @@ describe("MatrixEvent", () => {
const crypto = {
decryptEvent: jest
.fn()
.mockRejectedValue("DecryptionError: The sender has disabled encrypting to unverified devices."),
.mockRejectedValue(
new DecryptionError(
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
"The sender has disabled encrypting to unverified devices.",
),
),
} as unknown as Crypto;
await encryptedEvent.attemptDecryption(crypto);
+17 -16
View File
@@ -26,7 +26,6 @@ import { getRandomValues } from "node:crypto";
import { TextEncoder } from "node:util";
import { Method } from "../../../src";
import * as crypto from "../../../src/crypto/crypto";
import { logger } from "../../../src/logger";
import {
completeAuthorizationCodeGrant,
@@ -39,11 +38,6 @@ import { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "../../test-uti
jest.mock("jwt-decode");
const webCrypto = new Crypto();
// save for resetting mocks
const realSubtleCrypto = crypto.subtleCrypto;
describe("oidc authorization", () => {
const delegatedAuthConfig = makeDelegatedAuthConfig();
const authorizationEndpoint = delegatedAuthConfig.authorizationEndpoint;
@@ -62,7 +56,11 @@ describe("oidc authorization", () => {
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
mockOpenIdConfiguration(),
);
global.TextEncoder = TextEncoder;
});
beforeEach(() => {
const webCrypto = new Crypto();
Object.defineProperty(window, "crypto", {
value: {
getRandomValues,
@@ -70,12 +68,6 @@ describe("oidc authorization", () => {
subtle: webCrypto.subtle,
},
});
global.TextEncoder = TextEncoder;
});
afterEach(() => {
// @ts-ignore reset any ugly mocking we did
crypto.subtleCrypto = realSubtleCrypto;
});
it("should generate authorization params", () => {
@@ -97,11 +89,8 @@ describe("oidc authorization", () => {
describe("generateAuthorizationUrl()", () => {
it("should generate url with correct parameters", async () => {
// test the no crypto case here
// @ts-ignore mocking
crypto.subtleCrypto = undefined;
const authorizationParams = generateAuthorizationParams({ redirectUri: baseUrl });
authorizationParams.codeVerifier = "test-code-verifier";
const authUrl = new URL(
await generateAuthorizationUrl(authorizationEndpoint, clientId, authorizationParams),
);
@@ -113,6 +102,18 @@ describe("oidc authorization", () => {
expect(authUrl.searchParams.get("scope")).toEqual(authorizationParams.scope);
expect(authUrl.searchParams.get("state")).toEqual(authorizationParams.state);
expect(authUrl.searchParams.get("nonce")).toEqual(authorizationParams.nonce);
expect(authUrl.searchParams.get("code_challenge")).toEqual("0FLIKahrX7kqxncwhV5WD82lu_wi5GA8FsRSLubaOpU");
});
it("should log a warning if crypto is not available", async () => {
// test the no crypto case here
// @ts-ignore mocking
globalThis.crypto.subtle = undefined;
const authorizationParams = generateAuthorizationParams({ redirectUri: baseUrl });
const authUrl = new URL(
await generateAuthorizationUrl(authorizationEndpoint, clientId, authorizationParams),
);
// crypto not available, plain text code_challenge is used
expect(authUrl.searchParams.get("code_challenge")).toEqual(authorizationParams.codeVerifier);
+55
View File
@@ -1122,4 +1122,59 @@ describe("RoomState", function () {
).toBeFalsy();
});
});
describe("skipWrongOrderRoomStateInserts", () => {
const idNow = "now";
const idBefore = "before";
const endState = new RoomState(roomId);
const startState = new RoomState(roomId, undefined, true);
let onRoomStateEvent: (event: MatrixEvent, state: RoomState, lastStateEvent: MatrixEvent | null) => void;
const evNow = new MatrixEvent({
type: "m.call.member",
room_id: roomId,
state_key: "@user:example.org",
content: {},
});
evNow.event.unsigned = { replaces_state: idBefore };
evNow.event.event_id = idNow;
const evBefore = new MatrixEvent({
type: "m.call.member",
room_id: roomId,
state_key: "@user:example.org",
content: {},
});
const updatedStateWithBefore = jest.fn();
const updatedStateWithNow = jest.fn();
beforeEach(() => {
evBefore.event.event_id = idBefore;
onRoomStateEvent = (event, _state, _lastState) => {
if (event.event.event_id === idNow) {
updatedStateWithNow();
} else if (event.event.event_id === idBefore) {
updatedStateWithBefore();
}
};
endState.on(RoomStateEvent.Events, onRoomStateEvent);
startState.on(RoomStateEvent.Events, onRoomStateEvent);
});
afterEach(() => {
endState.off(RoomStateEvent.Events, onRoomStateEvent);
startState.off(RoomStateEvent.Events, onRoomStateEvent);
updatedStateWithNow.mockReset();
updatedStateWithBefore.mockReset();
});
it("should skip inserting state to the end of the timeline if the current endState events replaces_state id is the same as the inserted events id", () => {
endState.setStateEvents([evNow, evBefore]);
expect(updatedStateWithBefore).not.toHaveBeenCalled();
expect(updatedStateWithNow).toHaveBeenCalled();
});
it("should skip inserting state at the beginning of the timeline if the inserted events replaces_state is the event id of the current startState", () => {
startState.setStateEvents([evBefore, evNow]);
expect(updatedStateWithBefore).toHaveBeenCalled();
expect(updatedStateWithNow).not.toHaveBeenCalled();
});
});
});
@@ -95,6 +95,7 @@ describe("initRustCrypto", () => {
deleteSecretsFromInbox: jest.fn(),
registerReceiveSecretCallback: jest.fn(),
registerDevicesUpdatedCallback: jest.fn(),
registerRoomKeysWithheldCallback: jest.fn(),
outgoingRequests: jest.fn(),
isBackupEnabled: jest.fn().mockResolvedValue(false),
verifyBackup: jest.fn().mockResolvedValue({ trusted: jest.fn().mockReturnValue(false) }),
+1 -1
View File
@@ -434,7 +434,7 @@ describe("TimelineWindow", function () {
});
function idsOf(events: Array<MatrixEvent>): Array<string> {
return events.map((e) => (e ? e.getId() ?? "MISSING_ID" : "MISSING_EVENT"));
return events.map((e) => (e ? (e.getId() ?? "MISSING_ID") : "MISSING_EVENT"));
}
describe("removing events", () => {
+1 -1
View File
@@ -105,7 +105,7 @@ const mockGetStateEvents =
(events: MatrixEvent[] = FAKE_STATE_EVENTS as MatrixEvent[]) =>
(type: EventType, userId?: string): MatrixEvent[] | MatrixEvent | null => {
if (type === EventType.GroupCallMemberPrefix) {
return userId === undefined ? events : events.find((e) => e.getStateKey() === userId) ?? null;
return userId === undefined ? events : (events.find((e) => e.getStateKey() === userId) ?? null);
} else {
const fakeEvent = { getContent: () => ({}), getTs: () => 0 } as MatrixEvent;
return userId === undefined ? [fakeEvent] : fakeEvent;
-45
View File
@@ -29,20 +29,11 @@ declare global {
namespace NodeJS {
interface Global {
localStorage: Storage;
// marker variable used to detect both the browser & node entrypoints being used at once
__js_sdk_entrypoint: unknown;
}
}
interface Window {
webkitAudioContext: typeof AudioContext;
}
interface Crypto {
webkitSubtle?: Window["crypto"]["subtle"];
}
interface MediaDevices {
// This is experimental and types don't know about it yet
// https://github.com/microsoft/TypeScript/issues/33232
@@ -76,40 +67,4 @@ declare global {
// on webkit: we should check if we still need to do this
webkitGetUserMedia: DummyInterfaceWeShouldntBeUsingThis;
}
export interface ISettledFulfilled<T> {
status: "fulfilled";
value: T;
}
export interface ISettledRejected {
status: "rejected";
reason: any;
}
interface PromiseConstructor {
allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>>;
}
interface RTCRtpTransceiver {
// This has been removed from TS
// (https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1029),
// but we still need this for MatrixCall::getRidOfRTXCodecs()
setCodecPreferences(codecs: RTCRtpCodecCapability[]): void;
}
interface RequestInit {
/**
* Specifies the priority of the fetch request relative to other requests of the same type.
* Must be one of the following strings:
* high: A high priority fetch request relative to other requests of the same type.
* low: A low priority fetch request relative to other requests of the same type.
* auto: Automatically determine the priority of the fetch request relative to other requests of the same type (default).
*
* @see https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fetch-priority-attribute
* @see https://github.com/microsoft/TypeScript/issues/54472
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#browser_compatibility
* Not yet supported in Safari or Firefox
*/
priority?: "high" | "low" | "auto";
}
}
+50
View File
@@ -76,6 +76,56 @@ export interface ISendEventResponse {
event_id: string;
}
export type TimeoutDelay = {
delay: number;
};
export type ParentDelayId = {
parent_delay_id: string;
};
export type SendTimeoutDelayedEventRequestOpts = TimeoutDelay & Partial<ParentDelayId>;
export type SendActionDelayedEventRequestOpts = ParentDelayId;
export type SendDelayedEventRequestOpts = SendTimeoutDelayedEventRequestOpts | SendActionDelayedEventRequestOpts;
export type SendDelayedEventResponse = {
delay_id: string;
};
export enum UpdateDelayedEventAction {
Cancel = "cancel",
Restart = "restart",
Send = "send",
}
export type UpdateDelayedEventRequestOpts = SendDelayedEventResponse & {
action: UpdateDelayedEventAction;
};
type DelayedPartialTimelineEvent = {
room_id: string;
type: string;
content: IContent;
};
type DelayedPartialStateEvent = DelayedPartialTimelineEvent & {
state_key: string;
transaction_id: string;
};
type DelayedPartialEvent = DelayedPartialTimelineEvent | DelayedPartialStateEvent;
export type DelayedEventInfo = {
delayed_events: DelayedPartialEvent &
SendDelayedEventResponse &
SendDelayedEventRequestOpts &
{
running_since: number;
}[];
next_batch?: string;
};
export interface IPresenceOpts {
// One of "online", "offline" or "unavailable"
presence: "online" | "offline" | "unavailable";
+248 -53
View File
@@ -47,7 +47,7 @@ import { Direction, EventTimeline } from "./models/event-timeline";
import { IActionsObject, PushProcessor } from "./pushprocessor";
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
import * as olmlib from "./crypto/olmlib";
import { decodeBase64, encodeBase64 } from "./base64";
import { decodeBase64, encodeBase64, encodeUnpaddedBase64Url } from "./base64";
import { IExportedDevice as IExportedOlmDevice } from "./crypto/OlmDevice";
import { IOlmDevice } from "./crypto/algorithms/megolm";
import { TypedReEmitter } from "./ReEmitter";
@@ -114,6 +114,7 @@ import { NotificationCountType, Room, RoomEvent, RoomEventHandlerMap, RoomNameSt
import { RoomMemberEvent, RoomMemberEventHandlerMap } from "./models/room-member";
import { IPowerLevelsContent, RoomStateEvent, RoomStateEventHandlerMap } from "./models/room-state";
import {
DelayedEventInfo,
IAddThreePidOnlyBody,
IBindThreePidBody,
IContextResponse,
@@ -134,6 +135,9 @@ import {
IStatusResponse,
ITagsResponse,
KnockRoomOpts,
SendDelayedEventRequestOpts,
SendDelayedEventResponse,
UpdateDelayedEventAction,
} from "./@types/requests";
import {
EventType,
@@ -227,6 +231,7 @@ import { KnownMembership, Membership } from "./@types/membership";
import { RoomMessageEventContent, StickerEventContent } from "./@types/events";
import { ImageInfo } from "./@types/media";
import { Capabilities, ServerCapabilities } from "./serverCapabilities";
import { sha256 } from "./digest";
export type Store = IStore;
@@ -324,10 +329,13 @@ export interface ICreateClientOpts {
localTimeoutMs?: number;
/**
* Set to true to use
* Authorization header instead of query param to send the access token to the server.
* Set to false to send the access token to the server via a query parameter rather
* than the Authorization HTTP header.
*
* Default false.
* Note that as of v1.11 of the Matrix spec, sending the access token via a query
* is deprecated.
*
* Default true.
*/
useAuthorizationHeader?: boolean;
@@ -527,6 +535,8 @@ export const UNSTABLE_MSC2666_SHARED_ROOMS = "uk.half-shot.msc2666";
export const UNSTABLE_MSC2666_MUTUAL_ROOMS = "uk.half-shot.msc2666.mutual_rooms";
export const UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS = "uk.half-shot.msc2666.query_mutual_rooms";
export const UNSTABLE_MSC4140_DELAYED_EVENTS = "org.matrix.msc4140";
enum CrossSigningKeyType {
MasterKey = "master_key",
SelfSigningKey = "self_signing_key",
@@ -1075,7 +1085,7 @@ export type ClientEventHandlerMap = {
* });
* ```
*/
[ClientEvent.Sync]: (state: SyncState, lastState: SyncState | null, data?: ISyncStateData) => void;
[ClientEvent.Sync]: (state: SyncState, prevState: SyncState | null, data?: ISyncStateData) => void;
/**
* Fires whenever the SDK receives a new event.
* <p>
@@ -2232,9 +2242,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
cryptoCallbacks: this.cryptoCallbacks,
storePrefix: args.useIndexedDB === false ? null : RUST_SDK_STORE_PREFIX,
storeKey: args.storageKey,
// temporary compatibility hack: if there is no storageKey nor storagePassword, fall back to the pickleKey
storePassphrase: args.storagePassword ?? this.pickleKey,
storePassphrase: args.storagePassword,
legacyCryptoStore: this.cryptoStore,
legacyPickleKey: this.pickleKey ?? "DEFAULT_KEY",
@@ -4572,12 +4580,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
threadId = threadIdOrEventType;
}
// If we expect that an event is part of a thread but is missing the relation
// we need to add it manually, as well as the reply fallback
if (threadId && !content!["m.relates_to"]?.rel_type) {
const isReply = !!content!["m.relates_to"]?.["m.in_reply_to"];
content!["m.relates_to"] = {
...content!["m.relates_to"],
this.addThreadRelationIfNeeded(content, threadId, roomId);
return this.sendCompleteEvent(roomId, threadId, { type: eventType, content }, txnId);
}
/**
* If we expect that an event is part of a thread but is missing the relation
* we need to add it manually, as well as the reply fallback
*/
private addThreadRelationIfNeeded(content: IContent, threadId: string | null, roomId: string): void {
if (threadId && !content["m.relates_to"]?.rel_type) {
const isReply = !!content["m.relates_to"]?.["m.in_reply_to"];
content["m.relates_to"] = {
...content["m.relates_to"],
rel_type: THREAD_RELATION_TYPE.name,
event_id: threadId,
// Set is_falling_back to true unless this is actually intended to be a reply
@@ -4585,7 +4600,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
};
const thread = this.getRoom(roomId)?.getThread(threadId);
if (thread && !isReply) {
content!["m.relates_to"]["m.in_reply_to"] = {
content["m.relates_to"]["m.in_reply_to"] = {
event_id:
thread
.lastReply((ev: MatrixEvent) => {
@@ -4595,8 +4610,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
};
}
}
return this.sendCompleteEvent(roomId, threadId, { type: eventType, content }, txnId);
}
/**
@@ -4610,7 +4623,38 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
threadId: string | null,
eventObject: Partial<IEvent>,
txnId?: string,
): Promise<ISendEventResponse> {
): Promise<ISendEventResponse>;
/**
* Sends a delayed event (MSC4140).
* @param eventObject - An object with the partial structure of an event, to which event_id, user_id, room_id and origin_server_ts will be added.
* @param delayOpts - Properties of the delay for this event.
* @param txnId - Optional.
* @returns Promise which resolves: to an empty object `{}`
* @returns Rejects: with an error response.
*/
private sendCompleteEvent(
roomId: string,
threadId: string | null,
eventObject: Partial<IEvent>,
delayOpts: SendDelayedEventRequestOpts,
txnId?: string,
): Promise<SendDelayedEventResponse>;
private sendCompleteEvent(
roomId: string,
threadId: string | null,
eventObject: Partial<IEvent>,
delayOptsOrTxnId?: SendDelayedEventRequestOpts | string,
txnIdOrVoid?: string,
): Promise<ISendEventResponse | SendDelayedEventResponse> {
let delayOpts: SendDelayedEventRequestOpts | undefined;
let txnId: string | undefined;
if (typeof delayOptsOrTxnId === "string") {
txnId = delayOptsOrTxnId;
} else {
delayOpts = delayOptsOrTxnId;
txnId = txnIdOrVoid;
}
if (!txnId) {
txnId = this.makeTxnId();
}
@@ -4633,9 +4677,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
localEvent.setThread(thread);
}
// set up re-emitter for this new event - this is normally the job of EventMapper but we don't use it here
this.reEmitter.reEmit(localEvent, [MatrixEventEvent.Replaced, MatrixEventEvent.VisibilityChange]);
room?.reEmitter.reEmit(localEvent, [MatrixEventEvent.BeforeRedaction]);
if (!delayOpts) {
// set up re-emitter for this new event - this is normally the job of EventMapper but we don't use it here
this.reEmitter.reEmit(localEvent, [MatrixEventEvent.Replaced, MatrixEventEvent.VisibilityChange]);
room?.reEmitter.reEmit(localEvent, [MatrixEventEvent.BeforeRedaction]);
}
// if this is a relation or redaction of an event
// that hasn't been sent yet (e.g. with a local id starting with a ~)
@@ -4650,29 +4696,56 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
const type = localEvent.getType();
this.logger.debug(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`);
this.logger.debug(
`sendEvent of type ${type} in ${roomId} with txnId ${txnId}${delayOpts ? " (delayed event)" : ""}`,
);
localEvent.setTxnId(txnId);
localEvent.setStatus(EventStatus.SENDING);
// add this event immediately to the local store as 'sending'.
room?.addPendingEvent(localEvent, txnId);
// TODO: separate store for delayed events?
if (!delayOpts) {
// add this event immediately to the local store as 'sending'.
room?.addPendingEvent(localEvent, txnId);
// addPendingEvent can change the state to NOT_SENT if it believes
// that there's other events that have failed. We won't bother to
// try sending the event if the state has changed as such.
if (localEvent.status === EventStatus.NOT_SENT) {
return Promise.reject(new Error("Event blocked by other events not yet sent"));
// addPendingEvent can change the state to NOT_SENT if it believes
// that there's other events that have failed. We won't bother to
// try sending the event if the state has changed as such.
if (localEvent.status === EventStatus.NOT_SENT) {
return Promise.reject(new Error("Event blocked by other events not yet sent"));
}
return this.encryptAndSendEvent(room, localEvent);
} else {
return this.encryptAndSendEvent(room, localEvent, delayOpts);
}
return this.encryptAndSendEvent(room, localEvent);
}
/**
* encrypts the event if necessary; adds the event to the queue, or sends it; marks the event as sent/unsent
* @returns returns a promise which resolves with the result of the send request
*/
protected async encryptAndSendEvent(room: Room | null, event: MatrixEvent): Promise<ISendEventResponse> {
protected async encryptAndSendEvent(room: Room | null, event: MatrixEvent): Promise<ISendEventResponse>;
/**
* Simply sends a delayed event without encrypting it.
* TODO: Allow encrypted delayed events, and encrypt them properly
* @param delayOpts - Properties of the delay for this event.
* @returns returns a promise which resolves with the result of the delayed send request
*/
protected async encryptAndSendEvent(
room: Room | null,
event: MatrixEvent,
delayOpts: SendDelayedEventRequestOpts,
): Promise<SendDelayedEventResponse>;
protected async encryptAndSendEvent(
room: Room | null,
event: MatrixEvent,
delayOpts?: SendDelayedEventRequestOpts,
): Promise<ISendEventResponse | SendDelayedEventResponse> {
if (delayOpts) {
return this.sendEventHttpRequest(event, delayOpts);
}
try {
let cancelled: boolean;
this.eventsBeingEncrypted.add(event.getId()!);
@@ -4823,7 +4896,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
}
private sendEventHttpRequest(event: MatrixEvent): Promise<ISendEventResponse> {
private sendEventHttpRequest(event: MatrixEvent): Promise<ISendEventResponse>;
private sendEventHttpRequest(
event: MatrixEvent,
delayOpts: SendDelayedEventRequestOpts,
): Promise<SendDelayedEventResponse>;
private sendEventHttpRequest(
event: MatrixEvent,
delayOpts?: SendDelayedEventRequestOpts,
): Promise<ISendEventResponse | SendDelayedEventResponse> {
let txnId = event.getTxnId();
if (!txnId) {
txnId = this.makeTxnId();
@@ -4855,12 +4936,20 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
path = utils.encodeUri("/rooms/$roomId/send/$eventType/$txnId", pathParams);
}
return this.http
.authedRequest<ISendEventResponse>(Method.Put, path, undefined, event.getWireContent())
.then((res) => {
const content = event.getWireContent();
if (!delayOpts) {
return this.http.authedRequest<ISendEventResponse>(Method.Put, path, undefined, content).then((res) => {
this.logger.debug(`Event sent to ${event.getRoomId()} with event id ${res.event_id}`);
return res;
});
} else {
return this.http.authedRequest<SendDelayedEventResponse>(
Method.Put,
path,
getUnstableDelayQueryOpts(delayOpts),
content,
);
}
}
/**
@@ -5190,6 +5279,101 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.sendMessage(roomId, threadId, content);
}
/**
* Send a delayed timeline event.
*
* Note: This endpoint is unstable, and can throw an `Error`.
* Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details.
*/
// eslint-disable-next-line
public async _unstable_sendDelayedEvent<K extends keyof TimelineEvents>(
roomId: string,
delayOpts: SendDelayedEventRequestOpts,
threadId: string | null,
eventType: K,
content: TimelineEvents[K],
txnId?: string,
): Promise<SendDelayedEventResponse> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw Error("Server does not support the delayed events API");
}
this.addThreadRelationIfNeeded(content, threadId, roomId);
return this.sendCompleteEvent(roomId, threadId, { type: eventType, content }, delayOpts, txnId);
}
/**
* Send a delayed state event.
*
* Note: This endpoint is unstable, and can throw an `Error`.
* Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details.
*/
// eslint-disable-next-line
public async _unstable_sendDelayedStateEvent<K extends keyof StateEvents>(
roomId: string,
delayOpts: SendDelayedEventRequestOpts,
eventType: K,
content: StateEvents[K],
stateKey = "",
opts: IRequestOpts = {},
): Promise<SendDelayedEventResponse> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw Error("Server does not support the delayed events API");
}
const pathParams = {
$roomId: roomId,
$eventType: eventType,
$stateKey: stateKey,
};
let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
if (stateKey !== undefined) {
path = utils.encodeUri(path + "/$stateKey", pathParams);
}
return this.http.authedRequest(Method.Put, path, getUnstableDelayQueryOpts(delayOpts), content as Body, opts);
}
/**
* Get all pending delayed events for the calling user.
*
* Note: This endpoint is unstable, and can throw an `Error`.
* Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details.
*/
// eslint-disable-next-line
public async _unstable_getDelayedEvents(fromToken?: string): Promise<DelayedEventInfo> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw Error("Server does not support the delayed events API");
}
const queryDict = fromToken ? { from: fromToken } : undefined;
return await this.http.authedRequest(Method.Get, "/delayed_events", queryDict, undefined, {
prefix: `${ClientPrefix.Unstable}/${UNSTABLE_MSC4140_DELAYED_EVENTS}`,
});
}
/**
* Manage a delayed event associated with the given delay_id.
*
* Note: This endpoint is unstable, and can throw an `Error`.
* Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details.
*/
// eslint-disable-next-line
public async _unstable_updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise<{}> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw Error("Server does not support the delayed events API");
}
const path = utils.encodeUri("/delayed_events/$delayId", {
$delayId: delayId,
});
const data = {
action,
};
return await this.http.authedRequest(Method.Post, path, undefined, data, {
prefix: `${ClientPrefix.Unstable}/${UNSTABLE_MSC4140_DELAYED_EVENTS}`,
});
}
/**
* Send a receipt.
* @param event - The event being acknowledged
@@ -5403,10 +5587,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
private findPredecessorRooms(room: Room, verifyLinks: boolean, msc3946ProcessDynamicPredecessor: boolean): Room[] {
const ret: Room[] = [];
const seenRoomIDs = new Set<string>([room.roomId]);
// Work backwards from newer to older rooms
let predecessorRoomId = room.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
while (predecessorRoomId !== null) {
if (predecessorRoomId) {
if (seenRoomIDs.has(predecessorRoomId)) break;
seenRoomIDs.add(predecessorRoomId);
}
const predecessorRoom = this.getRoom(predecessorRoomId);
if (predecessorRoom === null) {
break;
@@ -6605,13 +6794,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* Peek into a room and receive updates about the room. This only works if the
* history visibility for the room is world_readable.
* @param roomId - The room to attempt to peek into.
* @param limit - The number of timeline events to initially retrieve.
* @returns Promise which resolves: Room object
* @returns Rejects: with an error response.
*/
public peekInRoom(roomId: string): Promise<Room> {
public peekInRoom(roomId: string, limit: number = 20): Promise<Room> {
this.peekSync?.stopPeeking();
this.peekSync = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
return this.peekSync.peek(roomId);
return this.peekSync.peek(roomId, limit);
}
/**
@@ -7410,7 +7600,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.http.authedRequest(Method.Post, path, undefined, undefined, { prefix: "" });
}
private async fetchClientWellKnown(): Promise<void> {
protected async fetchClientWellKnown(): Promise<void> {
// `getRawClientConfig` does not throw or reject on network errors, instead
// it absorbs errors and returns `{}`.
this.clientWellKnownPromise = AutoDiscovery.getRawClientConfig(this.getDomain() ?? undefined);
@@ -9300,20 +9490,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// When picking an algorithm, we pick the hashed over no hashes
if (hashes["algorithms"].includes("sha256")) {
// Abuse the olm hashing
const olmutil = new global.Olm.Utility();
params["addresses"] = addressPairs.map((p) => {
const addr = p[0].toLowerCase(); // lowercase to get consistent hashes
const med = p[1].toLowerCase();
const hashed = olmutil
.sha256(`${addr} ${med} ${params["pepper"]}`)
.replace(/\+/g, "-")
.replace(/\//g, "_"); // URL-safe base64
// Map the hash to a known (case-sensitive) address. We use the case
// sensitive version because the caller might be expecting that.
localMapping[hashed] = p[0];
return hashed;
});
params["addresses"] = await Promise.all(
addressPairs.map(async (p) => {
const addr = p[0].toLowerCase(); // lowercase to get consistent hashes
const med = p[1].toLowerCase();
const hashBuffer = await sha256(`${addr} ${med} ${params["pepper"]}`);
const hashed = encodeUnpaddedBase64Url(hashBuffer);
// Map the hash to a known (case-sensitive) address. We use the case
// sensitive version because the caller might be expecting that.
localMapping[hashed] = p[0];
return hashed;
}),
);
params["algorithm"] = "sha256";
} else if (hashes["algorithms"].includes("none")) {
params["addresses"] = addressPairs.map((p) => {
@@ -9890,6 +10079,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
}
function getUnstableDelayQueryOpts(delayOpts: SendDelayedEventRequestOpts): QueryDict {
return Object.fromEntries(
Object.entries(delayOpts).map(([k, v]) => [`${UNSTABLE_MSC4140_DELAYED_EVENTS}.${k}`, v]),
);
}
/**
* recalculates an accurate notifications count on event decryption.
* Servers do not have enough knowledge about encrypted events to calculate an
+24 -13
View File
@@ -15,15 +15,15 @@ limitations under the License.
*/
import type { SecretsBundle } from "@matrix-org/matrix-sdk-crypto-wasm";
import type { IMegolmSessionData } from "./@types/crypto";
import { Room } from "./models/room";
import { DeviceMap } from "./models/device";
import { UIAuthCallback } from "./interactive-auth";
import { PassphraseInfo, SecretStorageCallbacks, SecretStorageKeyDescription } from "./secret-storage";
import { VerificationRequest } from "./crypto-api/verification";
import { BackupTrustInfo, KeyBackupCheck, KeyBackupInfo } from "./crypto-api/keybackup";
import { ISignatures } from "./@types/signed";
import { MatrixEvent } from "./models/event";
import type { IMegolmSessionData } from "../@types/crypto";
import { Room } from "../models/room";
import { DeviceMap } from "../models/device";
import { UIAuthCallback } from "../interactive-auth";
import { PassphraseInfo, SecretStorageCallbacks, SecretStorageKeyDescription } from "../secret-storage";
import { VerificationRequest } from "./verification";
import { BackupTrustInfo, KeyBackupCheck, KeyBackupInfo } from "./keybackup";
import { ISignatures } from "../@types/signed";
import { MatrixEvent } from "../models/event";
/**
* Public interface to the cryptography parts of the js-sdk
@@ -557,6 +557,12 @@ export enum DecryptionFailureCode {
/** Message was encrypted with a Megolm session whose keys have not been shared with us. */
MEGOLM_UNKNOWN_INBOUND_SESSION_ID = "MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
/** A special case of {@link MEGOLM_UNKNOWN_INBOUND_SESSION_ID}: the sender has told us it is withholding the key. */
MEGOLM_KEY_WITHHELD = "MEGOLM_KEY_WITHHELD",
/** A special case of {@link MEGOLM_KEY_WITHHELD}: the sender has told us it is withholding the key, because the current device is unverified. */
MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE = "MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE",
/** Message was encrypted with a Megolm session which has been shared with us, but in a later ratchet state. */
OLM_UNKNOWN_MESSAGE_INDEX = "OLM_UNKNOWN_MESSAGE_INDEX",
@@ -848,9 +854,14 @@ export interface CreateSecretStorageOpts {
setupNewSecretStorage?: boolean;
/**
* Function called to get the user's
* current key backup passphrase. Should return a promise that resolves with a Uint8Array
* Function called to get the user's current key backup passphrase.
*
* Should return a promise that resolves with a Uint8Array
* containing the key, or rejects if the key cannot be obtained.
*
* Only used when the client has existing key backup, but no secret storage.
*
* @deprecated Not used by the Rust crypto stack.
*/
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
}
@@ -949,5 +960,5 @@ export interface OwnDeviceKeys {
curve25519: string;
}
export * from "./crypto-api/verification";
export * from "./crypto-api/keybackup";
export * from "./verification";
export * from "./keybackup";
+14 -14
View File
@@ -1221,13 +1221,13 @@ export class OlmDevice {
this.getInboundGroupSession(roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => {
if (session === null || sessionData === null) {
if (withheld) {
error = new DecryptionError(
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
calculateWithheldMessage(withheld),
{
session: senderKey + "|" + sessionId,
},
);
const failureCode =
withheld.code === "m.unverified"
? DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE
: DecryptionFailureCode.MEGOLM_KEY_WITHHELD;
error = new DecryptionError(failureCode, calculateWithheldMessage(withheld), {
session: senderKey + "|" + sessionId,
});
}
result = null;
return;
@@ -1237,13 +1237,13 @@ export class OlmDevice {
res = session.decrypt(body);
} catch (e) {
if ((<Error>e)?.message === "OLM.UNKNOWN_MESSAGE_INDEX" && withheld) {
error = new DecryptionError(
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
calculateWithheldMessage(withheld),
{
session: senderKey + "|" + sessionId,
},
);
const failureCode =
withheld.code === "m.unverified"
? DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE
: DecryptionFailureCode.MEGOLM_KEY_WITHHELD;
error = new DecryptionError(failureCode, calculateWithheldMessage(withheld), {
session: senderKey + "|" + sessionId,
});
} else {
error = <Error>e;
}
+12 -10
View File
@@ -15,7 +15,6 @@ limitations under the License.
*/
import { decodeBase64, encodeBase64 } from "../base64";
import { subtleCrypto, crypto, TextEncoder } from "./crypto";
// salt for HKDF, with 8 bytes of zeros
const zeroSalt = new Uint8Array(8);
@@ -49,7 +48,7 @@ export async function encryptAES(
iv = decodeBase64(ivStr);
} else {
iv = new Uint8Array(16);
crypto.getRandomValues(iv);
globalThis.crypto.getRandomValues(iv);
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
@@ -60,7 +59,7 @@ export async function encryptAES(
const [aesKey, hmacKey] = await deriveKeys(key, name);
const encodedData = new TextEncoder().encode(data);
const ciphertext = await subtleCrypto.encrypt(
const ciphertext = await globalThis.crypto.subtle.encrypt(
{
name: "AES-CTR",
counter: iv,
@@ -70,7 +69,7 @@ export async function encryptAES(
encodedData,
);
const hmac = await subtleCrypto.sign({ name: "HMAC" }, hmacKey, ciphertext);
const hmac = await globalThis.crypto.subtle.sign({ name: "HMAC" }, hmacKey, ciphertext);
return {
iv: encodeBase64(iv),
@@ -91,11 +90,11 @@ export async function decryptAES(data: IEncryptedPayload, key: Uint8Array, name:
const ciphertext = decodeBase64(data.ciphertext);
if (!(await subtleCrypto.verify({ name: "HMAC" }, hmacKey, decodeBase64(data.mac), ciphertext))) {
if (!(await globalThis.crypto.subtle.verify({ name: "HMAC" }, hmacKey, decodeBase64(data.mac), ciphertext))) {
throw new Error(`Error decrypting secret ${name}: bad MAC`);
}
const plaintext = await subtleCrypto.decrypt(
const plaintext = await globalThis.crypto.subtle.decrypt(
{
name: "AES-CTR",
counter: decodeBase64(data.iv),
@@ -109,8 +108,8 @@ export async function decryptAES(data: IEncryptedPayload, key: Uint8Array, name:
}
async function deriveKeys(key: Uint8Array, name: string): Promise<[CryptoKey, CryptoKey]> {
const hkdfkey = await subtleCrypto.importKey("raw", key, { name: "HKDF" }, false, ["deriveBits"]);
const keybits = await subtleCrypto.deriveBits(
const hkdfkey = await globalThis.crypto.subtle.importKey("raw", key, { name: "HKDF" }, false, ["deriveBits"]);
const keybits = await globalThis.crypto.subtle.deriveBits(
{
name: "HKDF",
salt: zeroSalt,
@@ -126,9 +125,12 @@ async function deriveKeys(key: Uint8Array, name: string): Promise<[CryptoKey, Cr
const aesKey = keybits.slice(0, 32);
const hmacKey = keybits.slice(32);
const aesProm = subtleCrypto.importKey("raw", aesKey, { name: "AES-CTR" }, false, ["encrypt", "decrypt"]);
const aesProm = globalThis.crypto.subtle.importKey("raw", aesKey, { name: "AES-CTR" }, false, [
"encrypt",
"decrypt",
]);
const hmacProm = subtleCrypto.importKey(
const hmacProm = globalThis.crypto.subtle.importKey(
"raw",
hmacKey,
{
-4
View File
@@ -66,7 +66,6 @@ export abstract class EncryptionAlgorithm {
protected readonly crypto: Crypto;
protected readonly olmDevice: OlmDevice;
protected readonly baseApis: MatrixClient;
protected readonly roomId?: string;
/**
* @param params - parameters
@@ -77,7 +76,6 @@ export abstract class EncryptionAlgorithm {
this.crypto = params.crypto;
this.olmDevice = params.olmDevice;
this.baseApis = params.baseApis;
this.roomId = params.roomId;
}
/**
@@ -127,14 +125,12 @@ export abstract class DecryptionAlgorithm {
protected readonly crypto: Crypto;
protected readonly olmDevice: OlmDevice;
protected readonly baseApis: MatrixClient;
protected readonly roomId?: string;
public constructor(params: DecryptionClassParams) {
this.userId = params.userId;
this.crypto = params.crypto;
this.olmDevice = params.olmDevice;
this.baseApis = params.baseApis;
this.roomId = params.roomId;
}
/**
+1 -2
View File
@@ -38,7 +38,6 @@ import {
} from "./keybackup";
import { UnstableValue } from "../NamespacedValue";
import { CryptoEvent } from "./index";
import { crypto } from "./crypto";
import { ClientPrefix, HTTPError, MatrixError, Method } from "../http-api";
import { BackupTrustInfo } from "../crypto-api/keybackup";
import { BackupDecryptor } from "../common-crypto/CryptoBackend";
@@ -764,7 +763,7 @@ export class Curve25519 implements BackupAlgorithm {
function randomBytes(size: number): Uint8Array {
const buf = new Uint8Array(size);
crypto.getRandomValues(buf);
globalThis.crypto.getRandomValues(buf);
return buf;
}
+2 -34
View File
@@ -14,37 +14,5 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from "../logger";
export let crypto = globalThis.crypto;
export let subtleCrypto = crypto?.subtle ?? crypto?.webkitSubtle; // TODO: Stop using webkitSubtle fallback
export let TextEncoder = globalThis.TextEncoder;
/* eslint-disable @typescript-eslint/no-var-requires */
if (!crypto) {
try {
crypto = require("crypto").webcrypto;
} catch (e) {
logger.error("Failed to load webcrypto", e);
}
}
if (!subtleCrypto) {
subtleCrypto = crypto?.subtle;
}
if (!TextEncoder) {
try {
TextEncoder = require("util").TextEncoder;
} catch (e) {
logger.error("Failed to load TextEncoder util", e);
}
}
/* eslint-enable @typescript-eslint/no-var-requires */
export function setCrypto(_crypto: Crypto): void {
crypto = _crypto;
subtleCrypto = _crypto.subtle ?? _crypto.webkitSubtle;
}
export function setTextEncoder(_TextEncoder: typeof TextEncoder): void {
TextEncoder = _TextEncoder;
}
/** @deprecated this is a no-op and should no longer be called. */
export function setCrypto(_crypto: Crypto): void {}
+2 -2
View File
@@ -276,7 +276,7 @@ export type CryptoEventHandlerMap = {
* @param deviceId - the id of the verified device
* @param deviceInfo - updated device information
*/
[CryptoEvent.DeviceVerificationChanged]: (userId: string, deviceId: string, device: DeviceInfo) => void;
[CryptoEvent.DeviceVerificationChanged]: (userId: string, deviceId: string, deviceInfo: DeviceInfo) => void;
/**
* Fires when the trust status of a user changes
* If userId is the userId of the logged-in user, this indicated a change
@@ -292,7 +292,7 @@ export type CryptoEventHandlerMap = {
/**
* Fires when we receive a room key request
*
* @param req - request details
* @param request - request details
*/
[CryptoEvent.RoomKeyRequest]: (request: IncomingRoomKeyRequest) => void;
/**
+9 -14
View File
@@ -15,7 +15,6 @@ limitations under the License.
*/
import { randomString } from "../randomstring";
import { subtleCrypto, TextEncoder } from "./crypto";
const DEFAULT_ITERATIONS = 500000;
@@ -36,10 +35,6 @@ interface IKey {
}
export function keyFromAuthData(authData: IAuthData, password: string): Promise<Uint8Array> {
if (!global.Olm) {
throw new Error("Olm is not available");
}
if (!authData.private_key_salt || !authData.private_key_iterations) {
throw new Error("Salt and/or iterations not found: " + "this backup cannot be restored with a passphrase");
}
@@ -53,10 +48,6 @@ export function keyFromAuthData(authData: IAuthData, password: string): Promise<
}
export async function keyFromPassphrase(password: string): Promise<IKey> {
if (!global.Olm) {
throw new Error("Olm is not available");
}
const salt = randomString(32);
const key = await deriveKey(password, salt, DEFAULT_ITERATIONS, DEFAULT_BITSIZE);
@@ -70,15 +61,19 @@ export async function deriveKey(
iterations: number,
numBits = DEFAULT_BITSIZE,
): Promise<Uint8Array> {
if (!subtleCrypto || !TextEncoder) {
if (!globalThis.crypto.subtle || !TextEncoder) {
throw new Error("Password-based backup is not available on this platform");
}
const key = await subtleCrypto.importKey("raw", new TextEncoder().encode(password), { name: "PBKDF2" }, false, [
"deriveBits",
]);
const key = await globalThis.crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
{ name: "PBKDF2" },
false,
["deriveBits"],
);
const keybits = await subtleCrypto.deriveBits(
const keybits = await globalThis.crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt: new TextEncoder().encode(salt),
+1 -1
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as bs58 from "bs58";
import bs58 from "bs58";
// picked arbitrarily but to try & avoid clashing with any bitcoin ones
// (which are also base58 encoded, but bitcoin's involve a lot more hashing)
+1 -2
View File
@@ -18,7 +18,6 @@ limitations under the License.
* QR code key verification.
*/
import { crypto } from "../crypto";
import { VerificationBase as Base } from "./Base";
import { newKeyMismatchError, newUserCancelledError } from "./Error";
import { decodeBase64, encodeUnpaddedBase64 } from "../../base64";
@@ -202,7 +201,7 @@ export class QRCodeData {
private static generateSharedSecret(): string {
const secretBytes = new Uint8Array(11);
crypto.getRandomValues(secretBytes);
globalThis.crypto.getRandomValues(secretBytes);
return encodeUnpaddedBase64(secretBytes);
}
+34
View File
@@ -0,0 +1,34 @@
/*
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.
*/
/**
* Computes a SHA-256 hash of a string (after utf-8 encoding) and returns it as an ArrayBuffer.
*
* @param plaintext The string to hash
* @returns An ArrayBuffer containing the SHA-256 hash of the input string
* @throws If the subtle crypto API is not available, for example if the code is running
* in a web page with an insecure context (eg. served over plain HTTP).
*/
export async function sha256(plaintext: string): Promise<ArrayBuffer> {
if (!globalThis.crypto.subtle) {
throw new Error("Crypto.subtle is not available: insecure context?");
}
const utf8 = new TextEncoder().encode(plaintext);
const digest = await globalThis.crypto.subtle.digest("SHA-256", utf8);
return digest;
}
+117 -5
View File
@@ -26,8 +26,13 @@ import {
} from "matrix-widget-api";
import { MatrixEvent, IEvent, IContent, EventStatus } from "./models/event";
import { ISendEventResponse } from "./@types/requests";
import { EventType } from "./@types/event";
import {
ISendEventResponse,
SendDelayedEventRequestOpts,
SendDelayedEventResponse,
UpdateDelayedEventAction,
} from "./@types/requests";
import { EventType, StateEvents } from "./@types/event";
import { logger } from "./logger";
import {
MatrixClient,
@@ -36,6 +41,7 @@ import {
IStartClientOpts,
SendToDeviceContentMap,
IOpenIDToken,
UNSTABLE_MSC4140_DELAYED_EVENTS,
} from "./client";
import { SyncApi, SyncState } from "./sync";
import { SlidingSyncSdk } from "./sliding-sync-sdk";
@@ -95,6 +101,20 @@ export interface ICapabilities {
* @defaultValue false
*/
turnServers?: boolean;
/**
* Whether this client needs to be able to send delayed events.
* @experimental Part of MSC4140 & MSC4157
* @defaultValue false
*/
sendDelayedEvents?: boolean;
/**
* Whether this client needs to be able to update delayed events.
* @experimental Part of MSC4140 & MSC4157
* @defaultValue false
*/
updateDelayedEvents?: boolean;
}
/**
@@ -162,6 +182,18 @@ export class RoomWidgetClient extends MatrixClient {
);
capabilities.sendToDevice?.forEach((eventType) => widgetApi.requestCapabilityToSendToDevice(eventType));
capabilities.receiveToDevice?.forEach((eventType) => widgetApi.requestCapabilityToReceiveToDevice(eventType));
if (
capabilities.sendDelayedEvents &&
(capabilities.sendEvent?.length ||
capabilities.sendMessage === true ||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
capabilities.sendState?.length)
) {
widgetApi.requestCapability(MatrixCapabilities.MSC4157SendDelayedEvent);
}
if (capabilities.updateDelayedEvents) {
widgetApi.requestCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent);
}
if (capabilities.turnServers) {
widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
}
@@ -218,6 +250,14 @@ export class RoomWidgetClient extends MatrixClient {
});
}) ?? [],
);
if (opts.clientWellKnownPollPeriod !== undefined) {
this.clientWellKnownIntervalID = setInterval(() => {
this.fetchClientWellKnown();
}, 1000 * opts.clientWellKnownPollPeriod);
this.fetchClientWellKnown();
}
this.setSyncState(SyncState.Syncing);
logger.info("Finished backfilling events");
@@ -240,7 +280,29 @@ export class RoomWidgetClient extends MatrixClient {
throw new Error(`Unknown room: ${roomIdOrAlias}`);
}
protected async encryptAndSendEvent(room: Room, event: MatrixEvent): Promise<ISendEventResponse> {
protected async encryptAndSendEvent(room: Room, event: MatrixEvent): Promise<ISendEventResponse>;
protected async encryptAndSendEvent(
room: Room,
event: MatrixEvent,
delayOpts: SendDelayedEventRequestOpts,
): Promise<SendDelayedEventResponse>;
protected async encryptAndSendEvent(
room: Room,
event: MatrixEvent,
delayOpts?: SendDelayedEventRequestOpts,
): Promise<ISendEventResponse | SendDelayedEventResponse> {
if (delayOpts) {
// TODO: updatePendingEvent for delayed events?
const response = await this.widgetApi.sendRoomEvent(
event.getType(),
event.getContent(),
room.roomId,
"delay" in delayOpts ? delayOpts.delay : undefined,
"parent_delay_id" in delayOpts ? delayOpts.parent_delay_id : undefined,
);
return this.validateSendDelayedEventResponse(response);
}
let response: ISendEventFromWidgetResponseData;
try {
response = await this.widgetApi.sendRoomEvent(event.getType(), event.getContent(), room.roomId);
@@ -249,8 +311,9 @@ export class RoomWidgetClient extends MatrixClient {
throw e;
}
// This also checks for an event id on the response
room.updatePendingEvent(event, EventStatus.SENT, response.event_id);
return { event_id: response.event_id };
return { event_id: response.event_id! };
}
public async sendStateEvent(
@@ -259,7 +322,56 @@ export class RoomWidgetClient extends MatrixClient {
content: any,
stateKey = "",
): Promise<ISendEventResponse> {
return await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId);
const response = await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId);
if (response.event_id === undefined) {
throw new Error("'event_id' absent from response to an event request");
}
return { event_id: response.event_id };
}
/**
* @experimental This currently relies on an unstable MSC (MSC4140).
*/
// eslint-disable-next-line
public async _unstable_sendDelayedStateEvent<K extends keyof StateEvents>(
roomId: string,
delayOpts: SendDelayedEventRequestOpts,
eventType: K,
content: StateEvents[K],
stateKey = "",
): Promise<SendDelayedEventResponse> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw Error("Server does not support the delayed events API");
}
const response = await this.widgetApi.sendStateEvent(
eventType,
stateKey,
content,
roomId,
"delay" in delayOpts ? delayOpts.delay : undefined,
"parent_delay_id" in delayOpts ? delayOpts.parent_delay_id : undefined,
);
return this.validateSendDelayedEventResponse(response);
}
private validateSendDelayedEventResponse(response: ISendEventFromWidgetResponseData): SendDelayedEventResponse {
if (response.delay_id === undefined) {
throw new Error("'delay_id' absent from response to a delayed event request");
}
return { delay_id: response.delay_id };
}
/**
* @experimental This currently relies on an unstable MSC (MSC4140).
*/
// eslint-disable-next-line
public async _unstable_updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise<{}> {
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
throw Error("Server does not support the delayed events API");
}
return await this.widgetApi.updateDelayedEvent(delayId, action);
}
public async sendToDevice(eventType: string, contentMap: SendToDeviceContentMap): Promise<{}> {
+28 -2
View File
@@ -156,8 +156,14 @@ export class CallMembership {
return this.membershipData.created_ts ?? this.parentEvent.getTs();
}
/**
* Gets the absolute expiry time of the membership if applicable to this membership type.
* @returns The absolute expiry time of the membership as a unix timestamp in milliseconds or undefined if not applicable
*/
public getAbsoluteExpiry(): number | undefined {
// if the membership is not a legacy membership, we assume it is MSC4143
if (!isLegacyCallMembershipData(this.membershipData)) return undefined;
if ("expires" in this.membershipData) {
// we know createdTs exists since we already do the isLegacyCallMembershipData check
return this.createdTs() + this.membershipData.expires;
@@ -167,9 +173,15 @@ export class CallMembership {
}
}
// gets the expiry time of the event, converted into the device's local time
/**
* Gets the expiry time of the event, converted into the device's local time.
* @deprecated This function has been observed returning bad data and is no longer used by MatrixRTC.
* @returns The local expiry time of the membership as a unix timestamp in milliseconds or undefined if not applicable
*/
public getLocalExpiry(): number | undefined {
// if the membership is not a legacy membership, we assume it is MSC4143
if (!isLegacyCallMembershipData(this.membershipData)) return undefined;
if ("expires" in this.membershipData) {
// we know createdTs exists since we already do the isLegacyCallMembershipData check
const relativeCreationTime = this.parentEvent.getTs() - this.createdTs();
@@ -184,10 +196,24 @@ export class CallMembership {
}
}
/**
* @returns The number of milliseconds until the membership expires or undefined if applicable
*/
public getMsUntilExpiry(): number | undefined {
if (isLegacyCallMembershipData(this.membershipData)) return this.getLocalExpiry()! - Date.now();
if (isLegacyCallMembershipData(this.membershipData)) {
// Assume that local clock is sufficiently in sync with other clocks in the distributed system.
// We used to try and adjust for the local clock being skewed, but there are cases where this is not accurate.
// The current implementation allows for the local clock to be -infinity to +MatrixRTCSession.MEMBERSHIP_EXPIRY_TIME/2
return this.getAbsoluteExpiry()! - Date.now();
}
// Assumed to be MSC4143
return undefined;
}
/**
* @returns true if the membership has expired, otherwise false
*/
public isExpired(): boolean {
if (isLegacyCallMembershipData(this.membershipData)) return this.getMsUntilExpiry()! <= 0;
+165 -50
View File
@@ -20,6 +20,7 @@ import { EventTimeline } from "../models/event-timeline";
import { Room } from "../models/room";
import { MatrixClient } from "../client";
import { EventType } from "../@types/event";
import { UpdateDelayedEventAction } from "../@types/requests";
import {
CallMembership,
CallMembershipData,
@@ -55,9 +56,9 @@ const USE_KEY_DELAY = 5000;
const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`;
const getParticipantIdFromMembership = (m: CallMembership): string => getParticipantId(m.sender!, m.deviceId);
function keysEqual(a: Uint8Array, b: Uint8Array): boolean {
function keysEqual(a: Uint8Array | undefined, b: Uint8Array | undefined): boolean {
if (a === b) return true;
return a && b && a.length === b.length && a.every((x, i) => x === b[i]);
return !!a && !!b && a.length === b.length && a.every((x, i) => x === b[i]);
}
export enum MatrixRTCSessionEvent {
@@ -133,10 +134,14 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
private manageMediaKeys = false;
private useLegacyMemberEvents = true;
// userId:deviceId => array of keys
private encryptionKeys = new Map<string, Array<Uint8Array>>();
// userId:deviceId => array of (key, timestamp)
private encryptionKeys = new Map<string, Array<{ key: Uint8Array; timestamp: number }>>();
private lastEncryptionKeyUpdateRequest?: number;
// We use this to store the last membership fingerprints we saw, so we can proactively re-send encryption keys
// if it looks like a membership has been updated.
private lastMembershipFingerprints: Set<string> | undefined;
/**
* The callId (sessionId) of the call.
*
@@ -160,26 +165,27 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
const callMemberships: CallMembership[] = [];
for (const memberEvent of callMemberEvents) {
const content = memberEvent.getContent();
const eventKeysCount = Object.keys(content).length;
// Dont even bother about empty events (saves us from costly type/"key in" checks in bigger rooms)
if (eventKeysCount === 0) continue;
let membershipContents: any[] = [];
// We first decide if its a MSC4143 event (per device state key)
if ("memberships" in content) {
if (eventKeysCount > 1 && "focus_active" in content) {
// We have a MSC4143 event membership event
membershipContents.push(content);
} else if (eventKeysCount === 1 && "memberships" in content) {
// we have a legacy (one event for all devices) event
if (!Array.isArray(content["memberships"])) {
logger.warn(`Malformed member event from ${memberEvent.getSender()}: memberships is not an array`);
continue;
}
membershipContents = content["memberships"];
} else {
// We have a MSC4143 event membership event
if (Object.keys(content).length !== 0) {
// We checked for empty content to not try to construct CallMembership's with {}.
membershipContents.push(content);
}
}
if (membershipContents.length === 0) {
continue;
}
if (membershipContents.length === 0) continue;
for (const membershipData of membershipContents) {
try {
const membership = new CallMembership(memberEvent, membershipData);
@@ -372,8 +378,15 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
}
}
/**
* Get the known encryption keys for a given participant device.
*
* @param userId the user ID of the participant
* @param deviceId the device ID of the participant
* @returns The encryption keys for the given participant, or undefined if they are not known.
*/
public getKeysForParticipant(userId: string, deviceId: string): Array<Uint8Array> | undefined {
return this.encryptionKeys.get(getParticipantId(userId, deviceId));
return this.encryptionKeys.get(getParticipantId(userId, deviceId))?.map((entry) => entry.key);
}
/**
@@ -381,7 +394,10 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
* cipher) given participant's media. This also includes our own key
*/
public getEncryptionKeys(): IterableIterator<[string, Array<Uint8Array>]> {
return this.encryptionKeys.entries();
// the returned array doesn't contain the timestamps
return Array.from(this.encryptionKeys.entries())
.map(([participantId, keys]): [string, Uint8Array[]] => [participantId, keys.map((k) => k.key)])
.values();
}
private getNewEncryptionKeyIndex(): number {
@@ -396,13 +412,15 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
/**
* Sets an encryption key at a specified index for a participant.
* The encryption keys for the local participanmt are also stored here under the
* The encryption keys for the local participant are also stored here under the
* user and device ID of the local participant.
* If the key is older than the existing key at the index, it will be ignored.
* @param userId - The user ID of the participant
* @param deviceId - Device ID of the participant
* @param encryptionKeyIndex - The index of the key to set
* @param encryptionKeyString - The string representation of the key to set in base64
* @param delayBeforeuse - If true, delay before emitting a key changed event. Useful when setting
* @param timestamp - The timestamp of the key. We assume that these are monotonic for each participant device.
* @param delayBeforeUse - If true, delay before emitting a key changed event. Useful when setting
* encryption keys for the local participant to allow time for the key to
* be distributed.
*/
@@ -411,18 +429,39 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
deviceId: string,
encryptionKeyIndex: number,
encryptionKeyString: string,
delayBeforeuse = false,
timestamp: number,
delayBeforeUse = false,
): void {
const keyBin = decodeBase64(encryptionKeyString);
const participantId = getParticipantId(userId, deviceId);
const encryptionKeys = this.encryptionKeys.get(participantId) ?? [];
if (!this.encryptionKeys.has(participantId)) {
this.encryptionKeys.set(participantId, []);
}
const participantKeys = this.encryptionKeys.get(participantId)!;
if (keysEqual(encryptionKeys[encryptionKeyIndex], keyBin)) return;
const existingKeyAtIndex = participantKeys[encryptionKeyIndex];
encryptionKeys[encryptionKeyIndex] = keyBin;
this.encryptionKeys.set(participantId, encryptionKeys);
if (delayBeforeuse) {
if (existingKeyAtIndex) {
if (existingKeyAtIndex.timestamp > timestamp) {
logger.info(
`Ignoring new key at index ${encryptionKeyIndex} for ${participantId} as it is older than existing known key`,
);
return;
}
if (keysEqual(existingKeyAtIndex.key, keyBin)) {
existingKeyAtIndex.timestamp = timestamp;
return;
}
}
participantKeys[encryptionKeyIndex] = {
key: keyBin,
timestamp,
};
if (delayBeforeUse) {
const useKeyTimeout = setTimeout(() => {
this.setNewKeyTimeouts.delete(useKeyTimeout);
logger.info(`Delayed-emitting key changed event for ${participantId} idx ${encryptionKeyIndex}`);
@@ -449,7 +488,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
const encryptionKey = secureRandomBase64Url(16);
const encryptionKeyIndex = this.getNewEncryptionKeyIndex();
logger.info("Generated new key at index " + encryptionKeyIndex);
this.setEncryptionKey(userId, deviceId, encryptionKeyIndex, encryptionKey, delayBeforeUse);
this.setEncryptionKey(userId, deviceId, encryptionKeyIndex, encryptionKey, Date.now(), delayBeforeUse);
}
/**
@@ -568,6 +607,13 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
}
}
/**
* Process `m.call.encryption_keys` events to track the encryption keys for call participants.
* This should be called each time the relevant event is received from a room timeline.
* If the event is malformed then it will be logged and ignored.
*
* @param event the event to process
*/
public onCallEncryption = (event: MatrixEvent): void => {
const userId = event.getSender();
const content = event.getContent<EncryptionKeysEventContent>();
@@ -629,11 +675,19 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
`Embedded-E2EE-LOG onCallEncryption userId=${userId}:${deviceId} encryptionKeyIndex=${encryptionKeyIndex}`,
this.encryptionKeys,
);
this.setEncryptionKey(userId, deviceId, encryptionKeyIndex, encryptionKey);
this.setEncryptionKey(userId, deviceId, encryptionKeyIndex, encryptionKey, event.getTs());
}
}
};
private isMyMembership = (m: CallMembership): boolean =>
m.sender === this.client.getUserId() && m.deviceId === this.client.getDeviceId();
/**
* Examines the latest call memberships and handles any encryption key sending or rotation that is needed.
*
* This function should be called when the room members or call memberships might have changed.
*/
public onMembershipUpdate = (): void => {
const oldMemberships = this.memberships;
this.memberships = MatrixRTCSession.callMembershipsForRoom(this.room);
@@ -649,19 +703,22 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
this.emit(MatrixRTCSessionEvent.MembershipsChanged, oldMemberships, this.memberships);
}
const isMyMembership = (m: CallMembership): boolean =>
m.sender === this.client.getUserId() && m.deviceId === this.client.getDeviceId();
if (this.manageMediaKeys && this.isJoined() && this.makeNewKeyTimeout === undefined) {
const oldMebershipIds = new Set(
oldMemberships.filter((m) => !isMyMembership(m)).map(getParticipantIdFromMembership),
const oldMembershipIds = new Set(
oldMemberships.filter((m) => !this.isMyMembership(m)).map(getParticipantIdFromMembership),
);
const newMebershipIds = new Set(
this.memberships.filter((m) => !isMyMembership(m)).map(getParticipantIdFromMembership),
const newMembershipIds = new Set(
this.memberships.filter((m) => !this.isMyMembership(m)).map(getParticipantIdFromMembership),
);
const anyLeft = Array.from(oldMebershipIds).some((x) => !newMebershipIds.has(x));
const anyJoined = Array.from(newMebershipIds).some((x) => !oldMebershipIds.has(x));
// We can use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference
// for this once available
const anyLeft = Array.from(oldMembershipIds).some((x) => !newMembershipIds.has(x));
const anyJoined = Array.from(newMembershipIds).some((x) => !oldMembershipIds.has(x));
const oldFingerprints = this.lastMembershipFingerprints;
// always store the fingerprints of these latest memberships
this.storeLastMembershipFingerprints();
if (anyLeft) {
logger.debug(`Member(s) have left: queueing sender key rotation`);
@@ -669,12 +726,33 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
} else if (anyJoined) {
logger.debug(`New member(s) have joined: re-sending keys`);
this.requestKeyEventSend();
} else if (oldFingerprints) {
// does it look like any of the members have updated their memberships?
const newFingerprints = this.lastMembershipFingerprints!;
// We can use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference
// for this once available
const candidateUpdates =
Array.from(oldFingerprints).some((x) => !newFingerprints.has(x)) ||
Array.from(newFingerprints).some((x) => !oldFingerprints.has(x));
if (candidateUpdates) {
logger.debug(`Member(s) have updated/reconnected: re-sending keys`);
this.requestKeyEventSend();
}
}
}
this.setExpiryTimer();
};
private storeLastMembershipFingerprints(): void {
this.lastMembershipFingerprints = new Set(
this.memberships
.filter((m) => !this.isMyMembership(m))
.map((m) => `${getParticipantIdFromMembership(m)}:${m.membershipID}:${m.createdTs()}`),
);
}
/**
* Constructs our own membership
* @param prevMembership - The previous value of our call membership, if any
@@ -824,9 +902,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
if (!localUserId || !localDeviceId) throw new Error("User ID or device ID was null!");
const callMemberEvents = roomState.events.get(EventType.GroupCallMemberPrefix);
const legacy =
!!this.useLegacyMemberEvents ||
(callMemberEvents?.size && this.stateEventsContainOngoingLegacySession(callMemberEvents));
const legacy = this.stateEventsContainOngoingLegacySession(callMemberEvents);
let newContent: {} | ExperimentalGroupCallRoomMemberState | SessionMembershipData = {};
if (legacy) {
const myCallMemberEvent = callMemberEvents?.get(localUserId);
@@ -864,28 +940,64 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
newContent = this.makeNewMembership(localDeviceId);
}
const stateKey = legacy ? localUserId : this.makeMembershipStateKey(localUserId, localDeviceId);
try {
await this.client.sendStateEvent(
this.room.roomId,
EventType.GroupCallMemberPrefix,
newContent,
legacy ? localUserId : this.makeMembershipStateKey(localUserId, localDeviceId),
);
await this.client.sendStateEvent(this.room.roomId, EventType.GroupCallMemberPrefix, newContent, stateKey);
logger.info(`Sent updated call member event.`);
// check periodically to see if we need to refresh our member event
if (this.isJoined() && legacy) {
this.memberEventTimeout = setTimeout(this.triggerCallMembershipEventUpdate, MEMBER_EVENT_CHECK_PERIOD);
if (this.isJoined()) {
if (legacy) {
this.memberEventTimeout = setTimeout(
this.triggerCallMembershipEventUpdate,
MEMBER_EVENT_CHECK_PERIOD,
);
} else {
try {
// TODO: If delayed event times out, re-join!
const res = await this.client._unstable_sendDelayedStateEvent(
this.room.roomId,
{
delay: 8000,
},
EventType.GroupCallMemberPrefix,
{}, // leave event
stateKey,
);
this.scheduleDelayDisconnection(res.delay_id);
} catch (e) {
logger.error("Failed to send delayed event:", e);
}
}
}
} catch (e) {
const resendDelay = CALL_MEMBER_EVENT_RETRY_DELAY_MIN + Math.random() * 2000;
logger.warn(`Failed to send call member event: retrying in ${resendDelay}`);
logger.warn(`Failed to send call member event (retrying in ${resendDelay}): ${e}`);
await new Promise((resolve) => setTimeout(resolve, resendDelay));
await this.triggerCallMembershipEventUpdate();
}
}
private stateEventsContainOngoingLegacySession(callMemberEvents: Map<string, MatrixEvent>): boolean {
private scheduleDelayDisconnection(delayId: string): void {
this.memberEventTimeout = setTimeout(() => this.delayDisconnection(delayId), 5000);
}
private async delayDisconnection(delayId: string): Promise<void> {
try {
await this.client._unstable_updateDelayedEvent(delayId, UpdateDelayedEventAction.Restart);
this.scheduleDelayDisconnection(delayId);
} catch (e) {
logger.error("Failed to delay our disconnection event", e);
}
}
private stateEventsContainOngoingLegacySession(callMemberEvents: Map<string, MatrixEvent> | undefined): boolean {
if (!callMemberEvents?.size) {
return this.useLegacyMemberEvents;
}
let containsAnyOngoingSession = false;
let containsUnknownOngoingSession = false;
for (const callMemberEvent of callMemberEvents.values()) {
const content = callMemberEvent.getContent();
if (Array.isArray(content["memberships"])) {
@@ -894,14 +1006,17 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
return true;
}
}
} else if (Object.keys(content).length > 0) {
containsAnyOngoingSession ||= true;
containsUnknownOngoingSession ||= !("focus_active" in content);
}
}
return false;
return containsAnyOngoingSession && !containsUnknownOngoingSession ? false : this.useLegacyMemberEvents;
}
private makeMembershipStateKey(localUserId: string, localDeviceId: string): string {
const stateKey = `${localUserId}_${localDeviceId}`;
if (/^org\.matrix\.msc3779\b/.exec(this.room.getVersion())) {
if (/^org\.matrix\.msc(3757|3779)\b/.exec(this.room.getVersion())) {
return stateKey;
} else {
return `_${stateKey}`;
+16 -1
View File
@@ -98,8 +98,23 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
return this.roomSessions.get(room.roomId)!;
}
private async consumeCallEncryptionEvent(event: MatrixEvent): Promise<void> {
private async consumeCallEncryptionEvent(event: MatrixEvent, isRetry = false): Promise<void> {
await this.client.decryptEventIfNeeded(event);
if (event.isDecryptionFailure()) {
if (!isRetry) {
logger.warn(
`Decryption failed for event ${event.getId()}: ${event.decryptionFailureReason} will retry once only`,
);
// retry after 1 second. After this we give up.
setTimeout(() => this.consumeCallEncryptionEvent(event, true), 1000);
} else {
logger.warn(`Decryption failed for event ${event.getId()}: ${event.decryptionFailureReason}`);
}
return;
} else if (isRetry) {
logger.info(`Decryption succeeded for event ${event.getId()} after retry`);
}
if (event.getType() !== EventType.CallEncryptionKeysPrefix) return Promise.resolve();
const room = this.client.getRoom(event.getRoomId());
+22
View File
@@ -0,0 +1,22 @@
/*
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.
*/
export * from "./CallMembership";
export * from "./focus";
export * from "./LivekitFocus";
export * from "./MatrixRTCSession";
export * from "./MatrixRTCSessionManager";
export * from "./types";
+1 -1
View File
@@ -118,7 +118,7 @@ export type EventTimelineSetHandlerMap = {
*/
[RoomEvent.TimelineReset]: (
room: Room | undefined,
eventTimelineSet: EventTimelineSet,
timelineSet: EventTimelineSet,
resetAllTimelines: boolean,
) => void;
};
+2 -2
View File
@@ -127,7 +127,7 @@ export class EventTimeline {
public constructor(private readonly eventTimelineSet: EventTimelineSet) {
this.roomId = eventTimelineSet.room?.roomId ?? null;
if (this.roomId) {
this.startState = new RoomState(this.roomId);
this.startState = new RoomState(this.roomId, undefined, true);
this.endState = new RoomState(this.roomId);
}
@@ -267,7 +267,7 @@ export class EventTimeline {
/**
* Get a pagination token
*
* @param direction - EventTimeline.BACKWARDS to get the pagination
* @param direction - EventTimeline.BACKWARDS to get the pagination
* token for going backwards in time; EventTimeline.FORWARDS to get the
* pagination token for going forwards in time.
*
+5 -12
View File
@@ -43,7 +43,6 @@ import { MatrixError } from "../http-api";
import { TypedEventEmitter } from "./typed-event-emitter";
import { EventStatus } from "./event-status";
import { CryptoBackend, DecryptionError } from "../common-crypto/CryptoBackend";
import { WITHHELD_MESSAGES } from "../crypto/OlmDevice";
import { IAnnotatedPushRule } from "../@types/PushRules";
import { Room } from "./room";
import { EventTimeline } from "./event-timeline";
@@ -312,12 +311,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
private thread?: Thread;
private threadId?: string;
/*
* True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and
* the sender has disabled encrypting to unverified devices.
*/
private encryptedDisabledForUnverifiedDevices = false;
/* Set an approximate timestamp for the event relative the local clock.
* This will inherently be approximate because it doesn't take into account
* the time between the server putting the 'age' field on the event as it sent
@@ -411,7 +404,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
// The fallback in these cases will be to use the origin_server_ts.
// For EDUs, the origin_server_ts also is not defined so we use Date.now().
const age = this.getAge();
this.localTimestamp = age !== undefined ? Date.now() - age : this.getTs() ?? Date.now();
this.localTimestamp = age !== undefined ? Date.now() - age : (this.getTs() ?? Date.now());
this.reEmitter = new TypedReEmitter(this);
}
@@ -787,12 +780,14 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
return this._decryptionFailureReason;
}
/*
/**
* True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and
* the sender has disabled encrypting to unverified devices.
*
* @deprecated: Prefer `event.decryptionFailureReason === DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE`.
*/
public get isEncryptedDisabledForUnverifiedDevices(): boolean {
return this.isDecryptionFailure() && this.encryptedDisabledForUnverifiedDevices;
return this.decryptionFailureReason === DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE;
}
public shouldAttemptDecryption(): boolean {
@@ -982,7 +977,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
this.claimedEd25519Key = decryptionResult.claimedEd25519Key ?? null;
this.forwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain || [];
this.untrusted = decryptionResult.untrusted || false;
this.encryptedDisabledForUnverifiedDevices = false;
this.invalidateExtensibleEvent();
}
@@ -1003,7 +997,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
this.claimedEd25519Key = null;
this.forwardingCurve25519KeyChain = [];
this.untrusted = false;
this.encryptedDisabledForUnverifiedDevices = reason === `DecryptionError: ${WITHHELD_MESSAGES["m.unverified"]}`;
this.invalidateExtensibleEvent();
}
+31 -3
View File
@@ -95,7 +95,7 @@ export type RoomStateEventHandlerMap = {
* });
* ```
*/
[RoomStateEvent.Events]: (event: MatrixEvent, state: RoomState, lastStateEvent: MatrixEvent | null) => void;
[RoomStateEvent.Events]: (event: MatrixEvent, state: RoomState, prevEvent: MatrixEvent | null) => void;
/**
* Fires whenever a member in the members dictionary is updated in any way.
* @param event - The matrix event which caused this event to fire.
@@ -194,10 +194,22 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
* As the timeline might get reset while they are loading, this state needs to be inherited
* and shared when the room state is cloned for the new timeline.
* This should only be passed from clone.
* @param isStartTimelineState - Optional. This state is marked as a start state.
* This is used to skip state insertions that are
* in the wrong order. The order is determined by the `replaces_state` id.
*
* Example:
* A current state events `replaces_state` value is `1`.
* Trying to insert a state event with `event_id` `1` in its place would fail if isStartTimelineState = false.
*
* A current state events `event_id` is `2`.
* Trying to insert a state event where its `replaces_state` value is `2` would fail if isStartTimelineState = true.
*/
public constructor(
public readonly roomId: string,
private oobMemberFlags = { status: OobStatus.NotStarted },
public readonly isStartTimelineState = false,
) {
super();
this.updateModifiedTime();
@@ -408,7 +420,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
* Fires {@link RoomStateEvent.Events}
* Fires {@link RoomStateEvent.Marker}
*/
public setStateEvents(stateEvents: MatrixEvent[], markerFoundOptions?: IMarkerFoundOptions): void {
public setStateEvents(stateEvents: MatrixEvent[], options?: IMarkerFoundOptions): void {
this.updateModifiedTime();
// update the core event dict
@@ -420,6 +432,22 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
}
const lastStateEvent = this.getStateEventMatching(event);
// Safety measure to not update the room (and emit the update) with older state.
// The sync loop really should not send old events but it does very regularly.
// Logging on return in those two conditions results in a large amount of logging. (on startup and when running element)
const lastReplaceId = lastStateEvent?.event.unsigned?.replaces_state;
const lastId = lastStateEvent?.event.event_id;
const newReplaceId = event.event.unsigned?.replaces_state;
const newId = event.event.event_id;
if (this.isStartTimelineState) {
// Add an event to the start of the timeline. Its replace id should not be the same as the one of the current/last start state event.
if (newReplaceId && lastId && newReplaceId === lastId) return;
} else {
// Add an event to the end of the timeline. It should not be the same as the one replaced by the current/last end state event.
if (lastReplaceId && newId && lastReplaceId === newId) return;
}
this.setStateEvent(event);
if (event.getType() === EventType.RoomMember) {
this.updateDisplayNameCache(event.getStateKey()!, event.getContent().displayname ?? "");
@@ -476,7 +504,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
// assume all our sentinels are now out-of-date
this.sentinels = {};
} else if (UNSTABLE_MSC2716_MARKER.matches(event.getType())) {
this.emit(RoomStateEvent.Marker, event, markerFoundOptions);
this.emit(RoomStateEvent.Marker, event, options);
}
});
+4 -3
View File
@@ -24,7 +24,7 @@ import {
} from "./event-timeline-set";
import { Direction, EventTimeline } from "./event-timeline";
import { getHttpUriForMxc } from "../content-repo";
import { compare, removeElement } from "../utils";
import { removeElement } from "../utils";
import { normalize, noUnsafeEventProps } from "../utils";
import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event";
import { EventStatus } from "./event-status";
@@ -209,7 +209,7 @@ export type RoomEventHandlerMap = {
* });
* ```
*/
[RoomEvent.AccountData]: (event: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => void;
[RoomEvent.AccountData]: (event: MatrixEvent, room: Room, prevEvent?: MatrixEvent) => void;
/**
* Fires whenever a receipt is received for a room
* @param event - The receipt event
@@ -3451,7 +3451,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
return true;
});
// make sure members have stable order
otherMembers.sort((a, b) => compare(a.userId, b.userId));
const collator = new Intl.Collator();
otherMembers.sort((a, b) => collator.compare(a.userId, b.userId));
// only 5 first members, immitate summaryHeroes
otherMembers = otherMembers.slice(0, 5);
otherNames = otherMembers.map((m) => m.name);
+9 -13
View File
@@ -16,18 +16,19 @@ limitations under the License.
import { IdTokenClaims, Log, OidcClient, SigninResponse, SigninState, WebStorageStateStore } from "oidc-client-ts";
import { subtleCrypto, TextEncoder } from "../crypto/crypto";
import { logger } from "../logger";
import { randomString } from "../randomstring";
import { OidcError } from "./error";
import {
validateIdToken,
ValidatedIssuerMetadata,
validateStoredUserState,
UserState,
BearerTokenResponse,
UserState,
validateBearerTokenResponse,
ValidatedIssuerMetadata,
validateIdToken,
validateStoredUserState,
} from "./validate";
import { sha256 } from "../digest";
import { encodeUnpaddedBase64Url } from "../base64";
// reexport for backwards compatibility
export type { BearerTokenResponse };
@@ -57,19 +58,14 @@ export const generateScope = (deviceId?: string): string => {
// https://www.rfc-editor.org/rfc/rfc7636
const generateCodeChallenge = async (codeVerifier: string): Promise<string> => {
if (!subtleCrypto) {
if (!globalThis.crypto.subtle) {
// @TODO(kerrya) should this be allowed? configurable?
logger.warn("A secure context is required to generate code challenge. Using plain text code challenge");
return codeVerifier;
}
const utf8 = new TextEncoder().encode(codeVerifier);
const digest = await subtleCrypto.digest("SHA-256", utf8);
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
const hashBuffer = await sha256(codeVerifier);
return encodeUnpaddedBase64Url(hashBuffer);
};
/**
+1 -2
View File
@@ -16,7 +16,6 @@ limitations under the License.
*/
import { encodeUnpaddedBase64Url } from "./base64";
import { crypto } from "./crypto/crypto";
const LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
@@ -24,7 +23,7 @@ const DIGITS = "0123456789";
export function secureRandomBase64Url(len: number): string {
const key = new Uint8Array(len);
crypto.getRandomValues(key);
globalThis.crypto.getRandomValues(key);
return encodeUnpaddedBase64Url(key);
}
@@ -17,16 +17,15 @@ limitations under the License.
import { SAS } from "@matrix-org/olm";
import {
RendezvousError,
RendezvousCode,
RendezvousIntent,
RendezvousChannel,
RendezvousTransportDetails,
RendezvousTransport,
LegacyRendezvousFailureReason as RendezvousFailureReason,
RendezvousChannel,
RendezvousCode,
RendezvousError,
RendezvousIntent,
RendezvousTransport,
RendezvousTransportDetails,
} from "..";
import { encodeUnpaddedBase64, decodeBase64 } from "../../base64";
import { crypto, subtleCrypto, TextEncoder } from "../../crypto/crypto";
import { decodeBase64, encodeUnpaddedBase64 } from "../../base64";
import { generateDecimalSas } from "../../crypto/verification/SASDecimal";
import { UnstableValue } from "../../NamespacedValue";
@@ -56,11 +55,11 @@ export interface EncryptedPayload {
}
async function importKey(key: Uint8Array): Promise<CryptoKey> {
if (!subtleCrypto) {
if (!globalThis.crypto.subtle) {
throw new Error("Web Crypto is not available");
}
const imported = subtleCrypto.importKey("raw", key, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
const imported = globalThis.crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
return imported;
}
@@ -164,16 +163,16 @@ export class MSC3903ECDHv2RendezvousChannel<T> implements RendezvousChannel<T> {
}
private async encrypt(data: T): Promise<MSC3903ECDHPayload> {
if (!subtleCrypto) {
if (!globalThis.crypto.subtle) {
throw new Error("Web Crypto is not available");
}
const iv = new Uint8Array(32);
crypto.getRandomValues(iv);
globalThis.crypto.getRandomValues(iv);
const encodedData = new TextEncoder().encode(JSON.stringify(data));
const ciphertext = await subtleCrypto.encrypt(
const ciphertext = await globalThis.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
@@ -208,11 +207,11 @@ export class MSC3903ECDHv2RendezvousChannel<T> implements RendezvousChannel<T> {
const ciphertextBytes = decodeBase64(ciphertext);
if (!subtleCrypto) {
if (!globalThis.crypto.subtle) {
throw new Error("Web Crypto is not available");
}
const plaintext = await subtleCrypto.decrypt(
const plaintext = await globalThis.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: decodeBase64(iv),
+1 -2
View File
@@ -21,7 +21,6 @@ import { encodeUri } from "../utils";
import { IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api";
import { IToDeviceEvent } from "../sync-accumulator";
import { ServerSideSecretStorage } from "../secret-storage";
import { crypto } from "../crypto/crypto";
import { decodeBase64, encodeUnpaddedBase64 } from "../base64";
import { Logger } from "../logger";
@@ -155,7 +154,7 @@ export class DehydratedDeviceManager {
*/
public async resetKey(): Promise<void> {
const key = new Uint8Array(32);
crypto.getRandomValues(key);
globalThis.crypto.getRandomValues(key);
await this.secretStorage.store(SECRET_STORAGE_NAME, encodeUnpaddedBase64(key));
this.key = key;
}
+12 -10
View File
@@ -14,16 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import {
CollectStrategy,
EncryptionAlgorithm,
EncryptionSettings,
HistoryVisibility as RustHistoryVisibility,
OlmMachine,
RoomId,
UserId,
HistoryVisibility as RustHistoryVisibility,
ToDeviceRequest,
UserId,
} from "@matrix-org/matrix-sdk-crypto-wasm";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { EventType } from "../@types/event";
import { IContent, MatrixEvent } from "../models/event";
@@ -105,10 +106,8 @@ export class RoomEncryptor {
(member.membership == KnownMembership.Invite && this.room.shouldEncryptForInvitedMembers())
) {
// make sure we are tracking the deviceList for this user
logDuration(this.prefixedLogger, "updateTrackedUsers", async () => {
this.olmMachine.updateTrackedUsers([new UserId(member.userId)]).catch((e) => {
this.prefixedLogger.error("Unable to update tracked users", e);
});
this.olmMachine.updateTrackedUsers([new UserId(member.userId)]).catch((e) => {
this.prefixedLogger.error("Unable to update tracked users", e);
});
}
@@ -144,7 +143,7 @@ export class RoomEncryptor {
* @param globalBlacklistUnverifiedDevices - When `true`, it will not send encrypted messages to unverified devices
*/
public encryptEvent(event: MatrixEvent | null, globalBlacklistUnverifiedDevices: boolean): Promise<void> {
const logger = new LogSpan(this.prefixedLogger, event ? event.getTxnId() ?? "" : "prepareForEncryption");
const logger = new LogSpan(this.prefixedLogger, event ? (event.getTxnId() ?? "") : "prepareForEncryption");
// Ensure order of encryption to avoid message ordering issues, as the scheduler only ensures
// events order after they have been encrypted.
const prom = this.currentEncryptionPromise
@@ -254,8 +253,11 @@ export class RoomEncryptor {
// When this.room.getBlacklistUnverifiedDevices() === null, the global settings should be used
// See Room#getBlacklistUnverifiedDevices
rustEncryptionSettings.onlyAllowTrustedDevices =
this.room.getBlacklistUnverifiedDevices() ?? globalBlacklistUnverifiedDevices;
if (this.room.getBlacklistUnverifiedDevices() ?? globalBlacklistUnverifiedDevices) {
rustEncryptionSettings.sharingStrategy = CollectStrategy.DeviceBasedStrategyOnlyTrustedDevices;
} else {
rustEncryptionSettings.sharingStrategy = CollectStrategy.DeviceBasedStrategyAllDevices;
}
await logDuration(this.prefixedLogger, "shareRoomKey", async () => {
const shareMessages: ToDeviceRequest[] = await this.olmMachine.shareRoomKey(
+3
View File
@@ -174,6 +174,9 @@ async function initOlmMachine(
await olmMachine.registerRoomKeyUpdatedCallback((sessions: RustSdkCryptoJs.RoomKeyInfo[]) =>
rustCrypto.onRoomKeysUpdated(sessions),
);
await olmMachine.registerRoomKeysWithheldCallback((withheld: RustSdkCryptoJs.RoomKeyWithheldInfo[]) =>
rustCrypto.onRoomKeysWithheld(withheld),
);
await olmMachine.registerUserIdentityUpdatedCallback((userId: RustSdkCryptoJs.UserId) =>
rustCrypto.onUserIdentityUpdated(userId),
);
+83 -61
View File
@@ -21,7 +21,7 @@ import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypt
import { KnownMembership } from "../@types/membership";
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator";
import type { IEncryptedEventInfo } from "../crypto/api";
import { IContent, MatrixEvent, MatrixEventEvent } from "../models/event";
import { MatrixEvent, MatrixEventEvent } from "../models/event";
import { Room } from "../models/room";
import { RoomMember } from "../models/room-member";
import { BackupDecryptor, CryptoBackend, DecryptionError, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
@@ -62,7 +62,6 @@ import { CrossSigningIdentity } from "./CrossSigningIdentity";
import { secretStorageCanAccessSecrets, secretStorageContainsCrossSigningKeys } from "./secret-storage";
import { keyFromPassphrase } from "../crypto/key_passphrase";
import { encodeRecoveryKey } from "../crypto/recoverykey";
import { crypto } from "../crypto/crypto";
import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification";
import { EventType, MsgType } from "../@types/event";
import { CryptoEvent } from "../crypto";
@@ -891,7 +890,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
} else {
// Using the navigator crypto API to generate the private key
const key = new Uint8Array(32);
crypto.getRandomValues(key);
globalThis.crypto.getRandomValues(key);
return {
privateKey: key,
encodedPrivateKey: encodeRecoveryKey(key),
@@ -1315,7 +1314,11 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
// look for interesting to-device messages
for (const message of processed) {
if (message.type === EventType.KeyVerificationRequest) {
this.onIncomingKeyVerificationRequest(message.sender, message.content);
const sender = message.sender;
const transactionId = message.content.transaction_id;
if (transactionId && sender) {
this.onIncomingKeyVerificationRequest(sender, transactionId);
}
}
}
return processed;
@@ -1411,18 +1414,13 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
}
/**
* Handle an incoming m.key.verification request event
* Handle an incoming m.key.verification.request event, received either in-room or in a to-device message.
*
* @param sender - the sender of the event
* @param content - the content of the event
* @param transactionId - the transaction ID for the verification. For to-device messages, this comes from the
* content of the message; for in-room messages it is the event ID.
*/
private onIncomingKeyVerificationRequest(sender: string, content: IContent): void {
const transactionId = content.transaction_id;
if (!transactionId || !sender) {
// not a valid request: ignore
return;
}
private onIncomingKeyVerificationRequest(sender: string, transactionId: string): void {
const request: RustSdkCryptoJs.VerificationRequest | undefined = this.olmMachine.getVerificationRequest(
new RustSdkCryptoJs.UserId(sender),
transactionId,
@@ -1438,6 +1436,12 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
this._supportedVerificationMethods,
),
);
} else {
// There are multiple reasons this can happen; probably the most likely is that the event is an
// in-room event which is too old.
this.logger.info(
`Ignoring just-received verification request ${transactionId} which did not start a rust-side verification`,
);
}
}
@@ -1482,7 +1486,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
this.logger.debug(
`Got update for session ${key.senderKey.toBase64()}|${key.sessionId} in ${key.roomId.toString()}`,
);
const pendingList = this.eventDecryptor.getEventsPendingRoomKey(key);
const pendingList = this.eventDecryptor.getEventsPendingRoomKey(key.roomId.toString(), key.sessionId);
if (pendingList.length === 0) return;
this.logger.debug(
@@ -1503,6 +1507,37 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
}
}
/**
* Callback for `OlmMachine.registerRoomKeyWithheldCallback`.
*
* Called by the rust sdk whenever we are told that a key has been withheld. We see if we had any events that
* failed to decrypt for the given session, and update their status if so.
*
* @param withheld - Details of the withheld sessions.
*/
public async onRoomKeysWithheld(withheld: RustSdkCryptoJs.RoomKeyWithheldInfo[]): Promise<void> {
for (const session of withheld) {
this.logger.debug(`Got withheld message for session ${session.sessionId} in ${session.roomId.toString()}`);
const pendingList = this.eventDecryptor.getEventsPendingRoomKey(
session.roomId.toString(),
session.sessionId,
);
if (pendingList.length === 0) return;
// The easiest way to update the status of the event is to have another go at decrypting it.
this.logger.debug(
"Retrying decryption on events:",
pendingList.map((e) => `${e.getId()}`),
);
for (const ev of pendingList) {
ev.attemptDecryption(this, { isRetry: true }).catch((_e) => {
// It's somewhat expected that we still can't decrypt here.
});
}
}
}
/**
* Callback for `OlmMachine.registerUserIdentityUpdatedCallback`
*
@@ -1597,7 +1632,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
const processEvent = async (evt: MatrixEvent): Promise<void> => {
// Process only verification event
if (isVerificationEvent(event)) {
await this.onKeyVerificationRequest(evt);
await this.onKeyVerificationEvent(evt);
}
};
@@ -1624,11 +1659,11 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
}
/**
* Handle key verification request.
* Handle an in-room key verification event.
*
* @param event - a key validation request event.
*/
private async onKeyVerificationRequest(event: MatrixEvent): Promise<void> {
private async onKeyVerificationEvent(event: MatrixEvent): Promise<void> {
const roomId = event.getRoomId();
if (!roomId) {
@@ -1655,27 +1690,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
event.getType() === EventType.RoomMessage &&
event.getContent().msgtype === MsgType.KeyVerificationRequest
) {
const request: RustSdkCryptoJs.VerificationRequest | undefined = this.olmMachine.getVerificationRequest(
new RustSdkCryptoJs.UserId(event.getSender()!),
event.getId()!,
);
if (!request) {
// There are multiple reasons this can happen; probably the most likely is that the event is too old.
this.logger.info(
`Ignoring just-received verification request ${event.getId()} which did not start a rust-side verification`,
);
} else {
this.emit(
CryptoEvent.VerificationRequestReceived,
new RustVerificationRequest(
this.olmMachine,
request,
this.outgoingRequestProcessor,
this._supportedVerificationMethods,
),
);
}
this.onIncomingKeyVerificationRequest(event.getSender()!, event.getId()!);
}
// that may have caused us to queue up outgoing requests, so make sure we send them.
@@ -1699,7 +1714,7 @@ class EventDecryptor {
/**
* Events which we couldn't decrypt due to unknown sessions / indexes.
*
* Map from senderKey to sessionId to Set of MatrixEvents
* Map from roomId to sessionId to Set of MatrixEvents
*/
private eventsPendingKey = new MapWithDefault<string, MapWithDefault<string, Set<MatrixEvent>>>(
() => new MapWithDefault<string, Set<MatrixEvent>>(() => new Set()),
@@ -1803,6 +1818,17 @@ class EventDecryptor {
}
}
// If we got a withheld code, expose that.
if (err.maybe_withheld) {
// Unfortunately the Rust SDK API doesn't let us distinguish between different withheld cases, other than
// by string-matching.
const failureCode =
err.maybe_withheld === "The sender has disabled encrypting to unverified devices."
? DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE
: DecryptionFailureCode.MEGOLM_KEY_WITHHELD;
throw new DecryptionError(failureCode, err.maybe_withheld, errorDetails);
}
switch (err.code) {
case RustSdkCryptoJs.DecryptionErrorCode.MissingRoomKey:
throw new DecryptionError(
@@ -1848,30 +1874,27 @@ class EventDecryptor {
* Look for events which are waiting for a given megolm session
*
* Returns a list of events which were encrypted by `session` and could not be decrypted
*
* @param session -
*/
public getEventsPendingRoomKey(session: RustSdkCryptoJs.RoomKeyInfo): MatrixEvent[] {
const senderPendingEvents = this.eventsPendingKey.get(session.senderKey.toBase64());
if (!senderPendingEvents) return [];
public getEventsPendingRoomKey(roomId: string, sessionId: string): MatrixEvent[] {
const roomPendingEvents = this.eventsPendingKey.get(roomId);
if (!roomPendingEvents) return [];
const sessionPendingEvents = senderPendingEvents.get(session.sessionId);
const sessionPendingEvents = roomPendingEvents.get(sessionId);
if (!sessionPendingEvents) return [];
const roomId = session.roomId.toString();
return [...sessionPendingEvents].filter((ev) => ev.getRoomId() === roomId);
return [...sessionPendingEvents];
}
/**
* Add an event to the list of those awaiting their session keys.
*/
private addEventToPendingList(event: MatrixEvent): void {
const content = event.getWireContent();
const senderKey = content.sender_key;
const sessionId = content.session_id;
const roomId = event.getRoomId();
// We shouldn't have events without a room id here.
if (!roomId) return;
const senderPendingEvents = this.eventsPendingKey.getOrCreate(senderKey);
const sessionPendingEvents = senderPendingEvents.getOrCreate(sessionId);
const roomPendingEvents = this.eventsPendingKey.getOrCreate(roomId);
const sessionPendingEvents = roomPendingEvents.getOrCreate(event.getWireContent().session_id);
sessionPendingEvents.add(event);
}
@@ -1879,23 +1902,22 @@ class EventDecryptor {
* Remove an event from the list of those awaiting their session keys.
*/
private removeEventFromPendingList(event: MatrixEvent): void {
const content = event.getWireContent();
const senderKey = content.sender_key;
const sessionId = content.session_id;
const roomId = event.getRoomId();
if (!roomId) return;
const senderPendingEvents = this.eventsPendingKey.get(senderKey);
if (!senderPendingEvents) return;
const roomPendingEvents = this.eventsPendingKey.getOrCreate(roomId);
if (!roomPendingEvents) return;
const sessionPendingEvents = senderPendingEvents.get(sessionId);
const sessionPendingEvents = roomPendingEvents.get(event.getWireContent().session_id);
if (!sessionPendingEvents) return;
sessionPendingEvents.delete(event);
// also clean up the higher-level maps if they are now empty
if (sessionPendingEvents.size === 0) {
senderPendingEvents.delete(sessionId);
if (senderPendingEvents.size === 0) {
this.eventsPendingKey.delete(senderKey);
roomPendingEvents.delete(event.getWireContent().session_id);
if (roomPendingEvents.size === 0) {
this.eventsPendingKey.delete(roomId);
}
}
}
+1 -1
View File
@@ -504,7 +504,7 @@ export class SyncAccumulator {
currentData._timeline.push({
event: transformedEvent,
token: index === 0 ? data.timeline.prev_batch ?? null : null,
token: index === 0 ? (data.timeline.prev_batch ?? null) : null,
});
});
+6 -3
View File
@@ -401,17 +401,18 @@ export class SyncApi {
* Peek into a room. This will result in the room in question being synced so it
* is accessible via getRooms(). Live updates for the room will be provided.
* @param roomId - The room ID to peek into.
* @param limit - The number of timeline events to initially retrieve.
* @returns A promise which resolves once the room has been added to the
* store.
*/
public peek(roomId: string): Promise<Room> {
public peek(roomId: string, limit: number = 20): Promise<Room> {
if (this._peekRoom?.roomId === roomId) {
return Promise.resolve(this._peekRoom);
}
const client = this.client;
this._peekRoom = this.createRoom(roomId);
return this.client.roomInitialSync(roomId, 20).then((response) => {
return this.client.roomInitialSync(roomId, limit).then((response) => {
if (this._peekRoom?.roomId !== roomId) {
throw new Error("Peeking aborted");
}
@@ -1402,7 +1403,9 @@ export class SyncApi {
if (limited) {
room.resetLiveTimeline(
joinObj.timeline.prev_batch,
this.syncOpts.canResetEntireTimeline!(room.roomId) ? null : syncEventData.oldSyncToken ?? null,
this.syncOpts.canResetEntireTimeline!(room.roomId)
? null
: (syncEventData.oldSyncToken ?? null),
);
// We have to assume any gap in any timeline is
-10
View File
@@ -649,16 +649,6 @@ export function lexicographicCompare(a: string, b: string): number {
}
}
const collator = new Intl.Collator();
/**
* Performant language-sensitive string comparison
* @param a - the first string to compare
* @param b - the second string to compare
*/
export function compare(a: string, b: string): number {
return collator.compare(a, b);
}
/**
* This function is similar to Object.assign() but it assigns recursively and
* allows you to ignore nullish values from the source
+1 -1
View File
@@ -91,7 +91,7 @@ export type GroupCallEventHandlerMap = {
* `MatrixCall.ERR_NO_USER_MEDIA`. `ERR_LOCAL_OFFER_FAILED` is emitted when the local client
* fails to create an offer. `ERR_NO_USER_MEDIA` is emitted when the user has denied access
* to their audio/video hardware.
* @param err - The error raised by MatrixCall.
* @param error - The error raised by MatrixCall.
* @example
* ```
* matrixCall.on("error", function(err){
+846 -1335
View File
File diff suppressed because it is too large Load Diff