Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec137cb5fb | |||
| a696e77652 | |||
| 582a76d87c | |||
| fdfddde55a | |||
| 0ecfef2352 | |||
| 4fdece6c1c | |||
| 6f0bce8708 | |||
| d3bdeb73f5 | |||
| 942fdf5bee | |||
| dd2635dbe6 | |||
| 3d1bcb73c1 | |||
| a960e686b3 | |||
| 946774c3fb | |||
| 15edbc8067 | |||
| 1398ac24a2 | |||
| c76df4cd8f | |||
| a5e4dbf2d3 | |||
| 3768187395 | |||
| 08d0ce25f1 | |||
| 23241f18e2 | |||
| 90da67aa95 | |||
| 0bf2702149 | |||
| c7a75c8824 | |||
| 98b2b9745d | |||
| 65d5b3172c | |||
| 2f72f9e889 | |||
| 18f500a1f8 | |||
| b1df58796a | |||
| 761b3771d6 | |||
| df88edfda0 | |||
| 1dee1ba581 | |||
| b274c74a30 | |||
| b489bb15cf | |||
| dff4922a42 | |||
| dc6ad0b54c | |||
| 9769c05dc5 | |||
| dd379d3d4c | |||
| 1b884a3e52 | |||
| ddb164490e | |||
| 4cc4c01dd8 | |||
| 0ae483ce27 | |||
| dbc1fa87ed | |||
| 0a3675b971 | |||
| ab3f529d29 | |||
| 607b712a07 | |||
| b6d9e49277 | |||
| 731d5943e2 | |||
| 01e7a43593 |
@@ -41,3 +41,6 @@
|
||||
- name: "Z-Flaky-Test"
|
||||
description: "A test is raising false alarms"
|
||||
color: "ededed"
|
||||
- name: "Z-Skip-Coverage"
|
||||
description: "Skip SonarQube coverage for this PR"
|
||||
color: "ededed"
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
deployments: write
|
||||
steps:
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -73,6 +73,8 @@ jobs:
|
||||
close-if-fork-develop:
|
||||
name: Forbid develop branch fork contributions
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
pull-requests: write
|
||||
if: >
|
||||
github.event.action == 'opened' &&
|
||||
github.event.pull_request.head.ref == 'develop' &&
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
with:
|
||||
ref: staging
|
||||
fetch-depth: 0
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
disable-autolabeler: true
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
|
||||
@@ -18,14 +18,14 @@ jobs:
|
||||
merge:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
with:
|
||||
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
|
||||
@@ -4,8 +4,6 @@ on:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
NPM_TOKEN:
|
||||
required: false
|
||||
GPG_PASSPHRASE:
|
||||
required: false
|
||||
GPG_PRIVATE_KEY:
|
||||
@@ -73,7 +71,7 @@ jobs:
|
||||
draft: true
|
||||
latest: true
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
with:
|
||||
ref: staging
|
||||
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
||||
@@ -81,7 +79,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
@@ -282,8 +280,6 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
post-release:
|
||||
name: Post release steps
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
name: Publish to npm
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
outputs:
|
||||
id:
|
||||
description: "The npm package@version string we published"
|
||||
@@ -20,7 +17,7 @@ jobs:
|
||||
id: ${{ steps.npm-publish.outputs.id }}
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
with:
|
||||
ref: staging
|
||||
|
||||
@@ -31,21 +28,18 @@ jobs:
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
node-version-file: package.json
|
||||
|
||||
# Ensure npm 11.5.1 or later is installed
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: 🚀 Publish to npm
|
||||
id: npm-publish
|
||||
run: |
|
||||
npm publish --provenance --access public --tag next
|
||||
npm publish --provenance --access public --tag "$TAG"
|
||||
release=$(jq -r '"\(.name)@\(.version)"' package.json)
|
||||
echo "id=$release" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: 🎖️ Add `latest` dist-tag to final releases
|
||||
if: steps.npm-publish.outputs.id && !contains(steps.npm-publish.outputs.id, '-rc.')
|
||||
run: npm dist-tag add "$release" latest
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
release: ${{ steps.npm-publish.outputs.id }}
|
||||
TAG: ${{ contains(steps.npm-publish.outputs.id, '-rc.') && 'next' || 'latest' }}
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
repo:
|
||||
- element-hq/element-web
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
with:
|
||||
repository: ${{ matrix.repo }}
|
||||
ref: staging
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: "🧮 Checkout code"
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
|
||||
if: ${{ !inputs.sharded }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
name: coverage
|
||||
path: coverage
|
||||
- name: 📥 Download sharded artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
|
||||
if: inputs.sharded
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -75,13 +75,14 @@ jobs:
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@820f7c2e9e94ba9e35add0f739691e5c7e23fa25 # v4.0
|
||||
uses: matrix-org/sonarcloud-workflow-action@035ca8a2a741dd940f2e6c16d7d078312d625ac6 # v4.0
|
||||
# workflow_run fails report against the develop commit always, we don't want that for PRs
|
||||
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
|
||||
with:
|
||||
skip_checkout: true
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
skip_coverage_label: Z-Skip-Coverage
|
||||
version_cmd: "cat package.json | jq -r .version"
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
name: "Node.js example"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
name: "Workflow Lint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
run: "yarn run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
||||
with:
|
||||
name: docs
|
||||
path: _docs
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
if: github.event_name == 'merge_group'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
with:
|
||||
repository: element-hq/element-web
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
node: ["lts/*", 22]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- name: Setup Node
|
||||
id: setupNode
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
|
||||
- name: Upload Artifact
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
||||
with:
|
||||
name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }}
|
||||
path: |
|
||||
|
||||
@@ -1,3 +1,45 @@
|
||||
Changes in [39.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.4.0) (2025-12-16)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Import room key bundles received after invite. ([#5080](https://github.com/matrix-org/matrix-js-sdk/pull/5080)). Contributed by @kaylendog.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Allow msc4354\_sticky\_key to be optional on sticky events. ([#5073](https://github.com/matrix-org/matrix-js-sdk/pull/5073)). Contributed by @Half-Shot.
|
||||
* Handle all response fields from /context API being optional ([#5089](https://github.com/matrix-org/matrix-js-sdk/pull/5089)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [39.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.3.0) (2025-12-02)
|
||||
==================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Re-add truthy check on room name/avatar/alias events ([#5081](https://github.com/matrix-org/matrix-js-sdk/pull/5081)). Contributed by @t3chguy.
|
||||
* Fix invalid state events corrupting room objects ([#5078](https://github.com/matrix-org/matrix-js-sdk/pull/5078)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [39.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.2.0) (2025-11-18)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Delayed event management: split endpoints, no auth ([#5066](https://github.com/matrix-org/matrix-js-sdk/pull/5066)). Contributed by @AndrewFerr.
|
||||
* do not set cache in authenticated fetch ([#5020](https://github.com/matrix-org/matrix-js-sdk/pull/5020)). Contributed by @pkuzco.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix media switching during legacy calls ([#5069](https://github.com/matrix-org/matrix-js-sdk/pull/5069)). Contributed by @langleyd.
|
||||
|
||||
|
||||
Changes in [39.1.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.1.2) (2025-11-04)
|
||||
==================================================================================================
|
||||
Re-release of v39.1.0 to fix npm publishing workflow
|
||||
|
||||
|
||||
|
||||
Changes in [39.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.1.1) (2025-11-04)
|
||||
==================================================================================================
|
||||
Re-release of v39.1.0 to fix npm publishing workflow
|
||||
|
||||
Changes in [39.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v39.1.0) (2025-11-04)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
+3
-4
@@ -71,7 +71,7 @@ Unless otherwise specified, the following applies to all code:
|
||||
11. If a variable is not receiving a value on declaration, its type must be defined.
|
||||
|
||||
```typescript
|
||||
let errorMessage: Optional<string>;
|
||||
let errorMessage: string;
|
||||
```
|
||||
|
||||
12. Objects can use shorthand declarations, including mixing of types.
|
||||
@@ -150,8 +150,7 @@ Unless otherwise specified, the following applies to all code:
|
||||
1. When using `any`, a comment explaining why must be present.
|
||||
27. `import` should be used instead of `require`, as `require` does not have types.
|
||||
28. Export only what can be reused.
|
||||
29. Prefer a type like `Optional<X>` (`type Optional<T> = T | null | undefined`) instead
|
||||
of truly optional parameters.
|
||||
29. Prefer a type like `X | null` instead of truly optional parameters.
|
||||
1. A notable exception is when the likelihood of a bug is minimal, such as when a function
|
||||
takes an argument that is more often not required than required. An example where the
|
||||
`?` operator is inappropriate is when taking a room ID: typically the caller should
|
||||
@@ -161,7 +160,7 @@ Unless otherwise specified, the following applies to all code:
|
||||
```typescript
|
||||
function doThingWithRoom(
|
||||
thing: string,
|
||||
room: Optional<string>, // require the caller to specify
|
||||
room: string | null, // require the caller to specify
|
||||
) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ export default {
|
||||
"ts-node",
|
||||
// Used by `@babel/plugin-transform-runtime`
|
||||
"@babel/runtime",
|
||||
// Transitive dep of jest
|
||||
"jsdom",
|
||||
],
|
||||
ignoreBinaries: [
|
||||
// Used when available by reusable workflow `.github/workflows/release-make.yml`
|
||||
|
||||
+9
-9
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "39.1.0",
|
||||
"version": "39.4.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
@@ -56,10 +56,10 @@
|
||||
"jwt-decode": "^4.0.0",
|
||||
"loglevel": "^1.9.2",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.10.0",
|
||||
"matrix-widget-api": "^1.14.0",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
"p-retry": "7",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"sdp-transform": "^3.0.0",
|
||||
"unhomoglyph": "^1.0.6",
|
||||
"uuid": "13"
|
||||
},
|
||||
@@ -84,12 +84,12 @@
|
||||
"@stylistic/eslint-plugin": "^5.0.0",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "18",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
"babel-jest": "^30.0.0",
|
||||
"babel-plugin-search-and-replace": "^1.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"eslint": "8.57.1",
|
||||
@@ -101,16 +101,16 @@
|
||||
"eslint-plugin-jsdoc": "^61.0.0",
|
||||
"eslint-plugin-matrix-org": "^3.0.0",
|
||||
"eslint-plugin-n": "^14.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.4.0",
|
||||
"eslint-plugin-tsdoc": "^0.5.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"fake-indexeddb": "^5.0.2",
|
||||
"fetch-mock": "11.1.5",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"husky": "^9.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^29.0.0",
|
||||
"jest": "^30.0.0",
|
||||
"jest-environment-jsdom": "^30.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^29.0.0",
|
||||
"jest-mock": "^30.0.0",
|
||||
"knip": "^5.0.0",
|
||||
"lint-staged": "^16.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver.ts";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder.ts";
|
||||
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints.ts";
|
||||
import { getSyncResponse, mkEventCustom, syncPromise } from "../../test-utils/test-utils.ts";
|
||||
import { getSyncResponse, mkEventCustom, syncPromise, waitFor } from "../../test-utils/test-utils.ts";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder.ts";
|
||||
import { flushPromises } from "../../test-utils/flushPromises.ts";
|
||||
import { E2EOTKClaimResponder } from "../../test-utils/E2EOTKClaimResponder.ts";
|
||||
@@ -80,6 +80,9 @@ describe("History Sharing", () => {
|
||||
let bobSyncResponder: SyncResponder;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Reset mocks.
|
||||
fetchMock.reset();
|
||||
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
@@ -201,6 +204,104 @@ describe("History Sharing", () => {
|
||||
expect(event.getContent().body).toEqual("Hi!");
|
||||
});
|
||||
|
||||
test("Room keys are imported correctly if invite is accepted before the bundle arrives", async () => {
|
||||
// Alice is in an encrypted room
|
||||
const syncResponse = getSyncResponse([aliceClient.getSafeUserId()], ROOM_ID);
|
||||
aliceSyncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// ... and she sends an event
|
||||
const msgProm = expectSendRoomEvent(ALICE_HOMESERVER_URL, "m.room.encrypted");
|
||||
await aliceClient.sendEvent(ROOM_ID, EventType.RoomMessage, { msgtype: MsgType.Text, body: "Hello!" });
|
||||
const sentMessage = await msgProm;
|
||||
debug(`Alice sent encrypted room event: ${JSON.stringify(sentMessage)}`);
|
||||
|
||||
// Now, Alice invites Bob
|
||||
const uploadProm = new Promise<Uint8Array>((resolve) => {
|
||||
fetchMock.postOnce(new URL("/_matrix/media/v3/upload", ALICE_HOMESERVER_URL).toString(), (url, request) => {
|
||||
const body = request.body as Uint8Array;
|
||||
debug(`Alice uploaded blob of length ${body.length}`);
|
||||
resolve(body);
|
||||
return { content_uri: "mxc://alice-server/here" };
|
||||
});
|
||||
});
|
||||
const toDeviceMessageProm = expectSendToDeviceMessage(ALICE_HOMESERVER_URL, "m.room.encrypted");
|
||||
// POST https://alice-server.com/_matrix/client/v3/rooms/!room%3Aexample.com/invite
|
||||
fetchMock.postOnce(`${ALICE_HOMESERVER_URL}/_matrix/client/v3/rooms/${encodeURIComponent(ROOM_ID)}/invite`, {});
|
||||
await aliceClient.invite(ROOM_ID, bobClient.getSafeUserId(), { shareEncryptedHistory: true });
|
||||
const uploadedBlob = await uploadProm;
|
||||
const sentToDeviceRequest = await toDeviceMessageProm;
|
||||
debug(`Alice sent encrypted to-device events: ${JSON.stringify(sentToDeviceRequest)}`);
|
||||
const bobToDeviceMessage = sentToDeviceRequest[bobClient.getSafeUserId()][bobClient.deviceId!];
|
||||
expect(bobToDeviceMessage).toBeDefined();
|
||||
|
||||
// Bob receives the room invite, but not the room key bundle
|
||||
const inviteEvent = mkEventCustom({
|
||||
type: "m.room.member",
|
||||
sender: aliceClient.getSafeUserId(),
|
||||
state_key: bobClient.getSafeUserId(),
|
||||
content: { membership: KnownMembership.Invite },
|
||||
});
|
||||
bobSyncResponder.sendOrQueueSyncResponse({
|
||||
rooms: { invite: { [ROOM_ID]: { invite_state: { events: [inviteEvent] } } } },
|
||||
});
|
||||
await syncPromise(bobClient);
|
||||
|
||||
const room = bobClient.getRoom(ROOM_ID);
|
||||
expect(room).toBeTruthy();
|
||||
expect(room?.getMyMembership()).toEqual(KnownMembership.Invite);
|
||||
|
||||
fetchMock.postOnce(`${BOB_HOMESERVER_URL}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
await bobClient.joinRoom(ROOM_ID, { acceptSharedHistory: true });
|
||||
|
||||
// Bob receives and attempts to decrypt the megolm message, but should not be able to (yet).
|
||||
const bobSyncResponse = getSyncResponse([aliceClient.getSafeUserId(), bobClient.getSafeUserId()], ROOM_ID);
|
||||
bobSyncResponse.rooms.join[ROOM_ID].timeline.events.push(
|
||||
mkEventCustom({
|
||||
type: "m.room.encrypted",
|
||||
sender: aliceClient.getSafeUserId(),
|
||||
content: sentMessage,
|
||||
event_id: "$event_id",
|
||||
}) as any,
|
||||
);
|
||||
bobSyncResponder.sendOrQueueSyncResponse(bobSyncResponse);
|
||||
await syncPromise(bobClient);
|
||||
const bobRoom = bobClient.getRoom(ROOM_ID);
|
||||
const event = bobRoom!.getLastLiveEvent()!;
|
||||
expect(event.getId()).toEqual("$event_id");
|
||||
await event.getDecryptionPromise();
|
||||
expect(event.isDecryptionFailure()).toBeTruthy();
|
||||
|
||||
// Now the room key bundle message arrives
|
||||
fetchMock.getOnce(
|
||||
`begin:${BOB_HOMESERVER_URL}/_matrix/client/v1/media/download/alice-server/here`,
|
||||
{ body: uploadedBlob },
|
||||
{ sendAsJson: false },
|
||||
);
|
||||
bobSyncResponder.sendOrQueueSyncResponse({
|
||||
to_device: {
|
||||
events: [
|
||||
{
|
||||
type: "m.room.encrypted",
|
||||
sender: aliceClient.getSafeUserId(),
|
||||
content: bobToDeviceMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await syncPromise(bobClient);
|
||||
|
||||
// Once the room key bundle finishes downloading, we should be able to decrypt the message.
|
||||
await waitFor(async () => {
|
||||
await event.getDecryptionPromise();
|
||||
expect(event.isDecryptionFailure()).toBeFalsy();
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent().body).toEqual("Hello!");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
bobClient.stopClient();
|
||||
aliceClient.stopClient();
|
||||
|
||||
@@ -305,7 +305,9 @@ export function encryptMegolmEventRawPlainText(opts: {
|
||||
},
|
||||
type: "m.room.encrypted",
|
||||
unsigned: {},
|
||||
state_key: opts.plaintext.state_key ? `${opts.plaintext.type}:${opts.plaintext.state_key}` : undefined,
|
||||
state_key: opts.plaintext.hasOwnProperty("state_key")
|
||||
? `${opts.plaintext.type}:${opts.plaintext.state_key}`
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -672,7 +672,7 @@ describe("MatrixClient event timelines", function () {
|
||||
expect(timeline!.getEvents().find((e) => e.getId() === THREAD_ROOT.event_id!)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return undefined when event is not in the thread that the given timelineSet is representing", () => {
|
||||
it("should return null when event is not in the thread that the given timelineSet is representing", () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
@@ -696,12 +696,12 @@ describe("MatrixClient event timelines", function () {
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id!)).resolves.toBeUndefined(),
|
||||
expect(client.getEventTimeline(timelineSet, EVENTS[0].event_id!)).resolves.toBeNull(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return undefined when event is within a thread but timelineSet is not", () => {
|
||||
it("should return null when event is within a thread but timelineSet is not", () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
@@ -723,7 +723,7 @@ describe("MatrixClient event timelines", function () {
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!)).resolves.toBeUndefined(),
|
||||
expect(client.getEventTimeline(timelineSet, THREAD_REPLY.event_id!)).resolves.toBeNull(),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ export function getSyncResponse(
|
||||
state_key: "",
|
||||
content: {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"io.element.msc3414.encrypt_state_events": encryptStateEvents,
|
||||
"io.element.msc4362.encrypt_state_events": encryptStateEvents,
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
+51
-10
@@ -86,7 +86,9 @@ class MockWidgetApi extends EventEmitter {
|
||||
? { event_id: `$${Math.random()}` }
|
||||
: { delay_id: `id-${Math.random()}` },
|
||||
);
|
||||
public updateDelayedEvent = jest.fn().mockResolvedValue(undefined);
|
||||
public cancelScheduledDelayedEvent = jest.fn().mockResolvedValue(undefined);
|
||||
public restartScheduledDelayedEvent = jest.fn().mockResolvedValue(undefined);
|
||||
public sendScheduledDelayedEvent = jest.fn().mockResolvedValue(undefined);
|
||||
public sendToDevice = jest.fn().mockResolvedValue(undefined);
|
||||
public requestOpenIDConnectToken = jest.fn(async () => {
|
||||
return testOIDCToken;
|
||||
@@ -531,17 +533,49 @@ describe("RoomWidgetClient", () => {
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("updates delayed events", async () => {
|
||||
it.each([UpdateDelayedEventAction.Cancel, UpdateDelayedEventAction.Restart, UpdateDelayedEventAction.Send])(
|
||||
"can %s scheduled delayed events (action in parameter)",
|
||||
async (action: UpdateDelayedEventAction) => {
|
||||
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(
|
||||
MatrixCapabilities.MSC4157UpdateDelayedEvent,
|
||||
);
|
||||
await client._unstable_updateDelayedEvent("id", action);
|
||||
let updateDelayedEvent: (delayId: string) => Promise<unknown>;
|
||||
switch (action) {
|
||||
case UpdateDelayedEventAction.Cancel:
|
||||
updateDelayedEvent = widgetApi.cancelScheduledDelayedEvent;
|
||||
break;
|
||||
case UpdateDelayedEventAction.Restart:
|
||||
updateDelayedEvent = widgetApi.cancelScheduledDelayedEvent;
|
||||
break;
|
||||
case UpdateDelayedEventAction.Send:
|
||||
updateDelayedEvent = widgetApi.sendScheduledDelayedEvent;
|
||||
break;
|
||||
}
|
||||
expect(updateDelayedEvent).toHaveBeenCalledWith("id");
|
||||
},
|
||||
);
|
||||
|
||||
it("can cancel scheduled delayed events (action in method)", async () => {
|
||||
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||
for (const action of [
|
||||
UpdateDelayedEventAction.Cancel,
|
||||
UpdateDelayedEventAction.Restart,
|
||||
UpdateDelayedEventAction.Send,
|
||||
]) {
|
||||
await client._unstable_updateDelayedEvent("id", action);
|
||||
expect(widgetApi.updateDelayedEvent).toHaveBeenCalledWith("id", action);
|
||||
}
|
||||
await client._unstable_cancelScheduledDelayedEvent("id");
|
||||
expect(widgetApi.cancelScheduledDelayedEvent).toHaveBeenCalledWith("id");
|
||||
});
|
||||
|
||||
it("can restart scheduled delayed events (action in method)", async () => {
|
||||
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||
await client._unstable_restartScheduledDelayedEvent("id");
|
||||
expect(widgetApi.restartScheduledDelayedEvent).toHaveBeenCalledWith("id");
|
||||
});
|
||||
|
||||
it("can send scheduled delayed events (action in method)", async () => {
|
||||
await makeClient({ updateDelayedEvents: true, sendEvent: ["org.matrix.rageshake_request"] });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||
await client._unstable_sendScheduledDelayedEvent("id");
|
||||
expect(widgetApi.sendScheduledDelayedEvent).toHaveBeenCalledWith("id");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -583,6 +617,13 @@ describe("RoomWidgetClient", () => {
|
||||
"Server does not support",
|
||||
);
|
||||
}
|
||||
for (const updateDelayedEvent of [
|
||||
client._unstable_cancelScheduledDelayedEvent,
|
||||
client._unstable_restartScheduledDelayedEvent,
|
||||
client._unstable_sendScheduledDelayedEvent,
|
||||
]) {
|
||||
await expect(updateDelayedEvent.call(client, "id")).rejects.toThrow("Server does not support");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`MatrixHttpApi should return expected object from \`getContentUri\` 1`] = `
|
||||
{
|
||||
|
||||
@@ -375,7 +375,7 @@ describe("FetchHttpApi", () => {
|
||||
refreshToken,
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toEqual(
|
||||
unknownTokenErr,
|
||||
);
|
||||
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
|
||||
@@ -397,7 +397,7 @@ describe("FetchHttpApi", () => {
|
||||
refreshToken,
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toEqual(
|
||||
unknownTokenErr,
|
||||
);
|
||||
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
|
||||
|
||||
@@ -801,6 +801,10 @@ describe("MatrixClient", function () {
|
||||
await expect(
|
||||
client._unstable_updateDelayedEvent("anyDelayId", UpdateDelayedEventAction.Send),
|
||||
).rejects.toThrow(errorMessage);
|
||||
|
||||
await expect(client._unstable_cancelScheduledDelayedEvent("anyDelayId")).rejects.toThrow(errorMessage);
|
||||
await expect(client._unstable_restartScheduledDelayedEvent("anyDelayId")).rejects.toThrow(errorMessage);
|
||||
await expect(client._unstable_sendScheduledDelayedEvent("anyDelayId")).rejects.toThrow(errorMessage);
|
||||
});
|
||||
|
||||
it("works with null threadId", async () => {
|
||||
@@ -1077,21 +1081,169 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
});
|
||||
|
||||
it("can update delayed events", async () => {
|
||||
it.each([UpdateDelayedEventAction.Cancel, UpdateDelayedEventAction.Restart, UpdateDelayedEventAction.Send])(
|
||||
"can %s scheduled delayed events (action in request body)",
|
||||
async (action: UpdateDelayedEventAction) => {
|
||||
const delayId = "id";
|
||||
httpLookups = [
|
||||
{
|
||||
method: "POST",
|
||||
prefix: unstableMSC4140Prefix,
|
||||
path: `/delayed_events/${encodeURIComponent(delayId)}`,
|
||||
data: {
|
||||
action,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await client._unstable_updateDelayedEvent(delayId, action);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([UpdateDelayedEventAction.Cancel, UpdateDelayedEventAction.Restart, UpdateDelayedEventAction.Send])(
|
||||
"can %s scheduled delayed events (action in request body fallback when auth required)",
|
||||
async (action: UpdateDelayedEventAction) => {
|
||||
const delayId = "id";
|
||||
const baseLookup = {
|
||||
method: "POST",
|
||||
prefix: unstableMSC4140Prefix,
|
||||
path: `/delayed_events/${encodeURIComponent(delayId)}`,
|
||||
};
|
||||
httpLookups = [
|
||||
{
|
||||
...baseLookup,
|
||||
error: {
|
||||
httpStatus: 401,
|
||||
errcode: "M_MISSING_TOKEN",
|
||||
},
|
||||
},
|
||||
{
|
||||
...baseLookup,
|
||||
data: {
|
||||
action,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await client._unstable_updateDelayedEvent(delayId, action);
|
||||
},
|
||||
);
|
||||
|
||||
it("can cancel scheduled delayed events (action in request path)", async () => {
|
||||
const delayId = "id";
|
||||
const action = UpdateDelayedEventAction.Restart;
|
||||
httpLookups = [
|
||||
{
|
||||
method: "POST",
|
||||
prefix: unstableMSC4140Prefix,
|
||||
path: `/delayed_events/${encodeURIComponent(delayId)}/cancel`,
|
||||
},
|
||||
];
|
||||
|
||||
await client._unstable_cancelScheduledDelayedEvent(delayId);
|
||||
});
|
||||
|
||||
it("can restart scheduled delayed events (action in request path)", async () => {
|
||||
const delayId = "id";
|
||||
httpLookups = [
|
||||
{
|
||||
method: "POST",
|
||||
prefix: unstableMSC4140Prefix,
|
||||
path: `/delayed_events/${encodeURIComponent(delayId)}/restart`,
|
||||
},
|
||||
];
|
||||
|
||||
await client._unstable_restartScheduledDelayedEvent(delayId);
|
||||
});
|
||||
|
||||
it("can send scheduled delayed events (action in request path)", async () => {
|
||||
const delayId = "id";
|
||||
httpLookups = [
|
||||
{
|
||||
method: "POST",
|
||||
prefix: unstableMSC4140Prefix,
|
||||
path: `/delayed_events/${encodeURIComponent(delayId)}/send`,
|
||||
},
|
||||
];
|
||||
|
||||
await client._unstable_sendScheduledDelayedEvent(delayId);
|
||||
});
|
||||
|
||||
it("can cancel scheduled delayed events (action in request path fallback when unsupported)", async () => {
|
||||
const delayId = "id";
|
||||
httpLookups = [
|
||||
{
|
||||
method: "POST",
|
||||
prefix: unstableMSC4140Prefix,
|
||||
path: `/delayed_events/${encodeURIComponent(delayId)}/cancel`,
|
||||
error: {
|
||||
httpStatus: 400,
|
||||
errcode: "M_UNRECOGNIZED",
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
prefix: unstableMSC4140Prefix,
|
||||
path: `/delayed_events/${encodeURIComponent(delayId)}`,
|
||||
data: {
|
||||
action,
|
||||
action: UpdateDelayedEventAction.Cancel,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await client._unstable_updateDelayedEvent(delayId, action);
|
||||
await client._unstable_cancelScheduledDelayedEvent(delayId);
|
||||
expect(httpLookups).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("can restart scheduled delayed events (action in request path fallback when unsupported)", async () => {
|
||||
const delayId = "id";
|
||||
httpLookups = [
|
||||
{
|
||||
method: "POST",
|
||||
prefix: unstableMSC4140Prefix,
|
||||
path: `/delayed_events/${encodeURIComponent(delayId)}/restart`,
|
||||
error: {
|
||||
httpStatus: 400,
|
||||
errcode: "M_UNRECOGNIZED",
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
prefix: unstableMSC4140Prefix,
|
||||
path: `/delayed_events/${encodeURIComponent(delayId)}`,
|
||||
data: {
|
||||
action: UpdateDelayedEventAction.Restart,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await client._unstable_restartScheduledDelayedEvent(delayId);
|
||||
expect(httpLookups).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("can send scheduled delayed events (action in request path fallback when unsupported)", async () => {
|
||||
const delayId = "id";
|
||||
httpLookups = [
|
||||
{
|
||||
method: "POST",
|
||||
prefix: unstableMSC4140Prefix,
|
||||
path: `/delayed_events/${encodeURIComponent(delayId)}/send`,
|
||||
error: {
|
||||
httpStatus: 400,
|
||||
errcode: "M_UNRECOGNIZED",
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
prefix: unstableMSC4140Prefix,
|
||||
path: `/delayed_events/${encodeURIComponent(delayId)}`,
|
||||
data: {
|
||||
action: UpdateDelayedEventAction.Send,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await client._unstable_sendScheduledDelayedEvent(delayId);
|
||||
expect(httpLookups).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -523,6 +523,9 @@ describe("MatrixRTCSession", () => {
|
||||
client.sendEvent = sendEventMock;
|
||||
|
||||
client._unstable_updateDelayedEvent = jest.fn();
|
||||
client._unstable_cancelScheduledDelayedEvent = jest.fn();
|
||||
client._unstable_restartScheduledDelayedEvent = jest.fn();
|
||||
client._unstable_sendScheduledDelayedEvent = jest.fn();
|
||||
|
||||
mockRoom = makeMockRoom([]);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
|
||||
@@ -94,6 +94,9 @@ describe("MembershipManager", () => {
|
||||
// Provide a default mock that is like the default "non error" server behaviour.
|
||||
(client._unstable_sendDelayedStateEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
|
||||
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
||||
(client._unstable_cancelScheduledDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
||||
(client._unstable_sendScheduledDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
||||
(client._unstable_sendStickyEvent as Mock<any>).mockResolvedValue({ event_id: "id" });
|
||||
(client._unstable_sendStickyDelayedEvent as Mock<any>).mockResolvedValue({ delay_id: "id" });
|
||||
(client.sendStateEvent as Mock<any>).mockResolvedValue({ event_id: "id" });
|
||||
@@ -122,7 +125,9 @@ describe("MembershipManager", () => {
|
||||
it("sends a membership event and schedules delayed leave when joining a call", async () => {
|
||||
// Spys/Mocks
|
||||
|
||||
const updateDelayedEventHandle = createAsyncHandle<void>(client._unstable_updateDelayedEvent as Mock);
|
||||
const restartScheduledDelayedEventHandle = createAsyncHandle<void>(
|
||||
client._unstable_restartScheduledDelayedEvent as Mock,
|
||||
);
|
||||
|
||||
// Test
|
||||
const memberManager = new MembershipManager(undefined, room, client, callSession);
|
||||
@@ -143,7 +148,7 @@ describe("MembershipManager", () => {
|
||||
},
|
||||
"_@alice:example.org_AAAAAAA_m.call",
|
||||
);
|
||||
updateDelayedEventHandle.resolve?.();
|
||||
restartScheduledDelayedEventHandle.resolve?.();
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
{ delay: 8000 },
|
||||
@@ -157,13 +162,13 @@ describe("MembershipManager", () => {
|
||||
it("reschedules delayed leave event if sending state cancels it", async () => {
|
||||
const memberManager = new MembershipManager(undefined, room, client, callSession);
|
||||
const waitForSendState = waitForMockCall(client.sendStateEvent);
|
||||
const waitForUpdateDelaye = waitForMockCallOnce(
|
||||
client._unstable_updateDelayedEvent,
|
||||
const waitForRestartScheduledDelayedEvent = waitForMockCallOnce(
|
||||
client._unstable_restartScheduledDelayedEvent,
|
||||
Promise.reject(new MatrixError({ errcode: "M_NOT_FOUND" })),
|
||||
);
|
||||
memberManager.join([focus], focusActive);
|
||||
await waitForSendState;
|
||||
await waitForUpdateDelaye;
|
||||
await waitForRestartScheduledDelayedEvent;
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
// Once for the initial event and once because of the errcode: "M_NOT_FOUND"
|
||||
// Different to "sends a membership event and schedules delayed leave when joining a call" where its only called once (1)
|
||||
@@ -179,7 +184,7 @@ describe("MembershipManager", () => {
|
||||
if (useOwnedStateEvents) {
|
||||
room.getVersion = jest.fn().mockReturnValue("org.matrix.msc3757.default");
|
||||
}
|
||||
const updatedDelayedEvent = waitForMockCall(client._unstable_updateDelayedEvent);
|
||||
const restartScheduledDelayedEvent = waitForMockCall(client._unstable_restartScheduledDelayedEvent);
|
||||
const sentDelayedState = waitForMockCall(
|
||||
client._unstable_sendDelayedStateEvent,
|
||||
Promise.resolve({
|
||||
@@ -265,13 +270,13 @@ describe("MembershipManager", () => {
|
||||
await sentDelayedState;
|
||||
|
||||
// should have prepared the heartbeat to keep delaying the leave event while still connected
|
||||
await updatedDelayedEvent;
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
await restartScheduledDelayedEvent;
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
|
||||
// ensures that we reach the code that schedules the timeout for the next delay update before we advance the timers.
|
||||
await jest.advanceTimersByTimeAsync(5000);
|
||||
// should update delayed disconnect
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(2);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(2);
|
||||
}
|
||||
|
||||
it("sends a membership event after rate limits during delayed event setup when joining a call", async () => {
|
||||
@@ -343,7 +348,7 @@ describe("MembershipManager", () => {
|
||||
// (onRTCSessionMemberUpdate)
|
||||
// - Only then do we resolve the sending of the delayed event.
|
||||
// - We test that the manager acknowledges the leave and sends a new membership state event.
|
||||
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValueOnce(
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValueOnce(
|
||||
new MatrixError({ errcode: "M_NOT_FOUND" }),
|
||||
);
|
||||
|
||||
@@ -404,17 +409,17 @@ describe("MembershipManager", () => {
|
||||
manager.join([focus]);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await manager.leave();
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenLastCalledWith("id", "send");
|
||||
expect(client._unstable_sendScheduledDelayedEvent).toHaveBeenLastCalledWith("id");
|
||||
expect(client.sendStateEvent).toHaveBeenCalled();
|
||||
});
|
||||
it("send leave event when leave is called and resolving delayed leave fails", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
manager.join([focus]);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue("unknown");
|
||||
(client._unstable_sendScheduledDelayedEvent as Mock<any>).mockRejectedValue("unknown");
|
||||
await manager.leave();
|
||||
|
||||
// We send a normal leave event since we failed using updateDelayedEvent with the "send" action.
|
||||
// We send a normal leave event since we failed using sendScheduledDelayedEvent.
|
||||
expect(client.sendStateEvent).toHaveBeenLastCalledWith(
|
||||
room.roomId,
|
||||
"org.matrix.msc3401.call.member",
|
||||
@@ -438,6 +443,9 @@ describe("MembershipManager", () => {
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_cancelScheduledDelayedEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_restartScheduledDelayedEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_sendScheduledDelayedEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
it("does nothing if own membership still present", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
@@ -447,6 +455,9 @@ describe("MembershipManager", () => {
|
||||
// reset all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
|
||||
(client.sendStateEvent as Mock).mockClear();
|
||||
(client._unstable_updateDelayedEvent as Mock).mockClear();
|
||||
(client._unstable_cancelScheduledDelayedEvent as Mock).mockClear();
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock).mockClear();
|
||||
(client._unstable_sendScheduledDelayedEvent as Mock).mockClear();
|
||||
(client._unstable_sendDelayedStateEvent as Mock).mockClear();
|
||||
|
||||
await manager.onRTCSessionMemberUpdate([
|
||||
@@ -462,6 +473,9 @@ describe("MembershipManager", () => {
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_cancelScheduledDelayedEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_restartScheduledDelayedEvent).not.toHaveBeenCalled();
|
||||
expect(client._unstable_sendScheduledDelayedEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
it("recreates membership if it is missing", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
@@ -469,7 +483,7 @@ describe("MembershipManager", () => {
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
|
||||
(client.sendStateEvent as Mock).mockClear();
|
||||
(client._unstable_updateDelayedEvent as Mock).mockClear();
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock).mockClear();
|
||||
(client._unstable_sendDelayedStateEvent as Mock).mockClear();
|
||||
|
||||
// Our own membership is removed:
|
||||
@@ -478,7 +492,7 @@ describe("MembershipManager", () => {
|
||||
expect(client.sendStateEvent).toHaveBeenCalled();
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalled();
|
||||
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalled();
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("updates the UpdateExpiry entry in the action scheduler", async () => {
|
||||
@@ -487,10 +501,10 @@ describe("MembershipManager", () => {
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
|
||||
(client.sendStateEvent as Mock).mockClear();
|
||||
(client._unstable_updateDelayedEvent as Mock).mockClear();
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock).mockClear();
|
||||
(client._unstable_sendDelayedStateEvent as Mock).mockClear();
|
||||
|
||||
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValueOnce(
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValueOnce(
|
||||
new MatrixError({ errcode: "M_NOT_FOUND" }),
|
||||
);
|
||||
|
||||
@@ -503,7 +517,7 @@ describe("MembershipManager", () => {
|
||||
expect(client.sendStateEvent).toHaveBeenCalled();
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalled();
|
||||
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalled();
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalled();
|
||||
expect(manager.status).toBe(Status.Connected);
|
||||
});
|
||||
});
|
||||
@@ -523,17 +537,17 @@ describe("MembershipManager", () => {
|
||||
|
||||
// The first call is from checking id the server deleted the delayed event
|
||||
// so it does not need a `advanceTimersByTime`
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
// TODO: Check that update delayed event is called with the correct HTTP request timeout
|
||||
// expect(client._unstable_updateDelayedEvent).toHaveBeenLastCalledWith("id", 10_000, { localTimeoutMs: 20_000 });
|
||||
// expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenLastCalledWith("id", 10_000, { localTimeoutMs: 20_000 });
|
||||
|
||||
for (let i = 2; i <= 12; i++) {
|
||||
// flush promises before advancing the timers to make sure schedulers are setup
|
||||
await jest.advanceTimersByTimeAsync(10_000);
|
||||
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(i);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(i);
|
||||
// TODO: Check that update delayed event is called with the correct HTTP request timeout
|
||||
// expect(client._unstable_updateDelayedEvent).toHaveBeenLastCalledWith("id", 10_000, { localTimeoutMs: 20_000 });
|
||||
// expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenLastCalledWith("id", 10_000, { localTimeoutMs: 20_000 });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -681,7 +695,7 @@ describe("MembershipManager", () => {
|
||||
});
|
||||
describe("retries sending update delayed leave event restart", () => {
|
||||
it("resends the initial check delayed update event", async () => {
|
||||
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue(
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValue(
|
||||
new MatrixError(
|
||||
{ errcode: "M_LIMIT_EXCEEDED" },
|
||||
429,
|
||||
@@ -695,17 +709,17 @@ describe("MembershipManager", () => {
|
||||
|
||||
// Hit rate limit
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Hit second rate limit.
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(2);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Setup resolve
|
||||
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockResolvedValue(undefined);
|
||||
await jest.advanceTimersByTimeAsync(1000);
|
||||
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(3);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(3);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -734,7 +748,7 @@ describe("MembershipManager", () => {
|
||||
// because legacy does not have a retry limit and no mechanism to communicate unrecoverable errors.
|
||||
it("throws, when reaching maximum number of retries", async () => {
|
||||
const delayEventRestartError = jest.fn();
|
||||
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue(
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockRejectedValue(
|
||||
new MatrixError(
|
||||
{ errcode: "M_LIMIT_EXCEEDED" },
|
||||
429,
|
||||
@@ -808,11 +822,11 @@ describe("MembershipManager", () => {
|
||||
manager.join([focus], focusActive);
|
||||
try {
|
||||
// Let the scheduler run one iteration so that we can send the join state event
|
||||
await waitForMockCall(client._unstable_updateDelayedEvent);
|
||||
await waitForMockCall(client._unstable_restartScheduledDelayedEvent);
|
||||
|
||||
// We never resolve the delayed event so that we can test the probablyLeft event.
|
||||
// This simulates the case where the server does not respond to the delayed event.
|
||||
client._unstable_updateDelayedEvent = jest.fn(() => stuckPromise);
|
||||
client._unstable_restartScheduledDelayedEvent = jest.fn(() => stuckPromise);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||
expect(manager.status).toBe(Status.Connected);
|
||||
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
|
||||
@@ -822,18 +836,18 @@ describe("MembershipManager", () => {
|
||||
await jest.advanceTimersByTimeAsync(5000);
|
||||
// No emission after 5s
|
||||
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(4999);
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(3);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(3);
|
||||
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
|
||||
|
||||
// Reset mocks before we setup the next delayed event restart by advancing the timers 1 more ms.
|
||||
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue({});
|
||||
(client._unstable_restartScheduledDelayedEvent as Mock<any>).mockResolvedValue({});
|
||||
|
||||
// Emit after 10s
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(4);
|
||||
expect(client._unstable_restartScheduledDelayedEvent).toHaveBeenCalledTimes(4);
|
||||
expect(probablyLeftEmit).toHaveBeenCalledWith(true);
|
||||
|
||||
// Mock a sync which does not include our own membership
|
||||
@@ -898,8 +912,8 @@ describe("MembershipManager", () => {
|
||||
describe("join()", () => {
|
||||
describe("sends an rtc membership event", () => {
|
||||
it("sends a membership event and schedules delayed leave when joining a call", async () => {
|
||||
const updateDelayedEventHandle = createAsyncHandle<void>(
|
||||
client._unstable_updateDelayedEvent as Mock,
|
||||
const restartScheduledDelayedEventHandle = createAsyncHandle<void>(
|
||||
client._unstable_restartScheduledDelayedEvent as Mock,
|
||||
);
|
||||
const memberManager = new StickyEventMembershipManager(undefined, room, client, callSession);
|
||||
|
||||
@@ -925,7 +939,7 @@ describe("MembershipManager", () => {
|
||||
msc4354_sticky_key: "_@alice:example.org_AAAAAAA_m.call",
|
||||
},
|
||||
);
|
||||
updateDelayedEventHandle.resolve?.();
|
||||
restartScheduledDelayedEventHandle.resolve?.();
|
||||
|
||||
// Ensure we have sent the delayed disconnect event.
|
||||
expect(client._unstable_sendStickyDelayedEvent).toHaveBeenCalledWith(
|
||||
|
||||
@@ -52,6 +52,9 @@ export type MockClient = Pick<
|
||||
| "sendStateEvent"
|
||||
| "_unstable_sendDelayedStateEvent"
|
||||
| "_unstable_updateDelayedEvent"
|
||||
| "_unstable_cancelScheduledDelayedEvent"
|
||||
| "_unstable_restartScheduledDelayedEvent"
|
||||
| "_unstable_sendScheduledDelayedEvent"
|
||||
| "_unstable_sendStickyEvent"
|
||||
| "_unstable_sendStickyDelayedEvent"
|
||||
| "cancelPendingEvent"
|
||||
@@ -67,6 +70,9 @@ export function makeMockClient(userId: string, deviceId: string): MockClient {
|
||||
sendStateEvent: jest.fn(),
|
||||
cancelPendingEvent: jest.fn(),
|
||||
_unstable_updateDelayedEvent: jest.fn(),
|
||||
_unstable_cancelScheduledDelayedEvent: jest.fn(),
|
||||
_unstable_restartScheduledDelayedEvent: jest.fn(),
|
||||
_unstable_sendScheduledDelayedEvent: jest.fn(),
|
||||
_unstable_sendDelayedStateEvent: jest.fn(),
|
||||
_unstable_sendStickyEvent: jest.fn(),
|
||||
_unstable_sendStickyDelayedEvent: jest.fn(),
|
||||
|
||||
@@ -127,15 +127,17 @@ describe("Poll", () => {
|
||||
|
||||
it("waits for existing relations request to finish when getting responses", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
// @ts-expect-error TS2769
|
||||
const spy = jest.spyOn(poll, "fetchResponses");
|
||||
const firstResponsePromise = poll.getResponses();
|
||||
const secondResponsePromise = poll.getResponses();
|
||||
await firstResponsePromise;
|
||||
expect(firstResponsePromise).toEqual(secondResponsePromise);
|
||||
await secondResponsePromise;
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(mockClient.relations).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("filters relations for relevent response events", async () => {
|
||||
it("filters relations for relevant response events", async () => {
|
||||
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
|
||||
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
|
||||
const unstableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Direction, type MatrixClient, MatrixEvent, Room } from "../../../src";
|
||||
import { Direction, EventType, type MatrixClient, MatrixEvent, Room } from "../../../src";
|
||||
import type { MockedObject } from "jest-mock";
|
||||
|
||||
const CREATOR_USER_ID = "@creator:example.org";
|
||||
@@ -185,4 +185,71 @@ describe("Room", () => {
|
||||
expectRedacted(messageEvents, room, false);
|
||||
});
|
||||
});
|
||||
|
||||
it("should ignore invalid m.room.name events", async () => {
|
||||
const mockClient = createMockClient();
|
||||
const room = new Room("!room:example.org", mockClient, CREATOR_USER_ID);
|
||||
const invalidNameEvent = new MatrixEvent({
|
||||
type: EventType.RoomName,
|
||||
content: {
|
||||
name: { invalid: 123 },
|
||||
},
|
||||
state_key: "",
|
||||
event_id: "$123",
|
||||
room_id: room.roomId,
|
||||
sender: CREATOR_USER_ID,
|
||||
});
|
||||
|
||||
// Set up the room
|
||||
room.currentState.setStateEvents([invalidNameEvent]);
|
||||
room.recalculate();
|
||||
|
||||
expect(room.name).toEqual("Empty room");
|
||||
});
|
||||
|
||||
describe("getAltAliases()", () => {
|
||||
it("should ignore invalid events", async () => {
|
||||
const mockClient = createMockClient();
|
||||
const room = new Room("!room:example.org", mockClient, CREATOR_USER_ID);
|
||||
const invalidAliasEvent = new MatrixEvent({
|
||||
type: EventType.RoomCanonicalAlias,
|
||||
content: {
|
||||
alt_aliases: [123, "#foo:bar"],
|
||||
},
|
||||
state_key: "",
|
||||
event_id: "$123",
|
||||
room_id: room.roomId,
|
||||
sender: CREATOR_USER_ID,
|
||||
});
|
||||
|
||||
// Set up the room
|
||||
room.currentState.setStateEvents([invalidAliasEvent]);
|
||||
room.recalculate();
|
||||
|
||||
expect(room.getAltAliases()).toEqual(["#foo:bar"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("calculateRoomName()", () => {
|
||||
it("should ignore empty m.room.name 'name' field", async () => {
|
||||
const mockClient = createMockClient();
|
||||
const room = new Room("!room:example.org", mockClient, CREATOR_USER_ID);
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomName,
|
||||
content: {
|
||||
name: "",
|
||||
},
|
||||
state_key: "",
|
||||
event_id: "$123",
|
||||
room_id: room.roomId,
|
||||
sender: CREATOR_USER_ID,
|
||||
});
|
||||
|
||||
// Set up the room
|
||||
room.currentState.setStateEvents([event]);
|
||||
room.recalculate();
|
||||
|
||||
expect(room.name).not.toEqual("");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, M_POLL_START, type Optional, PollStartEvent } from "matrix-events-sdk";
|
||||
import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, M_POLL_START, PollStartEvent } from "matrix-events-sdk";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { emitPromise, type IMessageOpts } from "../test-utils/test-utils";
|
||||
@@ -197,8 +197,8 @@ describe("Room", function () {
|
||||
|
||||
const addRoomThreads = (
|
||||
room: Room,
|
||||
thread1EventTs: Optional<number>,
|
||||
thread2EventTs: Optional<number>,
|
||||
thread1EventTs?: number,
|
||||
thread2EventTs?: number,
|
||||
): { thread1?: Thread; thread2?: Thread } => {
|
||||
const result: { thread1?: Thread; thread2?: Thread } = {};
|
||||
|
||||
@@ -4159,7 +4159,7 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
it("when there is only one thread, it should return this one", () => {
|
||||
const { thread1 } = addRoomThreads(room, 23, null);
|
||||
const { thread1 } = addRoomThreads(room, 23);
|
||||
expect(room.getLastThread()).toBe(thread1);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Mocked, type SpyInstance } from "jest-mock";
|
||||
import { type Mocked } from "jest-mock";
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import { type OlmMachine } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
@@ -211,14 +211,14 @@ describe("PerSessionKeyBackupDownloader", () => {
|
||||
fetchMock.get(`path:/_matrix/client/v3/room_keys/keys/!roomA/sessionA1`, mockCipherKey);
|
||||
|
||||
// @ts-ignore access to private function
|
||||
const spy: SpyInstance = jest.spyOn(downloader, "queryKeyBackup");
|
||||
const spy: jest.SpyInstance = jest.spyOn(downloader, "queryKeyBackup");
|
||||
|
||||
const expectImported = expectSessionImported("!roomA", "sessionA1");
|
||||
|
||||
downloader.onDecryptionKeyMissingError("!roomA", "sessionA0");
|
||||
await jest.runAllTimersAsync();
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toHaveLastReturnedWith(Promise.resolve({ ok: false, error: "MISSING_DECRYPTION_KEY" }));
|
||||
await expect(spy.mock.results[0].value).rejects.toThrow("MISSING_DECRYPTION_KEY");
|
||||
|
||||
downloader.onDecryptionKeyMissingError("!roomA", "sessionA1");
|
||||
await jest.runAllTimersAsync();
|
||||
@@ -237,7 +237,7 @@ describe("PerSessionKeyBackupDownloader", () => {
|
||||
});
|
||||
|
||||
// @ts-ignore access to private function
|
||||
const spy: SpyInstance = jest.spyOn(downloader, "queryKeyBackup");
|
||||
const spy: jest.SpyInstance = jest.spyOn(downloader, "queryKeyBackup");
|
||||
|
||||
downloader.onDecryptionKeyMissingError("!roomA", "sessionA0");
|
||||
await jest.runAllTimersAsync();
|
||||
@@ -297,7 +297,7 @@ describe("PerSessionKeyBackupDownloader", () => {
|
||||
});
|
||||
|
||||
describe("Given no usable backup available", () => {
|
||||
let getConfigSpy: SpyInstance;
|
||||
let getConfigSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockRustBackupManager.getActiveBackupVersion.mockResolvedValue(null);
|
||||
@@ -318,7 +318,7 @@ describe("PerSessionKeyBackupDownloader", () => {
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
expect(getConfigSpy).toHaveBeenCalledTimes(1);
|
||||
expect(getConfigSpy).toHaveReturnedWith(Promise.resolve(null));
|
||||
await expect(getConfigSpy.mock.results[0].value).resolves.toEqual(null);
|
||||
|
||||
// isKeyBackupDownloadConfigured remains false
|
||||
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
|
||||
@@ -336,7 +336,7 @@ describe("PerSessionKeyBackupDownloader", () => {
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
expect(getConfigSpy).toHaveBeenCalledTimes(1);
|
||||
expect(getConfigSpy).toHaveReturnedWith(Promise.resolve(null));
|
||||
await expect(getConfigSpy.mock.results[0].value).resolves.toEqual(null);
|
||||
|
||||
// isKeyBackupDownloadConfigured remains false
|
||||
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
|
||||
@@ -355,7 +355,7 @@ describe("PerSessionKeyBackupDownloader", () => {
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
expect(getConfigSpy).toHaveBeenCalledTimes(1);
|
||||
expect(getConfigSpy).toHaveReturnedWith(Promise.resolve(null));
|
||||
await expect(getConfigSpy.mock.results[0].value).resolves.toEqual(null);
|
||||
|
||||
// isKeyBackupDownloadConfigured remains false
|
||||
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
|
||||
@@ -377,7 +377,7 @@ describe("PerSessionKeyBackupDownloader", () => {
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
expect(getConfigSpy).toHaveBeenCalledTimes(1);
|
||||
expect(getConfigSpy).toHaveReturnedWith(Promise.resolve(null));
|
||||
await expect(getConfigSpy.mock.results[0].value).resolves.toEqual(null);
|
||||
|
||||
// isKeyBackupDownloadConfigured remains false
|
||||
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
|
||||
@@ -399,7 +399,7 @@ describe("PerSessionKeyBackupDownloader", () => {
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
expect(getConfigSpy).toHaveBeenCalledTimes(1);
|
||||
expect(getConfigSpy).toHaveReturnedWith(Promise.resolve(null));
|
||||
await expect(getConfigSpy.mock.results[0].value).resolves.toEqual(null);
|
||||
|
||||
// isKeyBackupDownloadConfigured remains false
|
||||
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
|
||||
@@ -488,7 +488,7 @@ describe("PerSessionKeyBackupDownloader", () => {
|
||||
const originalImplementation = downloader.queryKeyBackup.bind(downloader);
|
||||
|
||||
// @ts-ignore access to private function
|
||||
const keyQuerySpy: SpyInstance = jest.spyOn(downloader, "queryKeyBackup");
|
||||
const keyQuerySpy: jest.SpyInstance = jest.spyOn(downloader, "queryKeyBackup");
|
||||
const rateDeferred = Promise.withResolvers<void>();
|
||||
|
||||
keyQuerySpy.mockImplementation(
|
||||
@@ -542,7 +542,7 @@ describe("PerSessionKeyBackupDownloader", () => {
|
||||
const originalImplementation = downloader.queryKeyBackup.bind(downloader);
|
||||
|
||||
// @ts-ignore
|
||||
const keyQuerySpy: SpyInstance = jest.spyOn(downloader, "queryKeyBackup");
|
||||
const keyQuerySpy: jest.SpyInstance = jest.spyOn(downloader, "queryKeyBackup");
|
||||
const errorDeferred = Promise.withResolvers<void>();
|
||||
|
||||
keyQuerySpy.mockImplementation(
|
||||
@@ -606,7 +606,7 @@ describe("PerSessionKeyBackupDownloader", () => {
|
||||
});
|
||||
|
||||
// @ts-ignore access to private function
|
||||
const keyQuerySpy: SpyInstance = jest.spyOn(downloader, "queryKeyBackup");
|
||||
const keyQuerySpy: jest.SpyInstance = jest.spyOn(downloader, "queryKeyBackup");
|
||||
|
||||
downloader.onDecryptionKeyMissingError("!roomA", "sessionA0");
|
||||
downloader.onDecryptionKeyMissingError("!roomA", "sessionA1");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`RustCrypto importing and exporting room keys should import and export keys 1`] = `
|
||||
{
|
||||
|
||||
@@ -98,7 +98,7 @@ function createLinkedTimelines(): [EventTimeline, EventTimeline] {
|
||||
describe("TimelineIndex", function () {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockClient.getEventTimeline.mockResolvedValue(undefined);
|
||||
mockClient.getEventTimeline.mockResolvedValue(null);
|
||||
});
|
||||
|
||||
describe("minIndex", function () {
|
||||
@@ -193,7 +193,7 @@ describe("TimelineWindow", function () {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockClient.getEventTimeline.mockResolvedValue(undefined);
|
||||
mockClient.getEventTimeline.mockResolvedValue(null);
|
||||
mockClient.paginateEventTimeline.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -187,12 +187,14 @@ describe("Group Call", function () {
|
||||
});
|
||||
|
||||
it("does not start initializing local call feed twice", () => {
|
||||
const promise1 = groupCall.initLocalCallFeed();
|
||||
// @ts-expect-error TS2769
|
||||
const spy = jest.spyOn(groupCall, "initLocalCallFeedInternal");
|
||||
groupCall.initLocalCallFeed();
|
||||
// @ts-ignore Mock
|
||||
groupCall.state = GroupCallState.LocalCallFeedUninitialized;
|
||||
const promise2 = groupCall.initLocalCallFeed();
|
||||
groupCall.initLocalCallFeed();
|
||||
|
||||
expect(promise1).toEqual(promise2);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("sets state to local call feed uninitialized when getUserMedia() fails", async () => {
|
||||
|
||||
@@ -61,10 +61,10 @@ describe("Media Handler", function () {
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
audio: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_AUDIO_INPUT_ID },
|
||||
deviceId: { exact: FAKE_AUDIO_INPUT_ID },
|
||||
}),
|
||||
video: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_VIDEO_INPUT_ID },
|
||||
deviceId: { exact: FAKE_VIDEO_INPUT_ID },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -77,7 +77,7 @@ describe("Media Handler", function () {
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
audio: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_AUDIO_INPUT_ID },
|
||||
deviceId: { exact: FAKE_AUDIO_INPUT_ID },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -109,7 +109,7 @@ describe("Media Handler", function () {
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
video: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_VIDEO_INPUT_ID },
|
||||
deviceId: { exact: FAKE_VIDEO_INPUT_ID },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -122,10 +122,10 @@ describe("Media Handler", function () {
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
audio: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_AUDIO_INPUT_ID },
|
||||
deviceId: { exact: FAKE_AUDIO_INPUT_ID },
|
||||
}),
|
||||
video: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_VIDEO_INPUT_ID },
|
||||
deviceId: { exact: FAKE_VIDEO_INPUT_ID },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -331,6 +331,80 @@ describe("Media Handler", function () {
|
||||
|
||||
expect(stream.getVideoTracks().length).toEqual(0);
|
||||
});
|
||||
|
||||
it("falls back to ideal deviceId when exact deviceId fails", async () => {
|
||||
// First call with exact should fail
|
||||
mockMediaDevices.getUserMedia
|
||||
.mockRejectedValueOnce(new Error("OverconstrainedError"))
|
||||
.mockImplementation((constraints: MediaStreamConstraints) => {
|
||||
const stream = new MockMediaStream("local_stream");
|
||||
if (constraints.audio) {
|
||||
const track = new MockMediaStreamTrack("audio_track", "audio");
|
||||
track.settings = { deviceId: FAKE_AUDIO_INPUT_ID };
|
||||
stream.addTrack(track);
|
||||
}
|
||||
return Promise.resolve(stream.typed());
|
||||
});
|
||||
|
||||
const stream = await mediaHandler.getUserMediaStream(true, false);
|
||||
|
||||
// Should have been called twice: once with exact, once with ideal
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledTimes(2);
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
audio: expect.objectContaining({
|
||||
deviceId: { exact: FAKE_AUDIO_INPUT_ID },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
audio: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_AUDIO_INPUT_ID },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(stream).toBeTruthy();
|
||||
});
|
||||
|
||||
it("falls back to ideal deviceId for video when exact fails", async () => {
|
||||
// First call with exact should fail
|
||||
mockMediaDevices.getUserMedia
|
||||
.mockRejectedValueOnce(new Error("OverconstrainedError"))
|
||||
.mockImplementation((constraints: MediaStreamConstraints) => {
|
||||
const stream = new MockMediaStream("local_stream");
|
||||
if (constraints.video) {
|
||||
const track = new MockMediaStreamTrack("video_track", "video");
|
||||
track.settings = { deviceId: FAKE_VIDEO_INPUT_ID };
|
||||
stream.addTrack(track);
|
||||
}
|
||||
return Promise.resolve(stream.typed());
|
||||
});
|
||||
|
||||
const stream = await mediaHandler.getUserMediaStream(false, true);
|
||||
|
||||
// Should have been called twice: once with exact, once with ideal
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenCalledTimes(2);
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
video: expect.objectContaining({
|
||||
deviceId: { exact: FAKE_VIDEO_INPUT_ID },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(mockMediaDevices.getUserMedia).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
video: expect.objectContaining({
|
||||
deviceId: { ideal: FAKE_VIDEO_INPUT_ID },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(stream).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getScreensharingStream", () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`CallFeedStatsReporter should builds CallFeedReport 1`] = `
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type EitherAnd, NamespacedValue, type Optional, UnstableValue } from "matrix-events-sdk";
|
||||
import { type EitherAnd, NamespacedValue, UnstableValue } from "matrix-events-sdk";
|
||||
|
||||
import { isProvided } from "../extensible_events_v1/utilities.ts";
|
||||
|
||||
@@ -125,10 +125,7 @@ export type ExtensibleEventType = NamespacedValue<string, string> | string;
|
||||
* @param expected - The expected event type.
|
||||
* @returns True if the given type matches the expected type.
|
||||
*/
|
||||
export function isEventTypeSame(
|
||||
given: Optional<ExtensibleEventType>,
|
||||
expected: Optional<ExtensibleEventType>,
|
||||
): boolean {
|
||||
export function isEventTypeSame(given: ExtensibleEventType | null, expected: ExtensibleEventType | null): boolean {
|
||||
if (typeof given === "string") {
|
||||
if (typeof expected === "string") {
|
||||
return expected === given;
|
||||
|
||||
@@ -298,12 +298,12 @@ export interface IRelationsResponse {
|
||||
}
|
||||
|
||||
export interface IContextResponse {
|
||||
end: string;
|
||||
start: string;
|
||||
state: IEventWithRoomId[];
|
||||
events_before: IEventWithRoomId[];
|
||||
events_after: IEventWithRoomId[];
|
||||
event: IEventWithRoomId;
|
||||
end?: string;
|
||||
start?: string;
|
||||
state?: IEventWithRoomId[];
|
||||
events_before?: IEventWithRoomId[];
|
||||
events_after?: IEventWithRoomId[];
|
||||
event?: IEventWithRoomId;
|
||||
}
|
||||
|
||||
export interface IEventsResponse {
|
||||
|
||||
@@ -106,7 +106,7 @@ export interface RoomPinnedEventsEventContent {
|
||||
|
||||
export interface RoomEncryptionEventContent {
|
||||
"algorithm": "m.megolm.v1.aes-sha2";
|
||||
"io.element.msc3414.encrypt_state_events"?: boolean;
|
||||
"io.element.msc4362.encrypt_state_events"?: boolean;
|
||||
"rotation_period_ms"?: number;
|
||||
"rotation_period_msgs"?: number;
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Optional } from "matrix-events-sdk";
|
||||
|
||||
/**
|
||||
* Represents a simple Matrix namespaced value. This will assume that if a stable prefix
|
||||
* is provided that the stable prefix should be used when representing the identifier.
|
||||
@@ -62,7 +60,7 @@ export class NamespacedValue<S extends string, U extends string> {
|
||||
|
||||
// this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class
|
||||
// so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace.
|
||||
public findIn<T>(obj: any): Optional<T> {
|
||||
public findIn<T>(obj: any): T | undefined {
|
||||
let val: T | undefined = undefined;
|
||||
if (this.name) {
|
||||
val = obj?.[this.name];
|
||||
|
||||
+175
-47
@@ -18,8 +18,6 @@ limitations under the License.
|
||||
* This is an internal module. See {@link MatrixClient} for the public class.
|
||||
*/
|
||||
|
||||
import { type Optional } from "matrix-events-sdk";
|
||||
|
||||
import type { IDeviceKeys, IOneTimeKey } from "./@types/crypto.ts";
|
||||
import { type ISyncStateData, type SetPresence, SyncApi, type SyncApiOptions, SyncState } from "./sync.ts";
|
||||
import {
|
||||
@@ -106,6 +104,7 @@ import { RoomMemberEvent, type RoomMemberEventHandlerMap } from "./models/room-m
|
||||
import { type IPowerLevelsContent, type RoomStateEvent, type RoomStateEventHandlerMap } from "./models/room-state.ts";
|
||||
import {
|
||||
isSendDelayedEventRequestOpts,
|
||||
UpdateDelayedEventAction,
|
||||
type DelayedEventInfo,
|
||||
type IAddThreePidOnlyBody,
|
||||
type IBindThreePidBody,
|
||||
@@ -130,7 +129,6 @@ import {
|
||||
type KnockRoomOpts,
|
||||
type SendDelayedEventRequestOpts,
|
||||
type SendDelayedEventResponse,
|
||||
type UpdateDelayedEventAction,
|
||||
} from "./@types/requests.ts";
|
||||
import {
|
||||
type AccountDataEvents,
|
||||
@@ -2408,7 +2406,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
const roomId = res.room_id;
|
||||
if (opts.acceptSharedHistory && inviter && this.cryptoBackend) {
|
||||
await this.cryptoBackend.maybeAcceptKeyBundle(roomId, inviter);
|
||||
// Try to accept the room key bundle specified in a `m.room_key_bundle` to-device message we (might have) already received.
|
||||
const bundleDownloaded = await this.cryptoBackend.maybeAcceptKeyBundle(roomId, inviter);
|
||||
// If this fails, i.e. we haven't received this message yet, we need to wait until the to-device message arrives.
|
||||
if (!bundleDownloaded) {
|
||||
this.cryptoBackend.markRoomAsPendingKeyBundle(roomId, inviter);
|
||||
}
|
||||
}
|
||||
|
||||
// In case we were originally given an alias, check the room cache again
|
||||
@@ -3443,7 +3446,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
delayOpts: SendDelayedEventRequestOpts,
|
||||
threadId: string | null,
|
||||
eventType: K,
|
||||
content: TimelineEvents[K] & { msc4354_sticky_key: string },
|
||||
content: TimelineEvents[K] & { msc4354_sticky_key?: string },
|
||||
txnId?: string,
|
||||
): Promise<SendDelayedEventResponse> {
|
||||
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
|
||||
@@ -3516,7 +3519,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
stickDuration: number,
|
||||
threadId: string | null,
|
||||
eventType: K,
|
||||
content: TimelineEvents[K] & { msc4354_sticky_key: string },
|
||||
content: TimelineEvents[K] & { msc4354_sticky_key?: string },
|
||||
txnId?: string,
|
||||
): Promise<ISendEventResponse> {
|
||||
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4354_STICKY_EVENTS))) {
|
||||
@@ -3570,8 +3573,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* Note: This endpoint is unstable, and can throw an `Error`.
|
||||
* Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details.
|
||||
*
|
||||
* @deprecated Instead use one of:
|
||||
* - {@link _unstable_cancelScheduledDelayedEvent}
|
||||
* - {@link _unstable_restartScheduledDelayedEvent}
|
||||
* - {@link _unstable_sendScheduledDelayedEvent}
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public async _unstable_updateDelayedEvent(
|
||||
delayId: string,
|
||||
action: UpdateDelayedEventAction,
|
||||
@@ -3583,17 +3591,123 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
"updateDelayedEvent",
|
||||
);
|
||||
}
|
||||
return await this.updateScheduledDelayedEventWithActionInBody(delayId, action, requestOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the scheduled delivery of the delayed event matching the provided delayId.
|
||||
*
|
||||
* Note: This endpoint is unstable, and can throw an `Error`.
|
||||
* Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details.
|
||||
*
|
||||
* @throws A M_NOT_FOUND error if no matching delayed event could be found.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public async _unstable_cancelScheduledDelayedEvent(
|
||||
delayId: string,
|
||||
requestOptions: IRequestOpts = {},
|
||||
): Promise<EmptyObject> {
|
||||
return await this.updateScheduledDelayedEvent(delayId, UpdateDelayedEventAction.Cancel, requestOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the scheduled delivery of the delayed event matching the given delayId.
|
||||
*
|
||||
* Note: This endpoint is unstable, and can throw an `Error`.
|
||||
* Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details.
|
||||
*
|
||||
* @throws A M_NOT_FOUND error if no matching delayed event could be found.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public async _unstable_restartScheduledDelayedEvent(
|
||||
delayId: string,
|
||||
requestOptions: IRequestOpts = {},
|
||||
): Promise<EmptyObject> {
|
||||
return await this.updateScheduledDelayedEvent(delayId, UpdateDelayedEventAction.Restart, requestOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately send the delayed event matching the given delayId,
|
||||
* instead of waiting for its scheduled delivery.
|
||||
*
|
||||
* Note: This endpoint is unstable, and can throw an `Error`.
|
||||
* Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details.
|
||||
*
|
||||
* @throws A M_NOT_FOUND error if no matching delayed event could be found.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public async _unstable_sendScheduledDelayedEvent(
|
||||
delayId: string,
|
||||
requestOptions: IRequestOpts = {},
|
||||
): Promise<EmptyObject> {
|
||||
return await this.updateScheduledDelayedEvent(delayId, UpdateDelayedEventAction.Send, requestOptions);
|
||||
}
|
||||
|
||||
private async updateScheduledDelayedEvent(
|
||||
delayId: string,
|
||||
action: UpdateDelayedEventAction,
|
||||
requestOptions: IRequestOpts = {},
|
||||
): Promise<EmptyObject> {
|
||||
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
|
||||
throw new UnsupportedDelayedEventsEndpointError(
|
||||
"Server does not support the delayed events API",
|
||||
`${action}ScheduledDelayedEvent`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const path = utils.encodeUri("/delayed_events/$delayId/$action", {
|
||||
$delayId: delayId,
|
||||
$action: action,
|
||||
});
|
||||
return await this.http.request(Method.Post, path, undefined, undefined, {
|
||||
...requestOptions,
|
||||
prefix: `${ClientPrefix.Unstable}/${UNSTABLE_MSC4140_DELAYED_EVENTS}`,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof MatrixError && e.errcode === "M_UNRECOGNIZED") {
|
||||
// For backwards compatibility with an older version of this endpoint
|
||||
// which put the update action in the request body instead of the path
|
||||
return await this.updateScheduledDelayedEventWithActionInBody(delayId, action, requestOptions);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Present for backwards compatibility with an older version of MSC4140
|
||||
* which had a single, authenticated endpoint for updating a delayed event, instead
|
||||
* of one unauthenticated endpoint per update action.
|
||||
*/
|
||||
private async updateScheduledDelayedEventWithActionInBody(
|
||||
delayId: string,
|
||||
action: UpdateDelayedEventAction,
|
||||
requestOptions: IRequestOpts = {},
|
||||
): Promise<EmptyObject> {
|
||||
const path = utils.encodeUri("/delayed_events/$delayId", {
|
||||
$delayId: delayId,
|
||||
});
|
||||
const data = {
|
||||
action,
|
||||
};
|
||||
return await this.http.authedRequest(Method.Post, path, undefined, data, {
|
||||
...requestOptions,
|
||||
prefix: `${ClientPrefix.Unstable}/${UNSTABLE_MSC4140_DELAYED_EVENTS}`,
|
||||
});
|
||||
try {
|
||||
return await this.http.request(Method.Post, path, undefined, data, {
|
||||
...requestOptions,
|
||||
prefix: `${ClientPrefix.Unstable}/${UNSTABLE_MSC4140_DELAYED_EVENTS}`,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof MatrixError && e.errcode === "M_MISSING_TOKEN") {
|
||||
// For backwards compatibility with an older version of this endpoint
|
||||
// which required authentication
|
||||
return await this.http.authedRequest(Method.Post, path, undefined, data, {
|
||||
...requestOptions,
|
||||
prefix: `${ClientPrefix.Unstable}/${UNSTABLE_MSC4140_DELAYED_EVENTS}`,
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4336,6 +4450,46 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return eventMapperFor(this, options || {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the `/context` API for the given room ID & event ID.
|
||||
* Returns the response, with `event` asserted and all optional arrays defaulted to an empty array.
|
||||
* @param roomId - the room ID to request a context for
|
||||
* @param eventId - the event ID to request a context for
|
||||
* @throws if `event` in the response is missing
|
||||
* @private
|
||||
*/
|
||||
private async getEventContext(
|
||||
roomId: string,
|
||||
eventId: string,
|
||||
): Promise<IContextResponse & Omit<Required<IContextResponse>, "start" | "end">> {
|
||||
const path = utils.encodeUri("/rooms/$roomId/context/$eventId", {
|
||||
$roomId: roomId,
|
||||
$eventId: eventId,
|
||||
});
|
||||
|
||||
const params: Record<string, string | string[]> = {
|
||||
limit: "0",
|
||||
};
|
||||
if (this.clientOpts?.lazyLoadMembers) {
|
||||
params.filter = JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER);
|
||||
}
|
||||
|
||||
// TODO: we should implement a backoff (as per scrollback()) to deal more nicely with HTTP errors.
|
||||
const res = await this.http.authedRequest<IContextResponse>(Method.Get, path, params);
|
||||
if (res.event) {
|
||||
return {
|
||||
start: res.start,
|
||||
end: res.end,
|
||||
event: res.event,
|
||||
events_after: res.events_after ?? [],
|
||||
events_before: res.events_before ?? [],
|
||||
state: res.state ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error("'event' not in '/context' result - homeserver too old?");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an EventTimeline for the given event
|
||||
*
|
||||
@@ -4350,7 +4504,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves:
|
||||
* {@link EventTimeline} including the given event
|
||||
*/
|
||||
public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<Optional<EventTimeline>> {
|
||||
public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<EventTimeline | null> {
|
||||
// don't allow any timeline support unless it's been enabled.
|
||||
if (!this.timelineSupport) {
|
||||
throw new Error(
|
||||
@@ -4368,24 +4522,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
if (timelineSet.thread && this.supportsThreads()) {
|
||||
return this.getThreadTimeline(timelineSet, eventId);
|
||||
return (await this.getThreadTimeline(timelineSet, eventId)) ?? null;
|
||||
}
|
||||
|
||||
const path = utils.encodeUri("/rooms/$roomId/context/$eventId", {
|
||||
$roomId: timelineSet.room.roomId,
|
||||
$eventId: eventId,
|
||||
});
|
||||
|
||||
let params: Record<string, string | string[]> | undefined = undefined;
|
||||
if (this.clientOpts?.lazyLoadMembers) {
|
||||
params = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) };
|
||||
}
|
||||
|
||||
// TODO: we should implement a backoff (as per scrollback()) to deal more nicely with HTTP errors.
|
||||
const res = await this.http.authedRequest<IContextResponse>(Method.Get, path, params);
|
||||
if (!res.event) {
|
||||
throw new Error("'event' not in '/context' result - homeserver too old?");
|
||||
}
|
||||
const res = await this.getEventContext(timelineSet.room.roomId, eventId);
|
||||
|
||||
// by the time the request completes, the event might have ended up in the timeline.
|
||||
if (timelineSet.getTimelineForEvent(eventId)) {
|
||||
@@ -4396,7 +4536,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const event = mapper(res.event);
|
||||
if (event.isRelation(THREAD_RELATION_TYPE.name)) {
|
||||
this.logger.warn("Tried loading a regular timeline at the position of a thread event");
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
const events = [
|
||||
// Order events from most recent to oldest (reverse-chronological).
|
||||
@@ -4414,7 +4554,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
} else {
|
||||
timeline = timelineSet.addTimeline();
|
||||
timeline.initialiseState(res.state.map(mapper));
|
||||
timeline.getState(EventTimeline.FORWARDS)!.paginationToken = res.end;
|
||||
timeline.getState(EventTimeline.FORWARDS)!.paginationToken = res.end ?? null;
|
||||
}
|
||||
|
||||
const [timelineEvents, threadedEvents, unknownRelations] = timelineSet.room.partitionThreadedEvents(events);
|
||||
@@ -4447,20 +4587,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
throw new Error("could not get thread timeline: not a thread timeline");
|
||||
}
|
||||
|
||||
const path = utils.encodeUri("/rooms/$roomId/context/$eventId", {
|
||||
$roomId: timelineSet.room.roomId,
|
||||
$eventId: eventId,
|
||||
});
|
||||
const res = await this.getEventContext(timelineSet.room.roomId, eventId);
|
||||
|
||||
const params: Record<string, string | string[]> = {
|
||||
limit: "0",
|
||||
};
|
||||
if (this.clientOpts?.lazyLoadMembers) {
|
||||
params.filter = JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER);
|
||||
}
|
||||
|
||||
// TODO: we should implement a backoff (as per scrollback()) to deal more nicely with HTTP errors.
|
||||
const res = await this.http.authedRequest<IContextResponse>(Method.Get, path, params);
|
||||
const mapper = this.getEventMapper();
|
||||
const event = mapper(res.event);
|
||||
|
||||
@@ -4540,7 +4668,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
{ dir: Direction.Backward, from: res.start, recurse: recurse || undefined },
|
||||
);
|
||||
const eventsNewer: IEvent[] = [];
|
||||
let nextBatch: Optional<string> = res.end;
|
||||
let nextBatch = res.end;
|
||||
while (nextBatch) {
|
||||
const resNewer: IRelationsResponse = await this.fetchRelations(
|
||||
timelineSet.room.roomId,
|
||||
@@ -4549,7 +4677,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
null,
|
||||
{ dir: Direction.Forward, from: nextBatch, recurse: recurse || undefined },
|
||||
);
|
||||
nextBatch = resNewer.next_batch ?? null;
|
||||
nextBatch = resNewer.next_batch;
|
||||
eventsNewer.push(...resNewer.chunk);
|
||||
}
|
||||
const events = [
|
||||
@@ -4593,7 +4721,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves:
|
||||
* {@link EventTimeline} timeline with the latest events in the room
|
||||
*/
|
||||
public async getLatestTimeline(timelineSet: EventTimelineSet): Promise<Optional<EventTimeline>> {
|
||||
public async getLatestTimeline(timelineSet: EventTimelineSet): Promise<EventTimeline | null> {
|
||||
// don't allow any timeline support unless it's been enabled.
|
||||
if (!this.timelineSupport) {
|
||||
throw new Error(
|
||||
@@ -6805,7 +6933,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the event is excluded under MSC3414
|
||||
// Check if the event is excluded under MSC4362
|
||||
if (
|
||||
[
|
||||
"m.room.create",
|
||||
|
||||
@@ -90,8 +90,20 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
|
||||
*
|
||||
* @param inviter - The user who invited us to the room and is expected to have
|
||||
* sent the room key bundle.
|
||||
*
|
||||
* @returns `true` if the key bundle was successfuly downloaded and imported.
|
||||
*/
|
||||
maybeAcceptKeyBundle(roomId: string, inviter: string): Promise<void>;
|
||||
maybeAcceptKeyBundle(roomId: string, inviter: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Mark a room as pending a key bundle under MSC4268. The backend will listen for room key bundle messages, and if
|
||||
* it sees one matching the room specified, it will automatically import it as long as the message author's ID matches
|
||||
* the inviter's ID.
|
||||
*
|
||||
* @param roomId - The room we were invited to, for which we did not receive a key bundle before accepting the invite.
|
||||
* @param inviterId - The user who invited us to the room and is expected to send the room key bundle.
|
||||
*/
|
||||
markRoomAsPendingKeyBundle(roomId: string, inviterId: string): void;
|
||||
}
|
||||
|
||||
/** The methods which crypto implementations should expose to the Sync api
|
||||
|
||||
+67
-3
@@ -37,7 +37,7 @@ import {
|
||||
type ISendEventResponse,
|
||||
type SendDelayedEventRequestOpts,
|
||||
type SendDelayedEventResponse,
|
||||
type UpdateDelayedEventAction,
|
||||
UpdateDelayedEventAction,
|
||||
} from "./@types/requests.ts";
|
||||
import { EventType, type StateEvents } from "./@types/event.ts";
|
||||
import { logger } from "./logger.ts";
|
||||
@@ -459,8 +459,12 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
|
||||
/**
|
||||
* @experimental This currently relies on an unstable MSC (MSC4140).
|
||||
* @deprecated Instead use one of:
|
||||
* - {@link _unstable_cancelScheduledDelayedEvent}
|
||||
* - {@link _unstable_restartScheduledDelayedEvent}
|
||||
* - {@link _unstable_sendScheduledDelayedEvent}
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public async _unstable_updateDelayedEvent(delayId: string, action: UpdateDelayedEventAction): Promise<EmptyObject> {
|
||||
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
|
||||
throw new UnsupportedDelayedEventsEndpointError(
|
||||
@@ -469,7 +473,67 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
);
|
||||
}
|
||||
|
||||
await this.widgetApi.updateDelayedEvent(delayId, action).catch(timeoutToConnectionError);
|
||||
let updateDelayedEvent: (delayId: string) => Promise<unknown>;
|
||||
switch (action) {
|
||||
case UpdateDelayedEventAction.Cancel:
|
||||
updateDelayedEvent = this.widgetApi.cancelScheduledDelayedEvent;
|
||||
break;
|
||||
case UpdateDelayedEventAction.Restart:
|
||||
updateDelayedEvent = this.widgetApi.cancelScheduledDelayedEvent;
|
||||
break;
|
||||
case UpdateDelayedEventAction.Send:
|
||||
updateDelayedEvent = this.widgetApi.sendScheduledDelayedEvent;
|
||||
break;
|
||||
}
|
||||
await updateDelayedEvent.call(this.widgetApi, delayId).catch(timeoutToConnectionError);
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental This currently relies on an unstable MSC (MSC4140).
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public async _unstable_cancelScheduledDelayedEvent(delayId: string): Promise<EmptyObject> {
|
||||
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
|
||||
throw new UnsupportedDelayedEventsEndpointError(
|
||||
"Server does not support the delayed events API",
|
||||
"cancelScheduledDelayedEvent",
|
||||
);
|
||||
}
|
||||
|
||||
await this.widgetApi.cancelScheduledDelayedEvent(delayId).catch(timeoutToConnectionError);
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental This currently relies on an unstable MSC (MSC4140).
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public async _unstable_restartScheduledDelayedEvent(delayId: string): Promise<EmptyObject> {
|
||||
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
|
||||
throw new UnsupportedDelayedEventsEndpointError(
|
||||
"Server does not support the delayed events API",
|
||||
"restartScheduledDelayedEvent",
|
||||
);
|
||||
}
|
||||
|
||||
await this.widgetApi.restartScheduledDelayedEvent(delayId).catch(timeoutToConnectionError);
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental This currently relies on an unstable MSC (MSC4140).
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
public async _unstable_sendScheduledDelayedEvent(delayId: string): Promise<EmptyObject> {
|
||||
if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4140_DELAYED_EVENTS))) {
|
||||
throw new UnsupportedDelayedEventsEndpointError(
|
||||
"Server does not support the delayed events API",
|
||||
"sendScheduledDelayedEvent",
|
||||
);
|
||||
}
|
||||
|
||||
await this.widgetApi.sendScheduledDelayedEvent(delayId).catch(timeoutToConnectionError);
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
+8
-1
@@ -59,7 +59,14 @@ export class ClientStoppedError extends Error {
|
||||
export class UnsupportedDelayedEventsEndpointError extends Error {
|
||||
public constructor(
|
||||
message: string,
|
||||
public clientEndpoint: "sendDelayedEvent" | "updateDelayedEvent" | "sendDelayedStateEvent" | "getDelayedEvents",
|
||||
public clientEndpoint:
|
||||
| "sendDelayedEvent"
|
||||
| "updateDelayedEvent"
|
||||
| "cancelScheduledDelayedEvent"
|
||||
| "restartScheduledDelayedEvent"
|
||||
| "sendScheduledDelayedEvent"
|
||||
| "sendDelayedStateEvent"
|
||||
| "getDelayedEvents",
|
||||
) {
|
||||
super(message);
|
||||
this.name = "UnsupportedDelayedEventsEndpointError";
|
||||
|
||||
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Optional } from "matrix-events-sdk";
|
||||
|
||||
import { ExtensibleEvent } from "./ExtensibleEvent.ts";
|
||||
import {
|
||||
type ExtensibleEventType,
|
||||
@@ -46,7 +44,7 @@ export class MessageEvent extends ExtensibleEvent<ExtensibleAnyMessageEventConte
|
||||
/**
|
||||
* The default HTML for the event, if provided.
|
||||
*/
|
||||
public readonly html: Optional<string>;
|
||||
public readonly html?: string;
|
||||
|
||||
/**
|
||||
* All the different renderings of the message. Note that this is the same
|
||||
@@ -82,7 +80,7 @@ export class MessageEvent extends ExtensibleEvent<ExtensibleAnyMessageEventConte
|
||||
this.renderings = mmessage;
|
||||
} else if (isOptionalAString(mtext)) {
|
||||
this.text = mtext;
|
||||
this.html = mhtml;
|
||||
this.html = mhtml ?? undefined;
|
||||
this.renderings = [{ body: mtext, mimetype: "text/plain" }];
|
||||
if (this.html) {
|
||||
this.renderings.push({ body: this.html, mimetype: "text/html" });
|
||||
|
||||
@@ -279,6 +279,13 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
|
||||
const { signal, cleanup } = anySignal(signals);
|
||||
|
||||
// Set cache mode based on presence of Authorization header.
|
||||
// Browsers/proxies do not cache responses to requests with Authorization headers.
|
||||
// So specifying "no-cache" is redundant, and actually prevents caching
|
||||
// of preflight requests in CORS scenarios. As such, we only set "no-cache"
|
||||
// when there is no Authorization header.
|
||||
const cacheMode = "Authorization" in headers ? undefined : "no-cache";
|
||||
|
||||
let res: Response;
|
||||
const start = Date.now();
|
||||
try {
|
||||
@@ -291,7 +298,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
redirect: "follow",
|
||||
referrer: "",
|
||||
referrerPolicy: "no-referrer",
|
||||
cache: "no-cache",
|
||||
cache: cacheMode,
|
||||
credentials: "omit", // we send credentials via headers
|
||||
keepalive: keepAlive,
|
||||
priority: opts.priority,
|
||||
|
||||
@@ -178,7 +178,8 @@ export interface MembershipConfig {
|
||||
* In the presence of network packet loss (hurting TCP connections), the custom delayedEventRestartLocalTimeoutMs
|
||||
* helps by keeping more delayed event reset candidates in flight,
|
||||
* improving the chances of a successful reset. (its is equivalent to the js-sdk `localTimeout` configuration,
|
||||
* but only applies to calls to the `_unstable_updateDelayedEvent` endpoint with a body of `{action:"restart"}`.)
|
||||
* but only applies to calls to the `_unstable_restartScheduledDelayedEvent` endpoint
|
||||
* or the `_unstable_updateDelayedEvent` endpoint with a body of `{action:"restart"}`.)
|
||||
*/
|
||||
delayedLeaveEventRestartLocalTimeoutMs?: number;
|
||||
|
||||
@@ -514,6 +515,9 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
| "sendStateEvent"
|
||||
| "_unstable_sendDelayedStateEvent"
|
||||
| "_unstable_updateDelayedEvent"
|
||||
| "_unstable_cancelScheduledDelayedEvent"
|
||||
| "_unstable_restartScheduledDelayedEvent"
|
||||
| "_unstable_sendScheduledDelayedEvent"
|
||||
| "_unstable_sendStickyEvent"
|
||||
| "_unstable_sendStickyDelayedEvent"
|
||||
| "cancelPendingEvent"
|
||||
|
||||
@@ -16,11 +16,7 @@ limitations under the License.
|
||||
import { AbortError } from "p-retry";
|
||||
|
||||
import { EventType, RelationType } from "../@types/event.ts";
|
||||
import {
|
||||
type ISendEventResponse,
|
||||
type SendDelayedEventResponse,
|
||||
UpdateDelayedEventAction,
|
||||
} from "../@types/requests.ts";
|
||||
import { type ISendEventResponse, type SendDelayedEventResponse } from "../@types/requests.ts";
|
||||
import { type EmptyObject } from "../@types/common.ts";
|
||||
import type { MatrixClient } from "../client.ts";
|
||||
import { ConnectionError, HTTPError, MatrixError } from "../http-api/errors.ts";
|
||||
@@ -169,7 +165,14 @@ function createReplaceActionUpdate(type: MembershipActionType, offset?: number):
|
||||
|
||||
type MembershipManagerClient = Pick<
|
||||
MatrixClient,
|
||||
"getUserId" | "getDeviceId" | "sendStateEvent" | "_unstable_sendDelayedStateEvent" | "_unstable_updateDelayedEvent"
|
||||
| "getUserId"
|
||||
| "getDeviceId"
|
||||
| "sendStateEvent"
|
||||
| "_unstable_sendDelayedStateEvent"
|
||||
| "_unstable_updateDelayedEvent"
|
||||
| "_unstable_cancelScheduledDelayedEvent"
|
||||
| "_unstable_restartScheduledDelayedEvent"
|
||||
| "_unstable_sendScheduledDelayedEvent"
|
||||
>;
|
||||
|
||||
/**
|
||||
@@ -544,7 +547,7 @@ export class MembershipManager
|
||||
private async cancelKnownDelayIdBeforeSendDelayedEvent(delayId: string): Promise<ActionUpdate> {
|
||||
// Remove all running updates and restarts
|
||||
return await this.client
|
||||
._unstable_updateDelayedEvent(delayId, UpdateDelayedEventAction.Cancel)
|
||||
._unstable_cancelScheduledDelayedEvent(delayId)
|
||||
.then(() => {
|
||||
this.state.delayId = undefined;
|
||||
this.resetRateLimitCounter(MembershipActionType.SendDelayedEvent);
|
||||
@@ -552,7 +555,7 @@ export class MembershipManager
|
||||
})
|
||||
.catch((e) => {
|
||||
const repeatActionType = MembershipActionType.SendDelayedEvent;
|
||||
const update = this.actionUpdateFromErrors(e, repeatActionType, "updateDelayedEvent");
|
||||
const update = this.actionUpdateFromErrors(e, repeatActionType, "cancelScheduledDelayedEvent");
|
||||
if (update) return update;
|
||||
|
||||
if (this.isNotFoundError(e)) {
|
||||
@@ -606,10 +609,7 @@ export class MembershipManager
|
||||
|
||||
// The obvious choice here would be to use the `IRequestOpts` to set the timeout. Since this call might be forwarded
|
||||
// to the widget driver this information would get lost. That is why we mimic the AbortError using the race.
|
||||
return await Promise.race([
|
||||
this.client._unstable_updateDelayedEvent(delayId, UpdateDelayedEventAction.Restart),
|
||||
abortPromise,
|
||||
])
|
||||
return await Promise.race([this.client._unstable_restartScheduledDelayedEvent(delayId), abortPromise])
|
||||
.then(() => {
|
||||
// Whenever we successfully restart the delayed event we update the `state.expectedServerDelayLeaveTs`
|
||||
// which stores the predicted timestamp at which the server will send the delayed leave event if there wont be any further
|
||||
@@ -637,7 +637,7 @@ export class MembershipManager
|
||||
if (this.isUnsupportedDelayedEndpoint(e)) return {};
|
||||
|
||||
// TODO this also needs a test: get rate limit while checking id delayed event is scheduled
|
||||
const update = this.actionUpdateFromErrors(e, repeatActionType, "updateDelayedEvent");
|
||||
const update = this.actionUpdateFromErrors(e, repeatActionType, "restartScheduledDelayedEvent");
|
||||
if (update) return update;
|
||||
|
||||
// In other error cases we have no idea what is happening
|
||||
@@ -647,7 +647,7 @@ export class MembershipManager
|
||||
|
||||
private async sendScheduledDelayedLeaveEventOrFallbackToSendLeaveEvent(delayId: string): Promise<ActionUpdate> {
|
||||
return await this.client
|
||||
._unstable_updateDelayedEvent(delayId, UpdateDelayedEventAction.Send)
|
||||
._unstable_sendScheduledDelayedEvent(delayId)
|
||||
.then(() => {
|
||||
this.state.hasMemberStateEvent = false;
|
||||
this.resetRateLimitCounter(MembershipActionType.SendScheduledDelayedLeaveEvent);
|
||||
@@ -661,7 +661,7 @@ export class MembershipManager
|
||||
this.state.delayId = undefined;
|
||||
return createInsertActionUpdate(repeatActionType);
|
||||
}
|
||||
const update = this.actionUpdateFromErrors(e, repeatActionType, "updateDelayedEvent");
|
||||
const update = this.actionUpdateFromErrors(e, repeatActionType, "sendScheduledDelayedEvent");
|
||||
if (update) return update;
|
||||
|
||||
// On any other error we fall back to SendLeaveEvent (this includes hard errors from rate limiting)
|
||||
|
||||
+7
-7
@@ -19,7 +19,7 @@ limitations under the License.
|
||||
* the public classes.
|
||||
*/
|
||||
|
||||
import { type ExtensibleEvent, ExtensibleEvents, type Optional } from "matrix-events-sdk";
|
||||
import { type ExtensibleEvent, ExtensibleEvents } from "matrix-events-sdk";
|
||||
|
||||
import type { IEventDecryptionResult } from "../@types/crypto.ts";
|
||||
import { logger } from "../logger.ts";
|
||||
@@ -270,7 +270,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
// addition to a falsy cached event value. We check the flag later on in
|
||||
// a public getter to decide if the cache is valid.
|
||||
private _hasCachedExtEv = false;
|
||||
private _cachedExtEv: Optional<ExtensibleEvent> = undefined;
|
||||
private _cachedExtEv?: ExtensibleEvent = undefined;
|
||||
|
||||
/** If we failed to decrypt this event, the reason for the failure. Otherwise, `null`. */
|
||||
private _decryptionFailureReason: DecryptionFailureCode | null = null;
|
||||
@@ -481,9 +481,9 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
*
|
||||
* @deprecated Use stable functions where possible.
|
||||
*/
|
||||
public get unstableExtensibleEvent(): Optional<ExtensibleEvent> {
|
||||
public get unstableExtensibleEvent(): ExtensibleEvent | undefined {
|
||||
if (!this._hasCachedExtEv) {
|
||||
this._cachedExtEv = ExtensibleEvents.parse(this.getEffectiveEvent());
|
||||
this._cachedExtEv = ExtensibleEvents.parse(this.getEffectiveEvent()) ?? undefined;
|
||||
}
|
||||
return this._cachedExtEv;
|
||||
}
|
||||
@@ -753,7 +753,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
|
||||
/**
|
||||
* Get the event state_key if it has one. If necessary, this will perform
|
||||
* string-unpacking on the state key, as per MSC3414. This will return
|
||||
* string-unpacking on the state key, as per MSC4362. This will return
|
||||
* <code>undefined</code> for message events.
|
||||
* @returns The event's `state_key`.
|
||||
*/
|
||||
@@ -766,7 +766,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
|
||||
/**
|
||||
* Get the raw event state_key if it has one. This may be string-packed as per
|
||||
* MSC3414 if the state event is encrypted. This will return <code>undefined
|
||||
* MSC4362 if the state event is encrypted. This will return <code>undefined
|
||||
* </code> for message events.
|
||||
* @returns The event's `state_key`.
|
||||
*/
|
||||
@@ -789,7 +789,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* @returns The user's room membership, or `undefined` if the server does
|
||||
* not report it.
|
||||
*/
|
||||
public getMembershipAtEvent(): Optional<Membership | string> {
|
||||
public getMembershipAtEvent(): Membership | string | undefined {
|
||||
const unsigned = this.getUnsigned();
|
||||
return UNSIGNED_MEMBERSHIP_FIELD.findIn<Membership | string>(unsigned);
|
||||
}
|
||||
|
||||
+15
-20
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { M_POLL_START, type Optional } from "matrix-events-sdk";
|
||||
import { M_POLL_START } from "matrix-events-sdk";
|
||||
|
||||
import {
|
||||
DuplicateStrategy,
|
||||
@@ -1196,7 +1196,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
// Get the main TimelineSet
|
||||
const timelineSet = this.getUnfilteredTimelineSet();
|
||||
|
||||
let newTimeline: Optional<EventTimeline>;
|
||||
let newTimeline: EventTimeline | null = null;
|
||||
// If there isn't any event in the timeline, let's go fetch the latest
|
||||
// event and construct a timeline from it.
|
||||
//
|
||||
@@ -1783,12 +1783,11 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
allowDefault = true,
|
||||
useAuthentication: boolean = false,
|
||||
): string | null {
|
||||
const roomAvatarEvent = this.currentState.getStateEvents(EventType.RoomAvatar, "");
|
||||
if (!roomAvatarEvent && !allowDefault) {
|
||||
const mainUrl = this.getMxcAvatarUrl();
|
||||
if (!mainUrl && !allowDefault) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mainUrl = roomAvatarEvent ? roomAvatarEvent.getContent().url : null;
|
||||
if (mainUrl) {
|
||||
return getHttpUriForMxc(
|
||||
baseUrl,
|
||||
@@ -1810,7 +1809,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* @returns the mxc avatar url or falsy
|
||||
*/
|
||||
public getMxcAvatarUrl(): string | null {
|
||||
return this.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url || null;
|
||||
const url = this.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent().url;
|
||||
return url && typeof url === "string" ? url : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1820,11 +1820,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* @returns The room's canonical alias, or null if there is none
|
||||
*/
|
||||
public getCanonicalAlias(): string | null {
|
||||
const canonicalAlias = this.currentState.getStateEvents(EventType.RoomCanonicalAlias, "");
|
||||
if (canonicalAlias) {
|
||||
return canonicalAlias.getContent().alias || null;
|
||||
}
|
||||
return null;
|
||||
const canonicalAlias = this.currentState.getStateEvents(EventType.RoomCanonicalAlias, "")?.getContent().alias;
|
||||
return canonicalAlias && typeof canonicalAlias === "string" ? canonicalAlias : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1832,9 +1829,9 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* @returns The room's alternative aliases, or an empty array
|
||||
*/
|
||||
public getAltAliases(): string[] {
|
||||
const canonicalAlias = this.currentState.getStateEvents(EventType.RoomCanonicalAlias, "");
|
||||
if (canonicalAlias) {
|
||||
return canonicalAlias.getContent().alt_aliases || [];
|
||||
const altAliases = this.currentState.getStateEvents(EventType.RoomCanonicalAlias, "")?.getContent().alt_aliases;
|
||||
if (Array.isArray(altAliases)) {
|
||||
return altAliases.filter((alias) => typeof alias === "string");
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@@ -2493,7 +2490,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
};
|
||||
|
||||
private updateThreadRootEvent = (
|
||||
timelineSet: Optional<EventTimelineSet>,
|
||||
timelineSet: EventTimelineSet | undefined,
|
||||
thread: Thread,
|
||||
toStartOfTimeline: boolean,
|
||||
recreateEvent: boolean,
|
||||
@@ -3640,13 +3637,11 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
*/
|
||||
private calculateRoomName(userId: string, ignoreRoomNameEvent = false): string {
|
||||
if (!ignoreRoomNameEvent) {
|
||||
// check for an alias, if any. for now, assume first alias is the
|
||||
// official one.
|
||||
const mRoomName = this.currentState.getStateEvents(EventType.RoomName, "");
|
||||
if (mRoomName?.getContent().name) {
|
||||
const name = this.currentState.getStateEvents(EventType.RoomName, "")?.getContent().name;
|
||||
if (name && typeof name === "string") {
|
||||
return this.roomNameGenerator({
|
||||
type: RoomNameType.Actual,
|
||||
name: mRoomName.getContent().name,
|
||||
name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Optional } from "matrix-events-sdk";
|
||||
|
||||
import { type MatrixClient, PendingEventOrdering } from "../client.ts";
|
||||
import { TypedReEmitter } from "../ReEmitter.ts";
|
||||
import { RelationType } from "../@types/event.ts";
|
||||
@@ -476,7 +474,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
}
|
||||
}
|
||||
|
||||
public async processEvent(event: Optional<MatrixEvent>): Promise<void> {
|
||||
public async processEvent(event: MatrixEvent | null | undefined): Promise<void> {
|
||||
if (event) {
|
||||
this.setEventMetadata(event);
|
||||
await this.fetchEditsWhereNeeded(event);
|
||||
@@ -686,14 +684,14 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
}
|
||||
}
|
||||
|
||||
public setEventMetadata(event: Optional<MatrixEvent>): void {
|
||||
public setEventMetadata(event: MatrixEvent | null | undefined): void {
|
||||
if (event) {
|
||||
EventTimeline.setEventMetadata(event, this.roomState, false);
|
||||
event.setThread(this);
|
||||
}
|
||||
}
|
||||
|
||||
public clearEventMetadata(event: Optional<MatrixEvent>): void {
|
||||
public clearEventMetadata(event: MatrixEvent | null | undefined): void {
|
||||
if (event) {
|
||||
event.setThread(undefined);
|
||||
delete event.event?.unsigned?.["m.relations"]?.[THREAD_RELATION_TYPE.name];
|
||||
@@ -739,7 +737,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
* A getter for the last event of the thread.
|
||||
* This might be a synthesized event, if so, it will not emit any events to listeners.
|
||||
*/
|
||||
public get replyToEvent(): Optional<MatrixEvent> {
|
||||
public get replyToEvent(): MatrixEvent | null {
|
||||
return this.lastPendingEvent ?? this.lastEvent ?? this.lastReply();
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
/** mapping of roomId → encryptor class */
|
||||
private roomEncryptors: Record<string, RoomEncryptor> = {};
|
||||
|
||||
/** mapping of room ID -> inviter ID for rooms pending MSC4268 key bundles */
|
||||
private readonly roomsPendingKeyBundles: Map<string, string> = new Map();
|
||||
|
||||
private eventDecryptor: EventDecryptor;
|
||||
private keyClaimManager: KeyClaimManager;
|
||||
private outgoingRequestProcessor: OutgoingRequestProcessor;
|
||||
@@ -164,7 +167,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
/** Crypto callbacks provided by the application */
|
||||
private readonly cryptoCallbacks: CryptoCallbacks,
|
||||
|
||||
/** Enable support for encrypted state events under MSC3414. */
|
||||
/** Enable support for encrypted state events under MSC4362. */
|
||||
private readonly enableEncryptedStateEvents: boolean = false,
|
||||
) {
|
||||
super();
|
||||
@@ -329,10 +332,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
/**
|
||||
* Implementation of {@link CryptoBackend.maybeAcceptKeyBundle}.
|
||||
*/
|
||||
public async maybeAcceptKeyBundle(roomId: string, inviter: string): Promise<void> {
|
||||
public async maybeAcceptKeyBundle(roomId: string, inviter: string): Promise<boolean> {
|
||||
// TODO: retry this if it gets interrupted or it fails. (https://github.com/matrix-org/matrix-rust-sdk/issues/5112)
|
||||
// TODO: do this in the background.
|
||||
// TODO: handle the bundle message arriving after the invite (https://github.com/element-hq/element-web/issues/30740)
|
||||
|
||||
const logger = new LogSpan(this.logger, `maybeAcceptKeyBundle(${roomId}, ${inviter})`);
|
||||
|
||||
@@ -352,7 +354,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
);
|
||||
if (!bundleData) {
|
||||
logger.info("No key bundle found for user");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.info(`Fetching key bundle ${bundleData.url}`);
|
||||
@@ -391,7 +393,17 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
logger.warn(`Error receiving encrypted bundle:`, err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoBackend.markRoomAsPendingKeyBundle}.
|
||||
*/
|
||||
public markRoomAsPendingKeyBundle(roomId: string, inviter: string): void {
|
||||
this.roomsPendingKeyBundles.set(roomId, inviter);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// CryptoApi implementation
|
||||
@@ -1703,6 +1715,37 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
senderVerified: encryptionInfo.isSenderVerified(),
|
||||
},
|
||||
});
|
||||
|
||||
// If we have received a room key bundle message, and have previously marked the room
|
||||
// IDs it references as pending key bundles, tell the Rust SDK to try and accept it,
|
||||
// just in case it was received after invite.
|
||||
//
|
||||
// We don't actually need to validate the contents of the bundle message, or do
|
||||
// anything with its contents at all. We simply want to inform the Rust SDK we have
|
||||
// received a new room key bundle that we might be able to download.
|
||||
if (
|
||||
isRoomKeyBundleMessage(parsedMessage) &&
|
||||
this.roomsPendingKeyBundles.has(parsedMessage.content.room_id)
|
||||
) {
|
||||
// No `await`-ing here, as this is called from inside the `/sync` loop.
|
||||
this.maybeAcceptKeyBundle(
|
||||
parsedMessage.content.room_id,
|
||||
this.roomsPendingKeyBundles.get(parsedMessage.content.room_id)!,
|
||||
).then(
|
||||
(success) => {
|
||||
if (success) {
|
||||
this.roomsPendingKeyBundles.delete(parsedMessage.content.room_id);
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
this.logger.error(
|
||||
`Error attempting to download key bundle for room ${parsedMessage.content.room_id}`,
|
||||
);
|
||||
this.logger.error(err);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case RustSdkCryptoJs.ProcessedToDeviceEventType.PlainText: {
|
||||
@@ -1774,7 +1817,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
return;
|
||||
}
|
||||
|
||||
if (config["io.element.msc3414.encrypt_state_events"] && this.enableEncryptedStateEvents) {
|
||||
if (config["io.element.msc4362.encrypt_state_events"] && this.enableEncryptedStateEvents) {
|
||||
this.logger.info("crypto Enabling state event encryption...");
|
||||
settings.encryptStateEvents = true;
|
||||
}
|
||||
@@ -2468,5 +2511,25 @@ function rustEncryptionInfoToJsEncryptionInfo(
|
||||
return { shieldColour, shieldReason };
|
||||
}
|
||||
|
||||
interface RoomKeyBundleMessage {
|
||||
type: "io.element.msc4268.room_key_bundle";
|
||||
content: {
|
||||
room_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the given payload is a RoomKeyBundleMessage.
|
||||
*
|
||||
* A RoomKeyBundleMessage is identified by having a specific message type
|
||||
* ("io.element.msc4268.room_key_bundle") and a valid room_id in its content.
|
||||
*
|
||||
* @param message - The received to-device message to check.
|
||||
* @returns True if the payload matches the RoomKeyBundleMessage structure, false otherwise.
|
||||
*/
|
||||
function isRoomKeyBundleMessage(message: IToDeviceEvent): message is IToDeviceEvent & RoomKeyBundleMessage {
|
||||
return message.type === "io.element.msc4268.room_key_bundle" && typeof message.content.room_id === "string";
|
||||
}
|
||||
|
||||
type CryptoEvents = (typeof CryptoEvent)[keyof typeof CryptoEvent];
|
||||
type RustCryptoEvents = Exclude<CryptoEvents, CryptoEvent.LegacyCryptoStoreMigrationProgress>;
|
||||
|
||||
+1
-3
@@ -23,8 +23,6 @@ limitations under the License.
|
||||
* for HTTP and WS at some point.
|
||||
*/
|
||||
|
||||
import { type Optional } from "matrix-events-sdk";
|
||||
|
||||
import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend.ts";
|
||||
import { User } from "./models/user.ts";
|
||||
import { NotificationCountType, Room, RoomEvent } from "./models/room.ts";
|
||||
@@ -208,7 +206,7 @@ export class SyncApi {
|
||||
private readonly opts: IStoredClientOpts;
|
||||
private readonly syncOpts: SyncApiOptions;
|
||||
|
||||
private _peekRoom: Optional<Room> = null;
|
||||
private _peekRoom: Room | null = null;
|
||||
private currentSyncRequest?: Promise<ISyncResponse>;
|
||||
private abortController?: AbortController;
|
||||
private syncState: SyncState | null = null;
|
||||
|
||||
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Optional } from "matrix-events-sdk";
|
||||
|
||||
import { type Direction, EventTimeline } from "./models/event-timeline.ts";
|
||||
import { logger } from "./logger.ts";
|
||||
import { type MatrixClient } from "./client.ts";
|
||||
@@ -105,7 +103,7 @@ export class TimelineWindow {
|
||||
public load(initialEventId?: string, initialWindowSize = 20): Promise<void> {
|
||||
// given an EventTimeline, find the event we were looking for, and initialise our
|
||||
// fields so that the event in question is in the middle of the window.
|
||||
const initFields = (timeline: Optional<EventTimeline>): void => {
|
||||
const initFields = (timeline: EventTimeline | null): void => {
|
||||
if (!timeline) {
|
||||
throw new Error("No timeline given to initFields");
|
||||
}
|
||||
|
||||
+1
-2
@@ -20,7 +20,6 @@ limitations under the License.
|
||||
|
||||
import unhomoglyph from "unhomoglyph";
|
||||
import promiseRetry from "p-retry";
|
||||
import { type Optional } from "matrix-events-sdk";
|
||||
|
||||
import { type IEvent, type MatrixEvent } from "./models/event.ts";
|
||||
import { M_TIMESTAMP } from "./@types/location.ts";
|
||||
@@ -115,7 +114,7 @@ export function decodeParams(query: string): Record<string, string | string[]> {
|
||||
* variables with. E.g. `{ "$bar": "baz" }`.
|
||||
* @returns The result of replacing all template variables e.g. '/foo/baz'.
|
||||
*/
|
||||
export function encodeUri(pathTemplate: string, variables: Record<string, Optional<string>>): string {
|
||||
export function encodeUri(pathTemplate: string, variables: Record<string, string | null | undefined>): string {
|
||||
for (const key in variables) {
|
||||
if (!variables.hasOwnProperty(key)) {
|
||||
continue;
|
||||
|
||||
+40
-23
@@ -264,8 +264,19 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
}
|
||||
|
||||
if (!canReuseStream) {
|
||||
const constraints = this.getUserMediaContraints(shouldRequestAudio, shouldRequestVideo);
|
||||
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
let constraints: MediaStreamConstraints;
|
||||
try {
|
||||
// Not specifying exact for deviceId means switching devices does not always work,
|
||||
// try with exact and fallback to ideal if it fails
|
||||
constraints = this.getUserMediaContraints(shouldRequestAudio, shouldRequestVideo, true);
|
||||
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
`MediaHandler getUserMediaStreamInternal() error (e=${e}), retrying without exact deviceId`,
|
||||
);
|
||||
constraints = this.getUserMediaContraints(shouldRequestAudio, shouldRequestVideo, false);
|
||||
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
}
|
||||
logger.log(
|
||||
`MediaHandler getUserMediaStreamInternal() calling getUserMediaStream (streamId=${
|
||||
stream.id
|
||||
@@ -435,30 +446,36 @@ export class MediaHandler extends TypedEventEmitter<
|
||||
this.emit(MediaHandlerEvent.LocalStreamsChanged);
|
||||
}
|
||||
|
||||
private getUserMediaContraints(audio: boolean, video: boolean): MediaStreamConstraints {
|
||||
private getUserMediaContraints(audio: boolean, video: boolean, exactDeviceId?: boolean): MediaStreamConstraints {
|
||||
const isWebkit = !!navigator.webkitGetUserMedia;
|
||||
const deviceIdKey = exactDeviceId ? "exact" : "ideal";
|
||||
|
||||
const audioConstraints: MediaTrackConstraints = {};
|
||||
if (this.audioInput) {
|
||||
audioConstraints.deviceId = { [deviceIdKey]: this.audioInput };
|
||||
}
|
||||
if (this.audioSettings) {
|
||||
audioConstraints.autoGainControl = { ideal: this.audioSettings.autoGainControl };
|
||||
audioConstraints.echoCancellation = { ideal: this.audioSettings.echoCancellation };
|
||||
audioConstraints.noiseSuppression = { ideal: this.audioSettings.noiseSuppression };
|
||||
}
|
||||
|
||||
const videoConstraints: MediaTrackConstraints = {
|
||||
/* We want 640x360. Chrome will give it only if we ask exactly,
|
||||
FF refuses entirely if we ask exactly, so have to ask for ideal
|
||||
instead
|
||||
XXX: Is this still true?
|
||||
*/
|
||||
width: isWebkit ? { exact: 640 } : { ideal: 640 },
|
||||
height: isWebkit ? { exact: 360 } : { ideal: 360 },
|
||||
};
|
||||
if (this.videoInput) {
|
||||
videoConstraints.deviceId = { [deviceIdKey]: this.videoInput };
|
||||
}
|
||||
|
||||
return {
|
||||
audio: audio
|
||||
? {
|
||||
deviceId: this.audioInput ? { ideal: this.audioInput } : undefined,
|
||||
autoGainControl: this.audioSettings ? { ideal: this.audioSettings.autoGainControl } : undefined,
|
||||
echoCancellation: this.audioSettings ? { ideal: this.audioSettings.echoCancellation } : undefined,
|
||||
noiseSuppression: this.audioSettings ? { ideal: this.audioSettings.noiseSuppression } : undefined,
|
||||
}
|
||||
: false,
|
||||
video: video
|
||||
? {
|
||||
deviceId: this.videoInput ? { ideal: this.videoInput } : undefined,
|
||||
/* We want 640x360. Chrome will give it only if we ask exactly,
|
||||
FF refuses entirely if we ask exactly, so have to ask for ideal
|
||||
instead
|
||||
XXX: Is this still true?
|
||||
*/
|
||||
width: isWebkit ? { exact: 640 } : { ideal: 640 },
|
||||
height: isWebkit ? { exact: 360 } : { ideal: 360 },
|
||||
}
|
||||
: false,
|
||||
audio: audio ? audioConstraints : false,
|
||||
video: video ? videoConstraints : false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user