Compare commits

...

60 Commits

Author SHA1 Message Date
RiotRobot 72ee5504d5 v36.0.0 2025-01-14 14:08:54 +00:00
RiotRobot 38816589f5 v36.0.0-rc.0 2025-01-07 12:43:09 +00:00
Timo 6f743bfa1f MatrixRTC: Implement expiry logic for CallMembership and additional test coverage (#4587)
* remove all legacy call related code and adjust tests.
We actually had a bit of tests just for legacy and not for session events. All those tests got ported over so we do not remove any tests.

* dont adjust tests but remove legacy tests

* Remove deprecated CallMembership.getLocalExpiry()

* Remove references to legacy in test case names

* Clean up SessionMembershipData tsdoc

* Remove CallMembership.expires

* Use correct expire duration.

* make expiration methods not return optional values and update docstring

* add docs to `SessionMembershipData`

* Add new tests for session type member events that before only existed for legacy member events.

This reverts commit 795a3cffb61d672941c49e8139eb1d7b15c87d73.

* remove code we do not need yet.

* Cleanup

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
2025-01-07 10:14:09 +00:00
Timo ffd3c9575e Remove support for "legacy" MSC3898 group calling in MatrixRTCSession and CallMembership (#4583)
* remove all legacy call related code and adjust tests.
We actually had a bit of tests just for legacy and not for session events. All those tests got ported over so we do not remove any tests.

* dont adjust tests but remove legacy tests

* Remove deprecated CallMembership.getLocalExpiry()

* Remove references to legacy in test case names

* Clean up SessionMembershipData tsdoc

* Remove CallMembership.expires

* Use correct expire duration.

* make expiration methods not return optional values and update docstring

* add docs to `SessionMembershipData`

* Use `MSC4143` (instaed of `non-legacy`) wording in comment

Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>

* Incorporate feedback from review

* Fix test name

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>
2025-01-06 17:23:16 +00:00
David Baker 7678923e04 Don't retry on 4xx responses (#4601)
* Don't retry on 4xx responses

I'm not sure why this was limited to a small set of 4xx responses.
Nominally, no 4xx request should be retried (in fact the comment
below says this, but then the code didn't quite match it).

This was causing key backup requests to be retried even when the
server responded 404 because the backup in question had been deleted,
meaning the client would retry uselessly and it would take longer for
the client to prompt the user for action.

* Exclude 429s
2025-01-06 13:25:29 +00:00
Johannes Marbach 6f7c74f9ea Add syntax & type check for Node.js example on CI (#4410)
* Add syntax & type check for Node.js example on CI

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

* Fix quotes

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
Co-authored-by: Florian Duros <florianduros@element.io>
2025-01-03 18:49:57 +00:00
Michael Telatynski 3fcc56601b Use mapped types for account data content (#4590)
* Use mapped types around account data events

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

* Iterate

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

* Iterate

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

* Iterate

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

* Iterate

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

* Iterate

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

* Harden types for reading account data too

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

* Correct empty object type

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

* Update src/secret-storage.ts

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

* Iterate

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

* Iterate

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-12-19 22:53:58 +00:00
Andy Balaam bcf3d56bd5 Upgrade matrix-sdk-crypto-wasm to 12.1.0 (#4596)
... to fix https://github.com/matrix-org/matrix-rust-sdk/issues/4424
2024-12-19 14:33:25 +00:00
RiotRobot 647a2a1a19 Merge branch 'master' into develop 2024-12-18 14:11:02 +00:00
RiotRobot 1bf8533c03 v35.1.0 2024-12-18 14:10:31 +00:00
David Baker fb2f2dd6d4 Merge pull request #4593 from matrix-org/rav/migration-for-previouslyverified
Upgrade matrix-sdk-crypto-wasm to 1.11.0
2024-12-18 14:01:18 +00:00
Richard van der Hoff d33350ff26 Upgrade matrix-sdk-crypto-wasm to 1.11.0
... to fix https://github.com/matrix-org/matrix-rust-sdk/issues/4424
2024-12-18 13:29:05 +00:00
RiotRobot 349a86c119 Merge branch 'master' into develop 2024-12-17 13:22:36 +00:00
RiotRobot bee65ff13f v35.0.0 2024-12-17 13:22:10 +00:00
Hugh Nimmo-Smith e4182eb752 Update matrix-sdk-crypto-wasm to 12.0.0 (#4589)
It appears to "just work"... but I might be missing something
2024-12-17 11:24:46 +00:00
David Baker 3219aefc92 Avoid key prompts when resetting crypto (#4586)
* Avoid key prompts when resetting crypto

Attempting to get the backup key out of secret storage can cause
the user to be prompted for their key, which is not helpful if this
is being done as part of a reset. This check was redundant anyway
and we can just overwrite the key with the same value.

Also fix docs and remove check for active backup.

* Fix doc
2024-12-17 09:22:31 +00:00
Richard van der Hoff aba4e690af Improve documentation on various secret-storage related methods (#4585)
* Improve documentation on various secret-storage related methods

* fix link

* Apply suggestions from code review
2024-12-16 13:00:18 +00:00
Liam Diprose 693bb22ba1 Handle when aud OIDC claim is an Array (#4584)
* Handle when `aud` OIDC claim is an Array

The `aud` claim of OIDC id_tokens [can be an array](https://github.com/authts/oidc-client-ts/blob/ce6d694639c58e6a1c80904efdac5eda82b82042/src/Claims.ts#L92) but the existing logic
incorrectly assumes `aud` is always a string.

This PR adds the necessary check.

* Clarify `aud` OIDC claim check

* Fix for prettier

---------

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2024-12-16 11:38:34 +00:00
renovate[bot] 315e81b7de Update typescript-eslint monorepo to v8.17.0 (#4581)
* Update typescript-eslint monorepo to v8.17.0

* Fix lint errors

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2024-12-12 17:45:39 +00:00
David Baker a0502c5ee5 Save the key backup key to 4S during bootstrapCrossSigning (#4542)
* Save the key backup key to secret storage

When setting up secret storage, if we have a key backup key in cache
(like we do for the cross signing secrets).

* Add test

* Get the key directly from the olmMachine

saves converting it needlessly into a buffer to turn it back into
a base64 string

* Overwrite backup keyin storage if different

* Fix test

* Add integ test

* Test failure case for sonar

* Unused import

* Missed return

* Also check active backup version
2024-12-12 15:03:19 +00:00
Timo d1de32ea27 Only re-prepare MatrixrRTC delayed disconnection event on 404 (#4575)
* Set retry counts of event updating to 1000 (from 1)

 With it being set to one the following issue could occur:

```
// If sending state cancels your own delayed state, prepare another delayed state
// TODO: Remove this once MSC4140 is stable & doesn't cancel own delayed state
if (this.disconnectDelayId !== undefined) {
    try {
        const knownDisconnectDelayId = this.disconnectDelayId;
        await resendIfRateLimited(
            () =>
                this.client._unstable_updateDelayedEvent(
                    knownDisconnectDelayId,
                    UpdateDelayedEventAction.Restart,
                ),
            1000,
        );
    } catch (e) {
        logger.warn("Failed to update delayed disconnection event, prepare it again:", e);
        this.disconnectDelayId = undefined;
        await prepareDelayedDisconnection();
    }
}
```
This code looks like the `catch(e)` could never be triggered with 429 (rate limit) because they would be caught by `await resendIfRateLimited`. EXCEPT that this is only happening once: `resendIfRateLimited<T>(func: () => Promise<T>, numRetriesAllowed: number = 1)`. So as soon as the server sends two rate limits in a row we get the following:
 - we get into the `catch(e)`  because of the rate limit
 - we forget about `this.disconnectDelayId = undefined`
 - we start a new delayed event `await prepareDelayedDisconnection();`
 - we do not anymore update the old delayed event which is still running!
 - the running delay event will make us disconnect from the call (call member becomes `{}`)
 - we get into our outher error catching mechanism that resends the new state event
 - this cancels the newly created delay leave event (`await prepareDelayedDisconnection();`)
 - and create another delay leave event.
 - but if we are still reate limited (chances are really high due to the reconnect), this loop will REPEAT

* also check for M_NOT_FOUND

* Leave retry at current level

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
2024-12-12 14:58:50 +00:00
renovate[bot] 3ae25427a8 Update dependency @types/node to v18.19.67 (#4578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-11 09:53:08 +00:00
renovate[bot] 8155b0acfc Update mheap/github-action-required-labels digest to 388fd6a (#4577)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 22:11:53 +00:00
renovate[bot] 413c156624 Update guibranco/github-status-action-v2 digest to d469d49 (#4576)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 18:12:27 +00:00
renovate[bot] e78a3cec9f Update all non-major dependencies (#4579)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 16:50:03 +00:00
renovate[bot] 5998de365d Update dependency @babel/cli to v7.26.4 (#4580)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 16:31:44 +00:00
RiotRobot 3de2b9bf80 v35.0.0-rc.0 2024-12-10 15:43:43 +00:00
Hubert Chathi ded87290ce Update matrix-sdk-crypto-wasm to 11.0.0 (#4566)
* Update matrix-sdk-crypto-wasm to 11.0.0

* use `backend` variable to test for rust crypto

* apply changes from review
2024-12-09 23:11:02 +00:00
renovate[bot] cf39595bd7 Update dependency typedoc to v0.27.3 (#4573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-05 19:21:39 +00:00
Stanislav Demydiuk c90ea985c6 Fix age field check in event echo processing (#3635)
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2024-12-05 16:08:41 +00:00
Florian Duros c54ca29aa8 Rename initCrypto into initLegacyCrypto (#4567) 2024-12-05 11:08:38 +00:00
Michael Telatynski beb3721e7a Avoid use of Buffer as it does not exist in the Web natively (#4569) 2024-12-04 22:32:09 +00:00
Richard van der Hoff 1cad6f4451 Add multiprocess health warnings to initRustCrypto (#4571) 2024-12-04 14:38:48 +00:00
RiotRobot c4ea57d42d Merge branch 'master' into develop 2024-12-03 12:24:07 +00:00
RiotRobot d3f5526ec0 v34.13.0 2024-12-03 12:23:40 +00:00
renovate[bot] b8e332b53d Update dependency typescript to v5.7.2 (#4553)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-03 11:47:14 +00:00
renovate[bot] ab78acc7da Update typedoc (#4568)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-03 11:19:56 +00:00
Michael Telatynski 051f4e2ab9 Fix release-checks to not use reserved name GITHUB_TOKEN
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-12-02 10:47:35 +00:00
Michael Telatynski 8863e42e35 More typescript linting (#3310)
* More typescript linting

* Improve types

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

* Discard changes to src/models/MSC3089TreeSpace.ts

* Discard changes to src/realtime-callbacks.ts

* Fix tests

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

* Improve coverage

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-12-02 09:56:52 +00:00
Michael Telatynski bc5246970c Extract release sanity checks to reusable workflow (#4546)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-12-02 09:14:45 +00:00
Florian Duros edac6a9983 Replace deprecate imports (#4565)
* Replace deprecate imports

* Deprecate `CryptoBackend.getEventEncryptionInfo`

* Add deprecated alternative
2024-11-29 10:04:20 +00:00
Florian Duros 97ef1dc6df Remove deprecated calls of MatrixClient (#4563) 2024-11-29 09:49:52 +00:00
Florian Duros 125e45c24d Deprecate remaining legacy functions and move CryptoEvent.LegacyCryptoStoreMigrationProgress handler (#4560)
* Deprecate legacy functions in `MatrixClient`

* Move `CryptoEvent.LegacyCryptoStoreMigrationProgress` handler in rust crypto

* Remove `olmLib` usage in `MatrixClient`
2024-11-29 09:49:32 +00:00
Hugh Nimmo-Smith 3781b6ebfa Re-send MatrixRTC media encryption keys for a new joiner even if a rotation is in progress (#4561) 2024-11-28 12:05:39 +00:00
Michael Telatynski 8fc77c595a Move bump downstreams to js-sdk specific release workflow (#4547)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-27 14:15:51 +00:00
David Baker 5bcd26e506 Support MSC4222 state_after (#4487)
* WIP support for state_after

* Fix sliding sync sdk / embedded tests

* Allow both state & state_after to be undefined

Since it must have allowed state to be undefined previously: the test
had it as such.

* Fix limited sync handling

* Need to use state_after being undefined

if state can be undefined anyway

* Make sliding sync sdk tests pass

* Remove deprecated interfaces & backwards-compat code

* Remove useless assignment

* Use updates unstable prefix

* Clarify docs

* Remove additional semi-backwards compatible overload

* Update unstable prefixes

* Iterate

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

* Iterate

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

* Fix test

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

* Iterate

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

* Iterate

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

* Add test for MSC4222 behaviour

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

* Improve coverage

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

* Iterate

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

* Fix tests

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

* Iterate

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

* Tidy

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

* Add comments to explain why things work as they are.

* Fix sync accumulator for state_after sync handling

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

* Add tests

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

* Revert "Fix room state being updated with old (now overwritten) state and emitting for those updates. (#4242)"

This reverts commit 957329b218.

* Fix Sync Accumulator toJSON putting start timeline state in state_after field

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

* Update tests

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

* Add test case

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

* Iterate

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Timo <toger5@hotmail.de>
2024-11-27 11:40:41 +00:00
Timo 66f099b2e7 Revert "Fix room state being updated with old (now overwritten) state and emitting for those updates. (#4242)" (#4532)
This reverts commit 957329b218.
2024-11-27 10:49:29 +00:00
renovate[bot] b1445d7457 Update all non-major dependencies (#4558)
* Update all non-major dependencies

* Prettier

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-27 09:22:49 +00:00
Michael Telatynski b3794760c6 Merge remote-tracking branch 'origin/develop' into develop 2024-11-27 09:10:52 +00:00
Michael Telatynski fbdcc597b6 Fix workflow permissions
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-27 07:44:35 +00:00
renovate[bot] 8ac32b7894 Update dependency typedoc-plugin-coverage to v3.4.0 (#4559)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 18:51:22 +00:00
renovate[bot] b27c6de78d Update guibranco/github-status-action-v2 digest to 66088c4 (#4548)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 18:23:16 +00:00
renovate[bot] c8e0987774 Update dependency typedoc-plugin-missing-exports to v3.0.2 (#4550)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 17:41:22 +00:00
renovate[bot] 849fcd3341 Update dependency @types/node to v18.19.65 (#4549)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 17:33:27 +00:00
renovate[bot] 8df8266e4a Update dependency @stylistic/eslint-plugin to v2.11.0 (#4552)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 17:16:47 +00:00
renovate[bot] cba4edfd84 Update dependency typedoc-plugin-mdn-links to v4 (#4556)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 17:14:18 +00:00
renovate[bot] 4f50290cd6 Update all non-major dependencies (#4551)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 16:35:48 +00:00
renovate[bot] 35e31f02ac Update typescript-eslint monorepo to v8.15.0 (#4554)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 16:35:13 +00:00
Hubert Chathi 69647a33b6 Use shield status codes from Rust rather than string matching (#4529) 2024-11-26 15:06:57 +00:00
Michael Telatynski 006929ac0c Fix release workflow permissions
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-26 13:41:14 +00:00
115 changed files with 3118 additions and 2600 deletions
+1
View File
@@ -138,6 +138,7 @@ module.exports = {
tryExtensions: [".ts"],
},
],
"no-extra-boolean-cast": "error",
},
},
{
+4 -4
View File
@@ -2,7 +2,7 @@
## Checklist
- [ ] Tests written for new code (and old code if feasible).
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
- [ ] Linter and other CI checks pass.
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md)).
- [ ] Tests written for new code (and old code if feasible).
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
- [ ] Linter and other CI checks pass.
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md)).
+3 -1
View File
@@ -15,7 +15,7 @@ jobs:
name: Preview Changelog
runs-on: ubuntu-24.04
steps:
- uses: mheap/github-action-required-labels@d25134c992b943fb6ad00c25ea00eb5988c0a9dd # v5
- uses: mheap/github-action-required-labels@388fd6af37b34cdfe5a23b37060e763217e58b03 # v5
if: github.event_name != 'merge_group'
with:
labels: |
@@ -45,6 +45,8 @@ jobs:
name: Label Community PRs
runs-on: ubuntu-24.04
if: github.event.action == 'opened'
permissions:
pull-requests: write
steps:
- name: Check membership
if: github.event.pull_request.user.login != 'renovate[bot]'
+38
View File
@@ -0,0 +1,38 @@
name: Release Sanity checks
on:
workflow_call:
secrets:
ELEMENT_BOT_TOKEN:
required: false
inputs:
repository:
type: string
required: false
default: ${{ github.repository }}
description: "The repository (in form owner/repo) to check for release blockers"
permissions: {}
jobs:
checks:
name: Sanity checks
runs-on: ubuntu-24.04
steps:
- name: Check for X-Release-Blocker label on any open issues or PRs
uses: actions/github-script@v7
env:
REPO: ${{ inputs.repository }}
with:
github-token: ${{ secrets.ELEMENT_BOT_TOKEN || secrets.GITHUB_TOKEN }}
script: |
const { REPO } = process.env;
const { data } = await github.rest.search.issuesAndPullRequests({
q: `repo:${REPO} label:X-Release-Blocker is:open`,
per_page: 50,
});
if (data.total_count) {
data.items.forEach(item => {
core.error(`Release blocker: ${item.html_url}`);
});
core.setFailed(`Found release blockers!`);
}
+5 -52
View File
@@ -20,10 +20,6 @@ on:
description: Publish to npm
type: boolean
default: false
downstreams:
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
gpg-fingerprint:
description: Fingerprint of the GPG key to use for signing the git tag and assets, if any.
type: string
@@ -38,30 +34,18 @@ on:
description: The number of expected assets, including signatures, excluding generated zip & tarball.
type: number
required: false
outputs:
npm-id:
description: "The npm package@version string we published"
value: ${{ jobs.npm.outputs.id }}
permissions: {}
jobs:
checks:
name: Sanity checks
runs-on: ubuntu-24.04
permissions:
issues: read
pull-requests: read
steps:
- name: Check for X-Release-Blocker label on any open issues or PRs
uses: actions/github-script@v7
with:
script: |
const { data } = await github.rest.search.issuesAndPullRequests({
q: `repo:${context.repo.owner}/${context.repo.repo} label:X-Release-Blocker is:open`,
per_page: 50,
});
if (data.total_count) {
data.items.forEach(item => {
core.error(`Release blocker: ${item.html_url}`);
});
core.setFailed(`Found release blockers!`);
}
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop
release:
name: Release
@@ -327,34 +311,3 @@ jobs:
# wait-interval: 10
# check-name: merge
# allowed-conclusions: success
bump-downstreams:
name: Update npm dependency in downstream projects
needs: npm
runs-on: ubuntu-24.04
if: inputs.downstreams
strategy:
matrix:
repo: ${{ fromJSON(inputs.downstreams) }}
steps:
- uses: actions/checkout@v4
with:
repository: ${{ matrix.repo }}
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 }}
run: |
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
yarn upgrade "$DEPENDENCY" --exact
git add package.json yarn.lock
git commit -am"Upgrade dependency to $DEPENDENCY"
git push origin staging
+35 -1
View File
@@ -25,11 +25,45 @@ permissions: {} # No permissions required
jobs:
release:
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop
permissions:
contents: write
issues: write
pull-requests: read
secrets: inherit
with:
final: ${{ inputs.mode == 'final' }}
npm: ${{ inputs.npm }}
downstreams: '["element-hq/element-web"]'
bump-downstreams:
name: Update npm dependency in downstream projects
needs: release
runs-on: ubuntu-24.04
strategy:
matrix:
repo:
- element-hq/element-web
steps:
- uses: actions/checkout@v4
with:
repository: ${{ matrix.repo }}
ref: staging
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version: "lts/*"
- name: Bump dependency
env:
DEPENDENCY: ${{ needs.release.outputs.npm-id }}
run: |
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
yarn upgrade "$DEPENDENCY" --exact
git add package.json yarn.lock
git commit -am"Upgrade dependency to $DEPENDENCY"
git push origin staging
docs:
name: Publish Documentation
+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
steps:
# We create the status here and then update it to success/failure in the `report` stage
# This provides an easy link to this workflow_run from the PR before Sonarcloud is done.
- uses: guibranco/github-status-action-v2@1f26a0237cd1a57626fbb5a0eb2494c9b8797d07
- uses: guibranco/github-status-action-v2@d469d49426f5a7b8a1fbcac20ad274d3e4892321
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending
@@ -87,7 +87,7 @@ jobs:
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }}
- uses: guibranco/github-status-action-v2@1f26a0237cd1a57626fbb5a0eb2494c9b8797d07
- uses: guibranco/github-status-action-v2@d469d49426f5a7b8a1fbcac20ad274d3e4892321
if: always()
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
+35
View File
@@ -44,6 +44,41 @@ jobs:
- name: Run Linter
run: "yarn run lint:js"
node_example_lint:
name: "Node.js example"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version-file: package.json
- name: Install Deps
run: "yarn install"
- name: Build Types
run: "yarn build:types"
- uses: actions/setup-node@v4
with:
cache: "npm"
node-version-file: "examples/node/package.json"
# cache-dependency-path: '**/package-lock.json'
- name: Install Example Deps
run: "npm install"
working-directory: "examples/node"
- name: Check Syntax
run: "node --check app.js"
working-directory: "examples/node"
- name: Typecheck
run: "npx tsc"
working-directory: "examples/node"
workflow_lint:
name: "Workflow Lint"
runs-on: ubuntu-24.04
+1 -1
View File
@@ -116,7 +116,7 @@ jobs:
steps:
- name: Skip SonarCloud on merge queues
if: env.ENABLE_COVERAGE == 'false'
uses: guibranco/github-status-action-v2@1f26a0237cd1a57626fbb5a0eb2494c9b8797d07
uses: guibranco/github-status-action-v2@d469d49426f5a7b8a1fbcac20ad274d3e4892321
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success
+79
View File
@@ -1,3 +1,82 @@
Changes in [36.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v36.0.0) (2025-01-14)
==================================================================================================
## 🚨 BREAKING CHANGES
* Remove support for "legacy" MSC3898 group calling in MatrixRTCSession and CallMembership ([#4583](https://github.com/matrix-org/matrix-js-sdk/pull/4583)). Contributed by @toger5.
## ✨ Features
* MatrixRTC: Implement expiry logic for CallMembership and additional test coverage ([#4587](https://github.com/matrix-org/matrix-js-sdk/pull/4587)). Contributed by @toger5.
## 🐛 Bug Fixes
* Don't retry on 4xx responses ([#4601](https://github.com/matrix-org/matrix-js-sdk/pull/4601)). Contributed by @dbkr.
* Upgrade matrix-sdk-crypto-wasm to 12.1.0 ([#4596](https://github.com/matrix-org/matrix-js-sdk/pull/4596)). Contributed by @andybalaam.
* Avoid key prompts when resetting crypto ([#4586](https://github.com/matrix-org/matrix-js-sdk/pull/4586)). Contributed by @dbkr.
* Handle when aud OIDC claim is an Array ([#4584](https://github.com/matrix-org/matrix-js-sdk/pull/4584)). Contributed by @liamdiprose.
* Save the key backup key to 4S during `bootstrapSecretStorage ` ([#4542](https://github.com/matrix-org/matrix-js-sdk/pull/4542)). Contributed by @dbkr.
* Only re-prepare MatrixrRTC delayed disconnection event on 404 ([#4575](https://github.com/matrix-org/matrix-js-sdk/pull/4575)). Contributed by @toger5.
Changes in [35.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v35.1.0) (2024-12-18)
==================================================================================================
This release updates matrix-sdk-crypto-wasm to fix a bug which could prevent loading stored crypto state from storage.
## 🐛 Bug Fixes
* Upgrade matrix-sdk-crypto-wasm to 1.11.0 ([#4593](https://github.com/matrix-org/matrix-js-sdk/pull/4593)).
Changes in [35.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v35.0.0) (2024-12-17)
==================================================================================================
## 🚨 BREAKING CHANGES
This release contains several breaking changes which will need code changes in your app. Most notably, `initCrypto()`
no longer exists and has been moved to `initLegacyCrypto()` in preparation for the eventual removal of Olm. You can
continue to use legacy Olm crypto for now by calling `initLegacyCrypto()` instead.
You may also need to make further changes if you use more advanced APIs. See the individual PRs (listed in order of size of change) for specific APIs changed and how to migrate.
* Rename `MatrixClient.initCrypto` into `MatrixClient.initLegacyCrypto` ([#4567](https://github.com/matrix-org/matrix-js-sdk/pull/4567)). Contributed by @florianduros.
* Support MSC4222 `state_after` ([#4487](https://github.com/matrix-org/matrix-js-sdk/pull/4487)). Contributed by @dbkr.
* Avoid use of Buffer as it does not exist in the Web natively ([#4569](https://github.com/matrix-org/matrix-js-sdk/pull/4569)). Contributed by @t3chguy.
## 🦖 Deprecations
* Deprecate remaining legacy functions and move `CryptoEvent.LegacyCryptoStoreMigrationProgress` handler ([#4560](https://github.com/matrix-org/matrix-js-sdk/pull/4560)). Contributed by @florianduros.
## ✨ Features
* Rename `MatrixClient.initCrypto` into `MatrixClient.initLegacyCrypto` ([#4567](https://github.com/matrix-org/matrix-js-sdk/pull/4567)). Contributed by @florianduros.
* Avoid use of Buffer as it does not exist in the Web natively ([#4569](https://github.com/matrix-org/matrix-js-sdk/pull/4569)). Contributed by @t3chguy.
* Re-send MatrixRTC media encryption keys for a new joiner even if a rotation is in progress ([#4561](https://github.com/matrix-org/matrix-js-sdk/pull/4561)). Contributed by @hughns.
* Support MSC4222 `state_after` ([#4487](https://github.com/matrix-org/matrix-js-sdk/pull/4487)). Contributed by @dbkr.
* Revert "Fix room state being updated with old (now overwritten) state and emitting for those updates. (#4242)" ([#4532](https://github.com/matrix-org/matrix-js-sdk/pull/4532)). Contributed by @toger5.
## 🐛 Bug Fixes
* Fix age field check in event echo processing ([#3635](https://github.com/matrix-org/matrix-js-sdk/pull/3635)). Contributed by @stas-demydiuk.
Changes in [34.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.13.0) (2024-12-03)
====================================================================================================
## 🦖 Deprecations
* Deprecate `MatrixClient.isEventSenderVerified` ([#4527](https://github.com/matrix-org/matrix-js-sdk/pull/4527)). Contributed by @florianduros.
* Add `restoreKeybackup` to `CryptoApi`. ([#4476](https://github.com/matrix-org/matrix-js-sdk/pull/4476)). Contributed by @florianduros.
## ✨ Features
* Ensure we disambiguate display names which look like MXIDs ([#4540](https://github.com/matrix-org/matrix-js-sdk/pull/4540)). Contributed by @t3chguy.
* Add `CryptoApi.getBackupInfo` ([#4512](https://github.com/matrix-org/matrix-js-sdk/pull/4512)). Contributed by @florianduros.
* Fix local echo in embedded mode ([#4498](https://github.com/matrix-org/matrix-js-sdk/pull/4498)). Contributed by @toger5.
* Add `restoreKeybackup` to `CryptoApi`. ([#4476](https://github.com/matrix-org/matrix-js-sdk/pull/4476)). Contributed by @florianduros.
## 🐛 Bug Fixes
* Fix `RustBackupManager` remaining values after current backup removal ([#4537](https://github.com/matrix-org/matrix-js-sdk/pull/4537)). Contributed by @florianduros.
Changes in [34.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.12.0) (2024-11-19)
====================================================================================================
## 🦖 Deprecations
+25 -23
View File
@@ -126,26 +126,26 @@ const img = await fetch(downloadUrl, {
This SDK provides a full object model around the Matrix Client-Server API and emits
events for incoming data and state changes. Aside from wrapping the HTTP API, it:
- Handles syncing (via `/sync`)
- Handles the generation of "friendly" room and member names.
- Handles historical `RoomMember` information (e.g. display names).
- Manages room member state across multiple events (e.g. it handles typing, power
levels and membership changes).
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
which can be listened to for things like name changes, new messages, membership
changes, presence changes, and more.
- Handle "local echo" of messages sent using the SDK. This means that messages
that have just been sent will appear in the timeline as 'sending', until it
completes. This is beneficial because it prevents there being a gap between
hitting the send button and having the "remote echo" arrive.
- Mark messages which failed to send as not sent.
- Automatically retry requests to send messages due to network errors.
- Automatically retry requests to send messages due to rate limiting errors.
- Handle queueing of messages.
- Handles pagination.
- Handle assigning push actions for events.
- Handles room initial sync on accepting invites.
- Handles WebRTC calling.
- Handles syncing (via `/sync`)
- Handles the generation of "friendly" room and member names.
- Handles historical `RoomMember` information (e.g. display names).
- Manages room member state across multiple events (e.g. it handles typing, power
levels and membership changes).
- Exposes high-level objects like `Rooms`, `RoomState`, `RoomMembers` and `Users`
which can be listened to for things like name changes, new messages, membership
changes, presence changes, and more.
- Handle "local echo" of messages sent using the SDK. This means that messages
that have just been sent will appear in the timeline as 'sending', until it
completes. This is beneficial because it prevents there being a gap between
hitting the send button and having the "remote echo" arrive.
- Mark messages which failed to send as not sent.
- Automatically retry requests to send messages due to network errors.
- Automatically retry requests to send messages due to rate limiting errors.
- Handle queueing of messages.
- Handles pagination.
- Handle assigning push actions for events.
- Handles room initial sync on accepting invites.
- Handles WebRTC calling.
# Usage
@@ -307,7 +307,7 @@ Then visit `http://localhost:8005` to see the API docs.
## Initialization
**Do not use `matrixClient.initCrypto()`. This method is deprecated and no longer maintained.**
**Do not use `matrixClient.initLegacyCrypto()`. This method is deprecated and no longer maintained.**
To initialize the end-to-end encryption support in the matrix client:
@@ -325,6 +325,8 @@ await matrixClient.initRustCrypto();
After calling `initRustCrypto`, you can obtain a reference to the [`CryptoApi`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html) interface, which is the main entry point for end-to-end encryption, by calling [`MatrixClient.getCrypto`](https://matrix-org.github.io/matrix-js-sdk/classes/matrix.MatrixClient.html#getCrypto).
**WARNING**: the cryptography stack is not thread-safe. Having multiple `MatrixClient` instances connected to the same Indexed DB will cause data corruption and decryption failures. The application layer is responsible for ensuring that only one `MatrixClient` issue is instantiated at a time.
## Secret storage
You should normally set up [secret storage](https://spec.matrix.org/v1.12/client-server-api/#secret-storage) before using the end-to-end encryption. To do this, call [`CryptoApi.bootstrapSecretStorage`](https://matrix-org.github.io/matrix-js-sdk/interfaces/crypto_api.CryptoApi.html#bootstrapSecretStorage).
@@ -396,10 +398,10 @@ Once the cross-signing is set up on one of your devices, you can verify another
## Migrating from the legacy crypto stack to Rust crypto
If your application previously used the legacy crypto stack, (i.e, it called `MatrixClient.initCrypto()`), you will
If your application previously used the legacy crypto stack, (i.e, it called `MatrixClient.initLegacyCrypto()`), you will
need to migrate existing devices to the Rust crypto stack.
This migration happens automatically when you call `initRustCrypto()` instead of `initCrypto()`,
This migration happens automatically when you call `initRustCrypto()` instead of `initLegacyCrypto()`,
but you need to provide the legacy [`cryptoStore`](https://matrix-org.github.io/matrix-js-sdk/interfaces/matrix.ICreateClientOpts.html#cryptoStore) and [`pickleKey`](https://matrix-org.github.io/matrix-js-sdk/interfaces/matrix.ICreateClientOpts.html#pickleKey) to [`createClient`](https://matrix-org.github.io/matrix-js-sdk/functions/matrix.createClient.html):
```javascript
+3 -3
View File
@@ -1,8 +1,8 @@
# Summary
- [Introduction](../README.md)
- [Introduction](../README.md)
# Deep dive
- [Storage notes](storage-notes.md)
- [Unverified devices](warning-on-unverified-devices.md)
- [Storage notes](storage-notes.md)
- [Unverified devices](warning-on-unverified-devices.md)
+27 -27
View File
@@ -20,19 +20,19 @@ blurrier.
When we are low on disk space overall or near the group limit / origin quota:
- Chrome
- Log database may fail to start with AbortError
- IndexedDB fails to start for crypto: AbortError in connect from
indexeddb-store-worker
- When near the quota, QuotaExceededError is used more consistently
- Firefox
- The first error will be QuotaExceededError
- Future write attempts will fail with various errors when space is low,
including nonsense like "InvalidStateError: A mutation operation was
attempted on a database that did not allow mutations."
- Once you start getting errors, the DB is effectively wedged in read-only
mode
- Can revive access if you reopen the DB
- Chrome
- Log database may fail to start with AbortError
- IndexedDB fails to start for crypto: AbortError in connect from
indexeddb-store-worker
- When near the quota, QuotaExceededError is used more consistently
- Firefox
- The first error will be QuotaExceededError
- Future write attempts will fail with various errors when space is low,
including nonsense like "InvalidStateError: A mutation operation was
attempted on a database that did not allow mutations."
- Once you start getting errors, the DB is effectively wedged in read-only
mode
- Can revive access if you reopen the DB
## Cache Eviction
@@ -41,9 +41,9 @@ limited by a single quota, in practice, browsers appear to handle `localStorage`
separately from the others, so it has a separate quota limit and isn't evicted
when low on space.
- Chrome, Firefox
- IndexedDB for origin deleted
- Local Storage remains in place
- Chrome, Firefox
- IndexedDB for origin deleted
- Local Storage remains in place
## Persistent Storage
@@ -51,20 +51,20 @@ Storage Standard offers a `navigator.storage.persist` API that can be used to
request persistent storage that won't be deleted by the browser because of low
space.
- Chrome
- Chrome 75 seems to grant this without any prompt based on [interaction
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
- Firefox
- Firefox 67 shows a prompt to grant
- Reverting persistent seems to require revoking permission _and_ clearing
site data
- Chrome
- Chrome 75 seems to grant this without any prompt based on [interaction
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
- Firefox
- Firefox 67 shows a prompt to grant
- Reverting persistent seems to require revoking permission _and_ clearing
site data
## Storage Estimation
Storage Standard offers a `navigator.storage.estimate` API to get some clue of
how much space remains.
- Chrome, Firefox
- Can run this at any time to request an estimate of space remaining
- Firefox
- Returns `0` for `usage` if a site is persisted
- Chrome, Firefox
- Can run this at any time to request an estimate of space remaining
- Firefox
- Returns `0` for `usage` if a site is persisted
+7 -7
View File
@@ -17,13 +17,13 @@ Warn when you initial sync if the room has any new undefined devices since you w
Warn when the user tries to send a message:
- If the room has unverified devices which the user has not yet been told about in the context of this room
...or in the context of this user? currently all verification is per-user, not per-room.
...this should be good enough.
- If the room has unverified devices which the user has not yet been told about in the context of this room
...or in the context of this user? currently all verification is per-user, not per-room.
...this should be good enough.
- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned.
throw an error when trying to encrypt if there are pure unverified devices there
app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway?
- or megolm could warn which devices are causing the problems.
- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned.
throw an error when trying to encrypt if there are pure unverified devices there
app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway?
- or megolm could warn which devices are causing the problems.
Why do we wait to establish outbound sessions? It just makes a horrible pause when we first try to send a message... but could otherwise unnecessarily consume resources?
+9 -15
View File
@@ -94,20 +94,14 @@ rl.on("line", function (line) {
);
} else if (line.indexOf("/file ") === 0) {
var filename = line.split(" ")[1].trim();
var stream = fs.createReadStream(filename);
matrixClient
.uploadContent({
stream: stream,
name: filename,
})
.then(function (url) {
var content = {
msgtype: MsgType.File,
body: filename,
url: JSON.parse(url).content_uri,
};
matrixClient.sendMessage(viewingRoom.roomId, content);
let buffer = fs.readFileSync("./your_file_name");
matrixClient.uploadContent(new Blob([buffer])).then(function (response) {
matrixClient.sendMessage(viewingRoom.roomId, {
msgtype: MsgType.File,
body: filename,
url: response.content_uri,
});
});
} else {
matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function () {
printMessages();
@@ -167,7 +161,7 @@ matrixClient.on(RoomEvent.Timeline, function (event, room, toStartOfTimeline) {
if (toStartOfTimeline) {
return; // don't print paginated results
}
if (!viewingRoom || viewingRoom.roomId !== room.roomId) {
if (!viewingRoom || viewingRoom.roomId !== room?.roomId) {
return; // not viewing a room or viewing the wrong room.
}
printLine(event);
@@ -386,7 +380,7 @@ function print(str, formatter) {
}
console.log.apply(console.log, newArgs);
} else {
console.log.apply(console.log, arguments);
console.log.apply(console.log, [...arguments]);
}
}
+7
View File
@@ -9,5 +9,12 @@
"dependencies": {
"cli-color": "^1.0.0",
"matrix-js-sdk": "^34.5.0"
},
"devDependencies": {
"@types/cli-color": "^2.0.6",
"typescript": "^5.6.2"
},
"engines": {
"node": ">=20.0.0"
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es2022",
"module": "commonjs",
"esModuleInterop": true,
"noImplicitAny": false,
"noEmit": true,
"skipLibCheck": true,
"allowJs": true,
"checkJs": true,
"strict": true
},
"include": ["app.js"]
}
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "34.13.0-rc.0",
"version": "36.0.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=20.0.0"
@@ -50,7 +50,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^12.1.0",
"@matrix-org/olm": "3.2.15",
"another-json": "^0.2.0",
"bs58": "^6.0.0",
@@ -103,7 +103,7 @@
"eslint-plugin-jsdoc": "^50.0.0",
"eslint-plugin-matrix-org": "^2.0.1",
"eslint-plugin-n": "^14.0.0",
"eslint-plugin-tsdoc": "^0.3.0",
"eslint-plugin-tsdoc": "^0.4.0",
"eslint-plugin-unicorn": "^56.0.0",
"fake-indexeddb": "^5.0.2",
"fetch-mock": "11.1.5",
@@ -117,12 +117,12 @@
"lint-staged": "^15.0.2",
"matrix-mock-request": "^2.5.0",
"node-fetch": "^2.7.0",
"prettier": "3.3.3",
"prettier": "3.4.2",
"rimraf": "^6.0.0",
"ts-node": "^10.9.2",
"typedoc": "^0.26.0",
"typedoc": "^0.27.0",
"typedoc-plugin-coverage": "^3.0.0",
"typedoc-plugin-mdn-links": "^3.0.3",
"typedoc-plugin-mdn-links": "^4.0.0",
"typedoc-plugin-missing-exports": "^3.0.0",
"typescript": "^5.4.2"
},
+33 -7
View File
@@ -1327,7 +1327,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const syncResponse = getSyncResponse(["@bob:xyz"]);
// Every 2 messages in the room, the session should be rotated
syncResponse.rooms[Category.Join][ROOM_ID].state.events[0].content = {
syncResponse.rooms[Category.Join][ROOM_ID].state!.events[0].content = {
algorithm: "m.megolm.v1.aes-sha2",
rotation_period_msgs: 2,
};
@@ -1383,7 +1383,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const oneHourInMs = 60 * 60 * 1000;
// Every 1h the session should be rotated
syncResponse.rooms[Category.Join][ROOM_ID].state.events[0].content = {
syncResponse.rooms[Category.Join][ROOM_ID].state!.events[0].content = {
algorithm: "m.megolm.v1.aes-sha2",
rotation_period_ms: oneHourInMs,
};
@@ -1741,7 +1741,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
groupSession: groupSession,
room_id: ROOM_ID,
});
await testClient.client.initCrypto();
await testClient.client.initLegacyCrypto();
const keys = [
{
room_id: ROOM_ID,
@@ -1853,7 +1853,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
oldBackendOnly("Alice receives shared history before being invited to a room by the sharer", async () => {
const beccaTestClient = new TestClient("@becca:localhost", "foobar", "bazquux");
await beccaTestClient.client.initCrypto();
await beccaTestClient.client.initLegacyCrypto();
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
@@ -2007,7 +2007,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
oldBackendOnly("Alice receives shared history before being invited to a room by someone else", async () => {
const beccaTestClient = new TestClient("@becca:localhost", "foobar", "bazquux");
await beccaTestClient.client.initCrypto();
await beccaTestClient.client.initLegacyCrypto();
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
@@ -3121,6 +3121,32 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
const mskId = await aliceClient.getCrypto()!.getCrossSigningKeyId(CrossSigningKey.Master)!;
expect(signatures![aliceClient.getUserId()!][`ed25519:${mskId}`]).toBeDefined();
});
newBackendOnly("should upload existing megolm backup key to a new 4S store", async () => {
const backupKeyTo4SPromise = awaitMegolmBackupKeyUpload();
// we need these to set up the mocks but we don't actually care whether they
// resolve because we're not testing those things in this test.
awaitCrossSigningKeyUpload("master");
awaitCrossSigningKeyUpload("user_signing");
awaitCrossSigningKeyUpload("self_signing");
awaitSecretStorageKeyStoredInAccountData();
mockSetupCrossSigningRequests();
mockSetupMegolmBackupRequests("1");
await aliceClient.getCrypto()!.bootstrapCrossSigning({});
await aliceClient.getCrypto()!.resetKeyBackup();
await aliceClient.getCrypto()!.bootstrapSecretStorage({
setupNewSecretStorage: true,
createSecretStorageKey,
setupNewKeyBackup: false,
});
await backupKeyTo4SPromise;
expect(accountDataAccumulator.accountDataEvents.get("m.megolm_backup.v1")).toBeDefined();
});
});
describe("Manage Key Backup", () => {
@@ -3142,7 +3168,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
fetchMock.put(
"path:/_matrix/client/v3/room_keys/keys",
(url, request) => {
const uploadPayload: KeyBackup = JSON.parse(request.body?.toString() ?? "{}");
const uploadPayload: KeyBackup = JSON.parse((request.body as string) ?? "{}");
resolve(uploadPayload);
return {
status: 200,
@@ -3209,7 +3235,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
fetchMock.post(
"path:/_matrix/client/v3/room_keys/version",
(url, request) => {
const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}");
const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}");
backupData.version = newVersion;
backupData.count = 0;
backupData.etag = "zer";
+1 -1
View File
@@ -91,7 +91,7 @@ function mockUploadEmitter(
},
};
}
const uploadPayload: KeyBackup = JSON.parse(request.body?.toString() ?? "{}");
const uploadPayload: KeyBackup = JSON.parse((request.body as string) ?? "{}");
let count = 0;
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
for (const sessionId of Object.keys(value.sessions)) {
+2 -2
View File
@@ -345,10 +345,10 @@ describe("MatrixClient crypto", () => {
beforeEach(async () => {
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
await aliTestClient.client.initCrypto();
await aliTestClient.client.initLegacyCrypto();
bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken);
await bobTestClient.client.initCrypto();
await bobTestClient.client.initLegacyCrypto();
aliMessages = [];
bobMessages = [];
+30 -15
View File
@@ -78,6 +78,7 @@ import {
encryptGroupSessionKey,
encryptMegolmEvent,
encryptSecretSend,
getTestOlmAccountKeys,
ToDeviceEvent,
} from "./olm-utils";
import { KeyBackupInfo } from "../../../src/crypto-api";
@@ -473,21 +474,23 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(request.phase).toEqual(VerificationPhase.Ready);
// we should now have QR data we can display
const qrCodeBuffer = (await request.generateQRCode())!;
expect(qrCodeBuffer).toBeTruthy();
const rawQrCodeBuffer = (await request.generateQRCode())!;
expect(rawQrCodeBuffer).toBeTruthy();
const qrCodeBuffer = new Uint8Array(rawQrCodeBuffer);
const textDecoder = new TextDecoder();
// https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
expect(qrCodeBuffer.subarray(0, 6).toString("latin1")).toEqual("MATRIX");
expect(qrCodeBuffer.readUint8(6)).toEqual(0x02); // version
expect(qrCodeBuffer.readUint8(7)).toEqual(0x02); // mode
const txnIdLen = qrCodeBuffer.readUint16BE(8);
expect(qrCodeBuffer.subarray(10, 10 + txnIdLen).toString("utf-8")).toEqual(transactionId);
expect(textDecoder.decode(qrCodeBuffer.slice(0, 6))).toEqual("MATRIX");
expect(qrCodeBuffer[6]).toEqual(0x02); // version
expect(qrCodeBuffer[7]).toEqual(0x02); // mode
const txnIdLen = (qrCodeBuffer[8] << 8) + qrCodeBuffer[9];
expect(textDecoder.decode(qrCodeBuffer.slice(10, 10 + txnIdLen))).toEqual(transactionId);
// Alice's device's public key comes next, but we have nothing to do with it here.
// const aliceDevicePubKey = qrCodeBuffer.subarray(10 + txnIdLen, 32 + 10 + txnIdLen);
expect(qrCodeBuffer.subarray(42 + txnIdLen, 32 + 42 + txnIdLen)).toEqual(
Buffer.from(MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64, "base64"),
// const aliceDevicePubKey = qrCodeBuffer.slice(10 + txnIdLen, 32 + 10 + txnIdLen);
expect(encodeUnpaddedBase64(qrCodeBuffer.slice(42 + txnIdLen, 32 + 42 + txnIdLen))).toEqual(
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
);
const sharedSecret = qrCodeBuffer.subarray(74 + txnIdLen);
const sharedSecret = qrCodeBuffer.slice(74 + txnIdLen);
// we should still be "Ready" and have no verifier
expect(request.phase).toEqual(VerificationPhase.Ready);
@@ -805,7 +808,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// we should now have QR data we can display
const qrCodeBuffer = (await request.generateQRCode())!;
expect(qrCodeBuffer).toBeTruthy();
const sharedSecret = qrCodeBuffer.subarray(74 + transactionId.length);
const sharedSecret = qrCodeBuffer.slice(74 + transactionId.length);
// the dummy device "scans" the displayed QR code and acknowledges it with a "m.key.verification.start"
returnToDeviceMessageFromSync(buildReciprocateStartMessage(transactionId, sharedSecret));
@@ -990,6 +993,18 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
aliceClient.setGlobalErrorOnUnknownDevices(false);
syncResponder.sendOrQueueSyncResponse(getSyncResponse([BOB_TEST_USER_ID]));
await syncPromise(aliceClient);
// Rust crypto requires the sender's device keys before it accepts a
// verification request.
if (backend === "rust-sdk") {
const crypto = aliceClient.getCrypto()!;
const bobDeviceKeys = getTestOlmAccountKeys(testOlmAccount, BOB_TEST_USER_ID, "BobDevice");
e2eKeyResponder.addDeviceKeys(bobDeviceKeys);
syncResponder.sendOrQueueSyncResponse({ device_lists: { changed: [BOB_TEST_USER_ID] } });
await syncPromise(aliceClient);
await crypto.getUserDeviceInfo([BOB_TEST_USER_ID]);
}
});
/**
@@ -1627,7 +1642,7 @@ function buildReadyMessage(
}
/** build an m.key.verification.start to-device message suitable for the m.reciprocate.v1 flow, originating from the dummy device */
function buildReciprocateStartMessage(transactionId: string, sharedSecret: Uint8Array) {
function buildReciprocateStartMessage(transactionId: string, sharedSecret: ArrayBuffer) {
return {
type: "m.key.verification.start",
content: {
@@ -1723,7 +1738,7 @@ function buildQRCode(
key2Base64: string,
sharedSecret: string,
mode = 0x02,
): Uint8Array {
): Uint8ClampedArray {
// https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
const qrCodeBuffer = Buffer.alloc(150); // oversize
@@ -1739,5 +1754,5 @@ function buildQRCode(
idx += qrCodeBuffer.write(sharedSecret, idx);
// truncate to the right length
return qrCodeBuffer.subarray(0, idx);
return new Uint8ClampedArray(qrCodeBuffer.subarray(0, idx));
}
+1 -1
View File
@@ -77,7 +77,7 @@ describe("DeviceList management:", function () {
async function createTestClient() {
const testClient = new TestClient("@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend);
await testClient.client.initCrypto();
await testClient.client.initLegacyCrypto();
return testClient;
}
@@ -1144,7 +1144,7 @@ describe("MatrixClient event timelines", function () {
const prom = emitPromise(room, ThreadEvent.Update);
// Assume we're seeing the reply while loading backlog
await room.addLiveEvents([THREAD_REPLY2]);
await room.addLiveEvents([THREAD_REPLY2], { addToState: false });
httpBackend
.when(
"GET",
@@ -1155,7 +1155,7 @@ describe("MatrixClient event timelines", function () {
});
await flushHttp(prom);
// but while loading the metadata, a new reply has arrived
await room.addLiveEvents([THREAD_REPLY3]);
await room.addLiveEvents([THREAD_REPLY3], { addToState: false });
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
// then the events should still be all in the right order
expect(thread.events.map((it) => it.getId())).toEqual([
@@ -1247,7 +1247,7 @@ describe("MatrixClient event timelines", function () {
const prom = emitPromise(room, ThreadEvent.Update);
// Assume we're seeing the reply while loading backlog
await room.addLiveEvents([THREAD_REPLY2]);
await room.addLiveEvents([THREAD_REPLY2], { addToState: false });
httpBackend
.when(
"GET",
@@ -1263,7 +1263,7 @@ describe("MatrixClient event timelines", function () {
});
await flushHttp(prom);
// but while loading the metadata, a new reply has arrived
await room.addLiveEvents([THREAD_REPLY3]);
await room.addLiveEvents([THREAD_REPLY3], { addToState: false });
const thread = room.getThread(THREAD_ROOT_UPDATED.event_id!)!;
// then the events should still be all in the right order
expect(thread.events.map((it) => it.getId())).toEqual([
@@ -1560,7 +1560,7 @@ describe("MatrixClient event timelines", function () {
thread.initialEventsFetched = true;
const prom = emitPromise(room, ThreadEvent.NewReply);
respondToEvent(THREAD_ROOT_UPDATED);
await room.addLiveEvents([THREAD_REPLY2]);
await room.addLiveEvents([THREAD_REPLY2], { addToState: false });
await httpBackend.flushAllExpected();
await prom;
expect(thread.length).toBe(2);
@@ -1685,7 +1685,7 @@ describe("MatrixClient event timelines", function () {
thread.initialEventsFetched = true;
const prom = emitPromise(room, ThreadEvent.Update);
respondToEvent(THREAD_ROOT_UPDATED);
await room.addLiveEvents([THREAD_REPLY_REACTION]);
await room.addLiveEvents([THREAD_REPLY_REACTION], { addToState: false });
await httpBackend.flushAllExpected();
await prom;
expect(thread.length).toBe(1); // reactions don't count towards the length of a thread
+57 -26
View File
@@ -168,14 +168,17 @@ describe("MatrixClient", function () {
type: "test",
content: {},
});
room.addLiveEvents([
utils.mkMembership({
user: userId,
room: roomId,
mship: KnownMembership.Join,
event: true,
}),
]);
room.addLiveEvents(
[
utils.mkMembership({
user: userId,
room: roomId,
mship: KnownMembership.Join,
event: true,
}),
],
{ addToState: true },
);
httpBackend.verifyNoOutstandingRequests();
store.storeRoom(room);
@@ -188,14 +191,17 @@ describe("MatrixClient", function () {
const roomId = "!roomId:server";
const roomAlias = "#my-fancy-room:server";
const room = new Room(roomId, client, userId);
room.addLiveEvents([
utils.mkMembership({
user: userId,
room: roomId,
mship: KnownMembership.Join,
event: true,
}),
]);
room.addLiveEvents(
[
utils.mkMembership({
user: userId,
room: roomId,
mship: KnownMembership.Join,
event: true,
}),
],
{ addToState: true },
);
store.storeRoom(room);
// The method makes a request to resolve the alias
@@ -275,14 +281,17 @@ describe("MatrixClient", function () {
content: {},
});
room.addLiveEvents([
utils.mkMembership({
user: userId,
room: roomId,
mship: KnownMembership.Knock,
event: true,
}),
]);
room.addLiveEvents(
[
utils.mkMembership({
user: userId,
room: roomId,
mship: KnownMembership.Knock,
event: true,
}),
],
{ addToState: true },
);
httpBackend.verifyNoOutstandingRequests();
store.storeRoom(room);
@@ -641,9 +650,9 @@ describe("MatrixClient", function () {
}
beforeEach(function () {
// running initCrypto should trigger a key upload
// running initLegacyCrypto should trigger a key upload
httpBackend.when("POST", "/keys/upload").respond(200, {});
return Promise.all([client.initCrypto(), httpBackend.flush("/keys/upload", 1)]);
return Promise.all([client.initLegacyCrypto(), httpBackend.flush("/keys/upload", 1)]);
});
afterEach(() => {
@@ -1906,6 +1915,28 @@ describe("MatrixClient", function () {
return prom;
});
});
describe("getDomain", () => {
it("should return null if no userId is set", () => {
const client = new MatrixClient({ baseUrl: "http://localhost" });
expect(client.getDomain()).toBeNull();
});
it("should return the domain of the userId", () => {
expect(client.getDomain()).toBe("localhost");
});
});
describe("getUserIdLocalpart", () => {
it("should return null if no userId is set", () => {
const client = new MatrixClient({ baseUrl: "http://localhost" });
expect(client.getUserIdLocalpart()).toBeNull();
});
it("should return the localpart of the userId", () => {
expect(client.getUserIdLocalpart()).toBe("alice");
});
});
});
function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
+173 -7
View File
@@ -50,6 +50,13 @@ import { THREAD_RELATION_TYPE } from "../../src/models/thread";
import { IActionsObject } from "../../src/pushprocessor";
import { KnownMembership } from "../../src/@types/membership";
declare module "../../src/@types/event" {
interface AccountDataEvents {
a: {};
b: {};
}
}
describe("MatrixClient syncing", () => {
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
@@ -112,7 +119,7 @@ describe("MatrixClient syncing", () => {
});
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
await client!.initCrypto();
await client!.initLegacyCrypto();
const roomId = "!cycles:example.org";
@@ -227,7 +234,7 @@ describe("MatrixClient syncing", () => {
});
it("should emit RoomEvent.MyMembership for knock->leave->knock cycles", async () => {
await client!.initCrypto();
await client!.initLegacyCrypto();
const roomId = "!cycles:example.org";
@@ -556,7 +563,7 @@ describe("MatrixClient syncing", () => {
});
it("should resolve incoming invites from /sync", () => {
syncData.rooms.join[roomOne].state.events.push(
syncData.rooms.join[roomOne].state!.events.push(
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Invite,
@@ -589,7 +596,7 @@ describe("MatrixClient syncing", () => {
name: "The Ghost",
}) as IMinimalEvent,
];
syncData.rooms.join[roomOne].state.events.push(
syncData.rooms.join[roomOne].state!.events.push(
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Invite,
@@ -617,7 +624,7 @@ describe("MatrixClient syncing", () => {
name: "The Ghost",
}) as IMinimalEvent,
];
syncData.rooms.join[roomOne].state.events.push(
syncData.rooms.join[roomOne].state!.events.push(
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Invite,
@@ -644,7 +651,7 @@ describe("MatrixClient syncing", () => {
});
it("should no-op if resolveInvitesToProfiles is not set", () => {
syncData.rooms.join[roomOne].state.events.push(
syncData.rooms.join[roomOne].state!.events.push(
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Invite,
@@ -1373,6 +1380,114 @@ describe("MatrixClient syncing", () => {
expect(stateEventEmitCount).toEqual(2);
});
});
describe("msc4222", () => {
const roomOneSyncOne = {
"timeline": {
events: [
utils.mkMessage({
room: roomOne,
user: otherUserId,
msg: "hello",
}),
],
},
"org.matrix.msc4222.state_after": {
events: [
utils.mkEvent({
type: "m.room.name",
room: roomOne,
user: otherUserId,
content: {
name: "Initial room name",
},
}),
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Join,
user: otherUserId,
}),
utils.mkMembership({
room: roomOne,
mship: KnownMembership.Join,
user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create",
room: roomOne,
user: selfUserId,
content: {},
}),
],
},
};
const roomOneSyncTwo = {
"org.matrix.msc4222.state_after": {
events: [
utils.mkEvent({
type: "m.room.topic",
room: roomOne,
user: selfUserId,
content: { topic: "A new room topic" },
}),
],
},
"state": {
events: [
utils.mkEvent({
type: "m.room.name",
room: roomOne,
user: selfUserId,
content: { name: "A new room name" },
}),
],
},
};
it("should ignore state events in timeline when state_after is present", async () => {
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
join: { [roomOne]: roomOneSyncOne },
},
});
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
join: { [roomOne]: roomOneSyncTwo },
},
});
client!.startClient();
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent(2)]).then(() => {
const room = client!.getRoom(roomOne)!;
expect(room.name).toEqual("Initial room name");
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
"A new room topic",
);
});
});
it("should respect state events in state_after for left rooms", async () => {
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
join: { [roomOne]: roomOneSyncOne },
},
});
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
leave: { [roomOne]: roomOneSyncTwo },
},
});
client!.startClient();
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent(2)]).then(() => {
const room = client!.getRoom(roomOne)!;
expect(room.name).toEqual("Initial room name");
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
"A new room topic",
);
});
});
});
});
describe("timeline", () => {
@@ -2274,6 +2389,57 @@ describe("MatrixClient syncing", () => {
}),
]);
});
describe("msc4222", () => {
it("should respect state events in state_after for left rooms", async () => {
httpBackend!.when("POST", "/filter").respond(200, {
filter_id: "another_id",
});
httpBackend!.when("GET", "/sync").respond(200, {
rooms: {
leave: {
[roomOne]: {
"org.matrix.msc4222.state_after": {
events: [
utils.mkEvent({
type: "m.room.topic",
room: roomOne,
user: selfUserId,
content: { topic: "A new room topic" },
}),
],
},
"state": {
events: [
utils.mkEvent({
type: "m.room.name",
room: roomOne,
user: selfUserId,
content: { name: "A new room name" },
}),
],
},
},
},
},
});
const [[room]] = await Promise.all([
client!.syncLeftRooms(),
// first flush the filter request; this will make syncLeftRooms make its /sync call
httpBackend!.flush("/filter").then(() => {
return httpBackend!.flushAllExpected();
}),
]);
expect(room.name).toEqual("Empty room");
expect(room.currentState.getStateEvents("m.room.topic", "")?.getContent().topic).toBe(
"A new room topic",
);
});
});
});
describe("peek", () => {
@@ -2414,7 +2580,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
await idbClient.initCrypto();
await idbClient.initLegacyCrypto();
const roomId = "!invite:example.org";
@@ -128,7 +128,7 @@ describe("MatrixClient syncing", () => {
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
const threadReply = thread.events.at(-1)!;
await room.addLiveEvents([thread.rootEvent]);
await room.addLiveEvents([thread.rootEvent], { addToState: false });
// Initialize read receipt datastructure before testing the reaction
room.addReceiptToStructure(thread.rootEvent.getId()!, ReceiptType.Read, selfUserId, { ts: 1 }, false);
+14 -9
View File
@@ -45,6 +45,13 @@ import { emitPromise } from "../test-utils/test-utils";
import { defer } from "../../src/utils";
import { KnownMembership } from "../../src/@types/membership";
declare module "../../src/@types/event" {
interface AccountDataEvents {
global_test: {};
tester: {};
}
}
describe("SlidingSyncSdk", () => {
let client: MatrixClient | undefined;
let httpBackend: MockHttpBackend | undefined;
@@ -119,7 +126,7 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync = mockifySlidingSync(new SlidingSync("", new Map(), {}, client, 0));
if (testOpts.withCrypto) {
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
await client!.initCrypto();
await client!.initLegacyCrypto();
syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto;
}
httpBackend!.when("GET", "/_matrix/client/v3/pushrules").respond(200, {});
@@ -601,13 +608,13 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
initial: true,
name: "Room with Invite",
required_state: [],
timeline: [
required_state: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Invite }, invitee),
],
timeline: [],
});
await httpBackend!.flush("/profile", 1, 1000);
await emitPromise(client!, RoomMemberEvent.Name);
@@ -921,13 +928,12 @@ describe("SlidingSyncSdk", () => {
const roomId = "!room:id";
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with typing",
required_state: [],
timeline: [
required_state: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
timeline: [mkOwnEvent(EventType.RoomMessage, { body: "hello" })],
initial: true,
});
await emitPromise(client!, ClientEvent.Room);
@@ -962,13 +968,12 @@ describe("SlidingSyncSdk", () => {
const roomId = "!room:id";
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with typing",
required_state: [],
timeline: [
required_state: [
mkOwnStateEvent(EventType.RoomCreate, {}, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: KnownMembership.Join }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
mkOwnEvent(EventType.RoomMessage, { body: "hello" }),
],
timeline: [mkOwnEvent(EventType.RoomMessage, { body: "hello" })],
initial: true,
});
const room = client!.getRoom(roomId)!;
+1 -1
View File
@@ -88,7 +88,7 @@ export function mockSetupMegolmBackupRequests(backupVersion: string): void {
});
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (url, request) => {
const backupData: KeyBackupInfo = JSON.parse(request.body?.toString() ?? "{}");
const backupData: KeyBackupInfo = JSON.parse((request.body as string) ?? "{}");
backupData.version = backupVersion;
backupData.count = 0;
backupData.etag = "zer";
+2 -2
View File
@@ -86,7 +86,7 @@ export function getSyncResponse(roomMembers: string[], roomId = TEST_ROOM_ID): I
};
for (let i = 0; i < roomMembers.length; i++) {
roomResponse.state.events.push(
roomResponse.state!.events.push(
mkMembershipCustom({
membership: KnownMembership.Join,
sender: roomMembers[i],
@@ -561,7 +561,7 @@ export type InitCrypto = (_: MatrixClient) => Promise<void>;
CRYPTO_BACKENDS["rust-sdk"] = (client: MatrixClient) => client.initRustCrypto();
if (globalThis.Olm) {
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initCrypto();
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initLegacyCrypto();
}
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise((r) => e.once(k, r));
+1 -1
View File
@@ -178,6 +178,6 @@ export const populateThread = ({
}: MakeThreadProps): MakeThreadResult => {
const ret = mkThread({ room, client, authorId, participantUserIds, length, ts });
ret.thread.initialEventsFetched = true;
room.addLiveEvents(ret.events);
room.addLiveEvents(ret.events, { addToState: false });
return ret;
};
+3
View File
@@ -481,6 +481,9 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
public getUserId(): string {
return this.userId;
}
public getSafeUserId(): string {
return this.userId;
}
public getDeviceId(): string {
return this.deviceId;
+1 -25
View File
@@ -15,34 +15,10 @@ limitations under the License.
*/
import { TextEncoder, TextDecoder } from "util";
import NodeBuffer from "node:buffer";
import { decodeBase64, encodeBase64, encodeUnpaddedBase64, encodeUnpaddedBase64Url } from "../../src/base64";
describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => {
let origBuffer = Buffer;
beforeAll(() => {
if (env === "browser") {
origBuffer = Buffer;
// @ts-ignore
// eslint-disable-next-line no-global-assign
Buffer = undefined;
globalThis.atob = NodeBuffer.atob;
globalThis.btoa = NodeBuffer.btoa;
}
});
afterAll(() => {
// eslint-disable-next-line no-global-assign
Buffer = origBuffer;
// @ts-ignore
globalThis.atob = undefined;
// @ts-ignore
globalThis.btoa = undefined;
});
describe("Base64 encoding", () => {
it("Should decode properly encoded data", () => {
const decoded = new TextDecoder().decode(decodeBase64("ZW5jb2RpbmcgaGVsbG8gd29ybGQ="));
+16 -16
View File
@@ -119,7 +119,7 @@ describe("Crypto", function () {
it("getVersion() should return the current version of the olm library", async () => {
const client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto();
await client.initLegacyCrypto();
const olmVersionTuple = Crypto.getOlmVersion();
expect(client.getCrypto()?.getVersion()).toBe(
@@ -130,7 +130,7 @@ describe("Crypto", function () {
describe("encrypted events", function () {
it("provides encryption information for events from unverified senders", async function () {
const client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto();
await client.initLegacyCrypto();
// unencrypted event
const event = {
@@ -210,7 +210,7 @@ describe("Crypto", function () {
let client: MatrixClient;
beforeEach(async () => {
client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto();
await client.initLegacyCrypto();
// mock out the verification check
client.crypto!.checkUserTrust = (userId) => new UserTrustLevel(true, false, false);
@@ -306,7 +306,7 @@ describe("Crypto", function () {
it("doesn't throw an error when attempting to decrypt a redacted event", async () => {
const client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto();
await client.initLegacyCrypto();
const event = new MatrixEvent({
content: {},
@@ -439,10 +439,10 @@ describe("Crypto", function () {
secondAliceClient = new TestClient("@alice:example.com", "secondAliceDevice").client;
bobClient = new TestClient("@bob:example.com", "bobdevice").client;
claraClient = new TestClient("@clara:example.com", "claradevice").client;
await aliceClient.initCrypto();
await secondAliceClient.initCrypto();
await bobClient.initCrypto();
await claraClient.initCrypto();
await aliceClient.initLegacyCrypto();
await secondAliceClient.initLegacyCrypto();
await bobClient.initLegacyCrypto();
await claraClient.initLegacyCrypto();
});
afterEach(async function () {
@@ -1111,7 +1111,7 @@ describe("Crypto", function () {
jest.spyOn(logger, "debug").mockImplementation(() => {});
jest.setTimeout(10000);
const client = new TestClient("@a:example.com", "dev").client;
await client.initCrypto();
await client.initLegacyCrypto();
client.crypto!.isCrossSigningReady = async () => false;
client.crypto!.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
client.crypto!.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
@@ -1147,9 +1147,9 @@ describe("Crypto", function () {
client = new TestClient("@alice:example.org", "aliceweb");
// running initCrypto should trigger a key upload
// running initLegacyCrypto should trigger a key upload
client.httpBackend.when("POST", "/keys/upload").respond(200, {});
await Promise.all([client.client.initCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
await Promise.all([client.client.initLegacyCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
encryptedPayload = {
algorithm: "m.olm.v1.curve25519-aes-sha2",
@@ -1264,9 +1264,9 @@ describe("Crypto", function () {
client = new TestClient("@alice:example.org", "aliceweb");
// running initCrypto should trigger a key upload
// running initLegacyCrypto should trigger a key upload
client.httpBackend.when("POST", "/keys/upload").respond(200, {});
await Promise.all([client.client.initCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
await Promise.all([client.client.initLegacyCrypto(), client.httpBackend.flush("/keys/upload", 1)]);
encryptedPayload = {
algorithm: "m.olm.v1.curve25519-aes-sha2",
@@ -1362,7 +1362,7 @@ describe("Crypto", function () {
beforeEach(async () => {
client = new TestClient("@alice:example.org", "aliceweb");
await client.client.initCrypto();
await client.client.initLegacyCrypto();
});
afterEach(async () => {
@@ -1388,7 +1388,7 @@ describe("Crypto", function () {
beforeEach(async () => {
client = new TestClient("@alice:example.org", "aliceweb");
await client.client.initCrypto();
await client.client.initLegacyCrypto();
});
afterEach(async () => {
@@ -1414,7 +1414,7 @@ describe("Crypto", function () {
beforeEach(async () => {
client = new TestClient("@alice:example.org", "aliceweb");
await client.client.initCrypto();
await client.client.initLegacyCrypto();
});
afterEach(async function () {
+10 -6
View File
@@ -610,7 +610,11 @@ describe("MegolmDecryption", function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient1 = new TestClient("@bob:example.com", "bobdevice1").client;
const bobClient2 = new TestClient("@bob:example.com", "bobdevice2").client;
await Promise.all([aliceClient.initCrypto(), bobClient1.initCrypto(), bobClient2.initCrypto()]);
await Promise.all([
aliceClient.initLegacyCrypto(),
bobClient1.initLegacyCrypto(),
bobClient2.initLegacyCrypto(),
]);
const aliceDevice = aliceClient.crypto!.olmDevice;
const bobDevice1 = bobClient1.crypto!.olmDevice;
const bobDevice2 = bobClient2.crypto!.olmDevice;
@@ -704,7 +708,7 @@ describe("MegolmDecryption", function () {
it("does not block unverified devices when sending verification events", async function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
const bobDevice = bobClient.crypto!.olmDevice;
const encryptionCfg = {
@@ -789,7 +793,7 @@ describe("MegolmDecryption", function () {
it("notifies devices when unable to create olm session", async function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
const aliceDevice = aliceClient.crypto!.olmDevice;
const bobDevice = bobClient.crypto!.olmDevice;
@@ -873,7 +877,7 @@ describe("MegolmDecryption", function () {
it("throws an error describing why it doesn't have a key", async function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
const bobDevice = bobClient.crypto!.olmDevice;
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
@@ -955,7 +959,7 @@ describe("MegolmDecryption", function () {
it("throws an error describing the lack of an olm session", async function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
@@ -1051,7 +1055,7 @@ describe("MegolmDecryption", function () {
it("throws an error to indicate a wedged olm session", async function () {
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
const bobClient = new TestClient("@bob:example.com", "bobdevice").client;
await Promise.all([aliceClient.initCrypto(), bobClient.initCrypto()]);
await Promise.all([aliceClient.initLegacyCrypto(), bobClient.initLegacyCrypto()]);
const aliceEventEmitter = new TypedEventEmitter<ClientEvent.ToDeviceEvent, any>();
aliceClient.crypto!.registerEventHandlers(aliceEventEmitter);
+8 -8
View File
@@ -231,7 +231,7 @@ describe("MegolmBackup", function () {
test("fail if given backup has no version", async () => {
const client = makeTestClient(cryptoStore);
await client.initCrypto();
await client.initLegacyCrypto();
const data = {
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
auth_data: {
@@ -314,7 +314,7 @@ describe("MegolmBackup", function () {
megolmDecryption.olmlib = mockOlmLib;
return client
.initCrypto()
.initLegacyCrypto()
.then(() => {
return cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
cryptoStore.addEndToEndInboundGroupSession(
@@ -391,7 +391,7 @@ describe("MegolmBackup", function () {
megolmDecryption.olmlib = mockOlmLib;
return client
.initCrypto()
.initLegacyCrypto()
.then(() => {
return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32));
})
@@ -471,7 +471,7 @@ describe("MegolmBackup", function () {
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
await client.initCrypto();
await client.initLegacyCrypto();
client.uploadDeviceSigningKeys = async function (e) {
return {};
};
@@ -560,7 +560,7 @@ describe("MegolmBackup", function () {
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
await client.initCrypto();
await client.initLegacyCrypto();
await cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
@@ -636,7 +636,7 @@ describe("MegolmBackup", function () {
// @ts-ignore private field access
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto();
return client.initLegacyCrypto();
});
afterEach(function () {
@@ -773,7 +773,7 @@ describe("MegolmBackup", function () {
// initialising the crypto library will trigger a key upload request, which we can stub out
client.uploadKeysRequest = jest.fn();
await client.initCrypto();
await client.initLegacyCrypto();
cryptoStore.countSessionsNeedingBackup = jest.fn().mockReturnValue(6);
await expect(client.flagAllGroupSessionsForBackup()).resolves.toBe(6);
@@ -784,7 +784,7 @@ describe("MegolmBackup", function () {
describe("getKeyBackupInfo", () => {
it("should return throw an `Not implemented`", async () => {
const client = makeTestClient(cryptoStore);
await client.initCrypto();
await client.initLegacyCrypto();
await expect(client.getCrypto()?.getKeyBackupInfo()).rejects.toThrow("Not implemented");
});
});
+1 -1
View File
@@ -78,7 +78,7 @@ async function makeTestClient(
const testClient = new TestClient(userInfo.userId, userInfo.deviceId, undefined, undefined, options);
const client = testClient.client;
await client.initCrypto();
await client.initLegacyCrypto();
return { client, httpBackend: testClient.httpBackend };
}
+1 -1
View File
@@ -68,7 +68,7 @@ describe("Dehydration", () => {
},
});
await alice.client.initCrypto();
await alice.client.initLegacyCrypto();
alice.httpBackend.when("GET", "/room_keys/version").respond(404, {
errcode: "M_NOT_FOUND",
+8 -1
View File
@@ -30,6 +30,7 @@ import { ICurve25519AuthData } from "../../../src/crypto/keybackup";
import { SecretStorageKeyDescription, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { decodeBase64 } from "../../../src/base64";
import { CrossSigningKeyInfo } from "../../../src/crypto-api";
import { SecretInfo } from "../../../src/secret-storage.ts";
async function makeTestClient(
userInfo: { userId: string; deviceId: string },
@@ -43,7 +44,7 @@ async function makeTestClient(
return true;
};
await client.initCrypto();
await client.initLegacyCrypto();
// No need to download keys for these tests
jest.spyOn(client.crypto!, "downloadKeys").mockResolvedValue(new Map());
@@ -68,6 +69,12 @@ function sign<T extends IObject | ICurve25519AuthData>(
};
}
declare module "../../../src/@types/event" {
interface SecretStorageAccountDataEvents {
foo: SecretInfo;
}
}
describe("Secrets", function () {
if (!globalThis.Olm) {
logger.warn("Not running megolm backup unit tests: libolm not present");
+1 -1
View File
@@ -119,7 +119,7 @@ export async function makeTestClients(
clients.push(testClient);
}
await Promise.all(clients.map((testClient) => testClient.client.initCrypto()));
await Promise.all(clients.map((testClient) => testClient.client.initLegacyCrypto()));
const destroy = () => {
timeouts.forEach((t) => clearTimeout(t));
+4 -4
View File
@@ -261,7 +261,7 @@ describe("RoomWidgetClient", () => {
expect(injectSpy).toHaveBeenCalled();
const call = injectSpy.mock.calls[0] as any;
const injectedEv = call[2][0];
const injectedEv = call[3][0];
expect(injectedEv.getType()).toBe("org.matrix.rageshake_request");
expect(injectedEv.getUnsigned().transaction_id).toBe("widgetTxId");
});
@@ -287,7 +287,7 @@ describe("RoomWidgetClient", () => {
expect(injectSpy).toHaveBeenCalled();
const call = injectSpy.mock.calls[0] as any;
const injectedEv = call[2][0];
const injectedEv = call[3][0];
expect(injectedEv.getType()).toBe("org.matrix.rageshake_request");
expect(injectedEv.getUnsigned().transaction_id).toBe("widgetTxId");
});
@@ -326,13 +326,13 @@ describe("RoomWidgetClient", () => {
// it has been called with the event sent by ourselves
const call = injectSpy.mock.calls[0] as any;
const injectedEv = call[2][0];
const injectedEv = call[3][0];
expect(injectedEv.getType()).toBe("org.matrix.rageshake_request");
expect(injectedEv.getUnsigned().transaction_id).toBe("widgetTxId");
// It has been called by the event we blocked because of our send right afterwards
const call2 = injectSpy.mock.calls[1] as any;
const injectedEv2 = call2[2][0];
const injectedEv2 = call2[3][0];
expect(injectedEv2.getType()).toBe("org.matrix.rageshake_request");
expect(injectedEv2.getUnsigned().transaction_id).toBe("4567");
});
+2 -2
View File
@@ -74,7 +74,7 @@ describe("eventMapperFor", function () {
const event = mapper(eventDefinition);
expect(event).toBeInstanceOf(MatrixEvent);
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
expect(room.findEventById(eventId)).toBe(event);
const event2 = mapper(eventDefinition);
@@ -109,7 +109,7 @@ describe("eventMapperFor", function () {
room.oldState.setStateEvents([event]);
room.currentState.setStateEvents([event]);
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
expect(room.findEventById(eventId)).toBe(event);
const event2 = mapper(eventDefinition);
+21 -5
View File
@@ -104,7 +104,7 @@ describe("EventTimelineSet", () => {
it("Adds event to the live timeline in the timeline set", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addLiveEvent(messageEvent);
eventTimelineSet.addLiveEvent(messageEvent, { addToState: false });
expect(liveTimeline.getEvents().length).toStrictEqual(1);
});
@@ -113,6 +113,7 @@ describe("EventTimelineSet", () => {
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addLiveEvent(messageEvent, {
duplicateStrategy: DuplicateStrategy.Replace,
addToState: false,
});
expect(liveTimeline.getEvents().length).toStrictEqual(1);
@@ -130,6 +131,7 @@ describe("EventTimelineSet", () => {
// replace.
eventTimelineSet.addLiveEvent(duplicateMessageEvent, {
duplicateStrategy: DuplicateStrategy.Replace,
addToState: false,
});
const eventsInLiveTimeline = liveTimeline.getEvents();
@@ -144,6 +146,7 @@ describe("EventTimelineSet", () => {
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
expect(liveTimeline.getEvents().length).toStrictEqual(1);
});
@@ -151,10 +154,17 @@ describe("EventTimelineSet", () => {
it("Make sure legacy overload passing options directly as parameters still works", () => {
const liveTimeline = eventTimelineSet.getLiveTimeline();
expect(() => {
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, true);
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
}).not.toThrow();
expect(() => {
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, true, false);
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
fromCache: false,
addToState: false,
});
}).not.toThrow();
});
@@ -167,11 +177,13 @@ describe("EventTimelineSet", () => {
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addEventToTimeline(reactionEvent, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
expect(liveTimeline.getEvents().length).toStrictEqual(0);
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
expect(liveTimeline.getEvents()).toHaveLength(1);
const [event] = liveTimeline.getEvents();
@@ -202,6 +214,7 @@ describe("EventTimelineSet", () => {
expect(() => {
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline2, {
toStartOfTimeline: true,
addToState: false,
});
}).toThrow();
});
@@ -214,6 +227,7 @@ describe("EventTimelineSet", () => {
eventTimelineSet.addEventToTimeline(threadedReplyEvent, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
expect(liveTimeline.getEvents().length).toStrictEqual(0);
});
@@ -232,6 +246,7 @@ describe("EventTimelineSet", () => {
eventTimelineSetForThread.addEventToTimeline(normalMessage, liveTimeline, {
toStartOfTimeline: true,
addToState: false,
});
expect(liveTimeline.getEvents().length).toStrictEqual(0);
});
@@ -248,6 +263,7 @@ describe("EventTimelineSet", () => {
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(0);
nonRoomEventTimelineSet.addEventToTimeline(messageEvent, nonRoomEventTimeline, {
toStartOfTimeline: true,
addToState: false,
});
expect(nonRoomEventTimeline.getEvents().length).toStrictEqual(1);
});
@@ -257,7 +273,7 @@ describe("EventTimelineSet", () => {
describe("aggregateRelations", () => {
describe("with unencrypted events", () => {
beforeEach(() => {
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, eventTimeline, "foo");
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo");
});
itShouldReturnTheRelatedEvents();
@@ -279,7 +295,7 @@ describe("EventTimelineSet", () => {
replyEventShouldAttemptDecryptionSpy.mockReturnValue(true);
replyEventIsDecryptionFailureSpy = jest.spyOn(messageEvent, "isDecryptionFailure");
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, eventTimeline, "foo");
eventTimelineSet.addEventsToTimeline([messageEvent, replyEvent], true, false, eventTimeline, "foo");
});
it("should not return the related events", () => {
+27 -23
View File
@@ -98,7 +98,7 @@ describe("EventTimeline", function () {
expect(function () {
timeline.initialiseState(state);
}).not.toThrow();
timeline.addEvent(event, { toStartOfTimeline: false });
timeline.addEvent(event, { toStartOfTimeline: false, addToState: false });
expect(function () {
timeline.initialiseState(state);
}).toThrow();
@@ -182,9 +182,9 @@ describe("EventTimeline", function () {
];
it("should be able to add events to the end", function () {
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: false });
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: false });
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getEvents()[0]).toEqual(events[0]);
@@ -192,9 +192,9 @@ describe("EventTimeline", function () {
});
it("should be able to add events to the start", function () {
timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: false });
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: true });
timeline.addEvent(events[1], { toStartOfTimeline: true, addToState: false });
expect(timeline.getBaseIndex()).toEqual(initialIndex + 1);
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getEvents()[0]).toEqual(events[1]);
@@ -238,9 +238,9 @@ describe("EventTimeline", function () {
content: { name: "Old Room Name" },
});
timeline.addEvent(newEv, { toStartOfTimeline: false });
timeline.addEvent(newEv, { toStartOfTimeline: false, addToState: false });
expect(newEv.sender).toEqual(sentinel);
timeline.addEvent(oldEv, { toStartOfTimeline: true });
timeline.addEvent(oldEv, { toStartOfTimeline: true, addToState: false });
expect(oldEv.sender).toEqual(oldSentinel);
});
@@ -280,9 +280,9 @@ describe("EventTimeline", function () {
skey: userA,
event: true,
});
timeline.addEvent(newEv, { toStartOfTimeline: false });
timeline.addEvent(newEv, { toStartOfTimeline: false, addToState: false });
expect(newEv.target).toEqual(sentinel);
timeline.addEvent(oldEv, { toStartOfTimeline: true });
timeline.addEvent(oldEv, { toStartOfTimeline: true, addToState: false });
expect(oldEv.target).toEqual(oldSentinel);
});
@@ -308,8 +308,8 @@ describe("EventTimeline", function () {
}),
];
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false });
timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: true });
timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: true });
expect(timeline.getState(EventTimeline.FORWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], {
timelineWasEmpty: undefined,
@@ -347,8 +347,8 @@ describe("EventTimeline", function () {
}),
];
timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.addEvent(events[1], { toStartOfTimeline: true });
timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: true });
timeline.addEvent(events[1], { toStartOfTimeline: true, addToState: true });
expect(timeline.getState(EventTimeline.BACKWARDS)!.setStateEvents).toHaveBeenCalledWith([events[0]], {
timelineWasEmpty: undefined,
@@ -365,11 +365,15 @@ describe("EventTimeline", function () {
);
it("Make sure legacy overload passing options directly as parameters still works", () => {
expect(() => timeline.addEvent(events[0], { toStartOfTimeline: true })).not.toThrow();
expect(() => timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: false })).not.toThrow();
// @ts-ignore stateContext is not a valid param
expect(() => timeline.addEvent(events[0], { stateContext: new RoomState(roomId) })).not.toThrow();
expect(() =>
timeline.addEvent(events[0], { toStartOfTimeline: false, roomState: new RoomState(roomId) }),
timeline.addEvent(events[0], {
toStartOfTimeline: false,
addToState: false,
roomState: new RoomState(roomId),
}),
).not.toThrow();
});
});
@@ -397,8 +401,8 @@ describe("EventTimeline", function () {
];
it("should remove events", function () {
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false });
timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: false });
timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: false });
expect(timeline.getEvents().length).toEqual(2);
let ev = timeline.removeEvent(events[0].getId()!);
@@ -411,9 +415,9 @@ describe("EventTimeline", function () {
});
it("should update baseIndex", function () {
timeline.addEvent(events[0], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: true });
timeline.addEvent(events[2], { toStartOfTimeline: false });
timeline.addEvent(events[0], { toStartOfTimeline: false, addToState: false });
timeline.addEvent(events[1], { toStartOfTimeline: true, addToState: false });
timeline.addEvent(events[2], { toStartOfTimeline: false, addToState: false });
expect(timeline.getEvents().length).toEqual(3);
expect(timeline.getBaseIndex()).toEqual(1);
@@ -430,11 +434,11 @@ describe("EventTimeline", function () {
// - removing the last event got baseIndex into such a state that
// further addEvent(ev, false) calls made the index increase.
it("should not make baseIndex assplode when removing the last event", function () {
timeline.addEvent(events[0], { toStartOfTimeline: true });
timeline.addEvent(events[0], { toStartOfTimeline: true, addToState: false });
timeline.removeEvent(events[0].getId()!);
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], { toStartOfTimeline: false });
timeline.addEvent(events[2], { toStartOfTimeline: false });
timeline.addEvent(events[1], { toStartOfTimeline: false, addToState: false });
timeline.addEvent(events[2], { toStartOfTimeline: false, addToState: false });
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
});
+34 -24
View File
@@ -94,6 +94,12 @@ function convertQueryDictToMap(queryDict?: QueryDict): Map<string, string> {
return new Map(Object.entries(queryDict).map(([k, v]) => [k, String(v)]));
}
declare module "../../src/@types/event" {
interface AccountDataEvents {
"im.vector.test": {};
}
}
type HttpLookup = {
method: string;
path: string;
@@ -2799,24 +2805,28 @@ describe("MatrixClient", function () {
roomCreateEvent(room1.roomId, replacedByCreate1.roomId),
predecessorEvent(room1.roomId, replacedByDynamicPredecessor1.roomId),
],
{},
{ addToState: true },
);
room2.addLiveEvents(
[
roomCreateEvent(room2.roomId, replacedByCreate2.roomId),
predecessorEvent(room2.roomId, replacedByDynamicPredecessor2.roomId),
],
{},
{ addToState: true },
);
replacedByCreate1.addLiveEvents([tombstoneEvent(room1.roomId, replacedByCreate1.roomId)], {});
replacedByCreate2.addLiveEvents([tombstoneEvent(room2.roomId, replacedByCreate2.roomId)], {});
replacedByCreate1.addLiveEvents([tombstoneEvent(room1.roomId, replacedByCreate1.roomId)], {
addToState: true,
});
replacedByCreate2.addLiveEvents([tombstoneEvent(room2.roomId, replacedByCreate2.roomId)], {
addToState: true,
});
replacedByDynamicPredecessor1.addLiveEvents(
[tombstoneEvent(room1.roomId, replacedByDynamicPredecessor1.roomId)],
{},
{ addToState: true },
);
replacedByDynamicPredecessor2.addLiveEvents(
[tombstoneEvent(room2.roomId, replacedByDynamicPredecessor2.roomId)],
{},
{ addToState: true },
);
return {
@@ -2854,10 +2864,10 @@ describe("MatrixClient", function () {
const room2 = new Room("room2", client, "@daryl:alexandria.example.com");
client.store = new StubStore();
client.store.getRooms = () => [room1, replacedRoom1, replacedRoom2, room2];
room1.addLiveEvents([roomCreateEvent(room1.roomId, replacedRoom1.roomId)], {});
room2.addLiveEvents([roomCreateEvent(room2.roomId, replacedRoom2.roomId)], {});
replacedRoom1.addLiveEvents([tombstoneEvent(room1.roomId, replacedRoom1.roomId)], {});
replacedRoom2.addLiveEvents([tombstoneEvent(room2.roomId, replacedRoom2.roomId)], {});
room1.addLiveEvents([roomCreateEvent(room1.roomId, replacedRoom1.roomId)], { addToState: true });
room2.addLiveEvents([roomCreateEvent(room2.roomId, replacedRoom2.roomId)], { addToState: true });
replacedRoom1.addLiveEvents([tombstoneEvent(room1.roomId, replacedRoom1.roomId)], { addToState: true });
replacedRoom2.addLiveEvents([tombstoneEvent(room2.roomId, replacedRoom2.roomId)], { addToState: true });
// When we ask for the visible rooms
const rooms = client.getVisibleRooms();
@@ -2937,15 +2947,15 @@ describe("MatrixClient", function () {
const room4 = new Room("room4", client, "@michonne:hawthorne.example.com");
if (creates) {
room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)]);
room3.addLiveEvents([roomCreateEvent(room3.roomId, room2.roomId)]);
room4.addLiveEvents([roomCreateEvent(room4.roomId, room3.roomId)]);
room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)], { addToState: true });
room3.addLiveEvents([roomCreateEvent(room3.roomId, room2.roomId)], { addToState: true });
room4.addLiveEvents([roomCreateEvent(room4.roomId, room3.roomId)], { addToState: true });
}
if (tombstones) {
room1.addLiveEvents([tombstoneEvent(room2.roomId, room1.roomId)], {});
room2.addLiveEvents([tombstoneEvent(room3.roomId, room2.roomId)], {});
room3.addLiveEvents([tombstoneEvent(room4.roomId, room3.roomId)], {});
room1.addLiveEvents([tombstoneEvent(room2.roomId, room1.roomId)], { addToState: true });
room2.addLiveEvents([tombstoneEvent(room3.roomId, room2.roomId)], { addToState: true });
room3.addLiveEvents([tombstoneEvent(room4.roomId, room3.roomId)], { addToState: true });
}
mocked(store.getRoom).mockImplementation((roomId: string) => {
@@ -2980,17 +2990,17 @@ describe("MatrixClient", function () {
const dynRoom4 = new Room("dynRoom4", client, "@rick:grimes.example.com");
const dynRoom5 = new Room("dynRoom5", client, "@rick:grimes.example.com");
dynRoom1.addLiveEvents([tombstoneEvent(dynRoom2.roomId, dynRoom1.roomId)], {});
dynRoom2.addLiveEvents([predecessorEvent(dynRoom2.roomId, dynRoom1.roomId)]);
dynRoom1.addLiveEvents([tombstoneEvent(dynRoom2.roomId, dynRoom1.roomId)], { addToState: true });
dynRoom2.addLiveEvents([predecessorEvent(dynRoom2.roomId, dynRoom1.roomId)], { addToState: true });
dynRoom2.addLiveEvents([tombstoneEvent(room3.roomId, dynRoom2.roomId)], {});
room3.addLiveEvents([predecessorEvent(room3.roomId, dynRoom2.roomId)]);
dynRoom2.addLiveEvents([tombstoneEvent(room3.roomId, dynRoom2.roomId)], { addToState: true });
room3.addLiveEvents([predecessorEvent(room3.roomId, dynRoom2.roomId)], { addToState: true });
room3.addLiveEvents([tombstoneEvent(dynRoom4.roomId, room3.roomId)], {});
dynRoom4.addLiveEvents([predecessorEvent(dynRoom4.roomId, room3.roomId)]);
room3.addLiveEvents([tombstoneEvent(dynRoom4.roomId, room3.roomId)], { addToState: true });
dynRoom4.addLiveEvents([predecessorEvent(dynRoom4.roomId, room3.roomId)], { addToState: true });
dynRoom4.addLiveEvents([tombstoneEvent(dynRoom5.roomId, dynRoom4.roomId)], {});
dynRoom5.addLiveEvents([predecessorEvent(dynRoom5.roomId, dynRoom4.roomId)]);
dynRoom4.addLiveEvents([tombstoneEvent(dynRoom5.roomId, dynRoom4.roomId)], { addToState: true });
dynRoom5.addLiveEvents([predecessorEvent(dynRoom5.roomId, dynRoom4.roomId)], { addToState: true });
mocked(store.getRoom)
.mockClear()
+19 -112
View File
@@ -15,7 +15,8 @@ limitations under the License.
*/
import { MatrixEvent } from "../../../src";
import { CallMembership, CallMembershipDataLegacy, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
import { CallMembership, SessionMembershipData, DEFAULT_EXPIRE_DURATION } from "../../../src/matrixrtc/CallMembership";
import { membershipTemplate } from "./mocks";
function makeMockEvent(originTs = 0): MatrixEvent {
return {
@@ -25,91 +26,15 @@ function makeMockEvent(originTs = 0): MatrixEvent {
}
describe("CallMembership", () => {
describe("CallMembershipDataLegacy", () => {
const membershipTemplate: CallMembershipDataLegacy = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA",
expires: 5000,
membershipID: "bloop",
foci_active: [{ type: "livekit" }],
};
it("rejects membership with no expiry and no expires_ts", () => {
expect(() => {
new CallMembership(
makeMockEvent(),
Object.assign({}, membershipTemplate, { expires: undefined, expires_ts: undefined }),
);
}).toThrow();
});
it("rejects membership with no device_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined }));
}).toThrow();
});
it("rejects membership with no call_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined }));
}).toThrow();
});
it("allow membership with no scope", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
}).not.toThrow();
});
it("rejects with malformatted expires_ts", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires_ts: "string" }));
}).toThrow();
});
it("rejects with malformatted expires", () => {
expect(() => {
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires: "string" }));
}).toThrow();
});
it("uses event timestamp if no created_ts", () => {
const membership = new CallMembership(makeMockEvent(12345), membershipTemplate);
expect(membership.createdTs()).toEqual(12345);
});
it("uses created_ts if present", () => {
const membership = new CallMembership(
makeMockEvent(12345),
Object.assign({}, membershipTemplate, { created_ts: 67890 }),
);
expect(membership.createdTs()).toEqual(67890);
});
it("computes absolute expiry time based on expires", () => {
const membership = new CallMembership(makeMockEvent(1000), membershipTemplate);
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
});
it("computes absolute expiry time based on expires_ts", () => {
const membership = new CallMembership(
makeMockEvent(1000),
Object.assign({}, membershipTemplate, { expires_ts: 6000 }),
);
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
});
it("returns preferred foci", () => {
const fakeEvent = makeMockEvent();
const mockFocus = { type: "this_is_a_mock_focus" };
const membership = new CallMembership(
fakeEvent,
Object.assign({}, membershipTemplate, { foci_active: [mockFocus] }),
);
expect(membership.getPreferredFoci()).toEqual([mockFocus]);
});
});
describe("SessionMembershipData", () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
const membershipTemplate: SessionMembershipData = {
call_id: "",
scope: "m.room",
@@ -152,9 +77,14 @@ describe("CallMembership", () => {
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);
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1));
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(false);
});
it("considers memberships expired if local age large enough", () => {
const fakeEvent = makeMockEvent(1000);
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1));
expect(new CallMembership(fakeEvent, membershipTemplate).isExpired()).toEqual(true);
});
it("returns preferred foci", () => {
@@ -171,15 +101,6 @@ describe("CallMembership", () => {
describe("expiry calculation", () => {
let fakeEvent: MatrixEvent;
let membership: CallMembership;
const membershipTemplate: CallMembershipDataLegacy = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA",
expires: 5000,
membershipID: "bloop",
foci_active: [{ type: "livekit" }],
};
beforeEach(() => {
// server origin timestamp for this event is 1000
@@ -193,24 +114,10 @@ describe("CallMembership", () => {
jest.useRealTimers();
});
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
expect(membership.getLocalExpiry()).toEqual(7000);
});
it("calculates time until expiry", () => {
jest.setSystemTime(2000);
// should be using absolute expiry time
expect(membership.getMsUntilExpiry()).toEqual(4000);
expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000);
});
});
});
+151 -310
View File
@@ -14,30 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { encodeBase64, EventTimeline, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
import { encodeBase64, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
import { KnownMembership } from "../../../src/@types/membership";
import {
CallMembershipData,
CallMembershipDataLegacy,
SessionMembershipData,
} from "../../../src/matrixrtc/CallMembership";
import { SessionMembershipData, DEFAULT_EXPIRE_DURATION } from "../../../src/matrixrtc/CallMembership";
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
import { randomString } from "../../../src/randomstring";
import { makeMockRoom, makeMockRoomState, mockRTCEvent } from "./mocks";
const membershipTemplate: CallMembershipData = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA",
expires: 60 * 60 * 1000,
membershipID: "bloop",
foci_active: [{ type: "livekit", livekit_service_url: "https://lk.url" }],
};
import { makeMockRoom, makeMockRoomState, membershipTemplate } from "./mocks";
const mockFocus = { type: "mock" };
const textEncoder = new TextEncoder();
describe("MatrixRTCSession", () => {
let client: MatrixClient;
let sess: MatrixRTCSession | undefined;
@@ -57,7 +45,7 @@ describe("MatrixRTCSession", () => {
describe("roomSessionForRoom", () => {
it("creates a room-scoped session from room state", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
const mockRoom = makeMockRoom(membershipTemplate);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess?.memberships.length).toEqual(1);
@@ -65,7 +53,6 @@ describe("MatrixRTCSession", () => {
expect(sess?.memberships[0].scope).toEqual("m.room");
expect(sess?.memberships[0].application).toEqual("m.call");
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
expect(sess?.memberships[0].membershipID).toEqual("bloop");
expect(sess?.memberships[0].isExpired()).toEqual(false);
expect(sess?.callId).toEqual("");
});
@@ -85,7 +72,7 @@ describe("MatrixRTCSession", () => {
});
it("ignores memberships events of members not in the room", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
const mockRoom = makeMockRoom(membershipTemplate);
mockRoom.hasMembershipState = (state) => state === KnownMembership.Join;
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess?.memberships.length).toEqual(0);
@@ -179,14 +166,6 @@ describe("MatrixRTCSession", () => {
expect(sess.memberships).toHaveLength(0);
});
it("ignores memberships with no expires_ts", () => {
const expiredMembership = Object.assign({}, membershipTemplate);
(expiredMembership.expires as number | undefined) = undefined;
const mockRoom = makeMockRoom([expiredMembership]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess.memberships).toHaveLength(0);
});
it("ignores memberships with no device_id", () => {
const testMembership = Object.assign({}, membershipTemplate);
(testMembership.device_id as string | undefined) = undefined;
@@ -222,23 +201,7 @@ describe("MatrixRTCSession", () => {
describe("updateCallMembershipEvent", () => {
const mockFocus = { type: "livekit", livekit_service_url: "https://test.org" };
const joinSessionConfig = { useLegacyMemberEvents: false };
const legacyMembershipData: CallMembershipDataLegacy = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA_legacy",
expires: 60 * 60 * 1000,
membershipID: "bloop",
foci_active: [mockFocus],
};
const expiredLegacyMembershipData: CallMembershipDataLegacy = {
...legacyMembershipData,
device_id: "AAAAAAA_legacy_expired",
expires: 0,
};
const joinSessionConfig = {};
const sessionMembershipData: SessionMembershipData = {
call_id: "",
@@ -271,39 +234,22 @@ describe("MatrixRTCSession", () => {
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
});
async function testSession(
membershipData: CallMembershipData[] | SessionMembershipData,
shouldUseLegacy: boolean,
): Promise<void> {
async function testSession(membershipData: SessionMembershipData): 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);
expect(makeNewMembershipMock).toHaveBeenCalledTimes(1);
await Promise.race([sentDelayedState, new Promise((resolve) => setTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(shouldUseLegacy ? 0 : 1);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
}
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', async () => {
await testSession([sessionMembershipData], true);
});
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", async () => {
await testSession(sessionMembershipData, false);
it("sends events", async () => {
await testSession(sessionMembershipData);
});
});
@@ -324,7 +270,11 @@ describe("MatrixRTCSession", () => {
});
describe("getsActiveFocus", () => {
const activeFociConfig = { type: "livekit", livekit_service_url: "https://active.url" };
const firstPreferredFocus = {
type: "livekit",
livekit_service_url: "https://active.url",
livekit_alias: "!active:active.url",
};
it("gets the correct active focus with oldest_membership", () => {
jest.useFakeTimers();
jest.setSystemTime(3000);
@@ -332,7 +282,7 @@ describe("MatrixRTCSession", () => {
Object.assign({}, membershipTemplate, {
device_id: "foo",
created_ts: 500,
foci_active: [activeFociConfig],
foci_preferred: [firstPreferredFocus],
}),
Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }),
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
@@ -344,15 +294,15 @@ describe("MatrixRTCSession", () => {
type: "livekit",
focus_selection: "oldest_membership",
});
expect(sess.getActiveFocus()).toBe(activeFociConfig);
expect(sess.getActiveFocus()).toBe(firstPreferredFocus);
jest.useRealTimers();
});
it("does not provide focus if the selction method is unknown", () => {
it("does not provide focus if the selection method is unknown", () => {
const mockRoom = makeMockRoom([
Object.assign({}, membershipTemplate, {
device_id: "foo",
created_ts: 500,
foci_active: [activeFociConfig],
foci_preferred: [firstPreferredFocus],
}),
Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }),
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
@@ -366,25 +316,6 @@ 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",
created_ts: 500,
foci_active: [activeFociConfig],
}),
Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }),
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }]);
expect(sess.getActiveFocus()).toBe(activeFociConfig);
jest.useRealTimers();
});
});
describe("joining", () => {
@@ -446,24 +377,28 @@ describe("MatrixRTCSession", () => {
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
{
memberships: [
{
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
expires: 3600000,
expires_ts: Date.now() + 3600000,
foci_active: [mockFocus],
membershipID: expect.stringMatching(".*"),
},
],
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
expires: DEFAULT_EXPIRE_DURATION,
foci_preferred: [mockFocus],
focus_active: {
focus_selection: "oldest_membership",
type: "livekit",
},
},
"@alice:example.org",
"_@alice:example.org_AAAAAAA",
);
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(0);
// Because we actually want to send the state
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
// For checking if the delayed event is still there or got removed while sending the state.
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
// For scheduling the delayed event
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
// This returns no error so we do not check if we reschedule the event again. this is done in another test.
jest.useRealTimers();
});
@@ -476,28 +411,26 @@ describe("MatrixRTCSession", () => {
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
{
memberships: [
{
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
expires: 60000,
expires_ts: Date.now() + 60000,
foci_active: [mockFocus],
membershipID: expect.stringMatching(".*"),
},
],
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
expires: 60000,
foci_preferred: [mockFocus],
focus_active: {
focus_selection: "oldest_membership",
type: "livekit",
},
},
"@alice:example.org",
"_@alice:example.org_AAAAAAA",
);
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(0);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
jest.useRealTimers();
});
describe("non-legacy calls", () => {
describe("calls", () => {
const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" };
const activeFocus = { type: "livekit", focus_selection: "oldest_membership" };
@@ -555,7 +488,6 @@ describe("MatrixRTCSession", () => {
});
sess!.joinRoomSession([activeFocusConfig], activeFocus, {
useLegacyMemberEvents: false,
membershipServerSideExpiryTimeout: 9000,
});
@@ -577,6 +509,7 @@ describe("MatrixRTCSession", () => {
application: "m.call",
scope: "m.room",
call_id: "",
expires: 14400000,
device_id: "AAAAAAA",
foci_preferred: [activeFocusConfig],
focus_active: activeFocus,
@@ -596,7 +529,7 @@ describe("MatrixRTCSession", () => {
jest.useRealTimers();
}
it("sends a membership event with session payload when joining a non-legacy call", async () => {
it("sends a membership event with session payload when joining a call", async () => {
await testJoin(false);
});
@@ -605,91 +538,19 @@ describe("MatrixRTCSession", () => {
});
});
it("does nothing if join called when already joined", () => {
it("does nothing if join called when already joined", async () => {
sess!.joinRoomSession([mockFocus], mockFocus);
await sentStateEvent;
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
sess!.joinRoomSession([mockFocus], mockFocus);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
});
it("renews membership event before expiry time", async () => {
jest.useFakeTimers();
let resolveFn: ((_roomId: string, _type: string, val: Record<string, any>) => void) | undefined;
const eventSentPromise = new Promise<Record<string, any>>((r) => {
resolveFn = (_roomId: string, _type: string, val: Record<string, any>) => {
r(val);
};
});
try {
const sendStateEventMock = jest.fn().mockImplementation(resolveFn);
client.sendStateEvent = sendStateEventMock;
sess!.joinRoomSession([mockFocus], mockFocus);
const eventContent = await eventSentPromise;
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([
[
event.getType(),
{
size: () => true,
has: (_stateKey: string) => true,
get: (_stateKey: string) => event,
values: () => [event],
} as unknown as Map<string, MatrixEvent>,
],
]);
const eventReSentPromise = new Promise<Record<string, any>>((r) => {
resolveFn = (_roomId: string, _type: string, val: Record<string, any>) => {
r(val);
};
});
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;
expect(sendStateEventMock).toHaveBeenCalledWith(
mockRoom.roomId,
EventType.GroupCallMemberPrefix,
{
memberships: [
{
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
expires: 3600000 * 2,
expires_ts: 1000 + 3600000 * 2,
foci_active: [mockFocus],
created_ts: 1000,
membershipID: expect.stringMatching(".*"),
},
],
},
"@alice:example.org",
);
} finally {
jest.useRealTimers();
}
});
});
describe("onMembershipsChanged", () => {
it("does not emit if no membership changes", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
const mockRoom = makeMockRoom(membershipTemplate);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const onMembershipsChanged = jest.fn();
@@ -700,7 +561,7 @@ describe("MatrixRTCSession", () => {
});
it("emits on membership changes", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
const mockRoom = makeMockRoom(membershipTemplate);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const onMembershipsChanged = jest.fn();
@@ -712,26 +573,28 @@ describe("MatrixRTCSession", () => {
expect(onMembershipsChanged).toHaveBeenCalled();
});
it("emits an event at the time a membership event expires", () => {
jest.useFakeTimers();
try {
const membership = Object.assign({}, membershipTemplate);
const mockRoom = makeMockRoom([membership]);
// TODO: re-enable this test when expiry is implemented
// eslint-disable-next-line jest/no-commented-out-tests
// it("emits an event at the time a membership event expires", () => {
// jest.useFakeTimers();
// try {
// const membership = Object.assign({}, membershipTemplate);
// const mockRoom = makeMockRoom([membership]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const membershipObject = sess.memberships[0];
// sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
// const membershipObject = sess.memberships[0];
const onMembershipsChanged = jest.fn();
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
// const onMembershipsChanged = jest.fn();
// sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
jest.advanceTimersByTime(61 * 1000 * 1000);
// jest.advanceTimersByTime(61 * 1000 * 1000);
expect(onMembershipsChanged).toHaveBeenCalledWith([membershipObject], []);
expect(sess?.memberships.length).toEqual(0);
} finally {
jest.useRealTimers();
}
});
// expect(onMembershipsChanged).toHaveBeenCalledWith([membershipObject], []);
// expect(sess?.memberships.length).toEqual(0);
// } finally {
// jest.useRealTimers();
// }
// });
});
describe("key management", () => {
@@ -803,9 +666,13 @@ describe("MatrixRTCSession", () => {
}
});
it("does not send key if join called when already joined", () => {
it("does not send key if join called when already joined", async () => {
const sentStateEvent = new Promise((resolve) => {
sendStateEventMock = jest.fn(resolve);
});
client.sendStateEvent = sendStateEventMock;
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
await sentStateEvent;
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
expect(client.sendEvent).toHaveBeenCalledTimes(1);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
@@ -864,7 +731,64 @@ describe("MatrixRTCSession", () => {
expect(client.cancelPendingEvent).toHaveBeenCalledWith(eventSentinel);
});
it("Re-sends key if a new member joins", async () => {
it("re-sends key if a new member joins even if a key rotation is in progress", async () => {
jest.useFakeTimers();
try {
// session with two members
const member2 = Object.assign({}, membershipTemplate, {
device_id: "BBBBBBB",
});
const mockRoom = makeMockRoom([membershipTemplate, member2]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
// joining will trigger an initial key send
const keysSentPromise1 = new Promise<EncryptionKeysEventContent>((resolve) => {
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
});
sess.joinRoomSession([mockFocus], mockFocus, {
manageMediaKeys: true,
updateEncryptionKeyThrottle: 1000,
makeKeyDelay: 3000,
});
await keysSentPromise1;
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
// member2 leaves triggering key rotation
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId));
sess.onMembershipUpdate();
// member2 re-joins which should trigger an immediate re-send
const keysSentPromise2 = new Promise<EncryptionKeysEventContent>((resolve) => {
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
});
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
sess.onMembershipUpdate();
// but, that immediate resend is throttled so we need to wait a bit
jest.advanceTimersByTime(1000);
const { keys } = await keysSentPromise2;
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(2);
// key index should still be the original: 0
expect(keys[0].index).toEqual(0);
// check that the key rotation actually happens
const keysSentPromise3 = new Promise<EncryptionKeysEventContent>((resolve) => {
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
});
jest.advanceTimersByTime(2000);
const { keys: rotatedKeys } = await keysSentPromise3;
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(3);
// key index should now be the rotated one: 1
expect(rotatedKeys[0].index).toEqual(1);
} finally {
jest.useRealTimers();
}
});
it("re-sends key if a new member joins", async () => {
jest.useFakeTimers();
try {
const mockRoom = makeMockRoom([membershipTemplate]);
@@ -957,89 +881,6 @@ describe("MatrixRTCSession", () => {
}
});
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(".*"),
},
],
sent_ts: Date.now(),
},
);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
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(".*"),
},
],
sent_ts: Date.now(),
},
);
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(2);
} finally {
jest.useRealTimers();
}
});
it("re-sends key if a member changes created_ts", async () => {
jest.useFakeTimers();
jest.setSystemTime(1000);
@@ -1181,7 +1022,7 @@ describe("MatrixRTCSession", () => {
it("wraps key index around to 0 when it reaches the maximum", async () => {
// this should give us keys with index [0...255, 0, 1]
const membersToTest = 258;
const members: CallMembershipData[] = [];
const members: SessionMembershipData[] = [];
for (let i = 0; i < membersToTest; i++) {
members.push(Object.assign({}, membershipTemplate, { device_id: `DEVICE${i}` }));
}
@@ -1288,7 +1129,7 @@ describe("MatrixRTCSession", () => {
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
textEncoder.encode("this is the key"),
0,
"@bob:example.org:bobsphone",
);
@@ -1320,7 +1161,7 @@ describe("MatrixRTCSession", () => {
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
textEncoder.encode("this is the key"),
4,
"@bob:example.org:bobsphone",
);
@@ -1352,7 +1193,7 @@ describe("MatrixRTCSession", () => {
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
textEncoder.encode("this is the key"),
0,
"@bob:example.org:bobsphone",
);
@@ -1379,12 +1220,12 @@ describe("MatrixRTCSession", () => {
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(2);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
textEncoder.encode("this is the key"),
0,
"@bob:example.org:bobsphone",
);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("this is the key", "utf-8"),
textEncoder.encode("this is the key"),
4,
"@bob:example.org:bobsphone",
);
@@ -1432,7 +1273,7 @@ describe("MatrixRTCSession", () => {
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("newer key", "utf-8"),
textEncoder.encode("newer key"),
0,
"@bob:example.org:bobsphone",
);
@@ -1480,7 +1321,7 @@ describe("MatrixRTCSession", () => {
sess!.reemitEncryptionKeys();
expect(encryptionKeyChangedListener).toHaveBeenCalledTimes(1);
expect(encryptionKeyChangedListener).toHaveBeenCalledWith(
Buffer.from("second key", "utf-8"),
textEncoder.encode("second key"),
0,
"@bob:example.org:bobsphone",
);
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { Mock } from "jest-mock";
import {
ClientEvent,
EventTimeline,
@@ -24,19 +26,8 @@ import {
RoomEvent,
} from "../../../src";
import { RoomStateEvent } from "../../../src/models/room-state";
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
import { MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
import { makeMockRoom } from "./mocks";
const membershipTemplate: CallMembershipData = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA",
expires: 60 * 60 * 1000,
membershipID: "bloop",
foci_active: [{ type: "test" }],
};
import { makeMockRoom, makeMockRoomState, membershipTemplate } from "./mocks";
describe("MatrixRTCSessionManager", () => {
let client: MatrixClient;
@@ -69,16 +60,15 @@ describe("MatrixRTCSessionManager", () => {
it("Fires event when session ends", () => {
const onEnded = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
const memberships = [membershipTemplate];
const room1 = makeMockRoom(memberships);
const room1 = makeMockRoom(membershipTemplate);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);
client.emit(ClientEvent.Room, room1);
memberships.splice(0, 1);
(room1.getLiveTimeline as Mock).mockReturnValue({
getState: jest.fn().mockReturnValue(makeMockRoomState([{}], room1.roomId)),
});
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("")[0];
+49 -24
View File
@@ -15,16 +15,36 @@ limitations under the License.
*/
import { EventType, MatrixEvent, Room } from "../../../src";
import { CallMembershipData, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
import { SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
import { randomString } from "../../../src/randomstring";
type MembershipData = CallMembershipData[] | SessionMembershipData;
type MembershipData = SessionMembershipData[] | SessionMembershipData | {};
export const membershipTemplate: SessionMembershipData = {
application: "m.call",
call_id: "",
device_id: "AAAAAAA",
scope: "m.room",
focus_active: { type: "livekit", livekit_service_url: "https://lk.url" },
foci_preferred: [
{
livekit_alias: "!alias:something.org",
livekit_service_url: "https://livekit-jwt.something.io",
type: "livekit",
},
{
livekit_alias: "!alias:something.org",
livekit_service_url: "https://livekit-jwt.something.dev",
type: "livekit",
},
],
};
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);
return {
const room = {
roomId: roomId,
hasMembershipState: jest.fn().mockReturnValue(true),
getLiveTimeline: jest.fn().mockReturnValue({
@@ -32,41 +52,46 @@ export function makeMockRoom(membershipData: MembershipData): Room {
}),
getVersion: jest.fn().mockReturnValue("default"),
} as unknown as Room;
return room;
}
export function makeMockRoomState(membershipData: MembershipData, roomId: string) {
const event = mockRTCEvent(membershipData, roomId);
const events = Array.isArray(membershipData)
? membershipData.map((m) => mockRTCEvent(m, roomId))
: [mockRTCEvent(membershipData, roomId)];
const keysAndEvents = events.map((e) => {
const data = e.getContent() as SessionMembershipData;
return [`_${e.sender?.userId}_${data.device_id}`];
});
return {
on: jest.fn(),
off: jest.fn(),
getStateEvents: (_: string, stateKey: string) => {
if (stateKey !== undefined) return event;
return [event];
if (stateKey !== undefined) return keysAndEvents.find(([k]) => k === stateKey)?.[1];
return events;
},
events: new Map([
[
event.getType(),
{
size: () => true,
has: (_stateKey: string) => true,
get: (_stateKey: string) => event,
values: () => [event],
},
],
]),
events:
events.length === 0
? new Map()
: new Map([
[
EventType.GroupCallMemberPrefix,
{
size: () => true,
has: (stateKey: string) => keysAndEvents.find(([k]) => k === stateKey),
get: (stateKey: string) => keysAndEvents.find(([k]) => k === stateKey)?.[1],
values: () => events,
},
],
]),
};
}
export function mockRTCEvent(membershipData: MembershipData, roomId: string): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue(
!Array.isArray(membershipData)
? membershipData
: {
memberships: membershipData,
},
),
getContent: jest.fn().mockReturnValue(membershipData),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getTs: jest.fn().mockReturnValue(Date.now()),
getRoomId: jest.fn().mockReturnValue(roomId),
+6 -6
View File
@@ -99,7 +99,7 @@ describe("MatrixEvent", () => {
const room = new Room("!roomid:e.xyz", mockClient, "myname");
const ev = createEvent("$event1:server");
await room.addLiveEvents([ev]);
await room.addLiveEvents([ev], { addToState: false });
await room.createThreadsTimelineSets();
expect(ev.threadRootId).toBeUndefined();
expect(mainTimelineLiveEventIds(room)).toEqual([ev.getId()]);
@@ -120,7 +120,7 @@ describe("MatrixEvent", () => {
const threadRoot = createEvent("$threadroot:server");
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
await room.addLiveEvents([threadRoot, ev]);
await room.addLiveEvents([threadRoot, ev], { addToState: false });
await room.createThreadsTimelineSets();
expect(threadRoot.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
@@ -143,7 +143,7 @@ describe("MatrixEvent", () => {
const threadRoot = createEvent("$threadroot:server");
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
await room.addLiveEvents([threadRoot, ev]);
await room.addLiveEvents([threadRoot, ev], { addToState: false });
await room.createThreadsTimelineSets();
expect(ev.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
@@ -167,7 +167,7 @@ describe("MatrixEvent", () => {
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
const reaction = createReactionEvent("$reaction:server", ev.getId()!);
await room.addLiveEvents([threadRoot, ev, reaction]);
await room.addLiveEvents([threadRoot, ev, reaction], { addToState: false });
await room.createThreadsTimelineSets();
expect(reaction.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
@@ -191,7 +191,7 @@ describe("MatrixEvent", () => {
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
const edit = createEditEvent("$edit:server", ev.getId()!);
await room.addLiveEvents([threadRoot, ev, edit]);
await room.addLiveEvents([threadRoot, ev, edit], { addToState: false });
await room.createThreadsTimelineSets();
expect(edit.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
@@ -217,7 +217,7 @@ describe("MatrixEvent", () => {
const reply2 = createReplyEvent("$reply2:server", reply1.getId()!);
const reaction = createReactionEvent("$reaction:server", reply2.getId()!);
await room.addLiveEvents([threadRoot, ev, reply1, reply2, reaction]);
await room.addLiveEvents([threadRoot, ev, reply1, reply2, reaction], { addToState: false });
await room.createThreadsTimelineSets();
expect(reaction.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
+22 -22
View File
@@ -36,7 +36,7 @@ describe("RoomReceipts", () => {
// Given there are no receipts in the room
const room = createRoom();
const [event] = createEvent();
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
// When I ask about any event, then it is unread
expect(room.hasUserReadEvent(readerId, event.getId()!)).toBe(false);
@@ -46,7 +46,7 @@ describe("RoomReceipts", () => {
// Given there are no receipts in the room
const room = createRoom();
const [event] = createEventSentBy(readerId);
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
// When I ask about an event I sent, it is read (because a synthetic
// receipt was created and stored in RoomReceipts)
@@ -57,7 +57,7 @@ describe("RoomReceipts", () => {
// Given my event exists and is unread
const room = createRoom();
const [event, eventId] = createEvent();
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for this event+user
@@ -72,7 +72,7 @@ describe("RoomReceipts", () => {
const room = createRoom();
const [event1, event1Id] = createEvent();
const [event2] = createEvent();
room.addLiveEvents([event1, event2]);
room.addLiveEvents([event1, event2], { addToState: false });
// When we receive a receipt for the later event
room.addReceipt(createReceipt(readerId, event2));
@@ -86,7 +86,7 @@ describe("RoomReceipts", () => {
const room = createRoom();
const [oldEvent, oldEventId] = createEvent();
const [liveEvent] = createEvent();
room.addLiveEvents([liveEvent]);
room.addLiveEvents([liveEvent], { addToState: false });
createOldTimeline(room, [oldEvent]);
// When we receive a receipt for the live event
@@ -120,7 +120,7 @@ describe("RoomReceipts", () => {
const room = createRoom();
const [event1] = createEvent();
const [event2, event2Id] = createEvent();
room.addLiveEvents([event1, event2]);
room.addLiveEvents([event1, event2], { addToState: false });
// When we receive a receipt for the earlier event
room.addReceipt(createReceipt(readerId, event1));
@@ -133,7 +133,7 @@ describe("RoomReceipts", () => {
// Given my event exists and is unread
const room = createRoom();
const [event, eventId] = createEvent();
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for another user
@@ -151,7 +151,7 @@ describe("RoomReceipts", () => {
const room = createRoom();
const [previousEvent] = createEvent();
const [myEvent] = createEventSentBy(readerId);
room.addLiveEvents([previousEvent, myEvent]);
room.addLiveEvents([previousEvent, myEvent], { addToState: false });
// And I just received a receipt for the previous event
room.addReceipt(createReceipt(readerId, previousEvent));
@@ -165,7 +165,7 @@ describe("RoomReceipts", () => {
const room = createRoom();
const [myEvent] = createEventSentBy(readerId);
const [laterEvent] = createEvent();
room.addLiveEvents([myEvent, laterEvent]);
room.addLiveEvents([myEvent, laterEvent], { addToState: false });
// When I ask about the later event, it is unread (because it's after the synthetic receipt)
expect(room.hasUserReadEvent(readerId, laterEvent.getId()!)).toBe(false);
@@ -177,7 +177,7 @@ describe("RoomReceipts", () => {
const [event1] = createEvent();
const [event2, event2Id] = createEvent();
const [event3, event3Id] = createEvent();
room.addLiveEvents([event1, event2, event3]);
room.addLiveEvents([event1, event2, event3], { addToState: false });
// When we receive receipts for the older events out of order
room.addReceipt(createReceipt(readerId, event2));
@@ -192,7 +192,7 @@ describe("RoomReceipts", () => {
// Given my event exists and is unread
const room = createRoom();
const [event, eventId] = createEvent();
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for this event+user
@@ -208,7 +208,7 @@ describe("RoomReceipts", () => {
const [root, rootId] = createEvent();
const [event, eventId] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event]);
room.addLiveEvents([root, event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for this event on this thread
@@ -225,7 +225,7 @@ describe("RoomReceipts", () => {
const [event1, event1Id] = createThreadedEvent(root);
const [event2] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event1, event2]);
room.addLiveEvents([root, event1, event2], { addToState: false });
// When we receive a receipt for the later event
room.addReceipt(createThreadedReceipt(readerId, event2, rootId));
@@ -241,7 +241,7 @@ describe("RoomReceipts", () => {
const [event1] = createThreadedEvent(root);
const [event2, event2Id] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event1, event2]);
room.addLiveEvents([root, event1, event2], { addToState: false });
// When we receive a receipt for the earlier event
room.addReceipt(createThreadedReceipt(readerId, event1, rootId));
@@ -256,7 +256,7 @@ describe("RoomReceipts", () => {
const [root, rootId] = createEvent();
const [event, eventId] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event]);
room.addLiveEvents([root, event], { addToState: false });
expect(room.hasUserReadEvent(readerId, eventId)).toBe(false);
// When we receive a receipt for another user
@@ -278,7 +278,7 @@ describe("RoomReceipts", () => {
const [thread2] = createThreadedEvent(root2);
setupThread(room, root1);
setupThread(room, root2);
room.addLiveEvents([root1, root2, thread1, thread2]);
room.addLiveEvents([root1, root2, thread1, thread2], { addToState: false });
// When we receive a receipt for the later event
room.addReceipt(createThreadedReceipt(readerId, thread2, root2.getId()!));
@@ -295,7 +295,7 @@ describe("RoomReceipts", () => {
const [event2, event2Id] = createThreadedEvent(root);
const [event3, event3Id] = createThreadedEvent(root);
setupThread(room, root);
room.addLiveEvents([root, event1, event2, event3]);
room.addLiveEvents([root, event1, event2, event3], { addToState: false });
// When we receive receipts for the older events out of order
room.addReceipt(createThreadedReceipt(readerId, event2, rootId));
@@ -329,7 +329,7 @@ describe("RoomReceipts", () => {
const [thread2b, thread2bId] = createThreadedEvent(main2);
setupThread(room, main1);
setupThread(room, main2);
room.addLiveEvents([main1, thread1a, thread1b, main2, thread2a, main3, thread2b]);
room.addLiveEvents([main1, thread1a, thread1b, main2, thread2a, main3, thread2b], { addToState: false });
// And the timestamps on the events are consistent with the order above
main1.event.origin_server_ts = 1;
@@ -377,7 +377,7 @@ describe("RoomReceipts", () => {
// Add the event to the room
// The receipt is removed from the dangling state
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
// Then the event is read
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
@@ -398,7 +398,7 @@ describe("RoomReceipts", () => {
// Add the events to the room
// The receipt is removed from the dangling state
room.addLiveEvents([root, event]);
room.addLiveEvents([root, event], { addToState: false });
// Then the event is read
expect(room.hasUserReadEvent(readerId, eventId)).toBe(true);
@@ -418,7 +418,7 @@ describe("RoomReceipts", () => {
// Add the event to the room
// The two receipts should be processed
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
// Then the event is read
// We expect that the receipt of `otherUserId` didn't replace/erase the receipt of `readerId`
@@ -528,7 +528,7 @@ function createThreadedReceipt(userId: string, referencedEvent: MatrixEvent, thr
*/
function createOldTimeline(room: Room, events: MatrixEvent[]) {
const oldTimeline = room.getUnfilteredTimelineSet().addTimeline();
room.getUnfilteredTimelineSet().addEventsToTimeline(events, true, oldTimeline);
room.getUnfilteredTimelineSet().addEventsToTimeline(events, true, false, oldTimeline);
}
/**
+1 -1
View File
@@ -801,7 +801,7 @@ async function createThread(client: MatrixClient, user: string, roomId: string):
// Ensure the root is in the room timeline
root.setThreadId(root.getId());
await room.addLiveEvents([root]);
await room.addLiveEvents([root], { addToState: false });
// Create the thread and wait for it to be initialised
const thread = room.createThread(root.getId()!, root, [], false);
+1 -1
View File
@@ -106,7 +106,7 @@ describe("fixNotificationCountOnDecryption", () => {
mockClient,
);
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: false });
THREAD_ID = event.getId()!;
threadEvent = mkEvent({
+17
View File
@@ -170,6 +170,23 @@ describe("validateIdToken()", () => {
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid audience"));
});
it("should not throw when audience is an array that includes clientId", () => {
mocked(jwtDecode).mockReturnValue({
...validDecodedIdToken,
aud: [clientId],
});
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).not.toThrow();
});
it("should throw when audience is an array that does not include clientId", () => {
mocked(jwtDecode).mockReturnValue({
...validDecodedIdToken,
aud: [`${clientId},uiop`, "asdf"],
});
expect(() => validateIdToken(idToken, issuer, clientId, nonce)).toThrow(new Error(OidcError.InvalidIdToken));
expect(logger.error).toHaveBeenCalledWith("Invalid ID token", new Error("Invalid audience"));
});
it("should throw when nonce does not match", () => {
mocked(jwtDecode).mockReturnValue({
...validDecodedIdToken,
+4 -4
View File
@@ -198,8 +198,8 @@ describe("Relations", function () {
});
const timelineSet = new EventTimelineSet(room);
timelineSet.addLiveEvent(targetEvent);
timelineSet.addLiveEvent(relationEvent);
timelineSet.addLiveEvent(targetEvent, { addToState: false });
timelineSet.addLiveEvent(relationEvent, { addToState: false });
await relationsCreated;
}
@@ -212,8 +212,8 @@ describe("Relations", function () {
});
const timelineSet = new EventTimelineSet(room);
timelineSet.addLiveEvent(relationEvent);
timelineSet.addLiveEvent(targetEvent);
timelineSet.addLiveEvent(relationEvent, { addToState: false });
timelineSet.addLiveEvent(targetEvent, { addToState: false });
await relationsCreated;
}
-55
View File
@@ -1122,59 +1122,4 @@ 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();
});
});
});
+594 -437
View File
File diff suppressed because it is too large Load Diff
+167 -11
View File
@@ -30,6 +30,7 @@ import fetchMock from "fetch-mock-jest";
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
import { initRustCrypto } from "../../../src/rust-crypto";
import {
AccountDataEvents,
Device,
DeviceVerification,
encodeBase64,
@@ -572,15 +573,37 @@ describe("RustCrypto", () => {
});
it("emits VerificationRequestReceived on incoming m.key.verification.request", async () => {
rustCrypto = await makeTestRustCrypto(
new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
baseUrl: "http://server/",
prefix: "",
onlyData: true,
}),
testData.TEST_USER_ID,
);
fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} });
fetchMock.post("path:/_matrix/client/v3/keys/query", {
device_keys: {
[testData.TEST_USER_ID]: {
[testData.TEST_DEVICE_ID]: testData.SIGNED_TEST_DEVICE_DATA,
},
},
});
// wait until we know about the other device
rustCrypto.onSyncCompleted({});
await rustCrypto.getUserDeviceInfo([testData.TEST_USER_ID]);
const toDeviceEvent = {
type: "m.key.verification.request",
content: {
from_device: "testDeviceId",
from_device: testData.TEST_DEVICE_ID,
methods: ["m.sas.v1"],
transaction_id: "testTxn",
timestamp: Date.now() - 1000,
},
sender: "@user:id",
sender: testData.TEST_USER_ID,
};
const onEvent = jest.fn();
@@ -705,6 +728,119 @@ describe("RustCrypto", () => {
expect(resetKeyBackup.mock.calls).toHaveLength(2);
});
describe("upload existing key backup key to new 4S store", () => {
const secretStorageCallbacks = {
getSecretStorageKey: async (keys: any, name: string) => {
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
},
} as SecretStorageCallbacks;
let secretStorage: ServerSideSecretStorageImpl;
let backupAuthData: any;
let backupAlg: string;
const fetchMock = {
authedRequest: jest.fn().mockImplementation((method, path, query, body) => {
if (path === "/room_keys/version") {
if (method === "POST") {
backupAuthData = body["auth_data"];
backupAlg = body["algorithm"];
return Promise.resolve({ version: "1", algorithm: backupAlg, auth_data: backupAuthData });
} else if (method === "GET" && backupAuthData) {
return Promise.resolve({ version: "1", algorithm: backupAlg, auth_data: backupAuthData });
}
}
return Promise.resolve({});
}),
};
beforeEach(() => {
backupAuthData = undefined;
backupAlg = "";
secretStorage = new ServerSideSecretStorageImpl(new DummyAccountDataClient(), secretStorageCallbacks);
});
it("bootstrapSecretStorage saves megolm backup key if already cached", async () => {
const rustCrypto = await makeTestRustCrypto(
fetchMock as unknown as MatrixHttpApi<any>,
testData.TEST_USER_ID,
undefined,
secretStorage,
);
async function createSecretStorageKey() {
return {
keyInfo: {} as AddSecretStorageKeyOpts,
privateKey: new Uint8Array(32),
};
}
await rustCrypto.resetKeyBackup();
const storeSpy = jest.spyOn(secretStorage, "store");
await rustCrypto.bootstrapSecretStorage({
createSecretStorageKey,
setupNewSecretStorage: true,
setupNewKeyBackup: false,
});
expect(storeSpy).toHaveBeenCalledWith("m.megolm_backup.v1", expect.anything());
});
it("bootstrapSecretStorage doesn't try to save megolm backup key not in cache", async () => {
const mockOlmMachine = {
isBackupEnabled: jest.fn().mockResolvedValue(false),
sign: jest.fn().mockResolvedValue({
asJSON: jest.fn().mockReturnValue("{}"),
}),
saveBackupDecryptionKey: jest.fn(),
crossSigningStatus: jest.fn().mockResolvedValue({
hasMaster: true,
hasSelfSigning: true,
hasUserSigning: true,
}),
exportCrossSigningKeys: jest.fn().mockResolvedValue({
masterKey: "sosecret",
userSigningKey: "secrets",
self_signing_key: "ssshhh",
}),
getBackupKeys: jest.fn().mockResolvedValue({}),
verifyBackup: jest.fn().mockResolvedValue({ trusted: jest.fn().mockReturnValue(false) }),
} as unknown as OlmMachine;
const rustCrypto = new RustCrypto(
logger,
mockOlmMachine,
fetchMock as unknown as MatrixHttpApi<any>,
TEST_USER,
TEST_DEVICE_ID,
secretStorage,
{} as CryptoCallbacks,
);
async function createSecretStorageKey() {
return {
keyInfo: {} as AddSecretStorageKeyOpts,
privateKey: new Uint8Array(32),
};
}
await rustCrypto.resetKeyBackup();
const storeSpy = jest.spyOn(secretStorage, "store");
await rustCrypto.bootstrapSecretStorage({
createSecretStorageKey,
setupNewSecretStorage: true,
setupNewKeyBackup: false,
});
expect(storeSpy).not.toHaveBeenCalledWith("m.megolm_backup.v1", expect.anything());
});
});
it("isSecretStorageReady", async () => {
const mockSecretStorage = {
getDefaultKeyId: jest.fn().mockResolvedValue(null),
@@ -991,23 +1127,41 @@ describe("RustCrypto", () => {
});
it.each([
[undefined, null],
["Encrypted by an unverified user.", EventShieldReason.UNVERIFIED_IDENTITY],
["Encrypted by a device not verified by its owner.", EventShieldReason.UNSIGNED_DEVICE],
[undefined, undefined, null],
[
"Encrypted by an unverified user.",
RustSdkCryptoJs.ShieldStateCode.UnverifiedIdentity,
EventShieldReason.UNVERIFIED_IDENTITY,
],
[
"Encrypted by a device not verified by its owner.",
RustSdkCryptoJs.ShieldStateCode.UnsignedDevice,
EventShieldReason.UNSIGNED_DEVICE,
],
[
"The authenticity of this encrypted message can't be guaranteed on this device.",
RustSdkCryptoJs.ShieldStateCode.AuthenticityNotGuaranteed,
EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
],
["Encrypted by an unknown or deleted device.", EventShieldReason.UNKNOWN_DEVICE],
["bloop", EventShieldReason.UNKNOWN],
])("gets the right shield reason (%s)", async (rustReason, expectedReason) => {
[
"Encrypted by an unknown or deleted device.",
RustSdkCryptoJs.ShieldStateCode.UnknownDevice,
EventShieldReason.UNKNOWN_DEVICE,
],
["Not encrypted.", RustSdkCryptoJs.ShieldStateCode.SentInClear, EventShieldReason.SENT_IN_CLEAR],
[
"Encrypted by a previously-verified user who is no longer verified.",
RustSdkCryptoJs.ShieldStateCode.VerificationViolation,
EventShieldReason.VERIFICATION_VIOLATION,
],
])("gets the right shield reason (%s)", async (rustReason, rustCode, expectedReason) => {
// suppress the warning from the unknown shield reason
jest.spyOn(console, "warn").mockImplementation(() => {});
const mockEncryptionInfo = {
shieldState: jest
.fn()
.mockReturnValue({ color: RustSdkCryptoJs.ShieldColor.None, message: rustReason }),
.mockReturnValue({ color: RustSdkCryptoJs.ShieldColor.None, code: rustCode, message: rustReason }),
} as unknown as RustSdkCryptoJs.EncryptionInfo;
olmMachine.getRoomEventEncryptionInfo.mockResolvedValue(mockEncryptionInfo);
@@ -1771,11 +1925,13 @@ class DummyAccountDataClient
super();
}
public async getAccountDataFromServer<T extends Record<string, any>>(eventType: string): Promise<T | null> {
public async getAccountDataFromServer<K extends keyof AccountDataEvents>(
eventType: K,
): Promise<AccountDataEvents[K] | null> {
const ret = this.storage.get(eventType);
if (eventType) {
return ret as T;
return ret;
} else {
return null;
}
@@ -19,6 +19,18 @@ import {
secretStorageContainsCrossSigningKeys,
} from "../../../src/rust-crypto/secret-storage";
import { ServerSideSecretStorage } from "../../../src/secret-storage";
import { SecretInfo } from "../../../src/secret-storage.ts";
declare module "../../../src/@types/event" {
interface SecretStorageAccountDataEvents {
secretA: SecretInfo;
secretB: SecretInfo;
secretC: SecretInfo;
secretD: SecretInfo;
secretE: SecretInfo;
Unknown: SecretInfo;
}
}
describe("secret-storage", () => {
describe("secretStorageContainsCrossSigningKeys", () => {
+21 -7
View File
@@ -27,6 +27,14 @@ import {
trimTrailingEquals,
} from "../../src/secret-storage";
import { randomString } from "../../src/randomstring";
import { SecretInfo } from "../../src/secret-storage.ts";
import { AccountDataEvents } from "../../src";
declare module "../../src/@types/event" {
interface SecretStorageAccountDataEvents {
mysecret: SecretInfo;
}
}
describe("ServerSideSecretStorageImpl", function () {
describe(".addKey", function () {
@@ -117,9 +125,11 @@ describe("ServerSideSecretStorageImpl", function () {
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
const storedKey = { iv: "iv", mac: "mac" } as SecretStorageKeyDescriptionAesV1;
async function mockGetAccountData<T extends Record<string, any>>(eventType: string): Promise<T | null> {
async function mockGetAccountData<K extends keyof AccountDataEvents>(
eventType: string,
): Promise<AccountDataEvents[K] | null> {
if (eventType === "m.secret_storage.key.my_key") {
return storedKey as unknown as T;
return storedKey as any;
} else {
throw new Error(`unexpected eventType ${eventType}`);
}
@@ -135,11 +145,13 @@ describe("ServerSideSecretStorageImpl", function () {
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
const storedKey = { iv: "iv", mac: "mac" } as SecretStorageKeyDescriptionAesV1;
async function mockGetAccountData<T extends Record<string, any>>(eventType: string): Promise<T | null> {
async function mockGetAccountData<K extends keyof AccountDataEvents>(
eventType: string,
): Promise<AccountDataEvents[K] | null> {
if (eventType === "m.secret_storage.default_key") {
return { key: "default_key_id" } as unknown as T;
return { key: "default_key_id" } as any;
} else if (eventType === "m.secret_storage.key.default_key_id") {
return storedKey as unknown as T;
return storedKey as any;
} else {
throw new Error(`unexpected eventType ${eventType}`);
}
@@ -236,9 +248,11 @@ describe("ServerSideSecretStorageImpl", function () {
// stub out getAccountData to return a key with an unknown algorithm
const storedKey = { algorithm: "badalg" } as SecretStorageKeyDescriptionCommon;
async function mockGetAccountData<T extends Record<string, any>>(eventType: string): Promise<T | null> {
async function mockGetAccountData<K extends keyof AccountDataEvents>(
eventType: string,
): Promise<AccountDataEvents[K] | null> {
if (eventType === "m.secret_storage.key.keyid") {
return storedKey as unknown as T;
return storedKey as any;
} else {
throw new Error(`unexpected eventType ${eventType}`);
}
+153 -10
View File
@@ -17,8 +17,10 @@ limitations under the License.
import { ReceiptType } from "../../src/@types/read_receipts";
import {
IJoinedRoom,
Category,
IInvitedRoom,
IInviteState,
IJoinedRoom,
IKnockedRoom,
IKnockState,
ILeftRoom,
@@ -27,7 +29,6 @@ import {
IStrippedState,
ISyncResponse,
SyncAccumulator,
IInviteState,
} from "../../src/sync-accumulator";
import { IRoomSummary } from "../../src";
import * as utils from "../test-utils/test-utils";
@@ -85,6 +86,7 @@ describe("SyncAccumulator", function () {
// technically cheating since we also cheekily pre-populate keys we
// know that the sync accumulator will pre-populate.
// It isn't 100% transitive.
const events = [member("alice", KnownMembership.Join), member("bob", KnownMembership.Join)];
const res = {
next_batch: "abc",
rooms: {
@@ -92,18 +94,17 @@ describe("SyncAccumulator", function () {
leave: {},
join: {
"!foo:bar": {
account_data: { events: [] },
ephemeral: { events: [] },
unread_notifications: {},
state: {
events: [member("alice", KnownMembership.Join), member("bob", KnownMembership.Join)],
},
summary: {
"account_data": { events: [] },
"ephemeral": { events: [] },
"unread_notifications": {},
"org.matrix.msc4222.state_after": { events },
"state": { events },
"summary": {
"m.heroes": undefined,
"m.joined_member_count": undefined,
"m.invited_member_count": undefined,
},
timeline: {
"timeline": {
events: [msg("alice", "hi")],
prev_batch: "something",
},
@@ -882,6 +883,147 @@ describe("SyncAccumulator", function () {
).not.toBeUndefined();
});
});
describe("msc4222", () => {
it("should accumulate state_after events", () => {
const initState = {
events: [member("alice", KnownMembership.Knock)],
};
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": initState,
}),
);
expect(sa.getJSON().roomsData[Category.Join]["!foo:bar"].state).toEqual(initState);
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": {
events: [
utils.mkEvent({
user: "alice",
room: "!knock:bar",
type: "m.room.name",
content: {
name: "Room 1",
},
skey: "",
}) as IStateEvent,
],
},
}),
);
expect(
sa.getJSON().roomsData[Category.Join]["!foo:bar"].state?.events.find((e) => e.type === "m.room.name")
?.content.name,
).toEqual("Room 1");
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": {
events: [
utils.mkEvent({
user: "alice",
room: "!knock:bar",
type: "m.room.name",
content: {
name: "Room 2",
},
skey: "",
}) as IStateEvent,
],
},
}),
);
expect(
sa.getJSON().roomsData[Category.Join]["!foo:bar"].state?.events.find((e) => e.type === "m.room.name")
?.content.name,
).toEqual("Room 2");
});
it("should ignore state events in timeline", () => {
const initState = {
events: [member("alice", KnownMembership.Knock)],
};
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": initState,
}),
);
expect(sa.getJSON().roomsData[Category.Join]["!foo:bar"].state).toEqual(initState);
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": {
events: [],
},
"timeline": {
events: [
utils.mkEvent({
user: "alice",
room: "!knock:bar",
type: "m.room.name",
content: {
name: "Room 1",
},
skey: "",
}) as IStateEvent,
],
prev_batch: "something",
},
}),
);
expect(
sa.getJSON().roomsData[Category.Join]["!foo:bar"].state?.events.find((e) => e.type === "m.room.name")
?.content.name,
).not.toEqual("Room 1");
});
it("should not rewind state_after to start of timeline in toJSON", () => {
const initState = {
events: [member("alice", KnownMembership.Knock)],
};
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": initState,
"timeline": {
events: initState.events,
prev_batch: null,
},
}),
);
expect(sa.getJSON().roomsData[Category.Join]["!foo:bar"].state).toEqual(initState);
const joinEvent = member("alice", KnownMembership.Join);
joinEvent.unsigned = { prev_content: initState.events[0].content, prev_sender: initState.events[0].sender };
sa.accumulate(
syncSkeleton({
"org.matrix.msc4222.state_after": {
events: [joinEvent],
},
"timeline": {
events: [joinEvent],
prev_batch: "something",
},
}),
);
const roomData = sa.getJSON().roomsData[Category.Join]["!foo:bar"];
expect(roomData.state?.events.find((e) => e.type === "m.room.member")?.content.membership).toEqual(
KnownMembership.Knock,
);
expect(
roomData["org.matrix.msc4222.state_after"]?.events.find((e) => e.type === "m.room.member")?.content
.membership,
).toEqual(KnownMembership.Join);
expect(roomData.timeline?.events.find((e) => e.type === "m.room.member")?.content.membership).toEqual(
KnownMembership.Join,
);
});
});
});
function syncSkeleton(
@@ -961,5 +1103,6 @@ function member(localpart: string, membership: Membership) {
state_key: "@" + localpart + ":localhost",
sender: "@" + localpart + ":localhost",
type: "m.room.member",
unsigned: {},
};
}
+3 -3
View File
@@ -62,7 +62,7 @@ function addEventsToTimeline(timeline: EventTimeline, numEvents: number, toStart
user: USER_ID,
event: true,
}),
{ toStartOfTimeline },
{ toStartOfTimeline, addToState: false },
);
}
}
@@ -451,8 +451,8 @@ describe("TimelineWindow", function () {
const liveEvents = createEvents(5);
const [, , e3, e4, e5] = oldEvents;
const [, e7, e8, e9, e10] = liveEvents;
room.addLiveEvents(liveEvents);
room.addEventsToTimeline(oldEvents, true, oldTimeline);
room.addLiveEvents(liveEvents, { addToState: false });
room.addEventsToTimeline(oldEvents, true, false, oldTimeline);
// And 2 windows over the timelines in this room
const oldWindow = new TimelineWindow(mockClient, timelineSet);
+14 -11
View File
@@ -1430,7 +1430,7 @@ describe("Group Call", function () {
let client: MatrixClient;
beforeEach(() => {
client = new MatrixClient({ baseUrl: "base_url" });
client = new MatrixClient({ baseUrl: "base_url", userId: "my_user_id" });
jest.spyOn(client, "sendStateEvent").mockResolvedValue({} as any);
});
@@ -1566,16 +1566,19 @@ describe("Group Call", function () {
async (roomId, eventType, content, stateKey) => {
const eventId = `$${Math.random()}`;
if (roomId === room.roomId) {
room.addLiveEvents([
new MatrixEvent({
event_id: eventId,
type: eventType,
room_id: roomId,
sender: FAKE_USER_ID_2,
content,
state_key: stateKey,
}),
]);
room.addLiveEvents(
[
new MatrixEvent({
event_id: eventId,
type: eventType,
room_id: roomId,
sender: FAKE_USER_ID_2,
content,
state_key: stateKey,
}),
],
{ addToState: true },
);
}
return { event_id: eventId };
},
+36 -9
View File
@@ -35,11 +35,7 @@ import {
SpaceChildEventContent,
SpaceParentEventContent,
} from "./state_events.ts";
import {
ExperimentalGroupCallRoomMemberState,
IGroupCallRoomMemberState,
IGroupCallRoomState,
} from "../webrtc/groupCall.ts";
import { IGroupCallRoomMemberState, IGroupCallRoomState } from "../webrtc/groupCall.ts";
import { MSC3089EventContent } from "../models/MSC3089Branch.ts";
import { M_BEACON, M_BEACON_INFO, MBeaconEventContent, MBeaconInfoEventContent } from "./beacon.ts";
import { XOR } from "./common.ts";
@@ -58,6 +54,10 @@ import {
import { EncryptionKeysEventContent, ICallNotifyContent } from "../matrixrtc/types.ts";
import { M_POLL_END, M_POLL_START, PollEndEventContent, PollStartEventContent } from "./polls.ts";
import { SessionMembershipData } from "../matrixrtc/CallMembership.ts";
import { LocalNotificationSettings } from "./local_notifications.ts";
import { IPushRules } from "./PushRules.ts";
import { SecretInfo, SecretStorageKeyDescription } from "../secret-storage.ts";
import { POLICIES_ACCOUNT_EVENT_TYPE } from "../models/invites-ignorer-types.ts";
export enum EventType {
// Room state events
@@ -357,10 +357,7 @@ export interface StateEvents {
// MSC3401
[EventType.GroupCallPrefix]: IGroupCallRoomState;
[EventType.GroupCallMemberPrefix]: XOR<
XOR<IGroupCallRoomMemberState, ExperimentalGroupCallRoomMemberState>,
XOR<SessionMembershipData, {}>
>;
[EventType.GroupCallMemberPrefix]: XOR<IGroupCallRoomMemberState, XOR<SessionMembershipData, {}>>;
// MSC3089
[UNSTABLE_MSC3089_BRANCH.name]: MSC3089EventContent;
@@ -368,3 +365,33 @@ export interface StateEvents {
// MSC3672
[M_BEACON_INFO.name]: MBeaconInfoEventContent;
}
/**
* Mapped type from event type to content type for all specified global account_data events.
*/
export interface AccountDataEvents extends SecretStorageAccountDataEvents {
[EventType.PushRules]: IPushRules;
[EventType.Direct]: { [userId: string]: string[] };
[EventType.IgnoredUserList]: { [userId: string]: {} };
"m.secret_storage.default_key": { key: string };
"m.identity_server": { base_url: string | null };
[key: `${typeof LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${string}`]: LocalNotificationSettings;
[key: `m.secret_storage.key.${string}`]: SecretStorageKeyDescription;
// Invites-ignorer events
[POLICIES_ACCOUNT_EVENT_TYPE.name]: { [key: string]: any };
[POLICIES_ACCOUNT_EVENT_TYPE.altName]: { [key: string]: any };
}
/**
* Mapped type from event type to content type for all specified global events encrypted by secret storage.
*
* See https://spec.matrix.org/v1.13/client-server-api/#msecret_storagev1aes-hmac-sha2-1
*/
export interface SecretStorageAccountDataEvents {
"m.megolm_backup.v1": SecretInfo;
"m.cross_signing.master": SecretInfo;
"m.cross_signing.self_signing": SecretInfo;
"m.cross_signing.user_signing": SecretInfo;
"org.matrix.msc3814": SecretInfo;
}
+21 -1
View File
@@ -65,6 +65,26 @@ declare global {
interface Navigator {
// We check for the webkit-prefixed getUserMedia to detect if we're
// on webkit: we should check if we still need to do this
webkitGetUserMedia: DummyInterfaceWeShouldntBeUsingThis;
webkitGetUserMedia?: DummyInterfaceWeShouldntBeUsingThis;
}
export interface Uint8ArrayToBase64Options {
alphabet?: "base64" | "base64url";
omitPadding?: boolean;
}
interface Uint8Array {
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tobase64
toBase64?(options?: Uint8ArrayToBase64Options): string;
}
export interface Uint8ArrayFromBase64Options {
alphabet?: "base64"; // Our fallback code only handles base64.
lastChunkHandling?: "loose"; // Our fallback code doesn't support other handling at this time.
}
interface Uint8ArrayConstructor {
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.frombase64
fromBase64?(base64: string, options?: Uint8ArrayFromBase64Options): Uint8Array;
}
}
+1 -1
View File
@@ -130,7 +130,7 @@ export class AutoDiscovery {
* configuration, which may include error states. Rejects on unexpected
* failure, not when verification fails.
*/
public static async fromDiscoveryConfig(wellknown: IClientWellKnown): Promise<ClientConfig> {
public static async fromDiscoveryConfig(wellknown?: IClientWellKnown): Promise<ClientConfig> {
// Step 1 is to get the config, which is provided to us here.
// We default to an error state to make the first few checks easier to
+38 -40
View File
@@ -18,30 +18,32 @@ limitations under the License.
* Base64 encoding and decoding utilities
*/
function toBase64(uint8Array: Uint8Array, options: Uint8ArrayToBase64Options): string {
if (typeof uint8Array.toBase64 === "function") {
// Currently this is only supported in Firefox,
// but we match the options in the hope in the future we can rely on it for all environments.
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.prototype.tobase64
return uint8Array.toBase64(options);
}
let base64 = btoa(uint8Array.reduce((acc, current) => acc + String.fromCharCode(current), ""));
if (options.omitPadding) {
base64 = base64.replace(/={1,2}$/, "");
}
if (options.alphabet === "base64url") {
base64 = base64.replace(/\+/g, "-").replace(/\//g, "_");
}
return base64;
}
/**
* Encode a typed array of uint8 as base64.
* @param uint8Array - The data to encode.
* @returns The base64.
*/
export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string {
// A brief note on the state of base64 encoding in Javascript.
// As of 2023, there is still no common native impl between both browsers and
// node. Older Webpack provides an impl for Buffer and there is a polyfill class
// for it. There are also plenty of pure js impls, eg. base64-js which has 2336
// dependents at current count. Using this would probably be fine although it's
// a little under-docced and run by an individual. The node impl works fine,
// the browser impl works but predates Uint8Array and so only uses strings.
// Right now, switching between native (or polyfilled) impls like this feels
// like the least bad option, but... *shrugs*.
if (typeof Buffer === "function") {
return Buffer.from(uint8Array).toString("base64");
} else if (typeof btoa === "function" && uint8Array instanceof Uint8Array) {
// ArrayBuffer is a node concept so the param should always be a Uint8Array on
// the browser. We need to check because ArrayBuffers don't have reduce.
return btoa(uint8Array.reduce((acc, current) => acc + String.fromCharCode(current), ""));
} else {
throw new Error("No base64 impl found!");
}
export function encodeBase64(uint8Array: Uint8Array): string {
return toBase64(uint8Array, { alphabet: "base64", omitPadding: false });
}
/**
@@ -49,8 +51,8 @@ export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string {
* @param uint8Array - The data to encode.
* @returns The unpadded base64.
*/
export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
return encodeBase64(uint8Array).replace(/={1,2}$/, "");
export function encodeUnpaddedBase64(uint8Array: Uint8Array): string {
return toBase64(uint8Array, { alphabet: "base64", omitPadding: true });
}
/**
@@ -58,8 +60,19 @@ export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): stri
* @param uint8Array - The data to encode.
* @returns The unpadded base64.
*/
export function encodeUnpaddedBase64Url(uint8Array: ArrayBuffer | Uint8Array): string {
return encodeUnpaddedBase64(uint8Array).replace(/\+/g, "-").replace(/\//g, "_");
export function encodeUnpaddedBase64Url(uint8Array: Uint8Array): string {
return toBase64(uint8Array, { alphabet: "base64url", omitPadding: true });
}
function fromBase64(base64: string, options: Uint8ArrayFromBase64Options): Uint8Array {
if (typeof Uint8Array.fromBase64 === "function") {
// Currently this is only supported in Firefox,
// but we match the options in the hope in the future we can rely on it for all environments.
// https://tc39.es/proposal-arraybuffer-base64/spec/#sec-uint8array.frombase64
return Uint8Array.fromBase64(base64, options);
}
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
}
/**
@@ -68,21 +81,6 @@ export function encodeUnpaddedBase64Url(uint8Array: ArrayBuffer | Uint8Array): s
* @returns The decoded data.
*/
export function decodeBase64(base64: string): Uint8Array {
// See encodeBase64 for a short treatise on base64 en/decoding in JS
if (typeof Buffer === "function") {
return Buffer.from(base64, "base64");
} else if (typeof atob === "function") {
const itFunc = function* (): Generator<number> {
const decoded = atob(
// built-in atob doesn't support base64url: convert so we support either
base64.replace(/-/g, "+").replace(/_/g, "/"),
);
for (let i = 0; i < decoded.length; ++i) {
yield decoded.charCodeAt(i);
}
};
return Uint8Array.from(itFunc());
} else {
throw new Error("No base64 impl found!");
}
// The function requires us to select an alphabet, but we don't know if base64url was used so we convert.
return fromBase64(base64.replace(/-/g, "+").replace(/_/g, "/"), { alphabet: "base64", lastChunkHandling: "loose" });
}
+53 -44
View File
@@ -46,7 +46,6 @@ import { noUnsafeEventProps, QueryDict, replaceParam, safeSet, sleep } from "./u
import { Direction, EventTimeline } from "./models/event-timeline.ts";
import { IActionsObject, PushProcessor } from "./pushprocessor.ts";
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery.ts";
import * as olmlib from "./crypto/olmlib.ts";
import { decodeBase64, encodeBase64, encodeUnpaddedBase64Url } from "./base64.ts";
import { IExportedDevice as IExportedOlmDevice } from "./crypto/OlmDevice.ts";
import { IOlmDevice } from "./crypto/algorithms/megolm.ts";
@@ -137,6 +136,7 @@ import {
UpdateDelayedEventAction,
} from "./@types/requests.ts";
import {
AccountDataEvents,
EventType,
LOCAL_NOTIFICATION_SETTINGS_PREFIX,
MSC3912_RELATION_BASED_REDACTIONS_PROP,
@@ -233,6 +233,7 @@ import {
import { DeviceInfoMap } from "./crypto/DeviceList.ts";
import {
AddSecretStorageKeyOpts,
SecretStorageKey,
SecretStorageKeyDescription,
ServerSideSecretStorage,
ServerSideSecretStorageImpl,
@@ -252,6 +253,9 @@ export type Store = IStore;
export type ResetTimelineCallback = (roomId: string) => boolean;
const SCROLLBACK_DELAY_MS = 3000;
/**
* @deprecated Not supported for Rust Cryptography.
*/
export const CRYPTO_ENABLED: boolean = isCryptoAvailable();
const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes
@@ -292,7 +296,7 @@ export interface ICreateClientOpts {
* specified, uses a default implementation (indexeddb in the browser,
* in-memory otherwise).
*
* This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initCrypto}),
* This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initLegacyCrypto}),
* but if you use the rust crypto implementation ({@link MatrixClient#initRustCrypto}) and the device
* previously used legacy crypto (so must be migrated), then this must still be provided, so that the
* data can be migrated from the legacy store.
@@ -387,7 +391,7 @@ export interface ICreateClientOpts {
*
* This must be set to the same value every time the client is initialised for the same device.
*
* This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initCrypto}),
* This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initLegacyCrypto}),
* but if you use the rust crypto implementation ({@link MatrixClient#initRustCrypto}) and the device
* previously used legacy crypto (so must be migrated), then this must still be provided, so that the
* data can be migrated from the legacy store.
@@ -1220,7 +1224,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
private readonly logger: Logger;
public reEmitter = new TypedReEmitter<EmittedEvents, ClientEventHandlerMap>(this);
public olmVersion: [number, number, number] | null = null; // populated after initCrypto
public olmVersion: [number, number, number] | null = null; // populated after initLegacyCrypto
public usingExternalCrypto = false;
private _store!: Store;
public deviceId: string | null;
@@ -1603,7 +1607,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Try to rehydrate a device if available. The client must have been
* initialized with a `cryptoCallback.getDehydrationKey` option, and this
* function must be called before initCrypto and startClient are called.
* function must be called before initLegacyCrypto and startClient are called.
*
* @returns Promise which resolves to undefined if a device could not be dehydrated, or
* to the new device ID if the dehydration was successful.
@@ -1830,10 +1834,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns MXID for the logged-in user, or null if not logged in
*/
public getUserId(): string | null {
if (this.credentials && this.credentials.userId) {
return this.credentials.userId;
}
return null;
return this.credentials?.userId ?? null;
}
/**
@@ -1855,7 +1856,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Domain of this MXID
*/
public getDomain(): string | null {
if (this.credentials && this.credentials.userId) {
if (this.credentials?.userId) {
return this.credentials.userId.replace(/^.*?:/, "");
}
return null;
@@ -1866,10 +1867,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns The user ID localpart or null.
*/
public getUserIdLocalpart(): string | null {
if (this.credentials && this.credentials.userId) {
return this.credentials.userId.split(":")[0].substring(1);
}
return null;
return this.credentials?.userId?.split(":")[0].substring(1) ?? null;
}
/**
@@ -2151,7 +2149,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @deprecated libolm is deprecated. Prefer {@link initRustCrypto}.
*/
public async initCrypto(): Promise<void> {
public async initLegacyCrypto(): Promise<void> {
if (!isCryptoAvailable()) {
throw new Error(
`End-to-end encryption not supported in this js-sdk build: did ` +
@@ -2224,7 +2222,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Initialise support for end-to-end encryption in this client, using the rust matrix-sdk-crypto.
*
* An alternative to {@link initCrypto}.
* An alternative to {@link initLegacyCrypto}.
*
* **WARNING**: the cryptography stack is not thread-safe. Having multiple `MatrixClient` instances connected to
* the same Indexed DB will cause data corruption and decryption failures. The application layer is responsible for
* ensuring that only one `MatrixClient` issue is instantiated at a time.
*
* @param args.useIndexedDB - True to use an indexeddb store, false to use an in-memory store. Defaults to 'true'.
* @param args.storageKey - A key with which to encrypt the indexeddb store. If provided, it must be exactly
@@ -2321,7 +2323,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Access the crypto API for this client.
*
* If end-to-end encryption has been enabled for this client (via {@link initCrypto} or {@link initRustCrypto}),
* If end-to-end encryption has been enabled for this client (via {@link initLegacyCrypto} or {@link initRustCrypto}),
* returns an object giving access to the crypto API. Otherwise, returns `undefined`.
*/
public getCrypto(): CryptoApi | undefined {
@@ -2430,6 +2432,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @remarks
* Fires {@link CryptoEvent#DeviceVerificationChanged}
*
* @deprecated Not supported for Rust Cryptography.
*/
public setDeviceVerified(userId: string, deviceId: string, verified = true): Promise<void> {
const prom = this.setDeviceVerification(userId, deviceId, verified, null, null);
@@ -3068,7 +3072,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#isStored}.
*/
public isSecretStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
public isSecretStored(name: SecretStorageKey): Promise<Record<string, SecretStorageKeyDescription> | null> {
return this.secretStorage.isStored(name);
}
@@ -3897,28 +3901,28 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
private async restoreKeyBackup(
privKey: ArrayLike<number>,
privKey: Uint8Array,
targetRoomId: undefined,
targetSessionId: undefined,
backupInfo: IKeyBackupInfo,
opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult>;
private async restoreKeyBackup(
privKey: ArrayLike<number>,
privKey: Uint8Array,
targetRoomId: string,
targetSessionId: undefined,
backupInfo: IKeyBackupInfo,
opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult>;
private async restoreKeyBackup(
privKey: ArrayLike<number>,
privKey: Uint8Array,
targetRoomId: string,
targetSessionId: string,
backupInfo: IKeyBackupInfo,
opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult>;
private async restoreKeyBackup(
privKey: ArrayLike<number>,
privKey: Uint8Array,
targetRoomId: string | undefined,
targetSessionId: string | undefined,
backupInfo: IKeyBackupInfo,
@@ -4234,7 +4238,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: an empty object
* @returns Rejects: with an error response.
*/
public setAccountData(eventType: EventType | string, content: IContent): Promise<{}> {
public setAccountData<K extends keyof AccountDataEvents>(
eventType: K,
content: AccountDataEvents[K] | Record<string, never>,
): Promise<{}> {
const path = utils.encodeUri("/user/$userId/account_data/$type", {
$userId: this.credentials.userId!,
$type: eventType,
@@ -4249,7 +4256,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param eventType - The event type
* @returns The contents of the given account data event
*/
public getAccountData(eventType: string): MatrixEvent | undefined {
public getAccountData<K extends keyof AccountDataEvents>(eventType: K): MatrixEvent | undefined {
return this.store.getAccountData(eventType);
}
@@ -4261,7 +4268,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns Promise which resolves: The contents of the given account data event.
* @returns Rejects: with an error response.
*/
public async getAccountDataFromServer<T extends { [k: string]: any }>(eventType: string): Promise<T | null> {
public async getAccountDataFromServer<K extends keyof AccountDataEvents>(
eventType: K,
): Promise<AccountDataEvents[K] | null> {
if (this.isInitialSyncComplete()) {
const event = this.store.getAccountData(eventType);
if (!event) {
@@ -4269,7 +4278,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
// The network version below returns just the content, so this branch
// does the same to match.
return event.getContent<T>();
return event.getContent<AccountDataEvents[K]>();
}
const path = utils.encodeUri("/user/$userId/account_data/$type", {
$userId: this.credentials.userId!,
@@ -4285,7 +4294,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
}
public async deleteAccountData(eventType: string): Promise<void> {
public async deleteAccountData(eventType: keyof AccountDataEvents): Promise<void> {
const msc3391DeleteAccountDataServerSupport = this.canSupport.get(Feature.AccountDataDeletion);
// if deletion is not supported overwrite with empty content
if (msc3391DeleteAccountDataServerSupport === ServerSupport.Unsupported) {
@@ -4308,8 +4317,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @returns The array of users that are ignored (empty if none)
*/
public getIgnoredUsers(): string[] {
const event = this.getAccountData("m.ignored_user_list");
if (!event || !event.getContent() || !event.getContent()["ignored_users"]) return [];
const event = this.getAccountData(EventType.IgnoredUserList);
if (!event?.getContent()["ignored_users"]) return [];
return Object.keys(event.getContent()["ignored_users"]);
}
@@ -4324,7 +4333,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
userIds.forEach((u) => {
content.ignored_users[u] = {};
});
return this.setAccountData("m.ignored_user_list", content);
return this.setAccountData(EventType.IgnoredUserList, content);
}
/**
@@ -5063,7 +5072,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (this.canSupport.get(Feature.RelationBasedRedactions) === ServerSupport.Unsupported) {
throw new Error(
"Server does not support relation based redactions " +
`roomId ${roomId} eventId ${eventId} txnId: ${txnId} threadId ${threadId}`,
`roomId ${roomId} eventId ${eventId} txnId: ${txnId as string} threadId ${threadId}`,
);
}
@@ -6136,7 +6145,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
room.partitionThreadedEvents(matrixEvents);
this.processAggregatedTimelineEvents(room, timelineEvents);
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
room.addEventsToTimeline(timelineEvents, true, true, room.getLiveTimeline());
this.processThreadEvents(room, threadedEvents, true);
unknownRelations.forEach((event) => room.relations.aggregateChildEvent(event));
@@ -6248,7 +6257,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
const [timelineEvents, threadedEvents, unknownRelations] = timelineSet.room.partitionThreadedEvents(events);
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
timelineSet.addEventsToTimeline(timelineEvents, true, false, timeline, res.start);
// The target event is not in a thread but process the contextual events, so we can show any threads around it.
this.processThreadEvents(timelineSet.room, threadedEvents, true);
this.processAggregatedTimelineEvents(timelineSet.room, timelineEvents);
@@ -6342,10 +6351,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
timeline.initialiseState(res.state.map(mapper));
}
timelineSet.addEventsToTimeline(events, true, timeline, resNewer.next_batch);
timelineSet.addEventsToTimeline(events, true, false, timeline, resNewer.next_batch);
if (!resOlder.next_batch) {
const originalEvent = await this.fetchRoomEvent(timelineSet.room.roomId, thread.id);
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, timeline, null);
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, false, timeline, null);
}
timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
timeline.setPaginationToken(resNewer.next_batch ?? null, Direction.Forward);
@@ -6399,10 +6408,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const timeline = timelineSet.getLiveTimeline();
timeline.getState(EventTimeline.BACKWARDS)!.setUnknownStateEvents(res.state.map(mapper));
timelineSet.addEventsToTimeline(events, true, timeline, null);
timelineSet.addEventsToTimeline(events, true, false, timeline, null);
if (!resOlder.next_batch) {
const originalEvent = await this.fetchRoomEvent(timelineSet.room.roomId, thread.id);
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, timeline, null);
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, false, timeline, null);
}
timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
timeline.setPaginationToken(null, Direction.Forward);
@@ -6665,7 +6674,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// No need to partition events for threads here, everything lives
// in the notification timeline set
const timelineSet = eventTimeline.getTimelineSet();
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
timelineSet.addEventsToTimeline(matrixEvents, backwards, false, eventTimeline, token);
this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
// if we've hit the end of the timeline, we need to stop trying to
@@ -6708,7 +6717,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const matrixEvents = res.chunk.filter(noUnsafeEventProps).map(this.getEventMapper());
const timelineSet = eventTimeline.getTimelineSet();
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
timelineSet.addEventsToTimeline(matrixEvents, backwards, false, eventTimeline, token);
this.processAggregatedTimelineEvents(room, matrixEvents);
this.processThreadRoots(room, matrixEvents, backwards);
@@ -6756,12 +6765,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const newToken = res.next_batch;
const timelineSet = eventTimeline.getTimelineSet();
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, newToken ?? null);
timelineSet.addEventsToTimeline(matrixEvents, backwards, false, eventTimeline, newToken ?? null);
if (!newToken && backwards) {
const originalEvent =
thread.rootEvent ??
mapper(await this.fetchRoomEvent(eventTimeline.getRoomId() ?? "", thread.id));
timelineSet.addEventsToTimeline([originalEvent], true, eventTimeline, null);
timelineSet.addEventsToTimeline([originalEvent], true, false, eventTimeline, null);
}
this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
@@ -6800,7 +6809,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const timelineSet = eventTimeline.getTimelineSet();
const [timelineEvents, , unknownRelations] = room.partitionThreadedEvents(matrixEvents);
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
timelineSet.addEventsToTimeline(timelineEvents, backwards, false, eventTimeline, token);
this.processAggregatedTimelineEvents(room, timelineEvents);
this.processThreadRoots(
room,
@@ -9262,7 +9271,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
deviceId: string,
notificationSettings: LocalNotificationSettings,
): Promise<{}> {
const key = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
const key = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}` as const;
return this.setAccountData(key, notificationSettings);
}
@@ -10115,7 +10124,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
type: EventType.RoomEncryption,
state_key: "",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
algorithm: "m.megolm.v1.aes-sha2",
},
},
],
+2 -1
View File
@@ -78,6 +78,7 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
* Get information about the encryption of an event
*
* @param event - event to be checked
* @deprecated Use {@link CryptoApi#getEncryptionInfoForEvent} instead
*/
getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo;
@@ -107,7 +108,7 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
* @param backupInfo - The backup information
* @param privKey - The private decryption key.
*/
getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor>;
getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: Uint8Array): Promise<BackupDecryptor>;
/**
* Import a list of room keys restored from backup
+1
View File
@@ -29,4 +29,5 @@ export type CryptoEventHandlerMap = {
[CryptoEvent.KeysChanged]: (data: {}) => void;
[CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void;
[CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void;
[CryptoEvent.LegacyCryptoStoreMigrationProgress]: (progress: number, total: number) => void;
} & RustBackupCryptoEventMap;
+96 -20
View File
@@ -20,7 +20,7 @@ import type { ToDeviceBatch, ToDevicePayload } from "../models/ToDeviceMessage.t
import { Room } from "../models/room.ts";
import { DeviceMap } from "../models/device.ts";
import { UIAuthCallback } from "../interactive-auth.ts";
import { PassphraseInfo, SecretStorageCallbacks, SecretStorageKeyDescription } from "../secret-storage.ts";
import { PassphraseInfo, SecretStorageKeyDescription } from "../secret-storage.ts";
import { VerificationRequest } from "./verification.ts";
import {
BackupTrustInfo,
@@ -268,9 +268,9 @@ export interface CryptoApi {
* - is enabled on this account and trusted by this device
* - has private keys either cached locally or stored in secret storage
*
* If this function returns false, bootstrapCrossSigning() can be used
* If this function returns false, {@link bootstrapCrossSigning()} can be used
* to fix things such that it returns true. That is to say, after
* bootstrapCrossSigning() completes successfully, this function should
* `bootstrapCrossSigning()` completes successfully, this function should
* return true.
*
* @returns True if cross-signing is ready to be used on this device
@@ -317,9 +317,9 @@ export interface CryptoApi {
* - is storing cross-signing private keys
* - is storing session backup key (if enabled)
*
* If this function returns false, bootstrapSecretStorage() can be used
* If this function returns false, {@link bootstrapSecretStorage()} can be used
* to fix things such that it returns true. That is to say, after
* bootstrapSecretStorage() completes successfully, this function should
* `bootstrapSecretStorage()` completes successfully, this function should
* return true.
*
* @returns True if secret storage is ready to be used on this device
@@ -327,17 +327,20 @@ export interface CryptoApi {
isSecretStorageReady(): Promise<boolean>;
/**
* Bootstrap the secret storage by creating a new secret storage key, add it in the secret storage and
* store the cross signing keys in the secret storage.
* Bootstrap [secret storage](https://spec.matrix.org/v1.12/client-server-api/#storage).
*
* - Generate a new key {@link GeneratedSecretStorageKey} with `createSecretStorageKey`.
* Only if `setupNewSecretStorage` is set or if there is no AES key in the secret storage
* - Store this key in the secret storage and set it as the default key.
* - Call `cryptoCallbacks.cacheSecretStorageKey` if provided.
* - Store the cross signing keys in the secret storage if
* - the cross signing is ready
* - a new key was created during the previous step
* - or the secret storage already contains the cross signing keys
* - If secret storage is not already set up, or {@link CreateSecretStorageOpts.setupNewSecretStorage} is set:
* * Calls {@link CreateSecretStorageOpts.createSecretStorageKey} to generate a new key.
* * Stores the metadata of the new key in account data and sets it as the default secret storage key.
* * Calls {@link CryptoCallbacks.cacheSecretStorageKey} if provided.
* - Stores the private cross signing keys in the secret storage if they are known, and they are not
* already stored in secret storage.
* - If {@link CreateSecretStorageOpts.setupNewKeyBackup} is set, calls {@link CryptoApi.resetKeyBackup}; otherwise,
* stores the key backup decryption key in secret storage if it is known, and it is not
* already stored in secret storage.
*
* Note that there may be multiple accesses to secret storage during the course of this call, each of which will
* result in a call to {@link CryptoCallbacks.getSecretStorageKey}.
*
* @param opts - Options object.
*/
@@ -562,9 +565,10 @@ export interface CryptoApi {
*
* If there are existing backups they will be replaced.
*
* The decryption key will be saved in Secret Storage (the {@link matrix.SecretStorage.SecretStorageCallbacks.getSecretStorageKey} Crypto
* callback will be called)
* and the backup engine will be started.
* If secret storage is set up, the new decryption key will be saved (the {@link CryptoCallbacks.getSecretStorageKey}
* callback will be called to obtain the secret storage key).
*
* The backup engine will be started using the new backup version (i.e., {@link checkKeyBackupAndEnable} is called).
*/
resetKeyBackup(): Promise<void>;
@@ -995,15 +999,77 @@ export interface CrossSigningStatus {
/**
* Crypto callbacks provided by the application
*/
export interface CryptoCallbacks extends SecretStorageCallbacks {
export interface CryptoCallbacks {
/**
* Called to retrieve a secret storage encryption key.
*
* [Server-side secret storage](https://spec.matrix.org/v1.12/client-server-api/#key-storage)
* is, as the name implies, a mechanism for storing secrets which should be shared between
* clients on the server. For example, it is typically used for storing the
* [key backup decryption key](https://spec.matrix.org/v1.12/client-server-api/#decryption-key)
* and the private [cross-signing keys](https://spec.matrix.org/v1.12/client-server-api/#cross-signing).
*
* The secret storage mechanism encrypts the secrets before uploading them to the server using a
* secret storage key. The schema supports multiple keys, but in practice only one tends to be used
* at once; this is the "default secret storage key" and may be known as the "recovery key" (or, sometimes,
* the "security key").
*
* Secret storage can be set up by calling {@link CryptoApi.bootstrapSecretStorage}. Having done so, when
* the crypto stack needs to access secret storage (for example, when setting up a new device, or to
* store newly-generated secrets), it will use this callback (`getSecretStorageKey`).
*
* Note that the secret storage key may be needed several times in quick succession: it is recommended
* that applications use a temporary cache to avoid prompting the user multiple times for the key. See
* also {@link cacheSecretStorageKey} which is called when a new key is created.
*
* The helper method {@link deriveRecoveryKeyFromPassphrase} may be useful if the secret storage key
* was derived from a passphrase.
*
* @param opts - An options object.
*
* @param name - the name of the *secret* (NB: not the encryption key) being stored or retrieved.
* When the item is stored in account data, it will have this `type`.
*
* @returns a pair [`keyId`, `privateKey`], where `keyId` is one of the keys from the `keys` parameter,
* and `privateKey` is the raw private encryption key, as appropriate for the encryption algorithm.
* (For `m.secret_storage.v1.aes-hmac-sha2`, it is the input to an HKDF as defined in the
* [specification](https://spec.matrix.org/v1.6/client-server-api/#msecret_storagev1aes-hmac-sha2).)
*
* Alternatively, if none of the keys are known, may return `null` in which case the original
* operation that requires access to a secret in secret storage may fail with an exception.
*/
getSecretStorageKey?: (
opts: {
/**
* Details of the secret storage keys required: a map from the key ID
* (excluding the `m.secret_storage.key.` prefix) to details of the key.
*
* When storing a secret, `keys` will contain exactly one entry.
*
* For secret retrieval, `keys` may contain several entries, and the application can return
* any one of the requested keys. Unless your application specifically wants to offer the
* user the ability to have more than one secret storage key active at a time, it is recommended
* to call {@link matrix.SecretStorage.ServerSideSecretStorage.getDefaultKeyId | ServerSideSecretStorage.getDefaultKeyId}
* to figure out which is the current default key, and to return `null` if the default key is not listed in `keys`.
*/
keys: Record<string, SecretStorageKeyDescription>;
},
name: string,
) => Promise<[string, Uint8Array] | null>;
/** @deprecated: unused with the Rust crypto stack. */
getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array | null>;
/** @deprecated: unused with the Rust crypto stack. */
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
/** @deprecated: unused with the Rust crypto stack. */
shouldUpgradeDeviceVerifications?: (users: Record<string, any>) => Promise<string[]>;
/**
* Called by {@link CryptoApi#bootstrapSecretStorage}
* Called by {@link CryptoApi.bootstrapSecretStorage} when a new default secret storage key is created.
*
* Applications can use this to (temporarily) cache the secret storage key, for later return by
* {@link getSecretStorageKey}.
*
* @param keyId - secret storage key id
* @param keyInfo - secret storage key info
* @param key - private key to store
@@ -1157,6 +1223,16 @@ export enum EventShieldReason {
* decryption keys.
*/
MISMATCHED_SENDER_KEY,
/**
* The event was sent unencrypted in an encrypted room.
*/
SENT_IN_CLEAR,
/**
* The sender was previously verified but changed their identity.
*/
VERIFICATION_VIOLATION,
}
/** The result of a call to {@link CryptoApi.getOwnDeviceKeys} */
+1 -1
View File
@@ -26,7 +26,7 @@ const KEY_SIZE = 32;
* @param key
*/
export function encodeRecoveryKey(key: ArrayLike<number>): string | undefined {
const buf = Buffer.alloc(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
const buf = new Uint8Array(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
+3 -3
View File
@@ -155,7 +155,7 @@ export interface VerificationRequest
* @param qrCodeData - the decoded QR code.
* @returns A verifier; call `.verify()` on it to wait for the other side to complete the verification flow.
*/
scanQRCode(qrCodeData: Uint8Array): Promise<Verifier>;
scanQRCode(qrCodeData: Uint8ClampedArray): Promise<Verifier>;
/**
* The verifier which is doing the actual verification, once the method has been established.
@@ -170,7 +170,7 @@ export interface VerificationRequest
*
* @deprecated Not supported in Rust Crypto. Use {@link VerificationRequest#generateQRCode} instead.
*/
getQRCodeBytes(): Buffer | undefined;
getQRCodeBytes(): Uint8ClampedArray | undefined;
/**
* Generate the data for a QR code allowing the other device to verify this one, if it supports it.
@@ -178,7 +178,7 @@ export interface VerificationRequest
* Only returns data once `phase` is {@link VerificationPhase.Ready} and the other party can scan a QR code;
* otherwise returns `undefined`.
*/
generateQRCode(): Promise<Buffer | undefined>;
generateQRCode(): Promise<Uint8ClampedArray | undefined>;
/**
* If this request has been cancelled, the cancellation code (e.g `m.user`) which is responsible for cancelling
+1 -1
View File
@@ -184,7 +184,7 @@ export class CrossSigningInfo {
}
}
}
for (const type of ["self_signing", "user_signing"]) {
for (const type of ["self_signing", "user_signing"] as const) {
intersect((await secretStorage.isStored(`m.cross_signing.${type}`)) || {});
}
return Object.keys(stored).length ? stored : null;
+16 -19
View File
@@ -25,6 +25,7 @@ import { IKeyBackupInfo } from "./keybackup.ts";
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
import { AccountDataClient, SecretStorageKeyDescription } from "../secret-storage.ts";
import { BootstrapCrossSigningOpts, CrossSigningKeyInfo } from "../crypto-api/index.ts";
import { AccountDataEvents } from "../@types/event.ts";
interface ICrossSigningKeys {
authUpload: BootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
@@ -106,12 +107,15 @@ export class EncryptionSetupBuilder {
if (!this.keySignatures) {
this.keySignatures = {};
}
const userSignatures = this.keySignatures[userId] || {};
const userSignatures = this.keySignatures[userId] ?? {};
this.keySignatures[userId] = userSignatures;
userSignatures[deviceId] = signature;
}
public async setAccountData(type: string, content: object): Promise<void> {
public async setAccountData<K extends keyof AccountDataEvents>(
type: K,
content: AccountDataEvents[K],
): Promise<void> {
await this.accountDataClientAdapter.setAccountData(type, content);
}
@@ -160,7 +164,7 @@ export class EncryptionSetupOperation {
/**
*/
public constructor(
private readonly accountData: Map<string, object>,
private readonly accountData: Map<keyof AccountDataEvents, MatrixEvent>,
private readonly crossSigningKeys?: ICrossSigningKeys,
private readonly keyBackupInfo?: IKeyBackupInfo,
private readonly keySignatures?: KeySignatures,
@@ -190,7 +194,7 @@ export class EncryptionSetupOperation {
// set account data
if (this.accountData) {
for (const [type, content] of this.accountData) {
await baseApis.setAccountData(type, content);
await baseApis.setAccountData(type, content.getContent());
}
}
// upload first cross-signing signatures with the new key
@@ -236,7 +240,7 @@ class AccountDataClientAdapter
implements AccountDataClient
{
//
public readonly values = new Map<string, MatrixEvent>();
public readonly values = new Map<keyof AccountDataEvents, MatrixEvent>();
/**
* @param existingValues - existing account data
@@ -248,33 +252,26 @@ class AccountDataClientAdapter
/**
* @returns the content of the account data
*/
public getAccountDataFromServer<T extends { [k: string]: any }>(type: string): Promise<T | null> {
public getAccountDataFromServer<K extends keyof AccountDataEvents>(type: K): Promise<AccountDataEvents[K] | null> {
return Promise.resolve(this.getAccountData(type));
}
/**
* @returns the content of the account data
*/
public getAccountData<T extends { [k: string]: any }>(type: string): T | null {
const modifiedValue = this.values.get(type);
if (modifiedValue) {
return modifiedValue as unknown as T;
}
const existingValue = this.existingValues.get(type);
if (existingValue) {
return existingValue.getContent<T>();
}
return null;
public getAccountData<K extends keyof AccountDataEvents>(type: K): AccountDataEvents[K] | null {
const event = this.values.get(type) ?? this.existingValues.get(type);
return event?.getContent<AccountDataEvents[K]>() ?? null;
}
public setAccountData(type: string, content: any): Promise<{}> {
public setAccountData<K extends keyof AccountDataEvents>(type: K, content: AccountDataEvents[K]): Promise<{}> {
const event = new MatrixEvent({ type, content });
const lastEvent = this.values.get(type);
this.values.set(type, content);
this.values.set(type, event);
// ensure accountData is emitted on the next tick,
// as SecretStorage listens for it while calling this method
// and it seems to rely on this.
return Promise.resolve().then(() => {
const event = new MatrixEvent({ type, content });
this.emit(ClientEvent.AccountData, event, lastEvent);
return {};
});
+4 -4
View File
@@ -25,12 +25,12 @@ import {
AccountDataClient,
ServerSideSecretStorage,
ServerSideSecretStorageImpl,
SecretStorageKey,
} from "../secret-storage.ts";
import { ISecretRequest, SecretSharing } from "./SecretSharing.ts";
/* re-exports for backwards compatibility */
export type {
AccountDataClient as IAccountDataClient,
SecretStorageKeyTuple,
SecretStorageKeyObject,
SECRET_STORAGE_ALGORITHM_V1_AES,
@@ -101,21 +101,21 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> im
/**
* Store an encrypted secret on the server
*/
public store(name: string, secret: string, keys?: string[] | null): Promise<void> {
public store(name: SecretStorageKey, secret: string, keys?: string[] | null): Promise<void> {
return this.storageImpl.store(name, secret, keys);
}
/**
* Get a secret from storage.
*/
public get(name: string): Promise<string | undefined> {
public get(name: SecretStorageKey): Promise<string | undefined> {
return this.storageImpl.get(name);
}
/**
* Check if a secret is stored on the server.
*/
public async isStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
public async isStored(name: SecretStorageKey): Promise<Record<string, SecretStorageKeyDescription> | null> {
return this.storageImpl.isStored(name);
}
+3 -3
View File
@@ -27,8 +27,8 @@ import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts
import encryptAESSecretStorageItem from "../utils/encryptAESSecretStorageItem.ts";
export interface IDehydratedDevice {
device_id: string; // eslint-disable-line camelcase
device_data: SecretStorageKeyDescription & {
device_id?: string; // eslint-disable-line camelcase
device_data?: SecretStorageKeyDescription & {
// eslint-disable-line camelcase
algorithm: string;
account: string; // pickle
@@ -90,7 +90,7 @@ export class DehydrationManager {
}
public async setKey(
key: Uint8Array,
key?: Uint8Array,
keyInfo: { [props: string]: any } = {},
deviceDisplayName?: string,
): Promise<boolean | undefined> {
+5 -6
View File
@@ -77,6 +77,7 @@ import {
AddSecretStorageKeyOpts,
calculateKeyCheck,
SECRET_STORAGE_ALGORITHM_V1_AES,
SecretStorageKey,
SecretStorageKeyDescription,
SecretStorageKeyObject,
SecretStorageKeyTuple,
@@ -326,8 +327,6 @@ export type CryptoEventHandlerMap = CryptoApiCryptoEventHandlerMap & {
*/
[CryptoEvent.Warning]: (type: string) => void;
[CryptoEvent.UserCrossSigningUpdated]: (userId: string) => void;
[CryptoEvent.LegacyCryptoStoreMigrationProgress]: (progress: number, total: number) => void;
};
export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap> implements CryptoBackend {
@@ -1196,21 +1195,21 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
/**
* @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#store}.
*/
public storeSecret(name: string, secret: string, keys?: string[]): Promise<void> {
public storeSecret(name: SecretStorageKey, secret: string, keys?: string[]): Promise<void> {
return this.secretStorage.store(name, secret, keys);
}
/**
* @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#get}.
*/
public getSecret(name: string): Promise<string | undefined> {
public getSecret(name: SecretStorageKey): Promise<string | undefined> {
return this.secretStorage.get(name);
}
/**
* @deprecated Use {@link MatrixClient#secretStorage} and {@link SecretStorage.ServerSideSecretStorage#isStored}.
*/
public isSecretStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
public isSecretStored(name: SecretStorageKey): Promise<Record<string, SecretStorageKeyDescription> | null> {
return this.secretStorage.isStored(name);
}
@@ -1848,7 +1847,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
/**
* Implementation of {@link CryptoBackend#getBackupDecryptor}.
*/
public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor> {
public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: Uint8Array): Promise<BackupDecryptor> {
if (!(privKey instanceof Uint8Array)) {
throw new Error(`getBackupDecryptor expects Uint8Array`);
}
@@ -776,7 +776,7 @@ export class Backend implements CryptoStore {
ev.preventDefault();
logger.log("Ignoring duplicate inbound group session: " + senderCurve25519Key + " / " + sessionId);
} else {
abortWithException(txn, new Error("Failed to add inbound group session: " + addReq.error));
abortWithException(txn, new Error("Failed to add inbound group session: " + addReq.error?.name));
}
};
}
@@ -162,7 +162,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
func: (session: ISessionInfo) => void,
): void {
const sessions = this._getEndToEndSessions(deviceKey);
func(sessions[sessionId] || {});
func(sessions[sessionId] ?? {});
}
public getEndToEndSessions(
@@ -170,7 +170,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
txn: unknown,
func: (sessions: { [sessionId: string]: ISessionInfo }) => void,
): void {
func(this._getEndToEndSessions(deviceKey) || {});
func(this._getEndToEndSessions(deviceKey) ?? {});
}
public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void {
@@ -281,8 +281,9 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
*
* @deprecated Prefer `generateQRCode`.
*/
public getQRCodeBytes(): Buffer | undefined {
return this._qrCodeData?.getBuffer();
public getQRCodeBytes(): Uint8ClampedArray | undefined {
if (!this._qrCodeData) return;
return new Uint8ClampedArray(this._qrCodeData.getBuffer());
}
/**
@@ -291,7 +292,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
* Only returns data once `phase` is `Ready` and the other party can scan a QR code;
* otherwise returns `undefined`.
*/
public async generateQRCode(): Promise<Buffer | undefined> {
public async generateQRCode(): Promise<Uint8ClampedArray | undefined> {
return this.getQRCodeBytes();
}
@@ -478,7 +479,7 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
return verifier;
}
public scanQRCode(qrCodeData: Uint8Array): Promise<Verifier> {
public scanQRCode(qrCodeData: Uint8ClampedArray): Promise<Verifier> {
throw new Error("QR code scanning not supported by legacy crypto");
}
+3 -3
View File
@@ -18,11 +18,11 @@ 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
* @returns An Uint8Array 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> {
export async function sha256(plaintext: string): Promise<Uint8Array> {
if (!globalThis.crypto.subtle) {
throw new Error("Crypto.subtle is not available: insecure context?");
}
@@ -30,5 +30,5 @@ export async function sha256(plaintext: string): Promise<ArrayBuffer> {
const digest = await globalThis.crypto.subtle.digest("SHA-256", utf8);
return digest;
return new Uint8Array(digest);
}
+35 -2
View File
@@ -284,7 +284,13 @@ export class RoomWidgetClient extends MatrixClient {
const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [this.roomId]);
const events = rawEvents.map((rawEvent) => new MatrixEvent(rawEvent as Partial<IEvent>));
await this.syncApi!.injectRoomEvents(this.room!, [], events);
if (this.syncApi instanceof SyncApi) {
// Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode
// -> state events in `timelineEventList` will update the state.
await this.syncApi.injectRoomEvents(this.room!, undefined, events);
} else {
await this.syncApi!.injectRoomEvents(this.room!, events); // Sliding Sync
}
events.forEach((event) => {
this.emit(ClientEvent.Event, event);
logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
@@ -567,7 +573,34 @@ export class RoomWidgetClient extends MatrixClient {
// Only inject once we have update the txId
await this.updateTxId(event);
await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
// The widget API does not tell us whether a state event came from `state_after` or not so we assume legacy behaviour for now.
if (this.syncApi instanceof SyncApi) {
// The code will want to be something like:
// ```
// if (!params.addToTimeline && !params.addToState) {
// // Passing undefined for `stateAfterEventList` makes `injectRoomEvents` run in "legacy mode"
// // -> state events part of the `timelineEventList` parameter will update the state.
// this.injectRoomEvents(this.room!, [], undefined, [event]);
// } else {
// this.injectRoomEvents(this.room!, undefined, params.addToState ? [event] : [], params.addToTimeline ? [event] : []);
// }
// ```
// Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode
// -> state events in `timelineEventList` will update the state.
await this.syncApi.injectRoomEvents(this.room!, [], undefined, [event]);
} else {
// The code will want to be something like:
// ```
// if (!params.addToTimeline && !params.addToState) {
// this.injectRoomEvents(this.room!, [], [event]);
// } else {
// this.injectRoomEvents(this.room!, params.addToState ? [event] : [], params.addToTimeline ? [event] : []);
// }
// ```
await this.syncApi!.injectRoomEvents(this.room!, [], [event]); // Sliding Sync
}
this.emit(ClientEvent.Event, event);
this.setSyncState(SyncState.Syncing);
logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
+2 -2
View File
@@ -180,8 +180,8 @@ export function calculateRetryBackoff(err: any, attempts: number, retryConnectio
return -1;
}
if (err.httpStatus && (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401)) {
// client error; no amount of retrying will save you now.
if (err.httpStatus && Math.floor(err.httpStatus / 100) === 4 && err.httpStatus !== 429) {
// client error; no amount of retrying will save you now (except for rate limiting which is handled below)
return -1;
}
+71 -122
View File
@@ -14,35 +14,78 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EitherAnd } from "matrix-events-sdk/lib/types";
import { MatrixEvent } from "../matrix.ts";
import { deepCompare } from "../utils.ts";
import { Focus } from "./focus.ts";
import { isLivekitFocusActive } from "./LivekitFocus.ts";
/**
* The default duration in milliseconds that a membership is considered valid for.
* Ordinarily the client responsible for the session will update the membership before it expires.
* We use this duration as the fallback case where stale sessions are present for some reason.
*/
export const DEFAULT_EXPIRE_DURATION = 1000 * 60 * 60 * 4;
type CallScope = "m.room" | "m.user";
// Represents an entry in the memberships section of an m.call.member event as it is on the wire
// There are two different data interfaces. One for the Legacy types and one compliant with MSC4143
// MSC4143 (MatrixRTC) session membership data
/**
* MSC4143 (MatrixRTC) session membership data.
* Represents an entry in the memberships section of an m.call.member event as it is on the wire.
**/
export type SessionMembershipData = {
/**
* The RTC application defines the type of the RTC session.
*/
application: string;
/**
* The id of this session.
* A session can never span over multiple rooms so this id is to distinguish between
* multiple session in one room. A room wide session that is not associated with a user,
* and therefore immune to creation race conflicts, uses the `call_id: ""`.
*/
call_id: string;
/**
* The Matrix device ID of this session. A single user can have multiple sessions on different devices.
*/
device_id: string;
/**
* The focus selection system this user/membership is using.
*/
focus_active: Focus;
/**
* A list of possible foci this uses knows about. One of them might be used based on the focus_active
* selection system.
*/
foci_preferred: Focus[];
/**
* Optional field that contains the creation of the session. If it is undefined the creation
* is the `origin_server_ts` of the event itself. For updates to the event this property tracks
* the `origin_server_ts` of the initial join event.
* - If it is undefined it can be interpreted as a "Join".
* - If it is defined it can be interpreted as an "Update"
*/
created_ts?: number;
// Application specific data
scope?: CallScope;
};
export const isSessionMembershipData = (data: CallMembershipData): data is SessionMembershipData =>
"focus_active" in data;
/**
* If the `application` = `"m.call"` this defines if it is a room or user owned call.
* There can always be one room scroped call but multiple user owned calls (breakout sessions)
*/
scope?: CallScope;
/**
* Optionally we allow to define a delta to the `created_ts` that defines when the event is expired/invalid.
* This should be set to multiple hours. The only reason it exist is to deal with failed delayed events.
* (for example caused by a homeserver crashes)
**/
expires?: number;
};
const checkSessionsMembershipData = (data: any, errors: string[]): data is SessionMembershipData => {
const prefix = "Malformed session membership event: ";
@@ -59,65 +102,20 @@ const checkSessionsMembershipData = (data: any, errors: string[]): data is Sessi
return errors.length === 0;
};
// Legacy session membership data
export type CallMembershipDataLegacy = {
application: string;
call_id: string;
scope: CallScope;
device_id: string;
membershipID: string;
created_ts?: number;
foci_active?: Focus[];
} & EitherAnd<{ expires: number }, { expires_ts: number }>;
export const isLegacyCallMembershipData = (data: CallMembershipData): data is CallMembershipDataLegacy =>
"membershipID" in data;
const checkCallMembershipDataLegacy = (data: any, errors: string[]): data is CallMembershipDataLegacy => {
const prefix = "Malformed legacy rtc membership event: ";
if (!("expires" in data || "expires_ts" in data)) {
errors.push(prefix + "expires_ts or expires must be present");
}
if ("expires" in data) {
if (typeof data.expires !== "number") {
errors.push(prefix + "expires must be numeric");
}
}
if ("expires_ts" in data) {
if (typeof data.expires_ts !== "number") {
errors.push(prefix + "expires_ts must be numeric");
}
}
if (typeof data.device_id !== "string") errors.push(prefix + "device_id must be string");
if (typeof data.call_id !== "string") errors.push(prefix + "call_id must be string");
if (typeof data.application !== "string") errors.push(prefix + "application must be a string");
if (typeof data.membershipID !== "string") errors.push(prefix + "membershipID must be a string");
// optional elements
if (data.created_ts && typeof data.created_ts !== "number") errors.push(prefix + "created_ts must be number");
// application specific data (we first need to check if they exist)
if (data.scope && typeof data.scope !== "string") errors.push(prefix + "scope must be string");
return errors.length === 0;
};
export type CallMembershipData = CallMembershipDataLegacy | SessionMembershipData;
export class CallMembership {
public static equal(a: CallMembership, b: CallMembership): boolean {
return deepCompare(a.membershipData, b.membershipData);
}
private membershipData: CallMembershipData;
private membershipData: SessionMembershipData;
public constructor(
private parentEvent: MatrixEvent,
data: any,
) {
const sessionErrors: string[] = [];
const legacyErrors: string[] = [];
if (!checkSessionsMembershipData(data, sessionErrors) && !checkCallMembershipDataLegacy(data, legacyErrors)) {
if (!checkSessionsMembershipData(data, sessionErrors)) {
throw Error(
`unknown CallMembership data. Does not match legacy call.member (${legacyErrors.join(" & ")}) events nor MSC4143 (${sessionErrors.join(" & ")})`,
`unknown CallMembership data. Does not match MSC4143 call.member (${sessionErrors.join(" & ")}) events this could be a legacy membership event: (${data})`,
);
} else {
this.membershipData = data;
@@ -149,11 +147,10 @@ export class CallMembership {
}
public get membershipID(): string {
if (isLegacyCallMembershipData(this.membershipData)) return this.membershipData.membershipID;
// the createdTs behaves equivalent to the membershipID.
// we only need the field for the legacy member envents where we needed to update them
// synapse ignores sending state events if they have the same content.
else return this.createdTs().toString();
return this.createdTs().toString();
}
public createdTs(): number {
@@ -161,87 +158,39 @@ export class CallMembership {
}
/**
* Gets the absolute expiry time of the membership if applicable to this membership type.
* Gets the absolute expiry timestamp of the membership.
* @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;
} else {
// We know it exists because we checked for this in the constructor.
return this.membershipData.expires_ts;
}
}
/**
* 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();
const localCreationTs = this.parentEvent.localTimestamp - relativeCreationTime;
return localCreationTs + this.membershipData.expires;
} else {
// With expires_ts we cannot convert to local time.
// TODO: Check the server timestamp and compute a diff to local time.
return this.membershipData.expires_ts;
}
public getAbsoluteExpiry(): number {
// TODO: calculate this from the MatrixRTCSession join configuration directly
return this.createdTs() + (this.membershipData.expires ?? DEFAULT_EXPIRE_DURATION);
}
/**
* @returns The number of milliseconds until the membership expires or undefined if applicable
*/
public getMsUntilExpiry(): number | undefined {
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;
public getMsUntilExpiry(): number {
// 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();
}
/**
* @returns true if the membership has expired, otherwise false
*/
public isExpired(): boolean {
if (isLegacyCallMembershipData(this.membershipData)) return this.getMsUntilExpiry()! <= 0;
// MSC4143 events expire by being updated. So if the event exists, its not expired.
return false;
return this.getMsUntilExpiry() <= 0;
}
public getPreferredFoci(): Focus[] {
// To support both, the new and the old MatrixRTC memberships have two cases based
// on the availablitiy of `foci_preferred`
if (isLegacyCallMembershipData(this.membershipData)) return this.membershipData.foci_active ?? [];
// MSC4143 style membership
return this.membershipData.foci_preferred;
}
public getFocusSelection(): string | undefined {
if (isLegacyCallMembershipData(this.membershipData)) {
return "oldest_membership";
} else {
const focusActive = this.membershipData.focus_active;
if (isLivekitFocusActive(focusActive)) {
return focusActive.focus_selection;
}
const focusActive = this.membershipData.focus_active;
if (isLivekitFocusActive(focusActive)) {
return focusActive.focus_selection;
}
}
}
+32 -225
View File
@@ -21,23 +21,16 @@ import { Room } from "../models/room.ts";
import { MatrixClient } from "../client.ts";
import { EventType } from "../@types/event.ts";
import { UpdateDelayedEventAction } from "../@types/requests.ts";
import {
CallMembership,
CallMembershipData,
CallMembershipDataLegacy,
SessionMembershipData,
isLegacyCallMembershipData,
} from "./CallMembership.ts";
import { CallMembership, DEFAULT_EXPIRE_DURATION, SessionMembershipData } from "./CallMembership.ts";
import { RoomStateEvent } from "../models/room-state.ts";
import { Focus } from "./focus.ts";
import { randomString, secureRandomBase64Url } from "../randomstring.ts";
import { secureRandomBase64Url } from "../randomstring.ts";
import { EncryptionKeysEventContent } from "./types.ts";
import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts";
import { KnownMembership } from "../@types/membership.ts";
import { HTTPError, MatrixError, safeGetRetryAfterMs } from "../http-api/errors.ts";
import { MatrixEvent } from "../models/event.ts";
import { isLivekitFocusActive } from "./LivekitFocus.ts";
import { ExperimentalGroupCallRoomMemberState } from "../webrtc/groupCall.ts";
import { sleep } from "../utils.ts";
const logger = rootLogger.getChild("MatrixRTCSession");
@@ -82,14 +75,6 @@ export interface JoinSessionConfig {
*/
manageMediaKeys?: boolean;
/** Lets you configure how the events for the session are formatted.
* - legacy: use one event with a membership array.
* - MSC4143: use one event per membership (with only one membership per event)
* More details can be found in MSC4143 and by checking the types:
* `CallMembershipDataLegacy` and `SessionMembershipData`
*/
useLegacyMemberEvents?: boolean;
/**
* The timeout (in milliseconds) after we joined the call, that our membership should expire
* unless we have explicitly updated it.
@@ -161,11 +146,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
private joinConfig?: JoinSessionConfig;
private get membershipExpiryTimeout(): number {
return this.joinConfig?.membershipExpiryTimeout ?? 60 * 60 * 1000;
}
private get memberEventCheckPeriod(): number {
return this.joinConfig?.memberEventCheckPeriod ?? 2 * 60 * 1000;
return this.joinConfig?.membershipExpiryTimeout ?? DEFAULT_EXPIRE_DURATION;
}
private get callMemberEventRetryDelayMinimum(): number {
@@ -206,14 +187,6 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
return this.joinConfig?.callMemberEventRetryJitter ?? 2_000;
}
// An identifier for our membership of the call. This will allow us to easily recognise
// whether a membership was sent by this session or is stale from some other time.
// It also forces our membership events to be unique, because otherwise we could try
// to overwrite a membership from a previous session but it would do nothing because the
// event content would be identical. We need the origin_server_ts to update though, so
// forcing unique content fixes this.
private membershipId: string | undefined;
private memberEventTimeout?: ReturnType<typeof setTimeout>;
private expiryTimeout?: ReturnType<typeof setTimeout>;
private keysEventUpdateTimeout?: ReturnType<typeof setTimeout>;
@@ -229,7 +202,6 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
private needCallMembershipUpdate = false;
private manageMediaKeys = false;
private useLegacyMemberEvents = true;
// userId:deviceId => array of (key, timestamp)
private encryptionKeys = new Map<string, Array<{ key: Uint8Array; timestamp: number }>>();
private lastEncryptionKeyUpdateRequest?: number;
@@ -292,19 +264,14 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
// Dont even bother about empty events (saves us from costly type/"key in" checks in bigger rooms)
if (eventKeysCount === 0) continue;
let membershipContents: any[] = [];
const membershipContents: any[] = [];
// We first decide if its a MSC4143 event (per device state key)
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"];
logger.warn(`Legacy event found. Those are ignored, they do not contribute to the MatrixRTC session`);
}
if (membershipContents.length === 0) continue;
@@ -416,8 +383,6 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
this.joinConfig = joinConfig;
this.relativeExpiry = this.membershipExpiryTimeout;
this.manageMediaKeys = joinConfig?.manageMediaKeys ?? this.manageMediaKeys;
this.useLegacyMemberEvents = joinConfig?.useLegacyMemberEvents ?? this.useLegacyMemberEvents;
this.membershipId = randomString(5);
logger.info(`Joining call session in room ${this.room.roomId} with manageMediaKeys=${this.manageMediaKeys}`);
if (joinConfig?.manageMediaKeys) {
@@ -471,7 +436,6 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
this.relativeExpiry = undefined;
this.ownFocusActive = undefined;
this.manageMediaKeys = false;
this.membershipId = undefined;
this.emit(MatrixRTCSessionEvent.JoinStateChanged, false);
if (timeout) {
@@ -492,9 +456,9 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
const oldestMembership = this.getOldestMembership();
return oldestMembership?.getPreferredFoci()[0];
}
}
if (!this.ownFocusActive) {
// we use the legacy call.member events so default to oldest member
} else {
// We do not understand the membership format (could be legacy). We default to oldestMembership
// Once there are other methods this is a hard error!
const oldestMembership = this.getOldestMembership();
return oldestMembership?.getPreferredFoci()[0];
}
@@ -878,7 +842,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
}
}
if (this.manageMediaKeys && this.isJoined() && this.makeNewKeyTimeout === undefined) {
if (this.manageMediaKeys && this.isJoined()) {
const oldMembershipIds = new Set(
oldMemberships.filter((m) => !this.isMyMembership(m)).map(getParticipantIdFromMembership),
);
@@ -896,8 +860,12 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
this.storeLastMembershipFingerprints();
if (anyLeft) {
logger.debug(`Member(s) have left: queueing sender key rotation`);
this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, this.makeKeyDelay);
if (this.makeNewKeyTimeout) {
// existing rotation in progress, so let it complete
} else {
logger.debug(`Member(s) have left: queueing sender key rotation`);
this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, this.makeKeyDelay);
}
} else if (anyJoined) {
logger.debug(`New member(s) have joined: re-sending keys`);
this.requestSendCurrentKey();
@@ -924,37 +892,10 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
this.lastMembershipFingerprints = new Set(
this.memberships
.filter((m) => !this.isMyMembership(m))
.map((m) => `${getParticipantIdFromMembership(m)}:${m.membershipID}:${m.createdTs()}`),
.map((m) => `${getParticipantIdFromMembership(m)}:${m.createdTs()}`),
);
}
/**
* Constructs our own membership
* @param prevMembership - The previous value of our call membership, if any
*/
private makeMyMembershipLegacy(deviceId: string, prevMembership?: CallMembership): CallMembershipDataLegacy {
if (this.relativeExpiry === undefined) {
throw new Error("Tried to create our own membership event when we're not joined!");
}
if (this.membershipId === undefined) {
throw new Error("Tried to create our own membership event when we have no membership ID!");
}
const createdTs = prevMembership?.createdTs();
return {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: deviceId,
expires: this.relativeExpiry,
// TODO: Date.now() should be the origin_server_ts (now).
expires_ts: this.relativeExpiry + (createdTs ?? Date.now()),
// we use the fociPreferred since this is the list of foci.
// it is named wrong in the Legacy events.
foci_active: this.ownFociPreferred,
membershipID: this.membershipId,
...(createdTs ? { created_ts: createdTs } : {}),
};
}
/**
* Constructs our own membership
*/
@@ -964,36 +905,12 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
scope: "m.room",
application: "m.call",
device_id: deviceId,
expires: this.relativeExpiry,
focus_active: { type: "livekit", focus_selection: "oldest_membership" },
foci_preferred: this.ownFociPreferred ?? [],
};
}
/**
* Returns true if our membership event needs to be updated
*/
private membershipEventNeedsUpdate(
myPrevMembershipData?: CallMembershipData,
myPrevMembership?: CallMembership,
): boolean {
if (myPrevMembership && myPrevMembership.getMsUntilExpiry() === undefined) return false;
// Need to update if there's a membership for us but we're not joined (valid or otherwise)
if (!this.isJoined()) return !!myPrevMembershipData;
// ...or if we are joined, but there's no valid membership event
if (!myPrevMembership) return true;
const expiryTime = myPrevMembership.getMsUntilExpiry();
if (expiryTime !== undefined && expiryTime < this.membershipExpiryTimeout / 2) {
// ...or if the expiry time needs bumping
this.relativeExpiry! += this.membershipExpiryTimeout;
return true;
}
return false;
}
private makeNewMembership(deviceId: string): SessionMembershipData | {} {
// If we're joined, add our own
if (this.isJoined()) {
@@ -1001,49 +918,6 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
}
return {};
}
/**
* Makes a new membership list given the old list along with this user's previous membership event
* (if any) and this device's previous membership (if any)
*/
private makeNewLegacyMemberships(
oldMemberships: CallMembershipData[],
localDeviceId: string,
myCallMemberEvent?: MatrixEvent,
myPrevMembership?: CallMembership,
): ExperimentalGroupCallRoomMemberState {
const filterExpired = (m: CallMembershipData): boolean => {
let membershipObj;
try {
membershipObj = new CallMembership(myCallMemberEvent!, m);
} catch {
return false;
}
return !membershipObj.isExpired();
};
const transformMemberships = (m: CallMembershipData): CallMembershipData => {
if (m.created_ts === undefined) {
// we need to fill this in with the origin_server_ts from its original event
m.created_ts = myCallMemberEvent!.getTs();
}
return m;
};
// Filter our any invalid or expired memberships, and also our own - we'll add that back in next
let newMemberships = oldMemberships.filter(filterExpired).filter((m) => m.device_id !== localDeviceId);
// Fix up any memberships that need their created_ts adding
newMemberships = newMemberships.map(transformMemberships);
// If we're joined, add our own
if (this.isJoined()) {
newMemberships.push(this.makeMyMembershipLegacy(localDeviceId, myPrevMembership));
}
return { memberships: newMemberships };
}
private triggerCallMembershipEventUpdate = async (): Promise<void> => {
// TODO: Should this await on a shared promise?
@@ -1077,64 +951,14 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
const localDeviceId = this.client.getDeviceId();
if (!localUserId || !localDeviceId) throw new Error("User ID or device ID was null!");
const callMemberEvents = roomState.events.get(EventType.GroupCallMemberPrefix);
const legacy = this.stateEventsContainOngoingLegacySession(callMemberEvents);
let newContent: {} | ExperimentalGroupCallRoomMemberState | SessionMembershipData = {};
if (legacy) {
const myCallMemberEvent = callMemberEvents?.get(localUserId);
const content = myCallMemberEvent?.getContent() ?? {};
let myPrevMembership: CallMembership | undefined;
// We know its CallMembershipDataLegacy
const memberships: CallMembershipDataLegacy[] = Array.isArray(content["memberships"])
? content["memberships"]
: [];
const myPrevMembershipData = memberships.find((m) => m.device_id === localDeviceId);
try {
if (
myCallMemberEvent &&
myPrevMembershipData &&
isLegacyCallMembershipData(myPrevMembershipData) &&
myPrevMembershipData.membershipID === this.membershipId
) {
myPrevMembership = new CallMembership(myCallMemberEvent, myPrevMembershipData);
}
} catch (e) {
// This would indicate a bug or something weird if our own call membership
// wasn't valid
logger.warn("Our previous call membership was invalid - this shouldn't happen.", e);
}
if (myPrevMembership) {
logger.debug(`${myPrevMembership.getMsUntilExpiry()} until our membership expires`);
}
if (!this.membershipEventNeedsUpdate(myPrevMembershipData, myPrevMembership)) {
// nothing to do - reschedule the check again
this.memberEventTimeout = setTimeout(
this.triggerCallMembershipEventUpdate,
this.memberEventCheckPeriod,
);
return;
}
newContent = this.makeNewLegacyMemberships(memberships, localDeviceId, myCallMemberEvent, myPrevMembership);
} else {
newContent = this.makeNewMembership(localDeviceId);
}
let newContent: {} | SessionMembershipData = {};
// TODO: implement expiry logic to MSC4143 events
// previously we checked here if the event is timed out and scheduled a check if not.
// maybe there is a better way.
newContent = this.makeNewMembership(localDeviceId);
try {
if (legacy) {
await this.client.sendStateEvent(
this.room.roomId,
EventType.GroupCallMemberPrefix,
newContent,
localUserId,
);
if (this.isJoined()) {
// check periodically to see if we need to refresh our member event
this.memberEventTimeout = setTimeout(
this.triggerCallMembershipEventUpdate,
this.memberEventCheckPeriod,
);
}
} else if (this.isJoined()) {
if (this.isJoined()) {
const stateKey = this.makeMembershipStateKey(localUserId, localDeviceId);
const prepareDelayedDisconnection = async (): Promise<void> => {
try {
@@ -1185,15 +1009,21 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
),
);
} catch (e) {
logger.warn("Failed to update delayed disconnection event, prepare it again:", e);
this.disconnectDelayId = undefined;
await prepareDelayedDisconnection();
if (e instanceof MatrixError && e.errcode === "M_NOT_FOUND") {
// If we get a M_NOT_FOUND we prepare a new delayed event.
// In other error cases we do not want to prepare anything since we do not have the guarantee, that the
// future is not still running.
logger.warn("Failed to update delayed disconnection event, prepare it again:", e);
this.disconnectDelayId = undefined;
await prepareDelayedDisconnection();
}
}
}
if (this.disconnectDelayId !== undefined) {
this.scheduleDelayDisconnection();
}
} else {
// Not joined
let sentDelayedDisconnect = false;
if (this.disconnectDelayId !== undefined) {
try {
@@ -1246,29 +1076,6 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
}
};
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"])) {
for (const membership of content.memberships) {
if (!new CallMembership(callMemberEvent, membership).isExpired()) {
return true;
}
}
} else if (Object.keys(content).length > 0) {
containsAnyOngoingSession ||= true;
containsUnknownOngoingSession ||= !("focus_active" in content);
}
}
return containsAnyOngoingSession && !containsUnknownOngoingSession ? false : this.useLegacyMemberEvents;
}
private makeMembershipStateKey(localUserId: string, localDeviceId: string): string {
const stateKey = `${localUserId}_${localDeviceId}`;
if (/^org\.matrix\.msc(3757|3779)\b/.exec(this.room.getVersion())) {
+17 -38
View File
@@ -58,13 +58,13 @@ export interface IRoomTimelineData {
}
export interface IAddEventToTimelineOptions
extends Pick<IAddEventOptions, "toStartOfTimeline" | "roomState" | "timelineWasEmpty"> {
extends Pick<IAddEventOptions, "toStartOfTimeline" | "roomState" | "timelineWasEmpty" | "addToState"> {
/** Whether the sync response came from cache */
fromCache?: boolean;
}
export interface IAddLiveEventOptions
extends Pick<IAddEventToTimelineOptions, "fromCache" | "roomState" | "timelineWasEmpty"> {
extends Pick<IAddEventToTimelineOptions, "fromCache" | "roomState" | "timelineWasEmpty" | "addToState"> {
/** Applies to events in the timeline only. If this is 'replace' then if a
* duplicate is encountered, the event passed to this function will replace
* the existing event in the timeline. If this is not specified, or is
@@ -391,6 +391,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
public addEventsToTimeline(
events: MatrixEvent[],
toStartOfTimeline: boolean,
addToState: boolean,
timeline: EventTimeline,
paginationToken?: string | null,
): void {
@@ -495,6 +496,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
// we don't know about this event yet. Just add it to the timeline.
this.addEventToTimeline(event, timeline, {
toStartOfTimeline,
addToState,
});
lastEventWasNew = true;
didUpdate = true;
@@ -592,7 +594,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
*/
public addLiveEvent(
event: MatrixEvent,
{ duplicateStrategy, fromCache, roomState, timelineWasEmpty }: IAddLiveEventOptions = {},
{ duplicateStrategy, fromCache, roomState, timelineWasEmpty, addToState }: IAddLiveEventOptions,
): void {
if (this.filter) {
const events = this.filter.filterRoomTimeline([event]);
@@ -630,6 +632,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
fromCache,
roomState,
timelineWasEmpty,
addToState,
});
}
@@ -649,40 +652,8 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
public addEventToTimeline(
event: MatrixEvent,
timeline: EventTimeline,
{ toStartOfTimeline, fromCache, roomState, timelineWasEmpty }: IAddEventToTimelineOptions,
): void;
/**
* @deprecated In favor of the overload with `IAddEventToTimelineOptions`
*/
public addEventToTimeline(
event: MatrixEvent,
timeline: EventTimeline,
toStartOfTimeline: boolean,
fromCache?: boolean,
roomState?: RoomState,
): void;
public addEventToTimeline(
event: MatrixEvent,
timeline: EventTimeline,
toStartOfTimelineOrOpts: boolean | IAddEventToTimelineOptions,
fromCache = false,
roomState?: RoomState,
{ toStartOfTimeline, fromCache = false, roomState, timelineWasEmpty, addToState }: IAddEventToTimelineOptions,
): void {
let toStartOfTimeline = !!toStartOfTimelineOrOpts;
let timelineWasEmpty: boolean | undefined;
if (typeof toStartOfTimelineOrOpts === "object") {
({ toStartOfTimeline, fromCache = false, roomState, timelineWasEmpty } = toStartOfTimelineOrOpts);
} else if (toStartOfTimelineOrOpts !== undefined) {
// Deprecation warning
// FIXME: Remove after 2023-06-01 (technical debt)
logger.warn(
"Overload deprecated: " +
"`EventTimelineSet.addEventToTimeline(event, timeline, toStartOfTimeline, fromCache?, roomState?)` " +
"is deprecated in favor of the overload with " +
"`EventTimelineSet.addEventToTimeline(event, timeline, IAddEventToTimelineOptions)`",
);
}
if (timeline.getTimelineSet() !== this) {
throw new Error(`EventTimelineSet.addEventToTimeline: Timeline=${timeline.toString()} does not belong " +
"in timelineSet(threadId=${this.thread?.id})`);
@@ -713,6 +684,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
toStartOfTimeline,
roomState,
timelineWasEmpty,
addToState,
});
this._eventIdToTimeline.set(eventId, timeline);
@@ -741,7 +713,12 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
* @remarks
* Fires {@link RoomEvent.Timeline}
*/
public insertEventIntoTimeline(event: MatrixEvent, timeline: EventTimeline, roomState: RoomState): void {
public insertEventIntoTimeline(
event: MatrixEvent,
timeline: EventTimeline,
roomState: RoomState,
addToState: boolean,
): void {
if (timeline.getTimelineSet() !== this) {
throw new Error(`EventTimelineSet.insertEventIntoTimeline: Timeline=${timeline.toString()} does not belong " +
"in timelineSet(threadId=${this.thread?.id})`);
@@ -777,6 +754,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
fromCache: false,
timelineWasEmpty: false,
roomState,
addToState,
});
return;
}
@@ -799,7 +777,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
// If we got to the end of the loop, insertIndex points at the end of
// the list.
timeline.insertEvent(event, insertIndex, roomState);
timeline.insertEvent(event, insertIndex, roomState, addToState);
this._eventIdToTimeline.set(eventId, timeline);
const data: IRoomTimelineData = {
@@ -832,6 +810,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
} else if (!this.filter || this.filter.filterRoomTimeline([localEvent]).length) {
this.addEventToTimeline(localEvent, this.liveTimeline, {
toStartOfTimeline: false,
addToState: false,
});
}
}
+10 -5
View File
@@ -35,6 +35,11 @@ export interface IAddEventOptions extends Pick<IMarkerFoundOptions, "timelineWas
toStartOfTimeline: boolean;
/** The state events to reconcile metadata from */
roomState?: RoomState;
/** Whether to add timeline events to the state as was done in legacy sync v2.
* If true then timeline events will be added to the state.
* In sync v2 with org.matrix.msc4222.use_state_after and simplified sliding sync,
* all state arrives explicitly and timeline events should not be added. */
addToState: boolean;
}
export enum Direction {
@@ -127,7 +132,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, undefined, true);
this.startState = new RoomState(this.roomId);
this.endState = new RoomState(this.roomId);
}
@@ -362,7 +367,7 @@ export class EventTimeline {
*/
public addEvent(
event: MatrixEvent,
{ toStartOfTimeline, roomState, timelineWasEmpty }: IAddEventOptions = { toStartOfTimeline: false },
{ toStartOfTimeline, roomState, timelineWasEmpty, addToState }: IAddEventOptions,
): void {
if (!roomState) {
roomState = toStartOfTimeline ? this.startState : this.endState;
@@ -374,7 +379,7 @@ export class EventTimeline {
EventTimeline.setEventMetadata(event, roomState!, toStartOfTimeline);
// modify state but only on unfiltered timelineSets
if (event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
if (addToState && event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
roomState?.setStateEvents([event], { timelineWasEmpty });
// it is possible that the act of setting the state event means we
// can set more metadata (specifically sender/target props), so try
@@ -417,14 +422,14 @@ export class EventTimeline {
*
* @internal
*/
public insertEvent(event: MatrixEvent, insertIndex: number, roomState: RoomState): void {
public insertEvent(event: MatrixEvent, insertIndex: number, roomState: RoomState, addToState: boolean): void {
const timelineSet = this.getTimelineSet();
if (timelineSet.room) {
EventTimeline.setEventMetadata(event, roomState, false);
// modify state but only on unfiltered timelineSets
if (event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
if (addToState && event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
roomState.setStateEvents([event], {});
// it is possible that the act of setting the state event means we
// can set more metadata (specifically sender/target props), so try
+8 -6
View File
@@ -559,9 +559,9 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
return {} as T;
}
if (this.clearEvent) {
return (this.clearEvent.content || {}) as T;
return (this.clearEvent.content ?? {}) as T;
}
return (this.event.content || {}) as T;
return (this.event.content ?? {}) as T;
}
/**
@@ -575,7 +575,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
if (this._localRedactionEvent) {
return {} as T;
} else if (this._replacingEvent) {
return this._replacingEvent.getContent()["m.new_content"] || {};
return this._replacingEvent.getContent()["m.new_content"] ?? {};
} else {
return this.getOriginalContent();
}
@@ -1250,7 +1250,9 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
const timeline = room.getLiveTimeline();
// We use insertEventIntoTimeline to insert it in timestamp order,
// because we don't know where it should go (until we have MSC4033).
timeline.getTimelineSet().insertEventIntoTimeline(this, timeline, timeline.getState(EventTimeline.FORWARDS)!);
timeline
.getTimelineSet()
.insertEventIntoTimeline(this, timeline, timeline.getState(EventTimeline.FORWARDS)!, false);
}
/**
@@ -1394,7 +1396,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
this.emit(MatrixEventEvent.LocalEventIdReplaced, this);
}
this.localTimestamp = Date.now() - this.getAge()!;
this.localTimestamp = Date.now() - (this.getAge() ?? 0);
}
/**
@@ -1631,7 +1633,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* @param otherEvent - The other event to check against.
* @returns True if the events are the same, false otherwise.
*/
public isEquivalentTo(otherEvent: MatrixEvent): boolean {
public isEquivalentTo(otherEvent?: MatrixEvent): boolean {
if (!otherEvent) return false;
if (otherEvent === this) return true;
const myProps = deepSortedObjectEntries(this.event);
+58
View File
@@ -0,0 +1,58 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { UnstableValue } from "matrix-events-sdk";
/// The event type storing the user's individual policies.
///
/// Exported for testing purposes.
export const POLICIES_ACCOUNT_EVENT_TYPE = new UnstableValue("m.policies", "org.matrix.msc3847.policies");
/// The key within the user's individual policies storing the user's ignored invites.
///
/// Exported for testing purposes.
export const IGNORE_INVITES_ACCOUNT_EVENT_KEY = new UnstableValue(
"m.ignore.invites",
"org.matrix.msc3847.ignore.invites",
);
/// The types of recommendations understood.
export enum PolicyRecommendation {
Ban = "m.ban",
}
/**
* The various scopes for policies.
*/
export enum PolicyScope {
/**
* The policy deals with an individual user, e.g. reject invites
* from this user.
*/
User = "m.policy.user",
/**
* The policy deals with a room, e.g. reject invites towards
* a specific room.
*/
Room = "m.policy.room",
/**
* The policy deals with a server, e.g. reject invites from
* this server.
*/
Server = "m.policy.server",
}
+7 -42
View File
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { UnstableValue } from "matrix-events-sdk";
import { MatrixClient } from "../client.ts";
import { IContent, MatrixEvent } from "./event.ts";
import { EventTimeline } from "./event-timeline.ts";
@@ -23,47 +21,14 @@ import { Preset } from "../@types/partials.ts";
import { globToRegexp } from "../utils.ts";
import { Room } from "./room.ts";
import { EventType, StateEvents } from "../@types/event.ts";
import {
IGNORE_INVITES_ACCOUNT_EVENT_KEY,
POLICIES_ACCOUNT_EVENT_TYPE,
PolicyRecommendation,
PolicyScope,
} from "./invites-ignorer-types.ts";
/// The event type storing the user's individual policies.
///
/// Exported for testing purposes.
export const POLICIES_ACCOUNT_EVENT_TYPE = new UnstableValue("m.policies", "org.matrix.msc3847.policies");
/// The key within the user's individual policies storing the user's ignored invites.
///
/// Exported for testing purposes.
export const IGNORE_INVITES_ACCOUNT_EVENT_KEY = new UnstableValue(
"m.ignore.invites",
"org.matrix.msc3847.ignore.invites",
);
/// The types of recommendations understood.
export enum PolicyRecommendation {
Ban = "m.ban",
}
/**
* The various scopes for policies.
*/
export enum PolicyScope {
/**
* The policy deals with an individual user, e.g. reject invites
* from this user.
*/
User = "m.policy.user",
/**
* The policy deals with a room, e.g. reject invites towards
* a specific room.
*/
Room = "m.policy.room",
/**
* The policy deals with a server, e.g. reject invites from
* this server.
*/
Server = "m.policy.server",
}
export { IGNORE_INVITES_ACCOUNT_EVENT_KEY, POLICIES_ACCOUNT_EVENT_TYPE, PolicyRecommendation, PolicyScope };
const scopeToEventTypeMap: Record<PolicyScope, keyof StateEvents> = {
[PolicyScope.User]: EventType.PolicyRuleUser,
+4 -2
View File
@@ -20,6 +20,7 @@ import { RelationType } from "../@types/event.ts";
import { TypedEventEmitter } from "./typed-event-emitter.ts";
import { MatrixClient } from "../client.ts";
import { Room } from "./room.ts";
import { CryptoBackend } from "../common-crypto/CryptoBackend.ts";
export enum RelationsEvent {
Add = "Relations.add",
@@ -323,8 +324,9 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
return event;
}, null);
if (lastReplacement?.shouldAttemptDecryption() && this.client.isCryptoEnabled()) {
await lastReplacement.attemptDecryption(this.client.crypto!);
if (lastReplacement?.shouldAttemptDecryption() && this.client.getCrypto()) {
// Dirty but we are expecting to pass the cryptoBackend which is not accessible here
await lastReplacement.attemptDecryption(this.client.getCrypto() as CryptoBackend);
} else if (lastReplacement?.isBeingDecrypted()) {
await lastReplacement.getDecryptionPromise();
}
+2 -30
View File
@@ -194,22 +194,10 @@ 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();
@@ -420,7 +408,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
* Fires {@link RoomStateEvent.Events}
* Fires {@link RoomStateEvent.Marker}
*/
public setStateEvents(stateEvents: MatrixEvent[], options?: IMarkerFoundOptions): void {
public setStateEvents(stateEvents: MatrixEvent[], markerFoundOptions?: IMarkerFoundOptions): void {
this.updateModifiedTime();
// update the core event dict
@@ -432,22 +420,6 @@ 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 ?? "");
@@ -504,7 +476,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, options);
this.emit(RoomStateEvent.Marker, event, markerFoundOptions);
}
});
+20 -8
View File
@@ -545,7 +545,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
* @returns Signals when all events have been decrypted
*/
public async decryptCriticalEvents(): Promise<void> {
if (!this.client.isCryptoEnabled()) return;
if (!this.client.getCrypto()) return;
const readReceiptEventId = this.getEventReadUpTo(this.client.getUserId()!, true);
const events = this.getLiveTimeline().getEvents();
@@ -567,7 +567,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
* @returns Signals when all events have been decrypted
*/
public async decryptAllEvents(): Promise<void> {
if (!this.client.isCryptoEnabled()) return;
if (!this.client.getCrypto()) return;
const decryptionPromises = this.getUnfilteredTimelineSet()
.getLiveTimeline()
@@ -1739,10 +1739,11 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
public addEventsToTimeline(
events: MatrixEvent[],
toStartOfTimeline: boolean,
addToState: boolean,
timeline: EventTimeline,
paginationToken?: string,
): void {
timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken);
timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, addToState, timeline, paginationToken);
}
/**
@@ -1907,7 +1908,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
// see https://github.com/vector-im/vector-web/issues/2109
unfilteredLiveTimeline.getEvents().forEach(function (event) {
timelineSet.addLiveEvent(event);
timelineSet.addLiveEvent(event, { addToState: false }); // Filtered timeline sets should not track state
});
// find the earliest unfiltered timeline
@@ -1994,6 +1995,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
if (filterType !== ThreadFilterType.My || currentUserParticipated) {
timelineSet.getLiveTimeline().addEvent(thread.rootEvent!, {
toStartOfTimeline: false,
addToState: false,
});
}
});
@@ -2068,6 +2070,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
const opts = {
duplicateStrategy: DuplicateStrategy.Ignore,
fromCache: false,
addToState: false,
roomState,
};
this.threadsTimelineSets[0]?.addLiveEvent(rootEvent, opts);
@@ -2190,6 +2193,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
duplicateStrategy: DuplicateStrategy.Replace,
fromCache: false,
roomState,
addToState: false,
});
}
}
@@ -2381,9 +2385,13 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
duplicateStrategy: DuplicateStrategy.Replace,
fromCache: false,
roomState: this.currentState,
addToState: false,
});
} else {
timelineSet.addEventToTimeline(thread.rootEvent, timelineSet.getLiveTimeline(), { toStartOfTimeline });
timelineSet.addEventToTimeline(thread.rootEvent, timelineSet.getLiveTimeline(), {
toStartOfTimeline,
addToState: false,
});
}
}
};
@@ -2540,7 +2548,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
* Fires {@link RoomEvent.Timeline}
*/
private addLiveEvent(event: MatrixEvent, addLiveEventOptions: IAddLiveEventOptions): void {
const { duplicateStrategy, timelineWasEmpty, fromCache } = addLiveEventOptions;
const { duplicateStrategy, timelineWasEmpty, fromCache, addToState } = addLiveEventOptions;
// add to our timeline sets
for (const timelineSet of this.timelineSets) {
@@ -2548,6 +2556,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
duplicateStrategy,
fromCache,
timelineWasEmpty,
addToState,
});
}
@@ -2631,11 +2640,13 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
if (timelineSet.getFilter()!.filterRoomTimeline([event]).length) {
timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), {
toStartOfTimeline: false,
addToState: false, // We don't support localEcho of state events yet
});
}
} else {
timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), {
toStartOfTimeline: false,
addToState: false, // We don't support localEcho of state events yet
});
}
}
@@ -2886,8 +2897,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
* @param addLiveEventOptions - addLiveEvent options
* @throws If `duplicateStrategy` is not falsey, 'replace' or 'ignore'.
*/
public async addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): Promise<void> {
const { duplicateStrategy, fromCache, timelineWasEmpty = false } = addLiveEventOptions ?? {};
public async addLiveEvents(events: MatrixEvent[], addLiveEventOptions: IAddLiveEventOptions): Promise<void> {
const { duplicateStrategy, fromCache, timelineWasEmpty = false, addToState } = addLiveEventOptions;
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
}
@@ -2902,6 +2913,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
duplicateStrategy,
fromCache,
timelineWasEmpty,
addToState,
};
// List of extra events to check for being parents of any relations encountered
+4 -2
View File
@@ -208,6 +208,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
public static setServerSideSupport(status: FeatureSupport): void {
Thread.hasServerSideSupport = status;
// XXX: This global latching behaviour is really unexpected and means that you can't undo when moving to a server without support
if (status !== FeatureSupport.Stable) {
FILTER_RELATED_BY_SENDERS.setPreferUnstable(true);
FILTER_RELATED_BY_REL_TYPES.setPreferUnstable(true);
@@ -317,6 +318,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
toStartOfTimeline,
fromCache: false,
roomState: this.roomState,
addToState: false,
});
}
}
@@ -343,7 +345,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
if (this.findEventById(eventId)) {
return;
}
this.timelineSet.insertEventIntoTimeline(event, this.liveTimeline, this.roomState);
this.timelineSet.insertEventIntoTimeline(event, this.liveTimeline, this.roomState, false);
}
public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
@@ -618,7 +620,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
// if the thread has regular events, this will just load the last reply.
// if the thread is newly created, this will load the root event.
if (this.replyCount === 0 && this.rootEvent) {
this.timelineSet.addEventsToTimeline([this.rootEvent], true, this.liveTimeline, null);
this.timelineSet.addEventsToTimeline([this.rootEvent], true, false, this.liveTimeline, null);
this.liveTimeline.setPaginationToken(null, Direction.Backward);
} else {
this.initalEventFetchProm = this.client.paginateEventTimeline(this.liveTimeline, {
+2 -1
View File
@@ -179,7 +179,8 @@ export const validateIdToken = (
* The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, or if it contains additional audiences not trusted by the Client.
* EW: Don't accept tokens with other untrusted audiences
* */
if (claims.aud !== clientId) {
const sanitisedAuds = typeof claims.aud === "string" ? [claims.aud] : claims.aud;
if (!sanitisedAuds.includes(clientId)) {
throw new Error("Invalid audience");
}
+1 -1
View File
@@ -30,7 +30,6 @@ import {
} from "../crypto-api/keybackup.ts";
import { logger } from "../logger.ts";
import { ClientPrefix, IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api/index.ts";
import { IMegolmSessionData } from "../crypto/index.ts";
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
import { encodeUri, logDuration } from "../utils.ts";
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor.ts";
@@ -38,6 +37,7 @@ import { sleep } from "../utils.ts";
import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts";
import { ImportRoomKeyProgressData, ImportRoomKeysOpts, CryptoEvent } from "../crypto-api/index.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
import { IMegolmSessionData } from "../@types/crypto.ts";
/** Authentification of the backup info, depends on algorithm */
type AuthData = KeyBackupInfo["auth_data"];
+7 -2
View File
@@ -21,7 +21,6 @@ import { CryptoStore, MigrationState, SecretStorePrivateKeys } from "../crypto/s
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store.ts";
import { IHttpOpts, MatrixHttpApi } from "../http-api/index.ts";
import { requestKeyBackupVersion } from "./backup.ts";
import { IRoomEncryption } from "../crypto/RoomList.ts";
import { CrossSigningKeyInfo, Curve25519AuthData } from "../crypto-api/index.ts";
import { RustCrypto } from "./rust-crypto.ts";
import { KeyBackupInfo } from "../crypto-api/keybackup.ts";
@@ -30,6 +29,12 @@ import { encodeBase64 } from "../base64.ts";
import decryptAESSecretStorageItem from "../utils/decryptAESSecretStorageItem.ts";
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
interface LegacyRoomEncryption {
algorithm: string;
rotation_period_ms?: number;
rotation_period_msgs?: number;
}
/**
* Determine if any data needs migrating from the legacy store, and do so.
*
@@ -375,7 +380,7 @@ export async function migrateRoomSettingsFromLegacyCrypto({
return;
}
let rooms: Record<string, IRoomEncryption> = {};
let rooms: Record<string, LegacyRoomEncryption> = {};
await legacyStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
legacyStore.getEndToEndRooms(txn, (result) => {
+66 -26
View File
@@ -71,7 +71,7 @@ import {
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter.ts";
import { IDownloadKeyResult, IQueryKeysRequest } from "../client.ts";
import { Device, DeviceMap } from "../models/device.ts";
import { SECRET_STORAGE_ALGORITHM_V1_AES, ServerSideSecretStorage } from "../secret-storage.ts";
import { SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorageKey, ServerSideSecretStorage } from "../secret-storage.ts";
import { CrossSigningIdentity } from "./CrossSigningIdentity.ts";
import { secretStorageCanAccessSecrets, secretStorageContainsCrossSigningKeys } from "./secret-storage.ts";
import { isVerificationEvent, RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification.ts";
@@ -329,7 +329,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
/**
* Implementation of {@link CryptoBackend#getBackupDecryptor}.
*/
public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: ArrayLike<number>): Promise<BackupDecryptor> {
public async getBackupDecryptor(backupInfo: KeyBackupInfo, privKey: Uint8Array): Promise<BackupDecryptor> {
if (!(privKey instanceof Uint8Array)) {
throw new Error(`getBackupDecryptor: expects Uint8Array`);
}
@@ -653,7 +653,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* Implementation of {@link CryptoApi#getUserVerificationStatus}.
*/
public async getUserVerificationStatus(userId: string): Promise<UserVerificationStatus> {
const userIdentity: RustSdkCryptoJs.UserIdentity | RustSdkCryptoJs.OwnUserIdentity | undefined =
const userIdentity: RustSdkCryptoJs.OtherUserIdentity | RustSdkCryptoJs.OwnUserIdentity | undefined =
await this.getOlmMachineOrThrow().getIdentity(new RustSdkCryptoJs.UserId(userId));
if (userIdentity === undefined) {
return new UserVerificationStatus(false, false, false);
@@ -662,7 +662,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
const verified = userIdentity.isVerified();
const wasVerified = userIdentity.wasPreviouslyVerified();
const needsUserApproval =
userIdentity instanceof RustSdkCryptoJs.UserIdentity ? userIdentity.identityNeedsUserApproval() : false;
userIdentity instanceof RustSdkCryptoJs.OtherUserIdentity
? userIdentity.identityNeedsUserApproval()
: false;
userIdentity.free();
return new UserVerificationStatus(verified, wasVerified, false, needsUserApproval);
}
@@ -671,7 +673,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* Implementation of {@link CryptoApi#pinCurrentUserIdentity}.
*/
public async pinCurrentUserIdentity(userId: string): Promise<void> {
const userIdentity: RustSdkCryptoJs.UserIdentity | RustSdkCryptoJs.OwnUserIdentity | undefined =
const userIdentity: RustSdkCryptoJs.OtherUserIdentity | RustSdkCryptoJs.OwnUserIdentity | undefined =
await this.getOlmMachineOrThrow().getIdentity(new RustSdkCryptoJs.UserId(userId));
if (userIdentity === undefined) {
@@ -768,7 +770,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
*/
public async isSecretStorageReady(): Promise<boolean> {
// make sure that the cross-signing keys are stored
const secretsToCheck = [
const secretsToCheck: SecretStorageKey[] = [
"m.cross_signing.master",
"m.cross_signing.user_signing",
"m.cross_signing.self_signing",
@@ -841,11 +843,42 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
await this.secretStorage.store("m.cross_signing.self_signing", crossSigningPrivateKeys.self_signing_key);
}
if (setupNewKeyBackup) {
// likewise with the key backup key: if we have one, store it in secret storage (if it's not already there)
// also don't bother storing it if we're about to set up a new backup
if (!setupNewKeyBackup) {
await this.saveBackupKeyToStorage();
} else {
await this.resetKeyBackup();
}
}
/**
* If we have a backup key for the current, trusted backup in cache,
* save it to secret storage.
*/
private async saveBackupKeyToStorage(): Promise<void> {
const keyBackupInfo = await this.backupManager.getServerBackupInfo();
if (!keyBackupInfo || !keyBackupInfo.version) {
logger.info("Not saving backup key to secret storage: no backup info");
return;
}
const backupKeys: RustSdkCryptoJs.BackupKeys = await this.olmMachine.getBackupKeys();
if (!backupKeys.decryptionKey) {
logger.info("Not saving backup key to secret storage: no backup key");
return;
}
if (!decryptionKeyMatchesKeyBackupInfo(backupKeys.decryptionKey, keyBackupInfo)) {
logger.info("Not saving backup key to secret storage: decryption key does not match backup info");
return;
}
const backupKeyBase64 = backupKeys.decryptionKey.toBase64();
await this.secretStorage.store("m.megolm_backup.v1", backupKeyBase64);
}
/**
* Add the secretStorage key to the secret storage
* - The secret storage key must have the `keyInfo` field filled
@@ -1020,7 +1053,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* Implementation of {@link CryptoApi#requestVerificationDM}
*/
public async requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
const userIdentity: RustSdkCryptoJs.UserIdentity | undefined = await this.olmMachine.getIdentity(
const userIdentity: RustSdkCryptoJs.OtherUserIdentity | undefined = await this.olmMachine.getIdentity(
new RustSdkCryptoJs.UserId(userId),
);
@@ -1178,7 +1211,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
public async getSessionBackupPrivateKey(): Promise<Uint8Array | null> {
const backupKeys: RustSdkCryptoJs.BackupKeys = await this.olmMachine.getBackupKeys();
if (!backupKeys.decryptionKey) return null;
return Buffer.from(backupKeys.decryptionKey.toBase64(), "base64");
return decodeBase64(backupKeys.decryptionKey.toBase64());
}
/**
@@ -2035,7 +2068,7 @@ class EventDecryptor {
errorDetails,
);
case RustSdkCryptoJs.DecryptionErrorCode.SenderIdentityPreviouslyVerified:
case RustSdkCryptoJs.DecryptionErrorCode.SenderIdentityVerificationViolation:
// We're refusing to decrypt due to not trusting the sender,
// rather than failing to decrypt due to lack of keys, so we
// don't need to keep it on the pending list.
@@ -2180,22 +2213,29 @@ function rustEncryptionInfoToJsEncryptionInfo(
}
let shieldReason: EventShieldReason | null;
if (shieldState.message === undefined) {
shieldReason = null;
} else if (shieldState.message === "Encrypted by an unverified user.") {
// this case isn't actually used with lax shield semantics.
shieldReason = EventShieldReason.UNVERIFIED_IDENTITY;
} else if (shieldState.message === "Encrypted by a device not verified by its owner.") {
shieldReason = EventShieldReason.UNSIGNED_DEVICE;
} else if (
shieldState.message === "The authenticity of this encrypted message can't be guaranteed on this device."
) {
shieldReason = EventShieldReason.AUTHENTICITY_NOT_GUARANTEED;
} else if (shieldState.message === "Encrypted by an unknown or deleted device.") {
shieldReason = EventShieldReason.UNKNOWN_DEVICE;
} else {
logger.warn(`Unknown shield state message '${shieldState.message}'`);
shieldReason = EventShieldReason.UNKNOWN;
switch (shieldState.code) {
case undefined:
case null:
shieldReason = null;
break;
case RustSdkCryptoJs.ShieldStateCode.AuthenticityNotGuaranteed:
shieldReason = EventShieldReason.AUTHENTICITY_NOT_GUARANTEED;
break;
case RustSdkCryptoJs.ShieldStateCode.UnknownDevice:
shieldReason = EventShieldReason.UNKNOWN_DEVICE;
break;
case RustSdkCryptoJs.ShieldStateCode.UnsignedDevice:
shieldReason = EventShieldReason.UNSIGNED_DEVICE;
break;
case RustSdkCryptoJs.ShieldStateCode.UnverifiedIdentity:
shieldReason = EventShieldReason.UNVERIFIED_IDENTITY;
break;
case RustSdkCryptoJs.ShieldStateCode.SentInClear:
shieldReason = EventShieldReason.SENT_IN_CLEAR;
break;
case RustSdkCryptoJs.ShieldStateCode.VerificationViolation:
shieldReason = EventShieldReason.VERIFICATION_VIOLATION;
break;
}
return { shieldColour, shieldReason };
+2 -2
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ServerSideSecretStorage } from "../secret-storage.ts";
import { SecretStorageKey, ServerSideSecretStorage } from "../secret-storage.ts";
/**
* Check that the private cross signing keys (master, self signing, user signing) are stored in the secret storage and encrypted with the default secret storage key.
@@ -44,7 +44,7 @@ export async function secretStorageContainsCrossSigningKeys(secretStorage: Serve
*/
export async function secretStorageCanAccessSecrets(
secretStorage: ServerSideSecretStorage,
secretNames: string[],
secretNames: SecretStorageKey[],
): Promise<boolean> {
const defaultKeyId = await secretStorage.getDefaultKeyId();
if (!defaultKeyId) return false;

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