Compare commits
60 Commits
v34.13.0-rc.0
...
v36.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 72ee5504d5 | |||
| 38816589f5 | |||
| 6f743bfa1f | |||
| ffd3c9575e | |||
| 7678923e04 | |||
| 6f7c74f9ea | |||
| 3fcc56601b | |||
| bcf3d56bd5 | |||
| 647a2a1a19 | |||
| 1bf8533c03 | |||
| fb2f2dd6d4 | |||
| d33350ff26 | |||
| 349a86c119 | |||
| bee65ff13f | |||
| e4182eb752 | |||
| 3219aefc92 | |||
| aba4e690af | |||
| 693bb22ba1 | |||
| 315e81b7de | |||
| a0502c5ee5 | |||
| d1de32ea27 | |||
| 3ae25427a8 | |||
| 8155b0acfc | |||
| 413c156624 | |||
| e78a3cec9f | |||
| 5998de365d | |||
| 3de2b9bf80 | |||
| ded87290ce | |||
| cf39595bd7 | |||
| c90ea985c6 | |||
| c54ca29aa8 | |||
| beb3721e7a | |||
| 1cad6f4451 | |||
| c4ea57d42d | |||
| d3f5526ec0 | |||
| b8e332b53d | |||
| ab78acc7da | |||
| 051f4e2ab9 | |||
| 8863e42e35 | |||
| bc5246970c | |||
| edac6a9983 | |||
| 97ef1dc6df | |||
| 125e45c24d | |||
| 3781b6ebfa | |||
| 8fc77c595a | |||
| 5bcd26e506 | |||
| 66f099b2e7 | |||
| b1445d7457 | |||
| b3794760c6 | |||
| fbdcc597b6 | |||
| 8ac32b7894 | |||
| b27c6de78d | |||
| c8e0987774 | |||
| 849fcd3341 | |||
| 8df8266e4a | |||
| cba4edfd84 | |||
| 4f50290cd6 | |||
| 35e31f02ac | |||
| 69647a33b6 | |||
| 006929ac0c |
@@ -138,6 +138,7 @@ module.exports = {
|
||||
tryExtensions: [".ts"],
|
||||
},
|
||||
],
|
||||
"no-extra-boolean-cast": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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)).
|
||||
|
||||
@@ -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]'
|
||||
|
||||
@@ -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!`);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)!;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 () {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()]);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -106,7 +106,7 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
mockClient,
|
||||
);
|
||||
|
||||
room.addLiveEvents([event]);
|
||||
room.addLiveEvents([event], { addToState: false });
|
||||
|
||||
THREAD_ID = event.getId()!;
|
||||
threadEvent = mkEvent({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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", () => {
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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: {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Vendored
+21
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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} */
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {};
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()}`);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user