Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef97df8ed0 | |||
| 0ce751c462 | |||
| 2706873948 | |||
| 4a9006aea6 | |||
| 43c72d5bf5 | |||
| e551b92a07 | |||
| 32f51e852b | |||
| 82aa04d894 | |||
| ff89c9ec42 | |||
| ccd825fb39 | |||
| b313eb5912 | |||
| 2b12675675 | |||
| 246788b874 | |||
| b0b80401aa | |||
| 6a0164f37f | |||
| f963d61bcb | |||
| b32619ad24 | |||
| 3d3c3ba55f | |||
| d62c658a72 | |||
| bdc4a69023 | |||
| ab892420b5 | |||
| ed607c48b0 | |||
| a8d75b81e5 | |||
| 2f1d654f14 | |||
| c4c7f94514 | |||
| 1fac06e223 | |||
| 77c118084b | |||
| 097bfe451a | |||
| b80d0091d2 | |||
| 3a33c658bb | |||
| 81e42b9531 | |||
| 7f7ecd060d | |||
| 6126ee125a | |||
| 78f718ff82 | |||
| c1c1be0c5d | |||
| 1952eaa1ff | |||
| 8851f8b07c | |||
| 15eafe34b3 | |||
| f0d48236fa | |||
| dde9d48726 | |||
| 6d046edcb2 | |||
| 2abf7ca795 | |||
| 2b46579bd8 | |||
| 6d42ed338e | |||
| ef080c25f9 | |||
| c8d7b458b2 | |||
| f1ba8a8775 | |||
| d21adf568a | |||
| dea184e9ec | |||
| e119bf9040 | |||
| c7f982e190 | |||
| 2e2dd628c1 | |||
| 5ac5a8a799 | |||
| 7d75ab417a | |||
| 3ca81e409a |
@@ -22,7 +22,7 @@ runs:
|
||||
|
||||
- name: Upload tarball signature
|
||||
if: ${{ inputs.upload-url }}
|
||||
uses: shogo82148/actions-upload-release-asset@610b1987249a69a79de9565777e112fb38f22436 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@e6cd4579c50a5d0eb50ca05d0d597daafca8cebb # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ env.VERSION }}.tar.gz.asc
|
||||
|
||||
@@ -29,13 +29,13 @@ runs:
|
||||
|
||||
- name: Upload asset signatures
|
||||
if: inputs.gpg-fingerprint
|
||||
uses: shogo82148/actions-upload-release-asset@610b1987249a69a79de9565777e112fb38f22436 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@e6cd4579c50a5d0eb50ca05d0d597daafca8cebb # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}.asc
|
||||
|
||||
- name: Upload assets
|
||||
uses: shogo82148/actions-upload-release-asset@610b1987249a69a79de9565777e112fb38f22436 # v1
|
||||
uses: shogo82148/actions-upload-release-asset@e6cd4579c50a5d0eb50ca05d0d597daafca8cebb # v1
|
||||
with:
|
||||
upload_url: ${{ inputs.upload-url }}
|
||||
asset_path: ${{ inputs.asset-path }}
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
deployments: write
|
||||
steps:
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
ref: staging
|
||||
fetch-depth: 0
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
disable-autolabeler: true
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
|
||||
@@ -18,14 +18,14 @@ jobs:
|
||||
merge:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
draft: true
|
||||
latest: true
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
ref: staging
|
||||
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get actions scripts
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
repository: matrix-org/matrix-js-sdk
|
||||
persist-credentials: false
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
id: ${{ steps.npm-publish.outputs.id }}
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
ref: staging
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
repo:
|
||||
- element-hq/element-web
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
repository: ${{ matrix.repo }}
|
||||
ref: staging
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
run: yarn gendoc
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
|
||||
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
|
||||
with:
|
||||
path: _docs
|
||||
|
||||
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: "🧮 Checkout code"
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
if: ${{ !inputs.sharded }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
name: coverage
|
||||
path: coverage
|
||||
- name: 📥 Download sharded artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||
if: inputs.sharded
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
name: "Node.js example"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
name: "Workflow Lint"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
if: github.event_name == 'merge_group'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
repository: element-hq/element-web
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
node: ["lts/*", 22]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- name: Setup Node
|
||||
id: setupNode
|
||||
|
||||
@@ -1,3 +1,79 @@
|
||||
Changes in [38.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.3.0) (2025-09-23)
|
||||
==================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Remove knocked room when membership changes to join ([#4977](https://github.com/matrix-org/matrix-js-sdk/pull/4977)). Contributed by @svajunas-budrys.
|
||||
|
||||
|
||||
Changes in [38.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.2.0) (2025-09-16)
|
||||
==================================================================================================
|
||||
Fix [CVE-2025-59160](https://www.cve.org/CVERecord?id=CVE-2025-59160) / [GHSA-mp7c-m3rh-r56v](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-mp7c-m3rh-r56v)
|
||||
|
||||
|
||||
|
||||
Changes in [38.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.1.0) (2025-09-09)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Remove custom `org.matrix.msc4075.rtc.notification.parent` relation type ([#4979](https://github.com/matrix-org/matrix-js-sdk/pull/4979)). Contributed by @toger5.
|
||||
* MatrixRTC: Add RTC decline event ([#4978](https://github.com/matrix-org/matrix-js-sdk/pull/4978)). Contributed by @toger5.
|
||||
* Make a MatrixRTCSession emit once the RTCNotification is sent ([#4976](https://github.com/matrix-org/matrix-js-sdk/pull/4976)). Contributed by @toger5.
|
||||
* Use hydra semantics for unknown room versions ([#4957](https://github.com/matrix-org/matrix-js-sdk/pull/4957)). Contributed by @dbkr.
|
||||
* Expose the StatusChanged event through the RTCSession ([#4974](https://github.com/matrix-org/matrix-js-sdk/pull/4974)). Contributed by @toger5.
|
||||
* Add `probablyLeft` event to the MatrixRTCSession ([#4962](https://github.com/matrix-org/matrix-js-sdk/pull/4962)). Contributed by @toger5.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix `m.topic` format ([#4984](https://github.com/matrix-org/matrix-js-sdk/pull/4984)). Contributed by @tulir.
|
||||
* Fix stable-suffixed MSC4133 support ([#4983](https://github.com/matrix-org/matrix-js-sdk/pull/4983)). Contributed by @dbkr.
|
||||
* Fix thread edit aggregation race condition ([#4980](https://github.com/matrix-org/matrix-js-sdk/pull/4980)). Contributed by @basnijholt.
|
||||
|
||||
|
||||
Changes in [38.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.0.0) (2025-08-27)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* Release tranche of breaking changes ([#4975](https://github.com/matrix-org/matrix-js-sdk/pull/4975)).
|
||||
* Remove support for FetchHttpApi `onlyData = false`
|
||||
* Remove deprecated `IJoinRoomOpts.syncRoom`
|
||||
* Remove deprecated methods which are unsupported in rust crypto
|
||||
* Remove deprecated getAuthIssuer method
|
||||
* Remove deprecated beginKeyVerification method
|
||||
* Remove deprecated isEncryptedDisabledForUnverifiedDevices getter
|
||||
* Remove deprecated UndecryptableToDeviceEvent MatrixClient emit
|
||||
* Remove deprecated defer utility method
|
||||
* Remove deprecated UIAResponse dummy type
|
||||
* Remove deprecated MatrixRTCSession MembershipConfig fields
|
||||
* Remove deprecated findVerificationRequestDMInProgress and storeSessionBackupPrivateKey methods in favour of overloads
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Allow multiple rtc sessions per room (with different sessionDescriptions) ([#4945](https://github.com/matrix-org/matrix-js-sdk/pull/4945)). Contributed by @toger5.
|
||||
* Add support for login\_hint in authorization url generation ([#4943](https://github.com/matrix-org/matrix-js-sdk/pull/4943)). Contributed by @odelcroi.
|
||||
* Only process MatrixRTC sessions associated with calls for `callMembershipsForRoom` ([#4960](https://github.com/matrix-org/matrix-js-sdk/pull/4960)). Contributed by @fkwp.
|
||||
|
||||
|
||||
Changes in [37.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.13.0) (2025-08-11)
|
||||
====================================================================================================
|
||||
This release supports new v12 Matrix rooms and consequently has a breaking change, removing powerLevelNorm from the RoomMember object as this can't be supported with infinite power levels. Apps should use the non-normalised `powerLevel` instead.
|
||||
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* [Backport staging] Support for creator power level ([#4954](https://github.com/matrix-org/matrix-js-sdk/pull/4954)). Contributed by @RiotRobot.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* [Backport staging] Support v12 rooms in maySendEvent ([#4956](https://github.com/matrix-org/matrix-js-sdk/pull/4956)). Contributed by @RiotRobot.
|
||||
* [Backport staging] Support for creator power level ([#4954](https://github.com/matrix-org/matrix-js-sdk/pull/4954)). Contributed by @RiotRobot.
|
||||
* Experimental support for sharing encrypted history on invite ([#4920](https://github.com/matrix-org/matrix-js-sdk/pull/4920)). Contributed by @richvdh.
|
||||
* Use the logger associated with MatrixClient in rust sdk ([#4918](https://github.com/matrix-org/matrix-js-sdk/pull/4918)). Contributed by @richvdh.
|
||||
* Update to matrix-sdk-crypto-wasm 15.1.0, and add new `ShieldStateCode.MismatchedSender` ([#4916](https://github.com/matrix-org/matrix-js-sdk/pull/4916)). Contributed by @richvdh.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix unknown/broken state in the RTC Membership Manager causing unnecassary error logging. ([#4944](https://github.com/matrix-org/matrix-js-sdk/pull/4944)). Contributed by @toger5.
|
||||
|
||||
|
||||
Changes in [37.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.12.0) (2025-07-29)
|
||||
====================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "37.13.0-rc.0",
|
||||
"version": "38.3.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
@@ -49,7 +49,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^15.1.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^15.2.0",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^6.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
|
||||
@@ -1491,8 +1491,10 @@ describe("crypto", () => {
|
||||
|
||||
expect(ev.decryptionFailureReason).toEqual(expectedErrorCode);
|
||||
|
||||
// `isEncryptedDisabledForUnverifiedDevices` should be true for `m.unverified` and false for other errors.
|
||||
expect(ev.isEncryptedDisabledForUnverifiedDevices).toEqual(withheldCode === "m.unverified");
|
||||
// `decryptionFailureReason` should be `MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE` for `m.unverified`
|
||||
expect(
|
||||
ev.decryptionFailureReason === DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
|
||||
).toEqual(withheldCode === "m.unverified");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -183,39 +183,43 @@ describe("Topic content helpers", () => {
|
||||
it("creates fully defined event content without html", () => {
|
||||
expect(makeTopicContent("pizza")).toEqual({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
],
|
||||
[M_TOPIC.name]: {
|
||||
"m.text": [
|
||||
{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("creates fully defined event content with html", () => {
|
||||
expect(makeTopicContent("pizza", "<b>pizza</b>")).toEqual({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
},
|
||||
{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
],
|
||||
[M_TOPIC.name]: {
|
||||
"m.text": [
|
||||
{
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
},
|
||||
{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("creates an empty event when the topic is falsey", () => {
|
||||
expect(makeTopicContent(undefined)).toEqual({
|
||||
topic: undefined,
|
||||
[M_TOPIC.name]: [],
|
||||
[M_TOPIC.name]: { "m.text": [] },
|
||||
});
|
||||
expect(makeTopicContent(null)).toEqual({
|
||||
topic: null,
|
||||
[M_TOPIC.name]: [],
|
||||
[M_TOPIC.name]: { "m.text": [] },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -225,11 +229,13 @@ describe("Topic content helpers", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "pizza",
|
||||
},
|
||||
],
|
||||
[M_TOPIC.name]: {
|
||||
"m.text": [
|
||||
{
|
||||
body: "pizza",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
@@ -240,12 +246,14 @@ describe("Topic content helpers", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
],
|
||||
[M_TOPIC.name]: {
|
||||
"m.text": [
|
||||
{
|
||||
body: "pizza",
|
||||
mimetype: "text/plain",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
@@ -256,12 +264,14 @@ describe("Topic content helpers", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
topic: "pizza",
|
||||
[M_TOPIC.name]: [
|
||||
{
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
},
|
||||
],
|
||||
[M_TOPIC.name]: {
|
||||
"m.text": [
|
||||
{
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
@@ -279,11 +289,34 @@ describe("Topic content helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses legacy event content when new topic key is invalid", () => {
|
||||
// TODO delete this test and re-enable the next one after support for the invalid form is removed
|
||||
// https://github.com/matrix-org/matrix-js-sdk/pull/4984#pullrequestreview-3174251065
|
||||
it("parses malformed event content with html topic", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
"topic": "pizza",
|
||||
"m.topic": {} as any,
|
||||
"m.topic": [
|
||||
{
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
},
|
||||
] as any,
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
html: "<b>pizza</b>",
|
||||
});
|
||||
});
|
||||
it.skip("uses legacy event content when new topic key is invalid", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
"topic": "pizza",
|
||||
"m.topic": [
|
||||
{
|
||||
body: "<b>pizza</b>",
|
||||
mimetype: "text/html",
|
||||
},
|
||||
] as any,
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
|
||||
@@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { type Mocked } from "jest-mock";
|
||||
|
||||
import type { Mocked, MockedFunction } from "jest-mock";
|
||||
import { FetchHttpApi } from "../../../src/http-api/fetch";
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
import {
|
||||
@@ -43,8 +42,8 @@ describe("FetchHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should support aborting multiple times", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
|
||||
api.request(Method.Get, "/foo");
|
||||
api.request(Method.Get, "/baz");
|
||||
@@ -68,13 +67,13 @@ describe("FetchHttpApi", () => {
|
||||
it("should fall back to global fetch if fetchFn not provided", () => {
|
||||
globalThis.fetch = jest.fn();
|
||||
expect(globalThis.fetch).not.toHaveBeenCalled();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
api.fetch("test");
|
||||
expect(globalThis.fetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should update identity server base url", () => {
|
||||
const api = new FetchHttpApi<IHttpOpts>(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new FetchHttpApi<IHttpOpts>(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
expect(api.opts.idBaseUrl).toBeUndefined();
|
||||
api.setIdBaseUrl("https://id.foo.bar");
|
||||
expect(api.opts.idBaseUrl).toBe("https://id.foo.bar");
|
||||
@@ -82,23 +81,35 @@ describe("FetchHttpApi", () => {
|
||||
|
||||
describe("idServerRequest", () => {
|
||||
it("should throw if no idBaseUrl", () => {
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
expect(() => api.idServerRequest(Method.Get, "/test", {}, IdentityPrefix.V2)).toThrow(
|
||||
"No identity server base URL set",
|
||||
);
|
||||
});
|
||||
|
||||
it("should send params as query string for GET requests", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
idBaseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
onlyData: true,
|
||||
});
|
||||
api.idServerRequest(Method.Get, "/test", { foo: "bar", via: ["a", "b"] }, IdentityPrefix.V2);
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).toBe("bar");
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.getAll("via")).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("should send params as body for non-GET requests", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
idBaseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
onlyData: true,
|
||||
});
|
||||
const params = { foo: "bar", via: ["a", "b"] };
|
||||
api.idServerRequest(Method.Post, "/test", params, IdentityPrefix.V2);
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("foo")).not.toBe("bar");
|
||||
@@ -106,18 +117,27 @@ describe("FetchHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should add Authorization header if token provided", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, idBaseUrl, prefix, fetchFn });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
idBaseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
onlyData: true,
|
||||
});
|
||||
api.idServerRequest(Method.Post, "/test", {}, IdentityPrefix.V2, "token");
|
||||
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBe("Bearer token");
|
||||
});
|
||||
});
|
||||
|
||||
it("should return the Response object if onlyData=false", async () => {
|
||||
const res = { ok: true };
|
||||
const fetchFn = jest.fn().mockResolvedValue(res);
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: false });
|
||||
await expect(api.requestOtherUrl(Method.Get, "http://url")).resolves.toBe(res);
|
||||
it("should complain if constructed without `onlyData: true`", async () => {
|
||||
expect(
|
||||
() =>
|
||||
new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
}),
|
||||
).toThrow("Constructing FetchHttpApi without `onlyData=true` is no longer supported.");
|
||||
});
|
||||
|
||||
it("should set an Accept header, and parse the response as JSON, by default", async () => {
|
||||
@@ -165,37 +185,40 @@ describe("FetchHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should send token via query params if useAuthorizationHeader=false", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: false,
|
||||
onlyData: true,
|
||||
});
|
||||
await api.authedRequest(Method.Get, "/path");
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("token");
|
||||
});
|
||||
|
||||
it("should send token via headers by default", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
onlyData: true,
|
||||
});
|
||||
await api.authedRequest(Method.Get, "/path");
|
||||
expect(fetchFn.mock.calls[0][1].headers["Authorization"]).toBe("Bearer token");
|
||||
});
|
||||
|
||||
it("should not send a token if not calling `authedRequest`", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
onlyData: true,
|
||||
});
|
||||
api.request(Method.Get, "/path");
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
|
||||
@@ -203,13 +226,14 @@ describe("FetchHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should ensure no token is leaked out via query params if sending via headers", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: true,
|
||||
onlyData: true,
|
||||
});
|
||||
await api.authedRequest(Method.Get, "/path", { access_token: "123" });
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBeFalsy();
|
||||
@@ -217,26 +241,28 @@ describe("FetchHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should not override manually specified access token via query params", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: false,
|
||||
onlyData: true,
|
||||
});
|
||||
await api.authedRequest(Method.Get, "/path", { access_token: "RealToken" });
|
||||
expect(fetchFn.mock.calls[0][0].searchParams.get("access_token")).toBe("RealToken");
|
||||
});
|
||||
|
||||
it("should not override manually specified access token via header", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: true,
|
||||
onlyData: true,
|
||||
});
|
||||
await api.authedRequest(Method.Get, "/path", undefined, undefined, {
|
||||
headers: { Authorization: "Bearer RealToken" },
|
||||
@@ -245,8 +271,8 @@ describe("FetchHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should not override Accept header", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
await api.authedRequest(Method.Get, "/path", undefined, undefined, {
|
||||
headers: { Accept: "text/html" },
|
||||
});
|
||||
@@ -269,7 +295,7 @@ describe("FetchHttpApi", () => {
|
||||
),
|
||||
});
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn });
|
||||
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
|
||||
await Promise.all([
|
||||
emitPromise(emitter, HttpApiEvent.NoConsent),
|
||||
@@ -279,9 +305,9 @@ describe("FetchHttpApi", () => {
|
||||
|
||||
describe("authedRequest", () => {
|
||||
it("should not include token if unset", async () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const fetchFn = makeMockFetchFn();
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn });
|
||||
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
await api.authedRequest(Method.Post, "/account/password");
|
||||
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBeUndefined();
|
||||
});
|
||||
@@ -310,6 +336,7 @@ describe("FetchHttpApi", () => {
|
||||
const okayResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: jest.fn().mockResolvedValue({ x: 1 }),
|
||||
};
|
||||
|
||||
describe("without a tokenRefreshFunction", () => {
|
||||
@@ -317,7 +344,14 @@ describe("FetchHttpApi", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
jest.spyOn(emitter, "emit");
|
||||
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn, accessToken, refreshToken });
|
||||
const api = new FetchHttpApi(emitter, {
|
||||
baseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||
unknownTokenErr,
|
||||
);
|
||||
@@ -339,6 +373,7 @@ describe("FetchHttpApi", () => {
|
||||
tokenRefreshFunction,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||
unknownTokenErr,
|
||||
@@ -360,6 +395,7 @@ describe("FetchHttpApi", () => {
|
||||
tokenRefreshFunction,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||
unknownTokenErr,
|
||||
@@ -388,11 +424,12 @@ describe("FetchHttpApi", () => {
|
||||
tokenRefreshFunction,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
onlyData: true,
|
||||
});
|
||||
const result = await api.authedRequest(Method.Post, "/account/password", undefined, undefined, {
|
||||
headers: {},
|
||||
});
|
||||
expect(result).toEqual(okayResponse);
|
||||
expect(result).toEqual({ x: 1 });
|
||||
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledTimes(2);
|
||||
@@ -431,6 +468,7 @@ describe("FetchHttpApi", () => {
|
||||
tokenRefreshFunction,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||
unknownTokenErr,
|
||||
@@ -486,6 +524,7 @@ describe("FetchHttpApi", () => {
|
||||
tokenRefreshFunction,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
onlyData: true,
|
||||
});
|
||||
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
|
||||
unknownTokenErr,
|
||||
@@ -506,7 +545,7 @@ describe("FetchHttpApi", () => {
|
||||
const makeApi = (thisBaseUrl = baseUrl): FetchHttpApi<any> => {
|
||||
const fetchFn = jest.fn();
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
return new FetchHttpApi(emitter, { baseUrl: thisBaseUrl, prefix, fetchFn });
|
||||
return new FetchHttpApi(emitter, { baseUrl: thisBaseUrl, prefix, fetchFn, onlyData: true });
|
||||
};
|
||||
|
||||
type TestParams = {
|
||||
@@ -559,7 +598,13 @@ describe("FetchHttpApi", () => {
|
||||
const makeApiWithExtraParams = (extraParams: QueryDict): FetchHttpApi<any> => {
|
||||
const fetchFn = jest.fn();
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
return new FetchHttpApi(emitter, { baseUrl: localBaseUrl, prefix, fetchFn, extraParams });
|
||||
return new FetchHttpApi(emitter, {
|
||||
baseUrl: localBaseUrl,
|
||||
prefix,
|
||||
fetchFn,
|
||||
onlyData: true,
|
||||
extraParams,
|
||||
});
|
||||
};
|
||||
|
||||
const userId = "@rsb-tbg:localhost";
|
||||
@@ -612,7 +657,7 @@ describe("FetchHttpApi", () => {
|
||||
it("should work when extraParams is undefined", () => {
|
||||
const fetchFn = jest.fn();
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const api = new FetchHttpApi(emitter, { baseUrl: localBaseUrl, prefix, fetchFn });
|
||||
const api = new FetchHttpApi(emitter, { baseUrl: localBaseUrl, prefix, fetchFn, onlyData: true });
|
||||
|
||||
const queryParams = { userId: "123" };
|
||||
const result = api.getUrl("/test", queryParams);
|
||||
@@ -645,10 +690,11 @@ describe("FetchHttpApi", () => {
|
||||
prefix,
|
||||
fetchFn,
|
||||
logger: mockLogger,
|
||||
onlyData: true,
|
||||
});
|
||||
const prom = api.requestOtherUrl(Method.Get, "https://server:8448/some/path?query=param#fragment");
|
||||
jest.advanceTimersByTime(1234);
|
||||
responseResolvers.resolve({ ok: true, status: 200, text: () => Promise.resolve("RESPONSE") } as Response);
|
||||
responseResolvers.resolve({ ok: true, status: 200, json: () => Promise.resolve("RESPONSE") } as Response);
|
||||
await prom;
|
||||
expect(mockLogger.debug).not.toHaveBeenCalledWith("fragment");
|
||||
expect(mockLogger.debug).not.toHaveBeenCalledWith("query");
|
||||
@@ -691,6 +737,7 @@ describe("FetchHttpApi", () => {
|
||||
tokenRefreshFunction,
|
||||
accessToken: "ACCESS_TOKEN",
|
||||
refreshToken: "REFRESH_TOKEN",
|
||||
onlyData: true,
|
||||
});
|
||||
|
||||
const prom1 = api.authedRequest(Method.Get, "/path1");
|
||||
@@ -746,6 +793,7 @@ describe("FetchHttpApi", () => {
|
||||
tokenRefreshFunction,
|
||||
accessToken: "ACCESS_TOKEN",
|
||||
refreshToken: "REFRESH_TOKEN",
|
||||
onlyData: true,
|
||||
});
|
||||
|
||||
const prom1 = api.authedRequest(Method.Get, "/path1");
|
||||
@@ -783,3 +831,7 @@ describe("FetchHttpApi", () => {
|
||||
expect(api.opts.refreshToken).toBe("NEW_REFRESH_TOKEN");
|
||||
});
|
||||
});
|
||||
|
||||
function makeMockFetchFn(): MockedFunction<any> {
|
||||
return jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ describe("MatrixHttpApi", () => {
|
||||
it("should fall back to `fetch` where xhr is unavailable", async () => {
|
||||
globalThis.XMLHttpRequest = undefined!;
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
await upload;
|
||||
expect(fetchFn).toHaveBeenCalled();
|
||||
@@ -70,7 +70,7 @@ describe("MatrixHttpApi", () => {
|
||||
|
||||
it("should prefer xhr where available", () => {
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(fetchFn).not.toHaveBeenCalled();
|
||||
expect(xhr.open).toHaveBeenCalled();
|
||||
@@ -82,6 +82,7 @@ describe("MatrixHttpApi", () => {
|
||||
prefix,
|
||||
accessToken: "token",
|
||||
useAuthorizationHeader: false,
|
||||
onlyData: true,
|
||||
});
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(xhr.open).toHaveBeenCalledWith(
|
||||
@@ -96,6 +97,7 @@ describe("MatrixHttpApi", () => {
|
||||
baseUrl,
|
||||
prefix,
|
||||
accessToken: "token",
|
||||
onlyData: true,
|
||||
});
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload");
|
||||
@@ -103,7 +105,7 @@ describe("MatrixHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should include filename by default", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File, { name: "name" });
|
||||
expect(xhr.open).toHaveBeenCalledWith(
|
||||
Method.Post,
|
||||
@@ -112,13 +114,13 @@ describe("MatrixHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should allow not sending the filename", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File, { name: "name", includeFilename: false });
|
||||
expect(xhr.open).toHaveBeenCalledWith(Method.Post, baseUrl.toLowerCase() + "/_matrix/media/v3/upload");
|
||||
});
|
||||
|
||||
it("should abort xhr when the upload is aborted", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
api.cancelUpload(upload);
|
||||
expect(xhr.abort).toHaveBeenCalled();
|
||||
@@ -126,7 +128,7 @@ describe("MatrixHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should timeout if no progress in 30s", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
jest.advanceTimersByTime(25000);
|
||||
// @ts-ignore
|
||||
@@ -138,7 +140,7 @@ describe("MatrixHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should call progressHandler", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
const progressHandler = jest.fn();
|
||||
upload = api.uploadContent({} as File, { progressHandler });
|
||||
const progressEvent = new Event("progress") as ProgressEvent;
|
||||
@@ -154,7 +156,7 @@ describe("MatrixHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should error when no response body", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
@@ -167,7 +169,7 @@ describe("MatrixHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should error on a 400-code", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
@@ -184,7 +186,7 @@ describe("MatrixHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should return response on successful upload", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
@@ -198,14 +200,14 @@ describe("MatrixHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should abort xhr when calling `cancelUpload`", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(api.cancelUpload(upload)).toBeTruthy();
|
||||
expect(xhr.abort).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return false when `cancelUpload` is called but unsuccessful", async () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
|
||||
xhr.readyState = DONE;
|
||||
@@ -220,7 +222,7 @@ describe("MatrixHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should return active uploads in `getCurrentUploads`", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, onlyData: true });
|
||||
upload = api.uploadContent({} as File);
|
||||
expect(api.getCurrentUploads().find((u) => u.promise === upload)).toBeTruthy();
|
||||
api.cancelUpload(upload);
|
||||
@@ -228,7 +230,12 @@ describe("MatrixHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should return expected object from `getContentUri`", () => {
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, accessToken: "token" });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), {
|
||||
baseUrl,
|
||||
prefix,
|
||||
accessToken: "token",
|
||||
onlyData: true,
|
||||
});
|
||||
expect(api.getContentUri()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2998,6 +2998,8 @@ describe("MatrixClient", function () {
|
||||
replacedByDynamicPredecessor2,
|
||||
room2,
|
||||
];
|
||||
client.store.getRoom = (roomId: string) =>
|
||||
client.store.getRooms().find((r) => r.roomId === roomId) || null;
|
||||
room1.addLiveEvents(
|
||||
[
|
||||
roomCreateEvent(room1.roomId, replacedByCreate1.roomId),
|
||||
@@ -3036,6 +3038,7 @@ describe("MatrixClient", function () {
|
||||
replacedByDynamicPredecessor2,
|
||||
};
|
||||
}
|
||||
|
||||
it("Returns an empty list if there are no rooms", () => {
|
||||
client.store = new StubStore();
|
||||
client.store.getRooms = () => [];
|
||||
@@ -3062,6 +3065,8 @@ describe("MatrixClient", function () {
|
||||
const room2 = new Room("room2", client, "@daryl:alexandria.example.com");
|
||||
client.store = new StubStore();
|
||||
client.store.getRooms = () => [room1, replacedRoom1, replacedRoom2, room2];
|
||||
client.store.getRoom = (roomId: string) =>
|
||||
client.store.getRooms().find((r) => r.roomId === roomId) || null;
|
||||
room1.addLiveEvents([roomCreateEvent(room1.roomId, replacedRoom1.roomId)], { addToState: true });
|
||||
room2.addLiveEvents([roomCreateEvent(room2.roomId, replacedRoom2.roomId)], { addToState: true });
|
||||
replacedRoom1.addLiveEvents([tombstoneEvent(room1.roomId, replacedRoom1.roomId)], { addToState: true });
|
||||
@@ -3127,6 +3132,24 @@ describe("MatrixClient", function () {
|
||||
expect(rooms).toContain(room1);
|
||||
expect(rooms).toContain(room2);
|
||||
});
|
||||
|
||||
it("should ignore room replacements which are not reciprocated by the predecessor", () => {
|
||||
const room1 = new Room("room1", client, "@carol:alexandria.example.com");
|
||||
// Room 2 claims to replace room 1 but room 1 does not agree
|
||||
const room2 = new Room("replacedRoom1", client, "@daryl:alexandria.example.com");
|
||||
|
||||
client.store = new StubStore();
|
||||
client.store.getRooms = () => [room1, room2];
|
||||
client.store.getRoom = (roomId: string) =>
|
||||
client.store.getRooms().find((r) => r.roomId === roomId) || null;
|
||||
|
||||
room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)], { addToState: true });
|
||||
|
||||
// When we ask for the visible rooms
|
||||
const rooms = client.getVisibleRooms();
|
||||
expect(rooms).toContain(room1);
|
||||
expect(rooms).toContain(room2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRoomUpgradeHistory", () => {
|
||||
@@ -3597,24 +3620,6 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAuthIssuer", () => {
|
||||
it("should use unstable prefix", async () => {
|
||||
httpLookups = [
|
||||
{
|
||||
method: "GET",
|
||||
path: `/auth_issuer`,
|
||||
data: {
|
||||
issuer: "https://issuer/",
|
||||
},
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
|
||||
},
|
||||
];
|
||||
|
||||
await expect(client.getAuthIssuer()).resolves.toEqual({ issuer: "https://issuer/" });
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAuthMetadata", () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockReset();
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import { encodeBase64, EventType, MatrixClient, type MatrixError, type MatrixEvent, type Room } from "../../../src";
|
||||
import { KnownMembership } from "../../../src/@types/membership";
|
||||
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
|
||||
import { type EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
|
||||
import { Status, type EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
|
||||
import { secureRandomString } from "../../../src/randomstring";
|
||||
import { makeMockEvent, makeMockRoom, membershipTemplate, makeKey, type MembershipData, mockRoomState } from "./mocks";
|
||||
import { RTCEncryptionManager } from "../../../src/matrixrtc/RTCEncryptionManager.ts";
|
||||
@@ -26,6 +26,8 @@ const mockFocus = { type: "mock" };
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
const callSession = { id: "", application: "m.call" };
|
||||
|
||||
describe("MatrixRTCSession", () => {
|
||||
let client: MatrixClient;
|
||||
let sess: MatrixRTCSession | undefined;
|
||||
@@ -49,14 +51,33 @@ describe("MatrixRTCSession", () => {
|
||||
it("creates a room-scoped session from room state", () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
expect(sess?.memberships.length).toEqual(1);
|
||||
expect(sess?.memberships[0].callId).toEqual("");
|
||||
expect(sess?.memberships[0].sessionDescription.id).toEqual("");
|
||||
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?.callId).toEqual("");
|
||||
expect(sess?.sessionDescription.id).toEqual("");
|
||||
});
|
||||
|
||||
it("ignores memberships where application is not m.call", () => {
|
||||
const testMembership = Object.assign({}, membershipTemplate, {
|
||||
application: "not-m.call",
|
||||
});
|
||||
const mockRoom = makeMockRoom([testMembership]);
|
||||
const sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
expect(sess?.memberships).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("ignores memberships where callId is not empty", () => {
|
||||
const testMembership = Object.assign({}, membershipTemplate, {
|
||||
call_id: "not-empty",
|
||||
scope: "m.room",
|
||||
});
|
||||
const mockRoom = makeMockRoom([testMembership]);
|
||||
const sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
expect(sess?.memberships).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("ignores expired memberships events", () => {
|
||||
@@ -67,7 +88,7 @@ describe("MatrixRTCSession", () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate, expiredMembership]);
|
||||
|
||||
jest.advanceTimersByTime(2000);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
expect(sess?.memberships.length).toEqual(1);
|
||||
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
|
||||
jest.useRealTimers();
|
||||
@@ -76,7 +97,7 @@ describe("MatrixRTCSession", () => {
|
||||
it("ignores memberships events of members not in the room", () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
mockRoom.hasMembershipState = (state) => state === KnownMembership.Join;
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
expect(sess?.memberships.length).toEqual(0);
|
||||
});
|
||||
|
||||
@@ -87,14 +108,14 @@ describe("MatrixRTCSession", () => {
|
||||
expiredMembership.created_ts = 500;
|
||||
expiredMembership.expires = 1000;
|
||||
const mockRoom = makeMockRoom([expiredMembership]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("returns empty session if no membership events are present", () => {
|
||||
const mockRoom = makeMockRoom([]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
expect(sess?.memberships).toHaveLength(0);
|
||||
});
|
||||
|
||||
@@ -129,7 +150,7 @@ describe("MatrixRTCSession", () => {
|
||||
}),
|
||||
}),
|
||||
};
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom as unknown as Room);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom as unknown as Room, callSession);
|
||||
expect(sess.memberships).toHaveLength(0);
|
||||
});
|
||||
|
||||
@@ -164,7 +185,7 @@ describe("MatrixRTCSession", () => {
|
||||
}),
|
||||
}),
|
||||
};
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom as unknown as Room);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom as unknown as Room, callSession);
|
||||
expect(sess.memberships).toHaveLength(0);
|
||||
});
|
||||
|
||||
@@ -172,7 +193,7 @@ describe("MatrixRTCSession", () => {
|
||||
const testMembership = Object.assign({}, membershipTemplate);
|
||||
(testMembership.device_id as string | undefined) = undefined;
|
||||
const mockRoom = makeMockRoom([testMembership]);
|
||||
const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
const sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
expect(sess.memberships).toHaveLength(0);
|
||||
});
|
||||
|
||||
@@ -180,23 +201,7 @@ describe("MatrixRTCSession", () => {
|
||||
const testMembership = Object.assign({}, membershipTemplate);
|
||||
(testMembership.call_id as string | undefined) = undefined;
|
||||
const mockRoom = makeMockRoom([testMembership]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
expect(sess.memberships).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("ignores memberships with no scope", () => {
|
||||
const testMembership = Object.assign({}, membershipTemplate);
|
||||
(testMembership.scope as string | undefined) = undefined;
|
||||
const mockRoom = makeMockRoom([testMembership]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
expect(sess.memberships).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("ignores anything that's not a room-scoped call (for now)", () => {
|
||||
const testMembership = Object.assign({}, membershipTemplate);
|
||||
testMembership.scope = "m.user";
|
||||
const mockRoom = makeMockRoom([testMembership]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
expect(sess.memberships).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -211,7 +216,7 @@ describe("MatrixRTCSession", () => {
|
||||
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
|
||||
]);
|
||||
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
expect(sess.getOldestMembership()!.deviceId).toEqual("old");
|
||||
jest.useRealTimers();
|
||||
});
|
||||
@@ -236,7 +241,7 @@ describe("MatrixRTCSession", () => {
|
||||
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
|
||||
]);
|
||||
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
|
||||
sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }], {
|
||||
type: "livekit",
|
||||
@@ -256,7 +261,7 @@ describe("MatrixRTCSession", () => {
|
||||
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
|
||||
]);
|
||||
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
|
||||
sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }], {
|
||||
type: "livekit",
|
||||
@@ -283,7 +288,7 @@ describe("MatrixRTCSession", () => {
|
||||
client._unstable_updateDelayedEvent = jest.fn();
|
||||
|
||||
mockRoom = makeMockRoom([]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -304,8 +309,19 @@ describe("MatrixRTCSession", () => {
|
||||
expect(sess!.isJoined()).toEqual(true);
|
||||
});
|
||||
|
||||
it("sends a notification when starting a call", async () => {
|
||||
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" });
|
||||
const didSendEventFn = jest.fn();
|
||||
sess!.once(MatrixRTCSessionEvent.DidSendCallNotification, didSendEventFn);
|
||||
// Create an additional listener to create a promise that resolves after the emission.
|
||||
const didSendNotification = new Promise((resolve) => {
|
||||
sess!.once(MatrixRTCSessionEvent.DidSendCallNotification, resolve);
|
||||
});
|
||||
|
||||
sess!.joinRoomSession([mockFocus], mockFocus, { notificationType: "ring" });
|
||||
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 5000))]);
|
||||
mockRoomState(mockRoom, [{ ...membershipTemplate, user_id: client.getUserId()! }]);
|
||||
@@ -317,7 +333,7 @@ describe("MatrixRTCSession", () => {
|
||||
"notification_type": "ring",
|
||||
"m.relates_to": {
|
||||
event_id: ownMembershipId,
|
||||
rel_type: "org.matrix.msc4075.rtc.notification.parent",
|
||||
rel_type: "m.reference",
|
||||
},
|
||||
"lifetime": 30000,
|
||||
"sender_ts": expect.any(Number),
|
||||
@@ -330,6 +346,28 @@ describe("MatrixRTCSession", () => {
|
||||
"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),
|
||||
},
|
||||
{
|
||||
"application": "m.call",
|
||||
"call_id": "",
|
||||
"event_id": "legacy-evt",
|
||||
"m.mentions": { room: true, user_ids: [] },
|
||||
"notify_type": "ring",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("doesn't send a notification when joining an existing call", async () => {
|
||||
@@ -366,7 +404,7 @@ describe("MatrixRTCSession", () => {
|
||||
describe("onMembershipsChanged", () => {
|
||||
it("does not emit if no membership changes", () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
|
||||
const onMembershipsChanged = jest.fn();
|
||||
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
|
||||
@@ -377,7 +415,7 @@ describe("MatrixRTCSession", () => {
|
||||
|
||||
it("emits on membership changes", () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
|
||||
const onMembershipsChanged = jest.fn();
|
||||
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
|
||||
@@ -432,7 +470,7 @@ describe("MatrixRTCSession", () => {
|
||||
client.encryptAndSendToDevice = sendToDeviceMock;
|
||||
|
||||
mockRoom = makeMockRoom([]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -551,7 +589,7 @@ describe("MatrixRTCSession", () => {
|
||||
device_id: "BBBBBBB",
|
||||
});
|
||||
const mockRoom = makeMockRoom([membershipTemplate, member2]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
|
||||
// joining will trigger an initial key send
|
||||
const keysSentPromise1 = new Promise<EncryptionKeysEventContent>((resolve) => {
|
||||
@@ -600,7 +638,7 @@ describe("MatrixRTCSession", () => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
|
||||
const keysSentPromise1 = new Promise<EncryptionKeysEventContent>((resolve) => {
|
||||
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
|
||||
@@ -651,7 +689,7 @@ describe("MatrixRTCSession", () => {
|
||||
const mockRoom = makeMockRoom([member1, member2]);
|
||||
mockRoomState(mockRoom, [member1, member2]);
|
||||
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||
|
||||
await keysSentPromise1;
|
||||
@@ -696,7 +734,7 @@ describe("MatrixRTCSession", () => {
|
||||
};
|
||||
|
||||
const mockRoom = makeMockRoom([member1, member2]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||
|
||||
await keysSentPromise1;
|
||||
@@ -760,7 +798,7 @@ describe("MatrixRTCSession", () => {
|
||||
device_id: "BBBBBBB",
|
||||
});
|
||||
const mockRoom = makeMockRoom([membershipTemplate, member2]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
|
||||
const onMyEncryptionKeyChanged = jest.fn();
|
||||
sess.on(
|
||||
@@ -850,7 +888,7 @@ describe("MatrixRTCSession", () => {
|
||||
|
||||
if (i === 0) {
|
||||
// if first time around then set up the session
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||
} else {
|
||||
// otherwise update the state reducing the membership each time in order to trigger key rotation
|
||||
@@ -876,7 +914,7 @@ describe("MatrixRTCSession", () => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
|
||||
const keysSentPromise1 = new Promise((resolve) => {
|
||||
sendEventMock.mockImplementation(resolve);
|
||||
@@ -917,7 +955,7 @@ describe("MatrixRTCSession", () => {
|
||||
});
|
||||
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
|
||||
sess!.joinRoomSession([mockFocus], mockFocus, {
|
||||
manageMediaKeys: true,
|
||||
@@ -940,7 +978,7 @@ describe("MatrixRTCSession", () => {
|
||||
describe("receiving", () => {
|
||||
it("collects keys from encryption events", async () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||
mockRoom.emitTimelineEvent(
|
||||
makeMockEvent("io.element.call.encryption_keys", "@bob:example.org", "1234roomId", {
|
||||
@@ -965,7 +1003,7 @@ describe("MatrixRTCSession", () => {
|
||||
|
||||
it("collects keys at non-zero indices", async () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||
mockRoom.emitTimelineEvent(
|
||||
makeMockEvent("io.element.call.encryption_keys", "@bob:example.org", "1234roomId", {
|
||||
@@ -991,7 +1029,7 @@ describe("MatrixRTCSession", () => {
|
||||
|
||||
it("collects keys by merging", async () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||
mockRoom.emitTimelineEvent(
|
||||
makeMockEvent("io.element.call.encryption_keys", "@bob:example.org", "1234roomId", {
|
||||
@@ -1042,7 +1080,7 @@ describe("MatrixRTCSession", () => {
|
||||
|
||||
it("ignores older keys at same index", async () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||
mockRoom.emitTimelineEvent(
|
||||
makeMockEvent(
|
||||
@@ -1101,7 +1139,7 @@ describe("MatrixRTCSession", () => {
|
||||
|
||||
it("key timestamps are treated as monotonic", async () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||
mockRoom.emitTimelineEvent(
|
||||
makeMockEvent(
|
||||
@@ -1145,7 +1183,7 @@ describe("MatrixRTCSession", () => {
|
||||
|
||||
it("ignores keys event for the local participant", () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
|
||||
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||
mockRoom.emitTimelineEvent(
|
||||
@@ -1168,7 +1206,7 @@ describe("MatrixRTCSession", () => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
|
||||
// defaults to getTs()
|
||||
jest.setSystemTime(1000);
|
||||
@@ -1221,5 +1259,31 @@ describe("MatrixRTCSession", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("read status", () => {
|
||||
it("returns the correct probablyLeft status", () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
expect(sess!.probablyLeft).toBe(undefined);
|
||||
|
||||
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||
expect(sess!.probablyLeft).toBe(false);
|
||||
|
||||
// Simulate the membership manager believing the user has left
|
||||
const accessPrivateFieldsSession = sess as unknown as {
|
||||
membershipManager: { state: { probablyLeft: boolean } };
|
||||
};
|
||||
accessPrivateFieldsSession.membershipManager.state.probablyLeft = true;
|
||||
expect(sess!.probablyLeft).toBe(true);
|
||||
});
|
||||
|
||||
it("returns membershipStatus once joinRoomSession got called", () => {
|
||||
const mockRoom = makeMockRoom([membershipTemplate]);
|
||||
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
|
||||
expect(sess!.membershipStatus).toBe(undefined);
|
||||
|
||||
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||
expect(sess!.membershipStatus).toBe(Status.Connecting);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,8 +16,9 @@ limitations under the License.
|
||||
|
||||
import { ClientEvent, EventTimeline, MatrixClient } from "../../../src";
|
||||
import { RoomStateEvent } from "../../../src/models/room-state";
|
||||
import { MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
|
||||
import { MatrixRTCSessionManager, MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
|
||||
import { makeMockRoom, membershipTemplate, mockRoomState } from "./mocks";
|
||||
import { logger } from "../../../src/logger";
|
||||
|
||||
describe("MatrixRTCSessionManager", () => {
|
||||
let client: MatrixClient;
|
||||
@@ -47,6 +48,21 @@ describe("MatrixRTCSessionManager", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("Doesn't fire event if unrelated sessions starts", () => {
|
||||
const onStarted = jest.fn();
|
||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
|
||||
try {
|
||||
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }]);
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
expect(onStarted).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
}
|
||||
});
|
||||
|
||||
it("Fires event when session ends", () => {
|
||||
const onEnded = jest.fn();
|
||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
||||
@@ -59,9 +75,75 @@ describe("MatrixRTCSessionManager", () => {
|
||||
mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
|
||||
|
||||
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||
const membEvent = roomState.getStateEvents("")[0];
|
||||
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
|
||||
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
|
||||
|
||||
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
||||
});
|
||||
|
||||
it("Fires correctly with for with custom sessionDescription", () => {
|
||||
const onStarted = jest.fn();
|
||||
const onEnded = jest.fn();
|
||||
// create a session manager with a custom session description
|
||||
const sessionManager = new MatrixRTCSessionManager(logger, client, { id: "test", application: "m.notCall" });
|
||||
|
||||
// manually start the session manager (its not the default one started by the client)
|
||||
sessionManager.start();
|
||||
sessionManager.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
||||
sessionManager.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
|
||||
try {
|
||||
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }]);
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
expect(onStarted).not.toHaveBeenCalled();
|
||||
onStarted.mockClear();
|
||||
|
||||
const room2 = makeMockRoom([{ ...membershipTemplate, application: "m.notCall", call_id: "test" }]);
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room1, room2]);
|
||||
|
||||
client.emit(ClientEvent.Room, room2);
|
||||
expect(onStarted).toHaveBeenCalled();
|
||||
onStarted.mockClear();
|
||||
|
||||
mockRoomState(room2, [{ user_id: membershipTemplate.user_id }]);
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room2);
|
||||
|
||||
const roomState = room2.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
|
||||
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
|
||||
expect(onEnded).toHaveBeenCalled();
|
||||
onEnded.mockClear();
|
||||
|
||||
mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||
|
||||
const roomStateOther = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||
const membEventOther = roomStateOther.getStateEvents("org.matrix.msc3401.call.member")[0];
|
||||
client.emit(RoomStateEvent.Events, membEventOther, roomStateOther, null);
|
||||
expect(onEnded).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
|
||||
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
||||
}
|
||||
});
|
||||
|
||||
it("Doesn't fire event if unrelated sessions ends", () => {
|
||||
const onEnded = jest.fn();
|
||||
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
|
||||
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other_app" }]);
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room1);
|
||||
|
||||
client.emit(ClientEvent.Room, room1);
|
||||
|
||||
mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
|
||||
|
||||
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
|
||||
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
|
||||
|
||||
expect(onEnded).not.toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,14 @@ limitations under the License.
|
||||
|
||||
import { type MockedFunction, type Mock } from "jest-mock";
|
||||
|
||||
import { EventType, HTTPError, MatrixError, UnsupportedDelayedEventsEndpointError, type Room } from "../../../src";
|
||||
import {
|
||||
type EmptyObject,
|
||||
EventType,
|
||||
HTTPError,
|
||||
MatrixError,
|
||||
UnsupportedDelayedEventsEndpointError,
|
||||
type Room,
|
||||
} from "../../../src";
|
||||
import {
|
||||
MembershipManagerEvent,
|
||||
Status,
|
||||
@@ -64,6 +71,8 @@ function createAsyncHandle<T>(method: MockedFunction<any>) {
|
||||
return { reject, resolve };
|
||||
}
|
||||
|
||||
const callSession = { id: "", application: "m.call" };
|
||||
|
||||
describe("MembershipManager", () => {
|
||||
let client: MockClient;
|
||||
let room: Room;
|
||||
@@ -95,12 +104,12 @@ describe("MembershipManager", () => {
|
||||
|
||||
describe("isActivated()", () => {
|
||||
it("defaults to false", () => {
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
expect(manager.isActivated()).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns true after join()", () => {
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
manager.join([]);
|
||||
expect(manager.isActivated()).toEqual(true);
|
||||
});
|
||||
@@ -114,7 +123,7 @@ describe("MembershipManager", () => {
|
||||
const updateDelayedEventHandle = createAsyncHandle<void>(client._unstable_updateDelayedEvent as Mock);
|
||||
|
||||
// Test
|
||||
const memberManager = new MembershipManager(undefined, room, client, () => undefined);
|
||||
const memberManager = new MembershipManager(undefined, room, client, () => undefined, callSession);
|
||||
memberManager.join([focus], focusActive);
|
||||
// expects
|
||||
await waitForMockCall(client.sendStateEvent, Promise.resolve({ event_id: "id" }));
|
||||
@@ -130,7 +139,7 @@ describe("MembershipManager", () => {
|
||||
focus_active: focusActive,
|
||||
scope: "m.room",
|
||||
},
|
||||
"_@alice:example.org_AAAAAAA",
|
||||
"_@alice:example.org_AAAAAAA_m.call",
|
||||
);
|
||||
updateDelayedEventHandle.resolve?.();
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
|
||||
@@ -138,13 +147,13 @@ describe("MembershipManager", () => {
|
||||
{ delay: 8000 },
|
||||
"org.matrix.msc3401.call.member",
|
||||
{},
|
||||
"_@alice:example.org_AAAAAAA",
|
||||
"_@alice:example.org_AAAAAAA_m.call",
|
||||
);
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("reschedules delayed leave event if sending state cancels it", async () => {
|
||||
const memberManager = new MembershipManager(undefined, room, client, () => undefined);
|
||||
const memberManager = new MembershipManager(undefined, room, client, () => undefined, callSession);
|
||||
const waitForSendState = waitForMockCall(client.sendStateEvent);
|
||||
const waitForUpdateDelaye = waitForMockCallOnce(
|
||||
client._unstable_updateDelayedEvent,
|
||||
@@ -189,7 +198,7 @@ describe("MembershipManager", () => {
|
||||
});
|
||||
});
|
||||
|
||||
const userStateKey = `${!useOwnedStateEvents ? "_" : ""}@alice:example.org_AAAAAAA`;
|
||||
const userStateKey = `${!useOwnedStateEvents ? "_" : ""}@alice:example.org_AAAAAAA_m.call`;
|
||||
// preparing the delayed disconnect should handle ratelimiting
|
||||
const sendDelayedStateAttempt = new Promise<void>((resolve) => {
|
||||
const error = new MatrixError({ errcode: "M_LIMIT_EXCEEDED" });
|
||||
@@ -220,6 +229,7 @@ describe("MembershipManager", () => {
|
||||
room,
|
||||
client,
|
||||
() => undefined,
|
||||
callSession,
|
||||
);
|
||||
manager.join([focus], focusActive);
|
||||
|
||||
@@ -276,7 +286,7 @@ describe("MembershipManager", () => {
|
||||
describe("delayed leave event", () => {
|
||||
it("does not try again to schedule a delayed leave event if not supported", () => {
|
||||
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
manager.join([focus], focusActive);
|
||||
delayedHandle.reject?.(
|
||||
new UnsupportedDelayedEventsEndpointError(
|
||||
@@ -288,7 +298,7 @@ describe("MembershipManager", () => {
|
||||
});
|
||||
it("does try to schedule a delayed leave event again if rate limited", async () => {
|
||||
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
manager.join([focus], focusActive);
|
||||
delayedHandle.reject?.(new HTTPError("rate limited", 429, undefined));
|
||||
await jest.advanceTimersByTimeAsync(5000);
|
||||
@@ -300,6 +310,7 @@ describe("MembershipManager", () => {
|
||||
room,
|
||||
client,
|
||||
() => undefined,
|
||||
callSession,
|
||||
);
|
||||
manager.join([focus], focusActive);
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
|
||||
@@ -307,7 +318,7 @@ describe("MembershipManager", () => {
|
||||
{ delay: 123456 },
|
||||
"org.matrix.msc3401.call.member",
|
||||
{},
|
||||
"_@alice:example.org_AAAAAAA",
|
||||
"_@alice:example.org_AAAAAAA_m.call",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -319,6 +330,7 @@ describe("MembershipManager", () => {
|
||||
room,
|
||||
client,
|
||||
() => undefined,
|
||||
callSession,
|
||||
);
|
||||
// Join with the membership manager
|
||||
manager.join([focus], focusActive);
|
||||
@@ -351,7 +363,13 @@ describe("MembershipManager", () => {
|
||||
});
|
||||
|
||||
it("uses membershipEventExpiryMs from config", async () => {
|
||||
const manager = new MembershipManager({ membershipEventExpiryMs: 1234567 }, room, client, () => undefined);
|
||||
const manager = new MembershipManager(
|
||||
{ membershipEventExpiryMs: 1234567 },
|
||||
room,
|
||||
client,
|
||||
() => undefined,
|
||||
callSession,
|
||||
);
|
||||
|
||||
manager.join([focus], focusActive);
|
||||
await waitForMockCall(client.sendStateEvent);
|
||||
@@ -370,12 +388,12 @@ describe("MembershipManager", () => {
|
||||
type: "livekit",
|
||||
},
|
||||
},
|
||||
"_@alice:example.org_AAAAAAA",
|
||||
"_@alice:example.org_AAAAAAA_m.call",
|
||||
);
|
||||
});
|
||||
|
||||
it("does nothing if join called when already joined", async () => {
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
manager.join([focus], focusActive);
|
||||
await waitForMockCall(client.sendStateEvent);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||
@@ -387,7 +405,7 @@ describe("MembershipManager", () => {
|
||||
describe("leave()", () => {
|
||||
// TODO add rate limit cases.
|
||||
it("resolves delayed leave event when leave is called", async () => {
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
manager.join([focus], focusActive);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
await manager.leave();
|
||||
@@ -395,7 +413,7 @@ describe("MembershipManager", () => {
|
||||
expect(client.sendStateEvent).toHaveBeenCalled();
|
||||
});
|
||||
it("send leave event when leave is called and resolving delayed leave fails", async () => {
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
manager.join([focus], focusActive);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue("unknown");
|
||||
@@ -406,11 +424,11 @@ describe("MembershipManager", () => {
|
||||
room.roomId,
|
||||
"org.matrix.msc3401.call.member",
|
||||
{},
|
||||
"_@alice:example.org_AAAAAAA",
|
||||
"_@alice:example.org_AAAAAAA_m.call",
|
||||
);
|
||||
});
|
||||
it("does nothing if not joined", () => {
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
expect(async () => await manager.leave()).not.toThrow();
|
||||
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
@@ -420,7 +438,7 @@ describe("MembershipManager", () => {
|
||||
describe("getsActiveFocus", () => {
|
||||
it("gets the correct active focus with oldest_membership", () => {
|
||||
const getOldestMembership = jest.fn();
|
||||
const manager = new MembershipManager({}, room, client, getOldestMembership);
|
||||
const manager = new MembershipManager({}, room, client, getOldestMembership, callSession);
|
||||
// Before joining the active focus should be undefined (see FocusInUse on MatrixRTCSession)
|
||||
expect(manager.getActiveFocus()).toBe(undefined);
|
||||
manager.join([focus], focusActive);
|
||||
@@ -455,7 +473,7 @@ describe("MembershipManager", () => {
|
||||
});
|
||||
|
||||
it("does not provide focus if the selection method is unknown", () => {
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
manager.join([focus], Object.assign(focusActive, { type: "unknown_type" }));
|
||||
expect(manager.getActiveFocus()).toBe(undefined);
|
||||
});
|
||||
@@ -463,7 +481,7 @@ describe("MembershipManager", () => {
|
||||
|
||||
describe("onRTCSessionMemberUpdate()", () => {
|
||||
it("does nothing if not joined", async () => {
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
|
||||
await jest.advanceTimersToNextTimerAsync();
|
||||
expect(client.sendStateEvent).not.toHaveBeenCalled();
|
||||
@@ -471,7 +489,7 @@ describe("MembershipManager", () => {
|
||||
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
it("does nothing if own membership still present", async () => {
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
manager.join([focus], focusActive);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
const myMembership = (client.sendStateEvent as Mock).mock.calls[0][2];
|
||||
@@ -495,7 +513,7 @@ describe("MembershipManager", () => {
|
||||
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
it("recreates membership if it is missing", async () => {
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
manager.join([focus], focusActive);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
|
||||
@@ -513,7 +531,7 @@ describe("MembershipManager", () => {
|
||||
});
|
||||
|
||||
it("updates the UpdateExpiry entry in the action scheduler", async () => {
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
manager.join([focus], focusActive);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
|
||||
@@ -547,6 +565,7 @@ describe("MembershipManager", () => {
|
||||
room,
|
||||
client,
|
||||
() => undefined,
|
||||
{ id: "", application: "m.call" },
|
||||
);
|
||||
manager.join([focus], focusActive);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
@@ -578,6 +597,7 @@ describe("MembershipManager", () => {
|
||||
room,
|
||||
client,
|
||||
() => undefined,
|
||||
{ id: "", application: "m.call" },
|
||||
);
|
||||
manager.join([focus], focusActive);
|
||||
await waitForMockCall(client.sendStateEvent);
|
||||
@@ -598,16 +618,17 @@ describe("MembershipManager", () => {
|
||||
await testExpires(10_000, 1_000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("status updates", () => {
|
||||
it("starts 'Disconnected'", () => {
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
expect(manager.status).toBe(Status.Disconnected);
|
||||
});
|
||||
it("emits 'Connection' and 'Connected' after join", async () => {
|
||||
const handleDelayedEvent = createAsyncHandle<void>(client._unstable_sendDelayedStateEvent);
|
||||
const handleStateEvent = createAsyncHandle<void>(client.sendStateEvent);
|
||||
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
expect(manager.status).toBe(Status.Disconnected);
|
||||
const connectEmit = jest.fn();
|
||||
manager.on(MembershipManagerEvent.StatusChanged, connectEmit);
|
||||
@@ -621,7 +642,7 @@ describe("MembershipManager", () => {
|
||||
expect(connectEmit).toHaveBeenCalledWith(Status.Connecting, Status.Connected);
|
||||
});
|
||||
it("emits 'Disconnecting' and 'Disconnected' after leave", async () => {
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
const connectEmit = jest.fn();
|
||||
manager.on(MembershipManagerEvent.StatusChanged, connectEmit);
|
||||
manager.join([focus], focusActive);
|
||||
@@ -637,7 +658,7 @@ describe("MembershipManager", () => {
|
||||
it("sends retry if call membership event is still valid at time of retry", async () => {
|
||||
const handle = createAsyncHandle(client._unstable_sendDelayedStateEvent);
|
||||
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
manager.join([focus], focusActive);
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -664,7 +685,7 @@ describe("MembershipManager", () => {
|
||||
new Headers({ "Retry-After": "1" }),
|
||||
),
|
||||
);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
// Should call _unstable_sendDelayedStateEvent but not sendStateEvent because of the
|
||||
// RateLimit error.
|
||||
manager.join([focus], focusActive);
|
||||
@@ -684,7 +705,7 @@ describe("MembershipManager", () => {
|
||||
it("abandons retry loop if leave() was called before sending state event", async () => {
|
||||
const handle = createAsyncHandle(client._unstable_sendDelayedStateEvent);
|
||||
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
manager.join([focus], focusActive);
|
||||
handle.reject?.(
|
||||
new MatrixError(
|
||||
@@ -719,7 +740,7 @@ describe("MembershipManager", () => {
|
||||
new Headers({ "Retry-After": "1" }),
|
||||
),
|
||||
);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
manager.join([focus], focusActive);
|
||||
|
||||
// Hit rate limit
|
||||
@@ -752,7 +773,7 @@ describe("MembershipManager", () => {
|
||||
new Headers({ "Retry-After": "2" }),
|
||||
),
|
||||
);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
manager.join([focus], focusActive, delayEventSendError);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
@@ -772,7 +793,7 @@ describe("MembershipManager", () => {
|
||||
new Headers({ "Retry-After": "1" }),
|
||||
),
|
||||
);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
manager.join([focus], focusActive, delayEventRestartError);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
@@ -783,7 +804,7 @@ describe("MembershipManager", () => {
|
||||
it("falls back to using pure state events when some error occurs while sending delayed events", async () => {
|
||||
const unrecoverableError = jest.fn();
|
||||
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(new HTTPError("unknown", 601));
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
manager.join([focus], focusActive, unrecoverableError);
|
||||
await waitForMockCall(client.sendStateEvent);
|
||||
expect(unrecoverableError).not.toHaveBeenCalledWith();
|
||||
@@ -797,6 +818,7 @@ describe("MembershipManager", () => {
|
||||
room,
|
||||
client,
|
||||
() => undefined,
|
||||
callSession,
|
||||
);
|
||||
manager.join([focus], focusActive, unrecoverableError);
|
||||
for (let retries = 0; retries < 7; retries++) {
|
||||
@@ -814,7 +836,7 @@ describe("MembershipManager", () => {
|
||||
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(
|
||||
new UnsupportedDelayedEventsEndpointError("not supported", "sendDelayedStateEvent"),
|
||||
);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined);
|
||||
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
|
||||
manager.join([focus], focusActive, unrecoverableError);
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
|
||||
@@ -822,13 +844,70 @@ describe("MembershipManager", () => {
|
||||
expect(client.sendStateEvent).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe("probablyLeft", () => {
|
||||
it("emits probablyLeft when the membership manager could not hear back from the server for the duration of the delayed event", async () => {
|
||||
const manager = new MembershipManager(
|
||||
{ delayedLeaveEventDelayMs: 10000 },
|
||||
room,
|
||||
client,
|
||||
() => undefined,
|
||||
callSession,
|
||||
);
|
||||
const { promise: stuckPromise, reject: rejectStuckPromise } = Promise.withResolvers<EmptyObject>();
|
||||
const probablyLeftEmit = jest.fn();
|
||||
manager.on(MembershipManagerEvent.ProbablyLeft, probablyLeftEmit);
|
||||
manager.join([focus], focusActive);
|
||||
try {
|
||||
// Let the scheduler run one iteration so that we can send the join state event
|
||||
await waitForMockCall(client._unstable_updateDelayedEvent);
|
||||
|
||||
// We never resolve the delayed event so that we can test the probablyLeft event.
|
||||
// This simulates the case where the server does not respond to the delayed event.
|
||||
client._unstable_updateDelayedEvent = jest.fn(() => stuckPromise);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||
expect(manager.status).toBe(Status.Connected);
|
||||
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
|
||||
// We expect the probablyLeft event to be emitted after the `delayedLeaveEventDelayMs` = 10000.
|
||||
// We also track the calls to updated the delayed event that all will never resolve to simulate the server not responding.
|
||||
// The numbers are a bit arbitrary since we use the local timeout that does not perfectly match the 5s check interval in this test.
|
||||
await jest.advanceTimersByTimeAsync(5000);
|
||||
// No emission after 5s
|
||||
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(4999);
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(3);
|
||||
expect(probablyLeftEmit).not.toHaveBeenCalledWith(true);
|
||||
|
||||
// Reset mocks before we setup the next delayed event restart by advancing the timers 1 more ms.
|
||||
(client._unstable_updateDelayedEvent as Mock<any>).mockResolvedValue({});
|
||||
|
||||
// Emit after 10s
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(4);
|
||||
expect(probablyLeftEmit).toHaveBeenCalledWith(true);
|
||||
|
||||
// Mock a sync which does not include our own membership
|
||||
await manager.onRTCSessionMemberUpdate([]);
|
||||
// Wait for the current ongoing delayed event sending to finish
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
// We should send a new state event and an associated delayed leave event.
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(2);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(2);
|
||||
// At the same time we expect the probablyLeft event to be emitted with false so we are back operational.
|
||||
expect(probablyLeftEmit).toHaveBeenCalledWith(false);
|
||||
} finally {
|
||||
rejectStuckPromise();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("Should prefix log with MembershipManager used", () => {
|
||||
const client = makeMockClient("@alice:example.org", "AAAAAAA");
|
||||
const room = makeMockRoom([membershipTemplate]);
|
||||
|
||||
const membershipManager = new MembershipManager(undefined, room, client, () => undefined, logger);
|
||||
const membershipManager = new MembershipManager(undefined, room, client, () => undefined, callSession, logger);
|
||||
|
||||
const spy = jest.spyOn(console, "error");
|
||||
// Double join
|
||||
|
||||
@@ -379,7 +379,9 @@ describe("MatrixEvent", () => {
|
||||
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
|
||||
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
|
||||
expect(encryptedEvent.decryptionFailureReason).toEqual(DecryptionFailureCode.UNKNOWN_ERROR);
|
||||
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeFalsy();
|
||||
expect(encryptedEvent.decryptionFailureReason).not.toBe(
|
||||
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
|
||||
);
|
||||
expect(encryptedEvent.getContent()).toEqual({
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: "** Unable to decrypt: Error: test error **",
|
||||
@@ -403,7 +405,9 @@ describe("MatrixEvent", () => {
|
||||
expect(encryptedEvent.decryptionFailureReason).toEqual(
|
||||
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
||||
);
|
||||
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeFalsy();
|
||||
expect(encryptedEvent.decryptionFailureReason).not.toBe(
|
||||
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
|
||||
);
|
||||
expect(encryptedEvent.getContent()).toEqual({
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: "** Unable to decrypt: DecryptionError: uisi **",
|
||||
@@ -427,7 +431,9 @@ describe("MatrixEvent", () => {
|
||||
expect(encryptedEvent.isEncrypted()).toBeTruthy();
|
||||
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
|
||||
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
|
||||
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeTruthy();
|
||||
expect(encryptedEvent.decryptionFailureReason).toBe(
|
||||
DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
|
||||
);
|
||||
expect(encryptedEvent.getContent()).toEqual({
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: "** Unable to decrypt: DecryptionError: The sender has disabled encrypting to unverified devices. **",
|
||||
|
||||
@@ -22,7 +22,7 @@ import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../..
|
||||
import { makeThreadEvent, mkThread, populateThread } from "../../test-utils/thread";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { emitPromise, mkEdit, mkMessage, mkReaction, mock } from "../../test-utils/test-utils";
|
||||
import { Direction, EventStatus, EventType, MatrixEvent } from "../../../src";
|
||||
import { Direction, EventStatus, EventType, MatrixEvent, RelationType } from "../../../src";
|
||||
import { ReceiptType } from "../../../src/@types/read_receipts";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils/client";
|
||||
import { ReEmitter } from "../../../src/ReEmitter";
|
||||
@@ -773,6 +773,215 @@ describe("Thread", () => {
|
||||
expect(lastEvent).toBe(message1);
|
||||
expect(firstEvent).toBe(message2);
|
||||
});
|
||||
|
||||
it("Edit events are properly aggregated in threads with server-side support", async () => {
|
||||
// This test reproduces the race condition bug from https://github.com/element-hq/element-web/issues/30617
|
||||
// The bug occurs when edits arrive while the thread is not initialized,
|
||||
// causing aggregation to fail because the target event isn't in the timeline yet
|
||||
|
||||
// Given a thread exists with server-side support enabled
|
||||
const myUserId = "@alice:example.org";
|
||||
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
|
||||
const room = new Room("!room:z", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
|
||||
// Create a root event
|
||||
const rootEvent = mkMessage({
|
||||
room: room.roomId,
|
||||
user: myUserId,
|
||||
msg: "Root message",
|
||||
event: true,
|
||||
});
|
||||
|
||||
// Create thread manually - starts with initialEventsFetched = false
|
||||
const thread = new Thread(rootEvent.getId()!, rootEvent, {
|
||||
room: room,
|
||||
client: client,
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
// The thread is NOT initialized - this is the key to reproducing the bug!
|
||||
expect(thread.initialEventsFetched).toBe(false);
|
||||
|
||||
// Create a message that will be edited
|
||||
const originalMessage = mkMessage({
|
||||
room: room.roomId,
|
||||
user: myUserId,
|
||||
msg: "Original message in thread",
|
||||
relatesTo: {
|
||||
"rel_type": THREAD_RELATION_TYPE.name,
|
||||
"event_id": thread.id,
|
||||
"m.in_reply_to": {
|
||||
event_id: thread.id,
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
|
||||
// Create edit events BEFORE the original message is in the timeline
|
||||
const edit1 = mkEdit(originalMessage, client, myUserId, room.roomId, "Edit 1");
|
||||
const edit2 = mkEdit(originalMessage, client, myUserId, room.roomId, "Edit 2");
|
||||
const edit3 = mkEdit(originalMessage, client, myUserId, room.roomId, "Final edit");
|
||||
|
||||
// CRITICAL: Add edits while thread is NOT initialized
|
||||
// They will be queued in replayEvents and aggregation will be attempted but fail
|
||||
await thread.addEvent(edit1, false);
|
||||
|
||||
// Check the aggregation state after adding first edit
|
||||
// With our fix: edits should NOT be aggregated yet (thread not initialized)
|
||||
// Without fix: edits would be aggregated but fail to link to target
|
||||
const relationsAfterFirstEdit = thread.timelineSet.relations?.getChildEventsForEvent(
|
||||
originalMessage.getId()!,
|
||||
RelationType.Replace,
|
||||
EventType.RoomMessage,
|
||||
);
|
||||
|
||||
// With the fix, no aggregation happens yet (which is correct)
|
||||
// Without the fix, aggregation would happen but fail silently
|
||||
expect(relationsAfterFirstEdit).toBeUndefined();
|
||||
|
||||
// Add remaining edits
|
||||
await thread.addEvent(edit2, false);
|
||||
await thread.addEvent(edit3, false);
|
||||
|
||||
// Check that edits went to replayEvents
|
||||
expect(thread.replayEvents).toHaveLength(3);
|
||||
|
||||
// Now initialize the thread and add the original message
|
||||
thread.initialEventsFetched = true;
|
||||
|
||||
// Clear replayEvents and add the original message
|
||||
const replayEvents = [...(thread.replayEvents || [])];
|
||||
thread.replayEvents = [];
|
||||
|
||||
// Add original message first
|
||||
await thread.addEvent(originalMessage, false);
|
||||
|
||||
// At this point, the original message should NOT have the edits aggregated yet
|
||||
// because they were attempted when the target wasn't in timeline
|
||||
const replacingEventBeforeReplay = originalMessage.replacingEvent();
|
||||
// With the fix, edits should not be aggregated yet (pre-init)
|
||||
expect(replacingEventBeforeReplay).toBeNull();
|
||||
|
||||
// Then replay the edits
|
||||
for (const event of replayEvents) {
|
||||
await thread.addEvent(event, false);
|
||||
}
|
||||
|
||||
// After replay, check aggregation
|
||||
const replacingEvent = originalMessage.replacingEvent();
|
||||
|
||||
// This should now work because edits were re-aggregated when replayed
|
||||
expect(replacingEvent).toBe(edit3);
|
||||
|
||||
// The content should also be updated
|
||||
expect(originalMessage.getContent().body).toBe("Final edit");
|
||||
|
||||
// Relations for replaces should now exist and include all edits in order
|
||||
const replaceRels = thread.timelineSet.relations!.getChildEventsForEvent(
|
||||
originalMessage.getId()!,
|
||||
RelationType.Replace,
|
||||
EventType.RoomMessage,
|
||||
)!;
|
||||
const replaceIds = replaceRels.getRelations().map((e) => e.getId());
|
||||
expect(replaceIds).toHaveLength(3);
|
||||
expect(replaceIds[0]).toBe(edit1.getId());
|
||||
expect(replaceIds[1]).toBe(edit2.getId());
|
||||
expect(replaceIds[2]).toBe(edit3.getId());
|
||||
});
|
||||
|
||||
it("Reactions aggregate pre-init and remain idempotent on replay", async () => {
|
||||
const myUserId = "@alice:example.org";
|
||||
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
client.supportsThreads = jest.fn().mockReturnValue(true);
|
||||
|
||||
// Force server-side support so threads start uninitialised
|
||||
const prevSupport = Thread.hasServerSideSupport;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
|
||||
try {
|
||||
const room = new Room("!room:z", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
|
||||
// Create a root event and thread
|
||||
const rootEvent = mkMessage({ room: room.roomId, user: myUserId, msg: "Root", event: true });
|
||||
const thread = new Thread(rootEvent.getId()!, rootEvent, {
|
||||
room,
|
||||
client,
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
expect(thread.initialEventsFetched).toBe(false);
|
||||
|
||||
// A message inside the thread to react to
|
||||
const originalMessage = mkMessage({
|
||||
room: room.roomId,
|
||||
user: myUserId,
|
||||
msg: "Thread message",
|
||||
relatesTo: {
|
||||
"rel_type": THREAD_RELATION_TYPE.name,
|
||||
"event_id": thread.id,
|
||||
"m.in_reply_to": { event_id: thread.id },
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
|
||||
// Create 2 reactions before the message is in the timeline (pre-init)
|
||||
const reaction1 = mkReaction(originalMessage, client, myUserId, room.roomId);
|
||||
const reaction2 = mkReaction(originalMessage, client, myUserId, room.roomId);
|
||||
|
||||
// Add reactions while thread is NOT initialised
|
||||
thread.addEvent(reaction1, false);
|
||||
thread.addEvent(reaction2, false);
|
||||
|
||||
// Relations should already include the reactions pre-init
|
||||
const relsBefore = thread.timelineSet.relations!.getChildEventsForEvent(
|
||||
originalMessage.getId()!,
|
||||
RelationType.Annotation,
|
||||
EventType.Reaction,
|
||||
)!;
|
||||
expect(relsBefore).toBeTruthy();
|
||||
const beforeIds = new Set(relsBefore.getRelations().map((e) => e.getId()));
|
||||
expect(beforeIds.size).toBe(2);
|
||||
|
||||
// Now initialise and replay
|
||||
// Ensure reactions are queued for replay as well
|
||||
expect(thread.replayEvents).toHaveLength(2);
|
||||
const replay = [...(thread.replayEvents || [])];
|
||||
thread.replayEvents = [];
|
||||
thread.initialEventsFetched = true;
|
||||
|
||||
// Add the original message first so it becomes findable
|
||||
thread.addEvent(originalMessage, false);
|
||||
// Replay reactions
|
||||
for (const ev of replay) thread.addEvent(ev, false);
|
||||
|
||||
// Ensure no duplicates after replay (idempotent aggregation)
|
||||
const relsAfter = thread.timelineSet.relations!.getChildEventsForEvent(
|
||||
originalMessage.getId()!,
|
||||
RelationType.Annotation,
|
||||
EventType.Reaction,
|
||||
)!;
|
||||
const afterIds = new Set(relsAfter.getRelations().map((e) => e.getId()));
|
||||
expect(afterIds.size).toBe(2);
|
||||
expect(afterIds).toEqual(beforeIds);
|
||||
} finally {
|
||||
// restore
|
||||
Thread.setServerSideSupport(prevSupport);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -164,6 +164,23 @@ describe("oidc authorization", () => {
|
||||
|
||||
expect(authUrl.searchParams.get("prompt")).toEqual("create");
|
||||
});
|
||||
|
||||
it("should generate url with login_hint", async () => {
|
||||
const nonce = "abc123";
|
||||
|
||||
const authUrl = new URL(
|
||||
await generateOidcAuthorizationUrl({
|
||||
metadata: delegatedAuthConfig,
|
||||
homeserverUrl: baseUrl,
|
||||
clientId,
|
||||
redirectUri: baseUrl,
|
||||
nonce,
|
||||
loginHint: "login1234",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(authUrl.searchParams.get("login_hint")).toEqual("login1234");
|
||||
});
|
||||
});
|
||||
|
||||
describe("completeAuthorizationCodeGrant", () => {
|
||||
|
||||
+20
-146
@@ -20,8 +20,8 @@ import * as utils from "../test-utils/test-utils";
|
||||
import { RoomMember, RoomMemberEvent } from "../../src/models/room-member";
|
||||
import {
|
||||
createClient,
|
||||
EventType,
|
||||
type MatrixClient,
|
||||
MatrixEvent,
|
||||
type RoomState,
|
||||
UNSTABLE_MSC2666_MUTUAL_ROOMS,
|
||||
UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS,
|
||||
@@ -101,158 +101,32 @@ describe("RoomMember", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("setPowerLevelEvent", function () {
|
||||
it("should set 'powerLevel' and 'powerLevelNorm'.", function () {
|
||||
const event = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@bertha:bar": 200,
|
||||
"@invalid:user": 10, // shouldn't barf on this.
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
member.setPowerLevelEvent(event);
|
||||
expect(member.powerLevel).toEqual(20);
|
||||
expect(member.powerLevelNorm).toEqual(10);
|
||||
|
||||
const memberB = new RoomMember(roomId, userB);
|
||||
memberB.setPowerLevelEvent(event);
|
||||
expect(memberB.powerLevel).toEqual(200);
|
||||
expect(memberB.powerLevelNorm).toEqual(100);
|
||||
});
|
||||
|
||||
it("should emit 'RoomMember.powerLevel' if the power level changes.", function () {
|
||||
const event = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@bertha:bar": 200,
|
||||
"@invalid:user": 10, // shouldn't barf on this.
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
let emitCount = 0;
|
||||
|
||||
member.on(RoomMemberEvent.PowerLevel, function (emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
expect(emitMember).toEqual(member);
|
||||
expect(emitEvent).toEqual(event);
|
||||
});
|
||||
|
||||
member.setPowerLevelEvent(event);
|
||||
expect(emitCount).toEqual(1);
|
||||
member.setPowerLevelEvent(event); // no-op
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
|
||||
it("should honour power levels of zero.", function () {
|
||||
const event = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": 0,
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
let emitCount = 0;
|
||||
|
||||
// set the power level to something other than zero or we
|
||||
// won't get an event
|
||||
member.powerLevel = 1;
|
||||
member.on(RoomMemberEvent.PowerLevel, function (emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
expect(emitMember.userId).toEqual("@alice:bar");
|
||||
expect(emitMember.powerLevel).toEqual(0);
|
||||
expect(emitEvent).toEqual(event);
|
||||
});
|
||||
|
||||
member.setPowerLevelEvent(event);
|
||||
describe("setPowerLevel", function () {
|
||||
it("should set 'powerLevel'.", function () {
|
||||
member.setPowerLevel(0, new MatrixEvent());
|
||||
expect(member.powerLevel).toEqual(0);
|
||||
expect(emitCount).toEqual(1);
|
||||
member.setPowerLevel(200, new MatrixEvent());
|
||||
expect(member.powerLevel).toEqual(200);
|
||||
});
|
||||
|
||||
it("should not honor string power levels.", function () {
|
||||
const event = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": "5",
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
let emitCount = 0;
|
||||
it("should emit when power level set", function () {
|
||||
const onEmit = jest.fn();
|
||||
member.on(RoomMemberEvent.PowerLevel, onEmit);
|
||||
|
||||
member.on(RoomMemberEvent.PowerLevel, function (emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
expect(emitMember.userId).toEqual("@alice:bar");
|
||||
expect(emitMember.powerLevel).toEqual(20);
|
||||
expect(emitEvent).toEqual(event);
|
||||
});
|
||||
const aMatrixEvent = new MatrixEvent();
|
||||
member.setPowerLevel(10, aMatrixEvent);
|
||||
|
||||
member.setPowerLevelEvent(event);
|
||||
expect(member.powerLevel).toEqual(20);
|
||||
expect(emitCount).toEqual(1);
|
||||
expect(onEmit).toHaveBeenCalledWith(aMatrixEvent, member);
|
||||
});
|
||||
|
||||
it("should no-op if given a non-state or unrelated event", () => {
|
||||
const fn = jest.spyOn(member, "emit");
|
||||
expect(fn).not.toHaveBeenCalledWith(RoomMemberEvent.PowerLevel);
|
||||
member.setPowerLevelEvent(
|
||||
utils.mkEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": "5",
|
||||
},
|
||||
},
|
||||
skey: "invalid",
|
||||
event: true,
|
||||
}),
|
||||
);
|
||||
const nonStateEv = utils.mkEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": "5",
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
delete nonStateEv.event.state_key;
|
||||
member.setPowerLevelEvent(nonStateEv);
|
||||
member.setPowerLevelEvent(
|
||||
utils.mkEvent({
|
||||
type: EventType.Sticker,
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {},
|
||||
event: true,
|
||||
}),
|
||||
);
|
||||
expect(fn).not.toHaveBeenCalledWith(RoomMemberEvent.PowerLevel);
|
||||
it("should not emit if new power level is the same", function () {
|
||||
const onEmit = jest.fn();
|
||||
member.on(RoomMemberEvent.PowerLevel, onEmit);
|
||||
|
||||
const aMatrixEvent = new MatrixEvent();
|
||||
member.setPowerLevel(0, aMatrixEvent);
|
||||
|
||||
expect(onEmit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import * as utils from "../test-utils/test-utils";
|
||||
import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
|
||||
import { filterEmitCallsByEventType } from "../test-utils/emitter";
|
||||
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
|
||||
import { RoomMemberEvent } from "../../src/models/room-member";
|
||||
import { type Beacon, BeaconEvent, getBeaconInfoIdentifier } from "../../src/models/beacon";
|
||||
import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@types/event";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
@@ -38,10 +39,10 @@ describe("RoomState", function () {
|
||||
const userLazy = "@lazy:bar";
|
||||
|
||||
let state = new RoomState(roomId);
|
||||
let statev12 = new RoomState(roomId);
|
||||
|
||||
beforeEach(function () {
|
||||
state = new RoomState(roomId);
|
||||
state.setStateEvents([
|
||||
const commonEvents = [
|
||||
utils.mkMembership({
|
||||
// userA joined
|
||||
event: true,
|
||||
@@ -66,6 +67,11 @@ describe("RoomState", function () {
|
||||
name: "Room name goes here",
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
state = new RoomState(roomId);
|
||||
state.setStateEvents([
|
||||
...commonEvents,
|
||||
utils.mkEvent({
|
||||
// Room creation
|
||||
type: "m.room.create",
|
||||
@@ -75,6 +81,19 @@ describe("RoomState", function () {
|
||||
content: {},
|
||||
}),
|
||||
]);
|
||||
|
||||
statev12 = new RoomState(roomId);
|
||||
statev12.setStateEvents([
|
||||
...commonEvents,
|
||||
utils.mkEvent({
|
||||
// Room creation (v12 version)
|
||||
type: "m.room.create",
|
||||
user: userA,
|
||||
room: roomId,
|
||||
event: true,
|
||||
content: { room_version: "12" },
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
describe("getMembers", function () {
|
||||
@@ -259,7 +278,7 @@ describe("RoomState", function () {
|
||||
expect(emitCount).toEqual(2);
|
||||
});
|
||||
|
||||
it("should call setPowerLevelEvent on each RoomMember for m.room.power_levels", function () {
|
||||
it("should call setPowerLevel on each RoomMember for m.room.power_levels", function () {
|
||||
const powerLevelEvent = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
@@ -273,12 +292,12 @@ describe("RoomState", function () {
|
||||
});
|
||||
|
||||
// spy on the room members
|
||||
jest.spyOn(state.members[userA], "setPowerLevelEvent");
|
||||
jest.spyOn(state.members[userB], "setPowerLevelEvent");
|
||||
jest.spyOn(state.members[userA], "setPowerLevel");
|
||||
jest.spyOn(state.members[userB], "setPowerLevel");
|
||||
state.setStateEvents([powerLevelEvent]);
|
||||
|
||||
expect(state.members[userA].setPowerLevelEvent).toHaveBeenCalledWith(powerLevelEvent);
|
||||
expect(state.members[userB].setPowerLevelEvent).toHaveBeenCalledWith(powerLevelEvent);
|
||||
expect(state.members[userA].setPowerLevel).toHaveBeenCalledWith(10, powerLevelEvent);
|
||||
expect(state.members[userB].setPowerLevel).toHaveBeenCalledWith(10, powerLevelEvent);
|
||||
});
|
||||
|
||||
it("should call setPowerLevelEvent on a new RoomMember if power levels exist", function () {
|
||||
@@ -310,6 +329,156 @@ describe("RoomState", function () {
|
||||
expect(state.members[userC].powerLevel).toEqual(10);
|
||||
});
|
||||
|
||||
it("should calculate power level correctly", function () {
|
||||
const powerLevelEvent = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
[userB]: 200,
|
||||
"@invalid:user": 10, // shouldn't barf on this.
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
state.setStateEvents([powerLevelEvent]);
|
||||
|
||||
expect(state.getMember(userA)?.powerLevel).toEqual(20);
|
||||
expect(state.getMember(userB)?.powerLevel).toEqual(200);
|
||||
});
|
||||
|
||||
it("should set 'powerLevel' with a v12 room.", function () {
|
||||
const createEventV12 = utils.mkEvent({
|
||||
type: "m.room.create",
|
||||
room: roomId,
|
||||
sender: userA,
|
||||
content: { room_version: "12" },
|
||||
event: true,
|
||||
});
|
||||
const powerLevelEvent = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
[userB]: 200,
|
||||
"@invalid:user": 10, // shouldn't barf on this.
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
state.setStateEvents([createEventV12, powerLevelEvent]);
|
||||
expect(state.getMember(userA)?.powerLevel).toEqual(Infinity);
|
||||
});
|
||||
|
||||
it("should honour power levels of zero.", function () {
|
||||
const powerLevelEvent = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": 0,
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
let emitCount = 0;
|
||||
|
||||
const memberA = state.getMember(userA)!;
|
||||
// set the power level to something other than zero or we
|
||||
// won't get an event
|
||||
memberA.powerLevel = 1;
|
||||
memberA.on(RoomMemberEvent.PowerLevel, function (emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
expect(emitMember.userId).toEqual("@alice:bar");
|
||||
expect(emitMember.powerLevel).toEqual(0);
|
||||
expect(emitEvent).toEqual(powerLevelEvent);
|
||||
});
|
||||
|
||||
state.setStateEvents([powerLevelEvent]);
|
||||
expect(memberA.powerLevel).toEqual(0);
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
|
||||
it("should not honor string power levels.", function () {
|
||||
const powerLevelEvent = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": "5",
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
let emitCount = 0;
|
||||
|
||||
const memberA = state.getMember(userA)!;
|
||||
memberA.on(RoomMemberEvent.PowerLevel, function (emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
expect(emitMember.userId).toEqual("@alice:bar");
|
||||
expect(emitMember.powerLevel).toEqual(20);
|
||||
expect(emitEvent).toEqual(powerLevelEvent);
|
||||
});
|
||||
|
||||
state.setStateEvents([powerLevelEvent]);
|
||||
expect(memberA.powerLevel).toEqual(20);
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
|
||||
it("should no-op if given a non-state or unrelated event", () => {
|
||||
const memberA = state.getMember(userA)!;
|
||||
const fn = jest.spyOn(memberA, "emit");
|
||||
expect(fn).not.toHaveBeenCalledWith(RoomMemberEvent.PowerLevel);
|
||||
|
||||
const powerLevelEvent = utils.mkEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": "5",
|
||||
},
|
||||
},
|
||||
skey: "invalid",
|
||||
event: true,
|
||||
});
|
||||
|
||||
state.setStateEvents([powerLevelEvent]);
|
||||
const nonStateEv = utils.mkEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": "5",
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
delete nonStateEv.event.state_key;
|
||||
state.setStateEvents([nonStateEv]);
|
||||
state.setStateEvents([
|
||||
utils.mkEvent({
|
||||
type: EventType.Sticker,
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {},
|
||||
event: true,
|
||||
}),
|
||||
]);
|
||||
expect(fn).not.toHaveBeenCalledWith(RoomMemberEvent.PowerLevel);
|
||||
});
|
||||
|
||||
it("should call setMembershipEvent on the right RoomMember", function () {
|
||||
const memberEvent = utils.mkMembership({
|
||||
user: userB,
|
||||
@@ -851,6 +1020,24 @@ describe("RoomState", function () {
|
||||
expect(state.maySendEvent("m.room.other_thing", userA)).toEqual(true);
|
||||
expect(state.maySendEvent("m.room.other_thing", userB)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should recognise power level of room creators in v12 rooms", function () {
|
||||
const powerLevelEvent = new MatrixEvent({
|
||||
type: "m.room.power_levels",
|
||||
room_id: roomId,
|
||||
sender: userA,
|
||||
state_key: "",
|
||||
content: {
|
||||
users_default: 0,
|
||||
state_default: 100,
|
||||
events_default: 100,
|
||||
users: {},
|
||||
},
|
||||
});
|
||||
statev12.setStateEvents([powerLevelEvent]);
|
||||
|
||||
expect(statev12.maySendEvent("m.room.name", userA)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("processBeaconEvents", () => {
|
||||
|
||||
@@ -2353,6 +2353,7 @@ describe("RustCrypto", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
mockOlmMachine = {
|
||||
queryKeysForUsers: jest.fn().mockReturnValue({}),
|
||||
getReceivedRoomKeyBundleData: jest.fn(),
|
||||
receiveRoomKeyBundle: jest.fn(),
|
||||
} as unknown as Mocked<OlmMachine>;
|
||||
@@ -2377,6 +2378,7 @@ describe("RustCrypto", () => {
|
||||
it("does nothing if there is no key bundle", async () => {
|
||||
mockOlmMachine.getReceivedRoomKeyBundleData.mockResolvedValue(undefined);
|
||||
await rustCrypto.maybeAcceptKeyBundle("!room_id", "@bob:example.org");
|
||||
expect(mockOlmMachine.queryKeysForUsers).toHaveBeenCalledTimes(1);
|
||||
expect(mockOlmMachine.getReceivedRoomKeyBundleData).toHaveBeenCalledTimes(1);
|
||||
expect(mockOlmMachine.getReceivedRoomKeyBundleData.mock.calls[0][0].toString()).toEqual("!room_id");
|
||||
expect(mockOlmMachine.getReceivedRoomKeyBundleData.mock.calls[0][1].toString()).toEqual("@bob:example.org");
|
||||
|
||||
@@ -156,6 +156,8 @@ describe("VerificationRequest", () => {
|
||||
);
|
||||
|
||||
const verificationRequestContent = JSON.parse(await bobUserIdentity.verificationRequestContent(methods));
|
||||
todoFixupVerificationRequestContent(verificationRequestContent);
|
||||
|
||||
await bobOlmMachine.receiveVerificationEvent(
|
||||
JSON.stringify({
|
||||
type: "m.room.message",
|
||||
@@ -289,6 +291,8 @@ describe("VerificationRequest", () => {
|
||||
);
|
||||
|
||||
const verificationRequestContent = JSON.parse(await bobUserIdentity.verificationRequestContent(methods));
|
||||
todoFixupVerificationRequestContent(verificationRequestContent);
|
||||
|
||||
await bobOlmMachine.receiveVerificationEvent(
|
||||
JSON.stringify({
|
||||
type: "m.room.message",
|
||||
@@ -405,6 +409,8 @@ describe("VerificationRequest", () => {
|
||||
);
|
||||
|
||||
const verificationRequestContent = JSON.parse(await bobUserIdentity.verificationRequestContent(methods));
|
||||
todoFixupVerificationRequestContent(verificationRequestContent);
|
||||
|
||||
await bobOlmMachine.receiveVerificationEvent(
|
||||
JSON.stringify({
|
||||
type: "m.room.message",
|
||||
@@ -460,6 +466,15 @@ describe("VerificationRequest", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Needed until https://github.com/matrix-org/matrix-rust-sdk/issues/5643 is fixed.
|
||||
*
|
||||
* Modify the content of the supplied content to include `msgtype: m.key.verification.request`.
|
||||
*/
|
||||
function todoFixupVerificationRequestContent(content: any) {
|
||||
content.msgtype = "m.key.verification.request";
|
||||
}
|
||||
|
||||
describe("isVerificationEvent", () => {
|
||||
it.each([
|
||||
[EventType.KeyVerificationCancel],
|
||||
|
||||
@@ -571,6 +571,49 @@ describe("SyncAccumulator", function () {
|
||||
expect(sa.getJSON().roomsData.knock["!knock:bar"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should delete knock state when room transitions from knock to join", () => {
|
||||
const initKnockState = makeKnockState();
|
||||
sa.accumulate(
|
||||
syncSkeleton(
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{
|
||||
knock_state: initKnockState,
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(sa.getJSON().roomsData.knock["!knock:bar"].knock_state).toBe(initKnockState);
|
||||
|
||||
// Room transitions from knock to join (e.g., after approval and joining)
|
||||
const joinState = {
|
||||
account_data: { events: [] },
|
||||
ephemeral: { events: [] },
|
||||
unread_notifications: {},
|
||||
state: {
|
||||
events: [member("bob", KnownMembership.Join)],
|
||||
},
|
||||
};
|
||||
|
||||
const syncResponse = {
|
||||
next_batch: "abc",
|
||||
rooms: {
|
||||
join: {
|
||||
"!knock:bar": joinState,
|
||||
},
|
||||
invite: {},
|
||||
leave: {},
|
||||
},
|
||||
} as unknown as ISyncResponse;
|
||||
|
||||
sa.accumulate(syncResponse);
|
||||
|
||||
expect(sa.getJSON().roomsData.knock["!knock:bar"]).toBeUndefined();
|
||||
expect(sa.getJSON().roomsData.join["!knock:bar"].state?.events[0]?.content.membership).toEqual(
|
||||
KnownMembership.Join,
|
||||
);
|
||||
});
|
||||
|
||||
it("should accumulate read receipts", () => {
|
||||
const receipt1 = {
|
||||
type: "m.receipt",
|
||||
|
||||
+3
-1
@@ -53,6 +53,7 @@ import {
|
||||
} from "../webrtc/callEventTypes.ts";
|
||||
import {
|
||||
type IRTCNotificationContent,
|
||||
type IRTCDeclineContent,
|
||||
type EncryptionKeysEventContent,
|
||||
type ICallNotifyContent,
|
||||
} from "../matrixrtc/types.ts";
|
||||
@@ -152,6 +153,7 @@ export enum EventType {
|
||||
// MatrixRTC events
|
||||
CallNotify = "org.matrix.msc4075.call.notify",
|
||||
RTCNotification = "org.matrix.msc4075.rtc.notification",
|
||||
RTCDecline = "org.matrix.msc4310.rtc.decline",
|
||||
}
|
||||
|
||||
export enum RelationType {
|
||||
@@ -163,7 +165,6 @@ export enum RelationType {
|
||||
// moreover, our tests currently use the unstable prefix. Use THREAD_RELATION_TYPE.name.
|
||||
// Once we support *only* the stable prefix, THREAD_RELATION_TYPE can die and we can switch to this.
|
||||
Thread = "m.thread",
|
||||
unstable_RTCNotificationParent = "org.matrix.msc4075.rtc.notification.parent",
|
||||
}
|
||||
|
||||
export enum MsgType {
|
||||
@@ -332,6 +333,7 @@ export interface TimelineEvents {
|
||||
[EventType.CallEncryptionKeysPrefix]: EncryptionKeysEventContent;
|
||||
[EventType.CallNotify]: ICallNotifyContent;
|
||||
[EventType.RTCNotification]: IRTCNotificationContent;
|
||||
[EventType.RTCDecline]: IRTCDeclineContent;
|
||||
[M_BEACON.name]: MBeaconEventContent;
|
||||
[M_POLL_START.name]: PollStartEventContent;
|
||||
[M_POLL_END.name]: PollEndEventContent;
|
||||
|
||||
@@ -27,11 +27,6 @@ import { type EventType, type RelationType, type RoomType } from "./event.ts";
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
export interface IJoinRoomOpts {
|
||||
/**
|
||||
* @deprecated does nothing
|
||||
*/
|
||||
syncRoom?: boolean;
|
||||
|
||||
/**
|
||||
* If the caller has a keypair 3pid invite, the signing URL is passed in this parameter.
|
||||
*/
|
||||
|
||||
+9
-2
@@ -47,12 +47,19 @@ export const M_TOPIC = new NamespacedValue("m.topic");
|
||||
/**
|
||||
* The event content for an m.topic event (in content)
|
||||
*/
|
||||
export type MTopicContent = IMessageRendering[];
|
||||
export type MTopicContent = { "m.text": IMessageRendering[] };
|
||||
|
||||
/**
|
||||
* A previous incorrect form of m.topic used by matrix-js-sdk
|
||||
* TODO remove this after a few releases
|
||||
* https://github.com/matrix-org/matrix-js-sdk/pull/4984#pullrequestreview-3174251065
|
||||
*/
|
||||
export type MalformedMTopicEvent = { "m.topic": IMessageRendering[] };
|
||||
|
||||
/**
|
||||
* The event definition for an m.topic event (in content)
|
||||
*/
|
||||
export type MTopicEvent = { "m.topic": MTopicContent };
|
||||
export type MTopicEvent = { "m.topic": MTopicContent } | MalformedMTopicEvent;
|
||||
|
||||
/**
|
||||
* The event content for an m.room.topic event
|
||||
|
||||
@@ -22,9 +22,3 @@ import { type AuthDict } from "../interactive-auth.ts";
|
||||
export type UIARequest<T> = T & {
|
||||
auth?: AuthDict;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper type to represent HTTP response body for a UIA enabled endpoint
|
||||
* @deprecated - a successful response for a UIA enabled endpoint is no different, UIA is signalled via an error
|
||||
*/
|
||||
export type UIAResponse<T> = T;
|
||||
|
||||
+25
-49
@@ -542,6 +542,7 @@ export const UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS = "uk.half-shot.msc2666.query_m
|
||||
export const UNSTABLE_MSC4140_DELAYED_EVENTS = "org.matrix.msc4140";
|
||||
|
||||
export const UNSTABLE_MSC4133_EXTENDED_PROFILES = "uk.tcpip.msc4133";
|
||||
export const STABLE_MSC4133_EXTENDED_PROFILES = "uk.tcpip.msc4133.stable";
|
||||
|
||||
enum CrossSigningKeyType {
|
||||
MasterKey = "master_key",
|
||||
@@ -1085,20 +1086,6 @@ export enum ClientEvent {
|
||||
*/
|
||||
ClientWellKnown = "WellKnown.client",
|
||||
ReceivedVoipEvent = "received_voip_event",
|
||||
/**
|
||||
* @deprecated This event is not supported anymore.
|
||||
*
|
||||
* Fires if a to-device event is received that cannot be decrypted.
|
||||
* Encrypted to-device events will (generally) use plain Olm encryption,
|
||||
* in which case decryption failures are fatal: the event will never be
|
||||
* decryptable, unlike Megolm encrypted events where the key may simply
|
||||
* arrive later.
|
||||
*
|
||||
* An undecryptable to-device event is therefore likely to indicate problems.
|
||||
*
|
||||
* The payload is the undecyptable to-device event
|
||||
*/
|
||||
UndecryptableToDeviceEvent = "toDeviceEvent.undecryptable",
|
||||
TurnServers = "turnServers",
|
||||
TurnServersError = "turnServers.error",
|
||||
}
|
||||
@@ -1163,7 +1150,6 @@ export type ClientEventHandlerMap = {
|
||||
[ClientEvent.Event]: (event: MatrixEvent) => void;
|
||||
[ClientEvent.ToDeviceEvent]: (event: MatrixEvent) => void;
|
||||
[ClientEvent.ReceivedToDeviceMessage]: (payload: ReceivedToDeviceMessage) => void;
|
||||
[ClientEvent.UndecryptableToDeviceEvent]: (event: MatrixEvent) => void;
|
||||
[ClientEvent.AccountData]: (event: MatrixEvent, lastEvent?: MatrixEvent) => void;
|
||||
[ClientEvent.Room]: (room: Room) => void;
|
||||
[ClientEvent.DeleteRoom]: (roomId: string) => void;
|
||||
@@ -2155,21 +2141,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
public getVisibleRooms(msc3946ProcessDynamicPredecessor = false): Room[] {
|
||||
const allRooms = this.store.getRooms();
|
||||
|
||||
const replacedRooms = new Set();
|
||||
for (const r of allRooms) {
|
||||
const predecessor = r.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
|
||||
if (predecessor) {
|
||||
replacedRooms.add(predecessor);
|
||||
const visibleRooms = new Set(allRooms);
|
||||
for (const room of visibleRooms) {
|
||||
const predecessors = this.findPredecessorRooms(room, true, msc3946ProcessDynamicPredecessor);
|
||||
for (const predecessor of predecessors) {
|
||||
visibleRooms.delete(predecessor);
|
||||
}
|
||||
}
|
||||
|
||||
return allRooms.filter((r) => {
|
||||
const tombstone = r.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
if (tombstone && replacedRooms.has(r.roomId)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return Array.from(visibleRooms);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3604,6 +3583,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return await this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId, rpEventId);
|
||||
}
|
||||
|
||||
public sendRtcDecline(roomId: string, notificationEventId: string): Promise<ISendEventResponse> {
|
||||
return this.sendEvent(roomId, EventType.RTCDecline, {
|
||||
"m.relates_to": { event_id: notificationEventId, rel_type: RelationType.Reference },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a preview of the given URL as of (roughly) the given point in time,
|
||||
* described as an object with OpenGraph keys and associated values.
|
||||
@@ -3860,7 +3845,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
roomId: string,
|
||||
includeFuture = true,
|
||||
): Promise<{ [roomId: string]: Error | MatrixError | null }> {
|
||||
const upgradeHistory = this.getRoomUpgradeHistory(roomId);
|
||||
const upgradeHistory = this.getRoomUpgradeHistory(roomId, true);
|
||||
|
||||
let eligibleToLeave = upgradeHistory;
|
||||
if (!includeFuture) {
|
||||
@@ -6862,9 +6847,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @param opts - options object
|
||||
*
|
||||
* @returns Promise which resolves to response object, as
|
||||
* determined by this.opts.onlyData, opts.rawResponse, and
|
||||
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
|
||||
* @returns Promise which resolves to response object, or rejects with an error (usually a MatrixError).
|
||||
*/
|
||||
public uploadContent(file: FileType, opts?: UploadOpts): Promise<UploadResponse> {
|
||||
return this.http.uploadContent(file, opts);
|
||||
@@ -6914,7 +6897,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns `true` if supported, otherwise `false`
|
||||
*/
|
||||
public async doesServerSupportExtendedProfiles(): Promise<boolean> {
|
||||
return this.doesServerSupportUnstableFeature(UNSTABLE_MSC4133_EXTENDED_PROFILES);
|
||||
return (
|
||||
(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4133_EXTENDED_PROFILES)) ||
|
||||
(await this.doesServerSupportUnstableFeature(STABLE_MSC4133_EXTENDED_PROFILES))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8417,21 +8403,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the OIDC issuer responsible for authentication on this server, if any
|
||||
* @returns Resolves: A promise of an object containing the OIDC issuer if configured
|
||||
* @returns Rejects: when the request fails (module:http-api.MatrixError)
|
||||
* @experimental - part of MSC2965
|
||||
* @deprecated in favour of getAuthMetadata
|
||||
*/
|
||||
public async getAuthIssuer(): Promise<{
|
||||
issuer: string;
|
||||
}> {
|
||||
return this.http.request(Method.Get, "/auth_issuer", undefined, undefined, {
|
||||
prefix: ClientPrefix.Unstable + "/org.matrix.msc2965",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover and validate delegated auth configuration
|
||||
* - delegated auth issuer openid-configuration is reachable
|
||||
@@ -8451,7 +8422,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof MatrixError && e.errcode === "M_UNRECOGNIZED") {
|
||||
const { issuer } = await this.getAuthIssuer();
|
||||
// Fall back to older variant of MSC2965
|
||||
const { issuer } = await this.http.request<{
|
||||
issuer: string;
|
||||
}>(Method.Get, "/auth_issuer", undefined, undefined, {
|
||||
prefix: ClientPrefix.Unstable + "/org.matrix.msc2965",
|
||||
});
|
||||
return discoverAndValidateOIDCIssuerWellKnown(issuer);
|
||||
}
|
||||
throw e;
|
||||
|
||||
@@ -197,7 +197,7 @@ export const makeTopicContent: MakeTopicContent = (topic, htmlTopic) => {
|
||||
if (isProvided(topic)) {
|
||||
renderings.push({ body: topic, mimetype: "text/plain" });
|
||||
}
|
||||
return { topic, [M_TOPIC.name]: renderings };
|
||||
return { topic, [M_TOPIC.name]: { "m.text": renderings } };
|
||||
};
|
||||
|
||||
export type TopicState = {
|
||||
@@ -206,7 +206,11 @@ export type TopicState = {
|
||||
};
|
||||
|
||||
export const parseTopicContent = (content: MRoomTopicEventContent): TopicState => {
|
||||
const mtopic = M_TOPIC.findIn<MTopicContent>(content);
|
||||
const mtopicParent = M_TOPIC.findIn<MTopicContent>(content);
|
||||
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
|
||||
//const mtopic = M_TOPIC.findIn<MTopicContent>(content)?.["m.text"];
|
||||
if (!Array.isArray(mtopic)) {
|
||||
return { text: content.topic ?? undefined };
|
||||
}
|
||||
|
||||
@@ -468,16 +468,6 @@ export interface CryptoApi {
|
||||
*/
|
||||
getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[];
|
||||
|
||||
/**
|
||||
* Finds a DM verification request that is already in progress for the given room id
|
||||
*
|
||||
* @param roomId - the room to use for verification
|
||||
*
|
||||
* @returns the VerificationRequest that is in progress, if any
|
||||
* @deprecated prefer `userId` parameter variant.
|
||||
*/
|
||||
findVerificationRequestDMInProgress(roomId: string): VerificationRequest | undefined;
|
||||
|
||||
/**
|
||||
* Finds a DM verification request that is already in progress for the given room and user.
|
||||
*
|
||||
@@ -545,18 +535,6 @@ export interface CryptoApi {
|
||||
*/
|
||||
getSessionBackupPrivateKey(): Promise<Uint8Array | null>;
|
||||
|
||||
/**
|
||||
* Store the backup decryption key.
|
||||
*
|
||||
* This should be called if the client has received the key from another device via secret sharing (gossiping).
|
||||
* It is the responsability of the caller to check that the decryption key is valid for the current backup version.
|
||||
*
|
||||
* @param key - the backup decryption key
|
||||
*
|
||||
* @deprecated prefer the variant with a `version` parameter.
|
||||
*/
|
||||
storeSessionBackupPrivateKey(key: Uint8Array): Promise<void>;
|
||||
|
||||
/**
|
||||
* Store the backup decryption key.
|
||||
*
|
||||
@@ -801,45 +779,6 @@ export enum DecryptionFailureCode {
|
||||
|
||||
/** Unknown or unclassified error. */
|
||||
UNKNOWN_ERROR = "UNKNOWN_ERROR",
|
||||
|
||||
/** @deprecated only used in legacy crypto */
|
||||
MEGOLM_BAD_ROOM = "MEGOLM_BAD_ROOM",
|
||||
|
||||
/** @deprecated only used in legacy crypto */
|
||||
MEGOLM_MISSING_FIELDS = "MEGOLM_MISSING_FIELDS",
|
||||
|
||||
/** @deprecated only used in legacy crypto */
|
||||
OLM_DECRYPT_GROUP_MESSAGE_ERROR = "OLM_DECRYPT_GROUP_MESSAGE_ERROR",
|
||||
|
||||
/** @deprecated only used in legacy crypto */
|
||||
OLM_BAD_ENCRYPTED_MESSAGE = "OLM_BAD_ENCRYPTED_MESSAGE",
|
||||
|
||||
/** @deprecated only used in legacy crypto */
|
||||
OLM_BAD_RECIPIENT = "OLM_BAD_RECIPIENT",
|
||||
|
||||
/** @deprecated only used in legacy crypto */
|
||||
OLM_BAD_RECIPIENT_KEY = "OLM_BAD_RECIPIENT_KEY",
|
||||
|
||||
/** @deprecated only used in legacy crypto */
|
||||
OLM_BAD_ROOM = "OLM_BAD_ROOM",
|
||||
|
||||
/** @deprecated only used in legacy crypto */
|
||||
OLM_BAD_SENDER_CHECK_FAILED = "OLM_BAD_SENDER_CHECK_FAILED",
|
||||
|
||||
/** @deprecated only used in legacy crypto */
|
||||
OLM_BAD_SENDER = "OLM_BAD_SENDER",
|
||||
|
||||
/** @deprecated only used in legacy crypto */
|
||||
OLM_FORWARDED_MESSAGE = "OLM_FORWARDED_MESSAGE",
|
||||
|
||||
/** @deprecated only used in legacy crypto */
|
||||
OLM_MISSING_CIPHERTEXT = "OLM_MISSING_CIPHERTEXT",
|
||||
|
||||
/** @deprecated only used in legacy crypto */
|
||||
OLM_NOT_INCLUDED_IN_RECIPIENTS = "OLM_NOT_INCLUDED_IN_RECIPIENTS",
|
||||
|
||||
/** @deprecated only used in legacy crypto */
|
||||
UNKNOWN_ENCRYPTION_ALGORITHM = "UNKNOWN_ENCRYPTION_ALGORITHM",
|
||||
}
|
||||
|
||||
/** Base {@link DeviceIsolationMode} kind. */
|
||||
@@ -1104,8 +1043,6 @@ export type ImportRoomKeyProgressData = ImportRoomKeyFetchProgress | ImportRoomK
|
||||
export interface ImportRoomKeysOpts {
|
||||
/** Reports ongoing progress of the import process. Can be used for feedback. */
|
||||
progressCallback?: (stage: ImportRoomKeyProgressData) => void;
|
||||
/** @deprecated the rust SDK will always such imported keys as untrusted */
|
||||
untrusted?: boolean;
|
||||
/** @deprecated not useful externally */
|
||||
source?: string;
|
||||
}
|
||||
@@ -1193,13 +1130,6 @@ export interface CryptoCallbacks {
|
||||
name: string,
|
||||
) => Promise<[string, Uint8Array] | null>;
|
||||
|
||||
/** @deprecated: unused with the Rust crypto stack. */
|
||||
getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array | null>;
|
||||
/** @deprecated: unused with the Rust crypto stack. */
|
||||
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
|
||||
/** @deprecated: unused with the Rust crypto stack. */
|
||||
shouldUpgradeDeviceVerifications?: (users: Record<string, any>) => Promise<string[]>;
|
||||
|
||||
/**
|
||||
* Called by {@link CryptoApi.bootstrapSecretStorage} when a new default secret storage key is created.
|
||||
*
|
||||
@@ -1211,24 +1141,6 @@ export interface CryptoCallbacks {
|
||||
* @param key - private key to store
|
||||
*/
|
||||
cacheSecretStorageKey?: (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => void;
|
||||
|
||||
/** @deprecated: unused with the Rust crypto stack. */
|
||||
onSecretRequested?: (
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
requestId: string,
|
||||
secretName: string,
|
||||
deviceTrust: DeviceVerificationStatus,
|
||||
) => Promise<string | undefined>;
|
||||
|
||||
/** @deprecated: unused with the Rust crypto stack. */
|
||||
getDehydrationKey?: (
|
||||
keyInfo: SecretStorageKeyDescription,
|
||||
checkFunc: (key: Uint8Array) => void,
|
||||
) => Promise<Uint8Array>;
|
||||
|
||||
/** @deprecated: unused with the Rust crypto stack. */
|
||||
getBackupKey?: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1243,13 +1155,6 @@ export interface CreateSecretStorageOpts {
|
||||
*/
|
||||
createSecretStorageKey?: () => Promise<GeneratedSecretStorageKey>;
|
||||
|
||||
/**
|
||||
* The current key backup object. If passed,
|
||||
* the passphrase and recovery key from this backup will be used.
|
||||
* @deprecated Not used by the Rust crypto stack.
|
||||
*/
|
||||
keyBackupInfo?: KeyBackupInfo;
|
||||
|
||||
/**
|
||||
* If true, a new key backup version will be
|
||||
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
|
||||
@@ -1261,18 +1166,6 @@ export interface CreateSecretStorageOpts {
|
||||
* Reset even if keys already exist.
|
||||
*/
|
||||
setupNewSecretStorage?: boolean;
|
||||
|
||||
/**
|
||||
* Function called to get the user's current key backup passphrase.
|
||||
*
|
||||
* Should return a promise that resolves with a Uint8Array
|
||||
* containing the key, or rejects if the key cannot be obtained.
|
||||
*
|
||||
* Only used when the client has existing key backup, but no secret storage.
|
||||
*
|
||||
* @deprecated Not used by the Rust crypto stack.
|
||||
*/
|
||||
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
/** Types of cross-signing key */
|
||||
|
||||
@@ -114,25 +114,6 @@ export interface VerificationRequest
|
||||
*/
|
||||
cancel(params?: { reason?: string; code?: string }): Promise<void>;
|
||||
|
||||
/**
|
||||
* Create a {@link Verifier} to do this verification via a particular method.
|
||||
*
|
||||
* If a verifier has already been created for this request, returns that verifier.
|
||||
*
|
||||
* This does *not* send the `m.key.verification.start` event - to do so, call {@link Verifier.verify} on the
|
||||
* returned verifier.
|
||||
*
|
||||
* If no previous events have been sent, pass in `targetDevice` to set who to direct this request to.
|
||||
*
|
||||
* @param method - the name of the verification method to use.
|
||||
* @param targetDevice - details of where to send the request to.
|
||||
*
|
||||
* @returns The verifier which will do the actual verification.
|
||||
*
|
||||
* @deprecated Use {@link VerificationRequest#startVerification} instead.
|
||||
*/
|
||||
beginKeyVerification(method: string, targetDevice?: { userId?: string; deviceId?: string }): Verifier;
|
||||
|
||||
/**
|
||||
* Send an `m.key.verification.start` event to start verification via a particular method.
|
||||
*
|
||||
@@ -163,15 +144,6 @@ export interface VerificationRequest
|
||||
*/
|
||||
get verifier(): Verifier | undefined;
|
||||
|
||||
/**
|
||||
* Get the data for a QR code allowing the other device to verify this one, if it supports it.
|
||||
*
|
||||
* Only set after a .ready if the other party can scan a QR code, otherwise undefined.
|
||||
*
|
||||
* @deprecated Not supported in Rust Crypto. Use {@link VerificationRequest#generateQRCode} instead.
|
||||
*/
|
||||
getQRCodeBytes(): Uint8ClampedArray | undefined;
|
||||
|
||||
/**
|
||||
* Generate the data for a QR code allowing the other device to verify this one, if it supports it.
|
||||
*
|
||||
|
||||
@@ -25,9 +25,6 @@ export interface MapperOpts {
|
||||
preventReEmit?: boolean;
|
||||
// decrypt event proactively
|
||||
decrypt?: boolean;
|
||||
|
||||
/** @deprecated no longer used */
|
||||
toDevice?: boolean;
|
||||
}
|
||||
|
||||
export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper {
|
||||
|
||||
+17
-54
@@ -34,22 +34,6 @@ import { anySignal, parseErrorResponse, timeoutSignal } from "./utils.ts";
|
||||
import { type QueryDict } from "../utils.ts";
|
||||
import { TokenRefresher, TokenRefreshOutcome } from "./refresh.ts";
|
||||
|
||||
interface TypedResponse<T> extends Response {
|
||||
json(): Promise<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type returned by {@link FetchHttpApi.request}, etc.
|
||||
*
|
||||
* If {@link IHttpOpts.onlyData} is unset or false, then the request methods return a
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Response Response} object,
|
||||
* which we abstract via `TypedResponse`. Otherwise, we just cast it to `T`.
|
||||
*
|
||||
* @typeParam T - The type (specified by the application on the request method) that we will cast the response to.
|
||||
* @typeParam O - The type of the options object on the {@link FetchHttpApi} instance.
|
||||
*/
|
||||
export type ResponseType<T, O extends IHttpOpts> = O extends { onlyData: true } | undefined ? T : TypedResponse<T>;
|
||||
|
||||
export class FetchHttpApi<O extends IHttpOpts> {
|
||||
private abortController = new AbortController();
|
||||
private readonly tokenRefresher: TokenRefresher;
|
||||
@@ -59,7 +43,9 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
public readonly opts: O,
|
||||
) {
|
||||
checkObjectHasKeys(opts, ["baseUrl", "prefix"]);
|
||||
opts.onlyData = !!opts.onlyData;
|
||||
if (!opts.onlyData) {
|
||||
throw new Error("Constructing FetchHttpApi without `onlyData=true` is no longer supported.");
|
||||
}
|
||||
opts.useAuthorizationHeader = opts.useAuthorizationHeader ?? true;
|
||||
|
||||
this.tokenRefresher = new TokenRefresher(opts);
|
||||
@@ -91,7 +77,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
params: Record<string, string | string[]> | undefined,
|
||||
prefix: string,
|
||||
accessToken?: string,
|
||||
): Promise<ResponseType<T, O>> {
|
||||
): Promise<T> {
|
||||
if (!this.opts.idBaseUrl) {
|
||||
throw new Error("No identity server base URL set");
|
||||
}
|
||||
@@ -132,17 +118,8 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
* When `paramOpts.doNotAttemptTokenRefresh` is true, token refresh will not be attempted
|
||||
* when an expired token is encountered. Used to only attempt token refresh once.
|
||||
*
|
||||
* @returns Promise which resolves to
|
||||
* ```
|
||||
* {
|
||||
* data: {Object},
|
||||
* headers: {Object},
|
||||
* code: {Number},
|
||||
* }
|
||||
* ```
|
||||
* If `onlyData` is set, this will resolve to the `data` object only.
|
||||
* @returns Rejects with an error if a problem occurred.
|
||||
* This includes network problems and Matrix-specific error JSON.
|
||||
* @returns The parsed response.
|
||||
* @throws Error if a problem occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
public authedRequest<T>(
|
||||
method: Method,
|
||||
@@ -150,7 +127,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
queryParams: QueryDict = {},
|
||||
body?: Body,
|
||||
paramOpts: IRequestOpts = {},
|
||||
): Promise<ResponseType<T, O>> {
|
||||
): Promise<T> {
|
||||
return this.doAuthedRequest<T>(1, method, path, queryParams, body, paramOpts);
|
||||
}
|
||||
|
||||
@@ -162,7 +139,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
queryParams: QueryDict,
|
||||
body?: Body,
|
||||
paramOpts: IRequestOpts = {},
|
||||
): Promise<ResponseType<T, O>> {
|
||||
): Promise<T> {
|
||||
// avoid mutating paramOpts so they can be used on retry
|
||||
const opts = deepCopy(paramOpts);
|
||||
// we have to manually copy the abortSignal over as it is not a plain object
|
||||
@@ -228,18 +205,8 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
*
|
||||
* @param opts - additional options
|
||||
*
|
||||
* @returns Promise which resolves to
|
||||
* ```
|
||||
* {
|
||||
* data: {Object},
|
||||
* headers: {Object},
|
||||
* code: {Number},
|
||||
* }
|
||||
* ```
|
||||
* If `onlyData</code> is set, this will resolve to the <code>data`
|
||||
* object only.
|
||||
* @returns Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
* @returns The parsed response.
|
||||
* @throws Error if a problem occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
public request<T>(
|
||||
method: Method,
|
||||
@@ -247,7 +214,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
queryParams?: QueryDict,
|
||||
body?: Body,
|
||||
opts?: IRequestOpts,
|
||||
): Promise<ResponseType<T, O>> {
|
||||
): Promise<T> {
|
||||
const fullUri = this.getUrl(path, queryParams, opts?.prefix, opts?.baseUrl);
|
||||
return this.requestOtherUrl<T>(method, fullUri, body, opts);
|
||||
}
|
||||
@@ -261,17 +228,15 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
*
|
||||
* @param opts - additional options
|
||||
*
|
||||
* @returns Promise which resolves to data unless `onlyData` is specified as false,
|
||||
* where the resolved value will be a fetch Response object.
|
||||
* @returns Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
* @returns The parsed response.
|
||||
* @throws Error if a problem occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
public async requestOtherUrl<T>(
|
||||
method: Method,
|
||||
url: URL | string,
|
||||
body?: Body,
|
||||
opts: BaseRequestOpts = {},
|
||||
): Promise<ResponseType<T, O>> {
|
||||
): Promise<T> {
|
||||
if (opts.json !== undefined && opts.rawResponseBody !== undefined) {
|
||||
throw new Error("Invalid call to `FetchHttpApi` sets both `opts.json` and `opts.rawResponseBody`");
|
||||
}
|
||||
@@ -349,14 +314,12 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
throw parseErrorResponse(res, await res.text());
|
||||
}
|
||||
|
||||
if (!this.opts.onlyData) {
|
||||
return res as ResponseType<T, O>;
|
||||
} else if (opts.rawResponseBody) {
|
||||
return (await res.blob()) as ResponseType<T, O>;
|
||||
if (opts.rawResponseBody) {
|
||||
return (await res.blob()) as T;
|
||||
} else if (jsonResponse) {
|
||||
return await res.json();
|
||||
} else {
|
||||
return (await res.text()) as ResponseType<T, O>;
|
||||
return (await res.text()) as T;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,9 +48,7 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
*
|
||||
* @param opts - options object
|
||||
*
|
||||
* @returns Promise which resolves to response object, as
|
||||
* determined by this.opts.onlyData, opts.rawResponse, and
|
||||
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
|
||||
* @returns Promise which resolves to response object, or rejects with an error (usually a MatrixError).
|
||||
*/
|
||||
public uploadContent(file: FileType, opts: UploadOpts = {}): Promise<UploadResponse> {
|
||||
const includeFilename = opts.includeFilename ?? true;
|
||||
@@ -149,11 +147,7 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
prefix: MediaPrefix.V3,
|
||||
headers,
|
||||
abortSignal: abortController.signal,
|
||||
})
|
||||
.then((response) => {
|
||||
return this.opts.onlyData ? <UploadResponse>response : response.json();
|
||||
})
|
||||
.then(uploadResolvers.resolve, uploadResolvers.reject);
|
||||
}).then(uploadResolvers.resolve, uploadResolvers.reject);
|
||||
}
|
||||
|
||||
// remove the upload from the list on completion
|
||||
|
||||
@@ -69,10 +69,7 @@ export interface IHttpOpts {
|
||||
tokenRefreshFunction?: TokenRefreshFunction;
|
||||
useAuthorizationHeader?: boolean; // defaults to true
|
||||
|
||||
/**
|
||||
* Normally, methods in `FetchHttpApi` will return a {@link https://developer.mozilla.org/en-US/docs/Web/API/Response Response} object.
|
||||
* If this is set to `true`, they instead return the response body.
|
||||
*/
|
||||
/** For historical reasons, must be set to `true`. Will eventually be removed. */
|
||||
onlyData?: boolean;
|
||||
|
||||
localTimeoutMs?: number;
|
||||
@@ -103,11 +100,10 @@ export interface BaseRequestOpts extends Pick<RequestInit, "priority"> {
|
||||
*
|
||||
* * Set `Accept: application/json` in the request headers (again, unless overridden by {@link headers}).
|
||||
*
|
||||
* * If `IHTTPOpts.onlyData` is set to `true` on the `FetchHttpApi` instance, parse the response as
|
||||
* JSON and return the parsed response.
|
||||
* * Parse the response as JSON and return the parsed response.
|
||||
*
|
||||
* Setting this to `false` inhibits all three behaviors, and (if `IHTTPOpts.onlyData` is set to `true`) the response
|
||||
* is instead parsed as a UTF-8 string. It defaults to `true`, unless {@link rawResponseBody} is set.
|
||||
* Setting this to `false` inhibits all three behaviors, and the response is instead parsed as a UTF-8 string. It
|
||||
* defaults to `true`, unless {@link rawResponseBody} is set.
|
||||
*
|
||||
* @deprecated Instead of setting this to `false`, set {@link rawResponseBody} to `true`.
|
||||
*/
|
||||
@@ -118,9 +114,8 @@ export interface BaseRequestOpts extends Pick<RequestInit, "priority"> {
|
||||
*
|
||||
* * Inhibits the automatic addition of `Accept: application/json` in the request headers.
|
||||
*
|
||||
* * Assuming `IHTTPOpts.onlyData` is set to `true` on the `FetchHttpApi` instance, causes the
|
||||
* raw response to be returned as a {@link https://developer.mozilla.org/en-US/docs/Web/API/Blob|Blob}
|
||||
* instead of parsing it as `json`.
|
||||
* * Causes the raw response to be returned as a {@link https://developer.mozilla.org/en-US/docs/Web/API/Blob|Blob}
|
||||
* instead of parsing it as JSON.
|
||||
*/
|
||||
rawResponseBody?: boolean;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { type MatrixEvent } from "../matrix.ts";
|
||||
import { deepCompare } from "../utils.ts";
|
||||
import { type Focus } from "./focus.ts";
|
||||
import { isLivekitFocusActive } from "./LivekitFocus.ts";
|
||||
import { type SessionDescription } from "./MatrixRTCSession.ts";
|
||||
|
||||
/**
|
||||
* The default duration in milliseconds that a membership is considered valid for.
|
||||
@@ -130,6 +131,9 @@ export class CallMembership {
|
||||
return this.parentEvent.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use sessionDescription.id instead.
|
||||
*/
|
||||
public get callId(): string {
|
||||
return this.membershipData.call_id;
|
||||
}
|
||||
@@ -138,6 +142,13 @@ export class CallMembership {
|
||||
return this.membershipData.device_id;
|
||||
}
|
||||
|
||||
public get sessionDescription(): SessionDescription {
|
||||
return {
|
||||
application: this.membershipData.application,
|
||||
id: this.membershipData.call_id,
|
||||
};
|
||||
}
|
||||
|
||||
public get application(): string | undefined {
|
||||
return this.membershipData.application;
|
||||
}
|
||||
|
||||
@@ -17,13 +17,21 @@ limitations under the License.
|
||||
import type { CallMembership } from "./CallMembership.ts";
|
||||
import type { Focus } from "./focus.ts";
|
||||
import type { Status } from "./types.ts";
|
||||
import { type TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
||||
|
||||
export enum MembershipManagerEvent {
|
||||
StatusChanged = "StatusChanged",
|
||||
/**
|
||||
* Emitted when the membership manager has not heard back from the server for the duration
|
||||
* of the delayed event and hence failed to restart the delayed event.
|
||||
* This means that the user is probably not joined anymore and the leave event was distributed to other session members.
|
||||
*/
|
||||
ProbablyLeft = "ProbablyLeft",
|
||||
}
|
||||
|
||||
export type MembershipManagerEventHandlerMap = {
|
||||
[MembershipManagerEvent.StatusChanged]: (prefStatus: Status, newStatus: Status) => void;
|
||||
[MembershipManagerEvent.ProbablyLeft]: (probablyLeft: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -33,7 +41,8 @@ export type MembershipManagerEventHandlerMap = {
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export interface IMembershipManager {
|
||||
export interface IMembershipManager
|
||||
extends TypedEventEmitter<MembershipManagerEvent, MembershipManagerEventHandlerMap> {
|
||||
/**
|
||||
* If we are trying to join, or have successfully joined the session.
|
||||
* It does not reflect if the room state is already configured to represent us being joined.
|
||||
@@ -62,6 +71,12 @@ export interface IMembershipManager {
|
||||
*/
|
||||
get ownMembership(): CallMembership | undefined;
|
||||
|
||||
/**
|
||||
* If the membership manager has reason to believe that the hs sent a leave event
|
||||
* and as a consequence the current user is perceived as left for other session participants.
|
||||
*/
|
||||
get probablyLeft(): boolean;
|
||||
|
||||
/**
|
||||
* Start sending all necessary events to make this user participate in the RTC session.
|
||||
* @param fociPreferred the list of preferred foci to use in the joined RTC membership event.
|
||||
@@ -85,8 +100,4 @@ export interface IMembershipManager {
|
||||
* @returns the used active focus in the currently joined session or undefined if not joined.
|
||||
*/
|
||||
getActiveFocus(): Focus | undefined;
|
||||
|
||||
// TypedEventEmitter methods:
|
||||
on(event: MembershipManagerEvent.StatusChanged, listener: (oldStatus: Status, newStatus: Status) => void): this;
|
||||
off(event: MembershipManagerEvent.StatusChanged, listener: (oldStatus: Status, newStatus: Status) => void): this;
|
||||
}
|
||||
|
||||
@@ -20,16 +20,27 @@ import { EventTimeline } from "../models/event-timeline.ts";
|
||||
import { type Room } from "../models/room.ts";
|
||||
import { type MatrixClient } from "../client.ts";
|
||||
import { EventType, RelationType } from "../@types/event.ts";
|
||||
import { KnownMembership } from "../@types/membership.ts";
|
||||
import { type ISendEventResponse } from "../@types/requests.ts";
|
||||
import { CallMembership } from "./CallMembership.ts";
|
||||
import { RoomStateEvent } from "../models/room-state.ts";
|
||||
import { type Focus } from "./focus.ts";
|
||||
import { KnownMembership } from "../@types/membership.ts";
|
||||
import { MembershipManager } from "./MembershipManager.ts";
|
||||
import { EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts";
|
||||
import { logDurationSync } from "../utils.ts";
|
||||
import { type Statistics, type RTCNotificationType } from "./types.ts";
|
||||
import { deepCompare, logDurationSync } from "../utils.ts";
|
||||
import {
|
||||
type Statistics,
|
||||
type RTCNotificationType,
|
||||
type Status,
|
||||
type IRTCNotificationContent,
|
||||
type ICallNotifyContent,
|
||||
} from "./types.ts";
|
||||
import { RoomKeyTransport } from "./RoomKeyTransport.ts";
|
||||
import type { IMembershipManager } from "./IMembershipManager.ts";
|
||||
import {
|
||||
MembershipManagerEvent,
|
||||
type MembershipManagerEventHandlerMap,
|
||||
type IMembershipManager,
|
||||
} from "./IMembershipManager.ts";
|
||||
import { RTCEncryptionManager } from "./RTCEncryptionManager.ts";
|
||||
import {
|
||||
RoomAndToDeviceEvents,
|
||||
@@ -39,6 +50,9 @@ import {
|
||||
import { TypedReEmitter } from "../ReEmitter.ts";
|
||||
import { ToDeviceKeyTransport } from "./ToDeviceKeyTransport.ts";
|
||||
|
||||
/**
|
||||
* Events emitted by MatrixRTCSession
|
||||
*/
|
||||
export enum MatrixRTCSessionEvent {
|
||||
// A member joined, left, or updated a property of their membership.
|
||||
MembershipsChanged = "memberships_changed",
|
||||
@@ -50,6 +64,8 @@ export enum MatrixRTCSessionEvent {
|
||||
EncryptionKeyChanged = "encryption_key_changed",
|
||||
/** The membership manager had to shut down caused by an unrecoverable error */
|
||||
MembershipManagerError = "membership_manager_error",
|
||||
/** The RTCSession did send a call notification caused by joining the call as the first member */
|
||||
DidSendCallNotification = "did_send_call_notification",
|
||||
}
|
||||
|
||||
export type MatrixRTCSessionEventHandlerMap = {
|
||||
@@ -64,6 +80,10 @@ export type MatrixRTCSessionEventHandlerMap = {
|
||||
participantId: string,
|
||||
) => void;
|
||||
[MatrixRTCSessionEvent.MembershipManagerError]: (error: unknown) => void;
|
||||
[MatrixRTCSessionEvent.DidSendCallNotification]: (
|
||||
notificationContentNew: { event_id: string } & IRTCNotificationContent,
|
||||
notificationContentLegacy: { event_id: string } & ICallNotifyContent,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export interface SessionConfig {
|
||||
@@ -74,6 +94,14 @@ export interface SessionConfig {
|
||||
notificationType?: RTCNotificationType;
|
||||
}
|
||||
|
||||
/**
|
||||
* The session description is used to identify a session. Used in the state event.
|
||||
*/
|
||||
export interface SessionDescription {
|
||||
id: string;
|
||||
application: string;
|
||||
}
|
||||
|
||||
// The names follow these principles:
|
||||
// - we use the technical term delay if the option is related to delayed events.
|
||||
// - we use delayedLeaveEvent if the option is related to the delayed leave event.
|
||||
@@ -86,7 +114,7 @@ export interface MembershipConfig {
|
||||
* Use the new Manager.
|
||||
*
|
||||
* Default: `false`.
|
||||
* @deprecated does nothing anymore we always default to the new memberhip manager.
|
||||
* @deprecated does nothing anymore we always default to the new membership manager.
|
||||
*/
|
||||
useNewMembershipManager?: boolean;
|
||||
|
||||
@@ -97,8 +125,6 @@ export interface MembershipConfig {
|
||||
* This is what goes into the m.rtc.member event expiry field and is typically set to a number of hours.
|
||||
*/
|
||||
membershipEventExpiryMs?: number;
|
||||
/** @deprecated renamed to `membershipEventExpiryMs`*/
|
||||
membershipExpiryTimeout?: number;
|
||||
|
||||
/**
|
||||
* The time in (in milliseconds) which the manager will prematurely send the updated state event before the membership `expires` time to make sure it
|
||||
@@ -110,23 +136,17 @@ export interface MembershipConfig {
|
||||
* This value does not have an effect on the value of `SessionMembershipData.expires`.
|
||||
*/
|
||||
membershipEventExpiryHeadroomMs?: number;
|
||||
/** @deprecated renamed to `membershipEventExpiryHeadroomMs`*/
|
||||
membershipExpiryTimeoutHeadroom?: number;
|
||||
|
||||
/**
|
||||
* The timeout (in milliseconds) with which the deleayed leave event on the server is configured.
|
||||
* After this time the server will set the event to the disconnected stat if it has not received a keep-alive from the client.
|
||||
*/
|
||||
delayedLeaveEventDelayMs?: number;
|
||||
/** @deprecated renamed to `delayedLeaveEventDelayMs`*/
|
||||
membershipServerSideExpiryTimeout?: number;
|
||||
|
||||
/**
|
||||
* The interval (in milliseconds) in which the client will send membership keep-alives to the server.
|
||||
*/
|
||||
delayedLeaveEventRestartMs?: number;
|
||||
/** @deprecated renamed to `delayedLeaveEventRestartMs`*/
|
||||
membershipKeepAlivePeriod?: number;
|
||||
|
||||
/**
|
||||
* The maximum number of retries that the manager will do for delayed event sending/updating and state event sending when a server rate limit has been hit.
|
||||
@@ -144,9 +164,6 @@ export interface MembershipConfig {
|
||||
*/
|
||||
networkErrorRetryMs?: number;
|
||||
|
||||
/** @deprecated renamed to `networkErrorRetryMs`*/
|
||||
callMemberEventRetryDelayMinimum?: number;
|
||||
|
||||
/**
|
||||
* If true, use the new to-device transport for sending encryption keys.
|
||||
*/
|
||||
@@ -212,8 +229,8 @@ export type JoinSessionConfig = SessionConfig & MembershipConfig & EncryptionCon
|
||||
* This class doesn't deal with media at all, just membership & properties of a session.
|
||||
*/
|
||||
export class MatrixRTCSession extends TypedEventEmitter<
|
||||
MatrixRTCSessionEvent | RoomAndToDeviceEvents,
|
||||
MatrixRTCSessionEventHandlerMap & RoomAndToDeviceEventsHandlerMap
|
||||
MatrixRTCSessionEvent | RoomAndToDeviceEvents | MembershipManagerEvent,
|
||||
MatrixRTCSessionEventHandlerMap & RoomAndToDeviceEventsHandlerMap & MembershipManagerEventHandlerMap
|
||||
> {
|
||||
private membershipManager?: IMembershipManager;
|
||||
private encryptionManager?: IEncryptionManager;
|
||||
@@ -243,6 +260,14 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
},
|
||||
};
|
||||
|
||||
public get membershipStatus(): Status | undefined {
|
||||
return this.membershipManager?.status;
|
||||
}
|
||||
|
||||
public get probablyLeft(): boolean | undefined {
|
||||
return this.membershipManager?.probablyLeft;
|
||||
}
|
||||
|
||||
/**
|
||||
* The callId (sessionId) of the call.
|
||||
*
|
||||
@@ -254,10 +279,27 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the call memberships for a room, oldest first
|
||||
* Returns all the call memberships for a room that match the provided `sessionDescription`,
|
||||
* oldest first.
|
||||
*
|
||||
* @deprecated Use `MatrixRTCSession.sessionMembershipsForRoom` instead.
|
||||
*/
|
||||
public static callMembershipsForRoom(
|
||||
room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState">,
|
||||
): CallMembership[] {
|
||||
return MatrixRTCSession.sessionMembershipsForRoom(room, {
|
||||
id: "",
|
||||
application: "m.call",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the call memberships for a room that match the provided `sessionDescription`,
|
||||
* oldest first.
|
||||
*/
|
||||
public static sessionMembershipsForRoom(
|
||||
room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState">,
|
||||
sessionDescription: SessionDescription,
|
||||
): CallMembership[] {
|
||||
const logger = rootLogger.getChild(`[MatrixRTCSession ${room.roomId}]`);
|
||||
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
@@ -290,9 +332,10 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
try {
|
||||
const membership = new CallMembership(memberEvent, membershipData);
|
||||
|
||||
if (membership.callId !== "" || membership.scope !== "m.room") {
|
||||
// for now, just ignore anything that isn't a room scope call
|
||||
logger.info(`Ignoring user-scoped call`);
|
||||
if (!deepCompare(membership.sessionDescription, sessionDescription)) {
|
||||
logger.info(
|
||||
`Ignoring membership of user ${membership.sender} for a different session: ${JSON.stringify(membership.sessionDescription)}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -323,12 +366,33 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the MatrixRTC session for the room, whether there are currently active members or not
|
||||
* Return the MatrixRTC session for the room.
|
||||
* This returned session can be used to find out if there are active room call sessions
|
||||
* for the requested room.
|
||||
*
|
||||
* This method is an alias for `MatrixRTCSession.sessionForRoom` with
|
||||
* sessionDescription `{ id: "", application: "m.call" }`.
|
||||
*
|
||||
* @deprecated Use `MatrixRTCSession.sessionForRoom` with sessionDescription `{ id: "", application: "m.call" }` instead.
|
||||
*/
|
||||
public static roomSessionForRoom(client: MatrixClient, room: Room): MatrixRTCSession {
|
||||
const callMemberships = MatrixRTCSession.callMembershipsForRoom(room);
|
||||
const callMemberships = MatrixRTCSession.sessionMembershipsForRoom(room, { id: "", application: "m.call" });
|
||||
return new MatrixRTCSession(client, room, callMemberships, { id: "", application: "m.call" });
|
||||
}
|
||||
|
||||
return new MatrixRTCSession(client, room, callMemberships);
|
||||
/**
|
||||
* Return the MatrixRTC session for the room.
|
||||
* This returned session can be used to find out if there are active sessions
|
||||
* for the requested room and `sessionDescription`.
|
||||
*/
|
||||
public static sessionForRoom(
|
||||
client: MatrixClient,
|
||||
room: Room,
|
||||
sessionDescription: SessionDescription,
|
||||
): MatrixRTCSession {
|
||||
const callMemberships = MatrixRTCSession.sessionMembershipsForRoom(room, sessionDescription);
|
||||
|
||||
return new MatrixRTCSession(client, room, callMemberships, sessionDescription);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -373,10 +437,15 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
"getLiveTimeline" | "roomId" | "getVersion" | "hasMembershipState" | "on" | "off"
|
||||
>,
|
||||
public memberships: CallMembership[],
|
||||
/**
|
||||
* The session description is used to define the exact session this object is tracking.
|
||||
* A session is distinct from another session if one of those properties differ: `roomSubset.roomId`, `sessionDescription.application`, `sessionDescription.id`.
|
||||
*/
|
||||
public readonly sessionDescription: SessionDescription,
|
||||
) {
|
||||
super();
|
||||
this.logger = rootLogger.getChild(`[MatrixRTCSession ${roomSubset.roomId}]`);
|
||||
this._callId = memberships[0]?.callId;
|
||||
this._callId = memberships[0]?.sessionDescription.id;
|
||||
const roomState = this.roomSubset.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
// TODO: double check if this is actually needed. Should be covered by refreshRoom in MatrixRTCSessionManager
|
||||
roomState?.on(RoomStateEvent.Members, this.onRoomMemberUpdate);
|
||||
@@ -404,8 +473,8 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
roomState?.off(RoomStateEvent.Members, this.onRoomMemberUpdate);
|
||||
}
|
||||
private reEmitter = new TypedReEmitter<
|
||||
MatrixRTCSessionEvent | RoomAndToDeviceEvents,
|
||||
MatrixRTCSessionEventHandlerMap & RoomAndToDeviceEventsHandlerMap
|
||||
MatrixRTCSessionEvent | RoomAndToDeviceEvents | MembershipManagerEvent,
|
||||
MatrixRTCSessionEventHandlerMap & RoomAndToDeviceEventsHandlerMap & MembershipManagerEventHandlerMap
|
||||
>(this);
|
||||
|
||||
/**
|
||||
@@ -434,9 +503,14 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
this.roomSubset,
|
||||
this.client,
|
||||
() => this.getOldestMembership(),
|
||||
this.sessionDescription,
|
||||
this.logger,
|
||||
);
|
||||
|
||||
this.reEmitter.reEmit(this.membershipManager!, [
|
||||
MembershipManagerEvent.ProbablyLeft,
|
||||
MembershipManagerEvent.StatusChanged,
|
||||
]);
|
||||
// Create Encryption manager
|
||||
let transport;
|
||||
if (joinConfig?.useExperimentalToDeviceTransport) {
|
||||
@@ -594,29 +668,47 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
* Sends a notification corresponding to the configured notify type.
|
||||
*/
|
||||
private sendCallNotify(parentEventId: string, notificationType: RTCNotificationType): void {
|
||||
// Send legacy event:
|
||||
this.client
|
||||
.sendEvent(this.roomSubset.roomId, EventType.CallNotify, {
|
||||
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!,
|
||||
})
|
||||
.catch((e) => this.logger.error("Failed to send call notification", e));
|
||||
|
||||
// Send new event:
|
||||
this.client
|
||||
.sendEvent(this.roomSubset.roomId, EventType.RTCNotification, {
|
||||
};
|
||||
const response = await this.client.sendEvent(this.roomSubset.roomId, EventType.CallNotify, content);
|
||||
return { response, content };
|
||||
};
|
||||
const sendNewNotificationEvent = async (): Promise<{
|
||||
response: ISendEventResponse;
|
||||
content: IRTCNotificationContent;
|
||||
}> => {
|
||||
const content: IRTCNotificationContent = {
|
||||
"m.mentions": { user_ids: [], room: true },
|
||||
"notification_type": notificationType,
|
||||
"m.relates_to": {
|
||||
event_id: parentEventId,
|
||||
rel_type: RelationType.unstable_RTCNotificationParent,
|
||||
rel_type: RelationType.Reference,
|
||||
},
|
||||
"sender_ts": Date.now(),
|
||||
"lifetime": 30_000, // 30 seconds
|
||||
};
|
||||
const response = await this.client.sendEvent(this.roomSubset.roomId, EventType.RTCNotification, content);
|
||||
return { response, content };
|
||||
};
|
||||
|
||||
void Promise.all([sendLegacyNotificationEvent(), sendNewNotificationEvent()])
|
||||
.then(([legacy, newNotification]) => {
|
||||
// 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);
|
||||
})
|
||||
.catch((e) => this.logger.error("Failed to send call notification", e));
|
||||
.catch(([errorLegacy, errorNew]) =>
|
||||
this.logger.error("Failed to send call notification", errorLegacy, errorNew),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -642,9 +734,9 @@ export class MatrixRTCSession extends TypedEventEmitter<
|
||||
*/
|
||||
private recalculateSessionMembers = (): void => {
|
||||
const oldMemberships = this.memberships;
|
||||
this.memberships = MatrixRTCSession.callMembershipsForRoom(this.room);
|
||||
this.memberships = MatrixRTCSession.sessionMembershipsForRoom(this.room, this.sessionDescription);
|
||||
|
||||
this._callId = this._callId ?? this.memberships[0]?.callId;
|
||||
this._callId = this._callId ?? this.memberships[0]?.sessionDescription.id;
|
||||
|
||||
const changed =
|
||||
oldMemberships.length != this.memberships.length ||
|
||||
|
||||
@@ -20,7 +20,7 @@ import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
||||
import { type Room } from "../models/room.ts";
|
||||
import { type RoomState, RoomStateEvent } from "../models/room-state.ts";
|
||||
import { type MatrixEvent } from "../models/event.ts";
|
||||
import { MatrixRTCSession } from "./MatrixRTCSession.ts";
|
||||
import { MatrixRTCSession, type SessionDescription } from "./MatrixRTCSession.ts";
|
||||
import { EventType } from "../@types/event.ts";
|
||||
|
||||
export enum MatrixRTCSessionManagerEvents {
|
||||
@@ -37,6 +37,9 @@ type EventHandlerMap = {
|
||||
|
||||
/**
|
||||
* Holds all active MatrixRTC session objects and creates new ones as events arrive.
|
||||
* One `MatrixRTCSessionManager` is required for each MatrixRTC sessionDescription (application, session id) that the client wants to support.
|
||||
* If no application type is specified in the constructor, the default is "m.call".
|
||||
*
|
||||
* This interface is UNSTABLE and may change without warning.
|
||||
*/
|
||||
export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionManagerEvents, EventHandlerMap> {
|
||||
@@ -53,6 +56,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
|
||||
public constructor(
|
||||
rootLogger: Logger,
|
||||
private client: MatrixClient,
|
||||
private readonly sessionDescription: SessionDescription = { id: "", application: "m.call" }, // Default to the Matrix Call application
|
||||
) {
|
||||
super();
|
||||
this.logger = rootLogger.getChild("[MatrixRTCSessionManager]");
|
||||
@@ -62,7 +66,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
|
||||
// We shouldn't need to null-check here, but matrix-client.spec.ts mocks getRooms
|
||||
// returning nothing, and breaks tests if you change it to return an empty array :'(
|
||||
for (const room of this.client.getRooms() ?? []) {
|
||||
const session = MatrixRTCSession.roomSessionForRoom(this.client, room);
|
||||
const session = MatrixRTCSession.sessionForRoom(this.client, room, this.sessionDescription);
|
||||
if (session.memberships.length > 0) {
|
||||
this.roomSessions.set(room.roomId, session);
|
||||
}
|
||||
@@ -96,7 +100,10 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
|
||||
*/
|
||||
public getRoomSession(room: Room): MatrixRTCSession {
|
||||
if (!this.roomSessions.has(room.roomId)) {
|
||||
this.roomSessions.set(room.roomId, MatrixRTCSession.roomSessionForRoom(this.client, room));
|
||||
this.roomSessions.set(
|
||||
room.roomId,
|
||||
MatrixRTCSession.sessionForRoom(this.client, room, this.sessionDescription),
|
||||
);
|
||||
}
|
||||
|
||||
return this.roomSessions.get(room.roomId)!;
|
||||
|
||||
@@ -26,7 +26,7 @@ import { type CallMembership, DEFAULT_EXPIRE_DURATION, type SessionMembershipDat
|
||||
import { type Focus } from "./focus.ts";
|
||||
import { isMyMembership, Status } from "./types.ts";
|
||||
import { isLivekitFocusActive } from "./LivekitFocus.ts";
|
||||
import { type MembershipConfig } from "./MatrixRTCSession.ts";
|
||||
import { type SessionDescription, type MembershipConfig } from "./MatrixRTCSession.ts";
|
||||
import { ActionScheduler, type ActionUpdate } from "./MembershipManagerActionScheduler.ts";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
||||
import {
|
||||
@@ -101,7 +101,7 @@ export enum MembershipActionType {
|
||||
// -> MembershipActionType.Update if the timeout has passed so the next update is required.
|
||||
|
||||
SendScheduledDelayedLeaveEvent = "SendScheduledDelayedLeaveEvent",
|
||||
// -> MembershipActionType.SendLeaveEvent on failiour (not found) we need to send the leave manually and cannot use the scheduled delayed event
|
||||
// -> MembershipActionType.SendLeaveEvent on failure (not found) we need to send the leave manually and cannot use the scheduled delayed event
|
||||
// -> DelayedLeaveActionType.SendScheduledDelayedLeaveEvent on error we try again.
|
||||
|
||||
SendLeaveEvent = "SendLeaveEvent",
|
||||
@@ -129,6 +129,12 @@ export interface MembershipManagerState {
|
||||
rateLimitRetries: Map<MembershipActionType, number>;
|
||||
/** Retry counter for other errors */
|
||||
networkErrorRetries: Map<MembershipActionType, number>;
|
||||
/** The time at which we expect the server to send the delayed leave event. */
|
||||
expectedServerDelayLeaveTs?: number;
|
||||
/** This is used to track if the client expects the scheduled delayed leave event to have
|
||||
* been sent because restarting failed during the available time.
|
||||
* Once we resend the delayed event or successfully restarted it will get unset. */
|
||||
probablyLeft: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,6 +300,7 @@ export class MembershipManager
|
||||
| "_unstable_updateDelayedEvent"
|
||||
>,
|
||||
private getOldestMembership: () => CallMembership | undefined,
|
||||
public readonly sessionDescription: SessionDescription,
|
||||
parentLogger?: Logger,
|
||||
) {
|
||||
super();
|
||||
@@ -342,6 +349,7 @@ export class MembershipManager
|
||||
rateLimitRetries: new Map(),
|
||||
networkErrorRetries: new Map(),
|
||||
expireUpdateIterations: 1,
|
||||
probablyLeft: false,
|
||||
};
|
||||
}
|
||||
// Membership Event static parameters:
|
||||
@@ -354,35 +362,22 @@ export class MembershipManager
|
||||
private delayedLeaveEventDelayMsOverride?: number;
|
||||
|
||||
private get networkErrorRetryMs(): number {
|
||||
return this.joinConfig?.networkErrorRetryMs ?? this.joinConfig?.callMemberEventRetryDelayMinimum ?? 3_000;
|
||||
return this.joinConfig?.networkErrorRetryMs ?? 3_000;
|
||||
}
|
||||
private get membershipEventExpiryMs(): number {
|
||||
return (
|
||||
this.joinConfig?.membershipEventExpiryMs ??
|
||||
this.joinConfig?.membershipExpiryTimeout ??
|
||||
DEFAULT_EXPIRE_DURATION
|
||||
);
|
||||
return this.joinConfig?.membershipEventExpiryMs ?? DEFAULT_EXPIRE_DURATION;
|
||||
}
|
||||
private get membershipEventExpiryHeadroomMs(): number {
|
||||
return (
|
||||
this.joinConfig?.membershipEventExpiryHeadroomMs ??
|
||||
this.joinConfig?.membershipExpiryTimeoutHeadroom ??
|
||||
5_000
|
||||
);
|
||||
return this.joinConfig?.membershipEventExpiryHeadroomMs ?? 5_000;
|
||||
}
|
||||
private computeNextExpiryActionTs(iteration: number): number {
|
||||
return this.state.startTime + this.membershipEventExpiryMs * iteration - this.membershipEventExpiryHeadroomMs;
|
||||
}
|
||||
private get delayedLeaveEventDelayMs(): number {
|
||||
return (
|
||||
this.delayedLeaveEventDelayMsOverride ??
|
||||
this.joinConfig?.delayedLeaveEventDelayMs ??
|
||||
this.joinConfig?.membershipServerSideExpiryTimeout ??
|
||||
8_000
|
||||
);
|
||||
return this.delayedLeaveEventDelayMsOverride ?? this.joinConfig?.delayedLeaveEventDelayMs ?? 8_000;
|
||||
}
|
||||
private get delayedLeaveEventRestartMs(): number {
|
||||
return this.joinConfig?.delayedLeaveEventRestartMs ?? this.joinConfig?.membershipKeepAlivePeriod ?? 5_000;
|
||||
return this.joinConfig?.delayedLeaveEventRestartMs ?? 5_000;
|
||||
}
|
||||
private get maximumRateLimitRetryCount(): number {
|
||||
return this.joinConfig?.maximumRateLimitRetryCount ?? 10;
|
||||
@@ -465,6 +460,8 @@ export class MembershipManager
|
||||
this.stateKey,
|
||||
)
|
||||
.then((response) => {
|
||||
this.state.expectedServerDelayLeaveTs = Date.now() + this.delayedLeaveEventDelayMs;
|
||||
this.setAndEmitProbablyLeft(false);
|
||||
// On success we reset retries and set delayId.
|
||||
this.resetRateLimitCounter(MembershipActionType.SendDelayedEvent);
|
||||
this.state.delayId = response.delay_id;
|
||||
@@ -544,27 +541,58 @@ export class MembershipManager
|
||||
});
|
||||
}
|
||||
|
||||
private setAndEmitProbablyLeft(probablyLeft: boolean): void {
|
||||
if (this.state.probablyLeft === probablyLeft) {
|
||||
return;
|
||||
}
|
||||
this.state.probablyLeft = probablyLeft;
|
||||
this.emit(MembershipManagerEvent.ProbablyLeft, this.state.probablyLeft);
|
||||
}
|
||||
|
||||
private async restartDelayedEvent(delayId: string): Promise<ActionUpdate> {
|
||||
// Compute the duration until we expect the server to send the delayed leave event.
|
||||
const durationUntilServerDelayedLeave = this.state.expectedServerDelayLeaveTs
|
||||
? this.state.expectedServerDelayLeaveTs - Date.now()
|
||||
: undefined;
|
||||
const abortPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new AbortError("Restart delayed event timed out before the HS responded"));
|
||||
}, this.delayedLeaveEventRestartLocalTimeoutMs);
|
||||
setTimeout(
|
||||
() => {
|
||||
reject(new AbortError("Restart delayed event timed out before the HS responded"));
|
||||
},
|
||||
// We abort immediately at the time where we expect the server to send the delayed leave event.
|
||||
// At this point we want the catch block to run and set the `probablyLeft` state.
|
||||
//
|
||||
// While we are already in probablyLeft state, we use the unaltered delayedLeaveEventRestartLocalTimeoutMs.
|
||||
durationUntilServerDelayedLeave !== undefined && !this.state.probablyLeft
|
||||
? Math.min(this.delayedLeaveEventRestartLocalTimeoutMs, durationUntilServerDelayedLeave)
|
||||
: this.delayedLeaveEventRestartLocalTimeoutMs,
|
||||
);
|
||||
});
|
||||
|
||||
// The obvious choice here would be to use the `IRequestOpts` to set the timeout. Since this call might be forwarded
|
||||
// to the widget driver this information would ge lost. That is why we mimic the AbortError using the race.
|
||||
// to the widget driver this information would get lost. That is why we mimic the AbortError using the race.
|
||||
return await Promise.race([
|
||||
this.client._unstable_updateDelayedEvent(delayId, UpdateDelayedEventAction.Restart),
|
||||
abortPromise,
|
||||
])
|
||||
.then(() => {
|
||||
// Whenever we successfully restart the delayed event we update the `state.expectedServerDelayLeaveTs`
|
||||
// which stores the predicted timestamp at which the server will send the delayed leave event if there wont be any further
|
||||
// successful restart requests.
|
||||
this.state.expectedServerDelayLeaveTs = Date.now() + this.delayedLeaveEventDelayMs;
|
||||
this.resetRateLimitCounter(MembershipActionType.RestartDelayedEvent);
|
||||
this.setAndEmitProbablyLeft(false);
|
||||
return createInsertActionUpdate(
|
||||
MembershipActionType.RestartDelayedEvent,
|
||||
this.delayedLeaveEventRestartMs,
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (this.state.expectedServerDelayLeaveTs && this.state.expectedServerDelayLeaveTs <= Date.now()) {
|
||||
// Once we reach this point it's likely that the server is sending the delayed leave event so we emit `probablyLeft = true`.
|
||||
// It will emit `probablyLeft = false` once we notice about our leave through sync and successfully setup a new state event.
|
||||
this.setAndEmitProbablyLeft(true);
|
||||
}
|
||||
const repeatActionType = MembershipActionType.RestartDelayedEvent;
|
||||
if (this.isNotFoundError(e)) {
|
||||
this.state.delayId = undefined;
|
||||
@@ -619,6 +647,7 @@ export class MembershipManager
|
||||
this.stateKey,
|
||||
)
|
||||
.then(() => {
|
||||
this.setAndEmitProbablyLeft(false);
|
||||
this.state.startTime = Date.now();
|
||||
// The next update should already use twice the membershipEventExpiryTimeout
|
||||
this.state.expireUpdateIterations = 1;
|
||||
@@ -700,7 +729,7 @@ export class MembershipManager
|
||||
|
||||
// HELPERS
|
||||
private makeMembershipStateKey(localUserId: string, localDeviceId: string): string {
|
||||
const stateKey = `${localUserId}_${localDeviceId}`;
|
||||
const stateKey = `${localUserId}_${localDeviceId}_${this.sessionDescription.application}${this.sessionDescription.id}`;
|
||||
if (/^org\.matrix\.msc(3757|3779)\b/.exec(this.room.getVersion())) {
|
||||
return stateKey;
|
||||
} else {
|
||||
@@ -713,9 +742,10 @@ export class MembershipManager
|
||||
*/
|
||||
private makeMyMembership(expires: number): SessionMembershipData {
|
||||
return {
|
||||
call_id: "",
|
||||
// TODO: use the new format for m.rtc.member events where call_id becomes session.id
|
||||
application: this.sessionDescription.application,
|
||||
call_id: this.sessionDescription.id,
|
||||
scope: "m.room",
|
||||
application: "m.call",
|
||||
device_id: this.deviceId,
|
||||
expires,
|
||||
focus_active: { type: "livekit", focus_selection: "oldest_membership" },
|
||||
@@ -941,6 +971,10 @@ export class MembershipManager
|
||||
this.logger.error("MembershipManager has an unknown state. Actions: ", actions);
|
||||
return Status.Unknown;
|
||||
}
|
||||
|
||||
public get probablyLeft(): boolean {
|
||||
return this.state.probablyLeft;
|
||||
}
|
||||
}
|
||||
|
||||
function createInsertActionUpdate(type: MembershipActionType, offset?: number): ActionUpdate {
|
||||
|
||||
@@ -104,6 +104,12 @@ export interface IRTCNotificationContent extends RelationEvent {
|
||||
"lifetime": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* MSC4310 decline event content for `org.matrix.msc4310.rtc.decline`.
|
||||
* Sent as a standard m.reference relation to an `org.matrix.msc4075.rtc.notification` event.
|
||||
*/
|
||||
export interface IRTCDeclineContent extends RelationEvent {}
|
||||
|
||||
export enum Status {
|
||||
Disconnected = "Disconnected",
|
||||
Connecting = "Connecting",
|
||||
|
||||
@@ -822,16 +822,6 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
return this._decryptionFailureReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and
|
||||
* the sender has disabled encrypting to unverified devices.
|
||||
*
|
||||
* @deprecated: Prefer `event.decryptionFailureReason === DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE`.
|
||||
*/
|
||||
public get isEncryptedDisabledForUnverifiedDevices(): boolean {
|
||||
return this.decryptionFailureReason === DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE;
|
||||
}
|
||||
|
||||
public shouldAttemptDecryption(): boolean {
|
||||
if (this.isRedacted()) return false;
|
||||
if (this.isBeingDecrypted()) return false;
|
||||
|
||||
@@ -66,7 +66,6 @@ export type RoomMemberEventHandlerMap = {
|
||||
* ```
|
||||
* matrixClient.on("RoomMember.powerLevel", function(event, member){
|
||||
* var newPowerLevel = member.powerLevel;
|
||||
* var newNormPowerLevel = member.powerLevelNorm;
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
@@ -109,10 +108,6 @@ export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEve
|
||||
* The power level for this room member.
|
||||
*/
|
||||
public powerLevel = 0;
|
||||
/**
|
||||
* The normalised power level (0-100) for this room member.
|
||||
*/
|
||||
public powerLevelNorm = 0;
|
||||
/**
|
||||
* The User object for this room member, if one exists.
|
||||
*/
|
||||
@@ -226,43 +221,18 @@ export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEve
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this room member's power level event. May fire
|
||||
* "RoomMember.powerLevel" if this event updates this member's power levels.
|
||||
* @param powerLevelEvent - The `m.room.power_levels` event
|
||||
* Update this room member's power level event. Will fire
|
||||
* "RoomMember.powerLevel" if the new power level is different
|
||||
* @param powerLevel - The power level of the room member.
|
||||
*
|
||||
* @remarks
|
||||
* Fires {@link RoomMemberEvent.PowerLevel}
|
||||
*/
|
||||
public setPowerLevelEvent(powerLevelEvent: MatrixEvent): void {
|
||||
if (powerLevelEvent.getType() !== EventType.RoomPowerLevels || powerLevelEvent.getStateKey() !== "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const evContent = powerLevelEvent.getDirectionalContent();
|
||||
|
||||
let maxLevel = evContent.users_default || 0;
|
||||
const users: { [userId: string]: number } = evContent.users || {};
|
||||
Object.values(users).forEach((lvl: number) => {
|
||||
maxLevel = Math.max(maxLevel, lvl);
|
||||
});
|
||||
public setPowerLevel(powerLevel: number, powerLevelEvent: MatrixEvent): void {
|
||||
const oldPowerLevel = this.powerLevel;
|
||||
const oldPowerLevelNorm = this.powerLevelNorm;
|
||||
this.powerLevel = powerLevel;
|
||||
|
||||
if (users[this.userId] !== undefined && Number.isInteger(users[this.userId])) {
|
||||
this.powerLevel = users[this.userId];
|
||||
} else if (evContent.users_default !== undefined) {
|
||||
this.powerLevel = evContent.users_default;
|
||||
} else {
|
||||
this.powerLevel = 0;
|
||||
}
|
||||
this.powerLevelNorm = 0;
|
||||
if (maxLevel > 0) {
|
||||
this.powerLevelNorm = (this.powerLevel * 100) / maxLevel;
|
||||
}
|
||||
|
||||
// emit for changes in powerLevelNorm as well (since the app will need to
|
||||
// redraw everyone's level if the max has changed)
|
||||
if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) {
|
||||
if (oldPowerLevel !== this.powerLevel) {
|
||||
this.updateModifiedTime();
|
||||
this.emit(RoomMemberEvent.PowerLevel, powerLevelEvent, this);
|
||||
}
|
||||
|
||||
+86
-12
@@ -33,6 +33,7 @@ import { TypedReEmitter } from "../ReEmitter.ts";
|
||||
import { M_BEACON, M_BEACON_INFO } from "../@types/beacon.ts";
|
||||
import { KnownMembership } from "../@types/membership.ts";
|
||||
import { type RoomJoinRulesEventContent } from "../@types/state_events.ts";
|
||||
import { shouldUseHydraForRoomVersion } from "../utils/roomVersion.ts";
|
||||
|
||||
export interface IMarkerFoundOptions {
|
||||
/** Whether the timeline was empty before the marker event arrived in the
|
||||
@@ -173,6 +174,9 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
public readonly beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
private _liveBeaconIds: BeaconIdentifier[] = [];
|
||||
|
||||
// We only wants to print warnings about bad room state once.
|
||||
private getVersionWarning = false;
|
||||
|
||||
/**
|
||||
* Construct room state.
|
||||
*
|
||||
@@ -209,6 +213,22 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
this.updateModifiedTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the version of the room
|
||||
* @returns The version of the room
|
||||
*/
|
||||
public getRoomVersion(): string {
|
||||
const createEvent = this.getStateEvents(EventType.RoomCreate, "");
|
||||
if (!createEvent) {
|
||||
if (!this.getVersionWarning) {
|
||||
logger.warn("[getVersion] Room " + this.roomId + " does not have an m.room.create event");
|
||||
this.getVersionWarning = true;
|
||||
}
|
||||
return "1";
|
||||
}
|
||||
return createEvent.getContent()["room_version"] ?? "1";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of joined members in this room
|
||||
* This method caches the result.
|
||||
@@ -468,12 +488,20 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
return;
|
||||
}
|
||||
const members = Object.values(this.members);
|
||||
|
||||
const createEvent = this.getStateEvents(EventType.RoomCreate, "");
|
||||
const creators = getCreators(this.getRoomVersion(), createEvent);
|
||||
|
||||
members.forEach((member) => {
|
||||
// We only propagate `RoomState.members` event if the
|
||||
// power levels has been changed
|
||||
// large room suffer from large re-rendering especially when not needed
|
||||
const oldLastModified = member.getLastModifiedTime();
|
||||
member.setPowerLevelEvent(event);
|
||||
|
||||
if (createEvent) {
|
||||
const pl = powerLevelForUserId(member.userId, event, creators);
|
||||
member.setPowerLevel(pl, event);
|
||||
}
|
||||
if (oldLastModified !== member.getLastModifiedTime()) {
|
||||
this.emit(RoomStateEvent.Members, event, this, member);
|
||||
}
|
||||
@@ -625,9 +653,16 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
|
||||
private updateMember(member: RoomMember): void {
|
||||
// this member may have a power level already, so set it.
|
||||
const createEvent = this.getStateEvents(EventType.RoomCreate, "");
|
||||
const pwrLvlEvent = this.getStateEvents(EventType.RoomPowerLevels, "");
|
||||
if (pwrLvlEvent) {
|
||||
member.setPowerLevelEvent(pwrLvlEvent);
|
||||
if (pwrLvlEvent && createEvent) {
|
||||
const powerLevel = powerLevelForUserId(
|
||||
member.userId,
|
||||
pwrLvlEvent,
|
||||
getCreators(this.getRoomVersion(), createEvent),
|
||||
);
|
||||
|
||||
member.setPowerLevel(powerLevel, pwrLvlEvent);
|
||||
}
|
||||
|
||||
// blow away the sentinel which is now outdated
|
||||
@@ -904,7 +939,6 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
|
||||
let stateDefault = 0;
|
||||
let eventsDefault = 0;
|
||||
let powerLevel = 0;
|
||||
if (powerLevelsEvent) {
|
||||
powerLevels = powerLevelsEvent.getContent();
|
||||
eventsLevels = powerLevels.events || {};
|
||||
@@ -915,13 +949,6 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
stateDefault = 50;
|
||||
}
|
||||
|
||||
const userPowerLevel = powerLevels.users && powerLevels.users[userId];
|
||||
if (Number.isSafeInteger(userPowerLevel)) {
|
||||
powerLevel = userPowerLevel!;
|
||||
} else if (Number.isSafeInteger(powerLevels.users_default)) {
|
||||
powerLevel = powerLevels.users_default!;
|
||||
}
|
||||
|
||||
if (Number.isSafeInteger(powerLevels.events_default)) {
|
||||
eventsDefault = powerLevels.events_default!;
|
||||
}
|
||||
@@ -931,7 +958,11 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
if (Number.isSafeInteger(eventsLevels[eventType])) {
|
||||
requiredLevel = eventsLevels[eventType];
|
||||
}
|
||||
return powerLevel >= requiredLevel;
|
||||
|
||||
const roomMember = this.getMember(userId);
|
||||
const userPowerLevel = roomMember?.powerLevel ?? 0;
|
||||
|
||||
return userPowerLevel >= requiredLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1106,3 +1137,46 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the set of creator user IDs for a room: empty if the room is not a 'hydra' room, otherwise
|
||||
* computed from the sender of the m.room.create event plus the additional_creators field.
|
||||
* @param roomVersion The version of the room
|
||||
* @param roomCreateEvent The m.room.create event for the room
|
||||
* @returns A set of user IDs of the creators of the room.
|
||||
*/
|
||||
function getCreators(roomVersion: string, roomCreateEvent: MatrixEvent | null): Set<string> {
|
||||
const creators = new Set<string>();
|
||||
if (shouldUseHydraForRoomVersion(roomVersion) && roomCreateEvent) {
|
||||
const roomCreateSender = roomCreateEvent.getSender();
|
||||
if (roomCreateSender) creators.add(roomCreateSender);
|
||||
const additionalCreators = roomCreateEvent.getDirectionalContent().additional_creators;
|
||||
if (Array.isArray(additionalCreators)) additionalCreators.forEach((c) => creators.add(c));
|
||||
}
|
||||
return creators;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param userId The user ID to compute the power level for
|
||||
* @param powerLevelEvents The power level event for the room
|
||||
* @param creators The set of creator user IDs for the room if the room is a 'hydra' room, otherwise the empty set.
|
||||
*/
|
||||
function powerLevelForUserId(userId: string, powerLevelEvent: MatrixEvent, creators: Set<string>): number {
|
||||
if (creators.has(userId)) {
|
||||
// As of "Hydra", If the user is a creator, they always have the highest power level
|
||||
return Infinity;
|
||||
} else {
|
||||
const evContent = powerLevelEvent.getDirectionalContent();
|
||||
|
||||
const users: { [userId: string]: number } = evContent.users || {};
|
||||
|
||||
if (users[userId] !== undefined && Number.isInteger(users[userId])) {
|
||||
return users[userId];
|
||||
} else if (evContent.users_default !== undefined) {
|
||||
return evContent.users_default;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-12
@@ -374,7 +374,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
private heroes: Hero[] | null = null;
|
||||
// flags to stop logspam about missing m.room.create events
|
||||
private getTypeWarning = false;
|
||||
private getVersionWarning = false;
|
||||
private membersPromise?: Promise<boolean>;
|
||||
|
||||
// XXX: These should be read-only
|
||||
@@ -505,7 +504,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
this.pendingEventList = [];
|
||||
this.client.store.getPendingEvents(this.roomId).then((events) => {
|
||||
const mapper = this.client.getEventMapper({
|
||||
toDevice: false,
|
||||
decrypt: false,
|
||||
});
|
||||
events.forEach(async (serializedEvent: Partial<IEvent>) => {
|
||||
@@ -606,18 +604,10 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
|
||||
/**
|
||||
* Gets the version of the room
|
||||
* @returns The version of the room, or null if it could not be determined
|
||||
* @returns The version of the room
|
||||
*/
|
||||
public getVersion(): string {
|
||||
const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
if (!createEvent) {
|
||||
if (!this.getVersionWarning) {
|
||||
logger.warn("[getVersion] Room " + this.roomId + " does not have an m.room.create event");
|
||||
this.getVersionWarning = true;
|
||||
}
|
||||
return "1";
|
||||
}
|
||||
return createEvent.getContent()["room_version"] ?? "1";
|
||||
return this.currentState.getRoomVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -439,6 +439,13 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
* has been initialised properly.
|
||||
*/
|
||||
this.replayEvents?.push(event);
|
||||
|
||||
// For annotations (reactions), aggregate immediately (pre-init) to keep
|
||||
// reaction counts/summary visible while the thread is still initialising.
|
||||
// Only aggregate as child: parent aggregation is unnecessary here.
|
||||
if (event.isRelation(RelationType.Annotation)) {
|
||||
this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet);
|
||||
}
|
||||
} else {
|
||||
// Case 2: this is happening later, and we have a timeline. In
|
||||
// this case, these events might be out-of order.
|
||||
@@ -465,10 +472,8 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
|
||||
} else {
|
||||
this.addEventToTimeline(event, toStartOfTimeline);
|
||||
}
|
||||
// Aggregation is handled by EventTimelineSet when inserting/adding.
|
||||
}
|
||||
// Apply annotations and replace relations to the relations of the timeline only
|
||||
this.timelineSet.relations?.aggregateParentEvent(event);
|
||||
this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet);
|
||||
}
|
||||
|
||||
public async processEvent(event: Optional<MatrixEvent>): Promise<void> {
|
||||
|
||||
@@ -125,6 +125,8 @@ export const generateAuthorizationUrl = async (
|
||||
* @param prompt - indicates to the OP which flow the user should see - eg login or registration
|
||||
* See https://openid.net/specs/openid-connect-prompt-create-1_0.html#name-prompt-parameter
|
||||
* @param urlState - value to append to the opaque state identifier to uniquely identify the callback
|
||||
* @param loginHint - value to send as the `login_hint` to the OP, giving a hint about the login identifier the user might use to log in.
|
||||
* See {@link https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest OIDC core 3.1.2.1}.
|
||||
* @returns a Promise with the url as a string
|
||||
*/
|
||||
export const generateOidcAuthorizationUrl = async ({
|
||||
@@ -136,6 +138,7 @@ export const generateOidcAuthorizationUrl = async ({
|
||||
nonce,
|
||||
prompt,
|
||||
urlState,
|
||||
loginHint,
|
||||
}: {
|
||||
clientId: string;
|
||||
metadata: ValidatedAuthMetadata;
|
||||
@@ -145,6 +148,7 @@ export const generateOidcAuthorizationUrl = async ({
|
||||
nonce: string;
|
||||
prompt?: string;
|
||||
urlState?: string;
|
||||
loginHint?: string;
|
||||
}): Promise<string> => {
|
||||
const scope = generateScope();
|
||||
const oidcClient = new OidcClient({
|
||||
@@ -163,6 +167,7 @@ export const generateOidcAuthorizationUrl = async ({
|
||||
nonce,
|
||||
prompt,
|
||||
url_state: urlState,
|
||||
login_hint: loginHint,
|
||||
});
|
||||
|
||||
return request.url;
|
||||
|
||||
@@ -326,12 +326,22 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
* Implementation of {@link CryptoBackend.maybeAcceptKeyBundle}.
|
||||
*/
|
||||
public async maybeAcceptKeyBundle(roomId: string, inviter: string): Promise<void> {
|
||||
// TODO: retry this if it gets interrupted or it fails.
|
||||
// TODO: retry this if it gets interrupted or it fails. (https://github.com/matrix-org/matrix-rust-sdk/issues/5112)
|
||||
// TODO: do this in the background.
|
||||
// TODO: handle the bundle message arriving after the invite.
|
||||
// TODO: handle the bundle message arriving after the invite (https://github.com/element-hq/element-web/issues/30740)
|
||||
|
||||
const logger = new LogSpan(this.logger, `maybeAcceptKeyBundle(${roomId}, ${inviter})`);
|
||||
|
||||
// Make sure we have an up-to-date idea of the inviter's cross-signing keys, so that we can check if the
|
||||
// device that sent us the bundle data was correctly cross-signed.
|
||||
//
|
||||
// TODO: it would be nice to skip this step if we have an up-to-date copy of the inviter's cross-signing keys,
|
||||
// but we don't have an easy way to check that. Possibly the rust side could trigger a key request and then
|
||||
// block until it happens.
|
||||
logger.info(`Checking inviter cross-signing keys`);
|
||||
const request = this.olmMachine.queryKeysForUsers([new RustSdkCryptoJs.UserId(inviter)]);
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
|
||||
|
||||
const bundleData = await this.olmMachine.getReceivedRoomKeyBundleData(
|
||||
new RustSdkCryptoJs.RoomId(roomId),
|
||||
new RustSdkCryptoJs.UserId(inviter),
|
||||
@@ -1076,7 +1086,12 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
verificationMethodIdentifierToMethod(method),
|
||||
);
|
||||
// Get the request content to send to the DM room
|
||||
const verificationEventContent: string = await userIdentity.verificationRequestContent(methods);
|
||||
const verCont: string = await userIdentity.verificationRequestContent(methods);
|
||||
|
||||
// TODO: due to https://github.com/matrix-org/matrix-rust-sdk/issues/5643, we need to fix up the verification request content to include `msgtype`.
|
||||
const verContObj = JSON.parse(verCont);
|
||||
verContObj["msgtype"] = "m.key.verification.request";
|
||||
const verificationEventContent: string = JSON.stringify(verContObj);
|
||||
|
||||
// Send the request content to send to the DM room
|
||||
const eventId = await this.sendVerificationRequestContent(roomId, verificationEventContent);
|
||||
|
||||
@@ -317,7 +317,10 @@ export class SyncAccumulator {
|
||||
break;
|
||||
|
||||
case Category.Join:
|
||||
if (this.inviteRooms[roomId]) {
|
||||
if (this.knockRooms[roomId]) {
|
||||
// delete knock state on join
|
||||
delete this.knockRooms[roomId];
|
||||
} else if (this.inviteRooms[roomId]) {
|
||||
// (1)
|
||||
// was previously invite, now join. We expect /sync to give
|
||||
// the entire state and timeline on 'join', so delete previous
|
||||
|
||||
@@ -435,18 +435,6 @@ export function immediate(): Promise<void> {
|
||||
export function isNullOrUndefined(val: any): boolean {
|
||||
return val === null || val === undefined;
|
||||
}
|
||||
/**
|
||||
* @deprecated use {@link PromiseWithResolvers} instead.
|
||||
*/
|
||||
export type IDeferred<T> = PromiseWithResolvers<T>;
|
||||
|
||||
/**
|
||||
* Creates a deferred promise. This is a promise that can be resolved or rejected.
|
||||
* @deprecated use {@link Promise.withResolvers} instead.
|
||||
*/
|
||||
export function defer<T = void>(): IDeferred<T> {
|
||||
return Promise.withResolvers<T>();
|
||||
}
|
||||
|
||||
export async function promiseMapSeries<T>(
|
||||
promises: Array<T | Promise<T>>,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright 2025 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Room versions strings that we know about and do not use hydra semantics.
|
||||
*/
|
||||
const PRE_HYDRA_ROOM_VERSIONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"];
|
||||
|
||||
/**
|
||||
* Checks if the given room version is one where new "hydra" power level
|
||||
* semantics (ie. room version 12 or later) should be used
|
||||
* (see https://github.com/matrix-org/matrix-spec-proposals/pull/4289).
|
||||
* This will return `false` for versions that are known to the js-sdk and
|
||||
* do not use hydra: any room versions unknown to the js-sdk (experimental or
|
||||
* otherwise) will cause the function to return true.
|
||||
*
|
||||
* @param roomVersion - The version of the room to check.
|
||||
* @returns `true` if hydra semantics should be used for the room version, `false` otherwise.
|
||||
*/
|
||||
export function shouldUseHydraForRoomVersion(roomVersion: string): boolean {
|
||||
return !PRE_HYDRA_ROOM_VERSIONS.includes(roomVersion);
|
||||
}
|
||||
Reference in New Issue
Block a user