Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26d5b1cde2 | |||
| aba7f8a0d4 | |||
| 5495153c63 | |||
| 4f0696e2a4 | |||
| 0e659d294e | |||
| e74eb4928e | |||
| 327d2fa7c8 | |||
| 028357f15f | |||
| 872ec6755e | |||
| 333d6a7bd6 | |||
| 47532de452 | |||
| 87e1049dae | |||
| 4c8e38009f | |||
| 6e3efef0c5 | |||
| fb590627bb | |||
| 24cc17c270 | |||
| 68084e8fc3 | |||
| 0c3bb1f246 | |||
| 7f42b67f68 | |||
| 9b871ac969 | |||
| 49f7972a9e | |||
| c5ae4c8c0d | |||
| 2423300acd | |||
| 6cafa175b8 | |||
| 40165942e3 | |||
| f301251ff5 | |||
| 21cd5e98c1 | |||
| db070dca57 | |||
| 8b6ff0abcb | |||
| f2157f28bb | |||
| 739b8e1f89 | |||
| bc57a3f829 | |||
| f136f6ddf7 | |||
| d428e7119a | |||
| 5532066178 | |||
| 82b51d0d46 | |||
| fb12a5a1d6 | |||
| 61ea5a7dfc | |||
| 4e032317fe | |||
| dbb2ae5c07 | |||
| c8032a214e | |||
| 6e34ca6f2d | |||
| 4a7a699623 | |||
| 774776178e | |||
| ed4078528d | |||
| 7224961e9e | |||
| 35ca07e8fb | |||
| ccffb5df2d | |||
| 0d162f66f3 |
+1
-1
@@ -1,7 +1,7 @@
|
||||
* @matrix-org/element-web-reviewers
|
||||
/.github/workflows/** @matrix-org/element-web-team
|
||||
/package.json @matrix-org/element-web-team
|
||||
/yarn.lock @matrix-org/element-web-team
|
||||
/pnpm-lock.yaml @matrix-org/element-web-team
|
||||
/scripts/** @matrix-org/element-web-team
|
||||
/src/webrtc @matrix-org/element-call-reviewers
|
||||
/src/matrixrtc @matrix-org/element-call-reviewers
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
|
||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4
|
||||
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: ${{ matrix.repo }}
|
||||
|
||||
@@ -21,13 +21,14 @@ jobs:
|
||||
ref: staging
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
node-version-file: package.json
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- uses: t3chguy/release-drafter@105e541c2c3d857f032bd522c0764694758fabad
|
||||
id: draft-release
|
||||
|
||||
@@ -33,13 +33,14 @@ jobs:
|
||||
sparse-checkout: |
|
||||
scripts/release
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Set up git
|
||||
run: |
|
||||
@@ -73,7 +74,7 @@ jobs:
|
||||
fi
|
||||
|
||||
echo "Resetting $PACKAGE to develop branch..."
|
||||
yarn add "github:matrix-org/$PACKAGE#develop"
|
||||
pnpm add "github:matrix-org/$PACKAGE#develop"
|
||||
git add -u
|
||||
git commit -m "Reset $PACKAGE back to develop branch"
|
||||
done <<< "$DEPENDENCIES"
|
||||
|
||||
@@ -123,13 +123,14 @@ jobs:
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Handle develop dependencies
|
||||
run: |
|
||||
@@ -140,14 +141,14 @@ jobs:
|
||||
VERSION=${dep[1]}
|
||||
|
||||
echo "::warning title=Develop dependency found::$DEPENDENCY will be kept at $VERSION"
|
||||
yarn upgrade "$PACKAGE@$VERSION" --exact
|
||||
pnpm add "$PACKAGE@$VERSION" --save-exact
|
||||
git add -u
|
||||
git commit -m "Keep $PACKAGE at $VERSION"
|
||||
done
|
||||
|
||||
- name: Bump package.json version
|
||||
run: |
|
||||
yarn version --no-git-tag-version --new-version "${VERSION#v}"
|
||||
pnpm version --no-git-tag-version "${VERSION#v}"
|
||||
git add package.json
|
||||
|
||||
- name: Add to CHANGELOG.md
|
||||
@@ -175,7 +176,7 @@ jobs:
|
||||
|
||||
- name: Build assets
|
||||
if: steps.prepare.outputs.has-dist-script == '1'
|
||||
run: DIST_VERSION="$VERSION" yarn dist
|
||||
run: DIST_VERSION="$VERSION" pnpm dist
|
||||
|
||||
- name: Upload release assets & signatures
|
||||
if: inputs.asset-path
|
||||
|
||||
@@ -21,10 +21,11 @@ jobs:
|
||||
with:
|
||||
ref: staging
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- name: 🔧 pnpm cache
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
node-version-file: package.json
|
||||
|
||||
@@ -33,7 +34,7 @@ jobs:
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: 🚀 Publish to npm
|
||||
id: npm-publish
|
||||
|
||||
@@ -50,9 +50,10 @@ jobs:
|
||||
ref: staging
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Bump dependency
|
||||
@@ -61,8 +62,8 @@ jobs:
|
||||
run: |
|
||||
git config --global user.email "releases@riot.im"
|
||||
git config --global user.name "RiotRobot"
|
||||
yarn upgrade "$DEPENDENCY" --exact
|
||||
git add package.json yarn.lock
|
||||
pnpm add "$DEPENDENCY" --save-exact
|
||||
git add package.json pnpm-lock.yaml
|
||||
git commit -am"Upgrade dependency to $DEPENDENCY"
|
||||
git push origin staging
|
||||
|
||||
@@ -75,17 +76,18 @@ jobs:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- name: 🔧 pnpm cache
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: 📖 Generate docs
|
||||
run: yarn gendoc
|
||||
run: pnpm gendoc
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
steps:
|
||||
# We create the status here and then update it to success/failure in the `report` stage
|
||||
# This provides an easy link to this workflow_run from the PR before Sonarcloud is done.
|
||||
- uses: guibranco/github-status-action-v2@5530c593759f489bba08272e96986ffc571c1ea1
|
||||
- uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@9f6f057c1b30500877460c66d4ae937e53d168ee
|
||||
uses: matrix-org/sonarcloud-workflow-action@ea0cd9dbd5562e79816685972bc0d03c235a900c
|
||||
# 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:
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
- uses: guibranco/github-status-action-v2@5530c593759f489bba08272e96986ffc571c1ea1
|
||||
- uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -16,16 +16,17 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
run: "pnpm run lint:types"
|
||||
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
@@ -33,16 +34,17 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:js"
|
||||
run: "pnpm run lint:js"
|
||||
|
||||
node_example_lint:
|
||||
name: "Node.js example"
|
||||
@@ -50,22 +52,17 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Build Types
|
||||
run: "yarn build:types"
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
cache: "npm"
|
||||
node-version-file: "examples/node/package.json"
|
||||
# cache-dependency-path: '**/package-lock.json'
|
||||
run: "pnpm build:types"
|
||||
|
||||
- name: Install Example Deps
|
||||
run: "npm install"
|
||||
@@ -85,16 +82,17 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Run Linter
|
||||
run: "yarn lint:workflows"
|
||||
run: "pnpm lint:workflows"
|
||||
|
||||
docs:
|
||||
name: "JSDoc Checker"
|
||||
@@ -102,16 +100,17 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
|
||||
run: "pnpm run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
@@ -127,16 +126,17 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
run: "pnpm install --frozen-lockfile"
|
||||
|
||||
- name: Run linter
|
||||
run: "yarn run lint:knip"
|
||||
run: "pnpm run lint:knip"
|
||||
|
||||
element-web:
|
||||
name: Downstream tsc element-web
|
||||
@@ -147,9 +147,10 @@ jobs:
|
||||
with:
|
||||
repository: element-hq/element-web
|
||||
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Install Dependencies
|
||||
@@ -159,7 +160,7 @@ jobs:
|
||||
JS_SDK_GITHUB_BASE_REF: ${{ github.sha }}
|
||||
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
run: "pnpm run lint:types"
|
||||
|
||||
# Hook for branch protection to skip downstream typechecking outside of merge queues
|
||||
downstream:
|
||||
|
||||
@@ -24,15 +24,16 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- name: Setup Node
|
||||
id: setupNode
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache: "pnpm"
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
run: "pnpm install"
|
||||
|
||||
- name: Get number of CPU cores
|
||||
id: cpu-cores
|
||||
@@ -40,7 +41,7 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
yarn test \
|
||||
pnpm test \
|
||||
--coverage=${{ env.ENABLE_COVERAGE }} \
|
||||
--maxWorkers ${{ steps.cpu-cores.outputs.count }} \
|
||||
./spec/${{ matrix.specs }}
|
||||
@@ -112,7 +113,7 @@ jobs:
|
||||
steps:
|
||||
- name: Skip SonarCloud on merge queues
|
||||
if: env.ENABLE_COVERAGE == 'false'
|
||||
uses: guibranco/github-status-action-v2@5530c593759f489bba08272e96986ffc571c1ea1
|
||||
uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
/.npmrc
|
||||
/*.log
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
.lock-wscript
|
||||
build/Release
|
||||
|
||||
@@ -1,3 +1,67 @@
|
||||
Changes in [41.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.0.0) (2026-02-24)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* Add support for Matrix Spec v1.13 ([#5160](https://github.com/matrix-org/matrix-js-sdk/pull/5160)). Contributed by @t3chguy.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Download room keys from backup prior to buliding historic room key bundles ([#5171](https://github.com/matrix-org/matrix-js-sdk/pull/5171)). Contributed by @kaylendog.
|
||||
* Add support for Matrix Spec v1.13 ([#5160](https://github.com/matrix-org/matrix-js-sdk/pull/5160)). Contributed by @t3chguy.
|
||||
* Add logging on MSC4108 DELETE request ([#5140](https://github.com/matrix-org/matrix-js-sdk/pull/5140)). Contributed by @reivilibre.
|
||||
* Add `m.invite_permission_config` account data type ([#5183](https://github.com/matrix-org/matrix-js-sdk/pull/5183)). Contributed by @richvdh.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* fix(relations): prevent stale m.replace from overriding newer edits ([#5192](https://github.com/matrix-org/matrix-js-sdk/pull/5192)). Contributed by @basnijholt.
|
||||
* Fix reactive display name disambiguation ([#5135](https://github.com/matrix-org/matrix-js-sdk/pull/5135)). Contributed by @aditya-cherukuru.
|
||||
* Fix empty string to room compatibility trick to only apply to m.call ([#5172](https://github.com/matrix-org/matrix-js-sdk/pull/5172)). Contributed by @toger5.
|
||||
|
||||
|
||||
Changes in [40.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.2.0) (2026-02-10)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* [MatrixRTC] Remove sending of deprecated `notify` event (we now use `m.rtc.notification`) ([#5167](https://github.com/matrix-org/matrix-js-sdk/pull/5167)). Contributed by @toger5.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Use stable /auth\_metadata endpoint where homeserver supports v1.15 ([#5174](https://github.com/matrix-org/matrix-js-sdk/pull/5174)). Contributed by @hughns.
|
||||
* Support additional\_creators in upgradeRoom (MSC4289) ([#5173](https://github.com/matrix-org/matrix-js-sdk/pull/5173)). Contributed by @andybalaam.
|
||||
* [MatrixRTC] Minimal change to transition from "" to "ROOM" as the callId/slotId ([#5166](https://github.com/matrix-org/matrix-js-sdk/pull/5166)). Contributed by @toger5.
|
||||
* [MatrixRTC] Do not send the `livekit_alias` in sticky events ([#5165](https://github.com/matrix-org/matrix-js-sdk/pull/5165)). Contributed by @toger5.
|
||||
* Improve startup performance by using `promise.all` when processing rooms from sync ([#5095](https://github.com/matrix-org/matrix-js-sdk/pull/5095)). Contributed by @MidhunSureshR.
|
||||
* Add OAuthGrantType enum for OAuth 2.0 API grant types ([#5161](https://github.com/matrix-org/matrix-js-sdk/pull/5161)). Contributed by @hughns.
|
||||
* Add support for stable OAuth2.0 aware feature from MSC3824 ([#5159](https://github.com/matrix-org/matrix-js-sdk/pull/5159)). Contributed by @hughns.
|
||||
* Give RoomWidgetClient the ability to send and receive sticky events ([#5142](https://github.com/matrix-org/matrix-js-sdk/pull/5142)). Contributed by @robintown.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [js sdk embedded/widget] Fix race where this.syncApi.injectRoomEvents was called before the syncApi is instantiated ([#5168](https://github.com/matrix-org/matrix-js-sdk/pull/5168)). Contributed by @toger5.
|
||||
* [MatrixRTC] Fix delayId not resetting on leave ([#5156](https://github.com/matrix-org/matrix-js-sdk/pull/5156)). Contributed by @toger5.
|
||||
|
||||
|
||||
Changes in [40.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.1.0) (2026-01-27)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate unused `EventShieldReason` reason codes ([#5127](https://github.com/matrix-org/matrix-js-sdk/pull/5127)). Contributed by @richvdh.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Add stable m.oauth UIA stage enum ([#5138](https://github.com/matrix-org/matrix-js-sdk/pull/5138)). Contributed by @hughns.
|
||||
* Add `MatrixEvent.getKeyForwardingUser` ([#5128](https://github.com/matrix-org/matrix-js-sdk/pull/5128)). Contributed by @richvdh.
|
||||
* Add types for (unstable) policy servers ([#5116](https://github.com/matrix-org/matrix-js-sdk/pull/5116)). Contributed by @turt2live.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Recalculate room name on loading members ([#5164](https://github.com/matrix-org/matrix-js-sdk/pull/5164)). Contributed by @RiotRobot.
|
||||
* Avoid rapidly retrying failed requests ([#5146](https://github.com/matrix-org/matrix-js-sdk/pull/5146)). Contributed by @andybalaam.
|
||||
* [matrixRTC] MatrixRTCSessions, add missing event reemission. ([#5144](https://github.com/matrix-org/matrix-js-sdk/pull/5144)). Contributed by @toger5.
|
||||
* Use normal base64 encoding for RTC backend identities ([#5129](https://github.com/matrix-org/matrix-js-sdk/pull/5129)). Contributed by @robintown.
|
||||
* export parseCallNotificationContent and isMyMembership from RTC types ([#5132](https://github.com/matrix-org/matrix-js-sdk/pull/5132)). Contributed by @Half-Shot.
|
||||
|
||||
|
||||
Changes in [40.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v40.0.0) (2026-01-13)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
@@ -41,10 +41,10 @@ endpoints from before Matrix 1.1, for example.
|
||||
> Servers may require or use authenticated endpoints for media (images, files, avatars, etc). See the
|
||||
> [Authenticated Media](#authenticated-media) section for information on how to enable support for this.
|
||||
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
|
||||
if you do not have it already.
|
||||
Using `pnpm` instead of `npm` is recommended. Please see the pnpm [install
|
||||
guide](https://pnpm.io/installation#using-corepack) if you do not have it already.
|
||||
|
||||
`yarn add matrix-js-sdk`
|
||||
`pnpm add matrix-js-sdk`
|
||||
|
||||
```javascript
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
@@ -310,7 +310,7 @@ This SDK uses [Typedoc](https://typedoc.org/guides/doccomments) doc comments. Yo
|
||||
host the API reference from the source files like this:
|
||||
|
||||
```
|
||||
$ yarn gendoc
|
||||
$ pnpm gendoc
|
||||
$ cd docs
|
||||
$ python -m http.server 8005
|
||||
```
|
||||
@@ -453,7 +453,7 @@ want to use this SDK, skip this section._
|
||||
First, you need to pull in the right build tools:
|
||||
|
||||
```
|
||||
$ yarn install
|
||||
$ pnpm install
|
||||
```
|
||||
|
||||
## Building
|
||||
@@ -461,17 +461,17 @@ First, you need to pull in the right build tools:
|
||||
To build a browser version from scratch when developing:
|
||||
|
||||
```
|
||||
$ yarn build
|
||||
$ pnpm build
|
||||
```
|
||||
|
||||
To run tests:
|
||||
|
||||
```
|
||||
$ yarn test
|
||||
$ pnpm test
|
||||
```
|
||||
|
||||
To run linting:
|
||||
|
||||
```
|
||||
$ yarn lint
|
||||
$ pnpm lint
|
||||
```
|
||||
|
||||
@@ -21,4 +21,4 @@ export PATH="$rootdir/node_modules/.bin:$PATH"
|
||||
|
||||
# now run our checks
|
||||
cd "$tmpdir"
|
||||
yarn lint
|
||||
pnpm lint
|
||||
|
||||
+22
-11
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "40.1.0-rc.0",
|
||||
"version": "41.0.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
"prepare": "pnpm build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"clean": "rimraf lib",
|
||||
"build": "yarn clean && yarn build:compile && yarn build:types",
|
||||
"build": "pnpm clean && pnpm build:compile && pnpm build:types",
|
||||
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
|
||||
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"gendoc": "typedoc",
|
||||
"lint": "yarn lint:types && yarn lint:js && yarn lint:workflows",
|
||||
"lint": "pnpm lint:types && pnpm lint:js && pnpm lint:workflows",
|
||||
"lint:js": "eslint --max-warnings 0 src spec && prettier --check .",
|
||||
"lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
@@ -21,7 +21,7 @@
|
||||
"lint:knip": "knip",
|
||||
"test": "vitest",
|
||||
"test:watch": "vitest --watch",
|
||||
"coverage": "yarn test --coverage"
|
||||
"coverage": "pnpm test --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -49,14 +49,14 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^17.0.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^17.1.0",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^6.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"loglevel": "^1.9.2",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.14.0",
|
||||
"matrix-widget-api": "^1.16.1",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
"p-retry": "7",
|
||||
"sdp-transform": "^3.0.0",
|
||||
@@ -84,7 +84,7 @@
|
||||
"@stylistic/eslint-plugin": "^5.0.0",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/node": "18",
|
||||
"@types/node": "22",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
@@ -98,7 +98,7 @@
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-import-resolver-typescript": "^4.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsdoc": "^61.0.0",
|
||||
"eslint-plugin-jsdoc": "^62.0.0",
|
||||
"eslint-plugin-matrix-org": "^3.0.0",
|
||||
"eslint-plugin-n": "^14.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.5.0",
|
||||
@@ -110,7 +110,7 @@
|
||||
"knip": "^5.0.0",
|
||||
"lint-staged": "^16.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"prettier": "3.7.4",
|
||||
"prettier": "3.8.1",
|
||||
"rimraf": "^6.0.0",
|
||||
"typedoc": "^0.28.1",
|
||||
"typedoc-plugin-coverage": "^4.0.0",
|
||||
@@ -122,5 +122,16 @@
|
||||
},
|
||||
"resolutions": {
|
||||
"expect": "30.2.0"
|
||||
}
|
||||
},
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
"eslint": "8"
|
||||
}
|
||||
},
|
||||
"allowedDeprecatedVersions": {
|
||||
"eslint": "8"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264"
|
||||
}
|
||||
|
||||
Generated
+7805
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
nodeLinker: hoisted
|
||||
@@ -22,7 +22,9 @@ import {
|
||||
createClient,
|
||||
DebugLogger,
|
||||
EventType,
|
||||
HistoryVisibility,
|
||||
type IContent,
|
||||
type IRoomEvent,
|
||||
KnownMembership,
|
||||
type MatrixClient,
|
||||
MsgType,
|
||||
@@ -36,6 +38,15 @@ import { flushPromises } from "../../test-utils/flushPromises.ts";
|
||||
import { E2EOTKClaimResponder } from "../../test-utils/E2EOTKClaimResponder.ts";
|
||||
import { escapeRegExp } from "../../../src/utils.ts";
|
||||
import { EventShieldColour, EventShieldReason } from "../../../src/crypto-api";
|
||||
import {
|
||||
BACKUP_DECRYPTION_KEY_BASE64,
|
||||
CLEAR_EVENT,
|
||||
ENCRYPTED_EVENT,
|
||||
SIGNED_BACKUP_DATA,
|
||||
TEST_ROOM_ID,
|
||||
TEST_USER_ID,
|
||||
PER_ROOM_CURVE25519_KEY_BACKUP_DATA,
|
||||
} from "../../test-utils/test-data";
|
||||
|
||||
const debug = mkDebug("matrix-js-sdk:history-sharing");
|
||||
|
||||
@@ -86,7 +97,7 @@ describe("History Sharing", () => {
|
||||
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
const aliceId = "@alice:localhost";
|
||||
const aliceId = TEST_USER_ID;
|
||||
const bobId = "@bob:xyz";
|
||||
|
||||
const aliceKeyReceiver = new E2EKeyReceiver(ALICE_HOMESERVER_URL, "alice-");
|
||||
@@ -117,7 +128,7 @@ describe("History Sharing", () => {
|
||||
|
||||
test("Room keys are successfully shared on invite", async () => {
|
||||
// Alice is in an encrypted room
|
||||
const syncResponse = getSyncResponse([aliceClient.getSafeUserId()], ROOM_ID);
|
||||
const syncResponse = getSyncResponse([aliceClient.getSafeUserId()], HistoryVisibility.Shared, ROOM_ID);
|
||||
aliceSyncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
@@ -127,60 +138,15 @@ describe("History Sharing", () => {
|
||||
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(), (callLog) => {
|
||||
const body = callLog.options.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 to-device event and the room invite
|
||||
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] } } } },
|
||||
to_device: {
|
||||
events: [
|
||||
{
|
||||
type: "m.room.encrypted",
|
||||
sender: aliceClient.getSafeUserId(),
|
||||
content: bobToDeviceMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
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,
|
||||
});
|
||||
fetchMock.getOnce(`begin:${BOB_HOMESERVER_URL}/_matrix/client/v1/media/download/alice-server/here`, {
|
||||
body: uploadedBlob,
|
||||
});
|
||||
await bobClient.joinRoom(ROOM_ID, { acceptSharedHistory: true });
|
||||
// Alice invites Bob, and shares the room history with them.
|
||||
await assertInviteAndShareHistory(ROOM_ID);
|
||||
|
||||
// Bob receives, should be able to decrypt, the megolm message
|
||||
const bobSyncResponse = getSyncResponse([aliceClient.getSafeUserId(), bobClient.getSafeUserId()], ROOM_ID);
|
||||
const bobSyncResponse = getSyncResponse(
|
||||
[aliceClient.getSafeUserId(), bobClient.getSafeUserId()],
|
||||
HistoryVisibility.Shared,
|
||||
ROOM_ID,
|
||||
);
|
||||
bobSyncResponse.rooms.join[ROOM_ID].timeline.events.push(
|
||||
mkEventCustom({
|
||||
type: "m.room.encrypted",
|
||||
@@ -206,7 +172,7 @@ describe("History Sharing", () => {
|
||||
|
||||
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);
|
||||
const syncResponse = getSyncResponse([aliceClient.getSafeUserId()], HistoryVisibility.Shared, ROOM_ID);
|
||||
aliceSyncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
@@ -257,7 +223,11 @@ describe("History Sharing", () => {
|
||||
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);
|
||||
const bobSyncResponse = getSyncResponse(
|
||||
[aliceClient.getSafeUserId(), bobClient.getSafeUserId()],
|
||||
HistoryVisibility.Shared,
|
||||
ROOM_ID,
|
||||
);
|
||||
bobSyncResponse.rooms.join[ROOM_ID].timeline.events.push(
|
||||
mkEventCustom({
|
||||
type: "m.room.encrypted",
|
||||
@@ -304,11 +274,123 @@ describe("History Sharing", () => {
|
||||
expect(encryptionInfo?.shieldReason).toEqual(EventShieldReason.AUTHENTICITY_NOT_GUARANTEED);
|
||||
});
|
||||
|
||||
test("Room keys are downloaded from key backup before inviting", async () => {
|
||||
// Set up backup, and ignore requests to send room key requests
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", SIGNED_BACKUP_DATA);
|
||||
fetchMock.get(
|
||||
`express:/_matrix/client/v3/room_keys/keys/${encodeURIComponent(TEST_ROOM_ID)}`,
|
||||
PER_ROOM_CURVE25519_KEY_BACKUP_DATA,
|
||||
);
|
||||
|
||||
await aliceClient
|
||||
.getCrypto()!
|
||||
.storeSessionBackupPrivateKey(
|
||||
Buffer.from(BACKUP_DECRYPTION_KEY_BASE64, "base64"),
|
||||
SIGNED_BACKUP_DATA.version!,
|
||||
);
|
||||
|
||||
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
|
||||
|
||||
// Alice is in an encrypted room.
|
||||
const syncResponse = getSyncResponse([aliceClient.getSafeUserId()], HistoryVisibility.Shared, TEST_ROOM_ID);
|
||||
aliceSyncResponder.sendOrQueueSyncResponse(syncResponse);
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Alice invites Bob, and shares the room history with them.
|
||||
await assertInviteAndShareHistory(TEST_ROOM_ID);
|
||||
|
||||
// Bob receives, and should be able to decrypt, the historical message
|
||||
const bobSyncResponse = getSyncResponse(
|
||||
[aliceClient.getSafeUserId(), bobClient.getSafeUserId()],
|
||||
HistoryVisibility.Shared,
|
||||
TEST_ROOM_ID,
|
||||
);
|
||||
bobSyncResponse.rooms.join[TEST_ROOM_ID].timeline.events.push(ENCRYPTED_EVENT as IRoomEvent);
|
||||
bobSyncResponder.sendOrQueueSyncResponse(bobSyncResponse);
|
||||
await syncPromise(bobClient);
|
||||
|
||||
const bobRoom = bobClient.getRoom(TEST_ROOM_ID);
|
||||
const event = bobRoom!.getLastLiveEvent()!;
|
||||
expect(event.getId()).toEqual(ENCRYPTED_EVENT.event_id);
|
||||
await event.getDecryptionPromise();
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent().body).toEqual(CLEAR_EVENT.content!.body);
|
||||
expect(event.getKeyForwardingUser()).toEqual(aliceClient.getUserId());
|
||||
const encryptionInfo = await bobClient.getCrypto()!.getEncryptionInfoForEvent(event);
|
||||
expect(encryptionInfo?.shieldColour).toEqual(EventShieldColour.GREY);
|
||||
expect(encryptionInfo?.shieldReason).toEqual(EventShieldReason.AUTHENTICITY_NOT_GUARANTEED);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
bobClient.stopClient();
|
||||
aliceClient.stopClient();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to automatically test that room history is shared on invite.
|
||||
* The function performs the following:
|
||||
*
|
||||
* 1. Sets up the relevant fetchMock and to-device event listeners for Alice.
|
||||
* 2. Alice invites Bob to the room.
|
||||
* 3. Checks the key bundle was uploaded and that the `m.room_key_bundle`
|
||||
* to-device message was sent.
|
||||
* 4. Sends the invite event to Bob and ensures it is processed correctly.
|
||||
* 5. Sets up the relevant fetchMock listeners for Bob.
|
||||
* 5. Simulates Bob joining the room and verifies that the room history is shared.
|
||||
*
|
||||
* @param roomId The ID of the room where the invite and history sharing will be tested.
|
||||
*/
|
||||
async function assertInviteAndShareHistory(roomId: string): Promise<void> {
|
||||
const uploadProm = new Promise<Uint8Array>((resolve) => {
|
||||
fetchMock.postOnce(new URL("/_matrix/media/v3/upload", ALICE_HOMESERVER_URL).toString(), (callLog) => {
|
||||
const body = callLog.options.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");
|
||||
fetchMock.postOnce(`${ALICE_HOMESERVER_URL}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/invite`, {});
|
||||
await aliceClient.invite(roomId, 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();
|
||||
|
||||
const inviteEvent = mkEventCustom({
|
||||
type: "m.room.member",
|
||||
sender: aliceClient.getSafeUserId(),
|
||||
state_key: bobClient.getSafeUserId(),
|
||||
content: { membership: KnownMembership.Invite },
|
||||
});
|
||||
bobSyncResponder.sendOrQueueSyncResponse({
|
||||
rooms: { invite: { [roomId]: { invite_state: { events: [inviteEvent] } } } },
|
||||
to_device: {
|
||||
events: [
|
||||
{
|
||||
type: "m.room.encrypted",
|
||||
sender: aliceClient.getSafeUserId(),
|
||||
content: bobToDeviceMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await syncPromise(bobClient);
|
||||
|
||||
const room = bobClient.getRoom(roomId);
|
||||
expect(room).toBeTruthy();
|
||||
expect(room?.getMyMembership()).toEqual(KnownMembership.Invite);
|
||||
|
||||
fetchMock.postOnce(`${BOB_HOMESERVER_URL}/_matrix/client/v3/join/${encodeURIComponent(roomId)}`, {
|
||||
room_id: roomId,
|
||||
});
|
||||
fetchMock.getOnce(`begin:${BOB_HOMESERVER_URL}/_matrix/client/v1/media/download/alice-server/here`, {
|
||||
body: uploadedBlob,
|
||||
});
|
||||
await bobClient.joinRoom(roomId, { acceptSharedHistory: true });
|
||||
}
|
||||
});
|
||||
|
||||
function expectSendRoomEvent(homeserverUrl: string, msgtype: string): Promise<IContent> {
|
||||
|
||||
@@ -23,7 +23,13 @@ import * as testUtils from "../../test-utils/test-utils";
|
||||
import { getSyncResponse, syncPromise } from "../../test-utils/test-utils";
|
||||
import { TEST_ROOM_ID as ROOM_ID } from "../../test-utils/test-data";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { createClient, PendingEventOrdering, type IStartClientOpts, type MatrixClient } from "../../../src/matrix";
|
||||
import {
|
||||
createClient,
|
||||
HistoryVisibility,
|
||||
PendingEventOrdering,
|
||||
type IStartClientOpts,
|
||||
type MatrixClient,
|
||||
} from "../../../src/matrix";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { type ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
|
||||
@@ -197,7 +203,7 @@ describe("Encrypted State Events", () => {
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
// Alice shares a room with Bob
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"], ROOM_ID, true));
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"], HistoryVisibility.Joined, ROOM_ID, true));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// ... and claim one of Bob's OTKs ...
|
||||
|
||||
@@ -84,9 +84,9 @@ def main() -> None:
|
||||
* Do not edit by hand! This file is generated by `./generate-test-data.py`
|
||||
*/
|
||||
|
||||
import {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
|
||||
import {{ IDownloadKeyResult, IEvent }} from "../../../src";
|
||||
import {{ KeyBackupSession, KeyBackupInfo }} from "../../../src/crypto-api/keybackup";
|
||||
import type {{ IDeviceKeys, IMegolmSessionData }} from "../../../src/@types/crypto";
|
||||
import type {{ IDownloadKeyResult, IEvent }} from "../../../src";
|
||||
import type {{ KeyBackupSession, KeyBackupInfo, KeyBackupRoomSessions }} from "../../../src/crypto-api/keybackup";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
@@ -246,15 +246,6 @@ export const {prefix}SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult>
|
||||
/** Signed OTKs, returned by `POST /keys/claim` */
|
||||
export const {prefix}ONE_TIME_KEYS = { json.dumps(otks, indent=4) };
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE58 = "{ backup_recovery_key }";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
|
||||
export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
|
||||
|
||||
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||
export const {prefix}MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = {
|
||||
json.dumps(set_of_exported_room_keys, indent=4)
|
||||
@@ -278,6 +269,23 @@ export const {prefix}CLEAR_EVENT: Partial<IEvent> = {json.dumps(clear_event, ind
|
||||
|
||||
/** The encrypted CLEAR_EVENT by MEGOLM_SESSION_DATA */
|
||||
export const {prefix}ENCRYPTED_EVENT: Partial<IEvent> = {json.dumps(encrypted_event, indent=4)};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE64 = "{ user_data['B64_BACKUP_DECRYPTION_KEY'] }";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const {prefix}BACKUP_DECRYPTION_KEY_BASE58 = "{ backup_recovery_key }";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
|
||||
export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
|
||||
|
||||
/**
|
||||
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}`.
|
||||
* Contains the key from {prefix}MEGOLM_SESSION_DATA.
|
||||
*/
|
||||
export const {prefix}PER_ROOM_CURVE25519_KEY_BACKUP_DATA: KeyBackupRoomSessions = {{
|
||||
[{prefix}MEGOLM_SESSION_DATA.session_id]: {prefix}CURVE25519_KEY_BACKUP_DATA
|
||||
}};
|
||||
"""
|
||||
|
||||
alt_master_key = user_data.get("ALT_MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES")
|
||||
@@ -385,7 +393,7 @@ def sign_json(json_object: dict, private_key: ed25519.Ed25519PrivateKey) -> str:
|
||||
def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tuple[dict, ed25519.Ed25519PrivateKey]:
|
||||
"""
|
||||
Creates an exported megolm room key, as per https://gitlab.matrix.org/matrix-org/olm/blob/master/docs/megolm.md#session-export-format
|
||||
that can be imported via importRoomKeys API.
|
||||
that can be imported via importRoomKeys API, or shared via MSC4268 room history sharing.
|
||||
Returns the exported key, the matching privat edKey (needed to encrypt)
|
||||
"""
|
||||
index = 0
|
||||
@@ -409,11 +417,12 @@ def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tupl
|
||||
"session_id": encode_base64(
|
||||
private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
),
|
||||
"session_key": encode_base64(exported_key),
|
||||
"session_key": encode_base64(bytes(exported_key)),
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": encode_base64(ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
|
||||
},
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": True,
|
||||
}
|
||||
|
||||
return megolm_export, private_key
|
||||
@@ -458,7 +467,7 @@ def symetric_ratchet_step_of_megolm_key(previous: dict , megolm_private_key: ed2
|
||||
"room_id": "!room:id",
|
||||
"sender_key": previous["sender_key"],
|
||||
"session_id": previous["session_id"],
|
||||
"session_key": encode_base64(exported_key),
|
||||
"session_key": encode_base64(bytes(exported_key)),
|
||||
"sender_claimed_keys": previous["sender_claimed_keys"],
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
}
|
||||
@@ -609,7 +618,7 @@ def generate_encrypted_event_content(exported_key: dict, ed_key: ed25519.Ed25519
|
||||
|
||||
message += signature
|
||||
|
||||
cipher_text = encode_base64(message)
|
||||
cipher_text = encode_base64(bytes(message))
|
||||
|
||||
encrypted_payload = {
|
||||
"algorithm" : "m.megolm.v1.aes-sha2",
|
||||
@@ -653,7 +662,7 @@ def export_recovery_key(key_b64: str) -> str:
|
||||
export_bytes += parity_byte.to_bytes(1, 'big')
|
||||
|
||||
# The byte string is encoded using base58
|
||||
recovery_key = base58.b58encode(export_bytes).decode('utf-8')
|
||||
recovery_key = base58.b58encode(bytes(export_bytes)).decode('utf-8')
|
||||
|
||||
split = [recovery_key[i:i + 4] for i in range(0, len(recovery_key), 4)]
|
||||
return ' '.join(split)
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* Do not edit by hand! This file is generated by `./generate-test-data.py`
|
||||
*/
|
||||
|
||||
import { type IDeviceKeys, type IMegolmSessionData } from "../../../src/@types/crypto";
|
||||
import { type IDownloadKeyResult, type IEvent } from "../../../src";
|
||||
import { type KeyBackupSession, type KeyBackupInfo } from "../../../src/crypto-api/keybackup";
|
||||
import type { IDeviceKeys, IMegolmSessionData } from "../../../src/@types/crypto";
|
||||
import type { IDownloadKeyResult, IEvent } from "../../../src";
|
||||
import type { KeyBackupSession, KeyBackupInfo, KeyBackupRoomSessions } from "../../../src/crypto-api/keybackup";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
@@ -118,26 +118,6 @@ export const ONE_TIME_KEYS = {
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE58 = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
"signatures": {
|
||||
"@alice:localhost": {
|
||||
"ed25519:test_device": "KDSNeumirTsd8piI0oVfv/wzg4J4HlEc7rs5XhODFcJ/YAcUdg65ajsZG+rLI0TQOSSGjorJqcrSiSB1HRSCAA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||
export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
{
|
||||
@@ -149,7 +129,8 @@ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "QdgHgdpDgihgovpPzUiThXur1fbErTFh7paFvNKSgN0"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
},
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -160,7 +141,8 @@ export const MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "IrkbT6H+0urDf6wKDSyVC1fh1t84Vz6T62snni86Cog"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
}
|
||||
];
|
||||
|
||||
@@ -174,7 +156,8 @@ export const MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "Bhbpt6hqMZlSH4sJV7xiEEEiPVeTWz4Vkujl1EMdIPI"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
};
|
||||
|
||||
/** A ratcheted version of MEGOLM_SESSION_DATA */
|
||||
@@ -196,7 +179,7 @@ export const CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
|
||||
"forwarded_count": 0,
|
||||
"is_verified": false,
|
||||
"session_data": {
|
||||
"ciphertext": "r6HRk2/Im2yJe5cLP8R81aVjFWjYWPHpw7TVxphiSK1cdIDZTTK57r6MfU+0i/mTPn+/PosT74OvYwCnehy2d1BPGxhDl8AhPcBu3//Kzlq2o5CssPsw+88gRehkAsPg9Zp5G9sL9to6giltvTWTbsaQpmvv3HLmBOYSFIxvyZrOT/Ffqu325f0IEsKcyV2BdIkw8Ob9Xt+VWoe4MYEGG6y1T8W125zeFgKWI4Ow76uput64H9zZjIo+Cc+hCTO9Ea4EnosSjizCotevkNck7C/zGgfhBikiohROb6SbaZgxicSsEDZ+f7brnri9yP3iXS3PMDHHpa1+XzG2VOG/Y9OQZpkPq+pbLrCC+NWJeJPslDAK5i+RURwzjnPmaHKCRHTq86CwhFyiCDf61MGwCY3xjrmBJg44BCdxWqCx0YJvwsvVqqnl4vTieUfrwThNPsQ81aVkDHvlmrgrTt8icDa8jTJhu34jem+pbRSEM5aJikV4B+zYiLz+dH/v6UpYA2eG8ReOvwpPXp6CAcIlplRPpWbMBeLFVcPkT4KAXTp9exFpB4on4pf8OsaDomlt4qAA0rhAZmhPWPKcU/A0Tz4gyMu54OivVtw1SPj+5Iq+YDQ8jB6Po3ApzMf6fwF9x/FjevbboFB05X2Jr0NrbFqXMOUwXHMgDAGiIWX8+gkmmbaiNWqg2etjN94pobQSGZelb18XGN7kuwMk+Zwk7A",
|
||||
"ciphertext": "r6HRk2/Im2yJe5cLP8R81aVjFWjYWPHpw7TVxphiSK1cdIDZTTK57r6MfU+0i/mTPn+/PosT74OvYwCnehy2d/r0NTff1SQt+1GopZkT0nq6jF5Wh/oX+8iwtYjHvTxMpN1UQoXAvRF40O+EVg+Q3efJXh1t45cMco8EWU64VerOir+k7cQ3C9FtcgQw3kmz3s3HeVY10o13X/w6+rc8n6vXqxuIxYHnFxanxX8B6TgTMZNajNfVsmJV0aC1aezim7E2gsftc+6+zW5G+rCFaEsWV/IuSOUz0+Hh0U+7hzSrz9/4qXPEVmPy1f6Ll4hhquPAlXPVDwddqlJDYj7kmvzr1g3bKVpk+TtKDbWlVQDPaJx2DEI2jGkPYjhYb7okpTFKpUny94dZmFIQqCeSGPIniaq8Y+/CanugQ1ZRVQcThuXrTewqWhXcpVvkVHT9i4ImcpBl95HzCBXuiwSUv6FKvO25fp++w555rbn2piFtilrUwnkrZPW32jFuaQcKZF4mZwcLeH7POL5UCuS4TWyaKyArp7bRzXwWuIq1wPET2nAMUmUVL7ge2+tAevk1WOIsjLgSaz/g55wO3Yma7yhXRFKcnzTjS0hUQOZ3GfTNwCM4pjzAtIPzvVd4Fp0b1emWZS5WyOYdXsceEDi3c6WtkoHWOKhPU0zBzn8hA9TdlFFqKzf2QFbN5Zgg0gprDLnLWgpc3/ieI4C7ndEQ7ZeTNMXbT/Y10APFk3qO+IGkLXJ97/qTF41EXFDhlsL0",
|
||||
"ephemeral": "q+P1WdRtEiPIEtNuuGrRcueZxUbLnSKdsuTAkxewXgU",
|
||||
"mac": "OibmACbORhI"
|
||||
}
|
||||
@@ -229,6 +212,34 @@ export const ENCRYPTED_EVENT: Partial<IEvent> = {
|
||||
"origin_server_ts": 1507753886000
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE64 = "dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BACKUP_DECRYPTION_KEY_BASE58 = "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
"signatures": {
|
||||
"@alice:localhost": {
|
||||
"ed25519:test_device": "KDSNeumirTsd8piI0oVfv/wzg4J4HlEc7rs5XhODFcJ/YAcUdg65ajsZG+rLI0TQOSSGjorJqcrSiSB1HRSCAA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}`.
|
||||
* Contains the key from MEGOLM_SESSION_DATA.
|
||||
*/
|
||||
export const PER_ROOM_CURVE25519_KEY_BACKUP_DATA: KeyBackupRoomSessions = {
|
||||
[MEGOLM_SESSION_DATA.session_id]: CURVE25519_KEY_BACKUP_DATA
|
||||
};
|
||||
|
||||
// Bob data
|
||||
|
||||
export const BOB_TEST_USER_ID = "@bob:xyz";
|
||||
@@ -338,26 +349,6 @@ export const BOB_ONE_TIME_KEYS = {
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE58 = "EsT5 Sd5m mEXs NQYE ibRe 3q9E 4aXW rHih 5f9J 6rU6 AfwY mASR";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "ZRuVWcWlDuvOwZRygccUCD4Avtnt130800I+WQNwwRY",
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:bob_device": "lDIMj3VC0WazE2FamGHpmbiqKf9Z4pO4qapZ5TL5BnD3c+dvb+2waOEd6pgay/pmrQ6MW4Eu2KDEpe1fnHc3BA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** A set of megolm keys that can be imported via CryptoAPI#importRoomKeys */
|
||||
export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
{
|
||||
@@ -369,7 +360,8 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "F4P7f1Z0RjbiZMgHk1xBCG3KC4/Ng9PmxLJ4hQ13sHA"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
},
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -380,7 +372,8 @@ export const BOB_MEGOLM_SESSION_DATA_ARRAY: IMegolmSessionData[] = [
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "OsZMdC1gQ5nPr+L9tuT6xXsaFJkVPkgxP2FexHF1/QM"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
}
|
||||
];
|
||||
|
||||
@@ -394,7 +387,8 @@ export const BOB_MEGOLM_SESSION_DATA: IMegolmSessionData = {
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "zBdpQwWYyz1MkZuEUhXqcdMfUNN/B9psLFDDDTJOg64"
|
||||
},
|
||||
"forwarding_curve25519_key_chain": []
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": true
|
||||
};
|
||||
|
||||
/** A ratcheted version of BOB_MEGOLM_SESSION_DATA */
|
||||
@@ -416,7 +410,7 @@ export const BOB_CURVE25519_KEY_BACKUP_DATA: KeyBackupSession = {
|
||||
"forwarded_count": 0,
|
||||
"is_verified": false,
|
||||
"session_data": {
|
||||
"ciphertext": "d7UVOK17WEVky/8hK0h3HsTQrFMEbKbfqMcl2KtyTWcI9S5gGFWK9Git5BzVRxRggvxQ0c8PDfqL+dr3zHytAMW+71BJqIPQW910vV7SX3IcGylnoUcS3doVkJZiprXytXMP89AKcgv5Dj7mS2ZdvNGE+Atro74bzZ5yot5BrE0ZE5SjoUBPLaLMMu9HopLIV+qx01Rc3F0wmkocSPo51N0nv6wvO5Cst0FiOGHDK6r1pFlgDEJLmBkOyC4e8oMVbKTJzsSQVbJ8tJ37xuhI+T5P0ZlmiqKDqYRp8uh50w+txLEixYhEUunFgCTt1DAmiS9pLNYhLyl1ggwuQjzZe+AV6timbRxNJy18/AEcPomJw7z/pxYIiNLHRKOC13Wp8kGWx9cOgfMQ5KmBuLS8psGiLTBkfWPLOfNYqjbeqAR+OGZQoS6hUjbBYU7QuFa4FOYBHkNB2UqNsdsMb9qB/qs7QGTSb8Lok5YjW1c81BUpmIyKvuqnKma0MZskrpTYGQD2eJDABFCZwLFm+LgDyUTeSiV5xguYztLrHOk8LHKo9M8dIZgoBjeFVJxyjbcXKsVS3aQkMXKCrRlKLqhZTws/ZJwVfW9DbktZ9dT+tRZQvI7tjJofojcLX61AGJDnqUf5+2Gv1tEnmUI953gIzc8NlcFabPOsDsZEODt7MdOCTPT3w29umyhKbCsslpb64LoS/AB2QRPRCgkJS7snRA",
|
||||
"ciphertext": "d7UVOK17WEVky/8hK0h3HsTQrFMEbKbfqMcl2KtyTWcI9S5gGFWK9Git5BzVRxRggvxQ0c8PDfqL+dr3zHytAA7TEpHlx8Ks23hCqXmVW710VjqK2K9xnWCyJvkHfE8x0w6AYvffDj+tRVP8C8M7t4849rD2itn0uma+YMkvjG/nANUTxG1dBf3oUOZ673vflCPoaz7s7x9ZNhYDVSVH5JTdMgNwwN42R5dqqxnGTu516tJzJh/9BWvyD9oIPWJ8X0rt1sbzEJ3PZeBXcSy8GTlZ1SgSFjeiXlwYxOZCaX2sxprk4N1oI1db6g+wCDBhbCGGucJIlTDJna/h9/C5J4drGd/fkisG3SidUmJXXCyInhs/BhwjGAtTGeQS8j7R8UnJxhMulYBHSckzj0Kas71LElPp8W8M4Jq81APA03n5UfYB+U6jbxjDgf8OJnxGQyrteq9F2+SEvS/TwHe1pE3t6EM2mDYRoYDTpU5pTNYSJkGIQMfWJKRxxuWUGs29o1twewJ6dhHgm+SlCII0M7ESoVdV54vxZCvHZnPcR0NXDzal7ils7zBKJmamHfPQBuaqNPU3KmSo+5R8ngFPaWU5LbWqYp/WxSBfNCoLZ7Jf8Io5uitjXTATR2qy2r6l/RJmk3RlfP51kliQqI2TWqRF96oaB96IGgUGSFCX/2pv0psOBGc1SjfmMB3d7gYis+2iBYVbG3xmnpeXbqvlD0Lw9TiTIPkjhJkTW1+lXyhy1xVH9ZmcFamcL7bX15Jx",
|
||||
"ephemeral": "oO0VX84OUIzm2i/12zAhTWOZT5IFRH5mXaKZ8fXkCgU",
|
||||
"mac": "lEfHlqfJQwU"
|
||||
}
|
||||
@@ -449,6 +443,34 @@ export const BOB_ENCRYPTED_EVENT: Partial<IEvent> = {
|
||||
"origin_server_ts": 1507753886000
|
||||
};
|
||||
|
||||
/** base64-encoded backup decryption (private) key */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE64 = "DwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo=";
|
||||
|
||||
/** Backup decryption key in export format */
|
||||
export const BOB_BACKUP_DECRYPTION_KEY_BASE58 = "EsT5 Sd5m mEXs NQYE ibRe 3q9E 4aXW rHih 5f9J 6rU6 AfwY mASR";
|
||||
|
||||
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}/{sessionId}` */
|
||||
export const BOB_SIGNED_BACKUP_DATA: KeyBackupInfo = {
|
||||
"algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
"version": "1",
|
||||
"auth_data": {
|
||||
"public_key": "ZRuVWcWlDuvOwZRygccUCD4Avtnt130800I+WQNwwRY",
|
||||
"signatures": {
|
||||
"@bob:xyz": {
|
||||
"ed25519:bob_device": "lDIMj3VC0WazE2FamGHpmbiqKf9Z4pO4qapZ5TL5BnD3c+dvb+2waOEd6pgay/pmrQ6MW4Eu2KDEpe1fnHc3BA"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{roomId}`.
|
||||
* Contains the key from BOB_MEGOLM_SESSION_DATA.
|
||||
*/
|
||||
export const BOB_PER_ROOM_CURVE25519_KEY_BACKUP_DATA: KeyBackupRoomSessions = {
|
||||
[BOB_MEGOLM_SESSION_DATA.session_id]: BOB_CURVE25519_KEY_BACKUP_DATA
|
||||
};
|
||||
|
||||
/** A second set of signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||
export const BOB_ALT_SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
"master_keys": {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import {
|
||||
ClientEvent,
|
||||
EventType,
|
||||
HistoryVisibility,
|
||||
type IJoinedRoom,
|
||||
type IPusher,
|
||||
type ISyncResponse,
|
||||
@@ -57,14 +58,19 @@ export function syncPromise(client: MatrixClient, count = 1): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a sync response which contains a single room (by default TEST_ROOM_ID), with the members given
|
||||
* @param roomMembers
|
||||
* @param roomId
|
||||
* Return a sync response which contains a single room (by default `TEST_ROOM_ID`), with the members given
|
||||
* and history visibility set to `shared`.
|
||||
*
|
||||
* @returns the sync response
|
||||
* @param roomMembers - An array of user IDs representing the members of the room.
|
||||
* @param roomHistoryVisibility - The history visibility setting for the room. Defaults to `shared`.
|
||||
* @param roomId - The ID of the room. Defaults to `TEST_ROOM_ID`.
|
||||
* @param encryptStateEvents - A boolean indicating whether state events should be encrypted. Defaults to `false`.
|
||||
*
|
||||
* @returns The sync response object containing the room data.
|
||||
*/
|
||||
export function getSyncResponse(
|
||||
roomMembers: string[],
|
||||
roomHistoryVisibility: HistoryVisibility = HistoryVisibility.Shared,
|
||||
roomId = TEST_ROOM_ID,
|
||||
encryptStateEvents = false,
|
||||
): ISyncResponse {
|
||||
@@ -85,6 +91,14 @@ export function getSyncResponse(
|
||||
"io.element.msc4362.encrypt_state_events": encryptStateEvents,
|
||||
},
|
||||
}),
|
||||
mkEventCustom({
|
||||
sender: roomMembers[0],
|
||||
type: "m.room.history_visibility",
|
||||
state_key: "",
|
||||
content: {
|
||||
history_visibility: roomHistoryVisibility,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
timeline: {
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("NamespacedValue", () => {
|
||||
});
|
||||
|
||||
it("should have a falsey unstable if needed", () => {
|
||||
const ns = new NamespacedValue("stable");
|
||||
const ns = new NamespacedValue("stable", null);
|
||||
expect(ns.name).toBe(ns.stable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
expect(ns.names).toEqual([ns.stable]);
|
||||
|
||||
+171
-1
@@ -32,7 +32,14 @@ import {
|
||||
type IRoomEvent,
|
||||
} from "matrix-widget-api";
|
||||
|
||||
import { createRoomWidgetClient, MatrixError, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
|
||||
import {
|
||||
createRoomWidgetClient,
|
||||
EventType,
|
||||
type IEvent,
|
||||
MatrixError,
|
||||
MsgType,
|
||||
UpdateDelayedEventAction,
|
||||
} from "../../src/matrix";
|
||||
import { MatrixClient, ClientEvent, type ITurnServer as IClientTurnServer } from "../../src/client";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { type ICapabilities, type RoomWidgetClient } from "../../src/embedded";
|
||||
@@ -42,6 +49,7 @@ import { sleep } from "../../src/utils";
|
||||
import { SlidingSync } from "../../src/sliding-sync";
|
||||
import { logger } from "../../src/logger";
|
||||
import { flushPromises } from "../test-utils/flushPromises";
|
||||
import { RoomStickyEventsEvent, type RoomStickyEventsMap } from "../../src/models/room-sticky-events";
|
||||
|
||||
const testOIDCToken = {
|
||||
access_token: "12345678",
|
||||
@@ -49,6 +57,7 @@ const testOIDCToken = {
|
||||
matrix_server_name: "homeserver.oabc",
|
||||
token_type: "Bearer",
|
||||
};
|
||||
|
||||
class MockWidgetApi extends EventEmitter {
|
||||
public start = vi.fn().mockResolvedValue(undefined);
|
||||
public getClientVersions = vi.fn();
|
||||
@@ -167,6 +176,9 @@ describe("RoomWidgetClient", () => {
|
||||
"org.matrix.rageshake_request",
|
||||
{ request_id: 123 },
|
||||
"!1:example.org",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -422,6 +434,7 @@ describe("RoomWidgetClient", () => {
|
||||
"!1:example.org",
|
||||
2000,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -442,6 +455,7 @@ describe("RoomWidgetClient", () => {
|
||||
"!1:example.org",
|
||||
undefined,
|
||||
parentDelayId,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -855,6 +869,162 @@ describe("RoomWidgetClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("sticky events", () => {
|
||||
describe("when supported", () => {
|
||||
const doesServerSupportUnstableFeatureMock = vi.fn((feature) =>
|
||||
Promise.resolve(feature === "org.matrix.msc4354"),
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
doesServerSupportUnstableFeatureMock.mockReset();
|
||||
});
|
||||
|
||||
it("requests capabilities when set", async () => {
|
||||
await makeClient({ sendSticky: true, receiveSticky: true });
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407ReceiveStickyEvent);
|
||||
});
|
||||
|
||||
it("does not request capabilities when unset", async () => {
|
||||
await makeClient({});
|
||||
expect(widgetApi.requestCapability).not.toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
expect(widgetApi.requestCapability).not.toHaveBeenCalledWith(
|
||||
MatrixCapabilities.MSC4407ReceiveStickyEvent,
|
||||
);
|
||||
});
|
||||
|
||||
it("sends", async () => {
|
||||
await makeClient({ sendEvent: [EventType.RTCMembership], sendSticky: true });
|
||||
expect(widgetApi.requestCapabilityForRoomTimeline).toHaveBeenCalledWith("!1:example.org");
|
||||
expect(widgetApi.requestCapabilityToSendEvent).toHaveBeenCalledWith(EventType.RTCMembership);
|
||||
expect(widgetApi.requestCapability).toHaveBeenCalledWith(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
await client._unstable_sendStickyEvent("!1:example.org", 2000, null, EventType.RTCMembership, {
|
||||
msc4354_sticky_key: "test",
|
||||
});
|
||||
expect(widgetApi.sendRoomEvent).toHaveBeenCalledWith(
|
||||
EventType.RTCMembership,
|
||||
{ msc4354_sticky_key: "test" },
|
||||
"!1:example.org",
|
||||
undefined,
|
||||
undefined,
|
||||
2000,
|
||||
);
|
||||
});
|
||||
|
||||
it("receives (adds, updates, then removes when redacted)", async () => {
|
||||
await makeClient({ receiveEvent: [EventType.RTCMembership, EventType.RoomRedaction] });
|
||||
const room = client.getRoom("!1:example.org")!;
|
||||
|
||||
function expectStickyEvents(events: IEvent[]) {
|
||||
expect([...room._unstable_getStickyEvents()].map((e) => e.getEffectiveEvent())).toEqual(events);
|
||||
}
|
||||
|
||||
async function sendAndExpectStickyUpdate(
|
||||
eventToSend: IEvent,
|
||||
added: IEvent[],
|
||||
updated: { current: IEvent; previous: IEvent }[],
|
||||
removed: IEvent[],
|
||||
) {
|
||||
const emittedStickyUpdate = new Promise<
|
||||
Parameters<RoomStickyEventsMap[RoomStickyEventsEvent.Update]>
|
||||
>((resolve) => room.once(RoomStickyEventsEvent.Update, (...args) => resolve(args)));
|
||||
|
||||
widgetApi.emit(
|
||||
`action:${WidgetApiToWidgetAction.SendEvent}`,
|
||||
new CustomEvent(`action:${WidgetApiToWidgetAction.SendEvent}`, {
|
||||
detail: { data: eventToSend },
|
||||
}),
|
||||
);
|
||||
|
||||
const [addedReceived, updatedReceived, removedReceived] = await emittedStickyUpdate;
|
||||
expect(addedReceived.map((e) => e.getEffectiveEvent())).toEqual(added);
|
||||
expect(
|
||||
updatedReceived.map(({ current, previous }) => ({
|
||||
current: current.getEffectiveEvent(),
|
||||
previous: previous.getEffectiveEvent(),
|
||||
})),
|
||||
).toEqual(updated);
|
||||
expect(removedReceived.map((e) => e.getEffectiveEvent())).toEqual(removed);
|
||||
}
|
||||
|
||||
// First, add a new sticky event to the map. The client should emit.
|
||||
const event1 = new MatrixEvent({
|
||||
type: EventType.RTCMembership,
|
||||
event_id: "$pduhfiidph",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
msc4354_sticky: { duration_ms: 1200000 },
|
||||
content: { msc4354_sticky_key: "test" },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(event1, [event1], [], []);
|
||||
// It should remain cached in the sticky map
|
||||
expectStickyEvents([event1]);
|
||||
|
||||
// Next, update the same key in the sticky map
|
||||
const event2 = new MatrixEvent({
|
||||
type: EventType.RTCMembership,
|
||||
event_id: "$zshgyutptfh",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
msc4354_sticky: { duration_ms: 1200000 },
|
||||
content: { msc4354_sticky_key: "test" },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(event2, [], [{ current: event2, previous: event1 }], []);
|
||||
expectStickyEvents([event2]);
|
||||
|
||||
// Next, redact the second event. Because it has the first as a predecessor, the map should revert to
|
||||
// the first event.
|
||||
const redaction1 = new MatrixEvent({
|
||||
type: EventType.RoomRedaction,
|
||||
event_id: "$cimoexnvz",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
redacts: event2.event_id,
|
||||
content: { redacts: event2.event_id },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(redaction1, [], [{ current: event1, previous: event2 }], []);
|
||||
expectStickyEvents([event1]);
|
||||
|
||||
// Finally, redact the first event. Now everything should be gone from the map.
|
||||
const redaction2 = new MatrixEvent({
|
||||
type: EventType.RoomRedaction,
|
||||
event_id: "$drgzmenlh",
|
||||
room_id: "!1:example.org",
|
||||
sender: "@alice:example.org",
|
||||
redacts: event1.event_id,
|
||||
content: { redacts: event1.event_id },
|
||||
}).getEffectiveEvent();
|
||||
await sendAndExpectStickyUpdate(redaction2, [], [], [event1]);
|
||||
expectStickyEvents([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when unsupported", () => {
|
||||
const doesServerSupportUnstableFeatureMock = vi.fn().mockResolvedValue(false);
|
||||
|
||||
beforeAll(() => {
|
||||
MatrixClient.prototype.doesServerSupportUnstableFeature = doesServerSupportUnstableFeatureMock;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
doesServerSupportUnstableFeatureMock.mockReset();
|
||||
});
|
||||
|
||||
it("fails to send", async () => {
|
||||
await makeClient({ sendEvent: [EventType.RTCMembership], sendSticky: true });
|
||||
await expect(
|
||||
client._unstable_sendStickyEvent("!1:example.org", 2000, null, EventType.RTCMembership, {
|
||||
msc4354_sticky_key: "test",
|
||||
}),
|
||||
).rejects.toThrow("Server does not support");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("to-device messages", () => {
|
||||
const unencryptedContentMap = new Map([
|
||||
["@alice:example.org", new Map([["*", { hello: "alice!" }]])],
|
||||
|
||||
@@ -51,18 +51,21 @@ describe("SSO login URL", function () {
|
||||
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, undefined);
|
||||
const url = new URL(urlString);
|
||||
expect(url.searchParams.has("org.matrix.msc3824.action")).toBe(false);
|
||||
expect(url.searchParams.has("action")).toBe(false);
|
||||
});
|
||||
|
||||
it("register", function () {
|
||||
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.REGISTER);
|
||||
const url = new URL(urlString);
|
||||
expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("register");
|
||||
expect(url.searchParams.get("action")).toEqual("register");
|
||||
});
|
||||
|
||||
it("login", function () {
|
||||
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.LOGIN);
|
||||
const url = new URL(urlString);
|
||||
expect(url.searchParams.get("org.matrix.msc3824.action")).toEqual("login");
|
||||
expect(url.searchParams.get("action")).toEqual("login");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -88,7 +88,7 @@ import { mockOpenIdConfiguration } from "../test-utils/oidc.ts";
|
||||
import { type CryptoBackend } from "../../src/common-crypto/CryptoBackend";
|
||||
import { SyncResponder } from "../test-utils/SyncResponder.ts";
|
||||
import { mockInitialApiRequests } from "../test-utils/mockEndpoints.ts";
|
||||
import { type Transport } from "src/matrixrtc/index.ts";
|
||||
import { type Transport } from "../../src/matrixrtc/index.ts";
|
||||
|
||||
vi.useFakeTimers();
|
||||
|
||||
@@ -3877,6 +3877,27 @@ describe("MatrixClient", function () {
|
||||
makeClient();
|
||||
});
|
||||
|
||||
it("should use stable prefix", async () => {
|
||||
const metadata = mockOpenIdConfiguration();
|
||||
client.getVersions = vi.fn().mockResolvedValue({
|
||||
versions: ["v1.15"],
|
||||
});
|
||||
httpLookups = [
|
||||
{
|
||||
method: "GET",
|
||||
path: `/auth_metadata`,
|
||||
data: metadata,
|
||||
prefix: "/_matrix/client/v1",
|
||||
},
|
||||
];
|
||||
|
||||
await expect(client.getAuthMetadata()).resolves.toEqual({
|
||||
...metadata,
|
||||
signingKeys: [],
|
||||
});
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should use unstable prefix", async () => {
|
||||
const metadata = mockOpenIdConfiguration();
|
||||
httpLookups = [
|
||||
|
||||
@@ -158,8 +158,35 @@ describe("CallMembership", () => {
|
||||
expect(membership.eventId).toBe("$eventid");
|
||||
});
|
||||
it("returns correct slot_id", () => {
|
||||
expect(membership.slotId).toBe("m.call#");
|
||||
expect(membership.slotDescription).toStrictEqual({ id: "", application: "m.call" });
|
||||
// slot_id is application and call_id dependent. So we create
|
||||
// a membership for each possible combination
|
||||
|
||||
// non call application (should not alter call_id even with empty string)
|
||||
const nonCallMembership = createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
application: "m.not.a.call",
|
||||
call_id: "",
|
||||
});
|
||||
// non "" call id should not be altered
|
||||
const callMembershipCustomId = createCallMembership(makeMockEvent(), {
|
||||
...membershipTemplate,
|
||||
call_id: "customCallId",
|
||||
});
|
||||
|
||||
// for membership (application = m.call and call_id = "") we expect "" -> ROOM
|
||||
// for legacy events we expect the room to be added automagically
|
||||
// See INFO_SLOT_ID_LEGACY_CASE comments
|
||||
expect(membership.slotId).toBe("m.call#ROOM");
|
||||
expect(membership.slotDescription).toStrictEqual({ id: "ROOM", application: "m.call" });
|
||||
|
||||
expect(nonCallMembership.slotId).toBe("m.not.a.call#");
|
||||
expect(nonCallMembership.slotDescription).toStrictEqual({ id: "", application: "m.not.a.call" });
|
||||
|
||||
expect(callMembershipCustomId.slotId).toBe("m.call#customCallId");
|
||||
expect(callMembershipCustomId.slotDescription).toStrictEqual({
|
||||
id: "customCallId",
|
||||
application: "m.call",
|
||||
});
|
||||
});
|
||||
it("returns correct deviceId", () => {
|
||||
expect(membership.deviceId).toBe("AAAAAAA");
|
||||
|
||||
@@ -49,7 +49,7 @@ const mockFocus = { type: "mock" };
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
const callSession = { id: "", application: "m.call" };
|
||||
const callSession = { id: "ROOM", application: "m.call" };
|
||||
|
||||
describe("MatrixRTCSession", () => {
|
||||
let client: MatrixClient;
|
||||
@@ -134,12 +134,12 @@ describe("MatrixRTCSession", () => {
|
||||
);
|
||||
await flushPromises();
|
||||
expect(sess?.memberships.length).toEqual(1);
|
||||
expect(sess?.memberships[0].slotDescription.id).toEqual("");
|
||||
expect(sess?.memberships[0].slotDescription.id).toEqual("ROOM");
|
||||
expect(sess?.memberships[0].scope).toEqual("m.room");
|
||||
expect(sess?.memberships[0].application).toEqual("m.call");
|
||||
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
|
||||
expect(sess?.memberships[0].isExpired()).toEqual(false);
|
||||
expect(sess?.slotDescription.id).toEqual("");
|
||||
expect(sess?.slotDescription.id).toEqual("ROOM");
|
||||
});
|
||||
|
||||
it("ignores memberships where application is not m.call", () => {
|
||||
@@ -381,12 +381,12 @@ describe("MatrixRTCSession", () => {
|
||||
});
|
||||
await flushPromises();
|
||||
expect(sess?.memberships.length).toEqual(1);
|
||||
expect(sess?.memberships[0].slotDescription.id).toEqual("");
|
||||
expect(sess?.memberships[0].slotDescription.id).toEqual("ROOM");
|
||||
expect(sess?.memberships[0].scope).toEqual("m.room");
|
||||
expect(sess?.memberships[0].application).toEqual("m.call");
|
||||
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
|
||||
expect(sess?.memberships[0].isExpired()).toEqual(false);
|
||||
expect(sess?.slotDescription.id).toEqual("");
|
||||
expect(sess?.slotDescription.id).toEqual("ROOM");
|
||||
});
|
||||
it("combines sticky and membership events when both exist", async () => {
|
||||
// Create a room with identical member state and sticky state for the same user.
|
||||
@@ -415,7 +415,7 @@ describe("MatrixRTCSession", () => {
|
||||
const memberships = sess.memberships;
|
||||
expect(memberships.length).toEqual(2);
|
||||
expect(memberships[0].sender).toEqual(stickyUserId);
|
||||
expect(memberships[0].slotDescription.id).toEqual("");
|
||||
expect(memberships[0].slotDescription.id).toEqual("ROOM");
|
||||
expect(memberships[0].scope).toEqual("m.room");
|
||||
expect(memberships[0].application).toEqual("m.call");
|
||||
expect(memberships[0].deviceId).toEqual("AAAAAAA");
|
||||
@@ -424,7 +424,7 @@ describe("MatrixRTCSession", () => {
|
||||
// Then state
|
||||
expect(memberships[1].sender).toEqual(membershipTemplate.user_id);
|
||||
|
||||
expect(sess?.slotDescription.id).toEqual("");
|
||||
expect(sess?.slotDescription.id).toEqual("ROOM");
|
||||
});
|
||||
it("handles an incoming sticky event to an existing session", async () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
@@ -620,9 +620,7 @@ describe("MatrixRTCSession", () => {
|
||||
it("sends a notification when starting a call and emit DidSendCallNotification", async () => {
|
||||
// Simulate a join, including the update to the room state
|
||||
// Ensure sendEvent returns event IDs so the DidSendCallNotification payload includes them
|
||||
sendEventMock
|
||||
.mockResolvedValueOnce({ event_id: "legacy-evt" })
|
||||
.mockResolvedValueOnce({ event_id: "new-evt" });
|
||||
sendEventMock.mockResolvedValueOnce({ event_id: "new-evt" });
|
||||
const didSendEventFn = vi.fn();
|
||||
sess!.once(MatrixRTCSessionEvent.DidSendCallNotification, didSendEventFn);
|
||||
// Create an additional listener to create a promise that resolves after the emission.
|
||||
@@ -647,43 +645,25 @@ describe("MatrixRTCSession", () => {
|
||||
"sender_ts": expect.any(Number),
|
||||
});
|
||||
|
||||
// Check if deprecated notify event is also sent.
|
||||
expect(client.sendEvent).toHaveBeenCalledWith(mockRoom!.roomId, EventType.CallNotify, {
|
||||
"application": "m.call",
|
||||
"m.mentions": { user_ids: [], room: true },
|
||||
"notify_type": "ring",
|
||||
"call_id": "",
|
||||
});
|
||||
await didSendNotification;
|
||||
// And ensure we emitted the DidSendCallNotification event with both payloads
|
||||
expect(didSendEventFn).toHaveBeenCalledWith(
|
||||
{
|
||||
"event_id": "new-evt",
|
||||
"lifetime": 30000,
|
||||
"m.mentions": { room: true, user_ids: [] },
|
||||
"m.relates_to": {
|
||||
event_id: expect.any(String),
|
||||
rel_type: "m.reference",
|
||||
},
|
||||
"notification_type": "ring",
|
||||
"sender_ts": expect.any(Number),
|
||||
expect(didSendEventFn).toHaveBeenCalledWith({
|
||||
"event_id": "new-evt",
|
||||
"lifetime": 30000,
|
||||
"m.mentions": { room: true, user_ids: [] },
|
||||
"m.relates_to": {
|
||||
event_id: expect.any(String),
|
||||
rel_type: "m.reference",
|
||||
},
|
||||
{
|
||||
"application": "m.call",
|
||||
"call_id": "",
|
||||
"event_id": "legacy-evt",
|
||||
"m.mentions": { room: true, user_ids: [] },
|
||||
"notify_type": "ring",
|
||||
},
|
||||
);
|
||||
"notification_type": "ring",
|
||||
"sender_ts": expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
it("sends a notification with a intent when starting a call and emits DidSendCallNotification", async () => {
|
||||
// Simulate a join, including the update to the room state
|
||||
// Ensure sendEvent returns event IDs so the DidSendCallNotification payload includes them
|
||||
sendEventMock
|
||||
.mockResolvedValueOnce({ event_id: "legacy-evt" })
|
||||
.mockResolvedValueOnce({ event_id: "new-evt" });
|
||||
sendEventMock.mockResolvedValueOnce({ event_id: "new-evt" });
|
||||
const didSendEventFn = vi.fn();
|
||||
sess!.once(MatrixRTCSessionEvent.DidSendCallNotification, didSendEventFn);
|
||||
// Create an additional listener to create a promise that resolves after the emission.
|
||||
@@ -707,7 +687,7 @@ describe("MatrixRTCSession", () => {
|
||||
]);
|
||||
|
||||
await sess!._onRTCSessionMemberUpdate();
|
||||
const ownMembershipId = sess?.memberships[0].eventId;
|
||||
const ownMembershipEventId = sess?.memberships[0].eventId;
|
||||
expect(sess!.getConsensusCallIntent()).toEqual("audio");
|
||||
|
||||
expect(client.sendEvent).toHaveBeenCalledWith(mockRoom!.roomId, EventType.RTCNotification, {
|
||||
@@ -715,43 +695,27 @@ describe("MatrixRTCSession", () => {
|
||||
"notification_type": "ring",
|
||||
"m.call.intent": "audio",
|
||||
"m.relates_to": {
|
||||
event_id: ownMembershipId,
|
||||
event_id: ownMembershipEventId,
|
||||
rel_type: "m.reference",
|
||||
},
|
||||
"lifetime": 30000,
|
||||
"sender_ts": expect.any(Number),
|
||||
});
|
||||
|
||||
// Check if deprecated notify event is also sent.
|
||||
expect(client.sendEvent).toHaveBeenCalledWith(mockRoom!.roomId, EventType.CallNotify, {
|
||||
"application": "m.call",
|
||||
"m.mentions": { user_ids: [], room: true },
|
||||
"notify_type": "ring",
|
||||
"call_id": "",
|
||||
});
|
||||
await didSendNotification;
|
||||
// And ensure we emitted the DidSendCallNotification event with both payloads
|
||||
expect(didSendEventFn).toHaveBeenCalledWith(
|
||||
{
|
||||
"event_id": "new-evt",
|
||||
"lifetime": 30000,
|
||||
"m.mentions": { room: true, user_ids: [] },
|
||||
"m.relates_to": {
|
||||
event_id: expect.any(String),
|
||||
rel_type: "m.reference",
|
||||
},
|
||||
"notification_type": "ring",
|
||||
"m.call.intent": "audio",
|
||||
"sender_ts": expect.any(Number),
|
||||
expect(didSendEventFn).toHaveBeenCalledWith({
|
||||
"event_id": "new-evt",
|
||||
"lifetime": 30000,
|
||||
"m.mentions": { room: true, user_ids: [] },
|
||||
"m.relates_to": {
|
||||
event_id: expect.any(String),
|
||||
rel_type: "m.reference",
|
||||
},
|
||||
{
|
||||
"application": "m.call",
|
||||
"call_id": "",
|
||||
"event_id": "legacy-evt",
|
||||
"m.mentions": { room: true, user_ids: [] },
|
||||
"notify_type": "ring",
|
||||
},
|
||||
);
|
||||
"notification_type": "ring",
|
||||
"m.call.intent": "audio",
|
||||
"sender_ts": expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't send a notification when joining an existing call", async () => {
|
||||
|
||||
@@ -71,7 +71,7 @@ function createAsyncHandle<T>(method: MockedFunction<(...args: any[]) => any>) {
|
||||
return { reject, resolve };
|
||||
}
|
||||
|
||||
const callSession = { id: "", application: "m.call" };
|
||||
const callSession = { id: "ROOM", application: "m.call" };
|
||||
|
||||
describe("MembershipManager", () => {
|
||||
let client: MockClient;
|
||||
@@ -139,6 +139,7 @@ describe("MembershipManager", () => {
|
||||
"org.matrix.msc3401.call.member",
|
||||
{
|
||||
application: "m.call",
|
||||
// This tests INFO_SLOT_ID_LEGACY_CASE because it is using callSession = { id: "ROOM", application: "m.call" }
|
||||
call_id: "",
|
||||
device_id: "AAAAAAA",
|
||||
expires: 14400000,
|
||||
@@ -147,6 +148,7 @@ describe("MembershipManager", () => {
|
||||
focus_active: focusActive,
|
||||
scope: "m.room",
|
||||
},
|
||||
// This tests INFO_SLOT_ID_LEGACY_CASE because it is using callSession = { id: "ROOM", application: "m.call" }
|
||||
"_@alice:example.org_AAAAAAA_m.call",
|
||||
);
|
||||
restartScheduledDelayedEventHandle.resolve?.();
|
||||
@@ -160,6 +162,45 @@ describe("MembershipManager", () => {
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("sends correct call_id and state key when using non empty string. Not using empty string -> ROOM hack. See: INFO_SLOT_ID_LEGACY_CASE", async () => {
|
||||
// Spys/Mocks
|
||||
|
||||
const customCallSession = { id: "custom", application: "m.call" };
|
||||
const restartScheduledDelayedEventHandle = createAsyncHandle<void>(
|
||||
client._unstable_restartScheduledDelayedEvent,
|
||||
);
|
||||
|
||||
// Test
|
||||
const memberManager = new MembershipManager(undefined, room, client, customCallSession);
|
||||
memberManager.join([focus], undefined);
|
||||
// expects
|
||||
await waitForMockCall(client.sendStateEvent, Promise.resolve({ event_id: "id" }));
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
"org.matrix.msc3401.call.member",
|
||||
{
|
||||
application: "m.call",
|
||||
call_id: "custom",
|
||||
device_id: "AAAAAAA",
|
||||
expires: 14400000,
|
||||
foci_preferred: [focus],
|
||||
membershipID: "@alice:example.org:AAAAAAA",
|
||||
focus_active: focusActive,
|
||||
scope: "m.room",
|
||||
},
|
||||
"_@alice:example.org_AAAAAAA_m.callcustom",
|
||||
);
|
||||
restartScheduledDelayedEventHandle.resolve?.();
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
|
||||
room.roomId,
|
||||
{ delay: 8000 },
|
||||
"org.matrix.msc3401.call.member",
|
||||
{},
|
||||
"_@alice:example.org_AAAAAAA_m.callcustom",
|
||||
);
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("reschedules delayed leave event if sending state cancels it", async () => {
|
||||
const memberManager = new MembershipManager(undefined, room, client, callSession);
|
||||
const waitForSendState = waitForMockCall(client.sendStateEvent);
|
||||
@@ -416,8 +457,9 @@ describe("MembershipManager", () => {
|
||||
await manager.leave();
|
||||
expect(client._unstable_sendScheduledDelayedEvent).toHaveBeenLastCalledWith("id");
|
||||
expect(client.sendStateEvent).toHaveBeenCalled();
|
||||
expect(manager.delayId).toBe(undefined);
|
||||
});
|
||||
it("send leave event when leave is called and resolving delayed leave fails", async () => {
|
||||
it("send leave event when leave is called and resolving delayed leave fails unknown error", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
manager.join([focus]);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
@@ -431,6 +473,27 @@ describe("MembershipManager", () => {
|
||||
{},
|
||||
"_@alice:example.org_AAAAAAA_m.call",
|
||||
);
|
||||
// If there is a unknown error, we do not reset the delayId
|
||||
// The delayed event might still be around and we track it.
|
||||
expect(manager.delayId).not.toBe(undefined);
|
||||
});
|
||||
it("send leave event when leave is called and resolving delayed leave fails not found error", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
manager.join([focus]);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
(client._unstable_sendScheduledDelayedEvent as Mock<any>).mockRejectedValue(
|
||||
new MatrixError({ errcode: "M_NOT_FOUND" }, 404),
|
||||
);
|
||||
await manager.leave();
|
||||
|
||||
// We send a normal leave event since we failed using sendScheduledDelayedEvent.
|
||||
expect(client.sendStateEvent).toHaveBeenLastCalledWith(
|
||||
room.roomId,
|
||||
"org.matrix.msc3401.call.member",
|
||||
{},
|
||||
"_@alice:example.org_AAAAAAA_m.call",
|
||||
);
|
||||
expect(manager.delayId).toBe(undefined);
|
||||
});
|
||||
it("does nothing if not joined", async () => {
|
||||
const manager = new MembershipManager({}, room, client, callSession);
|
||||
@@ -947,8 +1010,8 @@ describe("MembershipManager", () => {
|
||||
id: "@alice:example.org:AAAAAAA_m.call",
|
||||
device_id: "AAAAAAA",
|
||||
},
|
||||
slot_id: "m.call#",
|
||||
rtc_transports: [focus],
|
||||
slot_id: "m.call#ROOM",
|
||||
rtc_transports: [{ type: focus.type, livekit_service_url: focus.livekit_service_url }],
|
||||
versions: [],
|
||||
msc4354_sticky_key: "@alice:example.org:AAAAAAA_m.call",
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@ import { type Mocked, type MockedObject } from "vitest";
|
||||
import { EventType, type Room, RoomEvent, type MatrixClient, type MatrixEvent } from "../../../src";
|
||||
import { CallMembership, type SessionMembershipData } from "../../../src/matrixrtc";
|
||||
import { secureRandomString } from "../../../src/randomstring";
|
||||
import { type CallMembershipIdentityParts } from "src/matrixrtc/EncryptionManager";
|
||||
import { type CallMembershipIdentityParts } from "../../../src/matrixrtc/EncryptionManager";
|
||||
import { logger } from "../../../src/logger.ts";
|
||||
|
||||
export type MembershipData = (SessionMembershipData | {}) & { user_id: string };
|
||||
|
||||
@@ -275,6 +275,108 @@ describe("Relations", function () {
|
||||
expect(badlyEditedTopic.getContent().topic).toBe("topic");
|
||||
});
|
||||
|
||||
describe("m.replace async ordering", () => {
|
||||
const userId = "@bob:example.com";
|
||||
const roomId = "!room:example.com";
|
||||
const targetEventId = "$target";
|
||||
|
||||
function makeEditEvent(eventId: string, ts: number): MatrixEvent {
|
||||
return new MatrixEvent({
|
||||
sender: userId,
|
||||
type: "m.room.message",
|
||||
event_id: eventId,
|
||||
room_id: roomId,
|
||||
origin_server_ts: ts,
|
||||
content: {
|
||||
"body": `edited ${eventId}`,
|
||||
"msgtype": "m.text",
|
||||
"m.new_content": {
|
||||
body: `edited ${eventId}`,
|
||||
msgtype: "m.text",
|
||||
},
|
||||
"m.relates_to": {
|
||||
event_id: targetEventId,
|
||||
rel_type: "m.replace",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
it("should not let a slow-decrypting older edit overwrite a newer one", async () => {
|
||||
const room = new Room(roomId, new TestClient(userId).client, userId);
|
||||
const relations = new Relations("m.replace", "m.room.message", room);
|
||||
|
||||
const targetEvent = new MatrixEvent({
|
||||
sender: userId,
|
||||
type: "m.room.message",
|
||||
event_id: targetEventId,
|
||||
room_id: roomId,
|
||||
origin_server_ts: 1000,
|
||||
content: { body: "original", msgtype: "m.text" },
|
||||
});
|
||||
|
||||
await relations.setTargetEvent(targetEvent);
|
||||
|
||||
// Create two edits: edit1 is older (ts=2000), edit2 is newer (ts=3000).
|
||||
const edit1 = makeEditEvent("$edit1", 2000);
|
||||
const edit2 = makeEditEvent("$edit2", 3000);
|
||||
|
||||
// Simulate edit1 being in the process of decryption: isBeingDecrypted()
|
||||
// returns true and getDecryptionPromise() returns a deferred promise.
|
||||
let resolveEdit1Decryption!: () => void;
|
||||
const edit1DecryptionPromise = new Promise<void>((resolve) => {
|
||||
resolveEdit1Decryption = resolve;
|
||||
});
|
||||
vi.spyOn(edit1, "isBeingDecrypted").mockReturnValue(true);
|
||||
vi.spyOn(edit1, "getDecryptionPromise").mockReturnValue(edit1DecryptionPromise);
|
||||
vi.spyOn(edit1, "shouldAttemptDecryption").mockReturnValue(false);
|
||||
|
||||
// edit2 is already decrypted.
|
||||
vi.spyOn(edit2, "isBeingDecrypted").mockReturnValue(false);
|
||||
vi.spyOn(edit2, "shouldAttemptDecryption").mockReturnValue(false);
|
||||
|
||||
// Add edit1 first (it will block on decryption).
|
||||
const addEdit1Promise = relations.addEvent(edit1);
|
||||
|
||||
// While edit1 is still decrypting, add edit2 (resolves immediately).
|
||||
await relations.addEvent(edit2);
|
||||
|
||||
// edit2 should be applied as the replacement (it's newer).
|
||||
expect(targetEvent.replacingEvent()).toBe(edit2);
|
||||
|
||||
// Now resolve edit1's decryption — the stale result must NOT overwrite edit2.
|
||||
resolveEdit1Decryption();
|
||||
await addEdit1Promise;
|
||||
|
||||
// edit2 must still be the replacing event, not edit1.
|
||||
expect(targetEvent.replacingEvent()).toBe(edit2);
|
||||
});
|
||||
|
||||
it("should apply an edit correctly when there is no concurrency", async () => {
|
||||
const room = new Room(roomId, new TestClient(userId).client, userId);
|
||||
const relations = new Relations("m.replace", "m.room.message", room);
|
||||
|
||||
const targetEvent = new MatrixEvent({
|
||||
sender: userId,
|
||||
type: "m.room.message",
|
||||
event_id: targetEventId,
|
||||
room_id: roomId,
|
||||
origin_server_ts: 1000,
|
||||
content: { body: "original", msgtype: "m.text" },
|
||||
});
|
||||
|
||||
await relations.setTargetEvent(targetEvent);
|
||||
|
||||
const edit = makeEditEvent("$edit1", 2000);
|
||||
vi.spyOn(edit, "isBeingDecrypted").mockReturnValue(false);
|
||||
vi.spyOn(edit, "shouldAttemptDecryption").mockReturnValue(false);
|
||||
|
||||
await relations.addEvent(edit);
|
||||
|
||||
expect(targetEvent.replacingEvent()).toBe(edit);
|
||||
});
|
||||
});
|
||||
|
||||
it("getSortedAnnotationsByKey should return null for non-annotation relations", async () => {
|
||||
const userId = "@user:server";
|
||||
const room = new Room("room123", new TestClient(userId).client, userId);
|
||||
|
||||
@@ -1308,4 +1308,144 @@ describe("RoomState", function () {
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("reactive display name disambiguation", function () {
|
||||
it("should disambiguate existing member when another member changes to the same name", function () {
|
||||
// Create a fresh state
|
||||
const testState = new RoomState(roomId);
|
||||
|
||||
// Alice joins with display name "Alice"
|
||||
const aliceJoinEvent = utils.mkMembership({
|
||||
user: userA,
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
name: "Alice",
|
||||
});
|
||||
|
||||
// Bob joins with display name "Bob"
|
||||
const bobJoinEvent = utils.mkMembership({
|
||||
user: userB,
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
name: "Bob",
|
||||
});
|
||||
|
||||
testState.setStateEvents([aliceJoinEvent, bobJoinEvent]);
|
||||
|
||||
// Verify no disambiguation needed initially
|
||||
const aliceBefore = testState.getMember(userA);
|
||||
const bobBefore = testState.getMember(userB);
|
||||
expect(aliceBefore?.disambiguate).toBe(false);
|
||||
expect(bobBefore?.disambiguate).toBe(false);
|
||||
expect(aliceBefore?.name).toBe("Alice");
|
||||
expect(bobBefore?.name).toBe("Bob");
|
||||
|
||||
// Bob changes display name to "Alice"
|
||||
const bobRenameEvent = utils.mkMembership({
|
||||
user: userB,
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
name: "Alice",
|
||||
});
|
||||
|
||||
testState.setStateEvents([bobRenameEvent]);
|
||||
|
||||
// Now both should be disambiguated
|
||||
const aliceAfter = testState.getMember(userA);
|
||||
const bobAfter = testState.getMember(userB);
|
||||
expect(aliceAfter?.disambiguate).toBe(true);
|
||||
expect(bobAfter?.disambiguate).toBe(true);
|
||||
expect(aliceAfter?.name).toContain(userA);
|
||||
expect(bobAfter?.name).toContain(userB);
|
||||
});
|
||||
|
||||
it("should un-disambiguate member when conflicting member changes to different name", function () {
|
||||
// Create a fresh state
|
||||
const testState = new RoomState(roomId);
|
||||
|
||||
// Both Alice and Bob join with display name "Alice"
|
||||
const aliceJoinEvent = utils.mkMembership({
|
||||
user: userA,
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
name: "Alice",
|
||||
});
|
||||
|
||||
const bobJoinEvent = utils.mkMembership({
|
||||
user: userB,
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
name: "Alice",
|
||||
});
|
||||
|
||||
testState.setStateEvents([aliceJoinEvent, bobJoinEvent]);
|
||||
|
||||
// Verify both are disambiguated
|
||||
const aliceBefore = testState.getMember(userA);
|
||||
const bobBefore = testState.getMember(userB);
|
||||
expect(aliceBefore?.disambiguate).toBe(true);
|
||||
expect(bobBefore?.disambiguate).toBe(true);
|
||||
|
||||
// Bob changes display name to "Bob"
|
||||
const bobRenameEvent = utils.mkMembership({
|
||||
user: userB,
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
name: "Bob",
|
||||
});
|
||||
|
||||
testState.setStateEvents([bobRenameEvent]);
|
||||
|
||||
// Alice should no longer be disambiguated, Bob should not be either
|
||||
const aliceAfter = testState.getMember(userA);
|
||||
const bobAfter = testState.getMember(userB);
|
||||
expect(aliceAfter?.disambiguate).toBe(false);
|
||||
expect(bobAfter?.disambiguate).toBe(false);
|
||||
expect(aliceAfter?.name).toBe("Alice");
|
||||
expect(bobAfter?.name).toBe("Bob");
|
||||
});
|
||||
|
||||
it("should emit RoomState.members for affected members when disambiguation changes", function () {
|
||||
// Create a fresh state
|
||||
const testState = new RoomState(roomId);
|
||||
|
||||
// Alice joins with display name "Alice"
|
||||
const aliceJoinEvent = utils.mkMembership({
|
||||
user: userA,
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
name: "Alice",
|
||||
});
|
||||
|
||||
testState.setStateEvents([aliceJoinEvent]);
|
||||
|
||||
// Set up listener for Members event
|
||||
const membersEmitted: string[] = [];
|
||||
testState.on(RoomStateEvent.Members, (_ev, _state, member) => {
|
||||
membersEmitted.push(member.userId);
|
||||
});
|
||||
|
||||
// Bob joins with display name "Alice" - should trigger disambiguation for Alice
|
||||
const bobJoinEvent = utils.mkMembership({
|
||||
user: userB,
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
name: "Alice",
|
||||
});
|
||||
|
||||
testState.setStateEvents([bobJoinEvent]);
|
||||
|
||||
// Both Alice and Bob should have emitted Members events
|
||||
expect(membersEmitted).toContain(userA);
|
||||
expect(membersEmitted).toContain(userB);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
Copyright 2026 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "../../src/client";
|
||||
|
||||
describe("Room upgrades", function () {
|
||||
it("Sends an HTTP request upgrading the room", () => {
|
||||
// Given a client with a fake authedRequest method
|
||||
const { client, authedRequest } = createClient();
|
||||
|
||||
// When we upgrade the room to version 12
|
||||
client.upgradeRoom("!r1", "12");
|
||||
|
||||
// Then we make an HTTP request to the correct endpoint, with the
|
||||
// version provided in the JSON.
|
||||
expect(authedRequest).toHaveBeenCalledWith("POST", "/rooms/!r1/upgrade", undefined, { new_version: "12" });
|
||||
});
|
||||
|
||||
it("Includes additional_creators if provided", () => {
|
||||
// Given a client with a fake authedRequest method
|
||||
const { client, authedRequest } = createClient();
|
||||
|
||||
// When we upgrade the room to version 13 and supply additionalCreators
|
||||
client.upgradeRoom("!r1", "13", ["@u:s.co", "@v:a.b"]);
|
||||
|
||||
// Then we make an HTTP request to the correct endpoint, with the
|
||||
// version and additional creators provided.
|
||||
expect(authedRequest).toHaveBeenCalledWith("POST", "/rooms/!r1/upgrade", undefined, {
|
||||
new_version: "13",
|
||||
additional_creators: ["@u:s.co", "@v:a.b"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
///
|
||||
function createClient(): { client: MatrixClient; authedRequest: any } {
|
||||
const authedRequest = vi.fn();
|
||||
const client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
userId: "@u:s.co",
|
||||
});
|
||||
client.http.authedRequest = authedRequest;
|
||||
return { client, authedRequest };
|
||||
}
|
||||
+22
-5
@@ -2103,7 +2103,7 @@ describe("Room", function () {
|
||||
}
|
||||
|
||||
const memberEvent = utils.mkMembership({
|
||||
user: "@user_a:bar",
|
||||
user: userA,
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
@@ -2114,7 +2114,7 @@ describe("Room", function () {
|
||||
const client = createClientMock([memberEvent]);
|
||||
const room = new Room(roomId, client as any, null!, { lazyLoadMembers: true });
|
||||
await room.loadMembersIfNeeded();
|
||||
const memberA = room.getMember("@user_a:bar")!;
|
||||
const memberA = room.getMember(userA)!;
|
||||
expect(memberA.name).toEqual("User A");
|
||||
const storedMembers = client.store.storedMembers!;
|
||||
expect(storedMembers.length).toEqual(1);
|
||||
@@ -2123,7 +2123,7 @@ describe("Room", function () {
|
||||
|
||||
it("should take members from storage if available", async function () {
|
||||
const memberEvent2 = utils.mkMembership({
|
||||
user: "@user_a:bar",
|
||||
user: userA,
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
@@ -2134,7 +2134,7 @@ describe("Room", function () {
|
||||
|
||||
await room.loadMembersIfNeeded();
|
||||
|
||||
const memberA = room.getMember("@user_a:bar")!;
|
||||
const memberA = room.getMember(userA)!;
|
||||
expect(memberA.name).toEqual("User A");
|
||||
});
|
||||
|
||||
@@ -2145,9 +2145,26 @@ describe("Room", function () {
|
||||
|
||||
client.members.mockReturnValue({ chunk: [memberEvent] });
|
||||
await room.loadMembersIfNeeded();
|
||||
const memberA = room.getMember("@user_a:bar")!;
|
||||
const memberA = room.getMember(userA)!;
|
||||
expect(memberA.name).toEqual("User A");
|
||||
});
|
||||
|
||||
it("should emit room name change event if loading members causes the room name to change", async function () {
|
||||
const otherMemberEvent = utils.mkMembership({
|
||||
user: userB,
|
||||
mship: KnownMembership.Join,
|
||||
room: roomId,
|
||||
event: true,
|
||||
name: "User B",
|
||||
});
|
||||
|
||||
const client = createClientMock([memberEvent, otherMemberEvent]);
|
||||
const room = new Room(roomId, client as any, userA, { lazyLoadMembers: true });
|
||||
|
||||
expect(room.name).toEqual("!foo:bar");
|
||||
await room.loadMembersIfNeeded();
|
||||
expect(room.name).toEqual("User B");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMyMembership", function () {
|
||||
|
||||
@@ -236,4 +236,105 @@ describe("OutgoingRequestsManager", () => {
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Repeated processing of outgoing requests", () => {
|
||||
it("Processes any requests still in the queue after processing", async () => {
|
||||
// Given that the first time we call outgoingRequests we get back
|
||||
// requests 1 and 2, but the second time, we get request 3
|
||||
const [request1, request2, request3] = setupOutgoingTwoRequestsThenOne();
|
||||
|
||||
// (And requests finish immediately)
|
||||
processor.makeOutgoingRequest.mockImplementation(async () => {
|
||||
return;
|
||||
});
|
||||
|
||||
// When we ask a manager to process the requests
|
||||
await manager.doProcessOutgoingRequests();
|
||||
|
||||
// Then all three are processed because we re-call outgoingRequests
|
||||
// to check for more
|
||||
await vi.waitFor(() => expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(3));
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(3);
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request1);
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request2);
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request3);
|
||||
});
|
||||
|
||||
it("Does reprocess if any request succeeded, even if some failed", async () => {
|
||||
// Given that the first time we call outgoingRequests we get back
|
||||
// requests 1 and 2, but the second time, we get request 3
|
||||
const [request1, request2, request3] = setupOutgoingTwoRequestsThenOne();
|
||||
|
||||
// And the first request fails, but subsequent ones pass
|
||||
processor.makeOutgoingRequest
|
||||
.mockImplementationOnce(async () => {
|
||||
throw new Error("This request failed!");
|
||||
})
|
||||
.mockImplementation(async () => {
|
||||
return;
|
||||
});
|
||||
|
||||
// When we ask a manager to process the requests
|
||||
await manager.doProcessOutgoingRequests();
|
||||
|
||||
// Then all three are processed because we re-call outgoingRequests
|
||||
// to check for more, even though one failed
|
||||
await vi.waitFor(() => expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(3));
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(3);
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request1);
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request2);
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request3);
|
||||
});
|
||||
|
||||
it("Does not reprocess if all requests failed", async () => {
|
||||
// Given that the first time we call outgoingRequests we get back
|
||||
// requests 1 and 2, but the second time, we get request 3
|
||||
const [request1, request2, request3] = setupOutgoingTwoRequestsThenOne();
|
||||
|
||||
// And the first two requests fail, but subsequent ones pass
|
||||
processor.makeOutgoingRequest
|
||||
.mockImplementationOnce(async () => {
|
||||
throw new Error("Request 1 failed!");
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
throw new Error("Request 2 failed!");
|
||||
})
|
||||
.mockImplementation(async () => {
|
||||
return;
|
||||
});
|
||||
|
||||
// When we ask a manager to process the requests
|
||||
await manager.doProcessOutgoingRequests();
|
||||
|
||||
// Then only the first two requests are processed, because since
|
||||
// they both failed we stop retrying
|
||||
await vi.waitFor(() => expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(1));
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(2);
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request1);
|
||||
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request2);
|
||||
expect(processor.makeOutgoingRequest).not.toHaveBeenCalledWith(request3);
|
||||
});
|
||||
|
||||
/// Mock calls to olmMachine.outgoingRequests. The first time it is
|
||||
// called, return two requests and the second time return a third one.
|
||||
//
|
||||
// Returns the three returned requests.
|
||||
function setupOutgoingTwoRequestsThenOne(): [OutgoingRequest, OutgoingRequest, OutgoingRequest] {
|
||||
const request1 = new RustSdkCryptoJs.KeysQueryRequest("1", "{}");
|
||||
const request2 = new RustSdkCryptoJs.KeysUploadRequest("2", "{}");
|
||||
const request3 = new RustSdkCryptoJs.KeysUploadRequest("3", "{}");
|
||||
|
||||
// Given that the first time we call outgoingRequests we get back
|
||||
// requests 1 and 2, but the second time, we get request 3
|
||||
olmMachine.outgoingRequests
|
||||
.mockImplementationOnce(async (): Promise<OutgoingRequest[]> => {
|
||||
return [request1, request2];
|
||||
})
|
||||
.mockImplementationOnce(async (): Promise<OutgoingRequest[]> => {
|
||||
return [request3];
|
||||
});
|
||||
|
||||
return [request1, request2, request3];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ exports[`RustCrypto > importing and exporting room keys > should import and expo
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": false,
|
||||
"org.matrix.msc3061.shared_history": true,
|
||||
"room_id": "!room:id",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "QdgHgdpDgihgovpPzUiThXur1fbErTFh7paFvNKSgN0",
|
||||
@@ -19,7 +19,7 @@ exports[`RustCrypto > importing and exporting room keys > should import and expo
|
||||
{
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
"forwarding_curve25519_key_chain": [],
|
||||
"org.matrix.msc3061.shared_history": false,
|
||||
"org.matrix.msc3061.shared_history": true,
|
||||
"room_id": "!room:id",
|
||||
"sender_claimed_keys": {
|
||||
"ed25519": "QdgHgdpDgihgovpPzUiThXur1fbErTFh7paFvNKSgN0",
|
||||
|
||||
@@ -314,7 +314,7 @@ describe("Call", function () {
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
[SDPStreamMetadataKey]: {
|
||||
[SDPStreamMetadataKey.name]: {
|
||||
remote_stream: {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
audio_muted: true,
|
||||
@@ -420,7 +420,7 @@ describe("Call", function () {
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
[SDPStreamMetadataKey]: {},
|
||||
[SDPStreamMetadataKey.name]: {},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -451,7 +451,7 @@ describe("Call", function () {
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
[SDPStreamMetadataKey]: {},
|
||||
[SDPStreamMetadataKey.name]: {},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -478,7 +478,7 @@ describe("Call", function () {
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
[SDPStreamMetadataKey]: {},
|
||||
[SDPStreamMetadataKey.name]: {},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -504,7 +504,7 @@ describe("Call", function () {
|
||||
|
||||
call.onSDPStreamMetadataChangedReceived(
|
||||
makeMockEvent("@test:foo", {
|
||||
[SDPStreamMetadataKey]: {
|
||||
[SDPStreamMetadataKey.name]: {
|
||||
remote_stream: {
|
||||
purpose: SDPStreamMetadataPurpose.Screenshare,
|
||||
audio_muted: true,
|
||||
@@ -849,7 +849,7 @@ describe("Call", function () {
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
[SDPStreamMetadataKey]: {
|
||||
[SDPStreamMetadataKey.name]: {
|
||||
[STREAM_ID]: {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
},
|
||||
@@ -959,8 +959,8 @@ describe("Call", function () {
|
||||
describe("sending sdp_stream_metadata_changed events", () => {
|
||||
it("should send sdp_stream_metadata_changed when muting audio", async () => {
|
||||
await call.setMicrophoneMuted(true);
|
||||
expect(mockSendVoipEvent).toHaveBeenCalledWith(EventType.CallSDPStreamMetadataChangedPrefix, {
|
||||
[SDPStreamMetadataKey]: {
|
||||
expect(mockSendVoipEvent).toHaveBeenCalledWith(EventType.CallSDPStreamMetadataChanged, {
|
||||
[SDPStreamMetadataKey.name]: {
|
||||
mock_stream_from_media_handler: {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
audio_muted: true,
|
||||
@@ -972,8 +972,8 @@ describe("Call", function () {
|
||||
|
||||
it("should send sdp_stream_metadata_changed when muting video", async () => {
|
||||
await call.setLocalVideoMuted(true);
|
||||
expect(mockSendVoipEvent).toHaveBeenCalledWith(EventType.CallSDPStreamMetadataChangedPrefix, {
|
||||
[SDPStreamMetadataKey]: {
|
||||
expect(mockSendVoipEvent).toHaveBeenCalledWith(EventType.CallSDPStreamMetadataChanged, {
|
||||
[SDPStreamMetadataKey.name]: {
|
||||
mock_stream_from_media_handler: {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
audio_muted: false,
|
||||
@@ -1001,7 +1001,7 @@ describe("Call", function () {
|
||||
);
|
||||
call.onSDPStreamMetadataChangedReceived({
|
||||
getContent: () => ({
|
||||
[SDPStreamMetadataKey]: metadata,
|
||||
[SDPStreamMetadataKey.name]: metadata,
|
||||
}),
|
||||
} as MatrixEvent);
|
||||
return metadata;
|
||||
@@ -1293,9 +1293,9 @@ describe("Call", function () {
|
||||
FAKE_ROOM_ID,
|
||||
EventType.CallNegotiate,
|
||||
expect.objectContaining({
|
||||
"version": "1",
|
||||
"call_id": call.callId,
|
||||
"org.matrix.msc3077.sdp_stream_metadata": expect.objectContaining({
|
||||
version: "1",
|
||||
call_id: call.callId,
|
||||
sdp_stream_metadata: expect.objectContaining({
|
||||
[SCREENSHARE_STREAM_ID]: expect.objectContaining({
|
||||
purpose: SDPStreamMetadataPurpose.Screenshare,
|
||||
}),
|
||||
|
||||
@@ -963,7 +963,7 @@ describe("Group Call", function () {
|
||||
const getMetadataEvent = (audio: boolean, video: boolean): MatrixEvent =>
|
||||
({
|
||||
getContent: () => ({
|
||||
[SDPStreamMetadataKey]: {
|
||||
[SDPStreamMetadataKey.name]: {
|
||||
stream: {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
audio_muted: audio,
|
||||
@@ -1330,7 +1330,7 @@ describe("Group Call", function () {
|
||||
call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember;
|
||||
call.onNegotiateReceived({
|
||||
getContent: () => ({
|
||||
[SDPStreamMetadataKey]: {
|
||||
[SDPStreamMetadataKey.name]: {
|
||||
screensharing_stream: {
|
||||
purpose: SDPStreamMetadataPurpose.Screenshare,
|
||||
},
|
||||
|
||||
+10
-4
@@ -48,11 +48,17 @@ export interface IPasswordFlow extends ILoginFlow {
|
||||
type: "m.login.password";
|
||||
}
|
||||
|
||||
export const DELEGATED_OIDC_COMPATIBILITY = new UnstableValue(
|
||||
"delegated_oidc_compatibility",
|
||||
export const OAUTH_AWARE_PREFERRED_FLOW_FIELD = new UnstableValue(
|
||||
"oauth_aware_preferred",
|
||||
"org.matrix.msc3824.delegated_oidc_compatibility",
|
||||
);
|
||||
|
||||
/**
|
||||
* @alias
|
||||
* @deprecated use `OAUTH_AWARE_PREFERRED_FLOW_FIELD` instead.
|
||||
*/
|
||||
export const DELEGATED_OIDC_COMPATIBILITY = OAUTH_AWARE_PREFERRED_FLOW_FIELD;
|
||||
|
||||
/**
|
||||
* Representation of SSO flow as per https://spec.matrix.org/v1.3/client-server-api/#client-login-via-sso
|
||||
*/
|
||||
@@ -60,8 +66,8 @@ export interface ISSOFlow extends ILoginFlow {
|
||||
type: "m.login.sso" | "m.login.cas";
|
||||
// eslint-disable-next-line camelcase
|
||||
identity_providers?: IIdentityProvider[];
|
||||
[DELEGATED_OIDC_COMPATIBILITY.name]?: boolean;
|
||||
[DELEGATED_OIDC_COMPATIBILITY.altName]?: boolean;
|
||||
[OAUTH_AWARE_PREFERRED_FLOW_FIELD.name]?: boolean;
|
||||
[OAUTH_AWARE_PREFERRED_FLOW_FIELD.altName]?: boolean;
|
||||
}
|
||||
|
||||
export enum IdentityProviderBrand {
|
||||
|
||||
+31
-2
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type EitherAnd } from "matrix-events-sdk";
|
||||
|
||||
import { NamespacedValue, UnstableValue } from "../NamespacedValue.ts";
|
||||
import {
|
||||
type PolicyRuleEventContent,
|
||||
@@ -50,7 +52,6 @@ import {
|
||||
type MCallReplacesEvent,
|
||||
type MCallSelectAnswer,
|
||||
type SDPStreamMetadata,
|
||||
type SDPStreamMetadataKey,
|
||||
} from "../webrtc/callEventTypes.ts";
|
||||
import {
|
||||
type IRTCNotificationContent,
|
||||
@@ -133,11 +134,13 @@ export enum EventType {
|
||||
FullyRead = "m.fully_read",
|
||||
Tag = "m.tag",
|
||||
SpaceOrder = "org.matrix.msc3230.space_order", // MSC3230
|
||||
MarkedUnread = "m.marked_unread",
|
||||
|
||||
// User account_data events
|
||||
PushRules = "m.push_rules",
|
||||
Direct = "m.direct",
|
||||
IgnoredUserList = "m.ignored_user_list",
|
||||
InvitePermissionConfig = "m.invite_permission_config", // MSC4380
|
||||
|
||||
// to_device events
|
||||
RoomKey = "m.room_key",
|
||||
@@ -334,7 +337,16 @@ export interface TimelineEvents {
|
||||
[EventType.CallCandidates]: MCallCandidates;
|
||||
[EventType.CallHangup]: MCallHangupReject;
|
||||
[EventType.CallReject]: MCallHangupReject;
|
||||
[EventType.CallSDPStreamMetadataChangedPrefix]: MCallBase & { [SDPStreamMetadataKey]: SDPStreamMetadata };
|
||||
[EventType.CallSDPStreamMetadataChangedPrefix]: MCallBase &
|
||||
EitherAnd<
|
||||
{ sdp_stream_metadata: SDPStreamMetadata },
|
||||
{ "org.matrix.msc3077.sdp_stream_metadata": SDPStreamMetadata }
|
||||
>;
|
||||
[EventType.CallSDPStreamMetadataChanged]: MCallBase &
|
||||
EitherAnd<
|
||||
{ sdp_stream_metadata: SDPStreamMetadata },
|
||||
{ "org.matrix.msc3077.sdp_stream_metadata": SDPStreamMetadata }
|
||||
>;
|
||||
[EventType.CallEncryptionKeysPrefix]: EncryptionKeysEventContent;
|
||||
[EventType.CallNotify]: ICallNotifyContent;
|
||||
[EventType.RTCNotification]: IRTCNotificationContent;
|
||||
@@ -386,6 +398,16 @@ export interface StateEvents {
|
||||
[M_BEACON_INFO.name]: MBeaconInfoEventContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapped type from event type to content type for all specified room-specific account_data events.
|
||||
*/
|
||||
export interface RoomAccountDataEvents extends SecretStorageAccountDataEvents {
|
||||
[EventType.FullyRead]: { event_id: string };
|
||||
[EventType.Tag]: { tags: { [name: string]: { order?: number } } };
|
||||
[EventType.SpaceOrder]: { order: string };
|
||||
[EventType.MarkedUnread]: { unread: boolean };
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapped type from event type to content type for all specified global account_data events.
|
||||
*/
|
||||
@@ -404,8 +426,15 @@ export interface AccountDataEvents extends SecretStorageAccountDataEvents {
|
||||
// Invites-ignorer events
|
||||
[POLICIES_ACCOUNT_EVENT_TYPE.name]: { [key: string]: any };
|
||||
[POLICIES_ACCOUNT_EVENT_TYPE.altName]: { [key: string]: any };
|
||||
|
||||
[EventType.InvitePermissionConfig]: { default_action?: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* Subset of AccountDataEvents, excluding events specified in https://spec.matrix.org/v1.17/client-server-api/#server-behaviour-12
|
||||
*/
|
||||
export type WritableAccountDataEvents = Exclude<AccountDataEvents, "m.fully_read" | "m.push_rules">;
|
||||
|
||||
/**
|
||||
* Mapped type from event type to content type for all specified global events encrypted by secret storage.
|
||||
*
|
||||
|
||||
Vendored
-24
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import "oidc-client-ts";
|
||||
|
||||
declare module "oidc-client-ts" {
|
||||
interface OidcMetadata {
|
||||
// Add the missing device_authorization_endpoint field to the OidcMetadata interface
|
||||
device_authorization_endpoint?: string;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -42,7 +42,7 @@ import { type IMessageRendering } from "./extensible_events.ts";
|
||||
/**
|
||||
* The event type for an m.topic event (in content)
|
||||
*/
|
||||
export const M_TOPIC = new NamespacedValue("m.topic");
|
||||
export const M_TOPIC = new NamespacedValue("m.topic", null);
|
||||
|
||||
/**
|
||||
* The event content for an m.topic event (in content)
|
||||
|
||||
@@ -22,11 +22,11 @@ export class NamespacedValue<S extends string, U extends string> {
|
||||
// Stable is optional, but one of the two parameters is required, hence the weird-looking types.
|
||||
// Goal is to to have developers explicitly say there is no stable value (if applicable).
|
||||
public constructor(stable: S, unstable: U);
|
||||
public constructor(stable: S, unstable?: U);
|
||||
public constructor(stable: null | undefined, unstable: U);
|
||||
public constructor(stable: S, unstable: U | null);
|
||||
public constructor(stable: null, unstable: U);
|
||||
public constructor(
|
||||
public readonly stable?: S | null,
|
||||
public readonly unstable?: U,
|
||||
public readonly stable: S | null,
|
||||
public readonly unstable: U | null,
|
||||
) {
|
||||
if (!this.unstable && !this.stable) {
|
||||
throw new Error("One of stable or unstable values must be supplied");
|
||||
@@ -60,8 +60,8 @@ 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): T | undefined {
|
||||
let val: T | undefined = undefined;
|
||||
public findIn<V>(obj: Partial<Record<NonNullable<S | U>, V>>): V | undefined {
|
||||
let val: V | undefined = undefined;
|
||||
if (this.name) {
|
||||
val = obj?.[this.name];
|
||||
}
|
||||
|
||||
+30
-12
@@ -138,6 +138,7 @@ import {
|
||||
MsgType,
|
||||
PUSHER_ENABLED,
|
||||
RelationType,
|
||||
type RoomAccountDataEvents,
|
||||
RoomCreateTypeField,
|
||||
RoomType,
|
||||
type StateEvents,
|
||||
@@ -145,6 +146,7 @@ import {
|
||||
UNSTABLE_MSC3088_ENABLED,
|
||||
UNSTABLE_MSC3088_PURPOSE,
|
||||
UNSTABLE_MSC3089_TREE_SUBTYPE,
|
||||
type WritableAccountDataEvents,
|
||||
} from "./@types/event.ts";
|
||||
import {
|
||||
GuestAccess,
|
||||
@@ -2221,7 +2223,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param eventType - The event type
|
||||
* @param content - the contents object for the event
|
||||
*/
|
||||
public async setAccountData<K extends keyof AccountDataEvents>(
|
||||
public async setAccountData<K extends keyof WritableAccountDataEvents>(
|
||||
eventType: K,
|
||||
content: AccountDataEvents[K] | Record<string, never>,
|
||||
): Promise<EmptyObject> {
|
||||
@@ -2273,7 +2275,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param eventType - The event type
|
||||
* @param content - the contents object for the event
|
||||
*/
|
||||
public setAccountDataRaw<K extends keyof AccountDataEvents>(
|
||||
public setAccountDataRaw<K extends keyof WritableAccountDataEvents>(
|
||||
eventType: K,
|
||||
content: AccountDataEvents[K] | Record<string, never>,
|
||||
): Promise<EmptyObject> {
|
||||
@@ -2328,7 +2330,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteAccountData(eventType: keyof AccountDataEvents): Promise<void> {
|
||||
public async deleteAccountData(eventType: keyof WritableAccountDataEvents): Promise<void> {
|
||||
const msc3391DeleteAccountDataServerSupport = this.canSupport.get(Feature.AccountDataDeletion);
|
||||
// if deletion is not supported overwrite with empty content
|
||||
if (msc3391DeleteAccountDataServerSupport === ServerSupport.Unsupported) {
|
||||
@@ -2584,12 +2586,17 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
/**
|
||||
* @param roomId - the ID of the room this event should be stored within
|
||||
* @param eventType - event type to be set
|
||||
* @param content - event content
|
||||
* @returns Promise which resolves: to an empty object `{}`
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public setRoomAccountData(roomId: string, eventType: string, content: Record<string, any>): Promise<EmptyObject> {
|
||||
public setRoomAccountData<K extends keyof RoomAccountDataEvents>(
|
||||
roomId: string,
|
||||
eventType: K,
|
||||
content: RoomAccountDataEvents[K] | Record<string, never>,
|
||||
): Promise<EmptyObject> {
|
||||
const path = utils.encodeUri("/user/$userId/rooms/$roomId/account_data/$type", {
|
||||
$userId: this.credentials.userId!,
|
||||
$roomId: roomId,
|
||||
@@ -6689,6 +6696,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
const params = {
|
||||
redirectUrl,
|
||||
[SSO_ACTION_PARAM.stable!]: action,
|
||||
[SSO_ACTION_PARAM.unstable!]: action,
|
||||
};
|
||||
|
||||
@@ -6936,13 +6944,23 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
/**
|
||||
* Upgrades a room to a new protocol version
|
||||
* @param newVersion - The target version to upgrade to
|
||||
* @param additionalCreators - an optional list of user IDs of users who
|
||||
* should have the same permissions as the user performing the
|
||||
* upgrade
|
||||
* @returns Promise which resolves: Object with key 'replacement_room'
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public upgradeRoom(roomId: string, newVersion: string): Promise<{ replacement_room: string }> {
|
||||
public upgradeRoom(
|
||||
roomId: string,
|
||||
newVersion: string,
|
||||
additionalCreators?: string[],
|
||||
): Promise<{ replacement_room: string }> {
|
||||
// eslint-disable-line camelcase
|
||||
const path = utils.encodeUri("/rooms/$roomId/upgrade", { $roomId: roomId });
|
||||
return this.http.authedRequest(Method.Post, path, undefined, { new_version: newVersion });
|
||||
return this.http.authedRequest(Method.Post, path, undefined, {
|
||||
new_version: newVersion,
|
||||
additional_creators: additionalCreators,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8826,21 +8844,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover and validate delegated auth configuration
|
||||
* - delegated auth issuer openid-configuration is reachable
|
||||
* - delegated auth issuer openid-configuration is configured correctly for us
|
||||
* Discover and validate the auth metadata for the OAuth 2.0 API.
|
||||
*
|
||||
* Fetches /auth_metadata falling back to legacy implementation using /auth_issuer followed by
|
||||
* https://oidc-issuer.example.com/.well-known/openid-configuration and other files linked therein.
|
||||
* When successful, validated metadata is returned
|
||||
* When successful, validated metadata is returned.
|
||||
*
|
||||
* @returns validated authentication metadata and optionally signing keys
|
||||
* @throws when delegated auth config is invalid or unreachable
|
||||
* @experimental - part of MSC2965
|
||||
*/
|
||||
public async getAuthMetadata(): Promise<OidcClientConfig> {
|
||||
let authMetadata: unknown | undefined;
|
||||
try {
|
||||
const useStable = await this.isVersionSupported("v1.15");
|
||||
authMetadata = await this.http.request<unknown>(Method.Get, "/auth_metadata", undefined, undefined, {
|
||||
prefix: ClientPrefix.Unstable + "/org.matrix.msc2965",
|
||||
prefix: useStable ? ClientPrefix.V1 : ClientPrefix.Unstable + "/org.matrix.msc2965",
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof MatrixError && e.errcode === "M_UNRECOGNIZED") {
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import { type MBeaconEventContent, type MBeaconInfoContent, type MBeaconInfoEventContent } from "./@types/beacon.ts";
|
||||
import { MsgType } from "./@types/event.ts";
|
||||
import { M_TEXT, REFERENCE_RELATION } from "./@types/extensible_events.ts";
|
||||
import { type IMessageRendering, M_TEXT, REFERENCE_RELATION } from "./@types/extensible_events.ts";
|
||||
import { isProvided } from "./extensible_events_v1/utilities.ts";
|
||||
import {
|
||||
M_ASSET,
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
type MAssetContent,
|
||||
type LegacyLocationEventContent,
|
||||
} from "./@types/location.ts";
|
||||
import { type MRoomTopicEventContent, type MTopicContent, M_TOPIC } from "./@types/topic.ts";
|
||||
import { type MRoomTopicEventContent, type MTopicContent, M_TOPIC, type MTopicEvent } from "./@types/topic.ts";
|
||||
import { type RoomMessageEventContent } from "./@types/events.ts";
|
||||
|
||||
/**
|
||||
@@ -206,7 +206,7 @@ export type TopicState = {
|
||||
};
|
||||
|
||||
export const parseTopicContent = (content: MRoomTopicEventContent): TopicState => {
|
||||
const mtopicParent = M_TOPIC.findIn<MTopicContent>(content);
|
||||
const mtopicParent = M_TOPIC.findIn<MTopicContent | IMessageRendering[]>(content as MTopicEvent);
|
||||
const mtopic = Array.isArray(mtopicParent) ? mtopicParent : mtopicParent?.["m.text"];
|
||||
// TODO remove support for the old malformed m.topic arrays after a few releases (only allow array in m.text)
|
||||
// https://github.com/matrix-org/matrix-js-sdk/pull/4984#pullrequestreview-3174251065
|
||||
|
||||
@@ -51,6 +51,7 @@ function validateMediaId(mediaId: string): boolean {
|
||||
* for authenticated media will *not* be checked - it is the caller's responsibility
|
||||
* to do so before calling this function. Note also that `useAuthentication`
|
||||
* implies `allowRedirects`. Defaults to false (unauthenticated endpoints).
|
||||
* @param animated - Whether the desired thumbnail should be animated.
|
||||
* @returns The complete URL to the content, may be an empty string if the provided mxc is not valid.
|
||||
*/
|
||||
export function getHttpUriForMxc(
|
||||
@@ -62,6 +63,7 @@ export function getHttpUriForMxc(
|
||||
allowDirectLinks = false,
|
||||
allowRedirects?: boolean,
|
||||
useAuthentication?: boolean,
|
||||
animated?: boolean,
|
||||
): string {
|
||||
if (typeof mxc !== "string" || !mxc) {
|
||||
return "";
|
||||
@@ -107,6 +109,9 @@ export function getHttpUriForMxc(
|
||||
if (resizeMethod) {
|
||||
url.searchParams.set("method", resizeMethod);
|
||||
}
|
||||
if (animated !== undefined) {
|
||||
url.searchParams.set("animated", String(animated));
|
||||
}
|
||||
|
||||
if (typeof allowRedirects === "boolean") {
|
||||
// We add this after, so we don't convert everything to a thumbnail request.
|
||||
|
||||
+110
-54
@@ -38,6 +38,7 @@ import {
|
||||
type SendDelayedEventRequestOpts,
|
||||
type SendDelayedEventResponse,
|
||||
UpdateDelayedEventAction,
|
||||
isSendDelayedEventRequestOpts,
|
||||
} from "./@types/requests.ts";
|
||||
import { EventType, type StateEvents } from "./@types/event.ts";
|
||||
import { logger } from "./logger.ts";
|
||||
@@ -56,7 +57,7 @@ import { ConnectionError, MatrixError } from "./http-api/errors.ts";
|
||||
import { User } from "./models/user.ts";
|
||||
import { type Room } from "./models/room.ts";
|
||||
import { type ToDeviceBatch, type ToDevicePayload } from "./models/ToDeviceMessage.ts";
|
||||
import { MapWithDefault, recursiveMapToObject } from "./utils.ts";
|
||||
import { MapWithDefault, type QueryDict, recursiveMapToObject } from "./utils.ts";
|
||||
import { type EmptyObject, TypedEventEmitter, UnsupportedDelayedEventsEndpointError } from "./matrix.ts";
|
||||
|
||||
interface IStateEventRequest {
|
||||
@@ -122,6 +123,20 @@ export interface ICapabilities {
|
||||
* @defaultValue false
|
||||
*/
|
||||
updateDelayedEvents?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this client needs to be able to send sticky events.
|
||||
* @experimental Part of MSC4354 & MSC4407
|
||||
* @defaultValue false
|
||||
*/
|
||||
sendSticky?: boolean;
|
||||
|
||||
/**
|
||||
* Whether this client needs to be able to receive sticky events.
|
||||
* @experimental Part of MSC4354 & MSC4407
|
||||
* @defaultValue false
|
||||
*/
|
||||
receiveSticky?: boolean;
|
||||
}
|
||||
|
||||
export enum RoomWidgetClientEvent {
|
||||
@@ -197,54 +212,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
)
|
||||
: Promise.resolve();
|
||||
|
||||
// Request capabilities for the functionality this client needs to support
|
||||
if (
|
||||
capabilities.sendEvent?.length ||
|
||||
capabilities.receiveEvent?.length ||
|
||||
capabilities.sendMessage === true ||
|
||||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
|
||||
capabilities.receiveMessage === true ||
|
||||
(Array.isArray(capabilities.receiveMessage) && capabilities.receiveMessage.length) ||
|
||||
capabilities.sendState?.length ||
|
||||
capabilities.receiveState?.length
|
||||
) {
|
||||
widgetApi.requestCapabilityForRoomTimeline(roomId);
|
||||
}
|
||||
capabilities.sendEvent?.forEach((eventType) => widgetApi.requestCapabilityToSendEvent(eventType));
|
||||
capabilities.receiveEvent?.forEach((eventType) => widgetApi.requestCapabilityToReceiveEvent(eventType));
|
||||
if (capabilities.sendMessage === true) {
|
||||
widgetApi.requestCapabilityToSendMessage();
|
||||
} else if (Array.isArray(capabilities.sendMessage)) {
|
||||
capabilities.sendMessage.forEach((msgType) => widgetApi.requestCapabilityToSendMessage(msgType));
|
||||
}
|
||||
if (capabilities.receiveMessage === true) {
|
||||
widgetApi.requestCapabilityToReceiveMessage();
|
||||
} else if (Array.isArray(capabilities.receiveMessage)) {
|
||||
capabilities.receiveMessage.forEach((msgType) => widgetApi.requestCapabilityToReceiveMessage(msgType));
|
||||
}
|
||||
capabilities.sendState?.forEach(({ eventType, stateKey }) =>
|
||||
widgetApi.requestCapabilityToSendState(eventType, stateKey),
|
||||
);
|
||||
capabilities.receiveState?.forEach(({ eventType, stateKey }) =>
|
||||
widgetApi.requestCapabilityToReceiveState(eventType, stateKey),
|
||||
);
|
||||
capabilities.sendToDevice?.forEach((eventType) => widgetApi.requestCapabilityToSendToDevice(eventType));
|
||||
capabilities.receiveToDevice?.forEach((eventType) => widgetApi.requestCapabilityToReceiveToDevice(eventType));
|
||||
if (
|
||||
capabilities.sendDelayedEvents &&
|
||||
(capabilities.sendEvent?.length ||
|
||||
capabilities.sendMessage === true ||
|
||||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
|
||||
capabilities.sendState?.length)
|
||||
) {
|
||||
widgetApi.requestCapability(MatrixCapabilities.MSC4157SendDelayedEvent);
|
||||
}
|
||||
if (capabilities.updateDelayedEvents) {
|
||||
widgetApi.requestCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||
}
|
||||
if (capabilities.turnServers) {
|
||||
widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
|
||||
}
|
||||
this.requestInitialCapabilities(capabilities, roomId);
|
||||
|
||||
widgetApi.on(`action:${WidgetApiToWidgetAction.SendEvent}`, this.onEvent);
|
||||
widgetApi.on(`action:${WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice);
|
||||
@@ -260,10 +228,70 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
if (sendContentLoaded) widgetApi.sendContentLoaded();
|
||||
}
|
||||
|
||||
private requestInitialCapabilities(capabilities: ICapabilities, roomId: string): void {
|
||||
// Request capabilities for the functionality this client needs to support
|
||||
if (
|
||||
capabilities.sendEvent?.length ||
|
||||
capabilities.receiveEvent?.length ||
|
||||
capabilities.sendMessage === true ||
|
||||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
|
||||
capabilities.receiveMessage === true ||
|
||||
(Array.isArray(capabilities.receiveMessage) && capabilities.receiveMessage.length) ||
|
||||
capabilities.sendState?.length ||
|
||||
capabilities.receiveState?.length
|
||||
) {
|
||||
this.widgetApi.requestCapabilityForRoomTimeline(roomId);
|
||||
}
|
||||
capabilities.sendEvent?.forEach((eventType) => this.widgetApi.requestCapabilityToSendEvent(eventType));
|
||||
capabilities.receiveEvent?.forEach((eventType) => this.widgetApi.requestCapabilityToReceiveEvent(eventType));
|
||||
if (capabilities.sendMessage === true) {
|
||||
this.widgetApi.requestCapabilityToSendMessage();
|
||||
} else if (Array.isArray(capabilities.sendMessage)) {
|
||||
capabilities.sendMessage.forEach((msgType) => this.widgetApi.requestCapabilityToSendMessage(msgType));
|
||||
}
|
||||
if (capabilities.receiveMessage === true) {
|
||||
this.widgetApi.requestCapabilityToReceiveMessage();
|
||||
} else if (Array.isArray(capabilities.receiveMessage)) {
|
||||
capabilities.receiveMessage.forEach((msgType) => this.widgetApi.requestCapabilityToReceiveMessage(msgType));
|
||||
}
|
||||
capabilities.sendState?.forEach(({ eventType, stateKey }) =>
|
||||
this.widgetApi.requestCapabilityToSendState(eventType, stateKey),
|
||||
);
|
||||
capabilities.receiveState?.forEach(({ eventType, stateKey }) =>
|
||||
this.widgetApi.requestCapabilityToReceiveState(eventType, stateKey),
|
||||
);
|
||||
capabilities.sendToDevice?.forEach((eventType) => this.widgetApi.requestCapabilityToSendToDevice(eventType));
|
||||
capabilities.receiveToDevice?.forEach((eventType) =>
|
||||
this.widgetApi.requestCapabilityToReceiveToDevice(eventType),
|
||||
);
|
||||
if (
|
||||
capabilities.sendDelayedEvents &&
|
||||
(capabilities.sendEvent?.length ||
|
||||
capabilities.sendMessage === true ||
|
||||
(Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length) ||
|
||||
capabilities.sendState?.length)
|
||||
) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC4157SendDelayedEvent);
|
||||
}
|
||||
if (capabilities.updateDelayedEvents) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||
}
|
||||
if (capabilities.sendSticky) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC4407SendStickyEvent);
|
||||
}
|
||||
if (capabilities.receiveSticky) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC4407ReceiveStickyEvent);
|
||||
}
|
||||
if (capabilities.turnServers) {
|
||||
this.widgetApi.requestCapability(MatrixCapabilities.MSC3846TurnServers);
|
||||
}
|
||||
}
|
||||
|
||||
public async supportUpdateState(): Promise<boolean> {
|
||||
return (await this.widgetApi.getClientVersions()).includes(UnstableApiVersion.MSC2762_UPDATE_STATE);
|
||||
}
|
||||
|
||||
private readonly syncApiResolver = Promise.withResolvers<void>();
|
||||
public async startClient(opts: IStartClientOpts = {}): Promise<void> {
|
||||
this.lifecycle = new AbortController();
|
||||
|
||||
@@ -282,6 +310,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
} else {
|
||||
this.syncApi = new SyncApi(this, opts, this.buildSyncApiOptions());
|
||||
}
|
||||
this.syncApiResolver.resolve();
|
||||
|
||||
this.room = this.syncApi.createRoom(this.roomId);
|
||||
this.store.storeRoom(this.room);
|
||||
@@ -346,17 +375,41 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
throw new Error(`Unknown room: ${roomIdOrAlias}`);
|
||||
}
|
||||
|
||||
protected async encryptAndSendEvent(room: Room, event: MatrixEvent): Promise<ISendEventResponse>;
|
||||
protected async encryptAndSendEvent(
|
||||
room: Room,
|
||||
event: MatrixEvent,
|
||||
queryDict?: QueryDict,
|
||||
): Promise<ISendEventResponse>;
|
||||
protected async encryptAndSendEvent(
|
||||
room: Room,
|
||||
event: MatrixEvent,
|
||||
delayOpts: SendDelayedEventRequestOpts,
|
||||
): Promise<SendDelayedEventResponse>;
|
||||
queryDict?: QueryDict,
|
||||
): Promise<ISendEventResponse>;
|
||||
protected async encryptAndSendEvent(
|
||||
room: Room,
|
||||
event: MatrixEvent,
|
||||
delayOpts?: SendDelayedEventRequestOpts,
|
||||
delayOptsOrQuery?: SendDelayedEventRequestOpts | QueryDict,
|
||||
queryDict?: QueryDict,
|
||||
): Promise<ISendEventResponse | SendDelayedEventResponse> {
|
||||
let queryOpts = queryDict;
|
||||
let delayOpts: SendDelayedEventRequestOpts | undefined;
|
||||
if (delayOptsOrQuery && isSendDelayedEventRequestOpts(delayOptsOrQuery)) {
|
||||
delayOpts = delayOptsOrQuery;
|
||||
} else if (!queryOpts) {
|
||||
queryOpts = delayOptsOrQuery;
|
||||
}
|
||||
|
||||
const stickyDurationMs = queryOpts?.["org.matrix.msc4354.sticky_duration_ms"];
|
||||
if (stickyDurationMs !== undefined && typeof stickyDurationMs !== "number") {
|
||||
throw new Error("Sticky duration must be a number when defined");
|
||||
}
|
||||
// This is save since we just checked that above
|
||||
// We need the additional as assertion for the EW linter to be happy.
|
||||
// It is not capable of implying the type based on the throw if `stickyDurationMs !== undefined && typeof stickyDurationMs !== "number"`
|
||||
// above
|
||||
const stickyDurationMsAsNumber: number | undefined = stickyDurationMs as number | undefined;
|
||||
|
||||
// We need to extend the content with the redacts parameter
|
||||
// The js sdk uses event.redacts but the widget api uses event.content.redacts
|
||||
// This will be converted back to event.redacts in the widget driver.
|
||||
@@ -374,6 +427,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
room.roomId,
|
||||
"delay" in delayOpts ? delayOpts.delay : undefined,
|
||||
"parent_delay_id" in delayOpts ? delayOpts.parent_delay_id : undefined,
|
||||
stickyDurationMsAsNumber,
|
||||
)
|
||||
.catch(timeoutToConnectionError);
|
||||
return this.validateSendDelayedEventResponse(response);
|
||||
@@ -386,7 +440,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
let response: ISendEventFromWidgetResponseData;
|
||||
try {
|
||||
response = await this.widgetApi
|
||||
.sendRoomEvent(event.getType(), content, room.roomId)
|
||||
.sendRoomEvent(event.getType(), content, room.roomId, undefined, undefined, stickyDurationMsAsNumber)
|
||||
.catch(timeoutToConnectionError);
|
||||
} catch (e) {
|
||||
this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
|
||||
@@ -683,7 +737,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
|
||||
// Only inject once we have update the txId
|
||||
await this.updateTxId(event);
|
||||
|
||||
await this.syncApiResolver.promise;
|
||||
if (this.syncApi instanceof SyncApi) {
|
||||
if (await this.supportUpdateState()) {
|
||||
await this.syncApi.injectRoomEvents(this.room!, undefined, [], [event]);
|
||||
@@ -704,6 +758,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
}
|
||||
|
||||
this.emit(ClientEvent.Event, event);
|
||||
if (event.unstableStickyInfo !== undefined) this.room!._unstable_addStickyEvents([event]);
|
||||
this.setSyncState(SyncState.Syncing);
|
||||
logger.info(`Received event ${event.getId()} ${event.getType()}`);
|
||||
} else {
|
||||
@@ -737,6 +792,7 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
"received update_state widget action but the widget driver did not claim to support 'org.matrix.msc2762_update_state'",
|
||||
);
|
||||
}
|
||||
await this.syncApiResolver.promise;
|
||||
for (const rawEvent of ev.detail.data.state) {
|
||||
// Verify the room ID matches, since it's possible for the client to
|
||||
// send us state updates from other rooms if this widget is always
|
||||
|
||||
@@ -67,6 +67,11 @@ export interface IHttpOpts {
|
||||
* Optional, only called when a refreshToken is present
|
||||
*/
|
||||
tokenRefreshFunction?: TokenRefreshFunction;
|
||||
|
||||
/**
|
||||
* Whether to use the HTTP Authorization header over the `access_token` query parameter
|
||||
* @deprecated as of v1.11 in https://spec.matrix.org/v1.17/client-server-api/#using-access-tokens
|
||||
*/
|
||||
useAuthorizationHeader?: boolean; // defaults to true
|
||||
|
||||
/** For historical reasons, must be set to `true`. Will eventually be removed. */
|
||||
|
||||
@@ -372,12 +372,53 @@ export class CallMembership {
|
||||
*/
|
||||
public get slotId(): string {
|
||||
const { kind, data } = this.membershipData;
|
||||
if (data.application === "m.call") {
|
||||
switch (kind) {
|
||||
case "rtc":
|
||||
return data.slot_id;
|
||||
case "session":
|
||||
default: {
|
||||
const [application, id] = [this.application, data.call_id];
|
||||
|
||||
// INFO_SLOT_ID_LEGACY_CASE (search for all occurances of this INFO to get the full picture)
|
||||
// The spec got changed to use `"ROOM"` instead of `""` empyt string for the implicit default call.
|
||||
// State events still are sent with `""` however. To find other events that should end up in the same call,
|
||||
// we use the slotId.
|
||||
// Since the CallMembership is the public representation of a rtc.member event, we just pretend it is a
|
||||
// "ROOM" slotId/call_id.
|
||||
// This makes all the remote members work with just this simple trick.
|
||||
//
|
||||
// We of course now need to be careful when sending legacy events (state events)
|
||||
// They get a slotDescription containing "ROOM" since this is what we use starting at the time this comment
|
||||
// is commited.
|
||||
//
|
||||
// See the Other INFO_SLOT_ID_LEGACY_CASE comments to see where we revert back to "" just before sending the event.
|
||||
let compatibilityAdaptedId: string;
|
||||
if (id === "") {
|
||||
compatibilityAdaptedId = "ROOM";
|
||||
this.logger?.info("use slotId compat hack emptyString -> ROOM");
|
||||
} else {
|
||||
compatibilityAdaptedId = id;
|
||||
}
|
||||
return slotDescriptionToId({
|
||||
application,
|
||||
id: compatibilityAdaptedId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger?.info("NOT using slotId compat hack emptyString -> ROOM");
|
||||
// This is what the function should look like for any other application that did not
|
||||
// go through a `""`=> `"ROOM"` rename
|
||||
switch (kind) {
|
||||
case "rtc":
|
||||
return data.slot_id;
|
||||
case "session":
|
||||
default:
|
||||
return slotDescriptionToId({ application: this.application, id: data.call_id });
|
||||
default: {
|
||||
const [application, id] = [this.application, data.call_id];
|
||||
return slotDescriptionToId({ application, id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,13 +26,12 @@ import { CallMembership } from "./CallMembership.ts";
|
||||
import { RoomStateEvent } from "../models/room-state.ts";
|
||||
import { MembershipManager, StickyEventMembershipManager } from "./MembershipManager.ts";
|
||||
import { type CallMembershipIdentityParts, EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts";
|
||||
import { deepCompare, logDurationSync } from "../utils.ts";
|
||||
import { logDurationSync } from "../utils.ts";
|
||||
import type {
|
||||
Statistics,
|
||||
RTCNotificationType,
|
||||
Status,
|
||||
IRTCNotificationContent,
|
||||
ICallNotifyContent,
|
||||
RTCCallIntent,
|
||||
Transport,
|
||||
} from "./types.ts";
|
||||
@@ -81,7 +80,6 @@ export type MatrixRTCSessionEventHandlerMap = {
|
||||
[MatrixRTCSessionEvent.MembershipManagerError]: (error: unknown) => void;
|
||||
[MatrixRTCSessionEvent.DidSendCallNotification]: (
|
||||
notificationContentNew: { event_id: string } & IRTCNotificationContent,
|
||||
notificationContentLegacy: { event_id: string } & ICallNotifyContent,
|
||||
) => void;
|
||||
};
|
||||
|
||||
@@ -328,7 +326,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
*/
|
||||
public static async sessionMembershipsForSlot(
|
||||
room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState" | "_unstable_getStickyEvents">,
|
||||
slotDescription: SlotDescription,
|
||||
slotId: string,
|
||||
// default both true this implied we combine sticky and state events for the final call state
|
||||
// (prefer sticky events in case of a duplicate)
|
||||
options: SessionMembershipsForSlotOpts = DEFAULT_SESSION_MEMBERSHIPS_FOR_SLOT_OPTS,
|
||||
@@ -339,7 +337,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
const callMemberships = await computeBackendIdentityAndVerifyMemberEvents(
|
||||
room,
|
||||
callMemberEvents,
|
||||
slotDescription,
|
||||
slotId,
|
||||
logger,
|
||||
);
|
||||
|
||||
@@ -713,20 +711,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
notificationType: RTCNotificationType,
|
||||
callIntent?: RTCCallIntent,
|
||||
): void {
|
||||
const sendLegacyNotificationEvent = async (): Promise<{
|
||||
response: ISendEventResponse;
|
||||
content: ICallNotifyContent;
|
||||
}> => {
|
||||
const content: ICallNotifyContent = {
|
||||
"application": "m.call",
|
||||
"m.mentions": { user_ids: [], room: true },
|
||||
"notify_type": notificationType === "notification" ? "notify" : notificationType,
|
||||
"call_id": this.callId!,
|
||||
};
|
||||
const response = await this.client.sendEvent(this.roomSubset.roomId, EventType.CallNotify, content);
|
||||
return { response, content };
|
||||
};
|
||||
const sendNewNotificationEvent = async (): Promise<{
|
||||
const sendNotificationEvent = async (): Promise<{
|
||||
response: ISendEventResponse;
|
||||
content: IRTCNotificationContent;
|
||||
}> => {
|
||||
@@ -747,12 +732,11 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
return { response, content };
|
||||
};
|
||||
|
||||
void Promise.all([sendLegacyNotificationEvent(), sendNewNotificationEvent()])
|
||||
.then(([legacy, newNotification]) => {
|
||||
void sendNotificationEvent()
|
||||
.then((notification) => {
|
||||
// Join event_id and origin event content
|
||||
const legacyResult = { ...legacy.response, ...legacy.content };
|
||||
const newResult = { ...newNotification.response, ...newNotification.content };
|
||||
this.emit(MatrixRTCSessionEvent.DidSendCallNotification, newResult, legacyResult);
|
||||
const newResult = { ...notification.response, ...notification.content };
|
||||
this.emit(MatrixRTCSessionEvent.DidSendCallNotification, newResult);
|
||||
})
|
||||
.catch(([errorLegacy, errorNew]) =>
|
||||
this.logger.error("Failed to send call notification", errorLegacy, errorNew),
|
||||
@@ -823,7 +807,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
|
||||
this.memberships = await MatrixRTCSession.sessionMembershipsForSlot(
|
||||
this.room,
|
||||
this.slotDescription,
|
||||
slotDescriptionToId(this.slotDescription),
|
||||
this.calculateMembershipsOpts,
|
||||
);
|
||||
|
||||
@@ -873,7 +857,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
async function computeBackendIdentityAndVerifyMemberEvents(
|
||||
room: Pick<Room, "hasMembershipState">,
|
||||
callMemberEvents: MatrixEvent[],
|
||||
slotDescription: SlotDescription,
|
||||
slotId: string,
|
||||
logger: Logger,
|
||||
): Promise<CallMembership[]> {
|
||||
const callMemberships: CallMembership[] = [];
|
||||
@@ -897,7 +881,7 @@ async function computeBackendIdentityAndVerifyMemberEvents(
|
||||
logger,
|
||||
);
|
||||
|
||||
if (isValidMembership(membership, room, slotDescription, logger)) {
|
||||
if (isValidMembership(membership, room, slotId, logger)) {
|
||||
callMemberships.push(membership);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -930,12 +914,12 @@ function quickFilterNonRelevantContents(content: IContent, logger: Logger): bool
|
||||
function isValidMembership(
|
||||
membership: CallMembership,
|
||||
room: Pick<Room, "hasMembershipState">,
|
||||
slotDescription: SlotDescription,
|
||||
slotId: string,
|
||||
logger: Logger,
|
||||
): boolean {
|
||||
if (!deepCompare(membership.slotDescription, slotDescription)) {
|
||||
if (membership.slotId !== slotId) {
|
||||
logger.info(
|
||||
`Ignoring membership of user ${membership.userId} for a different slot: ${JSON.stringify(membership.slotDescription)}`,
|
||||
`Ignoring membership of user ${membership.userId} for a different slot: user: ${JSON.stringify(membership.slotDescription)}, slotId: ${slotId})`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
|
||||
public constructor(
|
||||
rootLogger: Logger,
|
||||
private client: MatrixClient,
|
||||
private readonly slotDescription: SlotDescription = { application: "m.call", id: "" }, // Default to the Matrix Call application
|
||||
private readonly slotDescription: SlotDescription = { application: "m.call", id: "ROOM" }, // Default to the Matrix Call application
|
||||
) {
|
||||
super();
|
||||
this.logger = rootLogger.getChild("[MatrixRTCSessionManager]");
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
type IMembershipManager,
|
||||
type MembershipManagerEventHandlerMap,
|
||||
} from "./IMembershipManager.ts";
|
||||
import { isLivekitTransportConfig } from "./LivekitTransport.ts";
|
||||
|
||||
/* MembershipActionTypes:
|
||||
On Join: ───────────────┐ ┌───────────────(1)───────────┐
|
||||
@@ -584,7 +585,7 @@ export class MembershipManager
|
||||
this.emit(MembershipManagerEvent.ProbablyLeft, this.state.probablyLeft);
|
||||
}
|
||||
|
||||
private setAndEmitDelayId(delayId?: string): void {
|
||||
private setAndEmitDelayId(delayId: string | undefined): void {
|
||||
if (this.state.delayId === delayId) return;
|
||||
|
||||
this.state.delayId = delayId;
|
||||
@@ -654,6 +655,7 @@ export class MembershipManager
|
||||
._unstable_sendScheduledDelayedEvent(delayId)
|
||||
.then(() => {
|
||||
this.state.hasMemberStateEvent = false;
|
||||
this.setAndEmitDelayId(undefined);
|
||||
this.resetRateLimitCounter(MembershipActionType.SendScheduledDelayedLeaveEvent);
|
||||
|
||||
return { replace: [] };
|
||||
@@ -772,7 +774,12 @@ export class MembershipManager
|
||||
* which is not compatible with membershipID of session type member events. They have to be `${localUserId}:${localDeviceId}`
|
||||
*/
|
||||
private makeMembershipStateKey(localUserId: string, localDeviceId: string): string {
|
||||
const stateKey = `${localUserId}_${localDeviceId}_${this.slotDescription.application}${this.slotDescription.id}`;
|
||||
// INFO_SLOT_ID_LEGACY_CASE (search for all occurances of this INFO to get the full picture)
|
||||
// Revert back to "" just for the state key (state keys are always legacy. we use sticky events for non legacy events)
|
||||
const application = this.slotDescription.application;
|
||||
const needsEmptyStringRoomFix = application === "m.call" && this.slotDescription.id === "ROOM";
|
||||
const slotId = needsEmptyStringRoomFix ? "" : this.slotDescription.id;
|
||||
const stateKey = `${localUserId}_${localDeviceId}_${application}${slotId}`;
|
||||
if (/^org\.matrix\.msc(3757|3779)\b/.exec(this.room.getVersion())) {
|
||||
return stateKey;
|
||||
} else {
|
||||
@@ -785,6 +792,8 @@ export class MembershipManager
|
||||
*/
|
||||
protected makeMyMembership(expires: number): SessionMembershipData | RtcMembershipData {
|
||||
const ownMembership = this.ownMembership;
|
||||
const needsEmptyStringRoomFix =
|
||||
this.slotDescription.application === "m.call" && this.slotDescription.id === "ROOM";
|
||||
|
||||
const focusObjects =
|
||||
this.rtcTransport === undefined
|
||||
@@ -798,7 +807,9 @@ export class MembershipManager
|
||||
};
|
||||
return {
|
||||
"application": this.slotDescription.application,
|
||||
"call_id": this.slotDescription.id,
|
||||
// INFO_SLOT_ID_LEGACY_CASE (search for all occurances of this INFO to get the full picture)
|
||||
// Revert back to "" just for the sending the event.
|
||||
"call_id": needsEmptyStringRoomFix ? "" : this.slotDescription.id,
|
||||
"scope": "m.room",
|
||||
"device_id": this.deviceId,
|
||||
// DO NOT use this.memberId here since that is the state key (using application...)
|
||||
@@ -1091,6 +1102,7 @@ export class StickyEventMembershipManager extends MembershipManager {
|
||||
protected makeMyMembership(expires: number): SessionMembershipData | RtcMembershipData {
|
||||
const ownMembership = this.ownMembership;
|
||||
|
||||
const livekitTransport = isLivekitTransportConfig(this.rtcTransport) ? this.rtcTransport : undefined;
|
||||
const relationObject = ownMembership?.eventId
|
||||
? { "m.relation": { rel_type: RelationType.Reference, event_id: ownMembership?.eventId } }
|
||||
: {};
|
||||
@@ -1100,7 +1112,11 @@ export class StickyEventMembershipManager extends MembershipManager {
|
||||
...(this.callIntent ? { "m.call.intent": this.callIntent } : {}),
|
||||
},
|
||||
slot_id: slotDescriptionToId(this.slotDescription),
|
||||
rtc_transports: this.rtcTransport ? [this.rtcTransport] : [],
|
||||
// Make sure we do not add the alias to the transport.
|
||||
// It is not needed in matrix2.0. The additional session information will be used to find the right alias on the sfu.
|
||||
rtc_transports: livekitTransport
|
||||
? [{ type: livekitTransport.type, livekit_service_url: livekitTransport.livekit_service_url }]
|
||||
: [],
|
||||
member: { device_id: this.deviceId, user_id: this.userId, id: this.memberId },
|
||||
versions: [],
|
||||
...relationObject,
|
||||
|
||||
@@ -38,7 +38,6 @@ export type BeaconEventHandlerMap = {
|
||||
[BeaconEvent.LivenessChange]: (isLive: boolean, beacon: Beacon) => void;
|
||||
[BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
|
||||
[BeaconEvent.LocationUpdate]: (locationState: BeaconLocationState) => void;
|
||||
[BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
|
||||
};
|
||||
|
||||
export const isTimestampInDuration = (startTimestamp: number, durationMs: number, timestamp: number): boolean =>
|
||||
|
||||
+3
-1
@@ -76,6 +76,8 @@ export interface IUnsigned {
|
||||
"m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations
|
||||
"msc4354_sticky_duration_ttl_ms"?: number;
|
||||
[UNSIGNED_THREAD_ID_FIELD.name]?: string;
|
||||
"membership"?: Membership;
|
||||
"io.element.msc4115.membership"?: Membership;
|
||||
}
|
||||
|
||||
export interface IThreadBundledRelationship {
|
||||
@@ -786,7 +788,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
*/
|
||||
public getMembershipAtEvent(): Membership | string | undefined {
|
||||
const unsigned = this.getUnsigned();
|
||||
return UNSIGNED_MEMBERSHIP_FIELD.findIn<Membership | string>(unsigned);
|
||||
return UNSIGNED_MEMBERSHIP_FIELD.findIn<Membership>(unsigned);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+38
-16
@@ -53,6 +53,7 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
||||
private sortedAnnotationsByKey: [string, Set<MatrixEvent>][] = [];
|
||||
private targetEvent: MatrixEvent | null = null;
|
||||
private creationEmitted = false;
|
||||
private replacementUpdateId = 0;
|
||||
private readonly client: MatrixClient;
|
||||
|
||||
/**
|
||||
@@ -106,9 +107,8 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
||||
|
||||
if (this.relationType === RelationType.Annotation) {
|
||||
this.addAnnotationToAggregation(event);
|
||||
} else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) {
|
||||
const lastReplacement = await this.getLastReplacement();
|
||||
this.targetEvent.makeReplaced(lastReplacement!);
|
||||
} else if (this.relationType === RelationType.Replace) {
|
||||
await this.updateTargetEventReplacement();
|
||||
}
|
||||
|
||||
event.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
|
||||
@@ -132,9 +132,8 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
||||
|
||||
if (this.relationType === RelationType.Annotation) {
|
||||
this.removeAnnotationFromAggregation(event);
|
||||
} else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) {
|
||||
const lastReplacement = await this.getLastReplacement();
|
||||
this.targetEvent.makeReplaced(lastReplacement!);
|
||||
} else if (this.relationType === RelationType.Replace) {
|
||||
await this.updateTargetEventReplacement();
|
||||
}
|
||||
|
||||
this.emit(RelationsEvent.Remove, event);
|
||||
@@ -243,9 +242,8 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
||||
if (this.relationType === RelationType.Annotation) {
|
||||
// Remove the redacted annotation from aggregation by key
|
||||
this.removeAnnotationFromAggregation(redactedEvent);
|
||||
} else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) {
|
||||
const lastReplacement = await this.getLastReplacement();
|
||||
this.targetEvent.makeReplaced(lastReplacement!);
|
||||
} else if (this.relationType === RelationType.Replace) {
|
||||
await this.updateTargetEventReplacement();
|
||||
}
|
||||
|
||||
redactedEvent.removeListener(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
|
||||
@@ -343,18 +341,42 @@ export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap
|
||||
}
|
||||
this.targetEvent = event;
|
||||
|
||||
if (this.relationType === RelationType.Replace && !this.targetEvent.isState()) {
|
||||
const replacement = await this.getLastReplacement();
|
||||
// this is the initial update, so only call it if we already have something
|
||||
// to not emit Event.replaced needlessly
|
||||
if (replacement) {
|
||||
this.targetEvent.makeReplaced(replacement);
|
||||
}
|
||||
if (this.relationType === RelationType.Replace) {
|
||||
await this.updateTargetEventReplacement();
|
||||
}
|
||||
|
||||
this.maybeEmitCreated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the target event with the latest replacement.
|
||||
*
|
||||
* Multiple replacement updates can be triggered concurrently (for example
|
||||
* while edits are still being decrypted). A monotonic update counter guards
|
||||
* against older async resolutions overriding newer replacement selections.
|
||||
*/
|
||||
private async updateTargetEventReplacement(): Promise<void> {
|
||||
if (!this.targetEvent || this.targetEvent.isState()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetEvent = this.targetEvent;
|
||||
const updateId = ++this.replacementUpdateId;
|
||||
const lastReplacement = await this.getLastReplacement();
|
||||
|
||||
// If a newer update started while we were awaiting, discard this stale result.
|
||||
if (updateId !== this.replacementUpdateId || this.targetEvent !== targetEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid emitting Event.replaced when there is no replacement and none currently set.
|
||||
if (!lastReplacement && !targetEvent.replacingEvent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
targetEvent.makeReplaced(lastReplacement ?? undefined);
|
||||
}
|
||||
|
||||
private maybeEmitCreated(): void {
|
||||
if (this.creationEmitted) {
|
||||
return;
|
||||
|
||||
@@ -227,6 +227,42 @@ export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEve
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate the disambiguation flag for this member based on current room state.
|
||||
* This should be called when another member's display name changes and may affect
|
||||
* whether this member needs disambiguation.
|
||||
*
|
||||
* @param roomState - The current room state to use for disambiguation check
|
||||
* @returns true if the member's name changed as a result of the disambiguation update
|
||||
*
|
||||
* @remarks
|
||||
* Fires {@link RoomMemberEvent.Name}
|
||||
*/
|
||||
public recalculateDisambiguatedName(roomState: RoomState): boolean {
|
||||
if (!this.events.member) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const displayName = this.events.member.getDirectionalContent().displayname ?? "";
|
||||
const newDisambiguate = shouldDisambiguate(this.userId, displayName, roomState);
|
||||
|
||||
if (newDisambiguate === this.disambiguate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.disambiguate = newDisambiguate;
|
||||
const oldName = this.name;
|
||||
this.name = calculateDisplayName(this.userId, displayName, this.disambiguate);
|
||||
|
||||
if (oldName !== this.name) {
|
||||
this.updateModifiedTime();
|
||||
this.emit(RoomMemberEvent.Name, this.events.member, this, oldName);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this room member's power level event. Will fire
|
||||
* "RoomMember.powerLevel" if the new power level is different
|
||||
|
||||
@@ -438,6 +438,11 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
this.updateModifiedTime();
|
||||
|
||||
// update the core event dict
|
||||
// Track display names that change so we can recalculate disambiguation
|
||||
const affectedDisplayNames = new Set<string>();
|
||||
// Track userIds whose membership events we process so we don't emit duplicate events
|
||||
const processedMemberUserIds = new Set<string>();
|
||||
|
||||
stateEvents.forEach((event) => {
|
||||
if (event.getRoomId() !== this.roomId || !event.isState()) return;
|
||||
|
||||
@@ -448,7 +453,22 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
const lastStateEvent = this.getStateEventMatching(event);
|
||||
this.setStateEvent(event);
|
||||
if (event.getType() === EventType.RoomMember) {
|
||||
this.updateDisplayNameCache(event.getStateKey()!, event.getContent().displayname ?? "");
|
||||
const userId = event.getStateKey()!;
|
||||
processedMemberUserIds.add(userId);
|
||||
const newDisplayName = event.getContent().displayname ?? "";
|
||||
const oldDisplayName = this.userIdsToDisplayNames[userId];
|
||||
|
||||
// Track both old and new display names for disambiguation recalculation
|
||||
if (oldDisplayName) {
|
||||
const strippedOld = removeHiddenChars(oldDisplayName);
|
||||
if (strippedOld) affectedDisplayNames.add(strippedOld);
|
||||
}
|
||||
if (newDisplayName) {
|
||||
const strippedNew = removeHiddenChars(newDisplayName);
|
||||
if (strippedNew) affectedDisplayNames.add(strippedNew);
|
||||
}
|
||||
|
||||
this.updateDisplayNameCache(userId, newDisplayName);
|
||||
this.updateThirdPartyTokenCache(event);
|
||||
}
|
||||
this.emit(RoomStateEvent.Events, event, this, lastStateEvent);
|
||||
@@ -514,6 +534,33 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
}
|
||||
});
|
||||
|
||||
// Recalculate disambiguation for all members whose display names were affected.
|
||||
// This ensures that when a user changes their name to match (or stop matching)
|
||||
// another user, all affected users' disambiguation flags are updated correctly.
|
||||
if (affectedDisplayNames.size > 0) {
|
||||
// Collect all affected user IDs first to avoid duplicate processing
|
||||
const affectedUserIds = new Set<string>();
|
||||
for (const displayName of affectedDisplayNames) {
|
||||
const userIds = this.displayNameToUserIds.get(displayName) ?? [];
|
||||
userIds.forEach((id) => affectedUserIds.add(id));
|
||||
}
|
||||
|
||||
// Process each affected member once, excluding those whose membership
|
||||
// events were already processed (they already got their events emitted)
|
||||
for (const userId of affectedUserIds) {
|
||||
if (processedMemberUserIds.has(userId)) {
|
||||
continue;
|
||||
}
|
||||
const member = this.members[userId];
|
||||
if (member?.events.member) {
|
||||
const nameChanged = member.recalculateDisambiguatedName(this);
|
||||
if (nameChanged) {
|
||||
this.emit(RoomStateEvent.Members, member.events.member, this, member);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(RoomStateEvent.Update, this);
|
||||
}
|
||||
|
||||
@@ -1110,6 +1157,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
|
||||
private updateDisplayNameCache(userId: string, displayName: string): void {
|
||||
const oldName = this.userIdsToDisplayNames[userId];
|
||||
|
||||
delete this.userIdsToDisplayNames[userId];
|
||||
if (oldName) {
|
||||
// Remove the old name from the cache.
|
||||
|
||||
@@ -1103,6 +1103,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
const inMemoryUpdate = this.loadMembers()
|
||||
.then((result) => {
|
||||
this.currentState.setOutOfBandMembers(result.memberEvents);
|
||||
// recalculate the room name: it may have been based on members, so may have changed
|
||||
this.recalculate();
|
||||
return result.fromServer;
|
||||
})
|
||||
.catch((err) => {
|
||||
|
||||
+29
-2
@@ -52,7 +52,34 @@ interface OidcRegistrationRequestBody {
|
||||
application_type: "web" | "native";
|
||||
}
|
||||
|
||||
export const DEVICE_CODE_SCOPE = "urn:ietf:params:oauth:grant-type:device_code";
|
||||
/**
|
||||
* The OAuth 2.0 grant types that are defined for Matrix in https://spec.matrix.org/v1.17/client-server-api/#grant-types
|
||||
*/
|
||||
export enum OAuthGrantType {
|
||||
/**
|
||||
* See https://spec.matrix.org/v1.17/client-server-api/#authorization-code-grant
|
||||
*/
|
||||
AuthorizationCode = "authorization_code",
|
||||
/**
|
||||
* https://spec.matrix.org/v1.17/client-server-api/#refresh-token-grant
|
||||
*/
|
||||
RefreshToken = "refresh_token",
|
||||
/**
|
||||
* The OAuth 2.0 Device Authorization Grant type identifier as per
|
||||
* https://www.rfc-editor.org/rfc/rfc8628.html#section-7.2 from
|
||||
* [MSC4341](https://github.com/matrix-org/matrix-spec-proposals/pull/4341).
|
||||
*
|
||||
* @experimental Note that this is UNSTABLE and may have breaking changes without notice.
|
||||
*/
|
||||
DeviceAuthorization = "urn:ietf:params:oauth:grant-type:device_code",
|
||||
}
|
||||
|
||||
/**
|
||||
* The name "scope" is a misnomer here as it is actually a "grant type".
|
||||
*
|
||||
* @deprecated use `OAuthGrantType.DeviceAuthorization` instead
|
||||
*/
|
||||
export const DEVICE_CODE_SCOPE: string = OAuthGrantType.DeviceAuthorization;
|
||||
|
||||
// Check that URIs have a common base, as per the MSC2966 definition
|
||||
const urlHasCommonBase = (base: URL, urlStr?: string): boolean => {
|
||||
@@ -79,7 +106,7 @@ export const registerOidcClient = async (
|
||||
throw new Error(OidcError.DynamicRegistrationNotSupported);
|
||||
}
|
||||
|
||||
const grantTypes: NonEmptyArray<string> = ["authorization_code", "refresh_token"];
|
||||
const grantTypes: NonEmptyArray<string> = [OAuthGrantType.AuthorizationCode, OAuthGrantType.RefreshToken];
|
||||
if (grantTypes.some((scope) => !delegatedAuthConfig.grant_types_supported.includes(scope))) {
|
||||
throw new Error(OidcError.DynamicRegistrationNotSupported);
|
||||
}
|
||||
|
||||
+14
-8
@@ -19,13 +19,17 @@ import { type IdTokenClaims, type OidcMetadata, type SigninResponse } from "oidc
|
||||
|
||||
import { logger } from "../logger.ts";
|
||||
import { OidcError } from "./error.ts";
|
||||
import { OAuthGrantType } from "./index.ts";
|
||||
|
||||
/**
|
||||
* Metadata from OIDC authority discovery
|
||||
* Metadata from OAuth 2.0 client authentication API as per
|
||||
* https://spec.matrix.org/v1.17/client-server-api/#get_matrixclientv1auth_metadata
|
||||
* With validated properties required in type
|
||||
*/
|
||||
export type ValidatedAuthMetadata = Partial<OidcMetadata> &
|
||||
Pick<
|
||||
// These values are from [RFC8414](https://datatracker.ietf.org/doc/html/rfc8414#section-2)
|
||||
// so we can reuse the OidcMetadata definitions from oidc-client-ts
|
||||
OidcMetadata,
|
||||
| "issuer"
|
||||
| "authorization_endpoint"
|
||||
@@ -35,12 +39,14 @@ export type ValidatedAuthMetadata = Partial<OidcMetadata> &
|
||||
| "grant_types_supported"
|
||||
| "code_challenge_methods_supported"
|
||||
> & {
|
||||
// MSC2965 extensions to the OIDC spec
|
||||
// These values aren't part of RFC8414 so we add them here
|
||||
// Account management fields from stable MSC4191:
|
||||
account_management_uri?: string;
|
||||
account_management_actions_supported?: string[];
|
||||
// The OidcMetadata type from oidc-client-ts does not include `prompt_values_supported`
|
||||
// even though it is part of the OIDC spec
|
||||
// Value from [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html):
|
||||
prompt_values_supported?: string[];
|
||||
// Experimental MSC4341 value from [RFC8628](https://datatracker.ietf.org/doc/html/rfc8628#section-4):
|
||||
device_authorization_endpoint?: string;
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
@@ -79,9 +85,9 @@ const requiredArrayValue = (wellKnown: Record<string, unknown>, key: string, val
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates issuer `.well-known/openid-configuration`
|
||||
* As defined in RFC5785 https://openid.net/specs/openid-connect-discovery-1_0.html
|
||||
* validates that OP is compatible with Element's OIDC flow
|
||||
* Validates OAuth 2.0 auth metadata as defined by
|
||||
* https://spec.matrix.org/v1.17/client-server-api/#get_matrixclientv1auth_metadata
|
||||
* is compatible with Element's OAuth/OIDC flow
|
||||
* @param authMetadata - json object
|
||||
* @returns valid issuer config
|
||||
* @throws Error - when issuer config is not found or is invalid
|
||||
@@ -102,7 +108,7 @@ export const validateAuthMetadata = (authMetadata: unknown): ValidatedAuthMetada
|
||||
optionalStringProperty(authMetadata, "device_authorization_endpoint"),
|
||||
optionalStringArrayProperty(authMetadata, "account_management_actions_supported"),
|
||||
requiredArrayValue(authMetadata, "response_types_supported", "code"),
|
||||
requiredArrayValue(authMetadata, "grant_types_supported", "authorization_code"),
|
||||
requiredArrayValue(authMetadata, "grant_types_supported", OAuthGrantType.AuthorizationCode),
|
||||
requiredArrayValue(authMetadata, "code_challenge_methods_supported", "S256"),
|
||||
optionalStringArrayProperty(authMetadata, "prompt_values_supported"),
|
||||
].some((isValid) => !isValid);
|
||||
|
||||
@@ -27,7 +27,7 @@ import { logger } from "../logger.ts";
|
||||
import { type MSC4108SecureChannel } from "./channels/MSC4108SecureChannel.ts";
|
||||
import { MatrixError } from "../http-api/index.ts";
|
||||
import { sleep } from "../utils.ts";
|
||||
import { DEVICE_CODE_SCOPE, type OidcClientConfig } from "../oidc/index.ts";
|
||||
import { OAuthGrantType, type OidcClientConfig } from "../oidc/index.ts";
|
||||
import { type CryptoApi } from "../crypto-api/index.ts";
|
||||
|
||||
/**
|
||||
@@ -194,7 +194,7 @@ export class MSC4108SignInWithQR {
|
||||
logger.error("Failed to discover OIDC metadata", e);
|
||||
}
|
||||
|
||||
if (oidcClientConfig?.grant_types_supported.includes(DEVICE_CODE_SCOPE)) {
|
||||
if (oidcClientConfig?.grant_types_supported.includes(OAuthGrantType.DeviceAuthorization)) {
|
||||
await this.send<ProtocolsPayload>({
|
||||
type: PayloadType.Protocols,
|
||||
protocols: ["device_authorization_grant"],
|
||||
|
||||
@@ -262,7 +262,9 @@ export class MSC4108RendezvousSession {
|
||||
|
||||
if (!this.url) return;
|
||||
try {
|
||||
await this.fetch(this.url, { method: Method.Delete });
|
||||
const method = Method.Delete;
|
||||
logger.info(`=> ${method} ${this.url}`);
|
||||
await this.fetch(this.url, { method });
|
||||
} catch (e) {
|
||||
logger.warn(e);
|
||||
}
|
||||
|
||||
@@ -631,6 +631,24 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
||||
return this.importKeyBackup(keyBackup, backupVersion, backupDecryptor, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and import the keys for a given room from the current backup version.
|
||||
*
|
||||
* @param roomId - The room in question.
|
||||
*/
|
||||
public async downloadLatestRoomKeyBackup(roomId: string): Promise<void> {
|
||||
const { backupVersion, decryptionKey } = await this.olmMachine.getBackupKeys();
|
||||
if (!backupVersion || !decryptionKey) {
|
||||
this.logger.warn(
|
||||
`downloadLatestRoomKeyBackup: Could not download backup (backupVersion=${backupVersion}, hasDecryptionKey=${!!decryptionKey})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const sessions = await this.downloadRoomKeyBackup(backupVersion, roomId);
|
||||
const backupDecryptor = this.createBackupDecryptor(decryptionKey);
|
||||
this.importKeyBackup({ rooms: { [roomId]: { sessions } } }, backupVersion, backupDecryptor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call `/room_keys/keys` to download the key backup (room keys) for the given backup version.
|
||||
* https://spec.matrix.org/v1.12/client-server-api/#get_matrixclientv3room_keyskeys
|
||||
@@ -650,6 +668,21 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call `/room/keys/keys/{roomId}` to download the key backup (room keys) for a given backup version and room ID.
|
||||
* @param backupVersion - The version to download.
|
||||
* @param roomId - The ID of the room.
|
||||
* @returns The key backup response.
|
||||
*/
|
||||
private downloadRoomKeyBackup(backupVersion: string, roomId: string): Promise<KeyBackupRoomSessions> {
|
||||
const path = encodeUri("/room_keys/keys/$roomId", {
|
||||
$roomId: roomId,
|
||||
});
|
||||
return this.http.authedRequest<KeyBackupRoomSessions>(Method.Get, path, { version: backupVersion }, undefined, {
|
||||
prefix: ClientPrefix.V3,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the room keys from a `/room_keys/keys` call.
|
||||
* Calls `opts.progressCallback` with the progress of the import.
|
||||
|
||||
@@ -1616,25 +1616,31 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
|
||||
logger.info("Sharing message history");
|
||||
|
||||
// 1. Construct the key bundle
|
||||
// 1. Download keys from backup.
|
||||
if (!(await this.getOlmMachineOrThrow().hasDownloadedAllRoomKeys(new RustSdkCryptoJs.RoomId(roomId)))) {
|
||||
await this.backupManager.downloadLatestRoomKeyBackup(roomId);
|
||||
await this.getOlmMachineOrThrow().setHasDownloadedAllRoomKeys(new RustSdkCryptoJs.RoomId(roomId));
|
||||
}
|
||||
|
||||
// 2. Construct the key bundle
|
||||
const bundle = await this.getOlmMachineOrThrow().buildRoomKeyBundle(new RustSdkCryptoJs.RoomId(roomId));
|
||||
if (!bundle) {
|
||||
logger.info("No keys to share");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Upload the encrypted bundle to the server
|
||||
// 3. Upload the encrypted bundle to the server
|
||||
const uploadResponse = await this.http.uploadContent(bundle.encryptedData as Uint8Array<ArrayBuffer>);
|
||||
logger.info(`Uploaded encrypted key blob: ${JSON.stringify(uploadResponse)}`);
|
||||
|
||||
// 3. We may not share a room with the user, so get a fresh list of devices for the invited user.
|
||||
// 4. We may not share a room with the user, so get a fresh list of devices for the invited user.
|
||||
const req = this.getOlmMachineOrThrow().queryKeysForUsers([new RustSdkCryptoJs.UserId(userId)]);
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
|
||||
|
||||
// 4. Establish Olm sessions with all of the recipient's devices.
|
||||
// 5. Establish Olm sessions with all of the recipient's devices.
|
||||
await this.keyClaimManager.ensureSessionsForUsers(logger, [new RustSdkCryptoJs.UserId(userId)]);
|
||||
|
||||
// 5. Send to-device messages to the recipient to share the keys.
|
||||
// 6. Send to-device messages to the recipient to share the keys.
|
||||
const requests = await this.getOlmMachineOrThrow().shareRoomKeyBundleData(
|
||||
new RustSdkCryptoJs.UserId(userId),
|
||||
new RustSdkCryptoJs.RoomId(roomId),
|
||||
|
||||
+285
-271
@@ -26,7 +26,7 @@ limitations under the License.
|
||||
import type { SyncCryptoCallbacks } from "./common-crypto/CryptoBackend.ts";
|
||||
import { User } from "./models/user.ts";
|
||||
import { NotificationCountType, Room, RoomEvent } from "./models/room.ts";
|
||||
import { deepCopy, noUnsafeEventProps, promiseMapSeries, unsafeProp } from "./utils.ts";
|
||||
import { deepCopy, noUnsafeEventProps, unsafeProp } from "./utils.ts";
|
||||
import { Filter } from "./filter.ts";
|
||||
import { EventTimeline } from "./models/event-timeline.ts";
|
||||
import { type Logger } from "./logger.ts";
|
||||
@@ -1188,312 +1188,326 @@ export class SyncApi {
|
||||
this.notifEvents = [];
|
||||
|
||||
// Handle invites
|
||||
await promiseMapSeries(inviteRooms, async (inviteObj) => {
|
||||
const room = inviteObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room);
|
||||
await Promise.all(
|
||||
inviteRooms.map(async (inviteObj) => {
|
||||
const room = inviteObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room);
|
||||
|
||||
await this.injectRoomEvents(room, stateEvents, undefined);
|
||||
await this.injectRoomEvents(room, stateEvents, undefined);
|
||||
|
||||
if (inviteObj.isBrandNewRoom) {
|
||||
room.recalculate();
|
||||
client.store.storeRoom(room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
} else {
|
||||
// Update room state for invite->reject->invite cycles
|
||||
room.recalculate();
|
||||
}
|
||||
stateEvents.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
});
|
||||
if (inviteObj.isBrandNewRoom) {
|
||||
room.recalculate();
|
||||
client.store.storeRoom(room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
} else {
|
||||
// Update room state for invite->reject->invite cycles
|
||||
room.recalculate();
|
||||
}
|
||||
stateEvents.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Handle joins
|
||||
await promiseMapSeries(joinRooms, async (joinObj) => {
|
||||
const room = joinObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(joinObj.state, room);
|
||||
const stateAfterEvents = this.mapSyncEventsFormat(joinObj["org.matrix.msc4222.state_after"], room);
|
||||
// Prevent events from being decrypted ahead of time
|
||||
// this helps large account to speed up faster
|
||||
// room::decryptCriticalEvent is in charge of decrypting all the events
|
||||
// required for a client to function properly
|
||||
const timelineEvents = this.mapSyncEventsFormat(joinObj.timeline, room, false);
|
||||
const ephemeralEvents = this.mapSyncEventsFormat(joinObj.ephemeral);
|
||||
const accountDataEvents = this.mapSyncEventsFormat(joinObj.account_data);
|
||||
const stickyEvents = this.mapSyncEventsFormat(joinObj.msc4354_sticky);
|
||||
await Promise.all(
|
||||
joinRooms.map(async (joinObj) => {
|
||||
const room = joinObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(joinObj.state, room);
|
||||
const stateAfterEvents = this.mapSyncEventsFormat(joinObj["org.matrix.msc4222.state_after"], room);
|
||||
// Prevent events from being decrypted ahead of time
|
||||
// this helps large account to speed up faster
|
||||
// room::decryptCriticalEvent is in charge of decrypting all the events
|
||||
// required for a client to function properly
|
||||
const timelineEvents = this.mapSyncEventsFormat(joinObj.timeline, room, false);
|
||||
const ephemeralEvents = this.mapSyncEventsFormat(joinObj.ephemeral);
|
||||
const accountDataEvents = this.mapSyncEventsFormat(joinObj.account_data);
|
||||
const stickyEvents = this.mapSyncEventsFormat(joinObj.msc4354_sticky);
|
||||
|
||||
// If state_after is present, this is the events that form the state at the end of the timeline block and
|
||||
// regular timeline events do *not* count towards state. If it's not present, then the state is formed by
|
||||
// the state events plus the timeline events. Note mapSyncEventsFormat returns an empty array if the field
|
||||
// is absent so we explicitly check the field on the original object.
|
||||
const eventsFormingFinalState = joinObj["org.matrix.msc4222.state_after"]
|
||||
? stateAfterEvents
|
||||
: stateEvents.concat(timelineEvents);
|
||||
// If state_after is present, this is the events that form the state at the end of the timeline block and
|
||||
// regular timeline events do *not* count towards state. If it's not present, then the state is formed by
|
||||
// the state events plus the timeline events. Note mapSyncEventsFormat returns an empty array if the field
|
||||
// is absent so we explicitly check the field on the original object.
|
||||
const eventsFormingFinalState = joinObj["org.matrix.msc4222.state_after"]
|
||||
? stateAfterEvents
|
||||
: stateEvents.concat(timelineEvents);
|
||||
|
||||
const encrypted = this.isRoomEncrypted(room, eventsFormingFinalState);
|
||||
// We store the server-provided value first so it's correct when any of the events fire.
|
||||
if (joinObj.unread_notifications) {
|
||||
/**
|
||||
* We track unread notifications ourselves in encrypted rooms, so don't
|
||||
* bother setting it here. We trust our calculations better than the
|
||||
* server's for this case, and therefore will assume that our non-zero
|
||||
* count is accurate.
|
||||
* XXX: this is known faulty as the push rule for `.m.room.encrypted` may be disabled so server
|
||||
* may issue notification counts of 0 which we wrongly trust.
|
||||
* https://github.com/matrix-org/matrix-spec-proposals/pull/2654 would fix this
|
||||
*
|
||||
* @see import("./client").fixNotificationCountOnDecryption
|
||||
*/
|
||||
if (!encrypted || joinObj.unread_notifications.notification_count === 0) {
|
||||
// In an encrypted room, if the room has notifications enabled then it's typical for
|
||||
// the server to flag all new messages as notifying. However, some push rules calculate
|
||||
// events as ignored based on their event contents (e.g. ignoring msgtype=m.notice messages)
|
||||
// so we want to calculate this figure on the client in all cases.
|
||||
room.setUnreadNotificationCount(
|
||||
NotificationCountType.Total,
|
||||
joinObj.unread_notifications.notification_count ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
if (!encrypted || room.getUnreadNotificationCount(NotificationCountType.Highlight) <= 0) {
|
||||
// If the locally stored highlight count is zero, use the server provided value.
|
||||
room.setUnreadNotificationCount(
|
||||
NotificationCountType.Highlight,
|
||||
joinObj.unread_notifications.highlight_count ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const unreadThreadNotifications =
|
||||
joinObj[UNREAD_THREAD_NOTIFICATIONS.name] ?? joinObj[UNREAD_THREAD_NOTIFICATIONS.altName!];
|
||||
if (unreadThreadNotifications) {
|
||||
// This mirrors the logic above for rooms: take the *total* notification count from
|
||||
// the server for unencrypted rooms or is it's zero. Any threads not present in this
|
||||
// object implicitly have zero notifications, so start by clearing the total counts
|
||||
// for all such threads.
|
||||
room.resetThreadUnreadNotificationCountFromSync(Object.keys(unreadThreadNotifications));
|
||||
for (const [threadId, unreadNotification] of Object.entries(unreadThreadNotifications)) {
|
||||
if (!encrypted || unreadNotification.notification_count === 0) {
|
||||
room.setThreadUnreadNotificationCount(
|
||||
threadId,
|
||||
const encrypted = this.isRoomEncrypted(room, eventsFormingFinalState);
|
||||
// We store the server-provided value first so it's correct when any of the events fire.
|
||||
if (joinObj.unread_notifications) {
|
||||
/**
|
||||
* We track unread notifications ourselves in encrypted rooms, so don't
|
||||
* bother setting it here. We trust our calculations better than the
|
||||
* server's for this case, and therefore will assume that our non-zero
|
||||
* count is accurate.
|
||||
* XXX: this is known faulty as the push rule for `.m.room.encrypted` may be disabled so server
|
||||
* may issue notification counts of 0 which we wrongly trust.
|
||||
* https://github.com/matrix-org/matrix-spec-proposals/pull/2654 would fix this
|
||||
*
|
||||
* @see import("./client").fixNotificationCountOnDecryption
|
||||
*/
|
||||
if (!encrypted || joinObj.unread_notifications.notification_count === 0) {
|
||||
// In an encrypted room, if the room has notifications enabled then it's typical for
|
||||
// the server to flag all new messages as notifying. However, some push rules calculate
|
||||
// events as ignored based on their event contents (e.g. ignoring msgtype=m.notice messages)
|
||||
// so we want to calculate this figure on the client in all cases.
|
||||
room.setUnreadNotificationCount(
|
||||
NotificationCountType.Total,
|
||||
unreadNotification.notification_count ?? 0,
|
||||
joinObj.unread_notifications.notification_count ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
const hasNoNotifications =
|
||||
room.getThreadUnreadNotificationCount(threadId, NotificationCountType.Highlight) <= 0;
|
||||
if (!encrypted || (encrypted && hasNoNotifications)) {
|
||||
room.setThreadUnreadNotificationCount(
|
||||
threadId,
|
||||
if (!encrypted || room.getUnreadNotificationCount(NotificationCountType.Highlight) <= 0) {
|
||||
// If the locally stored highlight count is zero, use the server provided value.
|
||||
room.setUnreadNotificationCount(
|
||||
NotificationCountType.Highlight,
|
||||
unreadNotification.highlight_count ?? 0,
|
||||
joinObj.unread_notifications.highlight_count ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
room.resetThreadUnreadNotificationCountFromSync();
|
||||
}
|
||||
|
||||
joinObj.timeline = joinObj.timeline || ({} as ITimeline);
|
||||
const unreadThreadNotifications =
|
||||
joinObj[UNREAD_THREAD_NOTIFICATIONS.name] ?? joinObj[UNREAD_THREAD_NOTIFICATIONS.altName!];
|
||||
if (unreadThreadNotifications) {
|
||||
// This mirrors the logic above for rooms: take the *total* notification count from
|
||||
// the server for unencrypted rooms or is it's zero. Any threads not present in this
|
||||
// object implicitly have zero notifications, so start by clearing the total counts
|
||||
// for all such threads.
|
||||
room.resetThreadUnreadNotificationCountFromSync(Object.keys(unreadThreadNotifications));
|
||||
for (const [threadId, unreadNotification] of Object.entries(unreadThreadNotifications)) {
|
||||
if (!encrypted || unreadNotification.notification_count === 0) {
|
||||
room.setThreadUnreadNotificationCount(
|
||||
threadId,
|
||||
NotificationCountType.Total,
|
||||
unreadNotification.notification_count ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
if (joinObj.isBrandNewRoom) {
|
||||
// set the back-pagination token. Do this *before* adding any
|
||||
// events so that clients can start back-paginating.
|
||||
if (joinObj.timeline.prev_batch !== null) {
|
||||
room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
|
||||
}
|
||||
} else if (joinObj.timeline.limited) {
|
||||
let limited = true;
|
||||
|
||||
// we've got a limited sync, so we *probably* have a gap in the
|
||||
// timeline, so should reset. But we might have been peeking or
|
||||
// paginating and already have some of the events, in which
|
||||
// case we just want to append any subsequent events to the end
|
||||
// of the existing timeline.
|
||||
//
|
||||
// This is particularly important in the case that we already have
|
||||
// *all* of the events in the timeline - in that case, if we reset
|
||||
// the timeline, we'll end up with an entirely empty timeline,
|
||||
// which we'll try to paginate but not get any new events (which
|
||||
// will stop us linking the empty timeline into the chain).
|
||||
//
|
||||
for (let i = timelineEvents.length - 1; i >= 0; i--) {
|
||||
const eventId = timelineEvents[i].getId()!;
|
||||
if (room.getTimelineForEvent(eventId)) {
|
||||
this.syncOpts.logger.debug(`Already have event ${eventId} in limited sync - not resetting`);
|
||||
limited = false;
|
||||
|
||||
// we might still be missing some of the events before i;
|
||||
// we don't want to be adding them to the end of the
|
||||
// timeline because that would put them out of order.
|
||||
timelineEvents.splice(0, i);
|
||||
|
||||
// XXX: there's a problem here if the skipped part of the
|
||||
// timeline modifies the state set in stateEvents, because
|
||||
// we'll end up using the state from stateEvents rather
|
||||
// than the later state from timelineEvents. We probably
|
||||
// need to wind stateEvents forward over the events we're
|
||||
// skipping.
|
||||
|
||||
break;
|
||||
const hasNoNotifications =
|
||||
room.getThreadUnreadNotificationCount(threadId, NotificationCountType.Highlight) <= 0;
|
||||
if (!encrypted || (encrypted && hasNoNotifications)) {
|
||||
room.setThreadUnreadNotificationCount(
|
||||
threadId,
|
||||
NotificationCountType.Highlight,
|
||||
unreadNotification.highlight_count ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (limited) {
|
||||
room.resetLiveTimeline(
|
||||
joinObj.timeline.prev_batch,
|
||||
this.syncOpts.canResetEntireTimeline!(room.roomId)
|
||||
? null
|
||||
: (syncEventData.oldSyncToken ?? null),
|
||||
);
|
||||
|
||||
// We have to assume any gap in any timeline is
|
||||
// reason to stop incrementally tracking notifications and
|
||||
// reset the timeline.
|
||||
client.resetNotifTimelineSet();
|
||||
}
|
||||
}
|
||||
|
||||
// process any crypto events *before* emitting the RoomStateEvent events. This
|
||||
// avoids a race condition if the application tries to send a message after the
|
||||
// state event is processed, but before crypto is enabled, which then causes the
|
||||
// crypto layer to complain.
|
||||
|
||||
if (this.syncOpts.cryptoCallbacks) {
|
||||
for (const e of eventsFormingFinalState) {
|
||||
if (e.isState() && e.getType() === EventType.RoomEncryption && e.getStateKey() === "") {
|
||||
await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Proactively decrypt state events: normally we decrypt on demand, but for state
|
||||
// events we need them immediately, so we handle them here. Specifically, consumers
|
||||
// (e.g. Element Web) expect state events to be unencrypted upon receipt.
|
||||
for (const ev of timelineEvents.filter((ev) => ev.isState())) {
|
||||
await this.client.decryptEventIfNeeded(ev);
|
||||
}
|
||||
|
||||
try {
|
||||
if ("org.matrix.msc4222.state_after" in joinObj) {
|
||||
await this.injectRoomEvents(
|
||||
room,
|
||||
undefined,
|
||||
stateAfterEvents,
|
||||
timelineEvents,
|
||||
syncEventData.fromCache,
|
||||
);
|
||||
} else {
|
||||
await this.injectRoomEvents(room, stateEvents, undefined, timelineEvents, syncEventData.fromCache);
|
||||
room.resetThreadUnreadNotificationCountFromSync();
|
||||
}
|
||||
} catch (e) {
|
||||
this.syncOpts.logger.error(`Failed to process events on room ${room.roomId}:`, e);
|
||||
}
|
||||
|
||||
// set summary after processing events,
|
||||
// because it will trigger a name calculation
|
||||
// which needs the room state to be up to date
|
||||
if (joinObj.summary) {
|
||||
room.setSummary(joinObj.summary);
|
||||
}
|
||||
joinObj.timeline = joinObj.timeline || ({} as ITimeline);
|
||||
|
||||
// we deliberately don't add ephemeral events to the timeline
|
||||
room.addEphemeralEvents(ephemeralEvents);
|
||||
if (joinObj.isBrandNewRoom) {
|
||||
// set the back-pagination token. Do this *before* adding any
|
||||
// events so that clients can start back-paginating.
|
||||
if (joinObj.timeline.prev_batch !== null) {
|
||||
room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
|
||||
}
|
||||
} else if (joinObj.timeline.limited) {
|
||||
let limited = true;
|
||||
|
||||
// we deliberately don't add accountData to the timeline
|
||||
room.addAccountData(accountDataEvents);
|
||||
// we've got a limited sync, so we *probably* have a gap in the
|
||||
// timeline, so should reset. But we might have been peeking or
|
||||
// paginating and already have some of the events, in which
|
||||
// case we just want to append any subsequent events to the end
|
||||
// of the existing timeline.
|
||||
//
|
||||
// This is particularly important in the case that we already have
|
||||
// *all* of the events in the timeline - in that case, if we reset
|
||||
// the timeline, we'll end up with an entirely empty timeline,
|
||||
// which we'll try to paginate but not get any new events (which
|
||||
// will stop us linking the empty timeline into the chain).
|
||||
//
|
||||
for (let i = timelineEvents.length - 1; i >= 0; i--) {
|
||||
const eventId = timelineEvents[i].getId()!;
|
||||
if (room.getTimelineForEvent(eventId)) {
|
||||
this.syncOpts.logger.debug(`Already have event ${eventId} in limited sync - not resetting`);
|
||||
limited = false;
|
||||
|
||||
// Sticky events primarily come via the `timeline` field, with the
|
||||
// sticky info field marking them as sticky.
|
||||
// If the sync is "gappy" (meaning it is skipping events to catch up) then
|
||||
// sticky events will instead come down the sticky section.
|
||||
// This ensures we collect sticky events from both places.
|
||||
const stickyEventsAndStickyEventsFromTheTimeline = stickyEvents.concat(
|
||||
timelineEvents.filter((e) => e.unstableStickyInfo !== undefined),
|
||||
);
|
||||
// Note: We calculate sticky events before emitting `.Room` as it's nice to have
|
||||
// sticky events calculated and ready to go.
|
||||
room._unstable_addStickyEvents(stickyEventsAndStickyEventsFromTheTimeline);
|
||||
// we might still be missing some of the events before i;
|
||||
// we don't want to be adding them to the end of the
|
||||
// timeline because that would put them out of order.
|
||||
timelineEvents.splice(0, i);
|
||||
|
||||
room.recalculate();
|
||||
if (joinObj.isBrandNewRoom) {
|
||||
client.store.storeRoom(room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
}
|
||||
// XXX: there's a problem here if the skipped part of the
|
||||
// timeline modifies the state set in stateEvents, because
|
||||
// we'll end up using the state from stateEvents rather
|
||||
// than the later state from timelineEvents. We probably
|
||||
// need to wind stateEvents forward over the events we're
|
||||
// skipping.
|
||||
|
||||
this.processEventsForNotifs(room, timelineEvents);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const emitEvent = (e: MatrixEvent): boolean => client.emit(ClientEvent.Event, e);
|
||||
// this fires a couple of times for some events. (eg state events are in the timeline and the state)
|
||||
// should this get a sync section as an additional event emission param (e, syncSection))?
|
||||
stateEvents.forEach(emitEvent);
|
||||
timelineEvents.forEach(emitEvent);
|
||||
ephemeralEvents.forEach(emitEvent);
|
||||
accountDataEvents.forEach(emitEvent);
|
||||
stickyEvents
|
||||
.filter(
|
||||
(stickyEvent) =>
|
||||
// This is highly unlikey, but in the case where a sticky event
|
||||
// has appeared in the timeline AND the sticky section, we only
|
||||
// want to emit the event once.
|
||||
!timelineEvents.some((timelineEvent) => timelineEvent.getId() === stickyEvent.getId()),
|
||||
)
|
||||
.forEach(emitEvent);
|
||||
// Decrypt only the last message in all rooms to make sure we can generate a preview
|
||||
// And decrypt all events after the recorded read receipt to ensure an accurate
|
||||
// notification count
|
||||
room.decryptCriticalEvents();
|
||||
});
|
||||
if (limited) {
|
||||
room.resetLiveTimeline(
|
||||
joinObj.timeline.prev_batch,
|
||||
this.syncOpts.canResetEntireTimeline!(room.roomId)
|
||||
? null
|
||||
: (syncEventData.oldSyncToken ?? null),
|
||||
);
|
||||
|
||||
// We have to assume any gap in any timeline is
|
||||
// reason to stop incrementally tracking notifications and
|
||||
// reset the timeline.
|
||||
client.resetNotifTimelineSet();
|
||||
}
|
||||
}
|
||||
|
||||
// process any crypto events *before* emitting the RoomStateEvent events. This
|
||||
// avoids a race condition if the application tries to send a message after the
|
||||
// state event is processed, but before crypto is enabled, which then causes the
|
||||
// crypto layer to complain.
|
||||
|
||||
if (this.syncOpts.cryptoCallbacks) {
|
||||
for (const e of eventsFormingFinalState) {
|
||||
if (e.isState() && e.getType() === EventType.RoomEncryption && e.getStateKey() === "") {
|
||||
await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Proactively decrypt state events: normally we decrypt on demand, but for state
|
||||
// events we need them immediately, so we handle them here. Specifically, consumers
|
||||
// (e.g. Element Web) expect state events to be unencrypted upon receipt.
|
||||
for (const ev of timelineEvents.filter((ev) => ev.isState())) {
|
||||
await this.client.decryptEventIfNeeded(ev);
|
||||
}
|
||||
|
||||
try {
|
||||
if ("org.matrix.msc4222.state_after" in joinObj) {
|
||||
await this.injectRoomEvents(
|
||||
room,
|
||||
undefined,
|
||||
stateAfterEvents,
|
||||
timelineEvents,
|
||||
syncEventData.fromCache,
|
||||
);
|
||||
} else {
|
||||
await this.injectRoomEvents(
|
||||
room,
|
||||
stateEvents,
|
||||
undefined,
|
||||
timelineEvents,
|
||||
syncEventData.fromCache,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.syncOpts.logger.error(`Failed to process events on room ${room.roomId}:`, e);
|
||||
}
|
||||
|
||||
// set summary after processing events,
|
||||
// because it will trigger a name calculation
|
||||
// which needs the room state to be up to date
|
||||
if (joinObj.summary) {
|
||||
room.setSummary(joinObj.summary);
|
||||
}
|
||||
|
||||
// we deliberately don't add ephemeral events to the timeline
|
||||
room.addEphemeralEvents(ephemeralEvents);
|
||||
|
||||
// we deliberately don't add accountData to the timeline
|
||||
room.addAccountData(accountDataEvents);
|
||||
|
||||
// Sticky events primarily come via the `timeline` field, with the
|
||||
// sticky info field marking them as sticky.
|
||||
// If the sync is "gappy" (meaning it is skipping events to catch up) then
|
||||
// sticky events will instead come down the sticky section.
|
||||
// This ensures we collect sticky events from both places.
|
||||
const stickyEventsAndStickyEventsFromTheTimeline = stickyEvents.concat(
|
||||
timelineEvents.filter((e) => e.unstableStickyInfo !== undefined),
|
||||
);
|
||||
// Note: We calculate sticky events before emitting `.Room` as it's nice to have
|
||||
// sticky events calculated and ready to go.
|
||||
room._unstable_addStickyEvents(stickyEventsAndStickyEventsFromTheTimeline);
|
||||
|
||||
room.recalculate();
|
||||
if (joinObj.isBrandNewRoom) {
|
||||
client.store.storeRoom(room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
}
|
||||
|
||||
this.processEventsForNotifs(room, timelineEvents);
|
||||
|
||||
const emitEvent = (e: MatrixEvent): boolean => client.emit(ClientEvent.Event, e);
|
||||
// this fires a couple of times for some events. (eg state events are in the timeline and the state)
|
||||
// should this get a sync section as an additional event emission param (e, syncSection))?
|
||||
stateEvents.forEach(emitEvent);
|
||||
timelineEvents.forEach(emitEvent);
|
||||
ephemeralEvents.forEach(emitEvent);
|
||||
accountDataEvents.forEach(emitEvent);
|
||||
stickyEvents
|
||||
.filter(
|
||||
(stickyEvent) =>
|
||||
// This is highly unlikey, but in the case where a sticky event
|
||||
// has appeared in the timeline AND the sticky section, we only
|
||||
// want to emit the event once.
|
||||
!timelineEvents.some((timelineEvent) => timelineEvent.getId() === stickyEvent.getId()),
|
||||
)
|
||||
.forEach(emitEvent);
|
||||
// Decrypt only the last message in all rooms to make sure we can generate a preview
|
||||
// And decrypt all events after the recorded read receipt to ensure an accurate
|
||||
// notification count
|
||||
room.decryptCriticalEvents();
|
||||
}),
|
||||
);
|
||||
|
||||
// Handle leaves (e.g. kicked rooms)
|
||||
await promiseMapSeries(leaveRooms, async (leaveObj) => {
|
||||
const room = leaveObj.room;
|
||||
const { timelineEvents, stateEvents, stateAfterEvents } = await this.mapAndInjectRoomEvents(leaveObj);
|
||||
const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data);
|
||||
await Promise.all(
|
||||
leaveRooms.map(async (leaveObj) => {
|
||||
const room = leaveObj.room;
|
||||
const { timelineEvents, stateEvents, stateAfterEvents } = await this.mapAndInjectRoomEvents(leaveObj);
|
||||
const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data);
|
||||
|
||||
room.addAccountData(accountDataEvents);
|
||||
room.addAccountData(accountDataEvents);
|
||||
|
||||
room.recalculate();
|
||||
if (leaveObj.isBrandNewRoom) {
|
||||
client.store.storeRoom(room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
}
|
||||
room.recalculate();
|
||||
if (leaveObj.isBrandNewRoom) {
|
||||
client.store.storeRoom(room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
}
|
||||
|
||||
this.processEventsForNotifs(room, timelineEvents);
|
||||
this.processEventsForNotifs(room, timelineEvents);
|
||||
|
||||
stateEvents?.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
stateAfterEvents?.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
timelineEvents.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
accountDataEvents.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
});
|
||||
stateEvents?.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
stateAfterEvents?.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
timelineEvents.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
accountDataEvents.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Handle knocks
|
||||
await promiseMapSeries(knockRooms, async (knockObj) => {
|
||||
const room = knockObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(knockObj.knock_state, room);
|
||||
await Promise.all(
|
||||
knockRooms.map(async (knockObj) => {
|
||||
const room = knockObj.room;
|
||||
const stateEvents = this.mapSyncEventsFormat(knockObj.knock_state, room);
|
||||
|
||||
await this.injectRoomEvents(room, stateEvents, undefined);
|
||||
await this.injectRoomEvents(room, stateEvents, undefined);
|
||||
|
||||
if (knockObj.isBrandNewRoom) {
|
||||
room.recalculate();
|
||||
client.store.storeRoom(room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
} else {
|
||||
// Update room state for knock->leave->knock cycles
|
||||
room.recalculate();
|
||||
}
|
||||
stateEvents.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
});
|
||||
if (knockObj.isBrandNewRoom) {
|
||||
room.recalculate();
|
||||
client.store.storeRoom(room);
|
||||
client.emit(ClientEvent.Room, room);
|
||||
} else {
|
||||
// Update room state for knock->leave->knock cycles
|
||||
room.recalculate();
|
||||
}
|
||||
stateEvents.forEach(function (e) {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// update the notification timeline, if appropriate.
|
||||
// we only do this for live events, as otherwise we can't order them sanely
|
||||
|
||||
+2
-2
@@ -27,7 +27,7 @@ import { type RoomMember } from "./models/room-member.ts";
|
||||
import { EventType } from "./@types/event.ts";
|
||||
import { type DecryptionFailureCode } from "./crypto-api/index.ts";
|
||||
import { DecryptionError, type EventDecryptionResult } from "./common-crypto/CryptoBackend.ts";
|
||||
import { type OidcClientConfig, type ValidatedAuthMetadata } from "./oidc/index.ts";
|
||||
import { OAuthGrantType, type OidcClientConfig, type ValidatedAuthMetadata } from "./oidc/index.ts";
|
||||
|
||||
/**
|
||||
* Create a {@link MatrixEvent}.
|
||||
@@ -226,6 +226,6 @@ export const mockOpenIdConfiguration = (
|
||||
device_authorization_endpoint: issuer + "device",
|
||||
jwks_uri: issuer + "jwks",
|
||||
response_types_supported: ["code"],
|
||||
grant_types_supported: ["authorization_code", "refresh_token", ...additionalGrantTypes],
|
||||
grant_types_supported: [OAuthGrantType.AuthorizationCode, OAuthGrantType.RefreshToken, ...additionalGrantTypes],
|
||||
code_challenge_methods_supported: ["S256"],
|
||||
});
|
||||
|
||||
+15
-1
@@ -23,7 +23,21 @@ limitations under the License.
|
||||
* versions; only that we should be able to provide a base level of functionality with a server that offers support for
|
||||
* any of the listed versions.
|
||||
*/
|
||||
export const SUPPORTED_MATRIX_VERSIONS = ["v1.1", "v1.2", "v1.3", "v1.4", "v1.5", "v1.6", "v1.7", "v1.8", "v1.9"];
|
||||
export const SUPPORTED_MATRIX_VERSIONS = [
|
||||
"v1.1",
|
||||
"v1.2",
|
||||
"v1.3",
|
||||
"v1.4",
|
||||
"v1.5",
|
||||
"v1.6",
|
||||
"v1.7",
|
||||
"v1.8",
|
||||
"v1.9",
|
||||
"v1.10",
|
||||
"v1.11",
|
||||
"v1.12",
|
||||
"v1.13",
|
||||
];
|
||||
|
||||
/**
|
||||
* The oldest Matrix specification version the js-sdk supports.
|
||||
|
||||
+15
-13
@@ -301,7 +301,7 @@ type CallEventType =
|
||||
| EventType.CallCandidates
|
||||
| EventType.CallHangup
|
||||
| EventType.CallReject
|
||||
| EventType.CallSDPStreamMetadataChangedPrefix;
|
||||
| EventType.CallSDPStreamMetadataChanged;
|
||||
|
||||
export interface VoipEvent {
|
||||
type: "toDevice" | "sendEvent";
|
||||
@@ -954,7 +954,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
);
|
||||
}
|
||||
|
||||
const sdpStreamMetadata = invite[SDPStreamMetadataKey];
|
||||
const sdpStreamMetadata = SDPStreamMetadataKey.findIn(invite);
|
||||
if (sdpStreamMetadata) {
|
||||
this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
|
||||
} else {
|
||||
@@ -1596,8 +1596,8 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
}
|
||||
|
||||
public async sendMetadataUpdate(): Promise<void> {
|
||||
await this.sendVoipEvent(EventType.CallSDPStreamMetadataChangedPrefix, {
|
||||
[SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(),
|
||||
await this.sendVoipEvent(EventType.CallSDPStreamMetadataChanged, {
|
||||
[SDPStreamMetadataKey.name]: this.getLocalSDPStreamMetadata(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1628,15 +1628,15 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
}
|
||||
|
||||
private async sendAnswer(): Promise<void> {
|
||||
const answerContent = {
|
||||
const answerContent: Omit<MCallAnswer, "version" | "call_id" | "party_id" | "conf_id"> = {
|
||||
answer: {
|
||||
sdp: this.peerConn!.localDescription!.sdp,
|
||||
// type is now deprecated as of Matrix VoIP v1, but
|
||||
// required to still be sent for backwards compat
|
||||
type: this.peerConn!.localDescription!.type,
|
||||
},
|
||||
[SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(true),
|
||||
} as MCallAnswer;
|
||||
[SDPStreamMetadataKey.name]: this.getLocalSDPStreamMetadata(true),
|
||||
};
|
||||
|
||||
answerContent.capabilities = {
|
||||
"m.call.transferee": this.client.supportsCallTransfer,
|
||||
@@ -1894,7 +1894,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
|
||||
this.state = CallState.Connecting;
|
||||
|
||||
const sdpStreamMetadata = content[SDPStreamMetadataKey];
|
||||
const sdpStreamMetadata = SDPStreamMetadataKey.findIn(content);
|
||||
if (sdpStreamMetadata) {
|
||||
this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
|
||||
} else {
|
||||
@@ -1986,7 +1986,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
|
||||
const prevLocalOnHold = this.isLocalOnHold();
|
||||
|
||||
const sdpStreamMetadata = content[SDPStreamMetadataKey];
|
||||
const sdpStreamMetadata = SDPStreamMetadataKey.findIn<SDPStreamMetadata>(content);
|
||||
if (sdpStreamMetadata) {
|
||||
this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
|
||||
} else {
|
||||
@@ -2019,7 +2019,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
this.sendVoipEvent(EventType.CallNegotiate, {
|
||||
lifetime: CALL_TIMEOUT_MS,
|
||||
description: this.peerConn!.localDescription?.toJSON() as RTCSessionDescription,
|
||||
[SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(true),
|
||||
[SDPStreamMetadataKey.name]: this.getLocalSDPStreamMetadata(true),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -2048,8 +2048,10 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
|
||||
public onSDPStreamMetadataChangedReceived(event: MatrixEvent): void {
|
||||
const content = event.getContent<MCallSDPStreamMetadataChanged>();
|
||||
const metadata = content[SDPStreamMetadataKey];
|
||||
this.updateRemoteSDPStreamMetadata(metadata);
|
||||
const metadata = SDPStreamMetadataKey.findIn<SDPStreamMetadata>(content);
|
||||
if (metadata) {
|
||||
this.updateRemoteSDPStreamMetadata(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
public async onAssertedIdentityReceived(event: MatrixEvent): Promise<void> {
|
||||
@@ -2156,7 +2158,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
"m.call.dtmf": false,
|
||||
};
|
||||
|
||||
content[SDPStreamMetadataKey] = this.getLocalSDPStreamMetadata(true);
|
||||
content[SDPStreamMetadataKey.name] = this.getLocalSDPStreamMetadata(true);
|
||||
|
||||
// Get rid of any candidates waiting to be sent: they'll be included in the local
|
||||
// description we just got and will send in the offer.
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import { type CallErrorCode } from "./call.ts";
|
||||
import { NamespacedValue } from "../NamespacedValue.ts";
|
||||
|
||||
// TODO: Change to "sdp_stream_metadata" when MSC3077 is merged
|
||||
export const SDPStreamMetadataKey = "org.matrix.msc3077.sdp_stream_metadata";
|
||||
export const SDPStreamMetadataKey = new NamespacedValue(
|
||||
"sdp_stream_metadata",
|
||||
"org.matrix.msc3077.sdp_stream_metadata",
|
||||
);
|
||||
|
||||
export enum SDPStreamMetadataPurpose {
|
||||
Usermedia = "m.usermedia",
|
||||
@@ -41,10 +44,13 @@ export interface MCallBase {
|
||||
dest_session_id?: string;
|
||||
}
|
||||
|
||||
type Description = Pick<RTCSessionDescription, "type" | "sdp">;
|
||||
|
||||
export interface MCallAnswer extends MCallBase {
|
||||
answer: RTCSessionDescription;
|
||||
capabilities?: CallCapabilities;
|
||||
[SDPStreamMetadataKey]: SDPStreamMetadata;
|
||||
"answer": Description;
|
||||
"capabilities"?: CallCapabilities;
|
||||
"sdp_stream_metadata"?: SDPStreamMetadata;
|
||||
"org.matrix.msc3077.sdp_stream_metadata"?: SDPStreamMetadata;
|
||||
}
|
||||
|
||||
export interface MCallSelectAnswer extends MCallBase {
|
||||
@@ -52,18 +58,20 @@ export interface MCallSelectAnswer extends MCallBase {
|
||||
}
|
||||
|
||||
export interface MCallInviteNegotiate extends MCallBase {
|
||||
offer: RTCSessionDescription;
|
||||
description: RTCSessionDescription;
|
||||
lifetime: number;
|
||||
capabilities?: CallCapabilities;
|
||||
invitee?: string;
|
||||
sender_session_id?: string;
|
||||
dest_session_id?: string;
|
||||
[SDPStreamMetadataKey]: SDPStreamMetadata;
|
||||
"offer": Description;
|
||||
"description": Description;
|
||||
"lifetime": number;
|
||||
"capabilities"?: CallCapabilities;
|
||||
"invitee"?: string;
|
||||
"sender_session_id"?: string;
|
||||
"dest_session_id"?: string;
|
||||
"sdp_stream_metadata"?: SDPStreamMetadata;
|
||||
"org.matrix.msc3077.sdp_stream_metadata"?: SDPStreamMetadata;
|
||||
}
|
||||
|
||||
export interface MCallSDPStreamMetadataChanged extends MCallBase {
|
||||
[SDPStreamMetadataKey]: SDPStreamMetadata;
|
||||
"sdp_stream_metadata"?: SDPStreamMetadata;
|
||||
"org.matrix.msc3077.sdp_stream_metadata"?: SDPStreamMetadata;
|
||||
}
|
||||
|
||||
export interface MCallReplacesEvent extends MCallBase {
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"strict": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"lib": ["es2024", "dom", "dom.iterable"],
|
||||
"baseUrl": ".",
|
||||
"types": ["vitest/globals", "node"],
|
||||
"paths": {
|
||||
"#types/*": ["./node_modules/vite/types/*"]
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/en-us/json-schemas/tsdoc/v0/tsdoc.schema.json",
|
||||
"extends": ["typedoc/tsdoc.json"],
|
||||
"noStandardTags": false,
|
||||
"tagDefinitions": [
|
||||
{
|
||||
"tagName": "@alias",
|
||||
"syntaxKind": "modifier"
|
||||
}
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@ const slowTestReporter: Reporter = {
|
||||
onTestRunEnd(testModules, unhandledErrors, reason) {
|
||||
const tests = testModules
|
||||
.flatMap((m) => Array.from(m.children.allTests()))
|
||||
.filter((test) => test.diagnostic().slow);
|
||||
.filter((test) => test.diagnostic()?.slow);
|
||||
tests.sort((x, y) => x.diagnostic()?.duration! - y.diagnostic()?.duration!);
|
||||
tests.reverse();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user