Compare commits
91 Commits
v23.4.0
...
v24.1.0-rc.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f97853011 | |||
| 2a5c4b1edf | |||
| 65a3c6707c | |||
| 942477f0bf | |||
| 1770b3131a | |||
| 41d3ffdab9 | |||
| 78aa6cb62b | |||
| b56b8040e6 | |||
| d1cf98b177 | |||
| 5fc6b3ed17 | |||
| 6e41533fed | |||
| bc76532bd5 | |||
| 62f1dd79bc | |||
| fd3f53e814 | |||
| 798ac7b94c | |||
| eb0c0f7b93 | |||
| f03293f53d | |||
| 7d062387b7 | |||
| 9acf3b18ca | |||
| b41d067c94 | |||
| c8503b3120 | |||
| da03c3b529 | |||
| d48b19e052 | |||
| 6861c67f56 | |||
| c87048bd9f | |||
| 02269f33b7 | |||
| 7f46ae7b97 | |||
| 2ab3566f95 | |||
| 9a504af18e | |||
| 8b50986906 | |||
| f9a222ecea | |||
| 5b5a3d8b5e | |||
| 037cbdd214 | |||
| 0349411e6d | |||
| d7b75e4b9e | |||
| 254f043ab0 | |||
| 5f3e115545 | |||
| fc55c4c72a | |||
| f795577e14 | |||
| f12cee984a | |||
| c3b4572841 | |||
| 40fe159c10 | |||
| ddecc87947 | |||
| 23837266fc | |||
| 3c9ca8c373 | |||
| 7f2a4c2568 | |||
| 2ad647a73c | |||
| 26663e67fd | |||
| 0cfc67c679 | |||
| 7faba5c2f0 | |||
| 1d9250b277 | |||
| 08054c1d6d | |||
| 333872e878 | |||
| 913cd257f4 | |||
| 69f7789c40 | |||
| e79ef1f33a | |||
| fb8f61a5ec | |||
| 9b1f2a1d11 | |||
| 686216fb75 | |||
| f4b83e1a27 | |||
| 97f21b6635 | |||
| 87641a6803 | |||
| 7e4331172a | |||
| a976080d1b | |||
| bcf3bba44e | |||
| 54ac36d424 | |||
| e84c90dbbc | |||
| 4424438658 | |||
| 13d95c8219 | |||
| e119dc4e89 | |||
| b4cdc5a923 | |||
| a82e22b5de | |||
| c894d09d8c | |||
| 585ce07260 | |||
| 8cbbdaa239 | |||
| cd526a254d | |||
| e782a2afa3 | |||
| 565339b1fd | |||
| 493203050a | |||
| 41782c4593 | |||
| 86256a4e74 | |||
| 933a0c9909 | |||
| c8a4d9b88a | |||
| 437128d11b | |||
| c18d09fd22 | |||
| d80b7499fd | |||
| 9c8093eb3e | |||
| aec1c11037 | |||
| e2e9986059 | |||
| d70ffdbc02 | |||
| 21cc9c3d8a |
@@ -69,9 +69,6 @@ module.exports = {
|
||||
// TODO: There are many tests with invalid expects that should be fixed,
|
||||
// https://github.com/matrix-org/matrix-js-sdk/issues/2976
|
||||
"jest/valid-expect": "off",
|
||||
// TODO: There are many cases to refactor away,
|
||||
// https://github.com/matrix-org/matrix-js-sdk/issues/2978
|
||||
"jest/no-conditional-expect": "off",
|
||||
// Also treat "oldBackendOnly" as a test function.
|
||||
// Used in some crypto tests.
|
||||
"jest/no-standalone-expect": [
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
)
|
||||
)
|
||||
steps:
|
||||
- uses: tibdex/backport@v2
|
||||
- uses: tibdex/backport@2e217641d82d02ba0603f46b1aeedefb258890ac # v2
|
||||
with:
|
||||
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
|
||||
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@bd10f381a96414ce2b13a11bfa89902ba7cea07f # v2.24.3
|
||||
uses: dawidd6/action-download-artifact@5e780fc7bbd0cac69fc73271ed86edf5dcb72d67 # v2
|
||||
with:
|
||||
workflow: static_analysis.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
uses: peter-evans/repository-dispatch@26b39ed245ab8f31526069329e112ab2fb224588 # v2
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: ${{ matrix.repo }}
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Check membership
|
||||
uses: tspascoal/get-user-teams-membership@v2
|
||||
uses: tspascoal/get-user-teams-membership@37c08f7b52a72ca95d12af2e7ab2553ca9adf13b # v2
|
||||
id: teams
|
||||
with:
|
||||
username: ${{ github.event.pull_request.user.login }}
|
||||
|
||||
@@ -20,11 +20,11 @@ jobs:
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --pure-lockfile"
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: 🚀 Publish to npm
|
||||
id: npm-publish
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
uses: JS-DevTools/npm-publish@0f451a94170d1699fd50710966d48fb26194d939 # v1
|
||||
with:
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
access: public
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
cache: "yarn"
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --pure-lockfile"
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- name: 📖 Generate JSDoc
|
||||
run: "yarn gendoc"
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: 🚀 Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
uses: peaceiris/actions-gh-pages@bd8c6b06eba6b3d25d72b7a1767993c0aeee42e7 # v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
keep_files: true
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
# We create the status here and then update it to success/failure in the `report` stage
|
||||
# This provides an easy link to this workflow_run from the PR before Cypress is done.
|
||||
- uses: Sibz/github-status-action@v1
|
||||
- uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
coverage_extract_path: coverage
|
||||
extra_args: ${{ inputs.extra_args }}
|
||||
|
||||
- uses: Sibz/github-status-action@v1
|
||||
- uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -11,6 +11,7 @@ jobs:
|
||||
# This is a workaround for https://github.com/SonarSource/SonarJS/issues/578
|
||||
prepare:
|
||||
name: Prepare
|
||||
if: github.event.workflow_run.event != 'merge_group'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
reportPaths: ${{ steps.extra_args.outputs.reportPaths }}
|
||||
@@ -19,7 +20,7 @@ jobs:
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
uses: dawidd6/action-download-artifact@5e780fc7bbd0cac69fc73271ed86edf5dcb72d67 # v2
|
||||
with:
|
||||
workflow: tests.yaml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
@@ -35,6 +36,7 @@ jobs:
|
||||
|
||||
sonarqube:
|
||||
name: 🩻 SonarQube
|
||||
if: github.event.workflow_run.event != 'merge_group'
|
||||
needs: prepare
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||
secrets:
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Get number of CPU cores
|
||||
id: cpu-cores
|
||||
uses: SimenB/github-actions-cpu-cores@v1
|
||||
uses: SimenB/github-actions-cpu-cores@410541432439795d30db6501fb1d8178eb41e502 # v1
|
||||
|
||||
- name: Load metrics reporter
|
||||
id: metrics
|
||||
@@ -46,16 +46,20 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
yarn coverage \
|
||||
yarn ${{ github.event_name == 'merge_group' && 'test' || 'coverage' }} \
|
||||
--ci \
|
||||
--reporters github-actions ${{ steps.metrics.outputs.extra-reporter }} \
|
||||
--max-workers ${{ steps.cpu-cores.outputs.count }} \
|
||||
./spec/${{ matrix.specs }}
|
||||
mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
|
||||
env:
|
||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
||||
|
||||
- name: Move coverage files into place
|
||||
if: github.event_name != 'merge_group'
|
||||
run: mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
|
||||
|
||||
- name: Upload Artifact
|
||||
if: github.event_name != 'merge_group'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage
|
||||
@@ -63,6 +67,22 @@ jobs:
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
|
||||
skip_sonar:
|
||||
name: Skip SonarCloud on merge_queue
|
||||
if: github.event_name == 'merge_group'
|
||||
runs-on: ubuntu-latest
|
||||
needs: jest
|
||||
steps:
|
||||
- name: Skip SonarCloud
|
||||
uses: Sibz/github-status-action@faaa4d96fecf273bd762985e0e7f9f933c774918 # v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
description: SonarCloud skipped
|
||||
context: SonarCloud Code Analysis
|
||||
sha: ${{ github.sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
matrix-react-sdk:
|
||||
name: Downstream test matrix-react-sdk
|
||||
if: github.event_name == 'merge_group'
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
uses: peter-evans/create-pull-request@2b011faafdcbc9ceb11414d64d0573f37c774b04 # v4
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/upgrade-deps
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
T-Task
|
||||
|
||||
- name: Enable automerge
|
||||
uses: peter-evans/enable-pull-request-automerge@v2
|
||||
uses: peter-evans/enable-pull-request-automerge@684fed02ccc9b5eefcf7d40b65b3cd44255bd5bc # v2
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
@@ -19,3 +19,4 @@ out
|
||||
|
||||
.vscode
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
+38
-2
@@ -1,3 +1,41 @@
|
||||
Changes in [24.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v24.1.0-rc.1) (2023-04-04)
|
||||
============================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Allow via_servers property in findPredecessor (update to MSC3946) ([\#3240](https://github.com/matrix-org/matrix-js-sdk/pull/3240)). Contributed by @andybalaam.
|
||||
* Fire `closed` event when IndexedDB closes unexpectedly ([\#3218](https://github.com/matrix-org/matrix-js-sdk/pull/3218)).
|
||||
* Implement MSC3952: intentional mentions ([\#3092](https://github.com/matrix-org/matrix-js-sdk/pull/3092)). Fixes vector-im/element-web#24376.
|
||||
* Send one time key count and unused fallback keys for rust-crypto ([\#3215](https://github.com/matrix-org/matrix-js-sdk/pull/3215)). Fixes vector-im/element-web#24795. Contributed by @florianduros.
|
||||
* Improve `processBeaconEvents` hotpath ([\#3200](https://github.com/matrix-org/matrix-js-sdk/pull/3200)).
|
||||
* Implement MSC3966: a push rule condition to check if an array contains a value ([\#3180](https://github.com/matrix-org/matrix-js-sdk/pull/3180)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* indexddb-local-backend - return the current sync to database promise … ([\#3222](https://github.com/matrix-org/matrix-js-sdk/pull/3222)). Contributed by @texuf.
|
||||
* Revert "Add the call object to Call events" ([\#3236](https://github.com/matrix-org/matrix-js-sdk/pull/3236)).
|
||||
* Handle group call redaction ([\#3231](https://github.com/matrix-org/matrix-js-sdk/pull/3231)). Fixes vector-im/voip-internal#128.
|
||||
* Stop doing O(n^2) work to find event's home (`eventShouldLiveIn`) ([\#3227](https://github.com/matrix-org/matrix-js-sdk/pull/3227)). Contributed by @jryans.
|
||||
* Fix bug where video would not unmute if it started muted ([\#3213](https://github.com/matrix-org/matrix-js-sdk/pull/3213)). Fixes vector-im/element-call#925.
|
||||
* Fixes to event encryption in the Rust Crypto implementation ([\#3202](https://github.com/matrix-org/matrix-js-sdk/pull/3202)).
|
||||
|
||||
Changes in [24.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v24.0.0) (2023-03-28)
|
||||
==================================================================================================
|
||||
|
||||
## 🔒 Security
|
||||
* Fixes for [CVE-2023-28427](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE-2023-28427) / GHSA-mwq8-fjpf-c2gr
|
||||
|
||||
Changes in [23.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.5.0) (2023-03-15)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Implement MSC3758: a push rule condition to match event properties exactly ([\#3179](https://github.com/matrix-org/matrix-js-sdk/pull/3179)).
|
||||
* Enable group calls without video and audio track by configuration of MatrixClient ([\#3162](https://github.com/matrix-org/matrix-js-sdk/pull/3162)). Contributed by @EnricoSchw.
|
||||
* Updates to protocol used for Sign in with QR code ([\#3155](https://github.com/matrix-org/matrix-js-sdk/pull/3155)). Contributed by @hughns.
|
||||
* Implement MSC3873 to handle escaped dots in push rule keys ([\#3134](https://github.com/matrix-org/matrix-js-sdk/pull/3134)). Fixes undefined/matrix-js-sdk#1454.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix spec compliance issue around encrypted `m.relates_to` ([\#3178](https://github.com/matrix-org/matrix-js-sdk/pull/3178)).
|
||||
* Fix reactions in threads sometimes causing stuck notifications ([\#3146](https://github.com/matrix-org/matrix-js-sdk/pull/3146)). Fixes vector-im/element-web#24000. Contributed by @justjanne.
|
||||
|
||||
Changes in [23.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.4.0) (2023-02-28)
|
||||
==================================================================================================
|
||||
|
||||
@@ -7,8 +45,6 @@ Changes in [23.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23
|
||||
* Polls: count undecryptable poll relations ([\#3163](https://github.com/matrix-org/matrix-js-sdk/pull/3163)). Contributed by @kerryarchibald.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix spec compliance issue around encrypted `m.relates_to` ([\#3178](https://github.com/matrix-org/matrix-js-sdk/pull/3178)).
|
||||
* Fix reactions in threads sometimes causing stuck notifications ([\#3146](https://github.com/matrix-org/matrix-js-sdk/pull/3146)). Fixes vector-im/element-web#24000. Contributed by @justjanne.
|
||||
* Better type guard parseTopicContent ([\#3165](https://github.com/matrix-org/matrix-js-sdk/pull/3165)). Fixes matrix-org/element-web-rageshakes#20177 and matrix-org/element-web-rageshakes#20178.
|
||||
* Fix a bug where events in encrypted rooms would sometimes erroneously increment the total unread counter after being processed locally. ([\#3130](https://github.com/matrix-org/matrix-js-sdk/pull/3130)). Fixes vector-im/element-web#24448. Contributed by @Half-Shot.
|
||||
* Stop the ICE disconnected timer on call terminate ([\#3147](https://github.com/matrix-org/matrix-js-sdk/pull/3147)).
|
||||
|
||||
+29
-8
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "23.4.0",
|
||||
"version": "24.1.0-rc.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
@@ -14,7 +14,7 @@
|
||||
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
|
||||
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
|
||||
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"build:compile-browser": "mkdir dist && browserify -d src/browser-index.ts -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
|
||||
"build:compile-browser": "mkdir dist && BROWSERIFYSWAP_ENV='no-rust-crypto' browserify -d src/browser-index.ts -p [ tsify -p ./tsconfig-build.json ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
|
||||
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
|
||||
"gendoc": "typedoc",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
@@ -55,13 +55,13 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.3",
|
||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.5",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-widget-api": "^1.0.0",
|
||||
"matrix-widget-api": "^1.3.1",
|
||||
"p-retry": "4",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"unhomoglyph": "^1.0.6",
|
||||
@@ -97,19 +97,20 @@
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"browserify-swap": "^0.2.2",
|
||||
"debug": "^4.3.4",
|
||||
"docdash": "^2.0.0",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.33.0",
|
||||
"eslint": "8.35.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "^27.1.6",
|
||||
"eslint-plugin-jsdoc": "^39.6.4",
|
||||
"eslint-plugin-jsdoc": "^40.0.0",
|
||||
"eslint-plugin-matrix-org": "^1.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.17",
|
||||
"eslint-plugin-unicorn": "^45.0.0",
|
||||
"eslint-plugin-unicorn": "^46.0.0",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
@@ -118,7 +119,7 @@
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^29.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"prettier": "2.8.3",
|
||||
"prettier": "2.8.4",
|
||||
"rimraf": "^4.0.0",
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.2",
|
||||
@@ -148,5 +149,25 @@
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"browserify-swap",
|
||||
[
|
||||
"babelify",
|
||||
{
|
||||
"sourceMaps": "inline",
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-typescript"
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"browserify-swap": {
|
||||
"no-rust-crypto": {
|
||||
"src/rust-crypto/index.ts$": "./src/rust-crypto/browserify-index.ts"
|
||||
}
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
|
||||
+2
-2
@@ -130,7 +130,7 @@ fi
|
||||
# global cache here to ensure we get the right thing.
|
||||
yarn cache clean
|
||||
# Ensure all dependencies are updated
|
||||
yarn install --ignore-scripts --pure-lockfile
|
||||
yarn install --ignore-scripts --frozen-lockfile
|
||||
|
||||
# ignore leading v on release
|
||||
release="${1#v}"
|
||||
@@ -225,7 +225,7 @@ if [ $dodist -eq 0 ]; then
|
||||
pushd "$builddir"
|
||||
git clone "$projdir" .
|
||||
git checkout "$rel_branch"
|
||||
yarn install --pure-lockfile
|
||||
yarn install --frozen-lockfile
|
||||
# We haven't tagged yet, so tell the dist script what version
|
||||
# it's building
|
||||
DIST_VERSION="$tag" yarn dist
|
||||
|
||||
+183
-29
@@ -19,7 +19,7 @@ import anotherjson from "another-json";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import { MockResponse } from "fetch-mock";
|
||||
import { MockResponse, MockResponseFunction } from "fetch-mock";
|
||||
|
||||
import type { IDeviceKeys } from "../../src/@types/crypto";
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
import { E2EKeyReceiver, IE2EKeyReceiver } from "../test-utils/E2EKeyReceiver";
|
||||
import { ISyncResponder, SyncResponder } from "../test-utils/SyncResponder";
|
||||
import { escapeRegExp } from "../../src/utils";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -341,6 +342,11 @@ async function expectSendRoomKey(
|
||||
resolve(onSendRoomKey(content));
|
||||
return {};
|
||||
},
|
||||
{
|
||||
// append to the list of intercepts on this path (since we have some tests that call
|
||||
// this function multiple times)
|
||||
overwriteRoutes: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -359,12 +365,20 @@ async function expectSendMegolmMessage(
|
||||
inboundGroupSessionPromise: Promise<Olm.InboundGroupSession>,
|
||||
): Promise<Partial<IEvent>> {
|
||||
const encryptedMessageContent = await new Promise<IContent>((resolve) => {
|
||||
fetchMock.putOnce(new RegExp("/send/m.room.encrypted/"), (url: string, opts: RequestInit): MockResponse => {
|
||||
resolve(JSON.parse(opts.body as string));
|
||||
return {
|
||||
event_id: "$event_id",
|
||||
};
|
||||
});
|
||||
fetchMock.putOnce(
|
||||
new RegExp("/send/m.room.encrypted/"),
|
||||
(url: string, opts: RequestInit): MockResponse => {
|
||||
resolve(JSON.parse(opts.body as string));
|
||||
return {
|
||||
event_id: "$event_id",
|
||||
};
|
||||
},
|
||||
{
|
||||
// append to the list of intercepts on this path (since we have some tests that call
|
||||
// this function multiple times)
|
||||
overwriteRoutes: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// In some of the tests, the room key is sent *after* the actual event, so we may need to wait for it now.
|
||||
@@ -438,8 +452,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
|
||||
fetchMock.postOnce(
|
||||
new URL("/_matrix/client/r0/keys/query", aliceClient.getHomeserverUrl()).toString(),
|
||||
new RegExp(rootRegexp + "(r0|v3)/keys/query"),
|
||||
(url: string, opts: RequestInit) => onQueryRequest(JSON.parse(opts.body as string)),
|
||||
{
|
||||
// append to the list of intercepts on this path
|
||||
@@ -448,6 +463,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an expectation for a /keys/claim request for the MatrixClient under test
|
||||
*
|
||||
* @param response - the response to return from the request. Normally an {@link IClaimOTKsResult}
|
||||
* (or a function that returns one).
|
||||
*/
|
||||
function expectAliceKeyClaim(response: MockResponse | MockResponseFunction) {
|
||||
const rootRegexp = escapeRegExp(new URL("/_matrix/client/", aliceClient.getHomeserverUrl()).toString());
|
||||
fetchMock.postOnce(new RegExp(rootRegexp + "(r0|v3)/keys/claim"), response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the device keys for testOlmAccount in a format suitable for a
|
||||
* response to /keys/query
|
||||
@@ -536,6 +562,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
it("MatrixClient.getCrypto returns a CryptoApi", () => {
|
||||
expect(aliceClient.getCrypto()).toHaveProperty("globalBlacklistUnverifiedDevices");
|
||||
});
|
||||
|
||||
it("Alice receives a megolm message", async () => {
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
@@ -543,7 +573,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
// if we're using the old crypto impl, stub out some methods in the device manager.
|
||||
// TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic.
|
||||
if (aliceClient.crypto) {
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
aliceClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
}
|
||||
|
||||
@@ -603,7 +633,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
// if we're using the old crypto impl, stub out some methods in the device manager.
|
||||
// TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic.
|
||||
if (aliceClient.crypto) {
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
aliceClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
}
|
||||
|
||||
@@ -671,7 +701,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
// if we're using the old crypto impl, stub out some methods in the device manager.
|
||||
// TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic.
|
||||
if (aliceClient.crypto) {
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
aliceClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
}
|
||||
|
||||
@@ -738,7 +768,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expect(event.getContent().body).toEqual("42");
|
||||
});
|
||||
|
||||
oldBackendOnly("prepareToEncrypt", async () => {
|
||||
it("prepareToEncrypt", async () => {
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
aliceClient.setGlobalErrorOnUnknownDevices(false);
|
||||
@@ -751,10 +781,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz"));
|
||||
|
||||
// ... and then claim one of his OTKs
|
||||
fetchMock.postOnce(
|
||||
new URL("/_matrix/client/r0/keys/claim", aliceClient.getHomeserverUrl()).toString(),
|
||||
getTestKeysClaimResponse("@bob:xyz"),
|
||||
);
|
||||
expectAliceKeyClaim(getTestKeysClaimResponse("@bob:xyz"));
|
||||
|
||||
// fire off the prepare request
|
||||
const room = aliceClient.getRoom(ROOM_ID);
|
||||
@@ -768,7 +795,71 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
await p;
|
||||
});
|
||||
|
||||
it("Alice sends a megolm message with GlobalErrorOnUnknownDevices=false", async () => {
|
||||
aliceClient.setGlobalErrorOnUnknownDevices(false);
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
// Alice shares a room with Bob
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"]));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Once we send the message, Alice will check Bob's device list (twice, because reasons) ...
|
||||
expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz"));
|
||||
expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz"));
|
||||
|
||||
// ... and claim one of his OTKs ...
|
||||
expectAliceKeyClaim(getTestKeysClaimResponse("@bob:xyz"));
|
||||
|
||||
// ... and send an m.room_key message
|
||||
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||
|
||||
// Finally, send the message, and expect to get an `m.room.encrypted` event that we can decrypt.
|
||||
await Promise.all([
|
||||
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
||||
expectSendMegolmMessage(inboundGroupSessionPromise),
|
||||
]);
|
||||
});
|
||||
|
||||
it("We should start a new megolm session after forceDiscardSession", async () => {
|
||||
aliceClient.setGlobalErrorOnUnknownDevices(false);
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
// Alice shares a room with Bob
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse(["@bob:xyz"]));
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
// Once we send the message, Alice will check Bob's device list (twice, because reasons) ...
|
||||
expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz"));
|
||||
expectAliceKeyQuery(getTestKeysQueryResponse("@bob:xyz"));
|
||||
|
||||
// ... and claim one of his OTKs ...
|
||||
expectAliceKeyClaim(getTestKeysClaimResponse("@bob:xyz"));
|
||||
|
||||
// ... and send an m.room_key message
|
||||
const inboundGroupSessionPromise = expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||
|
||||
// Send the first message, and check we can decrypt it.
|
||||
await Promise.all([
|
||||
aliceClient.sendTextMessage(ROOM_ID, "test"),
|
||||
expectSendMegolmMessage(inboundGroupSessionPromise),
|
||||
]);
|
||||
|
||||
// Finally the interesting part: discard the session.
|
||||
aliceClient.forceDiscardSession(ROOM_ID);
|
||||
|
||||
// Now when we send the next message, we should get a *new* megolm session.
|
||||
const inboundGroupSessionPromise2 = expectSendRoomKey("@bob:xyz", testOlmAccount);
|
||||
const p2 = expectSendMegolmMessage(inboundGroupSessionPromise2);
|
||||
await Promise.all([aliceClient.sendTextMessage(ROOM_ID, "test2"), p2]);
|
||||
});
|
||||
|
||||
oldBackendOnly("Alice sends a megolm message", async () => {
|
||||
// TODO: do something about this for the rust backend.
|
||||
// Currently it fails because we don't respect the default GlobalErrorOnUnknownDevices and
|
||||
// send messages to unknown devices.
|
||||
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
const p2pSession = await establishOlmSession(aliceClient, keyReceiver, syncResponder, testOlmAccount);
|
||||
@@ -1027,20 +1118,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
throw new Error("sendTextMessage succeeded on an unknown device");
|
||||
} catch (e) {
|
||||
expect((e as any).name).toEqual("UnknownDeviceError");
|
||||
expect(Object.keys((e as any).devices)).toEqual([aliceClient.getUserId()!]);
|
||||
expect(Object.keys((e as any)?.devices[aliceClient.getUserId()!])).toEqual(["DEVICE_ID"]);
|
||||
expect([...(e as any).devices.keys()]).toEqual([aliceClient.getUserId()!]);
|
||||
expect((e as any).devices.get(aliceClient.getUserId()!).has("DEVICE_ID"));
|
||||
}
|
||||
|
||||
// mark the device as known, and resend.
|
||||
aliceClient.setDeviceKnown(aliceClient.getUserId()!, "DEVICE_ID");
|
||||
fetchMock.postOnce(
|
||||
new URL("/_matrix/client/r0/keys/claim", aliceClient.getHomeserverUrl()).toString(),
|
||||
(url: string, opts: RequestInit): MockResponse => {
|
||||
const content = JSON.parse(opts.body as string);
|
||||
expect(content.one_time_keys[aliceClient.getUserId()!].DEVICE_ID).toEqual("signed_curve25519");
|
||||
return getTestKeysClaimResponse(aliceClient.getUserId()!);
|
||||
},
|
||||
);
|
||||
expectAliceKeyClaim((url: string, opts: RequestInit): MockResponse => {
|
||||
const content = JSON.parse(opts.body as string);
|
||||
expect(content.one_time_keys[aliceClient.getUserId()!].DEVICE_ID).toEqual("signed_curve25519");
|
||||
return getTestKeysClaimResponse(aliceClient.getUserId()!);
|
||||
});
|
||||
|
||||
const inboundGroupSessionPromise = expectSendRoomKey(aliceClient.getUserId()!, testOlmAccount);
|
||||
|
||||
@@ -1099,7 +1187,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
// if we're using the old crypto impl, stub out some methods in the device manager.
|
||||
// TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic.
|
||||
if (aliceClient.crypto) {
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
aliceClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
}
|
||||
|
||||
@@ -1255,7 +1343,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
// if we're using the old crypto impl, stub out some methods in the device manager.
|
||||
// TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic.
|
||||
if (aliceClient.crypto) {
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
aliceClient.crypto.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
aliceClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
}
|
||||
|
||||
@@ -1322,7 +1410,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
// if we're using the old crypto impl, stub out some methods in the device manager.
|
||||
// TODO: replace this with intercepts of the /keys/query endpoint to make it impl agnostic.
|
||||
if (aliceClient.crypto) {
|
||||
aliceClient.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
aliceClient.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
aliceClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
aliceClient.crypto!.deviceList.getUserByIdentityKey = () => beccaTestClient.client.getUserId()!;
|
||||
}
|
||||
@@ -1849,4 +1937,70 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
expect(event.getContent().body).not.toContain("withheld");
|
||||
});
|
||||
});
|
||||
|
||||
describe("key upload request", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
function listenToUpload(): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
const listener = (url: string, options: RequestInit) => {
|
||||
const content = JSON.parse(options.body as string);
|
||||
const keysCount = Object.keys(content?.one_time_keys || {}).length;
|
||||
if (keysCount) resolve(keysCount);
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: keysCount ? 60 : keysCount,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// catch both r0 and v3 variants
|
||||
fetchMock.post(
|
||||
new URL("/_matrix/client/r0/keys/upload", aliceClient.getHomeserverUrl()).toString(),
|
||||
listener,
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
fetchMock.post(
|
||||
new URL("/_matrix/client/v3/keys/upload", aliceClient.getHomeserverUrl()).toString(),
|
||||
listener,
|
||||
{
|
||||
overwriteRoutes: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
it("should make key upload request after sync", async () => {
|
||||
let uploadPromise = listenToUpload();
|
||||
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await startClientAndAwaitFirstSync();
|
||||
|
||||
syncResponder.sendOrQueueSyncResponse(getSyncResponse([]));
|
||||
|
||||
await syncPromise(aliceClient);
|
||||
expect(await uploadPromise).toBeGreaterThan(0);
|
||||
|
||||
uploadPromise = listenToUpload();
|
||||
syncResponder.sendOrQueueSyncResponse({
|
||||
next_batch: 2,
|
||||
device_one_time_keys_count: { signed_curve25519: 0 },
|
||||
});
|
||||
|
||||
// Advance local date to 2 minutes
|
||||
// The old crypto only runs the upload every 60 seconds
|
||||
jest.setSystemTime(Date.now() + 2 * 60 * 1000);
|
||||
|
||||
await syncPromise(aliceClient);
|
||||
|
||||
expect(await uploadPromise).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -603,14 +603,14 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
const prom = client!.downloadKeys(["boris", "chaz"]).then(function (res) {
|
||||
assertObjectContains(res.boris.dev1, {
|
||||
assertObjectContains(res.get("boris")!.get("dev1")!, {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev1": ed25519key },
|
||||
algorithms: ["1"],
|
||||
unsigned: { abc: "def" },
|
||||
});
|
||||
|
||||
assertObjectContains(res.chaz.dev2, {
|
||||
assertObjectContains(res.get("chaz")!.get("dev2")!, {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev2": ed25519key },
|
||||
algorithms: ["2"],
|
||||
@@ -1336,18 +1336,25 @@ describe("MatrixClient", function () {
|
||||
it.each([
|
||||
{
|
||||
userId: "alice@localhost",
|
||||
powerLevel: 100,
|
||||
expectation: {
|
||||
"alice@localhost": 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
userId: ["alice@localhost", "bob@localhost"],
|
||||
powerLevel: 100,
|
||||
expectation: {
|
||||
"alice@localhost": 100,
|
||||
"bob@localhost": 100,
|
||||
},
|
||||
},
|
||||
])("should modify power levels of $userId correctly", async ({ userId, expectation }) => {
|
||||
{
|
||||
userId: "alice@localhost",
|
||||
powerLevel: undefined,
|
||||
expectation: {},
|
||||
},
|
||||
])("should modify power levels of $userId correctly", async ({ userId, powerLevel, expectation }) => {
|
||||
const event = {
|
||||
getType: () => "m.room.power_levels",
|
||||
getContent: () => ({
|
||||
@@ -1364,7 +1371,7 @@ describe("MatrixClient", function () {
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
const prom = client!.setPowerLevel("!room_id:server", userId, 100, event);
|
||||
const prom = client!.setPowerLevel("!room_id:server", userId, powerLevel, event);
|
||||
await httpBackend!.flushAllExpected();
|
||||
await prom;
|
||||
});
|
||||
|
||||
@@ -157,14 +157,10 @@ describe("MatrixClient opts", function () {
|
||||
error: "Ruh roh",
|
||||
}),
|
||||
);
|
||||
try {
|
||||
await Promise.all([
|
||||
expect(client.sendTextMessage("!foo:bar", "a body", "txn1")).rejects.toThrow(),
|
||||
httpBackend.flush("/txn1", 1),
|
||||
]);
|
||||
} catch (err) {
|
||||
expect((<MatrixError>err).errcode).toEqual("M_SOMETHING");
|
||||
}
|
||||
|
||||
await expect(
|
||||
Promise.all([client.sendTextMessage("!foo:bar", "a body", "txn1"), httpBackend.flush("/txn1", 1)]),
|
||||
).rejects.toThrow("MatrixError: [500] Unknown message");
|
||||
});
|
||||
|
||||
it("shouldn't queue events", async () => {
|
||||
|
||||
@@ -18,8 +18,21 @@ import "fake-indexeddb/auto";
|
||||
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import { Category, ISyncResponse, MatrixClient, NotificationCountType, Room } from "../../src";
|
||||
import {
|
||||
Category,
|
||||
ClientEvent,
|
||||
EventType,
|
||||
ISyncResponse,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
NotificationCountType,
|
||||
RelationType,
|
||||
Room,
|
||||
} from "../../src";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { mkThread } from "../test-utils/thread";
|
||||
import { SyncState } from "../../src/sync";
|
||||
|
||||
describe("MatrixClient syncing", () => {
|
||||
const userA = "@alice:localhost";
|
||||
@@ -51,6 +64,86 @@ describe("MatrixClient syncing", () => {
|
||||
return httpBackend!.stop();
|
||||
});
|
||||
|
||||
it("reactions in thread set the correct timeline to unread", async () => {
|
||||
const roomId = "!room:localhost";
|
||||
|
||||
// start the client, and wait for it to initialise
|
||||
httpBackend!.when("GET", "/sync").respond(200, {
|
||||
next_batch: "s_5_3",
|
||||
rooms: {
|
||||
[Category.Join]: {},
|
||||
[Category.Leave]: {},
|
||||
[Category.Invite]: {},
|
||||
},
|
||||
});
|
||||
client!.startClient({ threadSupport: true });
|
||||
await Promise.all([
|
||||
httpBackend?.flushAllExpected(),
|
||||
new Promise<void>((resolve) => {
|
||||
client!.on(ClientEvent.Sync, (state) => state === SyncState.Syncing && resolve());
|
||||
}),
|
||||
]);
|
||||
|
||||
const room = new Room(roomId, client!, selfUserId);
|
||||
jest.spyOn(client!, "getRoom").mockImplementation((id) => (id === roomId ? room : null));
|
||||
|
||||
const thread = mkThread({ room, client: client!, authorId: selfUserId, participantUserIds: [selfUserId] });
|
||||
const threadReply = thread.events.at(-1)!;
|
||||
room.addLiveEvents([thread.rootEvent]);
|
||||
|
||||
// Initialize read receipt datastructure before testing the reaction
|
||||
room.addReceiptToStructure(thread.rootEvent.getId()!, ReceiptType.Read, selfUserId, { ts: 1 }, false);
|
||||
thread.thread.addReceiptToStructure(
|
||||
threadReply.getId()!,
|
||||
ReceiptType.Read,
|
||||
selfUserId,
|
||||
{ thread_id: thread.thread.id, ts: 1 },
|
||||
false,
|
||||
);
|
||||
expect(room.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(thread.rootEvent.getId());
|
||||
expect(thread.thread.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(threadReply.getId());
|
||||
|
||||
const reactionEventId = `$9-${Math.random()}-${Math.random()}`;
|
||||
let lastEvent: MatrixEvent | null = null;
|
||||
jest.spyOn(client! as any, "sendEventHttpRequest").mockImplementation((event) => {
|
||||
lastEvent = event as MatrixEvent;
|
||||
return { event_id: reactionEventId };
|
||||
});
|
||||
|
||||
await client!.sendEvent(roomId, EventType.Reaction, {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
event_id: threadReply.getId(),
|
||||
key: "",
|
||||
},
|
||||
});
|
||||
|
||||
expect(lastEvent!.getId()).toEqual(reactionEventId);
|
||||
room.handleRemoteEcho(new MatrixEvent(lastEvent!.event), lastEvent!);
|
||||
|
||||
// Our ideal state after this is the following:
|
||||
//
|
||||
// Room: [synthetic: threadroot, actual: threadroot]
|
||||
// Thread: [synthetic: threadreaction, actual: threadreply]
|
||||
//
|
||||
// The reaction and reply are both in the thread, and their receipts should be isolated to the thread.
|
||||
// The reaction has not been acknowledged in a dedicated read receipt message, so only the synthetic receipt
|
||||
// should be updated.
|
||||
|
||||
// Ensure the synthetic receipt for the room has not been updated
|
||||
expect(room.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(thread.rootEvent.getId());
|
||||
expect(room.getEventReadUpTo(selfUserId, false)).toEqual(thread.rootEvent.getId());
|
||||
// Ensure the actual receipt for the room has not been updated
|
||||
expect(room.getReadReceiptForUserId(selfUserId, true)?.eventId).toEqual(thread.rootEvent.getId());
|
||||
expect(room.getEventReadUpTo(selfUserId, true)).toEqual(thread.rootEvent.getId());
|
||||
// Ensure the synthetic receipt for the thread has been updated
|
||||
expect(thread.thread.getReadReceiptForUserId(selfUserId, false)?.eventId).toEqual(reactionEventId);
|
||||
expect(thread.thread.getEventReadUpTo(selfUserId, false)).toEqual(reactionEventId);
|
||||
// Ensure the actual receipt for the thread has not been updated
|
||||
expect(thread.thread.getReadReceiptForUserId(selfUserId, true)?.eventId).toEqual(threadReply.getId());
|
||||
expect(thread.thread.getEventReadUpTo(selfUserId, true)).toEqual(threadReply.getId());
|
||||
});
|
||||
|
||||
describe("Stuck unread notifications integration tests", () => {
|
||||
const ROOM_ID = "!room:localhost";
|
||||
|
||||
|
||||
@@ -472,7 +472,7 @@ describe("MatrixClient crypto", () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
@@ -483,7 +483,7 @@ describe("MatrixClient crypto", () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
@@ -545,7 +545,7 @@ describe("MatrixClient crypto", () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
|
||||
@@ -891,11 +891,9 @@ describe("SlidingSyncSdk", () => {
|
||||
const evType = ev.getType();
|
||||
expect(seen[evType]).toBeFalsy();
|
||||
seen[evType] = true;
|
||||
if (evType === "m.key.verification.start" || evType === "m.key.verification.request") {
|
||||
expect(ev.isCancelled()).toEqual(true);
|
||||
} else {
|
||||
expect(ev.isCancelled()).toEqual(false);
|
||||
}
|
||||
expect(ev.isCancelled()).toEqual(
|
||||
evType === "m.key.verification.start" || evType === "m.key.verification.request",
|
||||
);
|
||||
});
|
||||
ext.onResponse({
|
||||
next_batch: "45678",
|
||||
|
||||
+145
-6
@@ -30,6 +30,7 @@ import {
|
||||
RoomState,
|
||||
RoomStateEvent,
|
||||
RoomStateEventHandlerMap,
|
||||
SendToDeviceContentMap,
|
||||
} from "../../src";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
@@ -122,6 +123,7 @@ export class MockRTCPeerConnection {
|
||||
public iceCandidateListener?: (e: RTCPeerConnectionIceEvent) => void;
|
||||
public iceConnectionStateChangeListener?: () => void;
|
||||
public onTrackListener?: (e: RTCTrackEvent) => void;
|
||||
public onDataChannelListener?: (ev: RTCDataChannelEvent) => void;
|
||||
public needsNegotiation = false;
|
||||
public readyToNegotiate: Promise<void>;
|
||||
private onReadyToNegotiate?: () => void;
|
||||
@@ -167,6 +169,8 @@ export class MockRTCPeerConnection {
|
||||
this.iceConnectionStateChangeListener = listener;
|
||||
} else if (type == "track") {
|
||||
this.onTrackListener = listener;
|
||||
} else if (type == "datachannel") {
|
||||
this.onDataChannelListener = listener;
|
||||
}
|
||||
}
|
||||
public createDataChannel(label: string, opts: RTCDataChannelInit) {
|
||||
@@ -231,6 +235,10 @@ export class MockRTCPeerConnection {
|
||||
this.negotiationNeededListener();
|
||||
}
|
||||
}
|
||||
|
||||
public triggerIncomingDataChannel(): void {
|
||||
this.onDataChannelListener?.({ channel: {} } as RTCDataChannelEvent);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockRTCRtpSender {
|
||||
@@ -443,11 +451,7 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
>();
|
||||
public sendToDevice = jest.fn<
|
||||
Promise<{}>,
|
||||
[
|
||||
eventType: string,
|
||||
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
|
||||
txnId?: string,
|
||||
]
|
||||
[eventType: string, contentMap: SendToDeviceContentMap, txnId?: string]
|
||||
>();
|
||||
|
||||
public isInitialSyncComplete(): boolean {
|
||||
@@ -502,18 +506,22 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
|
||||
public state = CallState.Ringing;
|
||||
public opponentUserId = FAKE_USER_ID_1;
|
||||
public opponentDeviceId = FAKE_DEVICE_ID_1;
|
||||
public opponentSessionId = FAKE_SESSION_ID_1;
|
||||
public opponentMember = { userId: this.opponentUserId };
|
||||
public callId = "1";
|
||||
public localUsermediaFeed = {
|
||||
setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(),
|
||||
isAudioMuted: jest.fn().mockReturnValue(false),
|
||||
isVideoMuted: jest.fn().mockReturnValue(false),
|
||||
stream: new MockMediaStream("stream"),
|
||||
};
|
||||
} as unknown as CallFeed;
|
||||
public remoteUsermediaFeed?: CallFeed;
|
||||
public remoteScreensharingFeed?: CallFeed;
|
||||
|
||||
public reject = jest.fn<void, []>();
|
||||
public answerWithCallFeeds = jest.fn<void, [CallFeed[]]>();
|
||||
public hangup = jest.fn<void, []>();
|
||||
public initStats = jest.fn<void, []>();
|
||||
|
||||
public sendMetadataUpdate = jest.fn<void, []>();
|
||||
|
||||
@@ -525,6 +533,14 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
|
||||
return this.opponentDeviceId;
|
||||
}
|
||||
|
||||
public getOpponentSessionId(): string | undefined {
|
||||
return this.opponentSessionId;
|
||||
}
|
||||
|
||||
public getLocalFeeds(): CallFeed[] {
|
||||
return [this.localUsermediaFeed];
|
||||
}
|
||||
|
||||
public typed(): MatrixCall {
|
||||
return this as unknown as MatrixCall;
|
||||
}
|
||||
@@ -585,6 +601,7 @@ export function makeMockGroupCallStateEvent(
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
},
|
||||
redacted?: boolean,
|
||||
): MatrixEvent {
|
||||
return {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallPrefix),
|
||||
@@ -592,6 +609,7 @@ export function makeMockGroupCallStateEvent(
|
||||
getTs: jest.fn().mockReturnValue(0),
|
||||
getContent: jest.fn().mockReturnValue(content),
|
||||
getStateKey: jest.fn().mockReturnValue(groupCallId),
|
||||
isRedacted: jest.fn().mockReturnValue(redacted ?? false),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
|
||||
@@ -604,3 +622,124 @@ export function makeMockGroupCallMemberStateEvent(roomId: string, groupCallId: s
|
||||
getStateKey: jest.fn().mockReturnValue(groupCallId),
|
||||
} as unknown as MatrixEvent;
|
||||
}
|
||||
|
||||
export const REMOTE_SFU_DESCRIPTION =
|
||||
"v=0\n" +
|
||||
"o=- 3242942315779688438 1678878001 IN IP4 0.0.0.0\n" +
|
||||
"s=-\n" +
|
||||
"t=0 0\n" +
|
||||
"a=fingerprint:sha-256 EA:30:B2:7F:49:B5:46:D6:40:72:BF:79:95:C1:65:08:6E:35:09:FB:90:89:DA:EF:6B:82:D1:38:8C:25:39:B2\n" +
|
||||
"a=group:BUNDLE 0 1 2\n" +
|
||||
"m=audio 9 UDP/TLS/RTP/SAVPF 111 9 0 8\n" +
|
||||
"c=IN IP4 0.0.0.0\n" +
|
||||
"a=setup:actpass\n" +
|
||||
"a=mid:0\n" +
|
||||
"a=ice-ufrag:obZwzAcRtxwuozPZ\n" +
|
||||
"a=ice-pwd:TWXNaPeyKTTvRLyIQhWHfHlZHJjtcoKs\n" +
|
||||
"a=rtcp-mux\n" +
|
||||
"a=rtcp-rsize\n" +
|
||||
"a=rtpmap:111 opus/48000/2\n" +
|
||||
"a=fmtp:111 minptime=10;usedtx=1;useinbandfec=1\n" +
|
||||
"a=rtcp-fb:111 transport-cc \n" +
|
||||
"a=rtpmap:9 G722/8000\n" +
|
||||
"a=rtpmap:0 PCMU/8000\n" +
|
||||
"a=rtpmap:8 PCMA/8000\n" +
|
||||
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\n" +
|
||||
"a=ssrc:2963372119 cname:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
|
||||
"a=ssrc:2963372119 msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 4b811ab6-6926-473d-8ca5-ac45f268c507\n" +
|
||||
"a=ssrc:2963372119 mslabel:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
|
||||
"a=ssrc:2963372119 label:4b811ab6-6926-473d-8ca5-ac45f268c507\n" +
|
||||
"a=msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 4b811ab6-6926-473d-8ca5-ac45f268c507\n" +
|
||||
"a=sendrecv\n" +
|
||||
"a=candidate:1155505470 1 udp 2130706431 13.41.173.213 41385 typ host\n" +
|
||||
"a=candidate:1155505470 2 udp 2130706431 13.41.173.213 41385 typ host\n" +
|
||||
"a=candidate:1155505470 1 udp 2130706431 13.41.173.213 40026 typ host\n" +
|
||||
"a=candidate:1155505470 2 udp 2130706431 13.41.173.213 40026 typ host\n" +
|
||||
"a=end-of-candidates\n" +
|
||||
"m=video 9 UDP/TLS/RTP/SAVPF 96 97 102 103 104 106 108 109 98 99 112 116\n" +
|
||||
"c=IN IP4 0.0.0.0\n" +
|
||||
"a=setup:actpass\n" +
|
||||
"a=mid:1\n" +
|
||||
"a=ice-ufrag:obZwzAcRtxwuozPZ\n" +
|
||||
"a=ice-pwd:TWXNaPeyKTTvRLyIQhWHfHlZHJjtcoKs\n" +
|
||||
"a=rtcp-mux\n" +
|
||||
"a=rtcp-rsize\n" +
|
||||
"a=rtpmap:96 VP8/90000\n" +
|
||||
"a=rtcp-fb:96 goog-remb \n" +
|
||||
"a=rtcp-fb:96 transport-cc \n" +
|
||||
"a=rtcp-fb:96 ccm fir\n" +
|
||||
"a=rtcp-fb:96 nack \n" +
|
||||
"a=rtcp-fb:96 nack pli\n" +
|
||||
"a=rtpmap:97 rtx/90000\n" +
|
||||
"a=fmtp:97 apt=96\n" +
|
||||
"a=rtpmap:102 H264/90000\n" +
|
||||
"a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\n" +
|
||||
"a=rtcp-fb:102 goog-remb \n" +
|
||||
"a=rtcp-fb:102 transport-cc \n" +
|
||||
"a=rtcp-fb:102 ccm fir\n" +
|
||||
"a=rtcp-fb:102 nack \n" +
|
||||
"a=rtcp-fb:102 nack pli\n" +
|
||||
"a=rtpmap:103 rtx/90000\n" +
|
||||
"a=fmtp:103 apt=102\n" +
|
||||
"a=rtpmap:104 H264/90000\n" +
|
||||
"a=fmtp:104 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f\n" +
|
||||
"a=rtcp-fb:104 goog-remb \n" +
|
||||
"a=rtcp-fb:104 transport-cc \n" +
|
||||
"a=rtcp-fb:104 ccm fir\n" +
|
||||
"a=rtcp-fb:104 nack \n" +
|
||||
"a=rtcp-fb:104 nack pli\n" +
|
||||
"a=rtpmap:106 H264/90000\n" +
|
||||
"a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\n" +
|
||||
"a=rtcp-fb:106 goog-remb \n" +
|
||||
"a=rtcp-fb:106 transport-cc \n" +
|
||||
"a=rtcp-fb:106 ccm fir\n" +
|
||||
"a=rtcp-fb:106 nack \n" +
|
||||
"a=rtcp-fb:106 nack pli\n" +
|
||||
"a=rtpmap:108 H264/90000\n" +
|
||||
"a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f\n" +
|
||||
"a=rtcp-fb:108 goog-remb \n" +
|
||||
"a=rtcp-fb:108 transport-cc \n" +
|
||||
"a=rtcp-fb:108 ccm fir\n" +
|
||||
"a=rtcp-fb:108 nack \n" +
|
||||
"a=rtcp-fb:108 nack pli\n" +
|
||||
"a=rtpmap:109 rtx/90000\n" +
|
||||
"a=fmtp:109 apt=108\n" +
|
||||
"a=rtpmap:98 VP9/90000\n" +
|
||||
"a=fmtp:98 profile-id=0\n" +
|
||||
"a=rtcp-fb:98 goog-remb \n" +
|
||||
"a=rtcp-fb:98 transport-cc \n" +
|
||||
"a=rtcp-fb:98 ccm fir\n" +
|
||||
"a=rtcp-fb:98 nack \n" +
|
||||
"a=rtcp-fb:98 nack pli\n" +
|
||||
"a=rtpmap:99 rtx/90000\n" +
|
||||
"a=fmtp:99 apt=98\n" +
|
||||
"a=rtpmap:112 H264/90000\n" +
|
||||
"a=fmtp:112 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f\n" +
|
||||
"a=rtcp-fb:112 goog-remb \n" +
|
||||
"a=rtcp-fb:112 transport-cc \n" +
|
||||
"a=rtcp-fb:112 ccm fir\n" +
|
||||
"a=rtcp-fb:112 nack \n" +
|
||||
"a=rtcp-fb:112 nack pli\n" +
|
||||
"a=rtpmap:116 ulpfec/90000\n" +
|
||||
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\n" +
|
||||
"a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\n" +
|
||||
"a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\n" +
|
||||
"a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\n" +
|
||||
"a=rid:f recv\n" +
|
||||
"a=rid:h recv\n" +
|
||||
"a=rid:q recv\n" +
|
||||
"a=simulcast:recv f;h;q\n" +
|
||||
"a=ssrc:1212931603 cname:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
|
||||
"a=ssrc:1212931603 msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 12905f48-75b9-499f-ba50-fc00f56a86c6\n" +
|
||||
"a=ssrc:1212931603 mslabel:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90\n" +
|
||||
"a=ssrc:1212931603 label:12905f48-75b9-499f-ba50-fc00f56a86c6\n" +
|
||||
"a=msid:dcc3a6d5-37a1-42e7-94a9-d520f20d0c90 12905f48-75b9-499f-ba50-fc00f56a86c6\n" +
|
||||
"a=sendrecv\n" +
|
||||
"m=application 9 UDP/DTLS/SCTP webrtc-datachannel\n" +
|
||||
"c=IN IP4 0.0.0.0\n" +
|
||||
"a=setup:actpass\n" +
|
||||
"a=mid:2\n" +
|
||||
"a=sendrecv\n" +
|
||||
"a=sctp-port:5000\n" +
|
||||
"a=ice-ufrag:obZwzAcRtxwuozPZ\n" +
|
||||
"a=ice-pwd:TWXNaPeyKTTvRLyIQhWHfHlZHJjtcoKs";
|
||||
|
||||
@@ -46,13 +46,7 @@ describe("NamespacedValue", () => {
|
||||
});
|
||||
|
||||
it("should not permit falsey values for both parts", () => {
|
||||
try {
|
||||
new UnstableValue(null!, null!);
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect((<Error>e).message).toBe("One of stable or unstable values must be supplied");
|
||||
}
|
||||
expect(() => new UnstableValue(null!, null!)).toThrow("One of stable or unstable values must be supplied");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,12 +66,6 @@ describe("UnstableValue", () => {
|
||||
});
|
||||
|
||||
it("should not permit falsey unstable values", () => {
|
||||
try {
|
||||
new UnstableValue("stable", null!);
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect((<Error>e).message).toBe("Unstable value must be supplied");
|
||||
}
|
||||
expect(() => new UnstableValue("stable", null!)).toThrow("Unstable value must be supplied");
|
||||
});
|
||||
});
|
||||
|
||||
+39
-26
@@ -304,20 +304,24 @@ describe("Crypto", function () {
|
||||
|
||||
describe("Key requests", function () {
|
||||
let aliceClient: MatrixClient;
|
||||
let secondAliceClient: MatrixClient;
|
||||
let bobClient: MatrixClient;
|
||||
let claraClient: MatrixClient;
|
||||
|
||||
beforeEach(async function () {
|
||||
aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
|
||||
secondAliceClient = new TestClient("@alice:example.com", "secondAliceDevice").client;
|
||||
bobClient = new TestClient("@bob:example.com", "bobdevice").client;
|
||||
claraClient = new TestClient("@clara:example.com", "claradevice").client;
|
||||
await aliceClient.initCrypto();
|
||||
await secondAliceClient.initCrypto();
|
||||
await bobClient.initCrypto();
|
||||
await claraClient.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
aliceClient.stopClient();
|
||||
secondAliceClient.stopClient();
|
||||
bobClient.stopClient();
|
||||
claraClient.stopClient();
|
||||
});
|
||||
@@ -401,7 +405,7 @@ describe("Crypto", function () {
|
||||
// the first message can't be decrypted yet, but the second one
|
||||
// can
|
||||
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
|
||||
bobClient.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobClient.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await decryptEventsPromise;
|
||||
@@ -568,17 +572,17 @@ describe("Crypto", function () {
|
||||
expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId);
|
||||
});
|
||||
|
||||
it("should accept forwarded keys which it requested", async function () {
|
||||
it("should accept forwarded keys it requested from one of its own user's other devices", async function () {
|
||||
const encryptionCfg = {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||
const bobRoom = new Room(roomId, secondAliceClient, "@alice:example.com", {});
|
||||
aliceClient.store.storeRoom(aliceRoom);
|
||||
bobClient.store.storeRoom(bobRoom);
|
||||
secondAliceClient.store.storeRoom(bobRoom);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await secondAliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
const events = [
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
@@ -614,7 +618,7 @@ describe("Crypto", function () {
|
||||
// @ts-ignore private properties
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient.crypto!.decryptEvent(event);
|
||||
await secondAliceClient.crypto!.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
@@ -623,10 +627,11 @@ describe("Crypto", function () {
|
||||
);
|
||||
|
||||
const device = new DeviceInfo(aliceClient.deviceId!);
|
||||
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
device.verified = DeviceInfo.DeviceVerification.VERIFIED;
|
||||
secondAliceClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
secondAliceClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
const cryptoStore = bobClient.crypto!.cryptoStore;
|
||||
const cryptoStore = secondAliceClient.crypto!.cryptoStore;
|
||||
const eventContent = events[0].getWireContent();
|
||||
const senderKey = eventContent.sender_key;
|
||||
const sessionId = eventContent.session_id;
|
||||
@@ -642,7 +647,7 @@ describe("Crypto", function () {
|
||||
state: RoomKeyRequestState.Sent,
|
||||
});
|
||||
|
||||
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
|
||||
const bobDecryptor = secondAliceClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
|
||||
|
||||
const decryptEventsPromise = Promise.all(
|
||||
events.map((ev) => {
|
||||
@@ -651,7 +656,7 @@ describe("Crypto", function () {
|
||||
);
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
const key = await secondAliceClient.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
events[0].getWireContent().sender_key,
|
||||
events[0].getWireContent().session_id,
|
||||
@@ -755,7 +760,7 @@ describe("Crypto", function () {
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
});
|
||||
|
||||
it("should accept forwarded keys from one of its own user's other devices", async function () {
|
||||
it("should not accept requested forwarded keys from other users", async function () {
|
||||
const encryptionCfg = {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
@@ -809,31 +814,39 @@ describe("Crypto", function () {
|
||||
}),
|
||||
);
|
||||
|
||||
const device = new DeviceInfo(claraClient.deviceId!);
|
||||
const cryptoStore = bobClient.crypto!.cryptoStore;
|
||||
const eventContent = events[0].getWireContent();
|
||||
const senderKey = eventContent.sender_key;
|
||||
const sessionId = eventContent.session_id;
|
||||
const roomKeyRequestBody = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: roomId,
|
||||
sender_key: senderKey,
|
||||
session_id: sessionId,
|
||||
};
|
||||
const outgoingReq = await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody);
|
||||
expect(outgoingReq).toBeDefined();
|
||||
await cryptoStore.updateOutgoingRoomKeyRequest(outgoingReq!.requestId, RoomKeyRequestState.Unsent, {
|
||||
state: RoomKeyRequestState.Sent,
|
||||
});
|
||||
|
||||
const device = new DeviceInfo(aliceClient.deviceId!);
|
||||
device.verified = DeviceInfo.DeviceVerification.VERIFIED;
|
||||
bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@bob:example.com";
|
||||
bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
const bobDecryptor = bobClient.crypto!.getRoomDecryptor(roomId, olmlib.MEGOLM_ALGORITHM);
|
||||
|
||||
const decryptEventsPromise = Promise.all(
|
||||
events.map((ev) => {
|
||||
return awaitEvent(ev, "Event.decrypted");
|
||||
}),
|
||||
);
|
||||
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||
ksEvent.event.sender = bobClient.getUserId()!;
|
||||
ksEvent.sender = new RoomMember(roomId, bobClient.getUserId()!);
|
||||
ksEvent.event.sender = aliceClient.getUserId()!;
|
||||
ksEvent.sender = new RoomMember(roomId, aliceClient.getUserId()!);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||
roomId,
|
||||
events[0].getWireContent().sender_key,
|
||||
events[0].getWireContent().session_id,
|
||||
);
|
||||
expect(key).not.toBeNull();
|
||||
await decryptEventsPromise;
|
||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
expect(key).toBeNull();
|
||||
});
|
||||
|
||||
it("should not accept unexpected forwarded keys for a room it's in", async function () {
|
||||
@@ -1026,7 +1039,7 @@ describe("Crypto", function () {
|
||||
|
||||
beforeEach(async () => {
|
||||
ensureOlmSessionsForDevices = jest.spyOn(olmlib, "ensureOlmSessionsForDevices");
|
||||
ensureOlmSessionsForDevices.mockResolvedValue({});
|
||||
ensureOlmSessionsForDevices.mockResolvedValue(new Map());
|
||||
encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice");
|
||||
encryptMessageForDevice.mockImplementation(async (...[result, , , , , , payload]) => {
|
||||
result.plaintext = { type: 0, body: JSON.stringify(payload) };
|
||||
|
||||
@@ -102,9 +102,10 @@ describe("CrossSigningInfo.getCrossSigningKey", function () {
|
||||
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { getCrossSigningKeyCache });
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
expect(getCrossSigningKeyCache).toHaveBeenCalledTimes(shouldCache ? 1 : 0);
|
||||
if (shouldCache) {
|
||||
expect(getCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(getCrossSigningKeyCache).toHaveBeenLastCalledWith(type, expect.any(String));
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -115,10 +116,10 @@ describe("CrossSigningInfo.getCrossSigningKey", function () {
|
||||
const info = new CrossSigningInfo(userId, { getCrossSigningKey }, { storeCrossSigningKeyCache });
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(shouldCache ? 1 : 0);
|
||||
expect(storeCrossSigningKeyCache).toHaveBeenCalledTimes(shouldCache ? 1 : 0);
|
||||
if (shouldCache) {
|
||||
expect(storeCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
|
||||
expect(storeCrossSigningKeyCache.mock.calls[0][1]).toBe(testKey);
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(storeCrossSigningKeyCache).toHaveBeenLastCalledWith(type, testKey);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import { ClientEvent, MatrixClient, RoomMember } from "../../../../src";
|
||||
import { DeviceInfo, IDevice } from "../../../../src/crypto/deviceinfo";
|
||||
import { DeviceTrustLevel } from "../../../../src/crypto/CrossSigning";
|
||||
import { MegolmEncryption as MegolmEncryptionClass } from "../../../../src/crypto/algorithms/megolm";
|
||||
import { recursiveMapToObject } from "../../../../src/utils";
|
||||
import { sleep } from "../../../../src/utils";
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
|
||||
@@ -183,14 +184,22 @@ describe("MegolmDecryption", function () {
|
||||
const deviceInfo = {} as DeviceInfo;
|
||||
mockCrypto.getStoredDevice.mockReturnValue(deviceInfo);
|
||||
|
||||
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue({
|
||||
"@alice:foo": {
|
||||
alidevice: {
|
||||
sessionId: "alisession",
|
||||
device: new DeviceInfo("alidevice"),
|
||||
},
|
||||
},
|
||||
});
|
||||
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue(
|
||||
new Map([
|
||||
[
|
||||
"@alice:foo",
|
||||
new Map([
|
||||
[
|
||||
"alidevice",
|
||||
{
|
||||
sessionId: "alisession",
|
||||
device: new DeviceInfo("alidevice"),
|
||||
},
|
||||
],
|
||||
]),
|
||||
],
|
||||
]),
|
||||
);
|
||||
|
||||
const awaitEncryptForDevice = new Promise<void>((res, rej) => {
|
||||
mockOlmLib.encryptMessageForDevice.mockImplementation(() => {
|
||||
@@ -357,11 +366,7 @@ describe("MegolmDecryption", function () {
|
||||
} as unknown as DeviceInfo;
|
||||
|
||||
mockCrypto.downloadKeys.mockReturnValue(
|
||||
Promise.resolve({
|
||||
"@alice:home.server": {
|
||||
aliceDevice: aliceDeviceInfo,
|
||||
},
|
||||
}),
|
||||
Promise.resolve(new Map([["@alice:home.server", new Map([["aliceDevice", aliceDeviceInfo]])]])),
|
||||
);
|
||||
|
||||
mockCrypto.checkDeviceTrust.mockReturnValue({
|
||||
@@ -523,23 +528,32 @@ describe("MegolmDecryption", function () {
|
||||
let megolm: MegolmEncryptionClass;
|
||||
let room: jest.Mocked<Room>;
|
||||
|
||||
const deviceMap: DeviceInfoMap = {
|
||||
"user-a": {
|
||||
"device-a": new DeviceInfo("device-a"),
|
||||
"device-b": new DeviceInfo("device-b"),
|
||||
"device-c": new DeviceInfo("device-c"),
|
||||
},
|
||||
"user-b": {
|
||||
"device-d": new DeviceInfo("device-d"),
|
||||
"device-e": new DeviceInfo("device-e"),
|
||||
"device-f": new DeviceInfo("device-f"),
|
||||
},
|
||||
"user-c": {
|
||||
"device-g": new DeviceInfo("device-g"),
|
||||
"device-h": new DeviceInfo("device-h"),
|
||||
"device-i": new DeviceInfo("device-i"),
|
||||
},
|
||||
};
|
||||
const deviceMap: DeviceInfoMap = new Map([
|
||||
[
|
||||
"user-a",
|
||||
new Map([
|
||||
["device-a", new DeviceInfo("device-a")],
|
||||
["device-b", new DeviceInfo("device-b")],
|
||||
["device-c", new DeviceInfo("device-c")],
|
||||
]),
|
||||
],
|
||||
[
|
||||
"user-b",
|
||||
new Map([
|
||||
["device-d", new DeviceInfo("device-d")],
|
||||
["device-e", new DeviceInfo("device-e")],
|
||||
["device-f", new DeviceInfo("device-f")],
|
||||
]),
|
||||
],
|
||||
[
|
||||
"user-c",
|
||||
new Map([
|
||||
["device-g", new DeviceInfo("device-g")],
|
||||
["device-h", new DeviceInfo("device-h")],
|
||||
["device-i", new DeviceInfo("device-i")],
|
||||
]),
|
||||
],
|
||||
]);
|
||||
|
||||
beforeEach(() => {
|
||||
room = testUtils.mock(Room, "Room") as jest.Mocked<Room>;
|
||||
@@ -572,8 +586,8 @@ describe("MegolmDecryption", function () {
|
||||
//@ts-ignore private member access, gross
|
||||
await megolm.encryptionPreparation?.promise;
|
||||
|
||||
for (const userId in deviceMap) {
|
||||
for (const deviceId in deviceMap[userId]) {
|
||||
for (const [userId, devices] of deviceMap) {
|
||||
for (const deviceId of devices.keys()) {
|
||||
expect(mockCrypto.checkDeviceTrust).toHaveBeenCalledWith(userId, deviceId);
|
||||
}
|
||||
}
|
||||
@@ -658,20 +672,20 @@ describe("MegolmDecryption", function () {
|
||||
expect(aliceClient.sendToDevice).toHaveBeenCalled();
|
||||
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
|
||||
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
|
||||
delete contentMap["@bob:example.com"].bobdevice1.session_id;
|
||||
delete contentMap["@bob:example.com"].bobdevice1["org.matrix.msgid"];
|
||||
delete contentMap["@bob:example.com"].bobdevice2.session_id;
|
||||
delete contentMap["@bob:example.com"].bobdevice2["org.matrix.msgid"];
|
||||
expect(contentMap).toStrictEqual({
|
||||
"@bob:example.com": {
|
||||
bobdevice1: {
|
||||
delete contentMap.get("@bob:example.com")?.get("bobdevice1")?.["session_id"];
|
||||
delete contentMap.get("@bob:example.com")?.get("bobdevice1")?.["org.matrix.msgid"];
|
||||
delete contentMap.get("@bob:example.com")?.get("bobdevice2")?.["session_id"];
|
||||
delete contentMap.get("@bob:example.com")?.get("bobdevice2")?.["org.matrix.msgid"];
|
||||
expect(recursiveMapToObject(contentMap)).toStrictEqual({
|
||||
["@bob:example.com"]: {
|
||||
["bobdevice1"]: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
code: "m.unverified",
|
||||
reason: "The sender has disabled encrypting to unverified devices.",
|
||||
sender_key: aliceDevice.deviceCurve25519Key,
|
||||
},
|
||||
bobdevice2: {
|
||||
["bobdevice2"]: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
code: "m.blacklisted",
|
||||
@@ -839,10 +853,10 @@ describe("MegolmDecryption", function () {
|
||||
expect(aliceClient.sendToDevice).toHaveBeenCalled();
|
||||
const [msgtype, contentMap] = mocked(aliceClient.sendToDevice).mock.calls[0];
|
||||
expect(msgtype).toMatch(/^(org.matrix|m).room_key.withheld$/);
|
||||
delete contentMap["@bob:example.com"]["bobdevice"]["org.matrix.msgid"];
|
||||
expect(contentMap).toStrictEqual({
|
||||
"@bob:example.com": {
|
||||
bobdevice: {
|
||||
delete contentMap.get("@bob:example.com")?.get("bobdevice")?.["org.matrix.msgid"];
|
||||
expect(recursiveMapToObject(contentMap)).toStrictEqual({
|
||||
["@bob:example.com"]: {
|
||||
["bobdevice"]: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
code: "m.no_olm",
|
||||
reason: "Unable to establish a secure channel.",
|
||||
|
||||
@@ -146,18 +146,21 @@ describe("OlmDevice", function () {
|
||||
});
|
||||
},
|
||||
} as unknown as MockedObject<MatrixClient>;
|
||||
const devicesByUser = {
|
||||
"@bob:example.com": [
|
||||
DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "akey",
|
||||
const devicesByUser = new Map([
|
||||
[
|
||||
"@bob:example.com",
|
||||
[
|
||||
DeviceInfo.fromStorage(
|
||||
{
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "akey",
|
||||
},
|
||||
},
|
||||
},
|
||||
"ABCDEFG",
|
||||
),
|
||||
"ABCDEFG",
|
||||
),
|
||||
],
|
||||
],
|
||||
};
|
||||
]);
|
||||
|
||||
// start two tasks that try to ensure that there's an olm session
|
||||
const promises = Promise.all([
|
||||
@@ -218,12 +221,8 @@ describe("OlmDevice", function () {
|
||||
// There's no required ordering of devices per user, so here we
|
||||
// create two different orderings so that each task reserves a
|
||||
// device the other task needs before continuing.
|
||||
const devicesByUserAB = {
|
||||
"@bob:example.com": [deviceBobA, deviceBobB],
|
||||
};
|
||||
const devicesByUserBA = {
|
||||
"@bob:example.com": [deviceBobB, deviceBobA],
|
||||
};
|
||||
const devicesByUserAB = new Map([["@bob:example.com", [deviceBobA, deviceBobB]]]);
|
||||
const devicesByUserBA = new Map([["@bob:example.com", [deviceBobB, deviceBobA]]]);
|
||||
|
||||
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(aliceOlmDevice, baseApis, devicesByUserAB));
|
||||
|
||||
|
||||
@@ -456,6 +456,7 @@ describe("MegolmBackup", function () {
|
||||
client.http.authedRequest = function (method, path, queryParams, data, opts): any {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(2);
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
if (numCalls === 1) {
|
||||
expect(method).toBe("POST");
|
||||
expect(path).toBe("/room_keys/version");
|
||||
@@ -482,6 +483,7 @@ describe("MegolmBackup", function () {
|
||||
reject(new Error("authedRequest called too many times"));
|
||||
return Promise.resolve({});
|
||||
}
|
||||
/* eslint-enable jest/no-conditional-expect */
|
||||
};
|
||||
}),
|
||||
client.createKeyBackupVersion({
|
||||
|
||||
@@ -25,10 +25,10 @@ import { encryptAES } from "../../../src/crypto/aes";
|
||||
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { ClientEvent, ICreateClientOpts, ICrossSigningKey, MatrixClient } from "../../../src/client";
|
||||
import { ISecretStorageKeyInfo } from "../../../src/crypto/api";
|
||||
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
import { ISignatures } from "../../../src/@types/signed";
|
||||
import { ICurve25519AuthData } from "../../../src/crypto/keybackup";
|
||||
import { SecretStorageKeyDescription } from "../../../src/secret-storage";
|
||||
|
||||
async function makeTestClient(
|
||||
userInfo: { userId: string; deviceId: string },
|
||||
@@ -45,7 +45,7 @@ async function makeTestClient(
|
||||
await client.initCrypto();
|
||||
|
||||
// No need to download keys for these tests
|
||||
jest.spyOn(client.crypto!, "downloadKeys").mockResolvedValue({});
|
||||
jest.spyOn(client.crypto!, "downloadKeys").mockResolvedValue(new Map());
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -274,7 +274,7 @@ describe("Secrets", function () {
|
||||
Object.values(otks)[0],
|
||||
);
|
||||
|
||||
osborne2.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
osborne2.client.crypto!.deviceList.downloadKeys = () => Promise.resolve(new Map());
|
||||
osborne2.client.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
|
||||
const request = await secretStorage.request("foo", ["VAX"]);
|
||||
@@ -541,7 +541,9 @@ describe("Secrets", function () {
|
||||
await alice.bootstrapSecretStorage({});
|
||||
|
||||
expect(alice.getAccountData("m.secret_storage.default_key")!.getContent()).toEqual({ key: "key_id" });
|
||||
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")!.getContent<ISecretStorageKeyInfo>();
|
||||
const keyInfo = alice
|
||||
.getAccountData("m.secret_storage.key.key_id")!
|
||||
.getContent<SecretStorageKeyDescription>();
|
||||
expect(keyInfo.algorithm).toEqual("m.secret_storage.v1.aes-hmac-sha2");
|
||||
expect(keyInfo.passphrase).toEqual({
|
||||
algorithm: "m.pbkdf2",
|
||||
|
||||
@@ -121,12 +121,12 @@ describe("SAS verification", function () {
|
||||
|
||||
alice.client.crypto!.deviceList.storeDevicesForUser("@bob:example.com", BOB_DEVICES);
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve(new Map());
|
||||
};
|
||||
|
||||
bob.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", ALICE_DEVICES);
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve(new Map());
|
||||
};
|
||||
|
||||
aliceSasEvent = null;
|
||||
@@ -176,6 +176,7 @@ describe("SAS verification", function () {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all([alice.stop(), bob.stop()]);
|
||||
|
||||
@@ -186,10 +187,14 @@ describe("SAS verification", function () {
|
||||
let macMethod;
|
||||
let keyAgreement;
|
||||
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = function (type, map) {
|
||||
bob.client.sendToDevice = async (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
|
||||
keyAgreement = map[alice.client.getUserId()!][alice.client.deviceId!].key_agreement_protocol;
|
||||
macMethod = map
|
||||
.get(alice.client.getUserId()!)
|
||||
?.get(alice.client.deviceId!)?.message_authentication_code;
|
||||
keyAgreement = map
|
||||
.get(alice.client.getUserId()!)
|
||||
?.get(alice.client.deviceId!)?.key_agreement_protocol;
|
||||
}
|
||||
return origSendToDevice(type, map);
|
||||
};
|
||||
@@ -237,7 +242,7 @@ describe("SAS verification", function () {
|
||||
// has, since it is the same object. If this does not
|
||||
// happen, the verification will fail due to a hash
|
||||
// commitment mismatch.
|
||||
map[bob.client.getUserId()!][bob.client.deviceId!].message_authentication_codes = [
|
||||
map.get(bob.client.getUserId()!)!.get(bob.client.deviceId!)!.message_authentication_codes = [
|
||||
"hkdf-hmac-sha256",
|
||||
];
|
||||
}
|
||||
@@ -246,7 +251,9 @@ describe("SAS verification", function () {
|
||||
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
|
||||
macMethod = map
|
||||
.get(alice.client.getUserId()!)!
|
||||
.get(alice.client.deviceId!)!.message_authentication_code;
|
||||
}
|
||||
return bobOrigSendToDevice(type, map);
|
||||
};
|
||||
@@ -291,14 +298,18 @@ describe("SAS verification", function () {
|
||||
// has, since it is the same object. If this does not
|
||||
// happen, the verification will fail due to a hash
|
||||
// commitment mismatch.
|
||||
map[bob.client.getUserId()!][bob.client.deviceId!].message_authentication_codes = ["hmac-sha256"];
|
||||
map.get(bob.client.getUserId()!)!.get(bob.client.deviceId!)!.message_authentication_codes = [
|
||||
"hmac-sha256",
|
||||
];
|
||||
}
|
||||
return aliceOrigSendToDevice(type, map);
|
||||
};
|
||||
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()!][alice.client.deviceId!].message_authentication_code;
|
||||
macMethod = map
|
||||
.get(alice.client.getUserId()!)!
|
||||
.get(alice.client.deviceId!)!.message_authentication_code;
|
||||
}
|
||||
return bobOrigSendToDevice(type, map);
|
||||
};
|
||||
@@ -454,7 +465,7 @@ describe("SAS verification", function () {
|
||||
);
|
||||
};
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve(new Map());
|
||||
};
|
||||
|
||||
bob.client.crypto!.setDeviceVerification = jest.fn();
|
||||
@@ -472,7 +483,7 @@ describe("SAS verification", function () {
|
||||
return "bob+base64+ed25519+key";
|
||||
};
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve({});
|
||||
return Promise.resolve(new Map());
|
||||
};
|
||||
|
||||
aliceSasEvent = null;
|
||||
|
||||
@@ -20,7 +20,7 @@ import { IContent, MatrixEvent } from "../../../../src/models/event";
|
||||
import { IRoomTimelineData } from "../../../../src/models/event-timeline-set";
|
||||
import { Room, RoomEvent } from "../../../../src/models/room";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { MatrixClient, ClientEvent, ICreateClientOpts } from "../../../../src/client";
|
||||
import { MatrixClient, ClientEvent, ICreateClientOpts, SendToDeviceContentMap } from "../../../../src/client";
|
||||
|
||||
interface UserInfo {
|
||||
userId: string;
|
||||
@@ -36,16 +36,16 @@ export async function makeTestClients(
|
||||
const clientMap: Record<string, Record<string, MatrixClient>> = {};
|
||||
const makeSendToDevice =
|
||||
(matrixClient: MatrixClient): MatrixClient["sendToDevice"] =>
|
||||
async (type, map) => {
|
||||
async (type: string, contentMap: SendToDeviceContentMap) => {
|
||||
// logger.log(this.getUserId(), "sends", type, map);
|
||||
for (const [userId, devMap] of Object.entries(map)) {
|
||||
for (const [userId, deviceMessages] of contentMap) {
|
||||
if (userId in clientMap) {
|
||||
for (const [deviceId, msg] of Object.entries(devMap)) {
|
||||
for (const [deviceId, message] of deviceMessages) {
|
||||
if (deviceId in clientMap[userId]) {
|
||||
const event = new MatrixEvent({
|
||||
sender: matrixClient.getUserId()!,
|
||||
type: type,
|
||||
content: msg,
|
||||
content: message,
|
||||
});
|
||||
const client = clientMap[userId][deviceId];
|
||||
const decryptionPromise = event.isEncrypted()
|
||||
|
||||
@@ -25,6 +25,7 @@ import { IContent, MatrixEvent } from "../../../../src/models/event";
|
||||
import { MatrixClient } from "../../../../src/client";
|
||||
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
|
||||
import { VerificationBase } from "../../../../src/crypto/verification/Base";
|
||||
import { MapWithDefault } from "../../../../src/utils";
|
||||
|
||||
type MockClient = MatrixClient & {
|
||||
popEvents: () => MatrixEvent[];
|
||||
@@ -33,7 +34,9 @@ type MockClient = MatrixClient & {
|
||||
function makeMockClient(userId: string, deviceId: string): MockClient {
|
||||
let counter = 1;
|
||||
let events: MatrixEvent[] = [];
|
||||
const deviceEvents: Record<string, Record<string, MatrixEvent[]>> = {};
|
||||
const deviceEvents: MapWithDefault<string, MapWithDefault<string, MatrixEvent[]>> = new MapWithDefault(
|
||||
() => new MapWithDefault(() => []),
|
||||
);
|
||||
return {
|
||||
getUserId() {
|
||||
return userId;
|
||||
@@ -58,15 +61,11 @@ function makeMockClient(userId: string, deviceId: string): MockClient {
|
||||
return Promise.resolve({ event_id: eventId });
|
||||
},
|
||||
|
||||
sendToDevice(type: string, msgMap: Record<string, Record<string, IContent>>) {
|
||||
for (const userId of Object.keys(msgMap)) {
|
||||
const deviceMap = msgMap[userId];
|
||||
for (const deviceId of Object.keys(deviceMap)) {
|
||||
const content = deviceMap[deviceId];
|
||||
sendToDevice(type: string, msgMap: Map<string, Map<string, IContent>>) {
|
||||
for (const [userId, deviceMessages] of msgMap) {
|
||||
for (const [deviceId, content] of deviceMessages) {
|
||||
const event = new MatrixEvent({ content, type });
|
||||
deviceEvents[userId] = deviceEvents[userId] || {};
|
||||
deviceEvents[userId][deviceId] = deviceEvents[userId][deviceId] || [];
|
||||
deviceEvents[userId][deviceId].push(event);
|
||||
deviceEvents.getOrCreate(userId).getOrCreate(deviceId).push(event);
|
||||
}
|
||||
}
|
||||
return Promise.resolve({});
|
||||
@@ -79,14 +78,9 @@ function makeMockClient(userId: string, deviceId: string): MockClient {
|
||||
return e;
|
||||
},
|
||||
|
||||
// @ts-ignore special testing fn
|
||||
popDeviceEvents(userId: string, deviceId: string): MatrixEvent[] {
|
||||
const forDevice = deviceEvents[userId];
|
||||
const events = forDevice && forDevice[deviceId];
|
||||
const result = events || [];
|
||||
if (events) {
|
||||
delete forDevice[deviceId];
|
||||
}
|
||||
const result = deviceEvents.get(userId)?.get(deviceId) || [];
|
||||
deviceEvents?.get(userId)?.delete(deviceId);
|
||||
return result;
|
||||
},
|
||||
} as unknown as MockClient;
|
||||
|
||||
@@ -204,9 +204,14 @@ describe("RoomWidgetClient", () => {
|
||||
});
|
||||
|
||||
describe("to-device messages", () => {
|
||||
const unencryptedContentMap = {
|
||||
"@alice:example.org": { "*": { hello: "alice!" } },
|
||||
"@bob:example.org": { bobDesktop: { hello: "bob!" } },
|
||||
const unencryptedContentMap = new Map([
|
||||
["@alice:example.org", new Map([["*", { hello: "alice!" }]])],
|
||||
["@bob:example.org", new Map([["bobDesktop", { hello: "bob!" }]])],
|
||||
]);
|
||||
|
||||
const expectedRequestData = {
|
||||
["@alice:example.org"]: { ["*"]: { hello: "alice!" } },
|
||||
["@bob:example.org"]: { ["bobDesktop"]: { hello: "bob!" } },
|
||||
};
|
||||
|
||||
it("sends unencrypted (sendToDevice)", async () => {
|
||||
@@ -214,7 +219,7 @@ describe("RoomWidgetClient", () => {
|
||||
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
await client.sendToDevice("org.example.foo", unencryptedContentMap);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, expectedRequestData);
|
||||
});
|
||||
|
||||
it("sends unencrypted (queueToDevice)", async () => {
|
||||
@@ -229,7 +234,7 @@ describe("RoomWidgetClient", () => {
|
||||
],
|
||||
};
|
||||
await client.queueToDevice(batch);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, unencryptedContentMap);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, expectedRequestData);
|
||||
});
|
||||
|
||||
it("sends encrypted (encryptAndSendToDevices)", async () => {
|
||||
|
||||
@@ -725,6 +725,7 @@ describe("MatrixClient", function () {
|
||||
getMyMembership: () => "join",
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
@@ -743,6 +744,7 @@ describe("MatrixClient", function () {
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
/* eslint-enable jest/no-conditional-expect */
|
||||
},
|
||||
} as Room["currentState"],
|
||||
} as unknown as Room;
|
||||
@@ -785,6 +787,7 @@ describe("MatrixClient", function () {
|
||||
getMyMembership: () => "join",
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
@@ -803,6 +806,7 @@ describe("MatrixClient", function () {
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
/* eslint-enable jest/no-conditional-expect */
|
||||
},
|
||||
} as Room["currentState"],
|
||||
} as unknown as Room;
|
||||
@@ -820,6 +824,7 @@ describe("MatrixClient", function () {
|
||||
getMyMembership: () => "join",
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
@@ -837,6 +842,7 @@ describe("MatrixClient", function () {
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
/* eslint-enable jest/no-conditional-expect */
|
||||
},
|
||||
} as Room["currentState"],
|
||||
} as unknown as Room;
|
||||
@@ -858,6 +864,7 @@ describe("MatrixClient", function () {
|
||||
const syncPromise = new Promise<void>((resolve, reject) => {
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
if (state === "SYNCING") {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
client.removeListener(ClientEvent.Sync, syncListener);
|
||||
resolve();
|
||||
@@ -944,6 +951,7 @@ describe("MatrixClient", function () {
|
||||
|
||||
const wasPreparedPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(2);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
@@ -955,6 +963,7 @@ describe("MatrixClient", function () {
|
||||
// unexpected state transition!
|
||||
expect(state).toEqual(null);
|
||||
}
|
||||
/* eslint-enable jest/no-conditional-expect */
|
||||
});
|
||||
});
|
||||
await client.startClient();
|
||||
@@ -976,8 +985,10 @@ describe("MatrixClient", function () {
|
||||
const isSyncingPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
expect(httpLookups.length).toEqual(1);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
/* eslint-enable jest/no-conditional-expect */
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
|
||||
jest.advanceTimersByTime(10000);
|
||||
@@ -1004,6 +1015,7 @@ describe("MatrixClient", function () {
|
||||
|
||||
const wasPreparedPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(3);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
@@ -1015,6 +1027,7 @@ describe("MatrixClient", function () {
|
||||
// unexpected state transition!
|
||||
expect(state).toEqual(null);
|
||||
}
|
||||
/* eslint-enable jest/no-conditional-expect */
|
||||
});
|
||||
});
|
||||
await client.startClient();
|
||||
|
||||
@@ -265,11 +265,13 @@ describe("MSC3089Branch", () => {
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
|
||||
expect(stateKey).toEqual(stateKeyOrder[stateFn.mock.calls.length - 1]);
|
||||
if (stateKey === fileEventId) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(content).toMatchObject({
|
||||
retained: true, // canary for copying state
|
||||
active: false,
|
||||
});
|
||||
} else if (stateKey === fileEventId2) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(content).toMatchObject({
|
||||
active: true,
|
||||
version: 2,
|
||||
|
||||
@@ -130,14 +130,8 @@ describe("MSC3089TreeSpace", () => {
|
||||
return Promise.reject(new MatrixError({ errcode: "M_FORBIDDEN", error: "Sample Failure" }));
|
||||
});
|
||||
client.invite = fn;
|
||||
try {
|
||||
await tree.invite(target, false, false);
|
||||
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect((<MatrixError>e).errcode).toEqual("M_FORBIDDEN");
|
||||
}
|
||||
await expect(tree.invite(target, false, false)).rejects.toThrow("MatrixError: Sample Failure");
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -357,13 +351,18 @@ describe("MSC3089TreeSpace", () => {
|
||||
.fn()
|
||||
.mockImplementation(async (roomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||
expect([tree.roomId, subspaceId]).toContain(roomId);
|
||||
|
||||
let expectedType: string;
|
||||
let expectedStateKey: string;
|
||||
if (roomId === subspaceId) {
|
||||
expect(eventType).toEqual(EventType.SpaceParent);
|
||||
expect(stateKey).toEqual(tree.roomId);
|
||||
expectedType = EventType.SpaceParent;
|
||||
expectedStateKey = tree.roomId;
|
||||
} else {
|
||||
expect(eventType).toEqual(EventType.SpaceChild);
|
||||
expect(stateKey).toEqual(subspaceId);
|
||||
expectedType = EventType.SpaceChild;
|
||||
expectedStateKey = subspaceId;
|
||||
}
|
||||
expect(eventType).toEqual(expectedType);
|
||||
expect(stateKey).toEqual(expectedStateKey);
|
||||
expect(content).toMatchObject({ via: [domain] });
|
||||
|
||||
// return value not used
|
||||
@@ -629,15 +628,8 @@ describe("MSC3089TreeSpace", () => {
|
||||
});
|
||||
|
||||
it("should throw when setting an order at the top level space", async () => {
|
||||
try {
|
||||
// The tree is what we've defined as top level, so it should work
|
||||
await tree.setOrder(2);
|
||||
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect((<Error>e).message).toEqual("Cannot set order of top level spaces currently");
|
||||
}
|
||||
// The tree is what we've defined as top level, so it should work
|
||||
await expect(tree.setOrder(2)).rejects.toThrow("Cannot set order of top level spaces currently");
|
||||
});
|
||||
|
||||
it("should return a stable order for unordered children", () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
import { Crypto } from "../../../src/crypto";
|
||||
import { Crypto, IEventDecryptionResult } from "../../../src/crypto";
|
||||
|
||||
describe("MatrixEvent", () => {
|
||||
it("should create copies of itself", () => {
|
||||
@@ -182,4 +182,38 @@ describe("MatrixEvent", () => {
|
||||
expect(encryptedEvent.getType()).toEqual("m.room.message");
|
||||
});
|
||||
});
|
||||
|
||||
describe("replyEventId", () => {
|
||||
it("should ignore 'm.relates_to' from encrypted content even if cleartext lacks one", async () => {
|
||||
const eventId = "test_encrypted_event";
|
||||
const encryptedEvent = new MatrixEvent({
|
||||
event_id: eventId,
|
||||
type: "m.room.encrypted",
|
||||
content: {
|
||||
ciphertext: "secrets",
|
||||
},
|
||||
});
|
||||
|
||||
const crypto = {
|
||||
decryptEvent: jest.fn().mockImplementationOnce(() => {
|
||||
return Promise.resolve<IEventDecryptionResult>({
|
||||
clearEvent: {
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "!anotherEvent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
} as unknown as Crypto;
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
expect(encryptedEvent.getType()).toEqual("m.room.message");
|
||||
expect(encryptedEvent.replyEventId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+301
-17
@@ -1,6 +1,6 @@
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { IActionsObject, PushProcessor } from "../../src/pushprocessor";
|
||||
import { EventType, IContent, MatrixClient, MatrixEvent } from "../../src";
|
||||
import { ConditionKind, EventType, IContent, MatrixClient, MatrixEvent, PushRuleActionName, RuleId } from "../../src";
|
||||
|
||||
describe("NotificationService", function () {
|
||||
const testUserId = "@ali:matrix.org";
|
||||
@@ -11,6 +11,23 @@ describe("NotificationService", function () {
|
||||
|
||||
let pushProcessor: PushProcessor;
|
||||
|
||||
const msc3914RoomCallRule = {
|
||||
rule_id: ".org.matrix.msc3914.rule.room.call",
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: [
|
||||
{
|
||||
kind: "event_match",
|
||||
key: "type",
|
||||
pattern: "org.matrix.msc3401.call",
|
||||
},
|
||||
{
|
||||
kind: "call_started",
|
||||
},
|
||||
],
|
||||
actions: ["notify", { set_tweak: "sound", value: "default" }],
|
||||
};
|
||||
|
||||
// These would be better if individual rules were configured in the tests themselves.
|
||||
const matrixClient = {
|
||||
getRoom: function () {
|
||||
@@ -31,6 +48,7 @@ describe("NotificationService", function () {
|
||||
credentials: {
|
||||
userId: testUserId,
|
||||
},
|
||||
supportsIntentionalMentions: () => true,
|
||||
pushRules: {
|
||||
device: {},
|
||||
global: {
|
||||
@@ -163,26 +181,11 @@ describe("NotificationService", function () {
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.room_one_to_one",
|
||||
},
|
||||
{
|
||||
rule_id: ".org.matrix.msc3914.rule.room.call",
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: [
|
||||
{
|
||||
kind: "event_match",
|
||||
key: "type",
|
||||
pattern: "org.matrix.msc3401.call",
|
||||
},
|
||||
{
|
||||
kind: "call_started",
|
||||
},
|
||||
],
|
||||
actions: ["notify", { set_tweak: "sound", value: "default" }],
|
||||
},
|
||||
],
|
||||
room: [],
|
||||
sender: [],
|
||||
underride: [
|
||||
msc3914RoomCallRule,
|
||||
{
|
||||
actions: ["dont-notify"],
|
||||
conditions: [
|
||||
@@ -287,6 +290,13 @@ describe("NotificationService", function () {
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
// TODO: This is not spec compliant behaviour.
|
||||
//
|
||||
// See https://spec.matrix.org/v1.5/client-server-api/#conditions-1 which
|
||||
// describes pattern should glob:
|
||||
//
|
||||
// 1. * matches 0 or more characters;
|
||||
// 2. ? matches exactly one character
|
||||
it("should bing on character group ([abc]) bing words.", function () {
|
||||
testEvent.event.content!.body = "Ping!";
|
||||
let actions = pushProcessor.actionsForEvent(testEvent);
|
||||
@@ -296,12 +306,14 @@ describe("NotificationService", function () {
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
// TODO: This is not spec compliant behaviour. (See above.)
|
||||
it("should bing on character range ([a-z]) bing words.", function () {
|
||||
testEvent.event.content!.body = "I ate 6 pies";
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(true);
|
||||
});
|
||||
|
||||
// TODO: This is not spec compliant behaviour. (See above.)
|
||||
it("should bing on character negation ([!a]) bing words.", function () {
|
||||
testEvent.event.content!.body = "boke";
|
||||
let actions = pushProcessor.actionsForEvent(testEvent);
|
||||
@@ -330,6 +342,8 @@ describe("NotificationService", function () {
|
||||
// invalid
|
||||
|
||||
it("should gracefully handle bad input.", function () {
|
||||
// The following body is an object (not a string) and thus is invalid
|
||||
// for matching against.
|
||||
testEvent.event.content!.body = { foo: "bar" };
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions.tweaks.highlight).toEqual(false);
|
||||
@@ -493,4 +507,274 @@ describe("NotificationService", function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test exact event matching", () => {
|
||||
it.each([
|
||||
// Simple string matching.
|
||||
{ value: "bar", eventValue: "bar", expected: true },
|
||||
// Matches are case-sensitive.
|
||||
{ value: "bar", eventValue: "BAR", expected: false },
|
||||
// Matches must match the full string.
|
||||
{ value: "bar", eventValue: "barbar", expected: false },
|
||||
// Values should not be type-coerced.
|
||||
{ value: "bar", eventValue: true, expected: false },
|
||||
{ value: "bar", eventValue: 1, expected: false },
|
||||
{ value: "bar", eventValue: false, expected: false },
|
||||
// Boolean matching.
|
||||
{ value: true, eventValue: true, expected: true },
|
||||
{ value: false, eventValue: false, expected: true },
|
||||
// Types should not be coerced.
|
||||
{ value: true, eventValue: "true", expected: false },
|
||||
{ value: true, eventValue: 1, expected: false },
|
||||
{ value: false, eventValue: null, expected: false },
|
||||
// Null matching.
|
||||
{ value: null, eventValue: null, expected: true },
|
||||
// Types should not be coerced
|
||||
{ value: null, eventValue: false, expected: false },
|
||||
{ value: null, eventValue: 0, expected: false },
|
||||
{ value: null, eventValue: "", expected: false },
|
||||
{ value: null, eventValue: undefined, expected: false },
|
||||
// Compound values should never be matched.
|
||||
{ value: "bar", eventValue: ["bar"], expected: false },
|
||||
{ value: "bar", eventValue: { bar: true }, expected: false },
|
||||
{ value: true, eventValue: [true], expected: false },
|
||||
{ value: true, eventValue: { true: true }, expected: false },
|
||||
{ value: null, eventValue: [], expected: false },
|
||||
{ value: null, eventValue: {}, expected: false },
|
||||
])("test $value against $eventValue", ({ value, eventValue, expected }) => {
|
||||
matrixClient.pushRules! = {
|
||||
global: {
|
||||
override: [
|
||||
{
|
||||
actions: [PushRuleActionName.Notify],
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.EventPropertyIs,
|
||||
key: "content.foo",
|
||||
value: value,
|
||||
},
|
||||
],
|
||||
default: true,
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.test",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
testEvent = utils.mkEvent({
|
||||
type: "m.room.message",
|
||||
room: testRoomId,
|
||||
user: "@alfred:localhost",
|
||||
event: true,
|
||||
content: {
|
||||
foo: eventValue,
|
||||
},
|
||||
});
|
||||
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(!!actions?.notify).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test event property contains", () => {
|
||||
it.each([
|
||||
// Simple string matching.
|
||||
{ value: "bar", eventValue: ["bar"], expected: true },
|
||||
// Matches are case-sensitive.
|
||||
{ value: "bar", eventValue: ["BAR"], expected: false },
|
||||
// Values should not be type-coerced.
|
||||
{ value: "bar", eventValue: [true], expected: false },
|
||||
{ value: "bar", eventValue: [1], expected: false },
|
||||
{ value: "bar", eventValue: [false], expected: false },
|
||||
// Boolean matching.
|
||||
{ value: true, eventValue: [true], expected: true },
|
||||
{ value: false, eventValue: [false], expected: true },
|
||||
// Types should not be coerced.
|
||||
{ value: true, eventValue: ["true"], expected: false },
|
||||
{ value: true, eventValue: [1], expected: false },
|
||||
{ value: false, eventValue: [null], expected: false },
|
||||
// Null matching.
|
||||
{ value: null, eventValue: [null], expected: true },
|
||||
// Types should not be coerced
|
||||
{ value: null, eventValue: [false], expected: false },
|
||||
{ value: null, eventValue: [0], expected: false },
|
||||
{ value: null, eventValue: [""], expected: false },
|
||||
{ value: null, eventValue: [undefined], expected: false },
|
||||
// Non-array or empty values should never be matched.
|
||||
{ value: "bar", eventValue: "bar", expected: false },
|
||||
{ value: "bar", eventValue: { bar: true }, expected: false },
|
||||
{ value: true, eventValue: { true: true }, expected: false },
|
||||
{ value: true, eventValue: true, expected: false },
|
||||
{ value: null, eventValue: [], expected: false },
|
||||
{ value: null, eventValue: {}, expected: false },
|
||||
{ value: null, eventValue: null, expected: false },
|
||||
{ value: null, eventValue: undefined, expected: false },
|
||||
])("test $value against $eventValue", ({ value, eventValue, expected }) => {
|
||||
matrixClient.pushRules! = {
|
||||
global: {
|
||||
override: [
|
||||
{
|
||||
actions: [PushRuleActionName.Notify],
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.EventPropertyContains,
|
||||
key: "content.foo",
|
||||
value: value,
|
||||
},
|
||||
],
|
||||
default: true,
|
||||
enabled: true,
|
||||
rule_id: ".m.rule.test",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
testEvent = utils.mkEvent({
|
||||
type: "m.room.message",
|
||||
room: testRoomId,
|
||||
user: "@alfred:localhost",
|
||||
event: true,
|
||||
content: {
|
||||
foo: eventValue,
|
||||
},
|
||||
});
|
||||
|
||||
const actions = pushProcessor.actionsForEvent(testEvent);
|
||||
expect(actions?.notify).toBe(expected ? true : undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
// The properly escaped key works.
|
||||
{ key: "content.m\\.test.foo", pattern: "bar", expected: true },
|
||||
// An unescaped version does not match.
|
||||
{ key: "content.m.test.foo", pattern: "bar", expected: false },
|
||||
// Over escaping does not match.
|
||||
{ key: "content.m\\.test\\.foo", pattern: "bar", expected: false },
|
||||
// Escaping backslashes should match.
|
||||
{ key: "content.m\\\\example", pattern: "baz", expected: true },
|
||||
// An unnecessary escape sequence leaves the backslash and still matches.
|
||||
{ key: "content.m\\example", pattern: "baz", expected: true },
|
||||
])("test against escaped dotted paths '$key'", ({ key, pattern, expected }) => {
|
||||
testEvent = utils.mkEvent({
|
||||
type: "m.room.message",
|
||||
room: testRoomId,
|
||||
user: "@alfred:localhost",
|
||||
event: true,
|
||||
content: {
|
||||
// A dot in the field name.
|
||||
"m.test": { foo: "bar" },
|
||||
// A backslash in a field name.
|
||||
"m\\example": "baz",
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
pushProcessor.ruleMatchesEvent(
|
||||
{
|
||||
rule_id: "rule1",
|
||||
actions: [],
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: key,
|
||||
pattern: pattern,
|
||||
},
|
||||
],
|
||||
default: false,
|
||||
enabled: true,
|
||||
},
|
||||
testEvent,
|
||||
),
|
||||
).toBe(expected);
|
||||
});
|
||||
|
||||
describe("getPushRuleById()", () => {
|
||||
it("returns null when rule id is not in rule set", () => {
|
||||
expect(pushProcessor.getPushRuleById("non-existant-rule")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns push rule when it is found in rule set", () => {
|
||||
expect(pushProcessor.getPushRuleById(".org.matrix.msc3914.rule.room.call")).toEqual(msc3914RoomCallRule);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPushRuleAndKindById()", () => {
|
||||
it("returns null when rule id is not in rule set", () => {
|
||||
expect(pushProcessor.getPushRuleAndKindById("non-existant-rule")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns push rule when it is found in rule set", () => {
|
||||
expect(pushProcessor.getPushRuleAndKindById(".org.matrix.msc3914.rule.room.call")).toEqual({
|
||||
kind: "underride",
|
||||
rule: msc3914RoomCallRule,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("test intentional mentions behaviour", () => {
|
||||
it.each([RuleId.ContainsUserName, RuleId.ContainsDisplayName, RuleId.AtRoomNotification])(
|
||||
"Rule %s matches unless intentional mentions are enabled",
|
||||
(ruleId) => {
|
||||
const rule = {
|
||||
rule_id: ruleId,
|
||||
actions: [],
|
||||
conditions: [],
|
||||
default: false,
|
||||
enabled: true,
|
||||
};
|
||||
expect(pushProcessor.ruleMatchesEvent(rule, testEvent)).toBe(true);
|
||||
|
||||
// Add the mentions property to the event and the rule is now disabled.
|
||||
testEvent = utils.mkEvent({
|
||||
type: "m.room.message",
|
||||
room: testRoomId,
|
||||
user: "@alfred:localhost",
|
||||
event: true,
|
||||
content: {
|
||||
"body": "",
|
||||
"msgtype": "m.text",
|
||||
"org.matrix.msc3952.mentions": {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(pushProcessor.ruleMatchesEvent(rule, testEvent)).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test PushProcessor.partsForDottedKey", function () {
|
||||
it.each([
|
||||
// A field with no dots.
|
||||
["m", ["m"]],
|
||||
// Simple dotted fields.
|
||||
["m.foo", ["m", "foo"]],
|
||||
["m.foo.bar", ["m", "foo", "bar"]],
|
||||
// Backslash is used as an escape character.
|
||||
["m\\.foo", ["m.foo"]],
|
||||
["m\\\\.foo", ["m\\", "foo"]],
|
||||
["m\\\\\\.foo", ["m\\.foo"]],
|
||||
["m\\\\\\\\.foo", ["m\\\\", "foo"]],
|
||||
["m\\foo", ["m\\foo"]],
|
||||
["m\\\\foo", ["m\\foo"]],
|
||||
["m\\\\\\foo", ["m\\\\foo"]],
|
||||
["m\\\\\\\\foo", ["m\\\\foo"]],
|
||||
// Ensure that escapes at the end don't cause issues.
|
||||
["m.foo\\", ["m", "foo\\"]],
|
||||
["m.foo\\\\", ["m", "foo\\"]],
|
||||
["m.foo\\.", ["m", "foo."]],
|
||||
["m.foo\\\\.", ["m", "foo\\", ""]],
|
||||
["m.foo\\\\\\.", ["m", "foo\\."]],
|
||||
// Empty parts (corresponding to properties which are an empty string) are allowed.
|
||||
[".m", ["", "m"]],
|
||||
["..m", ["", "", "m"]],
|
||||
["m.", ["m", ""]],
|
||||
["m..", ["m", "", ""]],
|
||||
["m..foo", ["m", "", "foo"]],
|
||||
])("partsFotDottedKey for %s", (path: string, expected: string[]) => {
|
||||
expect(PushProcessor.partsForDottedKey(path)).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import "../../olm-loader";
|
||||
import { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
|
||||
import { MSC3903ECDHPayload, MSC3903ECDHv1RendezvousChannel } from "../../../src/rendezvous/channels";
|
||||
import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "../../../src/rendezvous/channels";
|
||||
import { decodeBase64 } from "../../../src/crypto/olmlib";
|
||||
import { DummyTransport } from "./DummyTransport";
|
||||
|
||||
@@ -24,7 +24,7 @@ function makeTransport(name: string) {
|
||||
return new DummyTransport<any, MSC3903ECDHPayload>(name, { type: "dummy" });
|
||||
}
|
||||
|
||||
describe("ECDHv1", function () {
|
||||
describe("ECDHv2", function () {
|
||||
beforeAll(async function () {
|
||||
await global.Olm.init();
|
||||
});
|
||||
@@ -37,9 +37,9 @@ describe("ECDHv1", function () {
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
@@ -62,9 +62,9 @@ describe("ECDHv1", function () {
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
@@ -87,9 +87,9 @@ describe("ECDHv1", function () {
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
@@ -109,9 +109,9 @@ describe("ECDHv1", function () {
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
@@ -135,9 +135,9 @@ describe("ECDHv1", function () {
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
const aliceCode = await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
const bob = new MSC3903ECDHv1RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
const bob = new MSC3903ECDHv2RendezvousChannel(bobTransport, decodeBase64(aliceCode.rendezvous.key));
|
||||
|
||||
const bobChecksum = await bob.connect();
|
||||
const aliceChecksum = await alice.connect();
|
||||
@@ -159,7 +159,7 @@ describe("ECDHv1", function () {
|
||||
bobTransport.otherParty = aliceTransport;
|
||||
|
||||
// alice is signing in initiates and generates a code
|
||||
const alice = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const alice = new MSC3903ECDHv2RendezvousChannel(aliceTransport);
|
||||
await alice.generateCode(RendezvousIntent.LOGIN_ON_NEW_DEVICE);
|
||||
|
||||
await bobTransport.send({ iv: "dummy", ciphertext: "dummy" });
|
||||
@@ -19,9 +19,9 @@ import MockHttpBackend from "matrix-mock-request";
|
||||
import "../../olm-loader";
|
||||
import { MSC3906Rendezvous, RendezvousCode, RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
|
||||
import {
|
||||
ECDHv1RendezvousCode,
|
||||
ECDHv2RendezvousCode as ECDHRendezvousCode,
|
||||
MSC3903ECDHPayload,
|
||||
MSC3903ECDHv1RendezvousChannel,
|
||||
MSC3903ECDHv2RendezvousChannel as MSC3903ECDHRendezvousChannel,
|
||||
} from "../../../src/rendezvous/channels";
|
||||
import { MatrixClient } from "../../../src";
|
||||
import {
|
||||
@@ -126,7 +126,7 @@ describe("Rendezvous", function () {
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport);
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
|
||||
expect(aliceRz.code).toBeUndefined();
|
||||
@@ -143,7 +143,7 @@ describe("Rendezvous", function () {
|
||||
const code = JSON.parse(aliceRz.code!) as RendezvousCode;
|
||||
|
||||
expect(code.intent).toEqual(RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE);
|
||||
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256");
|
||||
expect(code.rendezvous?.algorithm).toEqual("org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256");
|
||||
expect(code.rendezvous?.transport.type).toEqual("org.matrix.msc3886.http.v1");
|
||||
expect((code.rendezvous?.transport as MSC3886SimpleHttpRendezvousTransportDetails).uri).toEqual(
|
||||
"https://fallbackserver/rz/123",
|
||||
@@ -181,11 +181,11 @@ describe("Rendezvous", function () {
|
||||
msc3882Enabled: false,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
@@ -193,7 +193,7 @@ describe("Rendezvous", function () {
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
@@ -235,11 +235,11 @@ describe("Rendezvous", function () {
|
||||
msc3882Enabled: true,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
@@ -247,7 +247,7 @@ describe("Rendezvous", function () {
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
@@ -293,11 +293,11 @@ describe("Rendezvous", function () {
|
||||
msc3882Enabled: true,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
@@ -305,7 +305,7 @@ describe("Rendezvous", function () {
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
@@ -351,11 +351,11 @@ describe("Rendezvous", function () {
|
||||
msc3882Enabled: true,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
@@ -363,7 +363,7 @@ describe("Rendezvous", function () {
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
@@ -411,11 +411,11 @@ describe("Rendezvous", function () {
|
||||
msc3882Enabled: true,
|
||||
msc3886Enabled: false,
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
@@ -423,7 +423,7 @@ describe("Rendezvous", function () {
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
@@ -485,11 +485,11 @@ describe("Rendezvous", function () {
|
||||
master: "mmmmm",
|
||||
},
|
||||
});
|
||||
const aliceEcdh = new MSC3903ECDHv1RendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
|
||||
const aliceRz = new MSC3906Rendezvous(aliceEcdh, alice);
|
||||
aliceTransport.onCancelled = aliceOnFailure;
|
||||
await aliceRz.generateCode();
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHv1RendezvousCode;
|
||||
const code = JSON.parse(aliceRz.code!) as ECDHRendezvousCode;
|
||||
|
||||
expect(code.rendezvous.key).toBeDefined();
|
||||
|
||||
@@ -497,7 +497,7 @@ describe("Rendezvous", function () {
|
||||
|
||||
// bob is try to sign in and scans the code
|
||||
const bobOnFailure = jest.fn();
|
||||
const bobEcdh = new MSC3903ECDHv1RendezvousChannel(
|
||||
const bobEcdh = new MSC3903ECDHRendezvousChannel(
|
||||
bobTransport,
|
||||
decodeBase64(code.rendezvous.key), // alice's public key
|
||||
bobOnFailure,
|
||||
|
||||
@@ -25,6 +25,8 @@ import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@typ
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { M_BEACON } from "../../src/@types/beacon";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { DecryptionError } from "../../src/crypto/algorithms";
|
||||
import { defer } from "../../src/utils";
|
||||
|
||||
describe("RoomState", function () {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -886,7 +888,7 @@ describe("RoomState", function () {
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("adds locations to beacons", () => {
|
||||
it("adds locations to beacons", async () => {
|
||||
const location1 = makeBeaconEvent(userA, {
|
||||
beaconInfoId: "$beacon1",
|
||||
timestamp: Date.now() + 1,
|
||||
@@ -906,7 +908,7 @@ describe("RoomState", function () {
|
||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
|
||||
const addLocationsSpy = jest.spyOn(beaconInstance, "addLocations");
|
||||
|
||||
state.processBeaconEvents([location1, location2, location3], mockClient);
|
||||
await state.processBeaconEvents([location1, location2, location3], mockClient);
|
||||
|
||||
expect(addLocationsSpy).toHaveBeenCalledTimes(2);
|
||||
// only called with locations for beacon1
|
||||
@@ -978,51 +980,50 @@ describe("RoomState", function () {
|
||||
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("decrypts related events if needed", () => {
|
||||
it("decrypts related events if needed", async () => {
|
||||
const location = makeBeaconEvent(userA, {
|
||||
beaconInfoId: beacon1.getId(),
|
||||
});
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
state.processBeaconEvents([location, relatedEncryptedEvent], mockClient);
|
||||
await state.processBeaconEvents([location, relatedEncryptedEvent], mockClient);
|
||||
// discard unrelated events early
|
||||
expect(mockClient.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("listens for decryption on events that are being decrypted", () => {
|
||||
it("awaits for decryption on events that are being decrypted", async () => {
|
||||
const decryptingRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: beacon1RelationContent,
|
||||
});
|
||||
jest.spyOn(decryptingRelatedEvent, "isBeingDecrypted").mockReturnValue(true);
|
||||
// spy on event.once
|
||||
const eventOnceSpy = jest.spyOn(decryptingRelatedEvent, "once");
|
||||
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
await state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
|
||||
// listener was added
|
||||
expect(eventOnceSpy).toHaveBeenCalled();
|
||||
expect(mockClient.decryptEventIfNeeded).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("listens for decryption on events that have decryption failure", () => {
|
||||
it("listens for decryption on events that have decryption failure", async () => {
|
||||
const failedDecryptionRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: beacon1RelationContent,
|
||||
});
|
||||
jest.spyOn(failedDecryptionRelatedEvent, "isDecryptionFailure").mockReturnValue(true);
|
||||
mockClient.decryptEventIfNeeded.mockRejectedValue(new DecryptionError("ERR", "msg"));
|
||||
// spy on event.once
|
||||
const eventOnceSpy = jest.spyOn(decryptingRelatedEvent, "once");
|
||||
const eventOnceSpy = jest.spyOn(failedDecryptionRelatedEvent, "once");
|
||||
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
await state.processBeaconEvents([failedDecryptionRelatedEvent], mockClient);
|
||||
|
||||
// listener was added
|
||||
expect(eventOnceSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("discard events that are not m.beacon type after decryption", () => {
|
||||
it("discard events that are not m.beacon type after decryption", async () => {
|
||||
const decryptingRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
@@ -1032,7 +1033,7 @@ describe("RoomState", function () {
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
|
||||
const addLocationsSpy = jest.spyOn(beacon, "addLocations").mockClear();
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
await state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
|
||||
// this event is a message after decryption
|
||||
decryptingRelatedEvent.event.type = EventType.RoomMessage;
|
||||
@@ -1041,7 +1042,7 @@ describe("RoomState", function () {
|
||||
expect(addLocationsSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("adds locations to beacons after decryption", () => {
|
||||
it("adds locations to beacons after decryption", async () => {
|
||||
const decryptingRelatedEvent = new MatrixEvent({
|
||||
sender: userA,
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
@@ -1051,16 +1052,20 @@ describe("RoomState", function () {
|
||||
beaconInfoId: "$beacon1",
|
||||
timestamp: Date.now() + 1,
|
||||
});
|
||||
jest.spyOn(decryptingRelatedEvent, "isBeingDecrypted").mockReturnValue(true);
|
||||
|
||||
const deferred = defer<void>();
|
||||
mockClient.decryptEventIfNeeded.mockReturnValue(deferred.promise);
|
||||
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
|
||||
const addLocationsSpy = jest.spyOn(beacon, "addLocations").mockClear();
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
const prom = state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
|
||||
// update type after '''decryption'''
|
||||
decryptingRelatedEvent.event.type = M_BEACON.name;
|
||||
decryptingRelatedEvent.event.content = locationEvent.event.content;
|
||||
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted, decryptingRelatedEvent);
|
||||
deferred.resolve();
|
||||
await prom;
|
||||
|
||||
expect(addLocationsSpy).toHaveBeenCalledWith([decryptingRelatedEvent]);
|
||||
});
|
||||
|
||||
+25
-24
@@ -390,36 +390,30 @@ describe("Room", function () {
|
||||
remoteEvent.event.unsigned = { transaction_id: "TXN_ID" };
|
||||
const remoteEventId = remoteEvent.getId();
|
||||
|
||||
let callCount = 0;
|
||||
room.on(RoomEvent.LocalEchoUpdated, (event, emitRoom, oldEventId, oldStatus) => {
|
||||
switch (callCount) {
|
||||
case 0:
|
||||
expect(event.getId()).toEqual(localEventId);
|
||||
expect(event.status).toEqual(EventStatus.SENDING);
|
||||
expect(emitRoom).toEqual(room);
|
||||
expect(oldEventId).toBeUndefined();
|
||||
expect(oldStatus).toBeUndefined();
|
||||
break;
|
||||
case 1:
|
||||
expect(event.getId()).toEqual(remoteEventId);
|
||||
expect(event.status).toBeNull();
|
||||
expect(emitRoom).toEqual(room);
|
||||
expect(oldEventId).toEqual(localEventId);
|
||||
expect(oldStatus).toBe(EventStatus.SENDING);
|
||||
break;
|
||||
}
|
||||
callCount += 1;
|
||||
});
|
||||
const stub = jest.fn();
|
||||
room.on(RoomEvent.LocalEchoUpdated, stub);
|
||||
|
||||
// first add the local echo
|
||||
room.addPendingEvent(localEvent, "TXN_ID");
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
expect(stub.mock.calls[0][0].getId()).toEqual(localEventId);
|
||||
expect(stub.mock.calls[0][0].status).toEqual(EventStatus.SENDING);
|
||||
expect(stub.mock.calls[0][1]).toEqual(room);
|
||||
expect(stub.mock.calls[0][2]).toBeUndefined();
|
||||
expect(stub.mock.calls[0][3]).toBeUndefined();
|
||||
|
||||
// then the remoteEvent
|
||||
room.addLiveEvents([remoteEvent]);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
expect(callCount).toEqual(2);
|
||||
expect(stub).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(stub.mock.calls[1][0].getId()).toEqual(remoteEventId);
|
||||
expect(stub.mock.calls[1][0].status).toBeNull();
|
||||
expect(stub.mock.calls[1][1]).toEqual(room);
|
||||
expect(stub.mock.calls[1][2]).toEqual(localEventId);
|
||||
expect(stub.mock.calls[1][3]).toBe(EventStatus.SENDING);
|
||||
});
|
||||
|
||||
it("should be able to update local echo without a txn ID (/send then /sync)", function () {
|
||||
@@ -3349,11 +3343,16 @@ describe("Room", function () {
|
||||
newRoomId: string,
|
||||
predecessorRoomId: string,
|
||||
tombstoneEventId: string | null = null,
|
||||
viaServers: string[] = [],
|
||||
): MatrixEvent {
|
||||
const content =
|
||||
tombstoneEventId === null
|
||||
? { predecessor_room_id: predecessorRoomId }
|
||||
: { predecessor_room_id: predecessorRoomId, last_known_event_id: tombstoneEventId };
|
||||
? { predecessor_room_id: predecessorRoomId, via_servers: viaServers }
|
||||
: {
|
||||
predecessor_room_id: predecessorRoomId,
|
||||
last_known_event_id: tombstoneEventId,
|
||||
via_servers: viaServers,
|
||||
};
|
||||
|
||||
return new MatrixEvent({
|
||||
content,
|
||||
@@ -3393,6 +3392,7 @@ describe("Room", function () {
|
||||
expect(room.findPredecessor(useMsc3946)).toEqual({
|
||||
roomId: "otherreplacedroomid",
|
||||
eventId: undefined, // m.predecessor did not include an event_id
|
||||
viaServers: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3400,12 +3400,13 @@ describe("Room", function () {
|
||||
const room = new Room("roomid", client!, "@u:example.com");
|
||||
room.addLiveEvents([
|
||||
roomCreateEvent("roomid", "replacedroomid"),
|
||||
predecessorEvent("roomid", "otherreplacedroomid", "lstevtid"),
|
||||
predecessorEvent("roomid", "otherreplacedroomid", "lstevtid", ["one.example.com", "two.example.com"]),
|
||||
]);
|
||||
const useMsc3946 = true;
|
||||
expect(room.findPredecessor(useMsc3946)).toEqual({
|
||||
roomId: "otherreplacedroomid",
|
||||
eventId: "lstevtid",
|
||||
viaServers: ["one.example.com", "two.example.com"],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ describe("RustCrypto", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("to-device messages", () => {
|
||||
describe("call preprocess methods", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -92,6 +92,16 @@ describe("RustCrypto", () => {
|
||||
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
|
||||
expect(res).toEqual(inputs);
|
||||
});
|
||||
|
||||
it("should pass through one time key counts", async () => {
|
||||
const oneTimeKeyCounts = new Map<string, number>([["signed_curve25519", 50]]);
|
||||
await expect(rustCrypto.preprocessOneTimeKeyCounts(oneTimeKeyCounts)).resolves.not.toBeDefined();
|
||||
});
|
||||
|
||||
it("should pass through unused fallback keys", async () => {
|
||||
const unusedFallbackKeys = new Set(["signed_curve25519"]);
|
||||
await expect(rustCrypto.preprocessUnusedFallbackKeys(unusedFallbackKeys)).resolves.not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("outgoing requests", () => {
|
||||
|
||||
@@ -58,10 +58,12 @@ describe("MatrixScheduler", function () {
|
||||
let yieldedA = false;
|
||||
scheduler.setProcessFunction(function (event) {
|
||||
if (yieldedA) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(event).toEqual(eventB);
|
||||
return deferB.promise;
|
||||
} else {
|
||||
yieldedA = true;
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(event).toEqual(eventA);
|
||||
return deferA.promise;
|
||||
}
|
||||
@@ -89,6 +91,7 @@ describe("MatrixScheduler", function () {
|
||||
scheduler.setProcessFunction(function (ev) {
|
||||
procCount += 1;
|
||||
if (procCount === 1) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(ev).toEqual(eventA);
|
||||
return deferred.promise;
|
||||
} else if (procCount === 2) {
|
||||
@@ -129,9 +132,11 @@ describe("MatrixScheduler", function () {
|
||||
scheduler.setProcessFunction(function (ev) {
|
||||
procCount += 1;
|
||||
if (procCount === 1) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(ev).toEqual(eventA);
|
||||
return deferA.promise;
|
||||
} else if (procCount === 2) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(ev).toEqual(eventB);
|
||||
return deferB.promise;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
|
||||
import { LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend";
|
||||
import { IndexedDBStoreWorker } from "../../../src/store/indexeddb-store-worker";
|
||||
import { defer } from "../../../src/utils";
|
||||
|
||||
function setupWorker(worker: IndexedDBStoreWorker): void {
|
||||
worker.onMessage({ data: { command: "setupWorker", args: [] } } as any);
|
||||
worker.onMessage({ data: { command: "connect", seq: 1 } } as any);
|
||||
}
|
||||
|
||||
describe("IndexedDBStore Worker", () => {
|
||||
it("should pass 'closed' event via postMessage", async () => {
|
||||
const deferred = defer<void>();
|
||||
const postMessage = jest.fn().mockImplementation(({ seq, command }) => {
|
||||
if (seq === 1 && command === "cmd_success") {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
const worker = new IndexedDBStoreWorker(postMessage);
|
||||
setupWorker(worker);
|
||||
|
||||
await deferred.promise;
|
||||
|
||||
// @ts-ignore - private field access
|
||||
(worker.backend as LocalIndexedDBStoreBackend).db!.onclose!({} as Event);
|
||||
expect(postMessage).toHaveBeenCalledWith({
|
||||
command: "closed",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -166,4 +166,92 @@ describe("IndexedDBStore", () => {
|
||||
|
||||
await expect(store.isNewlyCreated()).resolves.toBeFalsy();
|
||||
});
|
||||
|
||||
it("should emit 'closed' if database is unexpectedly closed", async () => {
|
||||
const store = new IndexedDBStore({
|
||||
indexedDB: indexedDB,
|
||||
dbName: "database",
|
||||
localStorage,
|
||||
});
|
||||
await store.startup();
|
||||
|
||||
const deferred = defer<void>();
|
||||
store.on("closed", deferred.resolve);
|
||||
|
||||
// @ts-ignore - private field access
|
||||
(store.backend as LocalIndexedDBStoreBackend).db!.onclose!({} as Event);
|
||||
await deferred.promise;
|
||||
});
|
||||
|
||||
it("should use remote backend if workerFactory passed", async () => {
|
||||
const deferred = defer<void>();
|
||||
class MockWorker {
|
||||
postMessage(data: any) {
|
||||
if (data.command === "setupWorker") {
|
||||
deferred.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const store = new IndexedDBStore({
|
||||
indexedDB: indexedDB,
|
||||
dbName: "database",
|
||||
localStorage,
|
||||
workerFactory: () => new MockWorker() as Worker,
|
||||
});
|
||||
store.startup();
|
||||
await deferred.promise;
|
||||
});
|
||||
|
||||
it("remote worker should pass closed event", async () => {
|
||||
const worker = new (class MockWorker {
|
||||
postMessage(data: any) {}
|
||||
})() as Worker;
|
||||
|
||||
const store = new IndexedDBStore({
|
||||
indexedDB: indexedDB,
|
||||
dbName: "database",
|
||||
localStorage,
|
||||
workerFactory: () => worker,
|
||||
});
|
||||
store.startup();
|
||||
|
||||
const deferred = defer<void>();
|
||||
store.on("closed", deferred.resolve);
|
||||
(worker as any).onmessage({ data: { command: "closed" } });
|
||||
await deferred.promise;
|
||||
});
|
||||
|
||||
it("remote worker should pass command failures", async () => {
|
||||
const worker = new (class MockWorker {
|
||||
private onmessage!: (data: any) => void;
|
||||
postMessage(data: any) {
|
||||
if (data.command === "setupWorker" || data.command === "connect") {
|
||||
this.onmessage({
|
||||
data: {
|
||||
command: "cmd_success",
|
||||
seq: data.seq,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.onmessage({
|
||||
data: {
|
||||
command: "cmd_fail",
|
||||
seq: data.seq,
|
||||
error: new Error("Test"),
|
||||
},
|
||||
});
|
||||
}
|
||||
})() as unknown as Worker;
|
||||
|
||||
const store = new IndexedDBStore({
|
||||
indexedDB: indexedDB,
|
||||
dbName: "database",
|
||||
localStorage,
|
||||
workerFactory: () => worker,
|
||||
});
|
||||
await expect(store.startup()).rejects.toThrow("Test");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("MemoryStore", () => {
|
||||
await store.deleteAllData();
|
||||
|
||||
// empty object
|
||||
expect(store.accountData).toEqual({});
|
||||
expect(store.accountData).toEqual(new Map());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,9 +24,12 @@ import {
|
||||
lexicographicCompare,
|
||||
nextString,
|
||||
prevString,
|
||||
recursiveMapToObject,
|
||||
simpleRetryOperation,
|
||||
stringToBase,
|
||||
sortEventsByLatestContentTimestamp,
|
||||
safeSet,
|
||||
MapWithDefault,
|
||||
} from "../../src/utils";
|
||||
import { logger } from "../../src/logger";
|
||||
import { mkMessage } from "../test-utils/test-utils";
|
||||
@@ -606,6 +609,105 @@ describe("utils", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("recursiveMapToObject", () => {
|
||||
it.each([
|
||||
// empty map
|
||||
{
|
||||
map: new Map(),
|
||||
expected: {},
|
||||
},
|
||||
// one level map
|
||||
{
|
||||
map: new Map<any, any>([
|
||||
["key1", "value 1"],
|
||||
["key2", 23],
|
||||
["key3", undefined],
|
||||
["key4", null],
|
||||
["key5", [1, 2, 3]],
|
||||
]),
|
||||
expected: { key1: "value 1", key2: 23, key3: undefined, key4: null, key5: [1, 2, 3] },
|
||||
},
|
||||
// two level map
|
||||
{
|
||||
map: new Map<any, any>([
|
||||
[
|
||||
"key1",
|
||||
new Map<any, any>([
|
||||
["key1_1", "value 1"],
|
||||
["key1_2", "value 1.2"],
|
||||
]),
|
||||
],
|
||||
["key2", "value 2"],
|
||||
]),
|
||||
expected: { key1: { key1_1: "value 1", key1_2: "value 1.2" }, key2: "value 2" },
|
||||
},
|
||||
// multi level map
|
||||
{
|
||||
map: new Map<any, any>([
|
||||
["key1", new Map<any, any>([["key1_1", new Map<any, any>([["key1_1_1", "value 1.1.1"]])]])],
|
||||
]),
|
||||
expected: { key1: { key1_1: { key1_1_1: "value 1.1.1" } } },
|
||||
},
|
||||
// list of maps
|
||||
{
|
||||
map: new Map<any, any>([
|
||||
[
|
||||
"key1",
|
||||
[new Map<any, any>([["key1_1", "value 1.1"]]), new Map<any, any>([["key1_2", "value 1.2"]])],
|
||||
],
|
||||
]),
|
||||
expected: { key1: [{ key1_1: "value 1.1" }, { key1_2: "value 1.2" }] },
|
||||
},
|
||||
// map → array → array → map
|
||||
{
|
||||
map: new Map<any, any>([["key1", [[new Map<any, any>([["key2", "value 2"]])]]]]),
|
||||
expected: {
|
||||
key1: [
|
||||
[
|
||||
{
|
||||
key2: "value 2",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
])("%# should convert the value", ({ map, expected }) => {
|
||||
expect(recursiveMapToObject(map)).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("safeSet", () => {
|
||||
it("should set a value", () => {
|
||||
const obj = {};
|
||||
safeSet(obj, "testProp", "test value");
|
||||
expect(obj).toEqual({ testProp: "test value" });
|
||||
});
|
||||
|
||||
it.each(["__proto__", "prototype", "constructor"])("should raise an error when setting »%s«", (prop) => {
|
||||
expect(() => {
|
||||
safeSet({}, prop, "teset value");
|
||||
}).toThrow("Trying to modify prototype or constructor");
|
||||
});
|
||||
});
|
||||
|
||||
describe("MapWithDefault", () => {
|
||||
it("getOrCreate should create the value if it does not exist", () => {
|
||||
const newValue = {};
|
||||
const map = new MapWithDefault(() => newValue);
|
||||
|
||||
// undefined before getOrCreate
|
||||
expect(map.get("test")).toBeUndefined();
|
||||
|
||||
expect(map.getOrCreate("test")).toBe(newValue);
|
||||
|
||||
// default value after getOrCreate
|
||||
expect(map.get("test")).toBe(newValue);
|
||||
|
||||
// test that it always returns the same value
|
||||
expect(map.getOrCreate("test")).toBe(newValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sleep", () => {
|
||||
it("resolves", async () => {
|
||||
await utils.sleep(0);
|
||||
|
||||
@@ -431,6 +431,58 @@ describe("Call", function () {
|
||||
expect(transceivers.get("m.usermedia:video")!.sender.track!.id).toBe("usermedia_video_track");
|
||||
});
|
||||
|
||||
it("should handle error on call upgrade", async () => {
|
||||
const onError = jest.fn();
|
||||
call.on(CallEvent.Error, onError);
|
||||
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
await call.onAnswerReceived(
|
||||
makeMockEvent("@test:foo", {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: "party_id",
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
[SDPStreamMetadataKey]: {},
|
||||
}),
|
||||
);
|
||||
|
||||
const mockGetUserMediaStream = jest.fn().mockRejectedValue(new Error("Test error"));
|
||||
client.client.getMediaHandler().getUserMediaStream = mockGetUserMediaStream;
|
||||
|
||||
// then unmute which should cause an upgrade
|
||||
await call.setLocalVideoMuted(false);
|
||||
|
||||
expect(onError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should unmute video after upgrading to video call", async () => {
|
||||
// Regression test for https://github.com/vector-im/element-call/issues/925
|
||||
await startVoiceCall(client, call);
|
||||
// start off with video muted
|
||||
await call.setLocalVideoMuted(true);
|
||||
|
||||
await call.onAnswerReceived(
|
||||
makeMockEvent("@test:foo", {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: "party_id",
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
[SDPStreamMetadataKey]: {},
|
||||
}),
|
||||
);
|
||||
|
||||
// then unmute which should cause an upgrade
|
||||
await call.setLocalVideoMuted(false);
|
||||
|
||||
// video should now be unmuted
|
||||
expect(call.isLocalVideoMuted()).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle SDPStreamMetadata changes", async () => {
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
@@ -712,11 +764,22 @@ describe("Call", function () {
|
||||
|
||||
const dataChannel = call.createDataChannel("data_channel_label", { id: 123 });
|
||||
|
||||
expect(dataChannelCallback).toHaveBeenCalledWith(dataChannel);
|
||||
expect(dataChannelCallback).toHaveBeenCalledWith(dataChannel, call);
|
||||
expect(dataChannel.label).toBe("data_channel_label");
|
||||
expect(dataChannel.id).toBe(123);
|
||||
});
|
||||
|
||||
it("should emit a data channel event when the other side adds a data channel", async () => {
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
const dataChannelCallback = jest.fn();
|
||||
call.on(CallEvent.DataChannel, dataChannelCallback);
|
||||
|
||||
(call.peerConn as unknown as MockRTCPeerConnection).triggerIncomingDataChannel();
|
||||
|
||||
expect(dataChannelCallback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("supportsMatrixCall", () => {
|
||||
it("should return true when the environment is right", () => {
|
||||
expect(supportsMatrixCall()).toBe(true);
|
||||
@@ -1579,7 +1642,7 @@ describe("Call", function () {
|
||||
hasAdvancedBy += advanceBy;
|
||||
|
||||
expect(lengthChangedListener).toHaveBeenCalledTimes(hasAdvancedBy);
|
||||
expect(lengthChangedListener).toHaveBeenCalledWith(hasAdvancedBy);
|
||||
expect(lengthChangedListener).toHaveBeenCalledWith(hasAdvancedBy, call);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1609,4 +1672,24 @@ describe("Call", function () {
|
||||
expect(call.hangup).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Call replace", () => {
|
||||
it("Fires event when call replaced", async () => {
|
||||
const onReplace = jest.fn();
|
||||
call.on(CallEvent.Replaced, onReplace);
|
||||
|
||||
await call.placeVoiceCall();
|
||||
|
||||
const call2 = new MatrixCall({
|
||||
client: client.client,
|
||||
roomId: FAKE_ROOM_ID,
|
||||
});
|
||||
call2.on(CallEvent.Error, errorListener);
|
||||
await fakeIncomingCall(client, call2);
|
||||
|
||||
call.replacedBy(call2);
|
||||
|
||||
expect(onReplace).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,7 +102,7 @@ describe("CallFeed", () => {
|
||||
[CallState.Connected, true],
|
||||
[CallState.Connecting, false],
|
||||
])("should react to call state, when !isLocal()", (state: CallState, expected: Boolean) => {
|
||||
call.emit(CallEvent.State, state);
|
||||
call.emit(CallEvent.State, state, CallState.InviteSent, call.typed());
|
||||
|
||||
expect(feed.connected).toBe(expected);
|
||||
});
|
||||
|
||||
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { EventType, GroupCallIntent, GroupCallType, MatrixCall, MatrixEvent, Room, RoomMember } from "../../../src";
|
||||
import { RoomStateEvent } from "../../../src/models/room-state";
|
||||
import { GroupCall, GroupCallEvent, GroupCallState } from "../../../src/webrtc/groupCall";
|
||||
@@ -95,16 +97,16 @@ const FAKE_STATE_EVENTS = [
|
||||
},
|
||||
];
|
||||
|
||||
const mockGetStateEvents = (type: EventType, userId?: string): MatrixEvent[] | MatrixEvent | null => {
|
||||
if (type === EventType.GroupCallMemberPrefix) {
|
||||
return userId === undefined
|
||||
? (FAKE_STATE_EVENTS as MatrixEvent[])
|
||||
: (FAKE_STATE_EVENTS.find((e) => e.getStateKey() === userId) as MatrixEvent);
|
||||
} else {
|
||||
const fakeEvent = { getContent: () => ({}), getTs: () => 0 } as MatrixEvent;
|
||||
return userId === undefined ? [fakeEvent] : fakeEvent;
|
||||
}
|
||||
};
|
||||
const mockGetStateEvents =
|
||||
(events: MatrixEvent[] = FAKE_STATE_EVENTS as MatrixEvent[]) =>
|
||||
(type: EventType, userId?: string): MatrixEvent[] | MatrixEvent | null => {
|
||||
if (type === EventType.GroupCallMemberPrefix) {
|
||||
return userId === undefined ? events : events.find((e) => e.getStateKey() === userId) ?? null;
|
||||
} else {
|
||||
const fakeEvent = { getContent: () => ({}), getTs: () => 0 } as MatrixEvent;
|
||||
return userId === undefined ? [fakeEvent] : fakeEvent;
|
||||
}
|
||||
};
|
||||
|
||||
const ONE_HOUR = 1000 * 60 * 60;
|
||||
|
||||
@@ -142,6 +144,10 @@ describe("Group Call", function () {
|
||||
} as unknown as RoomMember;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
groupCall.leave();
|
||||
});
|
||||
|
||||
it.each(Object.values(GroupCallState).filter((v) => v !== GroupCallState.LocalCallFeedUninitialized))(
|
||||
"throws when initializing local call feed in %s state",
|
||||
async (state: GroupCallState) => {
|
||||
@@ -567,7 +573,7 @@ describe("Group Call", function () {
|
||||
|
||||
// the call starts muted, so unmute to get in the right state to test
|
||||
await groupCall.setMicrophoneMuted(false);
|
||||
mockCall.localUsermediaFeed.setAudioVideoMuted.mockReset();
|
||||
mocked(mockCall.localUsermediaFeed.setAudioVideoMuted).mockReset();
|
||||
|
||||
let metadataUpdateResolve: () => void;
|
||||
const metadataUpdatePromise = new Promise<void>((resolve) => {
|
||||
@@ -688,15 +694,15 @@ describe("Group Call", function () {
|
||||
expect(client1.sendToDevice.mock.calls[0][0]).toBe("m.call.invite");
|
||||
|
||||
const toDeviceCallContent = client1.sendToDevice.mock.calls[0][1];
|
||||
expect(Object.keys(toDeviceCallContent).length).toBe(1);
|
||||
expect(Object.keys(toDeviceCallContent)[0]).toBe(FAKE_USER_ID_2);
|
||||
expect(toDeviceCallContent.size).toBe(1);
|
||||
expect(toDeviceCallContent.has(FAKE_USER_ID_2)).toBe(true);
|
||||
|
||||
const toDeviceBobDevices = toDeviceCallContent[FAKE_USER_ID_2];
|
||||
expect(Object.keys(toDeviceBobDevices).length).toBe(1);
|
||||
expect(Object.keys(toDeviceBobDevices)[0]).toBe(FAKE_DEVICE_ID_2);
|
||||
const toDeviceBobDevices = toDeviceCallContent.get(FAKE_USER_ID_2);
|
||||
expect(toDeviceBobDevices?.size).toBe(1);
|
||||
expect(toDeviceBobDevices?.has(FAKE_DEVICE_ID_2)).toBe(true);
|
||||
|
||||
const bobDeviceMessage = toDeviceBobDevices[FAKE_DEVICE_ID_2];
|
||||
expect(bobDeviceMessage.conf_id).toBe(FAKE_CONF_ID);
|
||||
const bobDeviceMessage = toDeviceBobDevices?.get(FAKE_DEVICE_ID_2);
|
||||
expect(bobDeviceMessage?.conf_id).toBe(FAKE_CONF_ID);
|
||||
} finally {
|
||||
await Promise.all([groupCall1.leave(), groupCall2.leave()]);
|
||||
}
|
||||
@@ -792,7 +798,7 @@ describe("Group Call", function () {
|
||||
call.isLocalVideoMuted = jest.fn().mockReturnValue(true);
|
||||
call.setLocalVideoMuted = jest.fn();
|
||||
|
||||
call.emit(CallEvent.State, CallState.Connected);
|
||||
call.emit(CallEvent.State, CallState.Connected, CallState.InviteSent, call);
|
||||
|
||||
expect(call.setMicrophoneMuted).toHaveBeenCalledWith(false);
|
||||
expect(call.setLocalVideoMuted).toHaveBeenCalledWith(false);
|
||||
@@ -811,7 +817,7 @@ describe("Group Call", function () {
|
||||
mockClient = typedMockClient as unknown as MatrixClient;
|
||||
|
||||
room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
|
||||
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents);
|
||||
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents());
|
||||
room.currentState.members[FAKE_USER_ID_1] = {
|
||||
userId: FAKE_USER_ID_1,
|
||||
membership: "join",
|
||||
@@ -882,11 +888,27 @@ describe("Group Call", function () {
|
||||
expect(await groupCall.setMicrophoneMuted(false)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when no permission for audio stream", async () => {
|
||||
const groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValueOnce(
|
||||
new Error("No Permission"),
|
||||
);
|
||||
expect(await groupCall.setMicrophoneMuted(false)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when unmuting video with no video device", async () => {
|
||||
const groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
jest.spyOn(mockClient.getMediaHandler(), "hasVideoDevice").mockResolvedValue(false);
|
||||
expect(await groupCall.setLocalVideoMuted(false)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when no permission for video stream", async () => {
|
||||
const groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValueOnce(
|
||||
new Error("No Permission"),
|
||||
);
|
||||
expect(await groupCall.setLocalVideoMuted(false)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("remote muting", () => {
|
||||
@@ -971,7 +993,14 @@ describe("Group Call", function () {
|
||||
mockClient = typedMockClient as unknown as MatrixClient;
|
||||
|
||||
room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_2);
|
||||
room.getMember = jest.fn().mockImplementation((userId) => ({ userId }));
|
||||
room.currentState.members[FAKE_USER_ID_1] = {
|
||||
userId: FAKE_USER_ID_1,
|
||||
membership: "join",
|
||||
} as unknown as RoomMember;
|
||||
room.currentState.members[FAKE_USER_ID_2] = {
|
||||
userId: FAKE_USER_ID_2,
|
||||
membership: "join",
|
||||
} as unknown as RoomMember;
|
||||
|
||||
groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
});
|
||||
@@ -1044,6 +1073,71 @@ describe("Group Call", function () {
|
||||
expect(call.answerWithCallFeeds).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const aliceEnters = () => {
|
||||
room.currentState.getStateEvents = jest.fn().mockImplementation(
|
||||
mockGetStateEvents([
|
||||
{
|
||||
getContent: () => ({
|
||||
"m.calls": [
|
||||
{
|
||||
"m.call_id": groupCall.groupCallId,
|
||||
"m.devices": [
|
||||
{
|
||||
device_id: FAKE_DEVICE_ID_1,
|
||||
session_id: FAKE_SESSION_ID_1,
|
||||
expires_ts: Date.now() + ONE_HOUR,
|
||||
feeds: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
getStateKey: () => FAKE_USER_ID_1,
|
||||
getRoomId: () => FAKE_ROOM_ID,
|
||||
getTs: () => 0,
|
||||
},
|
||||
] as unknown as MatrixEvent[]),
|
||||
);
|
||||
room.currentState.emit(RoomStateEvent.Update, room.currentState);
|
||||
};
|
||||
|
||||
const aliceLeaves = () => {
|
||||
room.currentState.getStateEvents = jest
|
||||
.fn()
|
||||
.mockImplementation(mockGetStateEvents([] as unknown as MatrixEvent[]));
|
||||
room.currentState.emit(RoomStateEvent.Update, room.currentState);
|
||||
};
|
||||
|
||||
it("enables tracks on expected calls, then disables them when the participant leaves", async () => {
|
||||
aliceEnters();
|
||||
|
||||
const mockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
|
||||
mockCall.answerWithCallFeeds.mockImplementation(([feed]) => (mockCall.localUsermediaFeed = feed));
|
||||
mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall);
|
||||
|
||||
// Tracks should be enabled
|
||||
expect(mockCall.localUsermediaFeed.stream.getTracks().every((t) => t.enabled)).toBe(true);
|
||||
|
||||
aliceLeaves();
|
||||
|
||||
// Tracks should be disabled
|
||||
expect(mockCall.localUsermediaFeed.stream.getTracks().every((t) => !t.enabled)).toBe(true);
|
||||
});
|
||||
|
||||
it("disables tracks on unexpected calls, then enables them when the participant joins", async () => {
|
||||
const mockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
|
||||
mockCall.answerWithCallFeeds.mockImplementation(([feed]) => (mockCall.localUsermediaFeed = feed));
|
||||
mockClient.emit(CallEventHandlerEvent.Incoming, mockCall as unknown as MatrixCall);
|
||||
|
||||
// Tracks should be disabled
|
||||
expect(mockCall.localUsermediaFeed.stream.getTracks().every((t) => !t.enabled)).toBe(true);
|
||||
|
||||
aliceEnters();
|
||||
|
||||
// Tracks should be enabled
|
||||
expect(mockCall.localUsermediaFeed.stream.getTracks().every((t) => t.enabled)).toBe(true);
|
||||
});
|
||||
|
||||
describe("handles call being replaced", () => {
|
||||
let callChangedListener: jest.Mock;
|
||||
let oldMockCall: MockMatrixCall;
|
||||
@@ -1064,7 +1158,7 @@ describe("Group Call", function () {
|
||||
});
|
||||
|
||||
it("handles regular case", () => {
|
||||
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed());
|
||||
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed(), oldMockCall.typed());
|
||||
|
||||
expect(oldMockCall.hangup).toHaveBeenCalled();
|
||||
expect(callChangedListener).toHaveBeenCalledWith(newCallsMap);
|
||||
@@ -1075,7 +1169,7 @@ describe("Group Call", function () {
|
||||
it("handles case where call is missing from the calls map", () => {
|
||||
// @ts-ignore
|
||||
groupCall.calls = new Map();
|
||||
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed());
|
||||
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed(), oldMockCall.typed());
|
||||
|
||||
expect(oldMockCall.hangup).toHaveBeenCalled();
|
||||
expect(callChangedListener).toHaveBeenCalledWith(newCallsMap);
|
||||
@@ -1141,7 +1235,7 @@ describe("Group Call", function () {
|
||||
userId: FAKE_USER_ID_2,
|
||||
membership: "join",
|
||||
} as unknown as RoomMember;
|
||||
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents);
|
||||
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents());
|
||||
|
||||
groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
});
|
||||
|
||||
@@ -98,6 +98,23 @@ describe("Group Call Event Handler", function () {
|
||||
|
||||
expect(groupCall.state).toBe(GroupCallState.Ended);
|
||||
});
|
||||
|
||||
it("terminates call when redacted", async () => {
|
||||
await groupCallEventHandler.start();
|
||||
mockClient.emitRoomState(makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID), {
|
||||
roomId: FAKE_ROOM_ID,
|
||||
} as unknown as RoomState);
|
||||
|
||||
const groupCall = groupCallEventHandler.groupCalls.get(FAKE_ROOM_ID)!;
|
||||
|
||||
expect(groupCall.state).toBe(GroupCallState.LocalCallFeedUninitialized);
|
||||
|
||||
mockClient.emitRoomState(makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID, undefined, true), {
|
||||
roomId: FAKE_ROOM_ID,
|
||||
} as unknown as RoomState);
|
||||
|
||||
expect(groupCall.state).toBe(GroupCallState.Ended);
|
||||
});
|
||||
});
|
||||
|
||||
it("waits until client starts syncing", async () => {
|
||||
@@ -222,9 +239,9 @@ describe("Group Call Event Handler", function () {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const setupCallAndStart = async (content?: IContent) => {
|
||||
const setupCallAndStart = async (content?: IContent, redacted?: boolean) => {
|
||||
mocked(mockRoom.currentState.getStateEvents).mockReturnValue([
|
||||
makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID, content),
|
||||
makeMockGroupCallStateEvent(FAKE_ROOM_ID, FAKE_GROUP_CALL_ID, content, redacted),
|
||||
] as unknown as MatrixEvent);
|
||||
mockClient.getRooms.mockReturnValue([mockRoom]);
|
||||
await groupCallEventHandler.start();
|
||||
@@ -285,5 +302,24 @@ describe("Group Call Event Handler", function () {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores redacted calls", async () => {
|
||||
await setupCallAndStart(
|
||||
{
|
||||
// Real event contents to make sure that it's specifically the
|
||||
// event being redacted that causes it to be ignored
|
||||
"m.type": GroupCallType.Video,
|
||||
"m.intent": GroupCallIntent.Prompt,
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
expect(mockClientEmit).not.toHaveBeenCalledWith(
|
||||
GroupCallEventHandlerEvent.Incoming,
|
||||
expect.objectContaining({
|
||||
groupCallId: FAKE_GROUP_CALL_ID,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -242,6 +242,11 @@ describe("Media Handler", function () {
|
||||
);
|
||||
expect(await mediaHandler.hasAudioDevice()).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns false if the system not permitting access audio inputs", async () => {
|
||||
mockMediaDevices.enumerateDevices.mockRejectedValueOnce(new Error("No Permission"));
|
||||
expect(await mediaHandler.hasAudioDevice()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasVideoDevice", () => {
|
||||
@@ -255,6 +260,11 @@ describe("Media Handler", function () {
|
||||
);
|
||||
expect(await mediaHandler.hasVideoDevice()).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns false if the system not permitting access video inputs", async () => {
|
||||
mockMediaDevices.enumerateDevices.mockRejectedValueOnce(new Error("No Permission"));
|
||||
expect(await mediaHandler.hasVideoDevice()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserMediaStream", () => {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { ConnectionStatsReporter } from "../../../../src/webrtc/stats/connectionStatsReporter";
|
||||
|
||||
describe("ConnectionStatsReporter", () => {
|
||||
describe("should on bandwidth stats", () => {
|
||||
it("build bandwidth report if chromium starts attributes available", () => {
|
||||
const stats = {
|
||||
availableIncomingBitrate: 1000,
|
||||
availableOutgoingBitrate: 2000,
|
||||
} as RTCIceCandidatePairStats;
|
||||
expect(ConnectionStatsReporter.buildBandwidthReport(stats)).toEqual({ download: 1, upload: 2 });
|
||||
});
|
||||
it("build empty bandwidth report if chromium starts attributes not available", () => {
|
||||
const stats = {} as RTCIceCandidatePairStats;
|
||||
expect(ConnectionStatsReporter.buildBandwidthReport(stats)).toEqual({ download: 0, upload: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on connection stats", () => {
|
||||
it("build bandwidth report if chromium starts attributes available", () => {
|
||||
const stats = {
|
||||
availableIncomingBitrate: 1000,
|
||||
availableOutgoingBitrate: 2000,
|
||||
} as RTCIceCandidatePairStats;
|
||||
expect(ConnectionStatsReporter.buildBandwidthReport(stats)).toEqual({ download: 1, upload: 2 });
|
||||
});
|
||||
it("build empty bandwidth report if chromium starts attributes not available", () => {
|
||||
const stats = {} as RTCIceCandidatePairStats;
|
||||
expect(ConnectionStatsReporter.buildBandwidthReport(stats)).toEqual({ download: 0, upload: 0 });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { GroupCallStats } from "../../../../src/webrtc/stats/groupCallStats";
|
||||
|
||||
const GROUP_CALL_ID = "GROUP_ID";
|
||||
const LOCAL_USER_ID = "LOCAL_USER_ID";
|
||||
const TIME_INTERVAL = 10000;
|
||||
|
||||
describe("GroupCallStats", () => {
|
||||
let stats: GroupCallStats;
|
||||
beforeEach(() => {
|
||||
stats = new GroupCallStats(GROUP_CALL_ID, LOCAL_USER_ID, TIME_INTERVAL);
|
||||
});
|
||||
|
||||
describe("should on adding a stats collector", () => {
|
||||
it("creating a new one if not existing.", async () => {
|
||||
expect(stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection())).toBeTruthy();
|
||||
});
|
||||
|
||||
it("creating only one when trying add the same collector multiple times.", async () => {
|
||||
expect(stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection())).toBeTruthy();
|
||||
expect(stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection())).toBeFalsy();
|
||||
// The User ID is not relevant! Because for stats the call is needed and the user id is for monitoring
|
||||
expect(stats.addStatsReportGatherer("CALL_ID", "SOME_OTHER_USER_ID", mockRTCPeerConnection())).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on removing a stats collector", () => {
|
||||
it("returning `true` if the collector exists", async () => {
|
||||
expect(stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection())).toBeTruthy();
|
||||
expect(stats.removeStatsReportGatherer("CALL_ID")).toBeTruthy();
|
||||
});
|
||||
it("returning false if the collector not exists", async () => {
|
||||
expect(stats.removeStatsReportGatherer("CALL_ID_NOT_EXIST")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on get stats collector", () => {
|
||||
it("returning `undefined` if collector not existing", async () => {
|
||||
expect(stats.getStatsReportGatherer("CALL_ID")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returning Collector if collector existing", async () => {
|
||||
expect(stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection())).toBeTruthy();
|
||||
expect(stats.getStatsReportGatherer("CALL_ID")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on start", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("starting processing as well without stats collectors", async () => {
|
||||
// @ts-ignore
|
||||
stats.processStats = jest.fn();
|
||||
stats.start();
|
||||
jest.advanceTimersByTime(TIME_INTERVAL);
|
||||
// @ts-ignore
|
||||
expect(stats.processStats).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("starting processing and calling the collectors", async () => {
|
||||
stats.addStatsReportGatherer("CALL_ID", "USER_ID", mockRTCPeerConnection());
|
||||
const collector = stats.getStatsReportGatherer("CALL_ID");
|
||||
let processStatsSpy;
|
||||
if (collector) {
|
||||
processStatsSpy = jest.spyOn(collector, "processStats");
|
||||
stats.start();
|
||||
jest.advanceTimersByTime(TIME_INTERVAL);
|
||||
} else {
|
||||
throw new Error("Test failed, because no Collector found!");
|
||||
}
|
||||
expect(processStatsSpy).toHaveBeenCalledWith(GROUP_CALL_ID, LOCAL_USER_ID);
|
||||
});
|
||||
|
||||
it("doing nothing if process already running", async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(global, "setInterval").mockReturnValue(22);
|
||||
stats.start();
|
||||
expect(setInterval).toHaveBeenCalledTimes(1);
|
||||
stats.start();
|
||||
stats.start();
|
||||
stats.start();
|
||||
stats.start();
|
||||
expect(setInterval).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on stop", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
it("finish stats process if was started", async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(global, "setInterval").mockReturnValue(22);
|
||||
jest.spyOn(global, "clearInterval");
|
||||
stats.start();
|
||||
expect(setInterval).toHaveBeenCalledTimes(1);
|
||||
stats.stop();
|
||||
expect(clearInterval).toHaveBeenCalledWith(22);
|
||||
});
|
||||
|
||||
it("do nothing if stats process was not started", async () => {
|
||||
jest.spyOn(global, "clearInterval");
|
||||
stats.stop();
|
||||
expect(clearInterval).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const mockRTCPeerConnection = (): RTCPeerConnection => {
|
||||
const pc = {} as RTCPeerConnection;
|
||||
pc.addEventListener = jest.fn();
|
||||
pc.getStats = jest.fn().mockResolvedValue(null);
|
||||
return pc;
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { Mid, Ssrc, MediaSsrcHandler } from "../../../../../src/webrtc/stats/media/mediaSsrcHandler";
|
||||
import { REMOTE_SFU_DESCRIPTION } from "../../../../test-utils/webrtc";
|
||||
|
||||
describe("MediaSsrcHandler", () => {
|
||||
const remoteMap = new Map<Mid, Ssrc[]>([
|
||||
["0", ["2963372119"]],
|
||||
["1", ["1212931603"]],
|
||||
]);
|
||||
let handler: MediaSsrcHandler;
|
||||
beforeEach(() => {
|
||||
handler = new MediaSsrcHandler();
|
||||
});
|
||||
describe("should parse description", () => {
|
||||
it("and build mid ssrc map", () => {
|
||||
handler.parse(REMOTE_SFU_DESCRIPTION, "remote");
|
||||
expect(handler.getSsrcToMidMap("remote")).toEqual(remoteMap);
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on find mid by ssrc", () => {
|
||||
it("and return mid if mapping exists.", () => {
|
||||
handler.parse(REMOTE_SFU_DESCRIPTION, "remote");
|
||||
expect(handler.findMidBySsrc("2963372119", "remote")).toEqual("0");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { MediaTrackHandler } from "../../../../../src/webrtc/stats/media/mediaTrackHandler";
|
||||
|
||||
describe("TrackHandler", () => {
|
||||
let pc: RTCPeerConnection;
|
||||
let handler: MediaTrackHandler;
|
||||
beforeEach(() => {
|
||||
pc = {
|
||||
getTransceivers: (): RTCRtpTransceiver[] => [mockTransceiver("1", "audio"), mockTransceiver("2", "video")],
|
||||
} as RTCPeerConnection;
|
||||
handler = new MediaTrackHandler(pc);
|
||||
});
|
||||
describe("should get local tracks", () => {
|
||||
it("returns video track", () => {
|
||||
expect(handler.getLocalTracks("video")).toEqual([
|
||||
{
|
||||
id: `sender-track-2`,
|
||||
kind: "video",
|
||||
} as MediaStreamTrack,
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns audio track", () => {
|
||||
expect(handler.getLocalTracks("audio")).toEqual([
|
||||
{
|
||||
id: `sender-track-1`,
|
||||
kind: "audio",
|
||||
} as MediaStreamTrack,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("should get local track by mid", () => {
|
||||
it("returns video track", () => {
|
||||
expect(handler.getLocalTrackIdByMid("2")).toEqual("sender-track-2");
|
||||
});
|
||||
|
||||
it("returns audio track", () => {
|
||||
expect(handler.getLocalTrackIdByMid("1")).toEqual("sender-track-1");
|
||||
});
|
||||
|
||||
it("returns undefined if not exists", () => {
|
||||
expect(handler.getLocalTrackIdByMid("3")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("should get remote track by mid", () => {
|
||||
it("returns video track", () => {
|
||||
expect(handler.getRemoteTrackIdByMid("2")).toEqual("receiver-track-2");
|
||||
});
|
||||
|
||||
it("returns audio track", () => {
|
||||
expect(handler.getRemoteTrackIdByMid("1")).toEqual("receiver-track-1");
|
||||
});
|
||||
|
||||
it("returns undefined if not exists", () => {
|
||||
expect(handler.getRemoteTrackIdByMid("3")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("should get track by id", () => {
|
||||
it("returns remote track", () => {
|
||||
expect(handler.getTackById("receiver-track-2")).toEqual({
|
||||
id: `receiver-track-2`,
|
||||
kind: "video",
|
||||
} as MediaStreamTrack);
|
||||
});
|
||||
|
||||
it("returns local track", () => {
|
||||
expect(handler.getTackById("sender-track-1")).toEqual({
|
||||
id: `sender-track-1`,
|
||||
kind: "audio",
|
||||
} as MediaStreamTrack);
|
||||
});
|
||||
|
||||
it("returns undefined if not exists", () => {
|
||||
expect(handler.getTackById("sender-track-3")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("should get simulcast track count", () => {
|
||||
it("returns 2", () => {
|
||||
expect(handler.getActiveSimulcastStreams()).toEqual(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const mockTransceiver = (mid: string, kind: "video" | "audio"): RTCRtpTransceiver => {
|
||||
return {
|
||||
mid,
|
||||
currentDirection: "sendrecv",
|
||||
sender: {
|
||||
track: { id: `sender-track-${mid}`, kind } as MediaStreamTrack,
|
||||
} as RTCRtpSender,
|
||||
receiver: {
|
||||
track: { id: `receiver-track-${mid}`, kind } as MediaStreamTrack,
|
||||
} as RTCRtpReceiver,
|
||||
} as RTCRtpTransceiver;
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { MediaTrackHandler } from "../../../../../src/webrtc/stats/media/mediaTrackHandler";
|
||||
import { MediaTrackStatsHandler } from "../../../../../src/webrtc/stats/media/mediaTrackStatsHandler";
|
||||
import { MediaSsrcHandler } from "../../../../../src/webrtc/stats/media/mediaSsrcHandler";
|
||||
|
||||
describe("MediaTrackStatsHandler", () => {
|
||||
let statsHandler: MediaTrackStatsHandler;
|
||||
let ssrcHandler: MediaSsrcHandler;
|
||||
let trackHandler: MediaTrackHandler;
|
||||
beforeEach(() => {
|
||||
ssrcHandler = {} as MediaSsrcHandler;
|
||||
trackHandler = {} as MediaTrackHandler;
|
||||
trackHandler.getLocalTrackIdByMid = jest.fn().mockReturnValue("2222");
|
||||
trackHandler.getRemoteTrackIdByMid = jest.fn().mockReturnValue("5555");
|
||||
trackHandler.getLocalTracks = jest.fn().mockReturnValue([{ id: "2222" } as MediaStreamTrack]);
|
||||
trackHandler.getTackById = jest.fn().mockReturnValue([{ id: "2222", kind: "audio" } as MediaStreamTrack]);
|
||||
statsHandler = new MediaTrackStatsHandler(ssrcHandler, trackHandler);
|
||||
});
|
||||
describe("should find track stats", () => {
|
||||
it("and returns stats if `trackIdentifier` exists in report", () => {
|
||||
const report = { trackIdentifier: "123" };
|
||||
expect(statsHandler.findTrack2Stats(report, "remote")?.trackId).toEqual("123");
|
||||
});
|
||||
it("and returns stats if `mid` exists in report", () => {
|
||||
const reportIn = { mid: "1", type: "inbound-rtp" };
|
||||
expect(statsHandler.findTrack2Stats(reportIn, "remote")?.trackId).toEqual("5555");
|
||||
const reportOut = { mid: "1", type: "outbound-rtp" };
|
||||
expect(statsHandler.findTrack2Stats(reportOut, "local")?.trackId).toEqual("2222");
|
||||
});
|
||||
it("and returns undefined if `ssrc` exists in report but not on connection", () => {
|
||||
const report = { ssrc: "142443", type: "inbound-rtp" };
|
||||
ssrcHandler.findMidBySsrc = jest.fn().mockReturnValue(undefined);
|
||||
expect(statsHandler.findTrack2Stats(report, "local")?.trackId).toBeUndefined();
|
||||
});
|
||||
it("and returns undefined if `ssrc` exists in inbound-rtp report", () => {
|
||||
const report = { ssrc: "142443", type: "inbound-rtp" };
|
||||
ssrcHandler.findMidBySsrc = jest.fn().mockReturnValue("2");
|
||||
expect(statsHandler.findTrack2Stats(report, "remote")?.trackId).toEqual("5555");
|
||||
});
|
||||
it("and returns undefined if `ssrc` exists in outbound-rtp report", () => {
|
||||
const report = { ssrc: "142443", type: "outbound-rtp" };
|
||||
ssrcHandler.findMidBySsrc = jest.fn().mockReturnValue("2");
|
||||
expect(statsHandler.findTrack2Stats(report, "local")?.trackId).toEqual("2222");
|
||||
});
|
||||
it("and returns undefined if needed property not existing", () => {
|
||||
const report = {};
|
||||
expect(statsHandler.findTrack2Stats(report, "remote")?.trackId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
describe("should find local video track stats", () => {
|
||||
it("and returns stats if `trackIdentifier` exists in report", () => {
|
||||
const report = { trackIdentifier: "2222" };
|
||||
expect(statsHandler.findLocalVideoTrackStats(report)?.trackId).toEqual("2222");
|
||||
});
|
||||
it("and returns stats if `mid` exists in report", () => {
|
||||
const report = { mid: "1" };
|
||||
expect(statsHandler.findLocalVideoTrackStats(report)?.trackId).toEqual("2222");
|
||||
});
|
||||
it("and returns undefined if `ssrc` exists", () => {
|
||||
const report = { ssrc: "142443", type: "outbound-rtp" };
|
||||
ssrcHandler.findMidBySsrc = jest.fn().mockReturnValue("2");
|
||||
expect(statsHandler.findTrack2Stats(report, "local")?.trackId).toEqual("2222");
|
||||
});
|
||||
it("and returns undefined if needed property not existing", () => {
|
||||
const report = {};
|
||||
expect(statsHandler.findTrack2Stats(report, "remote")?.trackId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { TrackID } from "../../../../src/webrtc/stats/statsReport";
|
||||
import { MediaTrackStats } from "../../../../src/webrtc/stats/media/mediaTrackStats";
|
||||
import { StatsReportBuilder } from "../../../../src/webrtc/stats/statsReportBuilder";
|
||||
|
||||
describe("StatsReportBuilder", () => {
|
||||
const LOCAL_VIDEO_TRACK_ID = "LOCAL_VIDEO_TRACK_ID";
|
||||
const LOCAL_AUDIO_TRACK_ID = "LOCAL_AUDIO_TRACK_ID";
|
||||
const REMOTE_AUDIO_TRACK_ID = "REMOTE_AUDIO_TRACK_ID";
|
||||
const REMOTE_VIDEO_TRACK_ID = "REMOTE_VIDEO_TRACK_ID";
|
||||
const localAudioTrack = new MediaTrackStats(LOCAL_AUDIO_TRACK_ID, "local", "audio");
|
||||
const localVideoTrack = new MediaTrackStats(LOCAL_VIDEO_TRACK_ID, "local", "video");
|
||||
const remoteAudioTrack = new MediaTrackStats(REMOTE_AUDIO_TRACK_ID, "remote", "audio");
|
||||
const remoteVideoTrack = new MediaTrackStats(REMOTE_VIDEO_TRACK_ID, "remote", "video");
|
||||
const stats = new Map<TrackID, MediaTrackStats>([
|
||||
[LOCAL_AUDIO_TRACK_ID, localAudioTrack],
|
||||
[LOCAL_VIDEO_TRACK_ID, localVideoTrack],
|
||||
[REMOTE_AUDIO_TRACK_ID, remoteAudioTrack],
|
||||
[REMOTE_VIDEO_TRACK_ID, remoteVideoTrack],
|
||||
]);
|
||||
beforeEach(() => {
|
||||
buildData();
|
||||
});
|
||||
|
||||
describe("should build stats", () => {
|
||||
it("by media track stats.", async () => {
|
||||
expect(StatsReportBuilder.build(stats)).toEqual({
|
||||
bitrate: {
|
||||
audio: {
|
||||
download: 4000,
|
||||
upload: 5000,
|
||||
},
|
||||
download: 5004000,
|
||||
upload: 3005000,
|
||||
video: {
|
||||
download: 5000000,
|
||||
upload: 3000000,
|
||||
},
|
||||
},
|
||||
codec: {
|
||||
local: new Map([
|
||||
["LOCAL_AUDIO_TRACK_ID", "opus"],
|
||||
["LOCAL_VIDEO_TRACK_ID", "v8"],
|
||||
]),
|
||||
remote: new Map([
|
||||
["REMOTE_AUDIO_TRACK_ID", "opus"],
|
||||
["REMOTE_VIDEO_TRACK_ID", "v9"],
|
||||
]),
|
||||
},
|
||||
framerate: {
|
||||
local: new Map([
|
||||
["LOCAL_AUDIO_TRACK_ID", 0],
|
||||
["LOCAL_VIDEO_TRACK_ID", 30],
|
||||
]),
|
||||
remote: new Map([
|
||||
["REMOTE_AUDIO_TRACK_ID", 0],
|
||||
["REMOTE_VIDEO_TRACK_ID", 60],
|
||||
]),
|
||||
},
|
||||
packetLoss: {
|
||||
download: 7,
|
||||
total: 15,
|
||||
upload: 28,
|
||||
},
|
||||
resolution: {
|
||||
local: new Map([
|
||||
["LOCAL_AUDIO_TRACK_ID", { height: -1, width: -1 }],
|
||||
["LOCAL_VIDEO_TRACK_ID", { height: 460, width: 780 }],
|
||||
]),
|
||||
remote: new Map([
|
||||
["REMOTE_AUDIO_TRACK_ID", { height: -1, width: -1 }],
|
||||
["REMOTE_VIDEO_TRACK_ID", { height: 960, width: 1080 }],
|
||||
]),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const buildData = (): void => {
|
||||
localAudioTrack.setCodec("opus");
|
||||
localAudioTrack.setLoss({ packetsTotal: 10, packetsLost: 5, isDownloadStream: false });
|
||||
localAudioTrack.setBitrate({ download: 0, upload: 5000 });
|
||||
|
||||
remoteAudioTrack.setCodec("opus");
|
||||
remoteAudioTrack.setLoss({ packetsTotal: 20, packetsLost: 0, isDownloadStream: true });
|
||||
remoteAudioTrack.setBitrate({ download: 4000, upload: 0 });
|
||||
|
||||
localVideoTrack.setCodec("v8");
|
||||
localVideoTrack.setLoss({ packetsTotal: 30, packetsLost: 6, isDownloadStream: false });
|
||||
localVideoTrack.setBitrate({ download: 0, upload: 3000000 });
|
||||
localVideoTrack.setFramerate(30);
|
||||
localVideoTrack.setResolution({ width: 780, height: 460 });
|
||||
|
||||
remoteVideoTrack.setCodec("v9");
|
||||
remoteVideoTrack.setLoss({ packetsTotal: 40, packetsLost: 4, isDownloadStream: true });
|
||||
remoteVideoTrack.setBitrate({ download: 5000000, upload: 0 });
|
||||
remoteVideoTrack.setFramerate(60);
|
||||
remoteVideoTrack.setResolution({ width: 1080, height: 960 });
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { StatsReportEmitter } from "../../../../src/webrtc/stats/statsReportEmitter";
|
||||
import { ByteSentStatsReport, ConnectionStatsReport, StatsReport } from "../../../../src/webrtc/stats/statsReport";
|
||||
|
||||
describe("StatsReportEmitter", () => {
|
||||
let emitter: StatsReportEmitter;
|
||||
beforeEach(() => {
|
||||
emitter = new StatsReportEmitter();
|
||||
});
|
||||
|
||||
it("should emit and receive ByteSendStatsReport", async () => {
|
||||
const report = {} as ByteSentStatsReport;
|
||||
return new Promise((resolve, _) => {
|
||||
emitter.on(StatsReport.BYTE_SENT_STATS, (r) => {
|
||||
expect(r).toBe(report);
|
||||
resolve(null);
|
||||
return;
|
||||
});
|
||||
emitter.emitByteSendReport(report);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit and receive ConnectionStatsReport", async () => {
|
||||
const report = {} as ConnectionStatsReport;
|
||||
return new Promise((resolve, _) => {
|
||||
emitter.on(StatsReport.CONNECTION_STATS, (r) => {
|
||||
expect(r).toBe(report);
|
||||
resolve(null);
|
||||
return;
|
||||
});
|
||||
emitter.emitConnectionStatsReport(report);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { StatsReportGatherer } from "../../../../src/webrtc/stats/statsReportGatherer";
|
||||
import { StatsReportEmitter } from "../../../../src/webrtc/stats/statsReportEmitter";
|
||||
|
||||
const CALL_ID = "CALL_ID";
|
||||
const USER_ID = "USER_ID";
|
||||
|
||||
describe("StatsReportGatherer", () => {
|
||||
let collector: StatsReportGatherer;
|
||||
let rtcSpy: RTCPeerConnection;
|
||||
let emitter: StatsReportEmitter;
|
||||
beforeEach(() => {
|
||||
rtcSpy = { getStats: () => new Promise<RTCStatsReport>(() => null) } as RTCPeerConnection;
|
||||
rtcSpy.addEventListener = jest.fn();
|
||||
emitter = new StatsReportEmitter();
|
||||
collector = new StatsReportGatherer(CALL_ID, USER_ID, rtcSpy, emitter);
|
||||
});
|
||||
|
||||
describe("on process stats", () => {
|
||||
it("if active calculate stats reports", async () => {
|
||||
const getStats = jest.spyOn(rtcSpy, "getStats");
|
||||
getStats.mockResolvedValue({} as RTCStatsReport);
|
||||
await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
|
||||
expect(getStats).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("if not active do not calculate stats reports", async () => {
|
||||
collector.setActive(false);
|
||||
const getStats = jest.spyOn(rtcSpy, "getStats");
|
||||
await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
|
||||
expect(getStats).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("if get reports fails, the collector becomes inactive", async () => {
|
||||
expect(collector.getActive()).toBeTruthy();
|
||||
const getStats = jest.spyOn(rtcSpy, "getStats");
|
||||
getStats.mockRejectedValue(new Error("unknown"));
|
||||
await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
|
||||
expect(getStats).toHaveBeenCalled();
|
||||
expect(collector.getActive()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("if active an RTCStatsReport not a promise the collector becomes inactive", async () => {
|
||||
const getStats = jest.spyOn(rtcSpy, "getStats");
|
||||
// @ts-ignore
|
||||
getStats.mockReturnValue({});
|
||||
const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
|
||||
expect(actual).toBeFalsy();
|
||||
expect(getStats).toHaveBeenCalled();
|
||||
expect(collector.getActive()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { StatsValueFormatter } from "../../../../src/webrtc/stats/statsValueFormatter";
|
||||
|
||||
describe("StatsValueFormatter", () => {
|
||||
describe("on get non negative values", () => {
|
||||
it("formatter shod return number", async () => {
|
||||
expect(StatsValueFormatter.getNonNegativeValue("2")).toEqual(2);
|
||||
expect(StatsValueFormatter.getNonNegativeValue(0)).toEqual(0);
|
||||
expect(StatsValueFormatter.getNonNegativeValue("-2")).toEqual(0);
|
||||
expect(StatsValueFormatter.getNonNegativeValue("")).toEqual(0);
|
||||
expect(StatsValueFormatter.getNonNegativeValue(NaN)).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { TrackStatsReporter } from "../../../../src/webrtc/stats/trackStatsReporter";
|
||||
import { MediaTrackStats } from "../../../../src/webrtc/stats/media/mediaTrackStats";
|
||||
|
||||
describe("TrackStatsReporter", () => {
|
||||
describe("should on frame and resolution stats", () => {
|
||||
it("creating empty frame and resolution report, if no data available.", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "local", "video");
|
||||
TrackStatsReporter.buildFramerateResolution(trackStats, {});
|
||||
expect(trackStats.getFramerate()).toEqual(0);
|
||||
expect(trackStats.getResolution()).toEqual({ width: -1, height: -1 });
|
||||
});
|
||||
it("creating empty frame and resolution report.", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "remote", "video");
|
||||
TrackStatsReporter.buildFramerateResolution(trackStats, {
|
||||
framesPerSecond: 22.2,
|
||||
frameHeight: 180,
|
||||
frameWidth: 360,
|
||||
});
|
||||
expect(trackStats.getFramerate()).toEqual(22);
|
||||
expect(trackStats.getResolution()).toEqual({ width: 360, height: 180 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on simulcast", () => {
|
||||
it("creating simulcast framerate.", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "local", "video");
|
||||
TrackStatsReporter.calculateSimulcastFramerate(
|
||||
trackStats,
|
||||
{
|
||||
framesSent: 100,
|
||||
timestamp: 1678957001000,
|
||||
},
|
||||
{
|
||||
framesSent: 10,
|
||||
timestamp: 1678957000000,
|
||||
},
|
||||
3,
|
||||
);
|
||||
expect(trackStats.getFramerate()).toEqual(30);
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on bytes received stats", () => {
|
||||
it("creating build bitrate received report.", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "remote", "video");
|
||||
TrackStatsReporter.buildBitrateReceived(
|
||||
trackStats,
|
||||
{
|
||||
bytesReceived: 2001000,
|
||||
timestamp: 1678957010,
|
||||
},
|
||||
{ bytesReceived: 2000000, timestamp: 1678957000 },
|
||||
);
|
||||
expect(trackStats.getBitrate()).toEqual({ download: 800, upload: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on bytes send stats", () => {
|
||||
it("creating build bitrate send report.", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "local", "video");
|
||||
TrackStatsReporter.buildBitrateSend(
|
||||
trackStats,
|
||||
{
|
||||
bytesSent: 2001000,
|
||||
timestamp: 1678957010,
|
||||
},
|
||||
{ bytesSent: 2000000, timestamp: 1678957000 },
|
||||
);
|
||||
expect(trackStats.getBitrate()).toEqual({ download: 0, upload: 800 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on codec stats", () => {
|
||||
it("creating build bitrate send report.", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "remote", "video");
|
||||
const remote = {} as RTCStatsReport;
|
||||
remote.get = jest.fn().mockReturnValue({ mimeType: "video/v8" });
|
||||
TrackStatsReporter.buildCodec(remote, trackStats, { codecId: "codecID" });
|
||||
expect(trackStats.getCodec()).toEqual("v8");
|
||||
});
|
||||
});
|
||||
|
||||
describe("should on package lost stats", () => {
|
||||
it("creating build package lost on send report.", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "local", "video");
|
||||
TrackStatsReporter.buildPacketsLost(
|
||||
trackStats,
|
||||
{
|
||||
type: "outbound-rtp",
|
||||
packetsSent: 200,
|
||||
packetsLost: 120,
|
||||
},
|
||||
{
|
||||
packetsSent: 100,
|
||||
packetsLost: 30,
|
||||
},
|
||||
);
|
||||
expect(trackStats.getLoss()).toEqual({ packetsTotal: 190, packetsLost: 90, isDownloadStream: false });
|
||||
});
|
||||
it("creating build package lost on received report.", async () => {
|
||||
const trackStats = new MediaTrackStats("1", "remote", "video");
|
||||
TrackStatsReporter.buildPacketsLost(
|
||||
trackStats,
|
||||
{
|
||||
type: "inbound-rtp",
|
||||
packetsReceived: 300,
|
||||
packetsLost: 100,
|
||||
},
|
||||
{
|
||||
packetsReceived: 100,
|
||||
packetsLost: 20,
|
||||
},
|
||||
);
|
||||
expect(trackStats.getLoss()).toEqual({ packetsTotal: 280, packetsLost: 80, isDownloadStream: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { TransportStatsReporter } from "../../../../src/webrtc/stats/transportStatsReporter";
|
||||
import { TransportStats } from "../../../../src/webrtc/stats/transportStats";
|
||||
|
||||
describe("TransportStatsReporter", () => {
|
||||
describe("should on build report", () => {
|
||||
const REMOTE_CANDIDATE_ID = "REMOTE_CANDIDATE_ID";
|
||||
const LOCAL_CANDIDATE_ID = "LOCAL_CANDIDATE_ID";
|
||||
const localIC = { ip: "88.88.99.1", port: 56670, protocol: "tcp", candidateType: "local", networkType: "lan" };
|
||||
const remoteIC = {
|
||||
ip: "123.88.99.1",
|
||||
port: 46670,
|
||||
protocol: "udp",
|
||||
candidateType: "srfx",
|
||||
networkType: "wifi",
|
||||
};
|
||||
const isFocus = false;
|
||||
const rtt = 200000;
|
||||
|
||||
it("build new transport stats if all properties there", () => {
|
||||
const { report, stats } = mockStatsReport(isFocus, 0);
|
||||
const conferenceStatsTransport: TransportStats[] = [];
|
||||
const transportStats = TransportStatsReporter.buildReport(report, stats, conferenceStatsTransport, isFocus);
|
||||
expect(transportStats).toEqual([
|
||||
{
|
||||
ip: `${remoteIC.ip + 0}:${remoteIC.port}`,
|
||||
type: remoteIC.protocol,
|
||||
localIp: `${localIC.ip + 0}:${localIC.port}`,
|
||||
isFocus,
|
||||
localCandidateType: localIC.candidateType,
|
||||
remoteCandidateType: remoteIC.candidateType,
|
||||
networkType: localIC.networkType,
|
||||
rtt,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("build next transport stats if candidates different", () => {
|
||||
const mock1 = mockStatsReport(isFocus, 0);
|
||||
const mock2 = mockStatsReport(isFocus, 1);
|
||||
let transportStats: TransportStats[] = [];
|
||||
transportStats = TransportStatsReporter.buildReport(mock1.report, mock1.stats, transportStats, isFocus);
|
||||
transportStats = TransportStatsReporter.buildReport(mock2.report, mock2.stats, transportStats, isFocus);
|
||||
expect(transportStats).toEqual([
|
||||
{
|
||||
ip: `${remoteIC.ip + 0}:${remoteIC.port}`,
|
||||
type: remoteIC.protocol,
|
||||
localIp: `${localIC.ip + 0}:${localIC.port}`,
|
||||
isFocus,
|
||||
localCandidateType: localIC.candidateType,
|
||||
remoteCandidateType: remoteIC.candidateType,
|
||||
networkType: localIC.networkType,
|
||||
rtt,
|
||||
},
|
||||
{
|
||||
ip: `${remoteIC.ip + 1}:${remoteIC.port}`,
|
||||
type: remoteIC.protocol,
|
||||
localIp: `${localIC.ip + 1}:${localIC.port}`,
|
||||
isFocus,
|
||||
localCandidateType: localIC.candidateType,
|
||||
remoteCandidateType: remoteIC.candidateType,
|
||||
networkType: localIC.networkType,
|
||||
rtt,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("build not a second transport stats if candidates the same", () => {
|
||||
const mock1 = mockStatsReport(isFocus, 0);
|
||||
const mock2 = mockStatsReport(isFocus, 0);
|
||||
let transportStats: TransportStats[] = [];
|
||||
transportStats = TransportStatsReporter.buildReport(mock1.report, mock1.stats, transportStats, isFocus);
|
||||
transportStats = TransportStatsReporter.buildReport(mock2.report, mock2.stats, transportStats, isFocus);
|
||||
expect(transportStats).toEqual([
|
||||
{
|
||||
ip: `${remoteIC.ip + 0}:${remoteIC.port}`,
|
||||
type: remoteIC.protocol,
|
||||
localIp: `${localIC.ip + 0}:${localIC.port}`,
|
||||
isFocus,
|
||||
localCandidateType: localIC.candidateType,
|
||||
remoteCandidateType: remoteIC.candidateType,
|
||||
networkType: localIC.networkType,
|
||||
rtt,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const mockStatsReport = (
|
||||
isFocus: boolean,
|
||||
prifix: number,
|
||||
): { report: RTCStatsReport; stats: RTCIceCandidatePairStats } => {
|
||||
const report = {} as RTCStatsReport;
|
||||
report.get = (key: string) => {
|
||||
if (key === LOCAL_CANDIDATE_ID) {
|
||||
return { ...localIC, ip: localIC.ip + prifix };
|
||||
}
|
||||
if (key === REMOTE_CANDIDATE_ID) {
|
||||
return { ...remoteIC, ip: remoteIC.ip + prifix };
|
||||
}
|
||||
// remote
|
||||
return {};
|
||||
};
|
||||
const stats = {
|
||||
remoteCandidateId: REMOTE_CANDIDATE_ID,
|
||||
localCandidateId: LOCAL_CANDIDATE_ID,
|
||||
currentRoundTripTime: 200,
|
||||
} as RTCIceCandidatePairStats;
|
||||
return { report, stats };
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -20,5 +20,5 @@ export interface IIdentityServerProvider {
|
||||
* for the associated client.
|
||||
* @returns Promise which resolves to the access token.
|
||||
*/
|
||||
getAccessToken(): Promise<string>;
|
||||
getAccessToken(): Promise<string | null>;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ export function isDmMemberCountCondition(condition: AnyMemberCountCondition): bo
|
||||
|
||||
export enum ConditionKind {
|
||||
EventMatch = "event_match",
|
||||
EventPropertyIs = "event_property_is",
|
||||
EventPropertyContains = "event_property_contains",
|
||||
ContainsDisplayName = "contains_display_name",
|
||||
RoomMemberCount = "room_member_count",
|
||||
SenderNotificationPermission = "sender_notification_permission",
|
||||
@@ -77,9 +79,21 @@ export interface IPushRuleCondition<N extends ConditionKind | string> {
|
||||
export interface IEventMatchCondition extends IPushRuleCondition<ConditionKind.EventMatch> {
|
||||
key: string;
|
||||
pattern?: string;
|
||||
// Note that value property is an optimization for patterns which do not do
|
||||
// any globbing and when the key is not "content.body".
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export interface IEventPropertyIsCondition extends IPushRuleCondition<ConditionKind.EventPropertyIs> {
|
||||
key: string;
|
||||
value: string | boolean | null | number;
|
||||
}
|
||||
|
||||
export interface IEventPropertyContainsCondition extends IPushRuleCondition<ConditionKind.EventPropertyContains> {
|
||||
key: string;
|
||||
value: string | boolean | null | number;
|
||||
}
|
||||
|
||||
export interface IContainsDisplayNameCondition extends IPushRuleCondition<ConditionKind.ContainsDisplayName> {
|
||||
// no additional fields
|
||||
}
|
||||
@@ -105,6 +119,8 @@ export interface ICallStartedPrefixCondition extends IPushRuleCondition<Conditio
|
||||
// IPushRuleCondition<Exclude<string, ConditionKind>> unfortunately does not resolve this at the time of writing.
|
||||
export type PushRuleCondition =
|
||||
| IEventMatchCondition
|
||||
| IEventPropertyIsCondition
|
||||
| IEventPropertyContainsCondition
|
||||
| IContainsDisplayNameCondition
|
||||
| IRoomMemberCountCondition
|
||||
| ISenderNotificationPermissionCondition
|
||||
@@ -121,6 +137,8 @@ export enum PushRuleKind {
|
||||
|
||||
export enum RuleId {
|
||||
Master = ".m.rule.master",
|
||||
IsUserMention = ".org.matrix.msc3952.is_user_mention",
|
||||
IsRoomMention = ".org.matrix.msc3952.is_room_mention",
|
||||
ContainsDisplayName = ".m.rule.contains_display_name",
|
||||
ContainsUserName = ".m.rule.contains_user_name",
|
||||
AtRoomNotification = ".m.rule.roomnotif",
|
||||
@@ -133,6 +151,14 @@ export enum RuleId {
|
||||
IncomingCall = ".m.rule.call",
|
||||
SuppressNotices = ".m.rule.suppress_notices",
|
||||
Tombstone = ".m.rule.tombstone",
|
||||
PollStart = ".m.rule.poll_start",
|
||||
PollStartUnstable = ".org.matrix.msc3930.rule.poll_start",
|
||||
PollEnd = ".m.rule.poll_end",
|
||||
PollEndUnstable = ".org.matrix.msc3930.rule.poll_end",
|
||||
PollStartOneToOne = ".m.rule.poll_start_one_to_one",
|
||||
PollStartOneToOneUnstable = ".org.matrix.msc3930.rule.poll_start_one_to_one",
|
||||
PollEndOneToOne = ".m.rule.poll_end_one_to_one",
|
||||
PollEndOneToOneUnstable = ".org.matrix.msc3930.rule.poll_end_one_to_one",
|
||||
}
|
||||
|
||||
export type PushRuleSet = {
|
||||
|
||||
@@ -38,7 +38,7 @@ export interface CachedReceipt {
|
||||
data: Receipt;
|
||||
}
|
||||
|
||||
export type ReceiptCache = { [eventId: string]: CachedReceipt[] };
|
||||
export type ReceiptCache = Map<string, CachedReceipt[]>;
|
||||
|
||||
export interface ReceiptContent {
|
||||
[eventId: string]: {
|
||||
@@ -49,11 +49,8 @@ export interface ReceiptContent {
|
||||
}
|
||||
|
||||
// We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer.
|
||||
export type Receipts = {
|
||||
[receiptType: string]: {
|
||||
[userId: string]: [WrappedReceipt | null, WrappedReceipt | null]; // Pair<real receipt, synthetic receipt> (both nullable)
|
||||
};
|
||||
};
|
||||
// map: receipt type → user Id → receipt
|
||||
export type Receipts = Map<string, Map<string, [real: WrappedReceipt | null, synthetic: WrappedReceipt | null]>>;
|
||||
|
||||
export type CachedReceiptStructure = {
|
||||
eventId: string;
|
||||
|
||||
@@ -21,6 +21,7 @@ import { MatrixError } from "./http-api";
|
||||
import { IndexedToDeviceBatch, ToDeviceBatch, ToDeviceBatchWithTxnId, ToDevicePayload } from "./models/ToDeviceMessage";
|
||||
import { MatrixScheduler } from "./scheduler";
|
||||
import { SyncState } from "./sync";
|
||||
import { MapWithDefault } from "./utils";
|
||||
|
||||
const MAX_BATCH_SIZE = 20;
|
||||
|
||||
@@ -122,12 +123,9 @@ export class ToDeviceMessageQueue {
|
||||
* Attempts to send a batch of to-device messages.
|
||||
*/
|
||||
private async sendBatch(batch: IndexedToDeviceBatch): Promise<void> {
|
||||
const contentMap: Record<string, Record<string, ToDevicePayload>> = {};
|
||||
const contentMap: MapWithDefault<string, Map<string, ToDevicePayload>> = new MapWithDefault(() => new Map());
|
||||
for (const item of batch.batch) {
|
||||
if (!contentMap[item.userId]) {
|
||||
contentMap[item.userId] = {};
|
||||
}
|
||||
contentMap[item.userId][item.deviceId] = item.payload;
|
||||
contentMap.getOrCreate(item.userId).set(item.deviceId, item.payload);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
|
||||
+156
-56
@@ -37,7 +37,7 @@ import { Filter, IFilterDefinition, IRoomEventFilter } from "./filter";
|
||||
import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from "./webrtc/callEventHandler";
|
||||
import { GroupCallEventHandlerEvent, GroupCallEventHandlerEventHandlerMap } from "./webrtc/groupCallEventHandler";
|
||||
import * as utils from "./utils";
|
||||
import { replaceParam, QueryDict, sleep } from "./utils";
|
||||
import { replaceParam, QueryDict, sleep, noUnsafeEventProps } from "./utils";
|
||||
import { Direction, EventTimeline } from "./models/event-timeline";
|
||||
import { IActionsObject, PushProcessor } from "./pushprocessor";
|
||||
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
|
||||
@@ -79,7 +79,7 @@ import {
|
||||
VerificationMethod,
|
||||
IRoomKeyRequestBody,
|
||||
} from "./crypto";
|
||||
import { DeviceInfo, IDevice } from "./crypto/deviceinfo";
|
||||
import { DeviceInfo } from "./crypto/deviceinfo";
|
||||
import { decodeRecoveryKey } from "./crypto/recoverykey";
|
||||
import { keyFromAuthData } from "./crypto/key_passphrase";
|
||||
import { User, UserEvent, UserEventHandlerMap } from "./models/user";
|
||||
@@ -106,7 +106,6 @@ import {
|
||||
IEncryptedEventInfo,
|
||||
IImportRoomKeysOpts,
|
||||
IRecoveryKey,
|
||||
ISecretStorageKeyInfo,
|
||||
} from "./crypto/api";
|
||||
import { EventTimelineSet } from "./models/event-timeline-set";
|
||||
import { VerificationRequest } from "./crypto/verification/request/VerificationRequest";
|
||||
@@ -206,6 +205,9 @@ import { LocalNotificationSettings } from "./@types/local_notifications";
|
||||
import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature";
|
||||
import { CryptoBackend } from "./common-crypto/CryptoBackend";
|
||||
import { RUST_SDK_STORE_PREFIX } from "./rust-crypto/constants";
|
||||
import { CryptoApi } from "./crypto-api";
|
||||
import { DeviceInfoMap } from "./crypto/DeviceList";
|
||||
import { SecretStorageKeyDescription } from "./secret-storage";
|
||||
|
||||
export type Store = IStore;
|
||||
|
||||
@@ -371,6 +373,13 @@ export interface ICreateClientOpts {
|
||||
* Defaults to a built-in English handler with basic pluralisation.
|
||||
*/
|
||||
roomNameGenerator?: (roomId: string, state: RoomNameState) => string | null;
|
||||
|
||||
/**
|
||||
* If true, participant can join group call without video and audio this has to be allowed. By default, a local
|
||||
* media stream is needed to establish a group call.
|
||||
* Default: false.
|
||||
*/
|
||||
isVoipWithNoMediaAllowed?: boolean;
|
||||
}
|
||||
|
||||
export interface IMatrixClientCreateOpts extends ICreateClientOpts {
|
||||
@@ -453,6 +462,11 @@ export interface IStartClientOpts {
|
||||
* @experimental
|
||||
*/
|
||||
slidingSync?: SlidingSync;
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
intentionalMentions?: boolean;
|
||||
}
|
||||
|
||||
export interface IStoredClientOpts extends IStartClientOpts {}
|
||||
@@ -498,6 +512,8 @@ enum CrossSigningKeyType {
|
||||
|
||||
export type CrossSigningKeys = Record<CrossSigningKeyType, ICrossSigningKey>;
|
||||
|
||||
export type SendToDeviceContentMap = Map<string, Map<string, Record<string, any>>>;
|
||||
|
||||
export interface ISignedKey {
|
||||
keys: Record<string, string>;
|
||||
signatures: ISignatures;
|
||||
@@ -829,7 +845,7 @@ interface IRoomHierarchy {
|
||||
next_batch?: string;
|
||||
}
|
||||
|
||||
interface ITimestampToEventResponse {
|
||||
export interface TimestampToEventResponse {
|
||||
event_id: string;
|
||||
origin_server_ts: string;
|
||||
}
|
||||
@@ -1159,7 +1175,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
public urlPreviewCache: { [key: string]: Promise<IPreviewUrlResponse> } = {};
|
||||
public identityServer?: IIdentityServerProvider;
|
||||
public http: MatrixHttpApi<IHttpOpts & { onlyData: true }>; // XXX: Intended private, used in code.
|
||||
public crypto?: Crypto; // libolm crypto implementation. XXX: Intended private, used in code. Being replaced by cryptoBackend
|
||||
|
||||
/**
|
||||
* The libolm crypto implementation, if it is in use.
|
||||
*
|
||||
* @deprecated This should not be used. Instead, use the methods exposed directly on this class or
|
||||
* (where they are available) via {@link getCrypto}.
|
||||
*/
|
||||
public crypto?: Crypto; // XXX: Intended private, used in code. Being replaced by cryptoBackend
|
||||
|
||||
private cryptoBackend?: CryptoBackend; // one of crypto or rustCrypto
|
||||
public cryptoCallbacks: ICryptoCallbacks; // XXX: Intended private, used in code.
|
||||
public callEventHandler?: CallEventHandler; // XXX: Intended private, used in code.
|
||||
@@ -1169,6 +1193,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
public iceCandidatePoolSize = 0; // XXX: Intended private, used in code.
|
||||
public idBaseUrl?: string;
|
||||
public baseUrl: string;
|
||||
public readonly isVoipWithNoMediaAllowed;
|
||||
|
||||
// Note: these are all `protected` to let downstream consumers make mistakes if they want to.
|
||||
// We don't technically support this usage, but have reasons to do this.
|
||||
@@ -1313,6 +1338,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.iceCandidatePoolSize = opts.iceCandidatePoolSize === undefined ? 0 : opts.iceCandidatePoolSize;
|
||||
this.supportsCallTransfer = opts.supportsCallTransfer || false;
|
||||
this.fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false;
|
||||
this.isVoipWithNoMediaAllowed = opts.isVoipWithNoMediaAllowed || false;
|
||||
|
||||
if (opts.useE2eForGroupCall !== undefined) this.useE2eForGroupCall = opts.useE2eForGroupCall;
|
||||
|
||||
@@ -1880,6 +1906,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
throw new Error(`Cannot find room ${roomId}`);
|
||||
}
|
||||
|
||||
// Because without Media section a WebRTC connection is not possible, so need a RTCDataChannel to set up a
|
||||
// no media WebRTC connection anyway.
|
||||
return new GroupCall(
|
||||
this,
|
||||
room,
|
||||
@@ -1887,8 +1915,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
isPtt,
|
||||
intent,
|
||||
undefined,
|
||||
dataChannelsEnabled,
|
||||
dataChannelsEnabled || this.isVoipWithNoMediaAllowed,
|
||||
dataChannelOptions,
|
||||
this.isVoipWithNoMediaAllowed,
|
||||
).create();
|
||||
}
|
||||
|
||||
@@ -2188,9 +2217,20 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.on(RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto));
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the crypto API for this client.
|
||||
*
|
||||
* If end-to-end encryption has been enabled for this client (via {@link initCrypto} or {@link initRustCrypto}),
|
||||
* returns an object giving access to the crypto API. Otherwise, returns `undefined`.
|
||||
*/
|
||||
public getCrypto(): CryptoApi | undefined {
|
||||
return this.cryptoBackend;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is end-to-end crypto enabled for this client.
|
||||
* @returns True if end-to-end is enabled.
|
||||
* @deprecated prefer {@link getCrypto}
|
||||
*/
|
||||
public isCryptoEnabled(): boolean {
|
||||
return !!this.cryptoBackend;
|
||||
@@ -2231,7 +2271,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @returns A promise which resolves to a map userId-\>deviceId-\>{@link DeviceInfo}
|
||||
*/
|
||||
public downloadKeys(userIds: string[], forceDownload?: boolean): Promise<Record<string, Record<string, IDevice>>> {
|
||||
public downloadKeys(userIds: string[], forceDownload?: boolean): Promise<DeviceInfoMap> {
|
||||
if (!this.crypto) {
|
||||
return Promise.reject(new Error("End-to-end encryption disabled"));
|
||||
}
|
||||
@@ -2423,7 +2463,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return this.crypto.beginKeyVerification(method, userId, deviceId);
|
||||
}
|
||||
|
||||
public checkSecretStorageKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise<boolean> {
|
||||
public checkSecretStorageKey(key: Uint8Array, info: SecretStorageKeyDescription): Promise<boolean> {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
@@ -2436,6 +2476,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* do not specify a value.
|
||||
*
|
||||
* @param value - whether to blacklist all unverified devices by default
|
||||
*
|
||||
* @deprecated Prefer direct access to {@link CryptoApi.globalBlacklistUnverifiedDevices}:
|
||||
*
|
||||
* ```javascript
|
||||
* client.getCrypto().globalBlacklistUnverifiedDevices = value;
|
||||
* ```
|
||||
*/
|
||||
public setGlobalBlacklistUnverifiedDevices(value: boolean): boolean {
|
||||
if (!this.cryptoBackend) {
|
||||
@@ -2447,6 +2493,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
/**
|
||||
* @returns whether to blacklist all unverified devices by default
|
||||
*
|
||||
* @deprecated Prefer direct access to {@link CryptoApi.globalBlacklistUnverifiedDevices}:
|
||||
*
|
||||
* ```javascript
|
||||
* value = client.getCrypto().globalBlacklistUnverifiedDevices;
|
||||
* ```
|
||||
*/
|
||||
public getGlobalBlacklistUnverifiedDevices(): boolean {
|
||||
if (!this.cryptoBackend) {
|
||||
@@ -2464,6 +2516,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* This API is currently UNSTABLE and may change or be removed without notice.
|
||||
*
|
||||
* @param value - whether error on unknown devices
|
||||
*
|
||||
* @deprecated Prefer direct access to {@link CryptoApi.globalBlacklistUnverifiedDevices}:
|
||||
*
|
||||
* ```ts
|
||||
* client.getCrypto().globalBlacklistUnverifiedDevices = value;
|
||||
* ```
|
||||
*/
|
||||
public setGlobalErrorOnUnknownDevices(value: boolean): void {
|
||||
if (!this.cryptoBackend) {
|
||||
@@ -2523,8 +2581,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* The cross-signing API is currently UNSTABLE and may change without notice.
|
||||
*
|
||||
* @param userId - The ID of the user to check.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
public checkUserTrust(userId: string): UserTrustLevel {
|
||||
if (!this.cryptoBackend) {
|
||||
@@ -2606,6 +2662,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* Perform any background tasks that can be done before a message is ready to
|
||||
* send, in order to speed up sending of the message.
|
||||
* @param room - the room the event is in
|
||||
*
|
||||
* @deprecated Prefer {@link CryptoApi.prepareToEncrypt | `CryptoApi.prepareToEncrypt`}:
|
||||
*
|
||||
* ```javascript
|
||||
* client.getCrypto().prepareToEncrypt(room);
|
||||
* ```
|
||||
*/
|
||||
public prepareToEncrypt(room: Room): void {
|
||||
if (!this.cryptoBackend) {
|
||||
@@ -2619,6 +2681,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* This means downloading the devicelist for the user and checking if the list includes
|
||||
* the cross-signing pseudo-device.
|
||||
*
|
||||
* @deprecated Prefer {@link CryptoApi.userHasCrossSigningKeys | `CryptoApi.userHasCrossSigningKeys`}:
|
||||
*
|
||||
* ```javascript
|
||||
* result = client.getCrypto().userHasCrossSigningKeys();
|
||||
* ```
|
||||
*/
|
||||
public userHasCrossSigningKeys(): Promise<boolean> {
|
||||
if (!this.cryptoBackend) {
|
||||
@@ -2795,7 +2863,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
algorithm: string,
|
||||
opts: IAddSecretStorageKeyOpts,
|
||||
keyName?: string,
|
||||
): Promise<{ keyId: string; keyInfo: ISecretStorageKeyInfo }> {
|
||||
): Promise<{ keyId: string; keyInfo: SecretStorageKeyDescription }> {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
@@ -2861,7 +2929,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* with, or null if it is not present or not encrypted with a trusted
|
||||
* key
|
||||
*/
|
||||
public isSecretStored(name: string): Promise<Record<string, ISecretStorageKeyInfo> | null> {
|
||||
public isSecretStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
|
||||
if (!this.crypto) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
@@ -3062,13 +3130,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* @param roomId - The ID of the room to discard the session for
|
||||
*
|
||||
* This should not normally be necessary.
|
||||
* @deprecated Prefer {@link CryptoApi.forceDiscardSession | `CryptoApi.forceDiscardSession`}:
|
||||
*
|
||||
*/
|
||||
public forceDiscardSession(roomId: string): void {
|
||||
if (!this.crypto) {
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("End-to-End encryption disabled");
|
||||
}
|
||||
this.crypto.forceDiscardSession(roomId);
|
||||
this.cryptoBackend.forceDiscardSession(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3076,8 +3145,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*
|
||||
* This should be encrypted before returning it to the user.
|
||||
*
|
||||
* @returns a promise which resolves to a list of
|
||||
* session export objects
|
||||
* @returns a promise which resolves to a list of session export objects
|
||||
*
|
||||
* @deprecated Prefer {@link CryptoApi.exportRoomKeys | `CryptoApi.exportRoomKeys`}:
|
||||
*
|
||||
* ```javascript
|
||||
* sessionData = await client.getCrypto().exportRoomKeys();
|
||||
* ```
|
||||
*/
|
||||
public exportRoomKeys(): Promise<IMegolmSessionData[]> {
|
||||
if (!this.cryptoBackend) {
|
||||
@@ -3232,7 +3306,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* encrypted with, or null if it is not present or not encrypted with a
|
||||
* trusted key
|
||||
*/
|
||||
public isKeyBackupKeyStored(): Promise<Record<string, ISecretStorageKeyInfo> | null> {
|
||||
public isKeyBackupKeyStored(): Promise<Record<string, SecretStorageKeyDescription> | null> {
|
||||
return Promise.resolve(this.isSecretStored("m.megolm_backup.v1"));
|
||||
}
|
||||
|
||||
@@ -3748,9 +3822,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
const deviceInfos = await this.crypto.downloadKeys(userIds);
|
||||
const devicesByUser: Record<string, DeviceInfo[]> = {};
|
||||
for (const [userId, devices] of Object.entries(deviceInfos)) {
|
||||
devicesByUser[userId] = Object.values(devices);
|
||||
const devicesByUser: Map<string, DeviceInfo[]> = new Map();
|
||||
for (const [userId, devices] of deviceInfos) {
|
||||
devicesByUser.set(userId, Array.from(devices.values()));
|
||||
}
|
||||
|
||||
// XXX: Private member access
|
||||
@@ -4130,7 +4204,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
public setPowerLevel(
|
||||
roomId: string,
|
||||
userId: string | string[],
|
||||
powerLevel: number,
|
||||
powerLevel: number | undefined,
|
||||
event: MatrixEvent | null,
|
||||
): Promise<ISendEventResponse> {
|
||||
let content = {
|
||||
@@ -4141,13 +4215,16 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// existing client state with a failed power level change
|
||||
content = utils.deepCopy(event.getContent());
|
||||
}
|
||||
if (Array.isArray(userId)) {
|
||||
for (const user of userId) {
|
||||
|
||||
const users = Array.isArray(userId) ? userId : [userId];
|
||||
for (const user of users) {
|
||||
if (powerLevel == null) {
|
||||
delete content.users[user];
|
||||
} else {
|
||||
content.users[user] = powerLevel;
|
||||
}
|
||||
} else {
|
||||
content.users[userId] = powerLevel;
|
||||
}
|
||||
|
||||
const path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", {
|
||||
$roomId: roomId,
|
||||
});
|
||||
@@ -5962,6 +6039,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const token = res.next_token;
|
||||
const matrixEvents: MatrixEvent[] = [];
|
||||
|
||||
res.notifications = res.notifications.filter(noUnsafeEventProps);
|
||||
|
||||
for (let i = 0; i < res.notifications.length; i++) {
|
||||
const notification = res.notifications[i];
|
||||
const event = this.getEventMapper()(notification.event);
|
||||
@@ -6008,11 +6087,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
.then((res) => {
|
||||
if (res.state) {
|
||||
const roomState = eventTimeline.getState(dir)!;
|
||||
const stateEvents = res.state.map(this.getEventMapper());
|
||||
const stateEvents = res.state.filter(noUnsafeEventProps).map(this.getEventMapper());
|
||||
roomState.setUnknownStateEvents(stateEvents);
|
||||
}
|
||||
const token = res.end;
|
||||
const matrixEvents = res.chunk.map(this.getEventMapper());
|
||||
const matrixEvents = res.chunk.filter(noUnsafeEventProps).map(this.getEventMapper());
|
||||
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
|
||||
@@ -6044,7 +6123,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
})
|
||||
.then(async (res) => {
|
||||
const mapper = this.getEventMapper();
|
||||
const matrixEvents = res.chunk.map(mapper);
|
||||
const matrixEvents = res.chunk.filter(noUnsafeEventProps).map(mapper);
|
||||
|
||||
// Process latest events first
|
||||
for (const event of matrixEvents.slice().reverse()) {
|
||||
@@ -6092,11 +6171,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
.then((res) => {
|
||||
if (res.state) {
|
||||
const roomState = eventTimeline.getState(dir)!;
|
||||
const stateEvents = res.state.map(this.getEventMapper());
|
||||
const stateEvents = res.state.filter(noUnsafeEventProps).map(this.getEventMapper());
|
||||
roomState.setUnknownStateEvents(stateEvents);
|
||||
}
|
||||
const token = res.end;
|
||||
const matrixEvents = res.chunk.map(this.getEventMapper());
|
||||
const matrixEvents = res.chunk.filter(noUnsafeEventProps).map(this.getEventMapper());
|
||||
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
const [timelineEvents] = room.partitionThreadedEvents(matrixEvents);
|
||||
@@ -8496,10 +8575,22 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*/
|
||||
public getPushRules(): Promise<IPushRules> {
|
||||
return this.http.authedRequest<IPushRules>(Method.Get, "/pushrules/").then((rules: IPushRules) => {
|
||||
return PushProcessor.rewriteDefaultRules(rules);
|
||||
this.setPushRules(rules);
|
||||
return this.pushRules!;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the push rules for the account. This should be called whenever
|
||||
* updated push rules are available.
|
||||
*/
|
||||
public setPushRules(rules: IPushRules): void {
|
||||
// Fix-up defaults, if applicable.
|
||||
this.pushRules = PushProcessor.rewriteDefaultRules(rules, this.getUserId()!);
|
||||
// Pre-calculate any necessary caches.
|
||||
this.pushProcessor.updateCachedPushRuleKeys(this.pushRules);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Promise which resolves: an empty object `{}`
|
||||
* @returns Rejects: with an error response.
|
||||
@@ -8746,18 +8837,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
email: string,
|
||||
clientSecret: string,
|
||||
sendAttempt: number,
|
||||
nextLink: string,
|
||||
nextLink?: string,
|
||||
identityAccessToken?: string,
|
||||
): Promise<any> {
|
||||
// TODO: Types
|
||||
const params = {
|
||||
): Promise<IRequestTokenResponse> {
|
||||
const params: Record<string, string> = {
|
||||
client_secret: clientSecret,
|
||||
email: email,
|
||||
send_attempt: sendAttempt?.toString(),
|
||||
next_link: nextLink,
|
||||
};
|
||||
if (nextLink) {
|
||||
params.next_link = nextLink;
|
||||
}
|
||||
|
||||
return this.http.idServerRequest(
|
||||
return this.http.idServerRequest<IRequestTokenResponse>(
|
||||
Method.Post,
|
||||
"/validate/email/requestToken",
|
||||
params,
|
||||
@@ -8788,7 +8880,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param identityAccessToken - The `access_token` field of the Identity
|
||||
* Server `/account/register` response (see {@link registerWithIdentityServer}).
|
||||
*
|
||||
* @returns Promise which resolves: TODO
|
||||
* @returns Promise which resolves to an object with a sid string
|
||||
* @returns Rejects: with an error response.
|
||||
* @throws Error if no identity server is set
|
||||
*/
|
||||
@@ -8797,19 +8889,20 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
phoneNumber: string,
|
||||
clientSecret: string,
|
||||
sendAttempt: number,
|
||||
nextLink: string,
|
||||
nextLink?: string,
|
||||
identityAccessToken?: string,
|
||||
): Promise<any> {
|
||||
// TODO: Types
|
||||
const params = {
|
||||
): Promise<IRequestMsisdnTokenResponse> {
|
||||
const params: Record<string, string> = {
|
||||
client_secret: clientSecret,
|
||||
country: phoneCountry,
|
||||
phone_number: phoneNumber,
|
||||
send_attempt: sendAttempt?.toString(),
|
||||
next_link: nextLink,
|
||||
};
|
||||
if (nextLink) {
|
||||
params.next_link = nextLink;
|
||||
}
|
||||
|
||||
return this.http.idServerRequest(
|
||||
return this.http.idServerRequest<IRequestMsisdnTokenResponse>(
|
||||
Method.Post,
|
||||
"/validate/msisdn/requestToken",
|
||||
params,
|
||||
@@ -9100,24 +9193,22 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* supplied.
|
||||
* @returns Promise which resolves: to an empty object `{}`
|
||||
*/
|
||||
public sendToDevice(
|
||||
eventType: string,
|
||||
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
|
||||
txnId?: string,
|
||||
): Promise<{}> {
|
||||
public sendToDevice(eventType: string, contentMap: SendToDeviceContentMap, txnId?: string): Promise<{}> {
|
||||
const path = utils.encodeUri("/sendToDevice/$eventType/$txnId", {
|
||||
$eventType: eventType,
|
||||
$txnId: txnId ? txnId : this.makeTxnId(),
|
||||
});
|
||||
|
||||
const body = {
|
||||
messages: contentMap,
|
||||
messages: utils.recursiveMapToObject(contentMap),
|
||||
};
|
||||
|
||||
const targets = Object.keys(contentMap).reduce<Record<string, string[]>>((obj, key) => {
|
||||
obj[key] = Object.keys(contentMap[key]);
|
||||
return obj;
|
||||
}, {});
|
||||
const targets = new Map<string, string[]>();
|
||||
|
||||
for (const [userId, deviceMessages] of contentMap) {
|
||||
targets.set(userId, Array.from(deviceMessages.keys()));
|
||||
}
|
||||
|
||||
logger.log(`PUT ${path}`, targets);
|
||||
|
||||
return this.http.authedRequest(Method.Put, path, undefined, body);
|
||||
@@ -9390,6 +9481,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return this.clientOpts?.threadSupport || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper to determine intentional mentions support
|
||||
* @returns a boolean to determine if intentional mentions are enabled
|
||||
* @experimental
|
||||
*/
|
||||
public supportsIntentionalMentions(): boolean {
|
||||
return this.clientOpts?.intentionalMentions || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the summary of a room as defined by an initial version of MSC3266 and implemented in Synapse
|
||||
* Proposed at https://github.com/matrix-org/matrix-doc/pull/3266
|
||||
@@ -9459,7 +9559,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
roomId: string,
|
||||
timestamp: number,
|
||||
dir: Direction,
|
||||
): Promise<ITimestampToEventResponse> {
|
||||
): Promise<TimestampToEventResponse> {
|
||||
const path = utils.encodeUri("/rooms/$roomId/timestamp_to_event", {
|
||||
$roomId: roomId,
|
||||
});
|
||||
|
||||
@@ -14,31 +14,26 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
||||
import type { IToDeviceEvent } from "../sync-accumulator";
|
||||
import type { DeviceTrustLevel, UserTrustLevel } from "../crypto/CrossSigning";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { Room } from "../models/room";
|
||||
import { CryptoApi } from "../crypto-api";
|
||||
import { DeviceTrustLevel, UserTrustLevel } from "../crypto/CrossSigning";
|
||||
import { IEncryptedEventInfo } from "../crypto/api";
|
||||
import { IEventDecryptionResult } from "../@types/crypto";
|
||||
|
||||
/**
|
||||
* Common interface for the crypto implementations
|
||||
*/
|
||||
export interface CryptoBackend extends SyncCryptoCallbacks {
|
||||
/**
|
||||
* Global override for whether the client should ever send encrypted
|
||||
* messages to unverified devices. This provides the default for rooms which
|
||||
* do not specify a value.
|
||||
*
|
||||
* If true, all unverified devices will be blacklisted by default
|
||||
*/
|
||||
globalBlacklistUnverifiedDevices: boolean;
|
||||
|
||||
export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
|
||||
/**
|
||||
* Whether sendMessage in a room with unknown and unverified devices
|
||||
* should throw an error and not send the message. This has 'Global' for
|
||||
* symmetry with setGlobalBlacklistUnverifiedDevices but there is currently
|
||||
* no room-level equivalent for this setting.
|
||||
*
|
||||
* @remarks this is here, rather than in `CryptoApi`, because I don't think we're
|
||||
* going to support it in the rust crypto implementation.
|
||||
*/
|
||||
globalErrorOnUnknownDevices: boolean;
|
||||
|
||||
@@ -47,16 +42,6 @@ export interface CryptoBackend extends SyncCryptoCallbacks {
|
||||
*/
|
||||
stop(): void;
|
||||
|
||||
/**
|
||||
* Checks if the user has previously published cross-signing keys
|
||||
*
|
||||
* This means downloading the devicelist for the user and checking if the list includes
|
||||
* the cross-signing pseudo-device.
|
||||
|
||||
* @returns true if the user has previously published cross-signing keys
|
||||
*/
|
||||
userHasCrossSigningKeys(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get the verification level for a given user
|
||||
*
|
||||
@@ -76,14 +61,6 @@ export interface CryptoBackend extends SyncCryptoCallbacks {
|
||||
*/
|
||||
checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel;
|
||||
|
||||
/**
|
||||
* Perform any background tasks that can be done before a message is ready to
|
||||
* send, in order to speed up sending of the message.
|
||||
*
|
||||
* @param room - the room the event is in
|
||||
*/
|
||||
prepareToEncrypt(room: Room): void;
|
||||
|
||||
/**
|
||||
* Encrypt an event according to the configuration of the room.
|
||||
*
|
||||
@@ -110,16 +87,6 @@ export interface CryptoBackend extends SyncCryptoCallbacks {
|
||||
* @param event - event to be checked
|
||||
*/
|
||||
getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo;
|
||||
|
||||
/**
|
||||
* Get a list containing all of the room keys
|
||||
*
|
||||
* This should be encrypted before returning it to the user.
|
||||
*
|
||||
* @returns a promise which resolves to a list of
|
||||
* session export objects
|
||||
*/
|
||||
exportRoomKeys(): Promise<IMegolmSessionData[]>;
|
||||
}
|
||||
|
||||
/** The methods which crypto implementations should expose to the Sync api */
|
||||
@@ -138,6 +105,34 @@ export interface SyncCryptoCallbacks {
|
||||
*/
|
||||
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]>;
|
||||
|
||||
/**
|
||||
* Called by the /sync loop whenever there are incoming to-device messages.
|
||||
*
|
||||
* The implementation may preprocess the received messages (eg, decrypt them) and return an
|
||||
* updated list of messages for dispatch to the rest of the system.
|
||||
*
|
||||
* Note that, unlike {@link ClientEvent.ToDeviceEvent} events, this is called on the raw to-device
|
||||
* messages, rather than the results of any decryption attempts.
|
||||
*
|
||||
* @param oneTimeKeysCounts - the received one time key counts
|
||||
* @returns A list of preprocessed to-device messages.
|
||||
*/
|
||||
preprocessOneTimeKeyCounts(oneTimeKeysCounts: Map<string, number>): Promise<void>;
|
||||
|
||||
/**
|
||||
* Called by the /sync loop whenever there are incoming to-device messages.
|
||||
*
|
||||
* The implementation may preprocess the received messages (eg, decrypt them) and return an
|
||||
* updated list of messages for dispatch to the rest of the system.
|
||||
*
|
||||
* Note that, unlike {@link ClientEvent.ToDeviceEvent} events, this is called on the raw to-device
|
||||
* messages, rather than the results of any decryption attempts.
|
||||
*
|
||||
* @param unusedFallbackKeys - the received unused fallback keys
|
||||
* @returns A list of preprocessed to-device messages.
|
||||
*/
|
||||
preprocessUnusedFallbackKeys(unusedFallbackKeys: Set<string>): Promise<void>;
|
||||
|
||||
/**
|
||||
* Called by the /sync loop whenever an m.room.encryption event is received.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IMegolmSessionData } from "./@types/crypto";
|
||||
import { Room } from "./models/room";
|
||||
|
||||
/**
|
||||
* Public interface to the cryptography parts of the js-sdk
|
||||
*
|
||||
* @remarks Currently, this is a work-in-progress. In time, more methods will be added here.
|
||||
*/
|
||||
export interface CryptoApi {
|
||||
/**
|
||||
* Global override for whether the client should ever send encrypted
|
||||
* messages to unverified devices. This provides the default for rooms which
|
||||
* do not specify a value.
|
||||
*
|
||||
* If true, all unverified devices will be blacklisted by default
|
||||
*/
|
||||
globalBlacklistUnverifiedDevices: boolean;
|
||||
|
||||
/**
|
||||
* Checks if the user has previously published cross-signing keys
|
||||
*
|
||||
* This means downloading the devicelist for the user and checking if the list includes
|
||||
* the cross-signing pseudo-device.
|
||||
*
|
||||
* @returns true if the user has previously published cross-signing keys
|
||||
*/
|
||||
userHasCrossSigningKeys(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Perform any background tasks that can be done before a message is ready to
|
||||
* send, in order to speed up sending of the message.
|
||||
*
|
||||
* @param room - the room the event is in
|
||||
*/
|
||||
prepareToEncrypt(room: Room): void;
|
||||
|
||||
/**
|
||||
* Discard any existing megolm session for the given room.
|
||||
*
|
||||
* This will ensure that a new session is created on the next call to {@link prepareToEncrypt},
|
||||
* or the next time a message is sent.
|
||||
*
|
||||
* This should not normally be necessary: it should only be used as a debugging tool if there has been a
|
||||
* problem with encryption.
|
||||
*
|
||||
* @param roomId - the room to discard sessions for
|
||||
*/
|
||||
forceDiscardSession(roomId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get a list containing all of the room keys
|
||||
*
|
||||
* This should be encrypted before returning it to the user.
|
||||
*
|
||||
* @returns a promise which resolves to a list of
|
||||
* session export objects
|
||||
*/
|
||||
exportRoomKeys(): Promise<IMegolmSessionData[]>;
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import { OlmDevice } from "./OlmDevice";
|
||||
import { ICryptoCallbacks } from ".";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
import { CryptoStore, SecretStorePrivateKeys } from "./store/base";
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
import { SecretStorageKeyDescription } from "../secret-storage";
|
||||
|
||||
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
|
||||
|
||||
@@ -169,7 +169,7 @@ export class CrossSigningInfo {
|
||||
// check what SSSS keys have encrypted the master key (if any)
|
||||
const stored = (await secretStorage.isStored("m.cross_signing.master")) || {};
|
||||
// then check which of those SSSS keys have also encrypted the SSK and USK
|
||||
function intersect(s: Record<string, ISecretStorageKeyInfo>): void {
|
||||
function intersect(s: Record<string, SecretStorageKeyDescription>): void {
|
||||
for (const k of Object.keys(stored)) {
|
||||
if (!s[k]) {
|
||||
delete stored[k];
|
||||
|
||||
@@ -58,7 +58,8 @@ export enum TrackingStatus {
|
||||
UpToDate,
|
||||
}
|
||||
|
||||
export type DeviceInfoMap = Record<string, Record<string, DeviceInfo>>;
|
||||
// user-Id → device-Id → DeviceInfo
|
||||
export type DeviceInfoMap = Map<string, Map<string, DeviceInfo>>;
|
||||
|
||||
type EmittedEvents = CryptoEvent.WillUpdateDevices | CryptoEvent.DevicesUpdated | CryptoEvent.UserCrossSigningUpdated;
|
||||
|
||||
@@ -301,13 +302,13 @@ export class DeviceList extends TypedEventEmitter<EmittedEvents, CryptoEventHand
|
||||
* @returns userId-\>deviceId-\>{@link DeviceInfo}.
|
||||
*/
|
||||
private getDevicesFromStore(userIds: string[]): DeviceInfoMap {
|
||||
const stored: DeviceInfoMap = {};
|
||||
userIds.forEach((u) => {
|
||||
stored[u] = {};
|
||||
const devices = this.getStoredDevicesForUser(u) || [];
|
||||
devices.forEach(function (dev) {
|
||||
stored[u][dev.deviceId] = dev;
|
||||
const stored: DeviceInfoMap = new Map();
|
||||
userIds.forEach((userId) => {
|
||||
const deviceMap = new Map();
|
||||
this.getStoredDevicesForUser(userId)?.forEach(function (device) {
|
||||
deviceMap.set(device.deviceId, device);
|
||||
});
|
||||
stored.set(userId, deviceMap);
|
||||
});
|
||||
return stored;
|
||||
}
|
||||
|
||||
@@ -28,10 +28,10 @@ import {
|
||||
ISignedKey,
|
||||
KeySignatures,
|
||||
} from "../client";
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { IAccountDataClient } from "./SecretStorage";
|
||||
import { SecretStorageKeyDescription } from "../secret-storage";
|
||||
|
||||
interface ICrossSigningKeys {
|
||||
authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
|
||||
@@ -61,7 +61,7 @@ export class EncryptionSetupBuilder {
|
||||
* @param accountData - pre-existing account data, will only be read, not written.
|
||||
* @param delegateCryptoCallbacks - crypto callbacks to delegate to if the key isn't in cache yet
|
||||
*/
|
||||
public constructor(accountData: Record<string, MatrixEvent>, delegateCryptoCallbacks?: ICryptoCallbacks) {
|
||||
public constructor(accountData: Map<string, MatrixEvent>, delegateCryptoCallbacks?: ICryptoCallbacks) {
|
||||
this.accountDataClientAdapter = new AccountDataClientAdapter(accountData);
|
||||
this.crossSigningCallbacks = new CrossSigningCallbacks();
|
||||
this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks);
|
||||
@@ -246,7 +246,7 @@ class AccountDataClientAdapter
|
||||
/**
|
||||
* @param existingValues - existing account data
|
||||
*/
|
||||
public constructor(private readonly existingValues: Record<string, MatrixEvent>) {
|
||||
public constructor(private readonly existingValues: Map<string, MatrixEvent>) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ class AccountDataClientAdapter
|
||||
if (modifiedValue) {
|
||||
return modifiedValue;
|
||||
}
|
||||
const existingValue = this.existingValues[type];
|
||||
const existingValue = this.existingValues.get(type);
|
||||
if (existingValue) {
|
||||
return existingValue.getContent();
|
||||
}
|
||||
@@ -326,7 +326,7 @@ class SSSSCryptoCallbacks {
|
||||
public constructor(private readonly delegateCryptoCallbacks?: ICryptoCallbacks) {}
|
||||
|
||||
public async getSecretStorageKey(
|
||||
{ keys }: { keys: Record<string, ISecretStorageKeyInfo> },
|
||||
{ keys }: { keys: Record<string, SecretStorageKeyDescription> },
|
||||
name: string,
|
||||
): Promise<[string, Uint8Array] | null> {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
@@ -348,7 +348,7 @@ class SSSSCryptoCallbacks {
|
||||
return null;
|
||||
}
|
||||
|
||||
public addPrivateKey(keyId: string, keyInfo: ISecretStorageKeyInfo, privKey: Uint8Array): void {
|
||||
public addPrivateKey(keyId: string, keyInfo: SecretStorageKeyDescription, privKey: Uint8Array): void {
|
||||
this.privateKeys.set(keyId, privKey);
|
||||
// Also pass along to application to cache if it wishes
|
||||
this.delegateCryptoCallbacks?.cacheSecretStorageKey?.(keyId, keyInfo, privKey);
|
||||
|
||||
@@ -21,6 +21,7 @@ import { MatrixClient } from "../client";
|
||||
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "./index";
|
||||
import { CryptoStore, OutgoingRoomKeyRequest } from "./store/base";
|
||||
import { EventType, ToDeviceMessageId } from "../@types/event";
|
||||
import { MapWithDefault } from "../utils";
|
||||
|
||||
/**
|
||||
* Internal module. Management of outgoing room key requests.
|
||||
@@ -460,15 +461,13 @@ export class OutgoingRoomKeyRequestManager {
|
||||
recipients: IRoomKeyRequestRecipient[],
|
||||
txnId?: string,
|
||||
): Promise<{}> {
|
||||
const contentMap: Record<string, Record<string, Record<string, any>>> = {};
|
||||
const contentMap = new MapWithDefault<string, Map<string, Record<string, any>>>(() => new Map());
|
||||
for (const recip of recipients) {
|
||||
if (!contentMap[recip.userId]) {
|
||||
contentMap[recip.userId] = {};
|
||||
}
|
||||
contentMap[recip.userId][recip.deviceId] = {
|
||||
const userDeviceMap = contentMap.getOrCreate(recip.userId);
|
||||
userDeviceMap.set(recip.deviceId, {
|
||||
...message,
|
||||
[ToDeviceMessageId]: uuidv4(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return this.baseApis.sendToDevice(EventType.RoomKeyRequest, contentMap, txnId);
|
||||
|
||||
+38
-43
@@ -23,16 +23,17 @@ import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./
|
||||
import { ICryptoCallbacks, IEncryptedContent } from ".";
|
||||
import { IContent, MatrixEvent } from "../models/event";
|
||||
import { ClientEvent, ClientEventHandlerMap, MatrixClient } from "../client";
|
||||
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from "./api";
|
||||
import { IAddSecretStorageKeyOpts } from "./api";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
import { defer, IDeferred } from "../utils";
|
||||
import { ToDeviceMessageId } from "../@types/event";
|
||||
import { SecretStorageKeyDescription, SecretStorageKeyDescriptionAesV1 } from "../secret-storage";
|
||||
|
||||
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
|
||||
|
||||
// Some of the key functions use a tuple and some use an object...
|
||||
export type SecretStorageKeyTuple = [keyId: string, keyInfo: ISecretStorageKeyInfo];
|
||||
export type SecretStorageKeyObject = { keyId: string; keyInfo: ISecretStorageKeyInfo };
|
||||
export type SecretStorageKeyTuple = [keyId: string, keyInfo: SecretStorageKeyDescription];
|
||||
export type SecretStorageKeyObject = { keyId: string; keyInfo: SecretStorageKeyDescription };
|
||||
|
||||
export interface ISecretRequest {
|
||||
requestId: string;
|
||||
@@ -127,30 +128,30 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
|
||||
opts: IAddSecretStorageKeyOpts = {},
|
||||
keyId?: string,
|
||||
): Promise<SecretStorageKeyObject> {
|
||||
const keyInfo = { algorithm } as ISecretStorageKeyInfo;
|
||||
if (algorithm !== SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
throw new Error(`Unknown key algorithm ${algorithm}`);
|
||||
}
|
||||
|
||||
const keyInfo = { algorithm } as SecretStorageKeyDescriptionAesV1;
|
||||
|
||||
if (opts.name) {
|
||||
keyInfo.name = opts.name;
|
||||
}
|
||||
|
||||
if (algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
if (opts.passphrase) {
|
||||
keyInfo.passphrase = opts.passphrase;
|
||||
}
|
||||
if (opts.key) {
|
||||
const { iv, mac } = await calculateKeyCheck(opts.key);
|
||||
keyInfo.iv = iv;
|
||||
keyInfo.mac = mac;
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown key algorithm ${algorithm}`);
|
||||
if (opts.passphrase) {
|
||||
keyInfo.passphrase = opts.passphrase;
|
||||
}
|
||||
if (opts.key) {
|
||||
const { iv, mac } = await calculateKeyCheck(opts.key);
|
||||
keyInfo.iv = iv;
|
||||
keyInfo.mac = mac;
|
||||
}
|
||||
|
||||
if (!keyId) {
|
||||
do {
|
||||
keyId = randomString(32);
|
||||
} while (
|
||||
await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
|
||||
await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
|
||||
`m.secret_storage.key.${keyId}`,
|
||||
)
|
||||
);
|
||||
@@ -181,7 +182,7 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
return keyInfo ? [keyId, keyInfo] : null;
|
||||
@@ -206,7 +207,7 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
|
||||
*
|
||||
* @returns whether or not the key matches
|
||||
*/
|
||||
public async checkKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise<boolean> {
|
||||
public async checkKey(key: Uint8Array, info: SecretStorageKeyDescription): Promise<boolean> {
|
||||
if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
if (info.mac) {
|
||||
const { mac } = await calculateKeyCheck(key, info.iv);
|
||||
@@ -245,7 +246,7 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
|
||||
|
||||
for (const keyId of keys) {
|
||||
// get key information from key storage
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
if (!keyInfo) {
|
||||
@@ -284,10 +285,10 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
|
||||
}
|
||||
|
||||
// get possible keys to decrypt
|
||||
const keys: Record<string, ISecretStorageKeyInfo> = {};
|
||||
const keys: Record<string, SecretStorageKeyDescription> = {};
|
||||
for (const keyId of Object.keys(secretInfo.encrypted)) {
|
||||
// get key information from key storage
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
const encInfo = secretInfo.encrypted[keyId];
|
||||
@@ -322,17 +323,17 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
|
||||
* with, or null if it is not present or not encrypted with a trusted
|
||||
* key
|
||||
*/
|
||||
public async isStored(name: string): Promise<Record<string, ISecretStorageKeyInfo> | null> {
|
||||
public async isStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
|
||||
// check if secret exists
|
||||
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretInfo>(name);
|
||||
if (!secretInfo?.encrypted) return null;
|
||||
|
||||
const ret: Record<string, ISecretStorageKeyInfo> = {};
|
||||
const ret: Record<string, SecretStorageKeyDescription> = {};
|
||||
|
||||
// filter secret encryption keys with supported algorithm
|
||||
for (const keyId of Object.keys(secretInfo.encrypted)) {
|
||||
// get key information from key storage
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<ISecretStorageKeyInfo>(
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer<SecretStorageKeyDescription>(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
if (!keyInfo) continue;
|
||||
@@ -367,13 +368,11 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
|
||||
requesting_device_id: this.baseApis.deviceId,
|
||||
request_id: requestId,
|
||||
};
|
||||
const toDevice: Record<string, typeof cancelData> = {};
|
||||
const toDevice: Map<string, typeof cancelData> = new Map();
|
||||
for (const device of devices) {
|
||||
toDevice[device] = cancelData;
|
||||
toDevice.set(device, cancelData);
|
||||
}
|
||||
this.baseApis.sendToDevice("m.secret.request", {
|
||||
[this.baseApis.getUserId()!]: toDevice,
|
||||
});
|
||||
this.baseApis.sendToDevice("m.secret.request", new Map([[this.baseApis.getUserId()!, toDevice]]));
|
||||
|
||||
// and reject the promise so that anyone waiting on it will be
|
||||
// notified
|
||||
@@ -388,14 +387,12 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
|
||||
request_id: requestId,
|
||||
[ToDeviceMessageId]: uuidv4(),
|
||||
};
|
||||
const toDevice: Record<string, typeof requestData> = {};
|
||||
const toDevice: Map<string, typeof requestData> = new Map();
|
||||
for (const device of devices) {
|
||||
toDevice[device] = requestData;
|
||||
toDevice.set(device, requestData);
|
||||
}
|
||||
logger.info(`Request secret ${name} from ${devices}, id ${requestId}`);
|
||||
this.baseApis.sendToDevice("m.secret.request", {
|
||||
[this.baseApis.getUserId()!]: toDevice,
|
||||
});
|
||||
this.baseApis.sendToDevice("m.secret.request", new Map([[this.baseApis.getUserId()!, toDevice]]));
|
||||
|
||||
return {
|
||||
requestId,
|
||||
@@ -469,9 +466,11 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
|
||||
ciphertext: {},
|
||||
[ToDeviceMessageId]: uuidv4(),
|
||||
};
|
||||
await olmlib.ensureOlmSessionsForDevices(this.baseApis.crypto!.olmDevice, this.baseApis, {
|
||||
[sender]: [this.baseApis.getStoredDevice(sender, deviceId)!],
|
||||
});
|
||||
await olmlib.ensureOlmSessionsForDevices(
|
||||
this.baseApis.crypto!.olmDevice,
|
||||
this.baseApis,
|
||||
new Map([[sender, [this.baseApis.getStoredDevice(sender, deviceId)!]]]),
|
||||
);
|
||||
await olmlib.encryptMessageForDevice(
|
||||
encryptedContent.ciphertext,
|
||||
this.baseApis.getUserId()!,
|
||||
@@ -481,11 +480,7 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
|
||||
this.baseApis.getStoredDevice(sender, deviceId)!,
|
||||
payload,
|
||||
);
|
||||
const contentMap = {
|
||||
[sender]: {
|
||||
[deviceId]: encryptedContent,
|
||||
},
|
||||
};
|
||||
const contentMap = new Map([[sender, new Map([[deviceId, encryptedContent]])]]);
|
||||
|
||||
logger.info(`Sending ${content.name} secret for ${deviceId}`);
|
||||
this.baseApis.sendToDevice("m.room.encrypted", contentMap);
|
||||
@@ -550,7 +545,7 @@ export class SecretStorage<B extends MatrixClient | undefined = MatrixClient> {
|
||||
}
|
||||
|
||||
private async getSecretStorageKey(
|
||||
keys: Record<string, ISecretStorageKeyInfo>,
|
||||
keys: Record<string, SecretStorageKeyDescription>,
|
||||
name: string,
|
||||
): Promise<[string, IDecryptors]> {
|
||||
if (!this.cryptoCallbacks.getSecretStorageKey) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { IContent, MatrixEvent, RoomMember } from "../../matrix";
|
||||
import { Crypto, IEncryptedContent, IEventDecryptionResult, IncomingRoomKeyRequest } from "..";
|
||||
import { DeviceInfo } from "../deviceinfo";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { DeviceInfoMap } from "../DeviceList";
|
||||
|
||||
/**
|
||||
* Map of registered encryption algorithm classes. A map from string to {@link EncryptionAlgorithm} class
|
||||
@@ -195,7 +196,7 @@ export abstract class DecryptionAlgorithm {
|
||||
}
|
||||
|
||||
public onRoomKeyWithheldEvent?(event: MatrixEvent): Promise<void>;
|
||||
public sendSharedHistoryInboundSessions?(devicesByUser: Record<string, DeviceInfo[]>): Promise<void>;
|
||||
public sendSharedHistoryInboundSessions?(devicesByUser: Map<string, DeviceInfo[]>): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -241,11 +242,7 @@ export class UnknownDeviceError extends Error {
|
||||
* @param msg - message describing the problem
|
||||
* @param devices - set of unknown devices per user we're warning about
|
||||
*/
|
||||
public constructor(
|
||||
msg: string,
|
||||
public readonly devices: Record<string, Record<string, object>>,
|
||||
public event?: MatrixEvent,
|
||||
) {
|
||||
public constructor(msg: string, public readonly devices: DeviceInfoMap, public event?: MatrixEvent) {
|
||||
super(msg);
|
||||
this.name = "UnknownDeviceError";
|
||||
this.devices = devices;
|
||||
|
||||
+456
-278
File diff suppressed because it is too large
Load Diff
+8
-18
@@ -16,6 +16,13 @@ limitations under the License.
|
||||
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
import { PassphraseInfo } from "../secret-storage";
|
||||
|
||||
/* re-exports for backwards compatibility. */
|
||||
export {
|
||||
PassphraseInfo as IPassphraseInfo,
|
||||
SecretStorageKeyDescription as ISecretStorageKeyInfo,
|
||||
} from "../secret-storage";
|
||||
|
||||
// TODO: Merge this with crypto.js once converted
|
||||
|
||||
@@ -98,26 +105,9 @@ export interface ICreateSecretStorageOpts {
|
||||
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
export interface ISecretStorageKeyInfo {
|
||||
name: string;
|
||||
algorithm: string;
|
||||
// technically the below are specific to AES keys. If we ever introduce another type,
|
||||
// we can split into separate interfaces.
|
||||
iv: string;
|
||||
mac: string;
|
||||
passphrase: IPassphraseInfo;
|
||||
}
|
||||
|
||||
export interface IPassphraseInfo {
|
||||
algorithm: "m.pbkdf2";
|
||||
iterations: number;
|
||||
salt: string;
|
||||
bits?: number;
|
||||
}
|
||||
|
||||
export interface IAddSecretStorageKeyOpts {
|
||||
pubkey?: string;
|
||||
passphrase?: IPassphraseInfo;
|
||||
passphrase?: PassphraseInfo;
|
||||
name?: string;
|
||||
key?: Uint8Array;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { DeviceTrustLevel } from "./CrossSigning";
|
||||
import { keyFromPassphrase } from "./key_passphrase";
|
||||
import { sleep } from "../utils";
|
||||
import { safeSet, sleep } from "../utils";
|
||||
import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
|
||||
import { encodeRecoveryKey } from "./recoverykey";
|
||||
import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from "./aes";
|
||||
@@ -498,9 +498,7 @@ export class BackupManager {
|
||||
const rooms: IKeyBackup["rooms"] = {};
|
||||
for (const session of sessions) {
|
||||
const roomId = session.sessionData!.room_id;
|
||||
if (rooms[roomId] === undefined) {
|
||||
rooms[roomId] = { sessions: {} };
|
||||
}
|
||||
safeSet(rooms, roomId, rooms[roomId] || { sessions: {} });
|
||||
|
||||
const sessionData = this.baseApis.crypto!.olmDevice.exportInboundGroupSession(
|
||||
session.senderKey,
|
||||
@@ -517,12 +515,12 @@ export class BackupManager {
|
||||
undefined;
|
||||
const verified = this.baseApis.crypto!.checkDeviceInfoTrust(userId!, device).isVerified();
|
||||
|
||||
rooms[roomId]["sessions"][session.sessionId] = {
|
||||
safeSet(rooms[roomId]["sessions"], session.sessionId, {
|
||||
first_message_index: sessionData.first_known_index,
|
||||
forwarded_count: forwardedCount,
|
||||
is_verified: verified,
|
||||
session_data: await this.algorithm!.encryptSession(sessionData),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
await this.baseApis.sendKeyBackup(undefined, undefined, this.backupInfo!.version, { rooms });
|
||||
|
||||
@@ -21,13 +21,13 @@ import { decodeBase64, encodeBase64 } from "./olmlib";
|
||||
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store";
|
||||
import { decryptAES, encryptAES } from "./aes";
|
||||
import { logger } from "../logger";
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
import { Crypto } from "./index";
|
||||
import { Method } from "../http-api";
|
||||
import { SecretStorageKeyDescription } from "../secret-storage";
|
||||
|
||||
export interface IDehydratedDevice {
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
device_data: ISecretStorageKeyInfo & {
|
||||
device_data: SecretStorageKeyDescription & {
|
||||
// eslint-disable-line camelcase
|
||||
algorithm: string;
|
||||
account: string; // pickle
|
||||
|
||||
+52
-28
@@ -48,7 +48,6 @@ import {
|
||||
IEncryptedEventInfo,
|
||||
IImportRoomKeysOpts,
|
||||
IRecoveryKey,
|
||||
ISecretStorageKeyInfo,
|
||||
} from "./api";
|
||||
import { OutgoingRoomKeyRequestManager } from "./OutgoingRoomKeyRequestManager";
|
||||
import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
|
||||
@@ -90,6 +89,8 @@ import { ISignatures } from "../@types/signed";
|
||||
import { IMessage } from "./algorithms/olm";
|
||||
import { CryptoBackend, OnSyncCompletedData } from "../common-crypto/CryptoBackend";
|
||||
import { RoomState, RoomStateEvent } from "../models/room-state";
|
||||
import { MapWithDefault, recursiveMapToObject } from "../utils";
|
||||
import { SecretStorageKeyDescription } from "../secret-storage";
|
||||
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
@@ -141,10 +142,10 @@ export interface ICryptoCallbacks {
|
||||
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
|
||||
shouldUpgradeDeviceVerifications?: (users: Record<string, any>) => Promise<string[]>;
|
||||
getSecretStorageKey?: (
|
||||
keys: { keys: Record<string, ISecretStorageKeyInfo> },
|
||||
keys: { keys: Record<string, SecretStorageKeyDescription> },
|
||||
name: string,
|
||||
) => Promise<[string, Uint8Array] | null>;
|
||||
cacheSecretStorageKey?: (keyId: string, keyInfo: ISecretStorageKeyInfo, key: Uint8Array) => void;
|
||||
cacheSecretStorageKey?: (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => void;
|
||||
onSecretRequested?: (
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
@@ -152,7 +153,10 @@ export interface ICryptoCallbacks {
|
||||
secretName: string,
|
||||
deviceTrust: DeviceTrustLevel,
|
||||
) => Promise<string | undefined>;
|
||||
getDehydrationKey?: (keyInfo: ISecretStorageKeyInfo, checkFunc: (key: Uint8Array) => void) => Promise<Uint8Array>;
|
||||
getDehydrationKey?: (
|
||||
keyInfo: SecretStorageKeyDescription,
|
||||
checkFunc: (key: Uint8Array) => void,
|
||||
) => Promise<Uint8Array>;
|
||||
getBackupKey?: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
@@ -399,7 +403,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
// deviceId: 1234567890000,
|
||||
// },
|
||||
// }
|
||||
private lastNewSessionForced: Record<string, Record<string, number>> = {};
|
||||
// Map: user Id → device Id → timestamp
|
||||
private lastNewSessionForced: MapWithDefault<string, MapWithDefault<string, number>> = new MapWithDefault(
|
||||
() => new MapWithDefault(() => 0),
|
||||
);
|
||||
|
||||
// This flag will be unset whilst the client processes a sync response
|
||||
// so that we don't start requesting keys until we've actually finished
|
||||
@@ -919,7 +926,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
return keyId;
|
||||
};
|
||||
|
||||
const ensureCanCheckPassphrase = async (keyId: string, keyInfo: ISecretStorageKeyInfo): Promise<void> => {
|
||||
const ensureCanCheckPassphrase = async (keyId: string, keyInfo: SecretStorageKeyDescription): Promise<void> => {
|
||||
if (!keyInfo.mac) {
|
||||
const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey?.(
|
||||
{ keys: { [keyId]: keyInfo } },
|
||||
@@ -1126,7 +1133,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
return this.secretStorage.get(name);
|
||||
}
|
||||
|
||||
public isSecretStored(name: string): Promise<Record<string, ISecretStorageKeyInfo> | null> {
|
||||
public isSecretStored(name: string): Promise<Record<string, SecretStorageKeyDescription> | null> {
|
||||
return this.secretStorage.isStored(name);
|
||||
}
|
||||
|
||||
@@ -1145,7 +1152,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
return this.secretStorage.setDefaultKeyId(k);
|
||||
}
|
||||
|
||||
public checkSecretStorageKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise<boolean> {
|
||||
public checkSecretStorageKey(key: Uint8Array, info: SecretStorageKeyDescription): Promise<boolean> {
|
||||
return this.secretStorage.checkKey(key, info);
|
||||
}
|
||||
|
||||
@@ -2488,13 +2495,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
*
|
||||
* This should not normally be necessary.
|
||||
*/
|
||||
public forceDiscardSession(roomId: string): void {
|
||||
public forceDiscardSession(roomId: string): Promise<void> {
|
||||
const alg = this.roomEncryptors.get(roomId);
|
||||
if (alg === undefined) throw new Error("Room not encrypted");
|
||||
if (alg.forceDiscardSession === undefined) {
|
||||
throw new Error("Room encryption algorithm doesn't support session discarding");
|
||||
}
|
||||
alg.forceDiscardSession();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2690,11 +2698,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
public ensureOlmSessionsForUsers(
|
||||
users: string[],
|
||||
force?: boolean,
|
||||
): Promise<Record<string, Record<string, olmlib.IOlmSessionResult>>> {
|
||||
const devicesByUser: Record<string, DeviceInfo[]> = {};
|
||||
): Promise<Map<string, Map<string, olmlib.IOlmSessionResult>>> {
|
||||
// map user Id → DeviceInfo[]
|
||||
const devicesByUser: Map<string, DeviceInfo[]> = new Map();
|
||||
|
||||
for (const userId of users) {
|
||||
devicesByUser[userId] = [];
|
||||
const userDevices: DeviceInfo[] = [];
|
||||
devicesByUser.set(userId, userDevices);
|
||||
|
||||
const devices = this.getStoredDevicesForUser(userId) || [];
|
||||
for (const deviceInfo of devices) {
|
||||
@@ -2708,7 +2718,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
continue;
|
||||
}
|
||||
|
||||
devicesByUser[userId].push(deviceInfo);
|
||||
userDevices.push(deviceInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3146,7 +3156,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
payload: encryptedContent,
|
||||
});
|
||||
|
||||
await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, { [userId]: [deviceInfo] });
|
||||
await olmlib.ensureOlmSessionsForDevices(
|
||||
this.olmDevice,
|
||||
this.baseApis,
|
||||
new Map([[userId, [deviceInfo]]]),
|
||||
);
|
||||
await olmlib.encryptMessageForDevice(
|
||||
encryptedContent.ciphertext,
|
||||
this.userId,
|
||||
@@ -3207,6 +3221,17 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
});
|
||||
}
|
||||
|
||||
public preprocessOneTimeKeyCounts(oneTimeKeysCounts: Map<string, number>): Promise<void> {
|
||||
const currentCount = oneTimeKeysCounts.get("signed_curve25519") || 0;
|
||||
this.updateOneTimeKeyCount(currentCount);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public preprocessUnusedFallbackKeys(unusedFallbackKeys: Set<string>): Promise<void> {
|
||||
this.setNeedsNewFallback(!unusedFallbackKeys.has("signed_curve25519"));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private onToDeviceEvent = (event: MatrixEvent): void => {
|
||||
try {
|
||||
logger.log(
|
||||
@@ -3448,8 +3473,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
|
||||
// check when we last forced a new session with this device: if we've already done so
|
||||
// recently, don't do it again.
|
||||
this.lastNewSessionForced[sender] = this.lastNewSessionForced[sender] || {};
|
||||
const lastNewSessionForced = this.lastNewSessionForced[sender][deviceKey] || 0;
|
||||
const lastNewSessionDevices = this.lastNewSessionForced.getOrCreate(sender);
|
||||
const lastNewSessionForced = lastNewSessionDevices.getOrCreate(deviceKey);
|
||||
if (lastNewSessionForced + MIN_FORCE_SESSION_INTERVAL_MS > Date.now()) {
|
||||
logger.debug(
|
||||
"New session already forced with device " +
|
||||
@@ -3482,11 +3507,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
return;
|
||||
}
|
||||
}
|
||||
const devicesByUser: Record<string, DeviceInfo[]> = {};
|
||||
devicesByUser[sender] = [device];
|
||||
const devicesByUser = new Map([[sender, [device]]]);
|
||||
await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, true);
|
||||
|
||||
this.lastNewSessionForced[sender][deviceKey] = Date.now();
|
||||
lastNewSessionDevices.set(deviceKey, Date.now());
|
||||
|
||||
// Now send a blank message on that session so the other side knows about it.
|
||||
// (The keyshare request is sent in the clear so that won't do)
|
||||
@@ -3513,11 +3537,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
await this.olmDevice.recordSessionProblem(deviceKey, "wedged", true);
|
||||
retryDecryption();
|
||||
|
||||
await this.baseApis.sendToDevice("m.room.encrypted", {
|
||||
[sender]: {
|
||||
[device.deviceId]: encryptedContent,
|
||||
},
|
||||
});
|
||||
await this.baseApis.sendToDevice(
|
||||
"m.room.encrypted",
|
||||
new Map([[sender, new Map([[device.deviceId, encryptedContent]])]]),
|
||||
);
|
||||
|
||||
// Most of the time this probably won't be necessary since we'll have queued up a key request when
|
||||
// we failed to decrypt the message and will be waiting a bit for the key to arrive before sending
|
||||
@@ -3824,15 +3847,16 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
* @param obj - Object to which we will add a 'signatures' property
|
||||
*/
|
||||
public async signObject<T extends ISignableObject & object>(obj: T): Promise<void> {
|
||||
const sigs = obj.signatures || {};
|
||||
const sigs = new Map(Object.entries(obj.signatures || {}));
|
||||
const unsigned = obj.unsigned;
|
||||
|
||||
delete obj.signatures;
|
||||
delete obj.unsigned;
|
||||
|
||||
sigs[this.userId] = sigs[this.userId] || {};
|
||||
sigs[this.userId]["ed25519:" + this.deviceId] = await this.olmDevice.sign(anotherjson.stringify(obj));
|
||||
obj.signatures = sigs;
|
||||
const userSignatures = sigs.get(this.userId) || {};
|
||||
sigs.set(this.userId, userSignatures);
|
||||
userSignatures["ed25519:" + this.deviceId] = await this.olmDevice.sign(anotherjson.stringify(obj));
|
||||
obj.signatures = recursiveMapToObject(sigs);
|
||||
if (unsigned !== undefined) obj.unsigned = unsigned;
|
||||
}
|
||||
}
|
||||
|
||||
+38
-37
@@ -30,6 +30,7 @@ import { ISignatures } from "../@types/signed";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { EventType } from "../@types/event";
|
||||
import { IMessage } from "./algorithms/olm";
|
||||
import { MapWithDefault } from "../utils";
|
||||
|
||||
enum Algorithm {
|
||||
Olm = "m.olm.v1.curve25519-aes-sha2",
|
||||
@@ -154,9 +155,11 @@ export async function getExistingOlmSessions(
|
||||
olmDevice: OlmDevice,
|
||||
baseApis: MatrixClient,
|
||||
devicesByUser: Record<string, DeviceInfo[]>,
|
||||
): Promise<[Record<string, DeviceInfo[]>, Record<string, Record<string, IExistingOlmSession>>]> {
|
||||
const devicesWithoutSession: { [userId: string]: DeviceInfo[] } = {};
|
||||
const sessions: { [userId: string]: { [deviceId: string]: IExistingOlmSession } } = {};
|
||||
): Promise<[Map<string, DeviceInfo[]>, Map<string, Map<string, IExistingOlmSession>>]> {
|
||||
// map user Id → DeviceInfo[]
|
||||
const devicesWithoutSession: MapWithDefault<string, DeviceInfo[]> = new MapWithDefault(() => []);
|
||||
// map user Id → device Id → IExistingOlmSession
|
||||
const sessions: MapWithDefault<string, Map<string, IExistingOlmSession>> = new MapWithDefault(() => new Map());
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
@@ -168,14 +171,12 @@ export async function getExistingOlmSessions(
|
||||
(async (): Promise<void> => {
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(key, true);
|
||||
if (sessionId === null) {
|
||||
devicesWithoutSession[userId] = devicesWithoutSession[userId] || [];
|
||||
devicesWithoutSession[userId].push(deviceInfo);
|
||||
devicesWithoutSession.getOrCreate(userId).push(deviceInfo);
|
||||
} else {
|
||||
sessions[userId] = sessions[userId] || {};
|
||||
sessions[userId][deviceId] = {
|
||||
sessions.getOrCreate(userId).set(deviceId, {
|
||||
device: deviceInfo,
|
||||
sessionId: sessionId,
|
||||
};
|
||||
});
|
||||
}
|
||||
})(),
|
||||
);
|
||||
@@ -210,24 +211,26 @@ export async function getExistingOlmSessions(
|
||||
export async function ensureOlmSessionsForDevices(
|
||||
olmDevice: OlmDevice,
|
||||
baseApis: MatrixClient,
|
||||
devicesByUser: Record<string, DeviceInfo[]>,
|
||||
devicesByUser: Map<string, DeviceInfo[]>,
|
||||
force = false,
|
||||
otkTimeout?: number,
|
||||
failedServers?: string[],
|
||||
log = logger,
|
||||
): Promise<Record<string, Record<string, IOlmSessionResult>>> {
|
||||
): Promise<Map<string, Map<string, IOlmSessionResult>>> {
|
||||
const devicesWithoutSession: [string, string][] = [
|
||||
// [userId, deviceId], ...
|
||||
];
|
||||
const result: { [userId: string]: { [deviceId: string]: IExistingOlmSession } } = {};
|
||||
const resolveSession: Record<string, (sessionId?: string) => void> = {};
|
||||
// map user Id → device Id → IExistingOlmSession
|
||||
const result: Map<string, Map<string, IExistingOlmSession>> = new Map();
|
||||
// map device key → resolve session fn
|
||||
const resolveSession: Map<string, (sessionId?: string) => void> = new Map();
|
||||
|
||||
// Mark all sessions this task intends to update as in progress. It is
|
||||
// important to do this for all devices this task cares about in a single
|
||||
// synchronous operation, as otherwise it is possible to have deadlocks
|
||||
// where multiple tasks wait indefinitely on another task to update some set
|
||||
// of common devices.
|
||||
for (const [, devices] of Object.entries(devicesByUser)) {
|
||||
for (const devices of devicesByUser.values()) {
|
||||
for (const deviceInfo of devices) {
|
||||
const key = deviceInfo.getIdentityKey();
|
||||
|
||||
@@ -242,17 +245,19 @@ export async function ensureOlmSessionsForDevices(
|
||||
// conditions. If we find that we already have a session, then
|
||||
// we'll resolve
|
||||
olmDevice.sessionsInProgress[key] = new Promise((resolve) => {
|
||||
resolveSession[key] = (v: any): void => {
|
||||
resolveSession.set(key, (v: any): void => {
|
||||
delete olmDevice.sessionsInProgress[key];
|
||||
resolve(v);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
result[userId] = {};
|
||||
for (const [userId, devices] of devicesByUser) {
|
||||
const resultDevices = new Map();
|
||||
result.set(userId, resultDevices);
|
||||
|
||||
for (const deviceInfo of devices) {
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
const key = deviceInfo.getIdentityKey();
|
||||
@@ -268,20 +273,21 @@ export async function ensureOlmSessionsForDevices(
|
||||
log.info("Attempted to start session with ourself! Ignoring");
|
||||
// We must fill in the section in the return value though, as callers
|
||||
// expect it to be there.
|
||||
result[userId][deviceId] = {
|
||||
resultDevices.set(deviceId, {
|
||||
device: deviceInfo,
|
||||
sessionId: null,
|
||||
};
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const forWhom = `for ${key} (${userId}:${deviceId})`;
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(key, !!resolveSession[key], log);
|
||||
if (sessionId !== null && resolveSession[key]) {
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(key, !!resolveSession.get(key), log);
|
||||
const resolveSessionFn = resolveSession.get(key);
|
||||
if (sessionId !== null && resolveSessionFn) {
|
||||
// we found a session, but we had marked the session as
|
||||
// in-progress, so resolve it now, which will unmark it and
|
||||
// unblock anything that was waiting
|
||||
resolveSession[key]();
|
||||
resolveSessionFn();
|
||||
}
|
||||
if (sessionId === null || force) {
|
||||
if (force) {
|
||||
@@ -291,10 +297,10 @@ export async function ensureOlmSessionsForDevices(
|
||||
}
|
||||
devicesWithoutSession.push([userId, deviceId]);
|
||||
}
|
||||
result[userId][deviceId] = {
|
||||
resultDevices.set(deviceId, {
|
||||
device: deviceInfo,
|
||||
sessionId: sessionId,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +316,7 @@ export async function ensureOlmSessionsForDevices(
|
||||
res = await baseApis.claimOneTimeKeys(devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout);
|
||||
log.debug(`Claimed ${taskDetail}`);
|
||||
} catch (e) {
|
||||
for (const resolver of Object.values(resolveSession)) {
|
||||
for (const resolver of resolveSession.values()) {
|
||||
resolver();
|
||||
}
|
||||
log.log(`Failed to claim ${taskDetail}`, e, devicesWithoutSession);
|
||||
@@ -323,7 +329,7 @@ export async function ensureOlmSessionsForDevices(
|
||||
|
||||
const otkResult = res.one_time_keys || ({} as IClaimOTKsResult["one_time_keys"]);
|
||||
const promises: Promise<void>[] = [];
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
for (const [userId, devices] of devicesByUser) {
|
||||
const userRes = otkResult[userId] || {};
|
||||
for (const deviceInfo of devices) {
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
@@ -336,7 +342,7 @@ export async function ensureOlmSessionsForDevices(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result[userId][deviceId].sessionId && !force) {
|
||||
if (result.get(userId)?.get(deviceId)?.sessionId && !force) {
|
||||
// we already have a result for this device
|
||||
continue;
|
||||
}
|
||||
@@ -351,24 +357,19 @@ export async function ensureOlmSessionsForDevices(
|
||||
|
||||
if (!oneTimeKey) {
|
||||
log.warn(`No one-time keys (alg=${oneTimeKeyAlgorithm}) ` + `for device ${userId}:${deviceId}`);
|
||||
if (resolveSession[key]) {
|
||||
resolveSession[key]();
|
||||
}
|
||||
resolveSession.get(key)?.();
|
||||
continue;
|
||||
}
|
||||
|
||||
promises.push(
|
||||
_verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo).then(
|
||||
(sid) => {
|
||||
if (resolveSession[key]) {
|
||||
resolveSession[key](sid ?? undefined);
|
||||
}
|
||||
result[userId][deviceId].sessionId = sid;
|
||||
resolveSession.get(key)?.(sid ?? undefined);
|
||||
const deviceInfo = result.get(userId)?.get(deviceId);
|
||||
if (deviceInfo) deviceInfo.sessionId = sid;
|
||||
},
|
||||
(e) => {
|
||||
if (resolveSession[key]) {
|
||||
resolveSession[key]();
|
||||
}
|
||||
resolveSession.get(key)?.();
|
||||
throw e;
|
||||
},
|
||||
),
|
||||
|
||||
@@ -21,6 +21,7 @@ import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { safeSet } from "../../utils";
|
||||
|
||||
/**
|
||||
* Internal module. Partial localStorage backed storage for e2e.
|
||||
@@ -178,11 +179,11 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
if (userId in notifiedErrorDevices) {
|
||||
if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
|
||||
ret.push(device);
|
||||
notifiedErrorDevices[userId][deviceInfo.deviceId] = true;
|
||||
safeSet(notifiedErrorDevices[userId], deviceInfo.deviceId, true);
|
||||
}
|
||||
} else {
|
||||
ret.push(device);
|
||||
notifiedErrorDevices[userId] = { [deviceInfo.deviceId]: true };
|
||||
safeSet(notifiedErrorDevices, userId, { [deviceInfo.deviceId]: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { safeSet } from "../../utils";
|
||||
|
||||
/**
|
||||
* Internal module. in-memory storage for e2e.
|
||||
@@ -375,11 +376,11 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
if (userId in notifiedErrorDevices) {
|
||||
if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
|
||||
ret.push(device);
|
||||
notifiedErrorDevices[userId][deviceInfo.deviceId] = true;
|
||||
safeSet(notifiedErrorDevices[userId], deviceInfo.deviceId, true);
|
||||
}
|
||||
} else {
|
||||
ret.push(device);
|
||||
notifiedErrorDevices[userId] = { [deviceInfo.deviceId]: true };
|
||||
safeSet(notifiedErrorDevices, userId, { [deviceInfo.deviceId]: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -269,12 +269,12 @@ export class ToDeviceChannel implements IVerificationChannel {
|
||||
|
||||
private async sendToDevices(type: string, content: Record<string, any>, devices: string[]): Promise<void> {
|
||||
if (devices.length) {
|
||||
const msgMap: Record<string, Record<string, any>> = {};
|
||||
const deviceMessages: Map<string, Record<string, any>> = new Map();
|
||||
for (const deviceId of devices) {
|
||||
msgMap[deviceId] = content;
|
||||
deviceMessages.set(deviceId, content);
|
||||
}
|
||||
|
||||
await this.client.sendToDevice(type, { [this.userId]: msgMap });
|
||||
await this.client.sendToDevice(type, new Map([[this.userId, deviceMessages]]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-15
@@ -29,15 +29,16 @@ import { IEvent, IContent, EventStatus } from "./models/event";
|
||||
import { ISendEventResponse } from "./@types/requests";
|
||||
import { EventType } from "./@types/event";
|
||||
import { logger } from "./logger";
|
||||
import { MatrixClient, ClientEvent, IMatrixClientCreateOpts, IStartClientOpts } from "./client";
|
||||
import { MatrixClient, ClientEvent, IMatrixClientCreateOpts, IStartClientOpts, SendToDeviceContentMap } from "./client";
|
||||
import { SyncApi, SyncState } from "./sync";
|
||||
import { SlidingSyncSdk } from "./sliding-sync-sdk";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
import { User } from "./models/user";
|
||||
import { Room } from "./models/room";
|
||||
import { ToDeviceBatch } from "./models/ToDeviceMessage";
|
||||
import { ToDeviceBatch, ToDevicePayload } from "./models/ToDeviceMessage";
|
||||
import { DeviceInfo } from "./crypto/deviceinfo";
|
||||
import { IOlmDevice } from "./crypto/algorithms/megolm";
|
||||
import { MapWithDefault, recursiveMapToObject } from "./utils";
|
||||
|
||||
interface IStateEventRequest {
|
||||
eventType: string;
|
||||
@@ -234,35 +235,32 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
return await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId);
|
||||
}
|
||||
|
||||
public async sendToDevice(
|
||||
eventType: string,
|
||||
contentMap: { [userId: string]: { [deviceId: string]: Record<string, any> } },
|
||||
): Promise<{}> {
|
||||
await this.widgetApi.sendToDevice(eventType, false, contentMap);
|
||||
public async sendToDevice(eventType: string, contentMap: SendToDeviceContentMap): Promise<{}> {
|
||||
await this.widgetApi.sendToDevice(eventType, false, recursiveMapToObject(contentMap));
|
||||
return {};
|
||||
}
|
||||
|
||||
public async queueToDevice({ eventType, batch }: ToDeviceBatch): Promise<void> {
|
||||
const contentMap: { [userId: string]: { [deviceId: string]: object } } = {};
|
||||
// map: user Id → device Id → payload
|
||||
const contentMap: MapWithDefault<string, Map<string, ToDevicePayload>> = new MapWithDefault(() => new Map());
|
||||
for (const { userId, deviceId, payload } of batch) {
|
||||
if (!contentMap[userId]) contentMap[userId] = {};
|
||||
contentMap[userId][deviceId] = payload;
|
||||
contentMap.getOrCreate(userId).set(deviceId, payload);
|
||||
}
|
||||
|
||||
await this.widgetApi.sendToDevice(eventType, false, contentMap);
|
||||
await this.widgetApi.sendToDevice(eventType, false, recursiveMapToObject(contentMap));
|
||||
}
|
||||
|
||||
public async encryptAndSendToDevices(userDeviceInfoArr: IOlmDevice<DeviceInfo>[], payload: object): Promise<void> {
|
||||
const contentMap: { [userId: string]: { [deviceId: string]: object } } = {};
|
||||
// map: user Id → device Id → payload
|
||||
const contentMap: MapWithDefault<string, Map<string, object>> = new MapWithDefault(() => new Map());
|
||||
for (const {
|
||||
userId,
|
||||
deviceInfo: { deviceId },
|
||||
} of userDeviceInfoArr) {
|
||||
if (!contentMap[userId]) contentMap[userId] = {};
|
||||
contentMap[userId][deviceId] = payload;
|
||||
contentMap.getOrCreate(userId).set(deviceId, payload);
|
||||
}
|
||||
|
||||
await this.widgetApi.sendToDevice((payload as { type: string }).type, true, contentMap);
|
||||
await this.widgetApi.sendToDevice((payload as { type: string }).type, true, recursiveMapToObject(contentMap));
|
||||
}
|
||||
|
||||
// Overridden since we get TURN servers automatically over the widget API,
|
||||
|
||||
@@ -70,7 +70,7 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
this.opts.idBaseUrl = url;
|
||||
}
|
||||
|
||||
public idServerRequest<T extends Record<string, unknown>>(
|
||||
public idServerRequest<T extends {} = Record<string, unknown>>(
|
||||
method: Method,
|
||||
path: string,
|
||||
params: Record<string, string | string[]> | undefined,
|
||||
|
||||
@@ -55,11 +55,13 @@ export * from "./@types/requests";
|
||||
export * from "./@types/search";
|
||||
export * from "./models/room-summary";
|
||||
export * as ContentHelpers from "./content-helpers";
|
||||
export * as SecretStorage from "./secret-storage";
|
||||
export type { ICryptoCallbacks } from "./crypto"; // used to be located here
|
||||
export { createNewMatrixCall } from "./webrtc/call";
|
||||
export type { MatrixCall } from "./webrtc/call";
|
||||
export { GroupCallEvent, GroupCallIntent, GroupCallState, GroupCallType } from "./webrtc/groupCall";
|
||||
export type { GroupCall } from "./webrtc/groupCall";
|
||||
export type { CryptoApi } from "./crypto-api";
|
||||
|
||||
let cryptoStoreFactory = (): CryptoStore => new MemoryCryptoStore();
|
||||
|
||||
|
||||
@@ -48,15 +48,17 @@ export const getBeaconInfoIdentifier = (event: MatrixEvent): BeaconIdentifier =>
|
||||
// https://github.com/matrix-org/matrix-spec-proposals/pull/3672
|
||||
export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.New>, BeaconEventHandlerMap> {
|
||||
public readonly roomId: string;
|
||||
private _beaconInfo?: BeaconInfoState;
|
||||
// beaconInfo is assigned by setBeaconInfo in the constructor
|
||||
// ! to make tsc believe it is definitely assigned
|
||||
private _beaconInfo!: BeaconInfoState;
|
||||
private _isLive?: boolean;
|
||||
private livenessWatchTimeout?: ReturnType<typeof setTimeout>;
|
||||
private _latestLocationEvent?: MatrixEvent;
|
||||
|
||||
public constructor(private rootEvent: MatrixEvent) {
|
||||
super();
|
||||
this.setBeaconInfo(this.rootEvent);
|
||||
this.roomId = this.rootEvent.getRoomId()!;
|
||||
this.setBeaconInfo(this.rootEvent);
|
||||
}
|
||||
|
||||
public get isLive(): boolean {
|
||||
@@ -79,7 +81,7 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
|
||||
return this.rootEvent.getType();
|
||||
}
|
||||
|
||||
public get beaconInfo(): BeaconInfoState | undefined {
|
||||
public get beaconInfo(): BeaconInfoState {
|
||||
return this._beaconInfo;
|
||||
}
|
||||
|
||||
@@ -158,9 +160,9 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
|
||||
if (!parsed.uri || !parsed.timestamp) return false; // we won't be able to process these
|
||||
const { timestamp } = parsed;
|
||||
return (
|
||||
this._beaconInfo!.timestamp &&
|
||||
this._beaconInfo.timestamp &&
|
||||
// only include positions that were taken inside the beacon's live period
|
||||
isTimestampInDuration(this._beaconInfo!.timestamp, this._beaconInfo!.timeout, timestamp) &&
|
||||
isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) &&
|
||||
// ignore positions older than our current latest location
|
||||
(!this.latestLocationState || timestamp > this.latestLocationState.timestamp!)
|
||||
);
|
||||
@@ -196,9 +198,9 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
|
||||
? this.beaconInfo.timestamp! - 360000 /* 6min */
|
||||
: this.beaconInfo.timestamp;
|
||||
this._isLive =
|
||||
!!this._beaconInfo?.live &&
|
||||
!!this._beaconInfo.live &&
|
||||
!!startTimestamp &&
|
||||
isTimestampInDuration(startTimestamp, this._beaconInfo?.timeout, Date.now());
|
||||
isTimestampInDuration(startTimestamp, this._beaconInfo.timeout, Date.now());
|
||||
|
||||
if (prevLiveness !== this.isLive) {
|
||||
this.emit(BeaconEvent.LivenessChange, this.isLive, this);
|
||||
|
||||
+10
-17
@@ -48,6 +48,8 @@ export interface IContent {
|
||||
"avatar_url"?: string;
|
||||
"displayname"?: string;
|
||||
"m.relates_to"?: IEventRelation;
|
||||
|
||||
"org.matrix.msc3952.mentions"?: IMentions;
|
||||
}
|
||||
|
||||
type StrippedState = Required<Pick<IEvent, "content" | "state_key" | "type" | "sender">>;
|
||||
@@ -114,6 +116,11 @@ export interface IEventRelation {
|
||||
"key"?: string;
|
||||
}
|
||||
|
||||
export interface IMentions {
|
||||
user_ids?: string[];
|
||||
room?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* When an event is a visibility change event, as per MSC3531,
|
||||
* the visibility change implied by the event.
|
||||
@@ -576,13 +583,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
}
|
||||
|
||||
public get replyEventId(): string | undefined {
|
||||
// We're prefer ev.getContent() over ev.getWireContent() to make sure
|
||||
// we grab the latest edit with potentially new relations. But we also
|
||||
// can't just rely on ev.getContent() by itself because historically we
|
||||
// still show the reply from the original message even though the edit
|
||||
// event does not include the relation reply.
|
||||
const mRelatesTo = this.getContent()["m.relates_to"] || this.getWireContent()["m.relates_to"];
|
||||
return mRelatesTo?.["m.in_reply_to"]?.event_id;
|
||||
return this.getWireContent()["m.relates_to"]?.["m.in_reply_to"]?.event_id;
|
||||
}
|
||||
|
||||
public get relationEventId(): string | undefined {
|
||||
@@ -799,22 +800,14 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* @returns array of recipients
|
||||
*/
|
||||
public getKeyRequestRecipients(userId: string): IKeyRequestRecipient[] {
|
||||
// send the request to all of our own devices, and the
|
||||
// original sending device if it wasn't us.
|
||||
const wireContent = this.getWireContent();
|
||||
// send the request to all of our own devices
|
||||
const recipients = [
|
||||
{
|
||||
userId,
|
||||
deviceId: "*",
|
||||
},
|
||||
];
|
||||
const sender = this.getSender();
|
||||
if (sender !== userId) {
|
||||
recipients.push({
|
||||
userId: sender!,
|
||||
deviceId: wireContent.device_id,
|
||||
});
|
||||
}
|
||||
|
||||
return recipients;
|
||||
}
|
||||
|
||||
|
||||
+26
-21
@@ -16,7 +16,6 @@ import {
|
||||
MAIN_ROOM_TIMELINE,
|
||||
Receipt,
|
||||
ReceiptCache,
|
||||
Receipts,
|
||||
ReceiptType,
|
||||
WrappedReceipt,
|
||||
} from "../@types/read_receipts";
|
||||
@@ -25,6 +24,7 @@ import * as utils from "../utils";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { EventType } from "../@types/event";
|
||||
import { EventTimelineSet } from "./event-timeline-set";
|
||||
import { MapWithDefault } from "../utils";
|
||||
import { NotificationCountType } from "./room";
|
||||
|
||||
export function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent {
|
||||
@@ -56,8 +56,11 @@ export abstract class ReadReceipt<
|
||||
// the form of this structure. This is sub-optimal for the exposed APIs
|
||||
// which pass in an event ID and get back some receipts, so we also store
|
||||
// a pre-cached list for this purpose.
|
||||
private receipts: Receipts = {}; // { receipt_type: { user_id: Receipt } }
|
||||
private receiptCacheByEventId: ReceiptCache = {}; // { event_id: CachedReceipt[] }
|
||||
// Map: receipt type → user Id → receipt
|
||||
private receipts = new MapWithDefault<string, Map<string, [WrappedReceipt | null, WrappedReceipt | null]>>(
|
||||
() => new Map(),
|
||||
);
|
||||
private receiptCacheByEventId: ReceiptCache = new Map();
|
||||
|
||||
public abstract getUnfilteredTimelineSet(): EventTimelineSet;
|
||||
public abstract timeline: MatrixEvent[];
|
||||
@@ -74,7 +77,7 @@ export abstract class ReadReceipt<
|
||||
ignoreSynthesized = false,
|
||||
receiptType = ReceiptType.Read,
|
||||
): WrappedReceipt | null {
|
||||
const [realReceipt, syntheticReceipt] = this.receipts[receiptType]?.[userId] ?? [];
|
||||
const [realReceipt, syntheticReceipt] = this.receipts.get(receiptType)?.get(userId) ?? [null, null];
|
||||
if (ignoreSynthesized) {
|
||||
return realReceipt;
|
||||
}
|
||||
@@ -126,14 +129,13 @@ export abstract class ReadReceipt<
|
||||
receipt: Receipt,
|
||||
synthetic: boolean,
|
||||
): void {
|
||||
if (!this.receipts[receiptType]) {
|
||||
this.receipts[receiptType] = {};
|
||||
}
|
||||
if (!this.receipts[receiptType][userId]) {
|
||||
this.receipts[receiptType][userId] = [null, null];
|
||||
}
|
||||
const receiptTypesMap = this.receipts.getOrCreate(receiptType);
|
||||
let pair = receiptTypesMap.get(userId);
|
||||
|
||||
const pair = this.receipts[receiptType][userId];
|
||||
if (!pair) {
|
||||
pair = [null, null];
|
||||
receiptTypesMap.set(userId, pair);
|
||||
}
|
||||
|
||||
let existingReceipt = pair[ReceiptPairRealIndex];
|
||||
if (synthetic) {
|
||||
@@ -185,23 +187,26 @@ export abstract class ReadReceipt<
|
||||
if (cachedReceipt === newCachedReceipt) return;
|
||||
|
||||
// clean up any previous cache entry
|
||||
if (cachedReceipt && this.receiptCacheByEventId[cachedReceipt.eventId]) {
|
||||
if (cachedReceipt && this.receiptCacheByEventId.get(cachedReceipt.eventId)) {
|
||||
const previousEventId = cachedReceipt.eventId;
|
||||
// Remove the receipt we're about to clobber out of existence from the cache
|
||||
this.receiptCacheByEventId[previousEventId] = this.receiptCacheByEventId[previousEventId].filter((r) => {
|
||||
return r.type !== receiptType || r.userId !== userId;
|
||||
});
|
||||
this.receiptCacheByEventId.set(
|
||||
previousEventId,
|
||||
this.receiptCacheByEventId.get(previousEventId)!.filter((r) => {
|
||||
return r.type !== receiptType || r.userId !== userId;
|
||||
}),
|
||||
);
|
||||
|
||||
if (this.receiptCacheByEventId[previousEventId].length < 1) {
|
||||
delete this.receiptCacheByEventId[previousEventId]; // clean up the cache keys
|
||||
if (this.receiptCacheByEventId.get(previousEventId)!.length < 1) {
|
||||
this.receiptCacheByEventId.delete(previousEventId); // clean up the cache keys
|
||||
}
|
||||
}
|
||||
|
||||
// cache the new one
|
||||
if (!this.receiptCacheByEventId[eventId]) {
|
||||
this.receiptCacheByEventId[eventId] = [];
|
||||
if (!this.receiptCacheByEventId.get(eventId)) {
|
||||
this.receiptCacheByEventId.set(eventId, []);
|
||||
}
|
||||
this.receiptCacheByEventId[eventId].push({
|
||||
this.receiptCacheByEventId.get(eventId)!.push({
|
||||
userId: userId,
|
||||
type: receiptType as ReceiptType,
|
||||
data: receipt,
|
||||
@@ -215,7 +220,7 @@ export abstract class ReadReceipt<
|
||||
* an empty list.
|
||||
*/
|
||||
public getReceiptsForEvent(event: MatrixEvent): CachedReceipt[] {
|
||||
return this.receiptCacheByEventId[event.getId()!] || [];
|
||||
return this.receiptCacheByEventId.get(event.getId()!) || [];
|
||||
}
|
||||
|
||||
public abstract addReceipt(event: MatrixEvent, synthetic: boolean): void;
|
||||
|
||||
+33
-18
@@ -467,7 +467,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
this.emit(RoomStateEvent.Update, this);
|
||||
}
|
||||
|
||||
public processBeaconEvents(events: MatrixEvent[], matrixClient: MatrixClient): void {
|
||||
public async processBeaconEvents(events: MatrixEvent[], matrixClient: MatrixClient): Promise<void> {
|
||||
if (
|
||||
!events.length ||
|
||||
// discard locations if we have no beacons
|
||||
@@ -476,10 +476,10 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
return;
|
||||
}
|
||||
|
||||
const beaconByEventIdDict: Record<string, Beacon> = [...this.beacons.values()].reduce(
|
||||
(dict, beacon) => ({ ...dict, [beacon.beaconInfoId]: beacon }),
|
||||
{},
|
||||
);
|
||||
const beaconByEventIdDict = [...this.beacons.values()].reduce<Record<string, Beacon>>((dict, beacon) => {
|
||||
dict[beacon.beaconInfoId] = beacon;
|
||||
return dict;
|
||||
}, {});
|
||||
|
||||
const processBeaconRelation = (beaconInfoEventId: string, event: MatrixEvent): void => {
|
||||
if (!M_BEACON.matches(event.getType())) {
|
||||
@@ -493,22 +493,24 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
}
|
||||
};
|
||||
|
||||
events.forEach((event: MatrixEvent) => {
|
||||
for (const event of events) {
|
||||
const relatedToEventId = event.getRelation()?.event_id;
|
||||
// not related to a beacon we know about; discard
|
||||
if (!relatedToEventId || !beaconByEventIdDict[relatedToEventId]) return;
|
||||
if (!M_BEACON.matches(event.getType()) && !event.isEncrypted()) return;
|
||||
|
||||
matrixClient.decryptEventIfNeeded(event);
|
||||
|
||||
if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
|
||||
// add an event listener for once the event is decrypted.
|
||||
event.once(MatrixEventEvent.Decrypted, async () => {
|
||||
processBeaconRelation(relatedToEventId, event);
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
await matrixClient.decryptEventIfNeeded(event);
|
||||
processBeaconRelation(relatedToEventId, event);
|
||||
} catch {
|
||||
if (event.isDecryptionFailure()) {
|
||||
// add an event listener for once the event is decrypted.
|
||||
event.once(MatrixEventEvent.Decrypted, async () => {
|
||||
processBeaconRelation(relatedToEventId, event);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -970,13 +972,21 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
* @param msc3946ProcessDynamicPredecessor - if true, look for an
|
||||
* m.room.predecessor state event and use it if found (MSC3946).
|
||||
* @returns null if this room has no predecessor. Otherwise, returns
|
||||
* the roomId and last eventId of the predecessor room.
|
||||
* the roomId, last eventId and viaServers of the predecessor room.
|
||||
*
|
||||
* If msc3946ProcessDynamicPredecessor is true, use m.predecessor events
|
||||
* as well as m.room.create events to find predecessors.
|
||||
*
|
||||
* Note: if an m.predecessor event is used, eventId may be undefined
|
||||
* since last_known_event_id is optional.
|
||||
*
|
||||
* Note: viaServers may be undefined, and will definitely be undefined if
|
||||
* this predecessor comes from a RoomCreate event (rather than a
|
||||
* RoomPredecessor, which has the optional via_servers property).
|
||||
*/
|
||||
public findPredecessor(msc3946ProcessDynamicPredecessor = false): { roomId: string; eventId?: string } | null {
|
||||
public findPredecessor(
|
||||
msc3946ProcessDynamicPredecessor = false,
|
||||
): { roomId: string; eventId?: string; viaServers?: string[] } | null {
|
||||
// Note: the tests for this function are against Room.findPredecessor,
|
||||
// which just calls through to here.
|
||||
|
||||
@@ -986,14 +996,19 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
const content = predecessorEvent.getContent<{
|
||||
predecessor_room_id: string;
|
||||
last_known_event_id?: string;
|
||||
via_servers?: string[];
|
||||
}>();
|
||||
const roomId = content.predecessor_room_id;
|
||||
let eventId = content.last_known_event_id;
|
||||
if (typeof eventId !== "string") {
|
||||
eventId = undefined;
|
||||
}
|
||||
let viaServers = content.via_servers;
|
||||
if (!Array.isArray(viaServers)) {
|
||||
viaServers = undefined;
|
||||
}
|
||||
if (typeof roomId === "string") {
|
||||
return { roomId, eventId };
|
||||
return { roomId, eventId, viaServers };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
-21
@@ -25,7 +25,7 @@ import {
|
||||
import { Direction, EventTimeline } from "./event-timeline";
|
||||
import { getHttpUriForMxc } from "../content-repo";
|
||||
import * as utils from "../utils";
|
||||
import { normalize } from "../utils";
|
||||
import { normalize, noUnsafeEventProps } from "../utils";
|
||||
import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event";
|
||||
import { EventStatus } from "./event-status";
|
||||
import { RoomMember } from "./room-member";
|
||||
@@ -311,7 +311,7 @@ export type RoomEventHandlerMap = {
|
||||
|
||||
export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
public readonly reEmitter: TypedReEmitter<RoomEmittedEvents, RoomEventHandlerMap>;
|
||||
private txnToEvent: Record<string, MatrixEvent> = {}; // Pending in-flight requests { string: MatrixEvent }
|
||||
private txnToEvent: Map<string, MatrixEvent> = new Map(); // Pending in-flight requests { string: MatrixEvent }
|
||||
private notificationCounts: NotificationCount = {};
|
||||
private readonly threadNotifications = new Map<string, NotificationCount>();
|
||||
public readonly cachedThreadReadReceipts = new Map<string, CachedReceiptStructure[]>();
|
||||
@@ -356,7 +356,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* accountData Dict of per-room account_data events; the keys are the
|
||||
* event type and the values are the events.
|
||||
*/
|
||||
public accountData: Record<string, MatrixEvent> = {}; // $eventType: $event
|
||||
public accountData: Map<string, MatrixEvent> = new Map(); // $eventType: $event
|
||||
/**
|
||||
* The room summary.
|
||||
*/
|
||||
@@ -902,7 +902,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
rawMembersEvents = await this.loadMembersFromServer();
|
||||
logger.log(`LL: got ${rawMembersEvents.length} ` + `members from server for room ${this.roomId}`);
|
||||
}
|
||||
const memberEvents = rawMembersEvents.map(this.client.getEventMapper());
|
||||
const memberEvents = rawMembersEvents.filter(noUnsafeEventProps).map(this.client.getEventMapper());
|
||||
return { memberEvents, fromServer };
|
||||
}
|
||||
|
||||
@@ -2028,8 +2028,11 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
};
|
||||
}
|
||||
|
||||
const parentEventId = event.getAssociatedId()!;
|
||||
const parentEvent = this.findEventById(parentEventId) ?? events?.find((e) => e.getId() === parentEventId);
|
||||
const parentEventId = event.getAssociatedId();
|
||||
let parentEvent: MatrixEvent | undefined;
|
||||
if (parentEventId) {
|
||||
parentEvent = this.findEventById(parentEventId) ?? events?.find((e) => e.getId() === parentEventId);
|
||||
}
|
||||
|
||||
// Treat relations and redactions as extensions of their parents so evaluate parentEvent instead
|
||||
if (parentEvent && (event.isRelation() || event.isRedaction())) {
|
||||
@@ -2151,14 +2154,17 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
// a reference to the cached receipts anymore.
|
||||
this.cachedThreadReadReceipts.delete(threadId);
|
||||
|
||||
// If we managed to create a thread and figure out its `id` then we can use it
|
||||
// This has to happen before thread.addEvents, because that adds events to the eventtimeline, and the
|
||||
// eventtimeline sometimes looks up thread information via the room.
|
||||
this.threads.set(thread.id, thread);
|
||||
|
||||
// This is necessary to be able to jump to events in threads:
|
||||
// If we jump to an event in a thread where neither the event, nor the root,
|
||||
// nor any thread event are loaded yet, we'll load the event as well as the thread root, create the thread,
|
||||
// and pass the event through this.
|
||||
thread.addEvents(events, false);
|
||||
|
||||
// If we managed to create a thread and figure out its `id` then we can use it
|
||||
this.threads.set(thread.id, thread);
|
||||
this.reEmitter.reEmit(thread, [
|
||||
ThreadEvent.Delete,
|
||||
ThreadEvent.Update,
|
||||
@@ -2249,8 +2255,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
const txnId = event.getUnsigned().transaction_id;
|
||||
if (!txnId && event.getSender() === this.myUserId) {
|
||||
// check the txn map for a matching event ID
|
||||
for (const tid in this.txnToEvent) {
|
||||
const localEvent = this.txnToEvent[tid];
|
||||
for (const [tid, localEvent] of this.txnToEvent) {
|
||||
if (localEvent.getId() === event.getId()) {
|
||||
logger.debug("processLiveEvent: found sent event without txn ID: ", tid, event.getId());
|
||||
// update the unsigned field so we can re-use the same codepaths
|
||||
@@ -2325,7 +2330,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
throw new Error("addPendingEvent called on an event with status " + event.status);
|
||||
}
|
||||
|
||||
if (this.txnToEvent[txnId]) {
|
||||
if (this.txnToEvent.get(txnId)) {
|
||||
throw new Error("addPendingEvent called on an event with known txnId " + txnId);
|
||||
}
|
||||
|
||||
@@ -2334,7 +2339,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
// on the unfiltered timelineSet.
|
||||
EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS)!, false);
|
||||
|
||||
this.txnToEvent[txnId] = event;
|
||||
this.txnToEvent.set(txnId, event);
|
||||
if (this.pendingEventList) {
|
||||
if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
|
||||
logger.warn("Setting event as NOT_SENT due to messages in the same state");
|
||||
@@ -2426,8 +2431,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
this.relations.aggregateChildEvent(event);
|
||||
}
|
||||
|
||||
public getEventForTxnId(txnId: string): MatrixEvent {
|
||||
return this.txnToEvent[txnId];
|
||||
public getEventForTxnId(txnId: string): MatrixEvent | undefined {
|
||||
return this.txnToEvent.get(txnId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2454,7 +2459,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
logger.debug(`Got remote echo for event ${oldEventId} -> ${newEventId} old status ${oldStatus}`);
|
||||
|
||||
// no longer pending
|
||||
delete this.txnToEvent[remoteEvent.getUnsigned().transaction_id!];
|
||||
this.txnToEvent.delete(remoteEvent.getUnsigned().transaction_id!);
|
||||
|
||||
// if it's in the pending list, remove it
|
||||
if (this.pendingEventList) {
|
||||
@@ -2467,6 +2472,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
|
||||
const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(remoteEvent);
|
||||
const thread = threadId ? this.getThread(threadId) : null;
|
||||
thread?.setEventMetadata(localEvent);
|
||||
thread?.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
|
||||
|
||||
if (shouldLiveInRoom) {
|
||||
@@ -2548,6 +2554,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
|
||||
const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(event);
|
||||
const thread = threadId ? this.getThread(threadId) : undefined;
|
||||
thread?.setEventMetadata(event);
|
||||
thread?.timelineSet.replaceEventId(oldEventId, newEventId!);
|
||||
|
||||
if (shouldLiveInRoom) {
|
||||
@@ -2665,7 +2672,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
this.processLiveEvent(event);
|
||||
|
||||
if (event.getUnsigned().transaction_id) {
|
||||
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id!];
|
||||
const existingEvent = this.txnToEvent.get(event.getUnsigned().transaction_id!);
|
||||
if (existingEvent) {
|
||||
// remote echo of an event we sent earlier
|
||||
this.handleRemoteEcho(event, existingEvent);
|
||||
@@ -2934,8 +2941,9 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
if (event.getType() === "m.tag") {
|
||||
this.addTags(event);
|
||||
}
|
||||
const lastEvent = this.accountData[event.getType()];
|
||||
this.accountData[event.getType()] = event;
|
||||
const eventType = event.getType();
|
||||
const lastEvent = this.accountData.get(eventType);
|
||||
this.accountData.set(eventType, event);
|
||||
this.emit(RoomEvent.AccountData, event, this, lastEvent);
|
||||
}
|
||||
}
|
||||
@@ -2946,7 +2954,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* @returns the account_data event in question
|
||||
*/
|
||||
public getAccountData(type: EventType | string): MatrixEvent | undefined {
|
||||
return this.accountData[type];
|
||||
return this.accountData.get(type);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3049,13 +3057,21 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* @param msc3946ProcessDynamicPredecessor - if true, look for an
|
||||
* m.room.predecessor state event and use it if found (MSC3946).
|
||||
* @returns null if this room has no predecessor. Otherwise, returns
|
||||
* the roomId and last eventId of the predecessor room.
|
||||
* the roomId, last eventId and viaServers of the predecessor room.
|
||||
*
|
||||
* If msc3946ProcessDynamicPredecessor is true, use m.predecessor events
|
||||
* as well as m.room.create events to find predecessors.
|
||||
*
|
||||
* Note: if an m.predecessor event is used, eventId may be undefined
|
||||
* since last_known_event_id is optional.
|
||||
*
|
||||
* Note: viaServers may be undefined, and will definitely be undefined if
|
||||
* this predecessor comes from a RoomCreate event (rather than a
|
||||
* RoomPredecessor, which has the optional via_servers property).
|
||||
*/
|
||||
public findPredecessor(msc3946ProcessDynamicPredecessor = false): { roomId: string; eventId?: string } | null {
|
||||
public findPredecessor(
|
||||
msc3946ProcessDynamicPredecessor = false,
|
||||
): { roomId: string; eventId?: string; viaServers?: string[] } | null {
|
||||
const currentState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
if (!currentState) {
|
||||
return null;
|
||||
|
||||
+259
-12
@@ -25,6 +25,8 @@ import {
|
||||
ICallStartedPrefixCondition,
|
||||
IContainsDisplayNameCondition,
|
||||
IEventMatchCondition,
|
||||
IEventPropertyIsCondition,
|
||||
IEventPropertyContainsCondition,
|
||||
IPushRule,
|
||||
IPushRules,
|
||||
IRoomMemberCountCondition,
|
||||
@@ -34,6 +36,7 @@ import {
|
||||
PushRuleCondition,
|
||||
PushRuleKind,
|
||||
PushRuleSet,
|
||||
RuleId,
|
||||
TweakName,
|
||||
} from "./@types/PushRules";
|
||||
import { EventType } from "./@types/event";
|
||||
@@ -68,6 +71,36 @@ const DEFAULT_OVERRIDE_RULES: IPushRule[] = [
|
||||
],
|
||||
actions: [PushRuleActionName.DontNotify],
|
||||
},
|
||||
{
|
||||
rule_id: RuleId.IsUserMention,
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.EventPropertyContains,
|
||||
key: "content.org\\.matrix\\.msc3952\\.mentions.user_ids",
|
||||
value: "", // The user ID is dynamically added in rewriteDefaultRules.
|
||||
},
|
||||
],
|
||||
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight }],
|
||||
},
|
||||
{
|
||||
rule_id: RuleId.IsRoomMention,
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.EventPropertyIs,
|
||||
key: "content.org\\.matrix\\.msc3952\\.mentions.room",
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
kind: ConditionKind.SenderNotificationPermission,
|
||||
key: "room",
|
||||
},
|
||||
],
|
||||
actions: [PushRuleActionName.Notify, { set_tweak: TweakName.Highlight }],
|
||||
},
|
||||
{
|
||||
// For homeservers which don't support MSC3786 yet
|
||||
rule_id: ".org.matrix.msc3786.rule.room.server_acl",
|
||||
@@ -123,6 +156,12 @@ export class PushProcessor {
|
||||
*/
|
||||
public constructor(private readonly client: MatrixClient) {}
|
||||
|
||||
/**
|
||||
* Maps the original key from the push rules to a list of property names
|
||||
* after unescaping.
|
||||
*/
|
||||
private readonly parsedKeys = new Map<string, string[]>();
|
||||
|
||||
/**
|
||||
* Convert a list of actions into a object with the actions as keys and their values
|
||||
* @example
|
||||
@@ -152,9 +191,10 @@ export class PushProcessor {
|
||||
* where applicable. Useful for upgrading push rules to more strict
|
||||
* conditions when the server is falling behind on defaults.
|
||||
* @param incomingRules - The client's existing push rules
|
||||
* @param userId - The Matrix ID of the client.
|
||||
* @returns The rewritten rules
|
||||
*/
|
||||
public static rewriteDefaultRules(incomingRules: IPushRules): IPushRules {
|
||||
public static rewriteDefaultRules(incomingRules: IPushRules, userId: string | undefined = undefined): IPushRules {
|
||||
let newRules: IPushRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone
|
||||
|
||||
// These lines are mostly to make the tests happy. We shouldn't run into these
|
||||
@@ -162,12 +202,26 @@ export class PushProcessor {
|
||||
if (!newRules) newRules = {} as IPushRules;
|
||||
if (!newRules.global) newRules.global = {} as PushRuleSet;
|
||||
if (!newRules.global.override) newRules.global.override = [];
|
||||
if (!newRules.global.override) newRules.global.underride = [];
|
||||
if (!newRules.global.underride) newRules.global.underride = [];
|
||||
|
||||
// Merge the client-level defaults with the ones from the server
|
||||
const globalOverrides = newRules.global.override;
|
||||
for (const override of DEFAULT_OVERRIDE_RULES) {
|
||||
const existingRule = globalOverrides.find((r) => r.rule_id === override.rule_id);
|
||||
for (const originalOverride of DEFAULT_OVERRIDE_RULES) {
|
||||
const existingRule = globalOverrides.find((r) => r.rule_id === originalOverride.rule_id);
|
||||
|
||||
// Dynamically add the user ID as the value for the is_user_mention rule.
|
||||
let override: IPushRule;
|
||||
if (originalOverride.rule_id === RuleId.IsUserMention) {
|
||||
// If the user ID wasn't provided, skip the rule.
|
||||
if (!userId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
override = JSON.parse(JSON.stringify(originalOverride)); // deep clone
|
||||
override.conditions![0].value = userId;
|
||||
} else {
|
||||
override = originalOverride;
|
||||
}
|
||||
|
||||
if (existingRule) {
|
||||
// Copy over the actions, default, and conditions. Don't touch the user's preference.
|
||||
@@ -202,6 +256,53 @@ export class PushProcessor {
|
||||
return newRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-caches the parsed keys for push rules and cleans out any obsolete cache
|
||||
* entries. Should be called after push rules are updated.
|
||||
* @param newRules - The new push rules.
|
||||
*/
|
||||
public updateCachedPushRuleKeys(newRules: IPushRules): void {
|
||||
// These lines are mostly to make the tests happy. We shouldn't run into these
|
||||
// properties missing in practice.
|
||||
if (!newRules) newRules = {} as IPushRules;
|
||||
if (!newRules.global) newRules.global = {} as PushRuleSet;
|
||||
if (!newRules.global.override) newRules.global.override = [];
|
||||
if (!newRules.global.room) newRules.global.room = [];
|
||||
if (!newRules.global.sender) newRules.global.sender = [];
|
||||
if (!newRules.global.underride) newRules.global.underride = [];
|
||||
|
||||
// Process the 'key' property on event_match conditions pre-cache the
|
||||
// values and clean-out any unused values.
|
||||
const toRemoveKeys = new Set(this.parsedKeys.keys());
|
||||
for (const ruleset of [
|
||||
newRules.global.override,
|
||||
newRules.global.room,
|
||||
newRules.global.sender,
|
||||
newRules.global.underride,
|
||||
]) {
|
||||
for (const rule of ruleset) {
|
||||
if (!rule.conditions) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const condition of rule.conditions) {
|
||||
if (condition.kind !== ConditionKind.EventMatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure we keep this key.
|
||||
toRemoveKeys.delete(condition.key);
|
||||
|
||||
// Pre-process the key.
|
||||
this.parsedKeys.set(condition.key, PushProcessor.partsForDottedKey(condition.key));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Any keys that were previously cached, but are no longer needed should
|
||||
// be removed.
|
||||
toRemoveKeys.forEach((k) => this.parsedKeys.delete(k));
|
||||
}
|
||||
|
||||
private static cachedGlobToRegex: Record<string, RegExp> = {}; // $glob: RegExp
|
||||
|
||||
private matchingRuleFromKindSet(ev: MatrixEvent, kindset: PushRuleSet): IAnnotatedPushRule | null {
|
||||
@@ -284,6 +385,10 @@ export class PushProcessor {
|
||||
switch (cond.kind) {
|
||||
case ConditionKind.EventMatch:
|
||||
return this.eventFulfillsEventMatchCondition(cond, ev);
|
||||
case ConditionKind.EventPropertyIs:
|
||||
return this.eventFulfillsEventPropertyIsCondition(cond, ev);
|
||||
case ConditionKind.EventPropertyContains:
|
||||
return this.eventFulfillsEventPropertyContains(cond, ev);
|
||||
case ConditionKind.ContainsDisplayName:
|
||||
return this.eventFulfillsDisplayNameCondition(cond, ev);
|
||||
case ConditionKind.RoomMemberCount:
|
||||
@@ -382,6 +487,13 @@ export class PushProcessor {
|
||||
return content.body.search(pat) > -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given event matches the push rule condition by fetching
|
||||
* the property from the event and comparing against the condition's glob-based
|
||||
* pattern.
|
||||
* @param cond - The push rule condition to check for a match.
|
||||
* @param ev - The event to check for a match.
|
||||
*/
|
||||
private eventFulfillsEventMatchCondition(cond: IEventMatchCondition, ev: MatrixEvent): boolean {
|
||||
if (!cond.key) {
|
||||
return false;
|
||||
@@ -392,6 +504,9 @@ export class PushProcessor {
|
||||
return false;
|
||||
}
|
||||
|
||||
// XXX This does not match in a case-insensitive manner.
|
||||
//
|
||||
// See https://spec.matrix.org/v1.5/client-server-api/#conditions-1
|
||||
if (cond.value) {
|
||||
return cond.value === val;
|
||||
}
|
||||
@@ -408,6 +523,38 @@ export class PushProcessor {
|
||||
return !!val.match(regex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given event matches the push rule condition by fetching
|
||||
* the property from the event and comparing exactly against the condition's
|
||||
* value.
|
||||
* @param cond - The push rule condition to check for a match.
|
||||
* @param ev - The event to check for a match.
|
||||
*/
|
||||
private eventFulfillsEventPropertyIsCondition(cond: IEventPropertyIsCondition, ev: MatrixEvent): boolean {
|
||||
if (!cond.key || cond.value === undefined) {
|
||||
return false;
|
||||
}
|
||||
return cond.value === this.valueForDottedKey(cond.key, ev);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given event matches the push rule condition by fetching
|
||||
* the property from the event and comparing exactly against the condition's
|
||||
* value.
|
||||
* @param cond - The push rule condition to check for a match.
|
||||
* @param ev - The event to check for a match.
|
||||
*/
|
||||
private eventFulfillsEventPropertyContains(cond: IEventPropertyContainsCondition, ev: MatrixEvent): boolean {
|
||||
if (!cond.key || cond.value === undefined) {
|
||||
return false;
|
||||
}
|
||||
const val = this.valueForDottedKey(cond.key, ev);
|
||||
if (!Array.isArray(val)) {
|
||||
return false;
|
||||
}
|
||||
return val.includes(cond.value);
|
||||
}
|
||||
|
||||
private eventFulfillsCallStartedCondition(
|
||||
_cond: ICallStartedCondition | ICallStartedPrefixCondition,
|
||||
ev: MatrixEvent,
|
||||
@@ -433,28 +580,105 @@ export class PushProcessor {
|
||||
return PushProcessor.cachedGlobToRegex[glob];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the key into the separate fields to search by splitting on
|
||||
* unescaped ".", and then removing any escape characters.
|
||||
*
|
||||
* @param str - The key of the push rule condition: a dotted field.
|
||||
* @returns The unescaped parts to fetch.
|
||||
* @internal
|
||||
*/
|
||||
public static partsForDottedKey(str: string): string[] {
|
||||
const result = [];
|
||||
|
||||
// The current field and whether the previous character was the escape
|
||||
// character (a backslash).
|
||||
let part = "";
|
||||
let escaped = false;
|
||||
|
||||
// Iterate over each character, and decide whether to append to the current
|
||||
// part (following the escape rules) or to start a new part (based on the
|
||||
// field separator).
|
||||
for (const c of str) {
|
||||
// If the previous character was the escape character (a backslash)
|
||||
// then decide what to append to the current part.
|
||||
if (escaped) {
|
||||
if (c === "\\" || c === ".") {
|
||||
// An escaped backslash or dot just gets added.
|
||||
part += c;
|
||||
} else {
|
||||
// A character that shouldn't be escaped gets the backslash prepended.
|
||||
part += "\\" + c;
|
||||
}
|
||||
// This always resets being escaped.
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == ".") {
|
||||
// The field separator creates a new part.
|
||||
result.push(part);
|
||||
part = "";
|
||||
} else if (c == "\\") {
|
||||
// A backslash adds no characters, but starts an escape sequence.
|
||||
escaped = true;
|
||||
} else {
|
||||
// Otherwise, just add the current character.
|
||||
part += c;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the final part is included. If there's an open escape sequence
|
||||
// it should be included.
|
||||
if (escaped) {
|
||||
part += "\\";
|
||||
}
|
||||
result.push(part);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a dotted field and event, fetch the value at that position, if one
|
||||
* exists.
|
||||
*
|
||||
* @param key - The key of the push rule condition: a dotted field to fetch.
|
||||
* @param ev - The matrix event to fetch the field from.
|
||||
* @returns The value at the dotted path given by key.
|
||||
*/
|
||||
private valueForDottedKey(key: string, ev: MatrixEvent): any {
|
||||
const parts = key.split(".");
|
||||
// The key should already have been parsed via updateCachedPushRuleKeys,
|
||||
// but if it hasn't (maybe via an old consumer of the SDK which hasn't
|
||||
// been updated?) then lazily calculate it here.
|
||||
let parts = this.parsedKeys.get(key);
|
||||
if (parts === undefined) {
|
||||
parts = PushProcessor.partsForDottedKey(key);
|
||||
this.parsedKeys.set(key, parts);
|
||||
}
|
||||
let val: any;
|
||||
|
||||
// special-case the first component to deal with encrypted messages
|
||||
const firstPart = parts[0];
|
||||
let currentIndex = 0;
|
||||
if (firstPart === "content") {
|
||||
val = ev.getContent();
|
||||
parts.shift();
|
||||
++currentIndex;
|
||||
} else if (firstPart === "type") {
|
||||
val = ev.getType();
|
||||
parts.shift();
|
||||
++currentIndex;
|
||||
} else {
|
||||
// use the raw event for any other fields
|
||||
val = ev.event;
|
||||
}
|
||||
|
||||
while (parts.length > 0) {
|
||||
const thisPart = parts.shift()!;
|
||||
if (isNullOrUndefined(val[thisPart])) {
|
||||
return null;
|
||||
for (; currentIndex < parts.length; ++currentIndex) {
|
||||
// The previous iteration resulted in null or undefined, bail (and
|
||||
// avoid the type error of attempting to retrieve a property).
|
||||
if (isNullOrUndefined(val)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const thisPart = parts[currentIndex];
|
||||
val = val[thisPart];
|
||||
}
|
||||
return val;
|
||||
@@ -490,6 +714,17 @@ export class PushProcessor {
|
||||
}
|
||||
|
||||
public ruleMatchesEvent(rule: Partial<IPushRule> & Pick<IPushRule, "conditions">, ev: MatrixEvent): boolean {
|
||||
// Disable the deprecated mentions push rules if the new mentions property exists.
|
||||
if (
|
||||
this.client.supportsIntentionalMentions() &&
|
||||
ev.getContent()["org.matrix.msc3952.mentions"] !== undefined &&
|
||||
(rule.rule_id === RuleId.ContainsUserName ||
|
||||
rule.rule_id === RuleId.ContainsDisplayName ||
|
||||
rule.rule_id === RuleId.AtRoomNotification)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !rule.conditions?.some((cond) => !this.eventFulfillsCondition(cond, ev));
|
||||
}
|
||||
|
||||
@@ -507,6 +742,18 @@ export class PushProcessor {
|
||||
* @returns The push rule, or null if no such rule was found
|
||||
*/
|
||||
public getPushRuleById(ruleId: string): IPushRule | null {
|
||||
const result = this.getPushRuleAndKindById(ruleId);
|
||||
return result?.rule ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get one of the users push rules by its ID
|
||||
*
|
||||
* @param ruleId - The ID of the rule to search for
|
||||
* @returns rule The push rule, or null if no such rule was found
|
||||
* @returns kind - The PushRuleKind of the rule to search for
|
||||
*/
|
||||
public getPushRuleAndKindById(ruleId: string): { rule: IPushRule; kind: PushRuleKind } | null {
|
||||
for (const scope of ["global"] as const) {
|
||||
if (this.client.pushRules?.[scope] === undefined) continue;
|
||||
|
||||
@@ -514,7 +761,7 @@ export class PushProcessor {
|
||||
if (this.client.pushRules[scope][kind] === undefined) continue;
|
||||
|
||||
for (const rule of this.client.pushRules[scope][kind]!) {
|
||||
if (rule.rule_id === ruleId) return rule;
|
||||
if (rule.rule_id === ruleId) return { rule, kind };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-23
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2023 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.
|
||||
@@ -25,20 +25,20 @@ import {
|
||||
RendezvousTransport,
|
||||
RendezvousFailureReason,
|
||||
} from "..";
|
||||
import { encodeBase64, decodeBase64 } from "../../crypto/olmlib";
|
||||
import { encodeUnpaddedBase64, decodeBase64 } from "../../crypto/olmlib";
|
||||
import { crypto, subtleCrypto, TextEncoder } from "../../crypto/crypto";
|
||||
import { generateDecimalSas } from "../../crypto/verification/SASDecimal";
|
||||
import { UnstableValue } from "../../NamespacedValue";
|
||||
|
||||
const ECDH_V1 = new UnstableValue(
|
||||
"m.rendezvous.v1.curve25519-aes-sha256",
|
||||
"org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256",
|
||||
const ECDH_V2 = new UnstableValue(
|
||||
"m.rendezvous.v2.curve25519-aes-sha256",
|
||||
"org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256",
|
||||
);
|
||||
|
||||
export interface ECDHv1RendezvousCode extends RendezvousCode {
|
||||
export interface ECDHv2RendezvousCode extends RendezvousCode {
|
||||
rendezvous: {
|
||||
transport: RendezvousTransportDetails;
|
||||
algorithm: typeof ECDH_V1.name | typeof ECDH_V1.altName;
|
||||
algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName;
|
||||
key: string;
|
||||
};
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export interface ECDHv1RendezvousCode extends RendezvousCode {
|
||||
export type MSC3903ECDHPayload = PlainTextPayload | EncryptedPayload;
|
||||
|
||||
export interface PlainTextPayload {
|
||||
algorithm: typeof ECDH_V1.name | typeof ECDH_V1.altName;
|
||||
algorithm: typeof ECDH_V2.name | typeof ECDH_V2.altName;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ async function importKey(key: Uint8Array): Promise<CryptoKey> {
|
||||
* X25519/ECDH key agreement based secure rendezvous channel.
|
||||
* Note that this is UNSTABLE and may have breaking changes without notice.
|
||||
*/
|
||||
export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
|
||||
export class MSC3903ECDHv2RendezvousChannel<T> implements RendezvousChannel<T> {
|
||||
private olmSAS?: SAS;
|
||||
private ourPublicKey: Uint8Array;
|
||||
private aesKey?: CryptoKey;
|
||||
@@ -85,17 +85,17 @@ export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
|
||||
this.ourPublicKey = decodeBase64(this.olmSAS.get_pubkey());
|
||||
}
|
||||
|
||||
public async generateCode(intent: RendezvousIntent): Promise<ECDHv1RendezvousCode> {
|
||||
public async generateCode(intent: RendezvousIntent): Promise<ECDHv2RendezvousCode> {
|
||||
if (this.transport.ready) {
|
||||
throw new Error("Code already generated");
|
||||
}
|
||||
|
||||
await this.transport.send({ algorithm: ECDH_V1.name });
|
||||
await this.transport.send({ algorithm: ECDH_V2.name });
|
||||
|
||||
const rendezvous: ECDHv1RendezvousCode = {
|
||||
const rendezvous: ECDHv2RendezvousCode = {
|
||||
rendezvous: {
|
||||
algorithm: ECDH_V1.name,
|
||||
key: encodeBase64(this.ourPublicKey),
|
||||
algorithm: ECDH_V2.name,
|
||||
key: encodeUnpaddedBase64(this.ourPublicKey),
|
||||
transport: await this.transport.details(),
|
||||
},
|
||||
intent,
|
||||
@@ -123,7 +123,7 @@ export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
|
||||
}
|
||||
const res = rawRes as Partial<PlainTextPayload>;
|
||||
const { key, algorithm } = res;
|
||||
if (!algorithm || !ECDH_V1.matches(algorithm) || !key) {
|
||||
if (!algorithm || !ECDH_V2.matches(algorithm) || !key) {
|
||||
throw new RendezvousError(
|
||||
"Unsupported algorithm: " + algorithm,
|
||||
RendezvousFailureReason.UnsupportedAlgorithm,
|
||||
@@ -134,20 +134,20 @@ export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
|
||||
} else {
|
||||
// send our public key unencrypted
|
||||
await this.transport.send({
|
||||
algorithm: ECDH_V1.name,
|
||||
key: encodeBase64(this.ourPublicKey),
|
||||
algorithm: ECDH_V2.name,
|
||||
key: encodeUnpaddedBase64(this.ourPublicKey),
|
||||
});
|
||||
}
|
||||
|
||||
this.connected = true;
|
||||
|
||||
this.olmSAS.set_their_key(encodeBase64(this.theirPublicKey!));
|
||||
this.olmSAS.set_their_key(encodeUnpaddedBase64(this.theirPublicKey!));
|
||||
|
||||
const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey!;
|
||||
const recipientKey = isInitiator ? this.theirPublicKey! : this.ourPublicKey;
|
||||
let aesInfo = ECDH_V1.name;
|
||||
aesInfo += `|${encodeBase64(initiatorKey)}`;
|
||||
aesInfo += `|${encodeBase64(recipientKey)}`;
|
||||
let aesInfo = ECDH_V2.name;
|
||||
aesInfo += `|${encodeUnpaddedBase64(initiatorKey)}`;
|
||||
aesInfo += `|${encodeUnpaddedBase64(recipientKey)}`;
|
||||
|
||||
const aesKeyBytes = this.olmSAS.generate_bytes(aesInfo, 32);
|
||||
|
||||
@@ -181,8 +181,8 @@ export class MSC3903ECDHv1RendezvousChannel<T> implements RendezvousChannel<T> {
|
||||
);
|
||||
|
||||
return {
|
||||
iv: encodeBase64(iv),
|
||||
ciphertext: encodeBase64(ciphertext),
|
||||
iv: encodeUnpaddedBase64(iv),
|
||||
ciphertext: encodeUnpaddedBase64(ciphertext),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,4 +14,4 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export * from "./MSC3903ECDHv1RendezvousChannel";
|
||||
export * from "./MSC3903ECDHv2RendezvousChannel";
|
||||
|
||||
@@ -54,7 +54,12 @@ export class KeyClaimManager {
|
||||
// The Rust-SDK requires that we only have one getMissingSessions process in flight at once. This little dance
|
||||
// ensures that, by only having one call to ensureSessionsForUsersInner active at once (and making them
|
||||
// queue up in order).
|
||||
const prom = this.currentClaimPromise.finally(() => this.ensureSessionsForUsersInner(userList));
|
||||
const prom = this.currentClaimPromise
|
||||
.catch(() => {
|
||||
// any errors in the previous claim will have been reported already, so there is nothing to do here.
|
||||
// we just throw away the error and start anew.
|
||||
})
|
||||
.then(() => this.ensureSessionsForUsersInner(userList));
|
||||
this.currentClaimPromise = prom;
|
||||
return prom;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { Room } from "../models/room";
|
||||
import { logger, PrefixedLogger } from "../logger";
|
||||
import { KeyClaimManager } from "./KeyClaimManager";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
|
||||
|
||||
/**
|
||||
* RoomEncryptor: responsible for encrypting messages to a given room
|
||||
@@ -38,6 +39,7 @@ export class RoomEncryptor {
|
||||
public constructor(
|
||||
private readonly olmMachine: OlmMachine,
|
||||
private readonly keyClaimManager: KeyClaimManager,
|
||||
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
|
||||
private readonly room: Room,
|
||||
private encryptionSettings: IContent,
|
||||
) {
|
||||
@@ -97,10 +99,31 @@ export class RoomEncryptor {
|
||||
const userList = members.map((u) => new UserId(u.userId));
|
||||
await this.keyClaimManager.ensureSessionsForUsers(userList);
|
||||
|
||||
this.prefixedLogger.debug("Sessions for users are ready; now sharing room key");
|
||||
|
||||
const rustEncryptionSettings = new EncryptionSettings();
|
||||
/* FIXME historyVisibility, rotation, etc */
|
||||
|
||||
await this.olmMachine.shareRoomKey(new RoomId(this.room.roomId), userList, rustEncryptionSettings);
|
||||
const shareMessages = await this.olmMachine.shareRoomKey(
|
||||
new RoomId(this.room.roomId),
|
||||
userList,
|
||||
rustEncryptionSettings,
|
||||
);
|
||||
if (shareMessages) {
|
||||
for (const m of shareMessages) {
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard any existing group session for this room
|
||||
*/
|
||||
public async forceDiscardSession(): Promise<void> {
|
||||
const r = await this.olmMachine.invalidateGroupSession(new RoomId(this.room.roomId));
|
||||
if (r) {
|
||||
this.prefixedLogger.info("Discarded existing group session");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Copyright 2023 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.
|
||||
*/
|
||||
|
||||
/* This file replaces rust-crypto/index.ts when the js-sdk is being built for browserify.
|
||||
*
|
||||
* It is a stub, so that we do not import the whole of the base64'ed wasm artifact into the browserify bundle.
|
||||
* It deliberately does nothing except raise an exception.
|
||||
*/
|
||||
|
||||
import { IHttpOpts, MatrixHttpApi } from "../http-api";
|
||||
|
||||
export async function initRustCrypto(
|
||||
_http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
_userId: string,
|
||||
_deviceId: string,
|
||||
): Promise<Crypto> {
|
||||
throw new Error("Rust crypto is not supported under browserify.");
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user