Compare commits
161 Commits
v23.1.0-rc.2
...
v24.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| c87048bd9f | |||
| 02269f33b7 | |||
| 7f46ae7b97 | |||
| 9a504af18e | |||
| 26663e67fd | |||
| 0cfc67c679 | |||
| 13d95c8219 | |||
| e119dc4e89 | |||
| b4cdc5a923 | |||
| a82e22b5de | |||
| c894d09d8c | |||
| 585ce07260 | |||
| 8cbbdaa239 | |||
| cd526a254d | |||
| e782a2afa3 | |||
| 565339b1fd | |||
| 493203050a | |||
| 41782c4593 | |||
| 86256a4e74 | |||
| 933a0c9909 | |||
| c8a4d9b88a | |||
| 437128d11b | |||
| c18d09fd22 | |||
| 2a5e5e6a59 | |||
| 68e354752b | |||
| d80b7499fd | |||
| 9c8093eb3e | |||
| aec1c11037 | |||
| e2e9986059 | |||
| d70ffdbc02 | |||
| 0f1f1db3d2 | |||
| 77b91a45cb | |||
| fe6add9396 | |||
| 21cc9c3d8a | |||
| 2d59c4647d | |||
| 1f0c6a6dc9 | |||
| c9b502fb0e | |||
| 330fbaccfc | |||
| 937f370655 | |||
| a8ad3ed26d | |||
| decac58a18 | |||
| 1a91ba59a6 | |||
| 89df43a975 | |||
| ad98706db4 | |||
| 195d1730bd | |||
| db4bd907f8 | |||
| cdd7dbbb2b | |||
| 108f157324 | |||
| 8da39ec8f4 | |||
| 182534288c | |||
| f81b7e5e6f | |||
| 6dda9e532d | |||
| 5e17626fe0 | |||
| abc9c9dcb0 | |||
| f346fcb056 | |||
| c67325ba07 | |||
| a063ae8ce7 | |||
| f9e5535492 | |||
| 015d9c5c4f | |||
| b6d40078d9 | |||
| b8a8f4850a | |||
| 1cc23d789c | |||
| 5cf0bb46a4 | |||
| 16672b3d0c | |||
| f61db81961 | |||
| e2a694115f | |||
| 71cf812d24 | |||
| 8a3d7d5671 | |||
| 2a363598dd | |||
| 05bf6428bc | |||
| e492a44dde | |||
| 44d2e47f96 | |||
| 31459a5d63 | |||
| 8f5db463e7 | |||
| 4e8affafcc | |||
| 2800681bb1 | |||
| b2a9e6f12f | |||
| fbd2c97f87 | |||
| 6c6304a620 | |||
| 0c1d5f6b25 | |||
| 1c26dc0233 | |||
| a163a202e7 | |||
| e15cf9976f | |||
| bdc3926417 | |||
| 4f918f684e | |||
| c142232f4d | |||
| c9bc20aa4d | |||
| 5c0cb3a536 | |||
| 415576d0a0 | |||
| 4f9fad66e4 | |||
| ebd9854980 | |||
| cb2fab64d8 | |||
| f446b49e49 | |||
| 22f5e41058 | |||
| fee5a006f1 | |||
| ef51ee28fd | |||
| cb61345780 | |||
| a18d4e226e | |||
| b09b33eb4c | |||
| 5fedc06d7c | |||
| d8c9f6db33 | |||
| 0af5fa0328 | |||
| b328b72cd5 | |||
| ce2a9d7036 | |||
| 66ae985af5 | |||
| 1828e2849c | |||
| b4f8b0fe4f | |||
| 70656e954f | |||
| 40a4c8d954 | |||
| 7ed787b86a | |||
| 1f58ee7f2c | |||
| 2e28e9117a | |||
| a58a36e062 | |||
| 6cf6a0c522 | |||
| 02aa3edda4 | |||
| e04ea02c62 | |||
| 64197bf4db | |||
| c309fe6942 | |||
| 8408f36c12 | |||
| dcd8f91e02 | |||
| cabe14d7e2 | |||
| c019f2bb19 | |||
| baeb4acddf | |||
| 4a6e9a0f8f | |||
| ea5ce8d1d8 | |||
| 41854918a5 | |||
| 495642d041 | |||
| c7210b9e9d | |||
| 83563c7a01 | |||
| 2fcc4811dd | |||
| c392bc455d | |||
| b8711f15fd | |||
| 81f3aef960 | |||
| d6b8332567 | |||
| 85b34b46c5 | |||
| 4179f2978d | |||
| 97df6db49c | |||
| 3e693fab23 | |||
| 1ee487a2ff | |||
| b76e7ca782 | |||
| ddce1bcd28 | |||
| a34d06c7c2 | |||
| 7b10fa367d | |||
| 7f5d7091de | |||
| 96f673ae92 | |||
| 79faee7a67 | |||
| 89d2984432 | |||
| bc78784688 | |||
| eb058edb1b | |||
| 4847d78b42 | |||
| 94f1eda830 | |||
| bc2a182ee9 | |||
| 789aec732a | |||
| 3246114772 | |||
| 39cf1863f1 | |||
| aa1e118f18 | |||
| d23c3cb8b2 | |||
| 9e37980e2d | |||
| de176dbd66 | |||
| ac10b40f67 | |||
| d7bf0f85c0 |
+1
-1
@@ -23,4 +23,4 @@ indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
indent_size = 4
|
||||
|
||||
+21
-1
@@ -1,6 +1,9 @@
|
||||
module.exports = {
|
||||
plugins: ["matrix-org", "import", "jsdoc"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:import/typescript"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/jest", "plugin:import/typescript"],
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
@@ -60,6 +63,23 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
// Disabled tests are a reality for now but as soon as all of the xits are
|
||||
// eliminated, we should enforce this.
|
||||
"jest/no-disabled-tests": "off",
|
||||
// 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": [
|
||||
"error",
|
||||
{
|
||||
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"],
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
||||
@@ -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@e6e25ac3a2b93187502a8be1ef9e9603afc34925 # v2.24.2
|
||||
uses: dawidd6/action-download-artifact@5e780fc7bbd0cac69fc73271ed86edf5dcb72d67 # v2.26.0
|
||||
with:
|
||||
workflow: static_analysis.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -2,13 +2,9 @@ name: Pull Request
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
workflow_call:
|
||||
inputs:
|
||||
labels:
|
||||
type: string
|
||||
default: "T-Defect,T-Deprecation,T-Enhancement,T-Task"
|
||||
required: false
|
||||
description: "No longer used, uses allchange logic now, will be removed at a later date"
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
@@ -19,6 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: matrix-org/allchange@main
|
||||
if: github.event_name != 'merge_group'
|
||||
with:
|
||||
ghToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
requireLabel: true
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Static Analysis
|
||||
on:
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: [develop, master]
|
||||
concurrency:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
name: Tests
|
||||
on:
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: [develop, master]
|
||||
concurrency:
|
||||
@@ -36,18 +38,19 @@ jobs:
|
||||
id: cpu-cores
|
||||
uses: SimenB/github-actions-cpu-cores@v1
|
||||
|
||||
- name: Run tests with coverage and metrics
|
||||
- name: Load metrics reporter
|
||||
id: metrics
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
run: |
|
||||
yarn coverage --ci --reporters github-actions '--reporters=<rootDir>/spec/slowReporter.js' --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
|
||||
echo "extra-reporter='--reporters=<rootDir>/spec/slowReporter.js'" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run tests with coverage
|
||||
if: github.ref != 'refs/heads/develop'
|
||||
- name: Run tests
|
||||
run: |
|
||||
yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }} ./spec/${{ matrix.specs }}
|
||||
yarn 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
|
||||
@@ -59,3 +62,22 @@ jobs:
|
||||
path: |
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
|
||||
matrix-react-sdk:
|
||||
name: Downstream test matrix-react-sdk
|
||||
if: github.event_name == 'merge_group'
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/tests.yml@develop
|
||||
with:
|
||||
disable_coverage: true
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
|
||||
# Hook for branch protection to work outside merge queues
|
||||
downstream:
|
||||
name: Downstream tests
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
needs:
|
||||
- matrix-react-sdk
|
||||
steps:
|
||||
- if: needs.matrix-react-sdk.result != 'skipped' && needs.matrix-react-sdk.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
@@ -19,3 +19,4 @@ out
|
||||
|
||||
.vscode
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
+82
-5
@@ -1,11 +1,85 @@
|
||||
Changes in [23.1.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0-rc.2) (2023-01-12)
|
||||
============================================================================================================
|
||||
Changes in [24.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v24.0.0) (2023-03-28)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Changes for matrix-js-sdk v24.0.0
|
||||
|
||||
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)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Add easy way to determine if the decryption failure is due to "DecryptionError: The sender has disabled encrypting to unverified devices." ([\#3167](https://github.com/matrix-org/matrix-js-sdk/pull/3167)). Contributed by @florianduros.
|
||||
* Polls: expose end event id on poll model ([\#3160](https://github.com/matrix-org/matrix-js-sdk/pull/3160)). Contributed by @kerryarchibald.
|
||||
* Polls: count undecryptable poll relations ([\#3163](https://github.com/matrix-org/matrix-js-sdk/pull/3163)). Contributed by @kerryarchibald.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* 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)).
|
||||
* Clear notifications when we can infer read status from receipts ([\#3139](https://github.com/matrix-org/matrix-js-sdk/pull/3139)). Fixes vector-im/element-web#23991.
|
||||
* Messages sent out of order after one message fails ([\#3131](https://github.com/matrix-org/matrix-js-sdk/pull/3131)). Fixes vector-im/element-web#22885 and vector-im/element-web#18942. Contributed by @justjanne.
|
||||
|
||||
Changes in [23.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.3.0) (2023-02-14)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Element-R: implement encryption of outgoing events ([\#3122](https://github.com/matrix-org/matrix-js-sdk/pull/3122)).
|
||||
* Poll model - page /relations results ([\#3073](https://github.com/matrix-org/matrix-js-sdk/pull/3073)). Contributed by @kerryarchibald.
|
||||
* Poll model - validate end events ([\#3072](https://github.com/matrix-org/matrix-js-sdk/pull/3072)). Contributed by @kerryarchibald.
|
||||
* Handle optional last_known_event_id property in m.predecessor ([\#3119](https://github.com/matrix-org/matrix-js-sdk/pull/3119)). Contributed by @andybalaam.
|
||||
* Add support for stable identifier for fixed MAC in SAS verification ([\#3101](https://github.com/matrix-org/matrix-js-sdk/pull/3101)).
|
||||
* Provide eventId as well as roomId from Room.findPredecessor ([\#3095](https://github.com/matrix-org/matrix-js-sdk/pull/3095)). Contributed by @andybalaam.
|
||||
* MSC3946 Dynamic room predecessors ([\#3042](https://github.com/matrix-org/matrix-js-sdk/pull/3042)). Contributed by @andybalaam.
|
||||
* Poll model ([\#3036](https://github.com/matrix-org/matrix-js-sdk/pull/3036)). Contributed by @kerryarchibald.
|
||||
* Remove video tracks on video mute without renegotiating ([\#3091](https://github.com/matrix-org/matrix-js-sdk/pull/3091)).
|
||||
* Introduces a backwards-compatible API change. `MegolmEncrypter#prepareToEncrypt`'s return type has changed from `void` to `() => void`. ([\#3035](https://github.com/matrix-org/matrix-js-sdk/pull/3035)). Contributed by @clarkf.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Stop the ICE disconnected timer on call terminate ([\#3147](https://github.com/matrix-org/matrix-js-sdk/pull/3147)).
|
||||
* Clear notifications when we can infer read status from receipts ([\#3139](https://github.com/matrix-org/matrix-js-sdk/pull/3139)). Fixes vector-im/element-web#23991.
|
||||
* Messages sent out of order after one message fails ([\#3131](https://github.com/matrix-org/matrix-js-sdk/pull/3131)). Fixes vector-im/element-web#22885 and vector-im/element-web#18942. Contributed by @justjanne.
|
||||
* Element-R: fix a bug which prevented encryption working after a reload ([\#3126](https://github.com/matrix-org/matrix-js-sdk/pull/3126)).
|
||||
* Element-R: Fix invite processing ([\#3121](https://github.com/matrix-org/matrix-js-sdk/pull/3121)).
|
||||
* Don't throw with no `opponentDeviceInfo` ([\#3107](https://github.com/matrix-org/matrix-js-sdk/pull/3107)).
|
||||
* Remove flaky megolm test ([\#3098](https://github.com/matrix-org/matrix-js-sdk/pull/3098)). Contributed by @clarkf.
|
||||
* Fix "verifyLinks" functionality of getRoomUpgradeHistory ([\#3089](https://github.com/matrix-org/matrix-js-sdk/pull/3089)). Contributed by @andybalaam.
|
||||
|
||||
Changes in [23.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.2.0) (2023-01-31)
|
||||
==================================================================================================
|
||||
|
||||
## ✨ Features
|
||||
* Implement decryption via the rust sdk ([\#3074](https://github.com/matrix-org/matrix-js-sdk/pull/3074)).
|
||||
* Handle edits which are bundled with an event, per MSC3925 ([\#3045](https://github.com/matrix-org/matrix-js-sdk/pull/3045)).
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Add null check for our own member event ([\#3082](https://github.com/matrix-org/matrix-js-sdk/pull/3082)).
|
||||
* Handle group call getting initialised twice in quick succession ([\#3078](https://github.com/matrix-org/matrix-js-sdk/pull/3078)). Fixes vector-im/element-call#847.
|
||||
* Correctly handle limited sync responses by resetting the thread timeline ([\#3056](https://github.com/matrix-org/matrix-js-sdk/pull/3056)). Fixes vector-im/element-web#23952. Contributed by @justjanne.
|
||||
* Fix failure to start in firefox private browser ([\#3058](https://github.com/matrix-org/matrix-js-sdk/pull/3058)). Fixes vector-im/element-web#24216.
|
||||
* Fix spurious "Decryption key withheld" messages ([\#3061](https://github.com/matrix-org/matrix-js-sdk/pull/3061)). Fixes vector-im/element-web#23803.
|
||||
* Fix browser entrypoint ([\#3051](https://github.com/matrix-org/matrix-js-sdk/pull/3051)). Fixes #3013.
|
||||
|
||||
Changes in [23.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0-rc.1) (2023-01-11)
|
||||
============================================================================================================
|
||||
Changes in [23.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.1) (2023-01-20)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix backwards compability for environment not support Array.prototype.at ([\#3080](https://github.com/matrix-org/matrix-js-sdk/pull/3080)).
|
||||
|
||||
Changes in [23.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.1.0) (2023-01-18)
|
||||
==================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Remove extensible events v1 field population on legacy events ([\#3040](https://github.com/matrix-org/matrix-js-sdk/pull/3040)).
|
||||
@@ -27,10 +101,13 @@ Changes in [23.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta
|
||||
* Avoid triggering decryption errors when decrypting redacted events ([\#3004](https://github.com/matrix-org/matrix-js-sdk/pull/3004)). Fixes vector-im/element-web#24084.
|
||||
* bugfix: upload OTKs in sliding sync mode ([\#3008](https://github.com/matrix-org/matrix-js-sdk/pull/3008)).
|
||||
* Apply edits discovered from sync after thread is initialised ([\#3002](https://github.com/matrix-org/matrix-js-sdk/pull/3002)). Fixes vector-im/element-web#23921.
|
||||
* sliding sync: Fix issue where no unsubs are sent when switching rooms ([\#2991](https://github.com/matrix-org/matrix-js-sdk/pull/2991)).
|
||||
* Sliding sync: Fix issue where no unsubs are sent when switching rooms ([\#2991](https://github.com/matrix-org/matrix-js-sdk/pull/2991)).
|
||||
* Threads are missing from the timeline ([\#2996](https://github.com/matrix-org/matrix-js-sdk/pull/2996)). Fixes vector-im/element-web#24036.
|
||||
* Close all streams when a call ends ([\#2992](https://github.com/matrix-org/matrix-js-sdk/pull/2992)). Fixes vector-im/element-call#742.
|
||||
* Resume to-device message queue after resumed sync ([\#2920](https://github.com/matrix-org/matrix-js-sdk/pull/2920)). Fixes matrix-org/element-web-rageshakes#17170.
|
||||
* Fix browser entrypoint ([\#3051](https://github.com/matrix-org/matrix-js-sdk/pull/3051)). Fixes #3013.
|
||||
* Fix failure to start in firefox private browser ([\#3058](https://github.com/matrix-org/matrix-js-sdk/pull/3058)). Fixes vector-im/element-web#24216.
|
||||
* Correctly handle limited sync responses by resetting the thread timeline ([\#3056](https://github.com/matrix-org/matrix-js-sdk/pull/3056)). Fixes vector-im/element-web#23952.
|
||||
|
||||
Changes in [23.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v23.0.0) (2022-12-21)
|
||||
==================================================================================================
|
||||
|
||||
+34
-9
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "23.1.0-rc.2",
|
||||
"version": "24.0.0",
|
||||
"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,7 +55,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.2",
|
||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.3",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
@@ -84,11 +84,12 @@
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/domexception": "^4.0.0",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "18",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@types/uuid": "7",
|
||||
"@types/uuid": "9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"allchange": "^1.0.6",
|
||||
@@ -96,26 +97,30 @@
|
||||
"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.29.0",
|
||||
"eslint": "8.34.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-jsdoc": "^39.6.4",
|
||||
"eslint-plugin-matrix-org": "^0.9.0",
|
||||
"eslint-plugin-jest": "^27.1.6",
|
||||
"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",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"jest": "^29.0.0",
|
||||
"jest-environment-jsdom": "^29.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^29.0.0",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"prettier": "2.8.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"prettier": "2.8.4",
|
||||
"rimraf": "^4.0.0",
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typedoc": "^0.23.20",
|
||||
@@ -144,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"
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
||||
|
||||
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
|
||||
# When merging to develop, we need revert the `main` and `typings` fields if we adjusted them previously.
|
||||
for i in main typings
|
||||
for i in main typings browser
|
||||
do
|
||||
# If a `lib` prefixed value is present, it means we adjusted the field
|
||||
# earlier at publish time, so we should revert it now.
|
||||
|
||||
+1
-1
@@ -180,7 +180,7 @@ yarn version --no-git-tag-version --new-version "$release"
|
||||
# they exist). This small bit of gymnastics allows us to use the TypeScript
|
||||
# source directly for development without needing to build before linting or
|
||||
# testing.
|
||||
for i in main typings
|
||||
for i in main typings browser
|
||||
do
|
||||
lib_value=$(jq -r ".matrix_lib_$i" package.json)
|
||||
if [ "$lib_value" != "null" ]; then
|
||||
|
||||
+37
-5
@@ -16,11 +16,16 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// `expect` is allowed in helper functions which are called within `test`/`it` blocks
|
||||
/* eslint-disable jest/no-standalone-expect */
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import "./olm-loader";
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../src/@types/crypto";
|
||||
import type { IE2EKeyReceiver } from "./test-utils/E2EKeyReceiver";
|
||||
import { LocalStorageCryptoStore } from "../src/crypto/store/localStorage-crypto-store";
|
||||
import { logger } from "../src/logger";
|
||||
import { syncPromise } from "./test-utils/test-utils";
|
||||
@@ -28,14 +33,18 @@ import { createClient, IStartClientOpts } from "../src/matrix";
|
||||
import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client";
|
||||
import { MockStorageApi } from "./MockStorageApi";
|
||||
import { encodeUri } from "../src/utils";
|
||||
import { IDeviceKeys, IOneTimeKey } from "../src/crypto/dehydration";
|
||||
import { IKeyBackupSession } from "../src/crypto/keybackup";
|
||||
import { IKeysUploadResponse, IUploadKeysRequest } from "../src/client";
|
||||
import { ISyncResponder } from "./test-utils/SyncResponder";
|
||||
|
||||
/**
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
*
|
||||
* @deprecated Avoid using this; it is tied too tightly to matrix-mock-request and is generally inconvenient to use.
|
||||
* Instead, construct a MatrixClient manually, use fetch-mock-jest to intercept the HTTP requests, and
|
||||
* use things like {@link E2EKeyReceiver} and {@link SyncResponder} to manage the requests.
|
||||
*/
|
||||
export class TestClient {
|
||||
export class TestClient implements IE2EKeyReceiver, ISyncResponder {
|
||||
public readonly httpBackend: MockHttpBackend;
|
||||
public readonly client: MatrixClient;
|
||||
public deviceKeys?: IDeviceKeys | null;
|
||||
@@ -115,13 +124,12 @@ export class TestClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will upload device keys.
|
||||
* Set up expectations that the client will upload device keys (and possibly one-time keys)
|
||||
*/
|
||||
public expectDeviceKeyUpload() {
|
||||
this.httpBackend
|
||||
.when("POST", "/keys/upload")
|
||||
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content) => {
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
expect(content.device_keys).toBeTruthy();
|
||||
|
||||
logger.log(this + ": received device keys");
|
||||
@@ -129,7 +137,17 @@ export class TestClient {
|
||||
expect(Object.keys(this.oneTimeKeys!).length).toEqual(0);
|
||||
|
||||
this.deviceKeys = content.device_keys;
|
||||
return { one_time_key_counts: { signed_curve25519: 0 } };
|
||||
|
||||
// the first batch of one-time keys may be uploaded at the same time.
|
||||
if (content.one_time_keys) {
|
||||
logger.log(`${this}: received ${Object.keys(content.one_time_keys).length} one-time keys`);
|
||||
this.oneTimeKeys = content.one_time_keys;
|
||||
}
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys!).length,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -231,8 +249,22 @@ export class TestClient {
|
||||
return this.deviceKeys!.keys[keyId];
|
||||
}
|
||||
|
||||
/** Next time we see a sync request (or immediately, if there is one waiting), send the given response
|
||||
*
|
||||
* Calling this will register a response for `/sync`, and then, in the background, flush a single `/sync` request.
|
||||
* Try calling {@link syncPromise} to wait for the sync to complete.
|
||||
*
|
||||
* @param response - response to /sync request
|
||||
*/
|
||||
public sendOrQueueSyncResponse(syncResponse: object): void {
|
||||
this.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
this.httpBackend.flush("/sync", 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* flush a single /sync request, and wait for the syncing event
|
||||
*
|
||||
* @deprecated: prefer to use {@link #sendOrQueueSyncResponse} followed by {@link syncPromise}.
|
||||
*/
|
||||
public flushSync(): Promise<void> {
|
||||
logger.log(`${this}: flushSync`);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -175,7 +175,7 @@ describe("MatrixClient events", function () {
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit User events", function (done) {
|
||||
it("should emit User events", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
let fired = false;
|
||||
@@ -192,10 +192,8 @@ describe("MatrixClient events", function () {
|
||||
});
|
||||
client!.startClient();
|
||||
|
||||
httpBackend!.flushAllExpected().then(function () {
|
||||
expect(fired).toBe(true);
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(fired).toBe(true);
|
||||
});
|
||||
|
||||
it("should emit Room events", function () {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 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.
|
||||
@@ -590,7 +590,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("should handle thread replies with server support by fetching a contiguous thread timeline", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
await client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
@@ -647,7 +647,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("should return relevant timeline from non-thread timelineSet when asking for the thread root", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
@@ -680,7 +680,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("should return undefined when event is not in the thread that the given timelineSet is representing", () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
@@ -709,7 +709,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("should return undefined when event is within a thread but timelineSet is not", () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
@@ -1127,7 +1127,7 @@ describe("MatrixClient event timelines", function () {
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
@@ -1263,7 +1263,7 @@ describe("MatrixClient event timelines", function () {
|
||||
describe("with server compatibility", function () {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
@@ -1421,7 +1421,7 @@ describe("MatrixClient event timelines", function () {
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
@@ -1473,7 +1473,7 @@ describe("MatrixClient event timelines", function () {
|
||||
describe("without server compatibility", function () {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.None);
|
||||
});
|
||||
@@ -1539,7 +1539,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("should add lazy loading filter", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
// @ts-ignore
|
||||
@@ -1567,7 +1567,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("should correctly pass pagination token", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
|
||||
@@ -1892,7 +1892,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("in stable mode", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
@@ -1902,7 +1902,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("in backwards compatible unstable mode", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Experimental);
|
||||
@@ -1912,7 +1912,7 @@ describe("MatrixClient event timelines", function () {
|
||||
|
||||
it("in backwards compatible mode", async () => {
|
||||
// @ts-ignore
|
||||
client.clientOpts.experimentalThreadSupport = true;
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Experimental);
|
||||
Thread.setServerSideListSupport(FeatureSupport.None);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.None);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 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.
|
||||
@@ -35,7 +35,7 @@ describe("MatrixClient", function () {
|
||||
let store: MemoryStore | undefined;
|
||||
|
||||
const defaultClientOpts: IStoredClientOpts = {
|
||||
experimentalThreadSupport: false,
|
||||
threadSupport: false,
|
||||
};
|
||||
const setupTests = (): [MatrixClient, HttpBackend, MemoryStore] => {
|
||||
const store = new MemoryStore();
|
||||
@@ -205,19 +205,17 @@ describe("MatrixClient", function () {
|
||||
describe("getFilter", function () {
|
||||
const filterId = "f1lt3r1d";
|
||||
|
||||
it("should return a filter from the store if allowCached", function (done) {
|
||||
it("should return a filter from the store if allowCached", async () => {
|
||||
const filter = Filter.fromJson(userId, filterId, {
|
||||
event_format: "client",
|
||||
});
|
||||
store!.storeFilter(filter);
|
||||
client!.getFilter(userId, filterId, true).then(function (gotFilter) {
|
||||
expect(gotFilter).toEqual(filter);
|
||||
done();
|
||||
});
|
||||
const gotFilter = await client!.getFilter(userId, filterId, true);
|
||||
expect(gotFilter).toEqual(filter);
|
||||
httpBackend!.verifyNoOutstandingRequests();
|
||||
});
|
||||
|
||||
it("should do an HTTP request if !allowCached even if one exists", function (done) {
|
||||
it("should do an HTTP request if !allowCached even if one exists", async () => {
|
||||
const httpFilterDefinition = {
|
||||
event_format: "federation",
|
||||
};
|
||||
@@ -230,15 +228,11 @@ describe("MatrixClient", function () {
|
||||
event_format: "client",
|
||||
});
|
||||
store!.storeFilter(storeFilter);
|
||||
client!.getFilter(userId, filterId, false).then(function (gotFilter) {
|
||||
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
||||
done();
|
||||
});
|
||||
|
||||
httpBackend!.flush("");
|
||||
const [gotFilter] = await Promise.all([client!.getFilter(userId, filterId, false), httpBackend!.flush("")]);
|
||||
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
||||
});
|
||||
|
||||
it("should do an HTTP request if nothing is in the cache and then store it", function (done) {
|
||||
it("should do an HTTP request if nothing is in the cache and then store it", async () => {
|
||||
const httpFilterDefinition = {
|
||||
event_format: "federation",
|
||||
};
|
||||
@@ -247,20 +241,16 @@ describe("MatrixClient", function () {
|
||||
httpBackend!
|
||||
.when("GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId)
|
||||
.respond(200, httpFilterDefinition);
|
||||
client!.getFilter(userId, filterId, true).then(function (gotFilter) {
|
||||
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
||||
expect(store!.getFilter(userId, filterId)).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
|
||||
httpBackend!.flush("");
|
||||
const [gotFilter] = await Promise.all([client!.getFilter(userId, filterId, true), httpBackend!.flush("")]);
|
||||
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
||||
expect(store!.getFilter(userId, filterId)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFilter", function () {
|
||||
const filterId = "f1llllllerid";
|
||||
|
||||
it("should do an HTTP request and then store the filter", function (done) {
|
||||
it("should do an HTTP request and then store the filter", async () => {
|
||||
expect(store!.getFilter(userId, filterId)).toBe(null);
|
||||
|
||||
const filterDefinition = {
|
||||
@@ -276,13 +266,9 @@ describe("MatrixClient", function () {
|
||||
filter_id: filterId,
|
||||
});
|
||||
|
||||
client!.createFilter(filterDefinition).then(function (gotFilter) {
|
||||
expect(gotFilter.getDefinition()).toEqual(filterDefinition);
|
||||
expect(store!.getFilter(userId, filterId)).toEqual(gotFilter);
|
||||
done();
|
||||
});
|
||||
|
||||
httpBackend!.flush("");
|
||||
const [gotFilter] = await Promise.all([client!.createFilter(filterDefinition), httpBackend!.flush("")]);
|
||||
expect(gotFilter.getDefinition()).toEqual(filterDefinition);
|
||||
expect(store!.getFilter(userId, filterId)).toEqual(gotFilter);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -617,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"],
|
||||
@@ -671,7 +657,7 @@ describe("MatrixClient", function () {
|
||||
// @ts-ignore setting private property
|
||||
client!.clientOpts = {
|
||||
...defaultClientOpts,
|
||||
experimentalThreadSupport: true,
|
||||
threadSupport: true,
|
||||
};
|
||||
|
||||
const eventPollResponseReference = buildEventPollResponseReference();
|
||||
@@ -702,7 +688,7 @@ describe("MatrixClient", function () {
|
||||
// @ts-ignore setting private property
|
||||
client!.clientOpts = {
|
||||
...defaultClientOpts,
|
||||
experimentalThreadSupport: true,
|
||||
threadSupport: true,
|
||||
};
|
||||
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
@@ -726,7 +712,7 @@ describe("MatrixClient", function () {
|
||||
// @ts-ignore setting private property
|
||||
client!.clientOpts = {
|
||||
...defaultClientOpts,
|
||||
experimentalThreadSupport: true,
|
||||
threadSupport: true,
|
||||
};
|
||||
|
||||
const eventPollResponseReference = buildEventPollResponseReference();
|
||||
@@ -750,7 +736,7 @@ describe("MatrixClient", function () {
|
||||
// @ts-ignore setting private property
|
||||
client!.clientOpts = {
|
||||
...defaultClientOpts,
|
||||
experimentalThreadSupport: true,
|
||||
threadSupport: true,
|
||||
};
|
||||
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
@@ -774,7 +760,7 @@ describe("MatrixClient", function () {
|
||||
// @ts-ignore setting private property
|
||||
client!.clientOpts = {
|
||||
...defaultClientOpts,
|
||||
experimentalThreadSupport: true,
|
||||
threadSupport: true,
|
||||
};
|
||||
// This is based on recording the events in a real room:
|
||||
|
||||
@@ -831,7 +817,7 @@ describe("MatrixClient", function () {
|
||||
// @ts-ignore setting private property
|
||||
client!.clientOpts = {
|
||||
...defaultClientOpts,
|
||||
experimentalThreadSupport: true,
|
||||
threadSupport: true,
|
||||
};
|
||||
|
||||
const threadRootEvent = buildEventPollStartThreadRoot();
|
||||
@@ -857,7 +843,7 @@ describe("MatrixClient", function () {
|
||||
// @ts-ignore setting private property
|
||||
client!.clientOpts = {
|
||||
...defaultClientOpts,
|
||||
experimentalThreadSupport: true,
|
||||
threadSupport: true,
|
||||
};
|
||||
|
||||
const threadRootEvent = buildEventPollStartThreadRoot();
|
||||
@@ -878,7 +864,7 @@ describe("MatrixClient", function () {
|
||||
// @ts-ignore setting private property
|
||||
client!.clientOpts = {
|
||||
...defaultClientOpts,
|
||||
experimentalThreadSupport: true,
|
||||
threadSupport: true,
|
||||
};
|
||||
|
||||
const threadRootEvent = buildEventPollStartThreadRoot();
|
||||
@@ -1350,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: () => ({
|
||||
@@ -1378,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;
|
||||
});
|
||||
|
||||
@@ -94,16 +94,16 @@ describe("MatrixClient opts", function () {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("should be able to send messages", function (done) {
|
||||
it("should be able to send messages", async () => {
|
||||
const eventId = "$flibble:wibble";
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
});
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
|
||||
expect(res.event_id).toEqual(eventId);
|
||||
done();
|
||||
});
|
||||
httpBackend.flush("/txn1", 1);
|
||||
const [res] = await Promise.all([
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1"),
|
||||
httpBackend.flush("/txn1", 1),
|
||||
]);
|
||||
expect(res.event_id).toEqual(eventId);
|
||||
});
|
||||
|
||||
it("should be able to sync / get new events", async function () {
|
||||
@@ -149,7 +149,7 @@ describe("MatrixClient opts", function () {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("shouldn't retry sending events", function (done) {
|
||||
it("shouldn't retry sending events", async () => {
|
||||
httpBackend.when("PUT", "/txn1").respond(
|
||||
500,
|
||||
new MatrixError({
|
||||
@@ -157,19 +157,17 @@ describe("MatrixClient opts", function () {
|
||||
error: "Ruh roh",
|
||||
}),
|
||||
);
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(
|
||||
function (res) {
|
||||
expect(false).toBe(true);
|
||||
},
|
||||
function (err) {
|
||||
expect(err.errcode).toEqual("M_SOMETHING");
|
||||
done();
|
||||
},
|
||||
);
|
||||
httpBackend.flush("/txn1", 1);
|
||||
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");
|
||||
}
|
||||
});
|
||||
|
||||
it("shouldn't queue events", function (done) {
|
||||
it("shouldn't queue events", async () => {
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: "AAA",
|
||||
});
|
||||
@@ -178,30 +176,38 @@ describe("MatrixClient opts", function () {
|
||||
});
|
||||
let sentA = false;
|
||||
let sentB = false;
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
|
||||
const messageASendPromise = client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
|
||||
sentA = true;
|
||||
// We expect messageB to be sent before messageA to ensure as we're
|
||||
// testing that there is no queueing that blocks each other
|
||||
expect(sentB).toBe(true);
|
||||
});
|
||||
client.sendTextMessage("!foo:bar", "b body", "txn2").then(function (res) {
|
||||
const messageBSendPromise = client.sendTextMessage("!foo:bar", "b body", "txn2").then(function (res) {
|
||||
sentB = true;
|
||||
// We expect messageB to be sent before messageA to ensure as we're
|
||||
// testing that there is no queueing that blocks each other
|
||||
expect(sentA).toBe(false);
|
||||
});
|
||||
httpBackend.flush("/txn2", 1).then(function () {
|
||||
httpBackend.flush("/txn1", 1).then(function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
// Allow messageB to succeed first
|
||||
await httpBackend.flush("/txn2", 1);
|
||||
// Then allow messageA to succeed
|
||||
await httpBackend.flush("/txn1", 1);
|
||||
|
||||
// Now await the message send promises to
|
||||
await messageBSendPromise;
|
||||
await messageASendPromise;
|
||||
});
|
||||
|
||||
it("should be able to send messages", function (done) {
|
||||
it("should be able to send messages", async () => {
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
event_id: "foo",
|
||||
});
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function (res) {
|
||||
expect(res.event_id).toEqual("foo");
|
||||
done();
|
||||
});
|
||||
httpBackend.flush("/txn1", 1);
|
||||
const [res] = await Promise.all([
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1"),
|
||||
httpBackend.flush("/txn1", 1),
|
||||
]);
|
||||
|
||||
expect(res.event_id).toEqual("foo");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,13 +48,13 @@ describe("MatrixClient retrying", function () {
|
||||
return httpBackend!.stop();
|
||||
});
|
||||
|
||||
xit("should retry according to MatrixScheduler.retryFn", function () {});
|
||||
it.skip("should retry according to MatrixScheduler.retryFn", function () {});
|
||||
|
||||
xit("should queue according to MatrixScheduler.queueFn", function () {});
|
||||
it.skip("should queue according to MatrixScheduler.queueFn", function () {});
|
||||
|
||||
xit("should mark events as EventStatus.NOT_SENT when giving up", function () {});
|
||||
it.skip("should mark events as EventStatus.NOT_SENT when giving up", function () {});
|
||||
|
||||
xit("should mark events as EventStatus.QUEUED when queued", function () {});
|
||||
it.skip("should mark events as EventStatus.QUEUED when queued", function () {});
|
||||
|
||||
it("should mark events as EventStatus.CANCELLED when cancelled", function () {
|
||||
// send a couple of events; the second will be queued
|
||||
@@ -130,7 +130,7 @@ describe("MatrixClient retrying", function () {
|
||||
});
|
||||
|
||||
describe("resending", function () {
|
||||
xit("should be able to resend a NOT_SENT event", function () {});
|
||||
xit("should be able to resend a sent event", function () {});
|
||||
it.skip("should be able to resend a NOT_SENT event", function () {});
|
||||
it.skip("should be able to resend a sent event", function () {});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -163,36 +163,38 @@ describe("MatrixClient room timelines", function () {
|
||||
it(
|
||||
"should be added immediately after calling MatrixClient.sendEvent " +
|
||||
"with EventStatus.SENDING and the right event.sender",
|
||||
function (done) {
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
async () => {
|
||||
const wasMessageAddedPromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, async (state) => {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
// check it was added
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
// check status
|
||||
expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
|
||||
// check member
|
||||
const member = room.timeline[1].sender;
|
||||
expect(member?.userId).toEqual(userId);
|
||||
expect(member?.name).toEqual(userName);
|
||||
client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
// check it was added
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
// check status
|
||||
expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
|
||||
// check member
|
||||
const member = room.timeline[1].sender;
|
||||
expect(member?.userId).toEqual(userId);
|
||||
expect(member?.name).toEqual(userName);
|
||||
|
||||
httpBackend!.flush("/sync", 1).then(function () {
|
||||
done();
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await wasMessageAddedPromise;
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"should be updated correctly when the send request finishes " +
|
||||
"BEFORE the event comes down the event stream",
|
||||
function (done) {
|
||||
async () => {
|
||||
const eventId = "$foo:bar";
|
||||
httpBackend!.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
@@ -207,28 +209,30 @@ describe("MatrixClient room timelines", function () {
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
client!.sendTextMessage(roomId, "I am a fish", "txn1").then(function () {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
httpBackend!.flush("/sync", 1).then(function () {
|
||||
const wasMessageAddedPromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
client!.sendTextMessage(roomId, "I am a fish", "txn1").then(async () => {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
done();
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
resolve(null);
|
||||
});
|
||||
httpBackend!.flush("/txn1", 1);
|
||||
});
|
||||
httpBackend!.flush("/txn1", 1);
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await wasMessageAddedPromise;
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"should be updated correctly when the send request finishes " +
|
||||
"AFTER the event comes down the event stream",
|
||||
function (done) {
|
||||
async () => {
|
||||
const eventId = "$foo:bar";
|
||||
httpBackend!.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
@@ -243,23 +247,24 @@ describe("MatrixClient room timelines", function () {
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
const promise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
httpBackend!.flush("/sync", 1).then(function () {
|
||||
const wasMessageAddedPromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, async (state) => {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
const messageSendPromise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
httpBackend!.flush("/txn1", 1);
|
||||
promise.then(function () {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
done();
|
||||
});
|
||||
await messageSendPromise;
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await wasMessageAddedPromise;
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -279,30 +284,29 @@ describe("MatrixClient room timelines", function () {
|
||||
});
|
||||
});
|
||||
|
||||
it("should set Room.oldState.paginationToken to null at the start" + " of the timeline.", function (done) {
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
it("should set Room.oldState.paginationToken to null at the start of the timeline.", async () => {
|
||||
const didPaginatePromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, async (state) => {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client!.scrollback(room).then(function () {
|
||||
await Promise.all([client!.scrollback(room), httpBackend!.flush("/messages", 1)]);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
expect(room.oldState.paginationToken).toBe(null);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
httpBackend!.flush("/messages", 1);
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await didPaginatePromise;
|
||||
});
|
||||
|
||||
it("should set the right event.sender values", function (done) {
|
||||
it("should set the right event.sender values", async () => {
|
||||
// We're aiming for an eventual timeline of:
|
||||
//
|
||||
// 'Old Alice' joined the room
|
||||
@@ -353,15 +357,17 @@ describe("MatrixClient room timelines", function () {
|
||||
joinMshipEvent,
|
||||
];
|
||||
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
// sync response
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
const didPaginatePromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, async (state) => {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
// sync response
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
await Promise.all([client!.scrollback(room), httpBackend!.flush("/messages", 1)]);
|
||||
|
||||
client!.scrollback(room).then(function () {
|
||||
expect(room.timeline.length).toEqual(5);
|
||||
const joinMsg = room.timeline[0];
|
||||
expect(joinMsg.sender?.name).toEqual("Old Alice");
|
||||
@@ -371,17 +377,15 @@ describe("MatrixClient room timelines", function () {
|
||||
expect(newMsg.sender?.name).toEqual(userName);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
httpBackend!.flush("/messages", 1);
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await didPaginatePromise;
|
||||
});
|
||||
|
||||
it("should add it them to the right place in the timeline", function (done) {
|
||||
it("should add it them to the right place in the timeline", async () => {
|
||||
// set the list of events to return on scrollback
|
||||
sbEvents = [
|
||||
utils.mkMessage({
|
||||
@@ -396,30 +400,30 @@ describe("MatrixClient room timelines", function () {
|
||||
}),
|
||||
];
|
||||
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
const didPaginatePromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, async (state) => {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
await Promise.all([client!.scrollback(room), httpBackend!.flush("/messages", 1)]);
|
||||
|
||||
client!.scrollback(room).then(function () {
|
||||
expect(room.timeline.length).toEqual(3);
|
||||
expect(room.timeline[0].event).toEqual(sbEvents[1]);
|
||||
expect(room.timeline[1].event).toEqual(sbEvents[0]);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
httpBackend!.flush("/messages", 1);
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await didPaginatePromise;
|
||||
});
|
||||
|
||||
it("should use 'end' as the next pagination token", function (done) {
|
||||
it("should use 'end' as the next pagination token", async () => {
|
||||
// set the list of events to return on scrollback
|
||||
sbEvents = [
|
||||
utils.mkMessage({
|
||||
@@ -429,25 +433,24 @@ describe("MatrixClient room timelines", function () {
|
||||
}),
|
||||
];
|
||||
|
||||
client!.on(ClientEvent.Sync, function (state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.oldState.paginationToken).toBeTruthy();
|
||||
const didPaginatePromise = new Promise((resolve) => {
|
||||
client!.on(ClientEvent.Sync, async (state) => {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.oldState.paginationToken).toBeTruthy();
|
||||
|
||||
client!.scrollback(room, 1).then(function () {
|
||||
await Promise.all([client!.scrollback(room, 1), httpBackend!.flush("/messages", 1)]);
|
||||
expect(room.oldState.paginationToken).toEqual(sbEndTok);
|
||||
});
|
||||
|
||||
httpBackend!.flush("/messages", 1).then(function () {
|
||||
// still have a sync to flush
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
httpBackend!.flush("/sync", 1);
|
||||
await httpBackend!.flush("/sync", 1);
|
||||
await didPaginatePromise;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -81,17 +81,15 @@ describe("MatrixClient syncing", () => {
|
||||
presence: {},
|
||||
};
|
||||
|
||||
it("should /sync after /pushrules and /filter.", (done) => {
|
||||
it("should /sync after /pushrules and /filter.", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
client!.startClient();
|
||||
|
||||
httpBackend!.flushAllExpected().then(() => {
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
});
|
||||
|
||||
it("should pass the 'next_batch' token from /sync to the since= param of the next /sync", (done) => {
|
||||
it("should pass the 'next_batch' token from /sync to the since= param of the next /sync", async () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend!
|
||||
.when("GET", "/sync")
|
||||
@@ -102,9 +100,7 @@ describe("MatrixClient syncing", () => {
|
||||
|
||||
client!.startClient();
|
||||
|
||||
httpBackend!.flushAllExpected().then(() => {
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
});
|
||||
|
||||
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
|
||||
@@ -724,7 +720,7 @@ describe("MatrixClient syncing", () => {
|
||||
// events that arrive in the incremental sync as if they preceeded the
|
||||
// timeline events, however this breaks peeking, so it's disabled
|
||||
// (see sync.js)
|
||||
xit("should correctly interpret state in incremental sync.", () => {
|
||||
it.skip("should correctly interpret state in incremental sync.", () => {
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
httpBackend!.when("GET", "/sync").respond(200, nextSyncData);
|
||||
|
||||
@@ -741,9 +737,9 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
xit("should update power levels for users in a room", () => {});
|
||||
it.skip("should update power levels for users in a room", () => {});
|
||||
|
||||
xit("should update the room topic", () => {});
|
||||
it.skip("should update the room topic", () => {});
|
||||
|
||||
describe("onMarkerStateEvent", () => {
|
||||
const normalMessageEvent = utils.mkMessage({
|
||||
@@ -840,6 +836,7 @@ describe("MatrixClient syncing", () => {
|
||||
roomVersion: "org.matrix.msc2716v3",
|
||||
},
|
||||
].forEach((testMeta) => {
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
describe(testMeta.label, () => {
|
||||
const roomCreateEvent = utils.mkEvent({
|
||||
type: "m.room.create",
|
||||
@@ -1592,27 +1589,24 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
describe("of a room", () => {
|
||||
xit(
|
||||
it.skip(
|
||||
"should sync when a join event (which changes state) for the user" +
|
||||
" arrives down the event stream (e.g. join from another device)",
|
||||
() => {},
|
||||
);
|
||||
|
||||
xit("should sync when the user explicitly calls joinRoom", () => {});
|
||||
it.skip("should sync when the user explicitly calls joinRoom", () => {});
|
||||
});
|
||||
|
||||
describe("syncLeftRooms", () => {
|
||||
beforeEach((done) => {
|
||||
beforeEach(async () => {
|
||||
client!.startClient();
|
||||
|
||||
httpBackend!.flushAllExpected().then(() => {
|
||||
// the /sync call from syncLeftRooms ends up in the request
|
||||
// queue behind the call from the running client; add a response
|
||||
// to flush the client's one out.
|
||||
httpBackend!.when("GET", "/sync").respond(200, {});
|
||||
|
||||
done();
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
// the /sync call from syncLeftRooms ends up in the request
|
||||
// queue behind the call from the running client; add a response
|
||||
// to flush the client's one out.
|
||||
await httpBackend!.when("GET", "/sync").respond(200, {});
|
||||
});
|
||||
|
||||
it("should create and use an appropriate filter", () => {
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
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 HttpBackend from "matrix-mock-request";
|
||||
|
||||
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";
|
||||
const userB = "@bob:localhost";
|
||||
|
||||
const selfUserId = userA;
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
|
||||
const setupTestClient = (): [MatrixClient, HttpBackend] => {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const client = testClient.client;
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
return [client, httpBackend];
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
[client, httpBackend] = setupTestClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
client!.stopClient();
|
||||
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";
|
||||
|
||||
const syncData = getSampleStuckNotificationSyncResponse(ROOM_ID);
|
||||
|
||||
it("resets notifications if the last event originates from the logged in user", async () => {
|
||||
httpBackend!
|
||||
.when("GET", "/sync")
|
||||
.check((req) => {
|
||||
expect(req.queryParams!.filter).toEqual("a filter id");
|
||||
})
|
||||
.respond(200, syncData);
|
||||
|
||||
client!.store.getSavedSyncToken = jest.fn().mockResolvedValue("this-is-a-token");
|
||||
client!.startClient({ initialSyncLimit: 1 });
|
||||
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
const room = client?.getRoom(ROOM_ID);
|
||||
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
expect(room?.getUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
function getSampleStuckNotificationSyncResponse(roomId: string): Partial<ISyncResponse> {
|
||||
return {
|
||||
next_batch: "batch_token",
|
||||
rooms: {
|
||||
[Category.Join]: {
|
||||
[roomId]: {
|
||||
timeline: {
|
||||
events: [
|
||||
{
|
||||
content: {
|
||||
creator: userB,
|
||||
room_version: "9",
|
||||
},
|
||||
origin_server_ts: 1,
|
||||
sender: userB,
|
||||
state_key: "",
|
||||
type: "m.room.create",
|
||||
event_id: "$event1",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
avatar_url: "",
|
||||
displayname: userB,
|
||||
membership: "join",
|
||||
},
|
||||
origin_server_ts: 2,
|
||||
sender: userB,
|
||||
state_key: userB,
|
||||
type: "m.room.member",
|
||||
event_id: "$event2",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
ban: 50,
|
||||
events: {
|
||||
"m.room.avatar": 50,
|
||||
"m.room.canonical_alias": 50,
|
||||
"m.room.encryption": 100,
|
||||
"m.room.history_visibility": 100,
|
||||
"m.room.name": 50,
|
||||
"m.room.power_levels": 100,
|
||||
"m.room.server_acl": 100,
|
||||
"m.room.tombstone": 100,
|
||||
},
|
||||
events_default: 0,
|
||||
historical: 100,
|
||||
invite: 0,
|
||||
kick: 50,
|
||||
redact: 50,
|
||||
state_default: 50,
|
||||
users: {
|
||||
[userA]: 100,
|
||||
[userB]: 100,
|
||||
},
|
||||
users_default: 0,
|
||||
},
|
||||
origin_server_ts: 3,
|
||||
sender: userB,
|
||||
state_key: "",
|
||||
type: "m.room.power_levels",
|
||||
event_id: "$event3",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
join_rule: "invite",
|
||||
},
|
||||
origin_server_ts: 4,
|
||||
sender: userB,
|
||||
state_key: "",
|
||||
type: "m.room.join_rules",
|
||||
event_id: "$event4",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
history_visibility: "shared",
|
||||
},
|
||||
origin_server_ts: 5,
|
||||
sender: userB,
|
||||
state_key: "",
|
||||
type: "m.room.history_visibility",
|
||||
event_id: "$event5",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
guest_access: "can_join",
|
||||
},
|
||||
origin_server_ts: 6,
|
||||
sender: userB,
|
||||
state_key: "",
|
||||
type: "m.room.guest_access",
|
||||
unsigned: {
|
||||
age: 1651569,
|
||||
},
|
||||
event_id: "$event6",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
origin_server_ts: 7,
|
||||
sender: userB,
|
||||
state_key: "",
|
||||
type: "m.room.encryption",
|
||||
event_id: "$event7",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
avatar_url: "",
|
||||
displayname: userA,
|
||||
is_direct: true,
|
||||
membership: "invite",
|
||||
},
|
||||
origin_server_ts: 8,
|
||||
sender: userB,
|
||||
state_key: userA,
|
||||
type: "m.room.member",
|
||||
event_id: "$event8",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "hello",
|
||||
},
|
||||
origin_server_ts: 9,
|
||||
sender: userB,
|
||||
type: "m.room.message",
|
||||
event_id: "$event9",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
avatar_url: "",
|
||||
displayname: userA,
|
||||
membership: "join",
|
||||
},
|
||||
origin_server_ts: 10,
|
||||
sender: userA,
|
||||
state_key: userA,
|
||||
type: "m.room.member",
|
||||
event_id: "$event10",
|
||||
},
|
||||
{
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "world",
|
||||
},
|
||||
origin_server_ts: 11,
|
||||
sender: userA,
|
||||
type: "m.room.message",
|
||||
event_id: "$event11",
|
||||
},
|
||||
],
|
||||
prev_batch: "123",
|
||||
limited: false,
|
||||
},
|
||||
state: {
|
||||
events: [],
|
||||
},
|
||||
account_data: {
|
||||
events: [
|
||||
{
|
||||
type: "m.fully_read",
|
||||
content: {
|
||||
event_id: "$dER5V1RCMxzAhHXQJoMjqyuoxpPtK2X6hCb9T8Jg2wU",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
ephemeral: {
|
||||
events: [
|
||||
{
|
||||
type: "m.receipt",
|
||||
content: {
|
||||
$event9: {
|
||||
"m.read": {
|
||||
[userA]: {
|
||||
ts: 100,
|
||||
},
|
||||
},
|
||||
"m.read.private": {
|
||||
[userA]: {
|
||||
ts: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
dER5V1RCMxzAhHXQJoMjqyuoxpPtK2X6hCb9T8Jg2wU: {
|
||||
"m.read": {
|
||||
[userB]: {
|
||||
ts: 666,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
unread_notifications: {
|
||||
notification_count: 1,
|
||||
highlight_count: 0,
|
||||
},
|
||||
summary: {
|
||||
"m.joined_member_count": 2,
|
||||
"m.invited_member_count": 0,
|
||||
"m.heroes": [userB],
|
||||
},
|
||||
},
|
||||
},
|
||||
[Category.Leave]: {},
|
||||
[Category.Invite]: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -22,20 +22,20 @@ limitations under the License.
|
||||
*
|
||||
* Note that megolm (group) conversation is not tested here.
|
||||
*
|
||||
* See also `megolm.spec.js`.
|
||||
* See also `crypto.spec.js`.
|
||||
*/
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import "../olm-loader";
|
||||
|
||||
import type { Session } from "@matrix-org/olm";
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
|
||||
import { logger } from "../../src/logger";
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../src/client";
|
||||
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
import { IDeviceKeys, IOneTimeKey } from "../../src/crypto/dehydration";
|
||||
|
||||
let aliTestClient: TestClient;
|
||||
const roomId = "!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();
|
||||
@@ -52,10 +52,9 @@ describe("SlidingSyncSdk", () => {
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
const mockifySlidingSync = (s: SlidingSync): SlidingSync => {
|
||||
s.getList = jest.fn();
|
||||
s.getListParams = jest.fn();
|
||||
s.getListData = jest.fn();
|
||||
s.getRoomSubscriptions = jest.fn();
|
||||
s.listLength = jest.fn();
|
||||
s.modifyRoomSubscriptionInfo = jest.fn();
|
||||
s.modifyRoomSubscriptions = jest.fn();
|
||||
s.registerExtension = jest.fn();
|
||||
@@ -115,7 +114,7 @@ describe("SlidingSyncSdk", () => {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0));
|
||||
mockSlidingSync = mockifySlidingSync(new SlidingSync("", new Map(), {}, client, 0));
|
||||
if (testOpts.withCrypto) {
|
||||
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
|
||||
await client!.initCrypto();
|
||||
@@ -154,11 +153,11 @@ describe("SlidingSyncSdk", () => {
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
expect(mockSlidingSync!.start).toBeCalled();
|
||||
expect(mockSlidingSync!.start).toHaveBeenCalled();
|
||||
});
|
||||
it("can stop()", async () => {
|
||||
sdk!.stop();
|
||||
expect(mockSlidingSync!.stop).toBeCalled();
|
||||
expect(mockSlidingSync!.stop).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -549,7 +548,7 @@ describe("SlidingSyncSdk", () => {
|
||||
it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, {
|
||||
pos: "h",
|
||||
lists: [],
|
||||
lists: {},
|
||||
rooms: {},
|
||||
extensions: {},
|
||||
});
|
||||
@@ -577,7 +576,7 @@ describe("SlidingSyncSdk", () => {
|
||||
it("emits SyncState.Syncing after a previous SyncState.Error", async () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, {
|
||||
pos: "i",
|
||||
lists: [],
|
||||
lists: {},
|
||||
rooms: {},
|
||||
extensions: {},
|
||||
});
|
||||
@@ -585,7 +584,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("emits SyncState.Error immediately when receiving M_UNKNOWN_TOKEN and stops syncing", async () => {
|
||||
expect(mockSlidingSync!.stop).not.toBeCalled();
|
||||
expect(mockSlidingSync!.stop).not.toHaveBeenCalled();
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle,
|
||||
SlidingSyncState.RequestFinished,
|
||||
@@ -596,7 +595,7 @@ describe("SlidingSyncSdk", () => {
|
||||
}),
|
||||
);
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
|
||||
expect(mockSlidingSync!.stop).toBeCalled();
|
||||
expect(mockSlidingSync!.stop).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+151
-147
@@ -64,10 +64,10 @@ describe("SlidingSync", () => {
|
||||
let slidingSync: SlidingSync;
|
||||
|
||||
it("should start the sync loop upon calling start()", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), {}, client!, 1);
|
||||
const fakeResp = {
|
||||
pos: "a",
|
||||
lists: [],
|
||||
lists: {},
|
||||
rooms: {},
|
||||
extensions: {},
|
||||
};
|
||||
@@ -90,7 +90,7 @@ describe("SlidingSync", () => {
|
||||
|
||||
it("should reset the connection on HTTP 400 and send everything again", async () => {
|
||||
// seed the connection with some lists, extensions and subscriptions to verify they are sent again
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), {}, client!, 1);
|
||||
const roomId = "!sub:localhost";
|
||||
const subInfo = {
|
||||
timeline_limit: 42,
|
||||
@@ -114,7 +114,7 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
||||
slidingSync.modifyRoomSubscriptionInfo(subInfo);
|
||||
slidingSync.setList(0, listInfo);
|
||||
slidingSync.setList("a", listInfo);
|
||||
slidingSync.registerExtension(ext);
|
||||
slidingSync.start();
|
||||
|
||||
@@ -128,7 +128,7 @@ describe("SlidingSync", () => {
|
||||
expect(body.room_subscriptions).toEqual({
|
||||
[roomId]: subInfo,
|
||||
});
|
||||
expect(body.lists[0]).toEqual(listInfo);
|
||||
expect(body.lists["a"]).toEqual(listInfo);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
expect(body.extensions["custom_extension"]).toEqual({ initial: true });
|
||||
expect(req.queryParams!["pos"]).toBeUndefined();
|
||||
@@ -137,7 +137,7 @@ describe("SlidingSync", () => {
|
||||
.respond(200, function () {
|
||||
return {
|
||||
pos: "11",
|
||||
lists: [{ count: 5 }],
|
||||
lists: { a: { count: 5 } },
|
||||
extensions: {},
|
||||
txn_id: txnId,
|
||||
};
|
||||
@@ -151,7 +151,7 @@ describe("SlidingSync", () => {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
expect(body.lists[0]).toEqual({
|
||||
expect(body.lists["a"]).toEqual({
|
||||
ranges: [[0, 10]],
|
||||
});
|
||||
expect(body.extensions).toBeTruthy();
|
||||
@@ -161,7 +161,7 @@ describe("SlidingSync", () => {
|
||||
.respond(200, function () {
|
||||
return {
|
||||
pos: "12",
|
||||
lists: [{ count: 5 }],
|
||||
lists: { a: { count: 5 } },
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
@@ -185,7 +185,7 @@ describe("SlidingSync", () => {
|
||||
expect(body.room_subscriptions).toEqual({
|
||||
[roomId]: subInfo,
|
||||
});
|
||||
expect(body.lists[0]).toEqual(listInfo);
|
||||
expect(body.lists["a"]).toEqual(listInfo);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
expect(body.extensions["custom_extension"]).toEqual({ initial: true });
|
||||
expect(req.queryParams!["pos"]).toBeUndefined();
|
||||
@@ -193,7 +193,7 @@ describe("SlidingSync", () => {
|
||||
.respond(200, function () {
|
||||
return {
|
||||
pos: "1",
|
||||
lists: [{ count: 6 }],
|
||||
lists: { a: { count: 6 } },
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
@@ -221,7 +221,7 @@ describe("SlidingSync", () => {
|
||||
|
||||
it("should be able to subscribe to a room", async () => {
|
||||
// add the subscription
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), roomSubInfo, client!, 1);
|
||||
slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
@@ -233,7 +233,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "a",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {
|
||||
[roomId]: wantRoomData,
|
||||
@@ -266,7 +266,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "a",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {
|
||||
[roomId]: wantRoomData,
|
||||
@@ -313,7 +313,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {
|
||||
[anotherRoomID]: anotherRoomData,
|
||||
@@ -344,7 +344,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
});
|
||||
|
||||
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
@@ -402,19 +402,19 @@ describe("SlidingSync", () => {
|
||||
is_dm: true,
|
||||
},
|
||||
};
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client!, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, new Map([["a", listReq]]), {}, client!, 1);
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
.check(function (req) {
|
||||
const body = req.data;
|
||||
logger.log("list", body);
|
||||
expect(body.lists).toBeTruthy();
|
||||
expect(body.lists[0]).toEqual(listReq);
|
||||
expect(body.lists["a"]).toEqual(listReq);
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "a",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
@@ -424,7 +424,7 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
rooms: rooms,
|
||||
});
|
||||
const listenerData: Record<string, MSC3575RoomData> = {};
|
||||
@@ -444,15 +444,14 @@ describe("SlidingSync", () => {
|
||||
expect(listenerData[roomB]).toEqual(rooms[roomB]);
|
||||
expect(listenerData[roomC]).toEqual(rooms[roomC]);
|
||||
|
||||
expect(slidingSync.listLength()).toEqual(1);
|
||||
slidingSync.off(SlidingSyncEvent.RoomData, dataListener);
|
||||
});
|
||||
|
||||
it("should be possible to retrieve list data", () => {
|
||||
expect(slidingSync.getList(0)).toBeDefined();
|
||||
expect(slidingSync.getList(5)).toBeNull();
|
||||
expect(slidingSync.getListData(5)).toBeNull();
|
||||
const syncData = slidingSync.getListData(0)!;
|
||||
expect(slidingSync.getListParams("a")).toBeDefined();
|
||||
expect(slidingSync.getListParams("b")).toBeNull();
|
||||
expect(slidingSync.getListData("b")).toBeNull();
|
||||
const syncData = slidingSync.getListData("a")!;
|
||||
expect(syncData.joinedCount).toEqual(500); // from previous test
|
||||
expect(syncData.roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
@@ -467,17 +466,17 @@ describe("SlidingSync", () => {
|
||||
.when("POST", syncUrl)
|
||||
.check(function (req) {
|
||||
const body = req.data;
|
||||
logger.log("next ranges", body.lists[0].ranges);
|
||||
logger.log("next ranges", body.lists["a"].ranges);
|
||||
expect(body.lists).toBeTruthy();
|
||||
expect(body.lists[0]).toEqual({
|
||||
expect(body.lists["a"]).toEqual({
|
||||
// only the ranges should be sent as the rest are unchanged and sticky
|
||||
ranges: newRanges,
|
||||
});
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
@@ -487,15 +486,17 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.RequestFinished;
|
||||
});
|
||||
slidingSync.setListRanges(0, newRanges);
|
||||
slidingSync.setListRanges("a", newRanges);
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
// setListRanges for an invalid list key returns an error
|
||||
await expect(slidingSync.setListRanges("idontexist", newRanges)).rejects.toBeTruthy();
|
||||
});
|
||||
|
||||
it("should be possible to add an extra list", async () => {
|
||||
@@ -513,19 +514,19 @@ describe("SlidingSync", () => {
|
||||
const body = req.data;
|
||||
logger.log("extra list", body);
|
||||
expect(body.lists).toBeTruthy();
|
||||
expect(body.lists[0]).toEqual({
|
||||
expect(body.lists["a"]).toEqual({
|
||||
// only the ranges should be sent as the rest are unchanged and sticky
|
||||
ranges: newRanges,
|
||||
});
|
||||
expect(body.lists[1]).toEqual(extraListReq);
|
||||
expect(body.lists["b"]).toEqual(extraListReq);
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "c",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
},
|
||||
{
|
||||
b: {
|
||||
count: 50,
|
||||
ops: [
|
||||
{
|
||||
@@ -535,10 +536,10 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
listenUntil(slidingSync, "SlidingSync.List", (listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(1);
|
||||
listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("b");
|
||||
expect(joinedCount).toEqual(50);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
@@ -550,7 +551,7 @@ describe("SlidingSync", () => {
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
slidingSync.setList(1, extraListReq);
|
||||
slidingSync.setList("b", extraListReq);
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
});
|
||||
@@ -559,8 +560,8 @@ describe("SlidingSync", () => {
|
||||
// move C (2) to A (0)
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "e",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
@@ -574,16 +575,16 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
let listPromise = listenUntil(
|
||||
slidingSync,
|
||||
"SlidingSync.List",
|
||||
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
(listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(500);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
@@ -603,8 +604,8 @@ describe("SlidingSync", () => {
|
||||
// move C (0) back to A (2)
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "f",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
@@ -618,13 +619,13 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
listPromise = listenUntil(slidingSync, "SlidingSync.List", (listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
listPromise = listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(500);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
@@ -644,8 +645,8 @@ describe("SlidingSync", () => {
|
||||
it("should ignore invalid list indexes", async () => {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "e",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
@@ -654,16 +655,16 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const listPromise = listenUntil(
|
||||
slidingSync,
|
||||
"SlidingSync.List",
|
||||
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
(listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(500);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
@@ -684,8 +685,8 @@ describe("SlidingSync", () => {
|
||||
it("should be possible to update a list", async () => {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "g",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 42,
|
||||
ops: [
|
||||
{
|
||||
@@ -699,13 +700,13 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
// update the list with a new filter
|
||||
slidingSync.setList(0, {
|
||||
slidingSync.setList("a", {
|
||||
filters: {
|
||||
is_encrypted: true,
|
||||
},
|
||||
@@ -714,8 +715,8 @@ describe("SlidingSync", () => {
|
||||
const listPromise = listenUntil(
|
||||
slidingSync,
|
||||
"SlidingSync.List",
|
||||
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
(listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(42);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomB,
|
||||
@@ -738,12 +739,12 @@ describe("SlidingSync", () => {
|
||||
0: roomB,
|
||||
1: roomC,
|
||||
};
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual(indexToRoomId);
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual(indexToRoomId);
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "f",
|
||||
// currently the list is [B,C] so we will insert D then immediately delete it
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
@@ -761,16 +762,16 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const listPromise = listenUntil(
|
||||
slidingSync,
|
||||
"SlidingSync.List",
|
||||
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
(listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(500);
|
||||
expect(roomIndexToRoomId).toEqual(indexToRoomId);
|
||||
return true;
|
||||
@@ -785,14 +786,14 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
it("should handle deletions correctly", async () => {
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
|
||||
0: roomB,
|
||||
1: roomC,
|
||||
});
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "g",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 499,
|
||||
ops: [
|
||||
{
|
||||
@@ -801,16 +802,16 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const listPromise = listenUntil(
|
||||
slidingSync,
|
||||
"SlidingSync.List",
|
||||
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
(listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(499);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
@@ -827,13 +828,13 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
it("should handle insertions correctly", async () => {
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
});
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "h",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 500,
|
||||
ops: [
|
||||
{
|
||||
@@ -843,16 +844,16 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
let listPromise = listenUntil(
|
||||
slidingSync,
|
||||
"SlidingSync.List",
|
||||
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
(listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(500);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
@@ -870,8 +871,8 @@ describe("SlidingSync", () => {
|
||||
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "h",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 501,
|
||||
ops: [
|
||||
{
|
||||
@@ -881,13 +882,13 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b: {
|
||||
count: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
listPromise = listenUntil(slidingSync, "SlidingSync.List", (listIndex, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listIndex).toEqual(0);
|
||||
listPromise = listenUntil(slidingSync, "SlidingSync.List", (listKey, joinedCount, roomIndexToRoomId) => {
|
||||
expect(listKey).toEqual("a");
|
||||
expect(joinedCount).toEqual(501);
|
||||
expect(roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
@@ -910,11 +911,14 @@ describe("SlidingSync", () => {
|
||||
it("should handle insertions with a spurious DELETE correctly", async () => {
|
||||
slidingSync = new SlidingSync(
|
||||
proxyBaseUrl,
|
||||
[
|
||||
{
|
||||
ranges: [[0, 20]],
|
||||
},
|
||||
],
|
||||
new Map([
|
||||
[
|
||||
"a",
|
||||
{
|
||||
ranges: [[0, 20]],
|
||||
},
|
||||
],
|
||||
]),
|
||||
{},
|
||||
client!,
|
||||
1,
|
||||
@@ -922,22 +926,22 @@ describe("SlidingSync", () => {
|
||||
// initially start with nothing
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "a",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 0,
|
||||
ops: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({});
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({});
|
||||
|
||||
// insert a room
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "b",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 1,
|
||||
ops: [
|
||||
{
|
||||
@@ -951,18 +955,18 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
|
||||
0: roomA,
|
||||
});
|
||||
|
||||
// insert another room
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "c",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 1,
|
||||
ops: [
|
||||
{
|
||||
@@ -976,10 +980,10 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
|
||||
0: roomB,
|
||||
1: roomA,
|
||||
});
|
||||
@@ -987,8 +991,8 @@ describe("SlidingSync", () => {
|
||||
// insert a final room
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "c",
|
||||
lists: [
|
||||
{
|
||||
lists: {
|
||||
a: {
|
||||
count: 1,
|
||||
ops: [
|
||||
{
|
||||
@@ -1002,10 +1006,10 @@ describe("SlidingSync", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({
|
||||
expect(slidingSync.getListData("a")!.roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
1: roomB,
|
||||
2: roomA,
|
||||
@@ -1028,7 +1032,7 @@ describe("SlidingSync", () => {
|
||||
required_state: [["m.room.name", ""]],
|
||||
};
|
||||
// add the subscription
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), roomSubInfo, client!, 1);
|
||||
// modification before SlidingSync.start()
|
||||
const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
||||
let txnId: string | undefined;
|
||||
@@ -1046,7 +1050,7 @@ describe("SlidingSync", () => {
|
||||
return {
|
||||
pos: "aaa",
|
||||
txn_id: txnId,
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {
|
||||
[roomId]: {
|
||||
@@ -1065,7 +1069,7 @@ describe("SlidingSync", () => {
|
||||
const newList = {
|
||||
ranges: [[0, 20]],
|
||||
};
|
||||
const promise = slidingSync.setList(0, newList);
|
||||
const promise = slidingSync.setList("a", newList);
|
||||
let txnId: string | undefined;
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
@@ -1073,7 +1077,7 @@ describe("SlidingSync", () => {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
expect(body.lists[0]).toEqual(newList);
|
||||
expect(body.lists["a"]).toEqual(newList);
|
||||
expect(body.txn_id).toBeTruthy();
|
||||
txnId = body.txn_id;
|
||||
})
|
||||
@@ -1081,7 +1085,7 @@ describe("SlidingSync", () => {
|
||||
return {
|
||||
pos: "bbb",
|
||||
txn_id: txnId,
|
||||
lists: [{ count: 5 }],
|
||||
lists: { a: { count: 5 } },
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
@@ -1090,7 +1094,7 @@ describe("SlidingSync", () => {
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
it("should resolve setListRanges during a connection", async () => {
|
||||
const promise = slidingSync.setListRanges(0, [[20, 40]]);
|
||||
const promise = slidingSync.setListRanges("a", [[20, 40]]);
|
||||
let txnId: string | undefined;
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
@@ -1098,7 +1102,7 @@ describe("SlidingSync", () => {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
expect(body.lists[0]).toEqual({
|
||||
expect(body.lists["a"]).toEqual({
|
||||
ranges: [[20, 40]],
|
||||
});
|
||||
expect(body.txn_id).toBeTruthy();
|
||||
@@ -1108,7 +1112,7 @@ describe("SlidingSync", () => {
|
||||
return {
|
||||
pos: "ccc",
|
||||
txn_id: txnId,
|
||||
lists: [{ count: 5 }],
|
||||
lists: { a: { count: 5 } },
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
@@ -1150,10 +1154,10 @@ describe("SlidingSync", () => {
|
||||
const pushTxn = function (req: MockHttpBackend["requests"][0]) {
|
||||
gotTxnIds.push(req.data.txn_id);
|
||||
};
|
||||
const failPromise = slidingSync.setListRanges(0, [[20, 40]]);
|
||||
const failPromise = slidingSync.setListRanges("a", [[20, 40]]);
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id
|
||||
await httpBackend!.flushAllExpected();
|
||||
const failPromise2 = slidingSync.setListRanges(0, [[60, 70]]);
|
||||
const failPromise2 = slidingSync.setListRanges("a", [[60, 70]]);
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
@@ -1162,7 +1166,7 @@ describe("SlidingSync", () => {
|
||||
expect(failPromise).rejects.toEqual(gotTxnIds[0]);
|
||||
expect(failPromise2).rejects.toEqual(gotTxnIds[1]);
|
||||
|
||||
const okPromise = slidingSync.setListRanges(0, [[0, 20]]);
|
||||
const okPromise = slidingSync.setListRanges("a", [[0, 20]]);
|
||||
let txnId: string | undefined;
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
@@ -1187,10 +1191,10 @@ describe("SlidingSync", () => {
|
||||
const pushTxn = function (req: MockHttpBackend["requests"][0]) {
|
||||
gotTxnIds.push(req.data?.txn_id);
|
||||
};
|
||||
const A = slidingSync.setListRanges(0, [[20, 40]]);
|
||||
const A = slidingSync.setListRanges("a", [[20, 40]]);
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" });
|
||||
await httpBackend!.flushAllExpected();
|
||||
const B = slidingSync.setListRanges(0, [[60, 70]]);
|
||||
const B = slidingSync.setListRanges("a", [[60, 70]]);
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
@@ -1198,7 +1202,7 @@ describe("SlidingSync", () => {
|
||||
// which is a fail.
|
||||
expect(A).rejects.toEqual(gotTxnIds[0]);
|
||||
|
||||
const C = slidingSync.setListRanges(0, [[0, 20]]);
|
||||
const C = slidingSync.setListRanges("a", [[0, 20]]);
|
||||
let pendingC = true;
|
||||
C.finally(() => {
|
||||
pendingC = false;
|
||||
@@ -1219,7 +1223,7 @@ describe("SlidingSync", () => {
|
||||
expect(pendingC).toBe(true); // C is pending still
|
||||
});
|
||||
it("should do nothing for unknown txn_ids", async () => {
|
||||
const promise = slidingSync.setListRanges(0, [[20, 40]]);
|
||||
const promise = slidingSync.setListRanges("a", [[20, 40]]);
|
||||
let pending = true;
|
||||
promise.finally(() => {
|
||||
pending = false;
|
||||
@@ -1231,7 +1235,7 @@ describe("SlidingSync", () => {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
expect(body.lists[0]).toEqual({
|
||||
expect(body.lists["a"]).toEqual({
|
||||
ranges: [[20, 40]],
|
||||
});
|
||||
expect(body.txn_id).toBeTruthy();
|
||||
@@ -1241,7 +1245,7 @@ describe("SlidingSync", () => {
|
||||
return {
|
||||
pos: "ccc",
|
||||
txn_id: "bogus transaction id",
|
||||
lists: [{ count: 5 }],
|
||||
lists: { a: { count: 5 } },
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
@@ -1279,7 +1283,7 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
|
||||
it("should be possible to use custom subscriptions on startup", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
|
||||
// the intention is for clients to set this up at startup
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.addCustomSubscription(customSubName2, customSub2);
|
||||
@@ -1302,7 +1306,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
@@ -1312,7 +1316,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
it("should be possible to use custom subscriptions mid-connection", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
|
||||
// the intention is for clients to set this up at startup
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.addCustomSubscription(customSubName2, customSub2);
|
||||
@@ -1326,7 +1330,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
@@ -1344,7 +1348,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
@@ -1363,7 +1367,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
@@ -1383,7 +1387,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
@@ -1395,7 +1399,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
it("uses the default subscription for unknown subscription names", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.useCustomSubscription(roomA, "unknown name");
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||
@@ -1410,7 +1414,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
@@ -1420,7 +1424,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
it("should not be possible to add/modify an already added custom subscription", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub2);
|
||||
slidingSync.useCustomSubscription(roomA, customSubName1);
|
||||
@@ -1436,7 +1440,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
@@ -1446,7 +1450,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
it("should change the custom subscription if they are different", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, new Map(), defaultSub, client!, 1);
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.addCustomSubscription(customSubName2, customSub2);
|
||||
slidingSync.useCustomSubscription(roomA, customSubName1);
|
||||
@@ -1463,7 +1467,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
@@ -1484,7 +1488,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
@@ -1506,7 +1510,7 @@ describe("SlidingSync", () => {
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
lists: {},
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
@@ -1559,7 +1563,7 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
|
||||
it("should be able to register an extension", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), {}, client!, 1);
|
||||
slidingSync.registerExtension(extPre);
|
||||
|
||||
const callbackOrder: string[] = [];
|
||||
@@ -1684,7 +1688,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
it("is not possible to register the same extension name twice", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, new Map(), {}, client!, 1);
|
||||
slidingSync.registerExtension(extPre);
|
||||
expect(() => {
|
||||
slidingSync.registerExtension(extPre);
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
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 debugFunc from "debug";
|
||||
import { Debugger } from "debug";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
|
||||
|
||||
/** Interface implemented by classes that intercept `/keys/upload` requests from test clients to catch the uploaded keys
|
||||
*
|
||||
* Common interface implemented by {@link TestClient} and {@link E2EKeyReceiver}
|
||||
*/
|
||||
export interface IE2EKeyReceiver {
|
||||
/**
|
||||
* get the uploaded ed25519 device key
|
||||
*
|
||||
* @returns base64 device key
|
||||
*/
|
||||
getSigningKey(): string;
|
||||
|
||||
/**
|
||||
* get the uploaded curve25519 device key
|
||||
*
|
||||
* @returns base64 device key
|
||||
*/
|
||||
getDeviceKey(): string;
|
||||
|
||||
/**
|
||||
* Wait for one-time-keys to be uploaded, then return them.
|
||||
*
|
||||
* @returns Promise for the one-time keys
|
||||
*/
|
||||
awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>>;
|
||||
}
|
||||
|
||||
/** E2EKeyReceiver: An object which intercepts `/keys/uploads` fetches via fetch-mock.
|
||||
*
|
||||
* It stashes the uploaded keys for use elsewhere in the tests.
|
||||
*/
|
||||
export class E2EKeyReceiver implements IE2EKeyReceiver {
|
||||
private readonly debug: Debugger;
|
||||
|
||||
private deviceKeys: IDeviceKeys | null = null;
|
||||
private oneTimeKeys: Record<string, IOneTimeKey> = {};
|
||||
private readonly oneTimeKeysPromise: Promise<void>;
|
||||
|
||||
/**
|
||||
* Construct a new E2EKeyReceiver.
|
||||
*
|
||||
* It will immediately register an intercept of `/keys/uploads` requests for the given homeserverUrl.
|
||||
* Only /upload requests made to this server will be intercepted: this allows a single test to use more than one
|
||||
* client and have the keys collected separately.
|
||||
*
|
||||
* @param homeserverUrl - the Homeserver Url of the client under test.
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
this.debug = debugFunc(`e2e-key-receiver:[${homeserverUrl}]`);
|
||||
|
||||
// set up a listener for /keys/upload.
|
||||
this.oneTimeKeysPromise = new Promise((resolveOneTimeKeys) => {
|
||||
const listener = (url: string, options: RequestInit) =>
|
||||
this.onKeyUploadRequest(resolveOneTimeKeys, options);
|
||||
|
||||
// catch both r0 and v3 variants
|
||||
fetchMock.post(new URL("/_matrix/client/r0/keys/upload", homeserverUrl).toString(), listener);
|
||||
fetchMock.post(new URL("/_matrix/client/v3/keys/upload", homeserverUrl).toString(), listener);
|
||||
});
|
||||
}
|
||||
|
||||
private async onKeyUploadRequest(onOnTimeKeysUploaded: () => void, options: RequestInit): Promise<object> {
|
||||
const content = JSON.parse(options.body as string);
|
||||
|
||||
// device keys may only be uploaded once
|
||||
if (content.device_keys && Object.keys(content.device_keys).length > 0) {
|
||||
if (this.deviceKeys) {
|
||||
throw new Error("Application attempted to upload E2E device keys multiple times");
|
||||
}
|
||||
this.debug(`received device keys`);
|
||||
this.deviceKeys = content.device_keys;
|
||||
}
|
||||
|
||||
if (content.one_time_keys && Object.keys(content.one_time_keys).length > 0) {
|
||||
// this is a one-time-key upload
|
||||
|
||||
// if we already have a batch of one-time keys, then slow-roll the response,
|
||||
// otherwise the client ends up tight-looping one-time-key-uploads and filling the logs with junk.
|
||||
if (Object.keys(this.oneTimeKeys).length > 0) {
|
||||
this.debug(`received second batch of one-time keys: blocking response`);
|
||||
await new Promise(() => {});
|
||||
}
|
||||
|
||||
this.debug(`received ${Object.keys(content.one_time_keys).length} one-time keys`);
|
||||
Object.assign(this.oneTimeKeys, content.one_time_keys);
|
||||
onOnTimeKeysUploaded();
|
||||
}
|
||||
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys).length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Get the uploaded Ed25519 key
|
||||
*
|
||||
* If device keys have not yet been uploaded, throws an error
|
||||
*/
|
||||
public getSigningKey(): string {
|
||||
if (!this.deviceKeys) {
|
||||
throw new Error("Device keys not yet uploaded");
|
||||
}
|
||||
const keyIds = Object.keys(this.deviceKeys.keys).filter((v) => v.startsWith("ed25519:"));
|
||||
if (keyIds.length != 1) {
|
||||
throw new Error(`Expected exactly 1 ed25519 key uploaded, got ${keyIds}`);
|
||||
}
|
||||
return this.deviceKeys.keys[keyIds[0]];
|
||||
}
|
||||
|
||||
/** Get the uploaded Curve25519 key
|
||||
*
|
||||
* If device keys have not yet been uploaded, throws an error
|
||||
*/
|
||||
public getDeviceKey(): string {
|
||||
if (!this.deviceKeys) {
|
||||
throw new Error("Device keys not yet uploaded");
|
||||
}
|
||||
const keyIds = Object.keys(this.deviceKeys.keys).filter((v) => v.startsWith("curve25519:"));
|
||||
if (keyIds.length != 1) {
|
||||
throw new Error(`Expected exactly 1 curve25519 key uploaded, got ${keyIds}`);
|
||||
}
|
||||
return this.deviceKeys.keys[keyIds[0]];
|
||||
}
|
||||
|
||||
/**
|
||||
* If one-time keys have already been uploaded, return them. Otherwise,
|
||||
* set up an expectation that the keys will be uploaded, and wait for
|
||||
* that to happen.
|
||||
*
|
||||
* @returns Promise for the one-time keys
|
||||
*/
|
||||
public async awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
|
||||
await this.oneTimeKeysPromise;
|
||||
return this.oneTimeKeys;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
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 debugFunc from "debug";
|
||||
import { Debugger } from "debug";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { MockResponse } from "fetch-mock";
|
||||
|
||||
/** Interface implemented by classes that intercept `/sync` requests from test clients
|
||||
*
|
||||
* Common interface implemented by {@link TestClient} and {@link SyncResponder}
|
||||
*/
|
||||
export interface ISyncResponder {
|
||||
/** Next time we see a sync request (or immediately, if there is one waiting), send the given response
|
||||
*
|
||||
* @param response - response to /sync request
|
||||
*/
|
||||
sendOrQueueSyncResponse(response: object): void;
|
||||
}
|
||||
|
||||
enum SyncResponderState {
|
||||
IDLE,
|
||||
WAITING_FOR_REQUEST,
|
||||
WAITING_FOR_RESPONSE,
|
||||
}
|
||||
|
||||
/** SyncResponder: An object which intercepts `/sync` fetches via fetch-mock.
|
||||
*
|
||||
* Two modes are possible:
|
||||
* * A response can be queued up; the next call to `/sync` will return it.
|
||||
* * If a call to `/sync` arrives before a response is queued, it will block until a call to {@link #sendOrQueueSyncResponse}.
|
||||
*/
|
||||
export class SyncResponder implements ISyncResponder {
|
||||
private readonly debug: Debugger;
|
||||
private state: SyncResponderState = SyncResponderState.IDLE;
|
||||
|
||||
/*
|
||||
* properties that are only valid in WAITING_FOR_REQUEST
|
||||
*/
|
||||
|
||||
/** the response to be sent when the request is made */
|
||||
private pendingResponse: object | null = null;
|
||||
|
||||
/*
|
||||
* properties that are only valid in WAITING_FOR_RESPONSE
|
||||
*/
|
||||
|
||||
/** a callback to be called with a response once one is registered.
|
||||
*
|
||||
* It will release the /sync request and update the state.
|
||||
*/
|
||||
private onResponseReceived: ((response: object) => void) | null = null;
|
||||
|
||||
/**
|
||||
* Construct a new SyncResponder.
|
||||
*
|
||||
* It will immediately register an intercept of `/sync` requests for the given homeserverUrl.
|
||||
* Only /sync requests made to this server will be intercepted: this allows a single test to use more than one
|
||||
* client and have overlapping /sync requests.
|
||||
*
|
||||
* @param homeserverUrl - the Homeserver Url of the client under test.
|
||||
*/
|
||||
public constructor(homeserverUrl: string) {
|
||||
this.debug = debugFunc(`sync-responder:[${homeserverUrl}]`);
|
||||
fetchMock.get("begin:" + new URL("/_matrix/client/r0/sync?", homeserverUrl).toString(), (_url, _options) =>
|
||||
this.onSyncRequest(),
|
||||
);
|
||||
}
|
||||
|
||||
private async onSyncRequest(): Promise<MockResponse> {
|
||||
switch (this.state) {
|
||||
case SyncResponderState.IDLE: {
|
||||
this.debug("Got /sync request: waiting for response to be ready");
|
||||
const res = await new Promise<object>((resolve) => {
|
||||
this.onResponseReceived = resolve;
|
||||
this.state = SyncResponderState.WAITING_FOR_RESPONSE;
|
||||
});
|
||||
this.debug("Responding to /sync");
|
||||
this.state = SyncResponderState.IDLE;
|
||||
this.onResponseReceived = null;
|
||||
return res;
|
||||
}
|
||||
|
||||
case SyncResponderState.WAITING_FOR_REQUEST: {
|
||||
this.debug("Got /sync request: responding immediately with queued response");
|
||||
const res = this.pendingResponse!;
|
||||
this.state = SyncResponderState.IDLE;
|
||||
this.pendingResponse = null;
|
||||
return res;
|
||||
}
|
||||
|
||||
default:
|
||||
// we must already be in WAITING_FOR_RESPONSE, ie we already have a /sync request in progress
|
||||
throw new Error(`Got unexpected /sync request in state ${this.state}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Next time we see a sync request (or immediately, if there is one waiting), send the given response
|
||||
*
|
||||
* @param response - response to /sync request
|
||||
*/
|
||||
public sendOrQueueSyncResponse(response: object): void {
|
||||
switch (this.state) {
|
||||
case SyncResponderState.IDLE:
|
||||
this.pendingResponse = response;
|
||||
this.state = SyncResponderState.WAITING_FOR_REQUEST;
|
||||
break;
|
||||
|
||||
case SyncResponderState.WAITING_FOR_RESPONSE:
|
||||
this.onResponseReceived!(response);
|
||||
break;
|
||||
|
||||
default:
|
||||
// we already have a response queued
|
||||
throw new Error(`Cannot queue more than one /sync response`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ export const getMockClientWithEventEmitter = (
|
||||
*/
|
||||
export const mockClientMethodsUser = (userId = "@alice:domain") => ({
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
getSafeUserId: jest.fn().mockReturnValue(userId),
|
||||
getUser: jest.fn().mockReturnValue(new User(userId)),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
|
||||
|
||||
@@ -403,3 +403,15 @@ export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
|
||||
pushkey: "pushpush",
|
||||
...extra,
|
||||
});
|
||||
|
||||
/**
|
||||
* a list of the supported crypto implementations, each with a callback to initialise that implementation
|
||||
* for the given client
|
||||
*/
|
||||
export const CRYPTO_BACKENDS: Record<string, InitCrypto> = {};
|
||||
export type InitCrypto = (_: MatrixClient) => Promise<void>;
|
||||
|
||||
CRYPTO_BACKENDS["rust-sdk"] = (client: MatrixClient) => client.initRustCrypto();
|
||||
if (global.Olm) {
|
||||
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initCrypto();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 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.
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
RoomState,
|
||||
RoomStateEvent,
|
||||
RoomStateEventHandlerMap,
|
||||
SendToDeviceContentMap,
|
||||
} from "../../src";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
@@ -443,13 +444,13 @@ 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 {
|
||||
return false;
|
||||
}
|
||||
|
||||
public getMediaHandler(): MediaHandler {
|
||||
return this.mediaHandler.typed();
|
||||
}
|
||||
@@ -476,7 +477,7 @@ export class MockCallMatrixClient extends TypedEventEmitter<EmittedEvents, Emitt
|
||||
public getRooms = jest.fn<Room[], []>().mockReturnValue([]);
|
||||
public getRoom = jest.fn();
|
||||
|
||||
public supportsExperimentalThreads(): boolean {
|
||||
public supportsThreads(): boolean {
|
||||
return true;
|
||||
}
|
||||
public async decryptEventIfNeeded(): Promise<void> {}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getMockClientWithEventEmitter } from "../test-utils/client";
|
||||
import { StubStore } from "../../src/store/stub";
|
||||
import { IndexedToDeviceBatch } from "../../src/models/ToDeviceMessage";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { defer } from "../../src/utils";
|
||||
|
||||
describe("onResumedSync", () => {
|
||||
let batch: IndexedToDeviceBatch | null;
|
||||
@@ -58,7 +59,9 @@ describe("onResumedSync", () => {
|
||||
queue = new ToDeviceMessageQueue(mockClient);
|
||||
});
|
||||
|
||||
it("resends queue after connectivity restored", (done) => {
|
||||
it("resends queue after connectivity restored", async () => {
|
||||
const deferred = defer();
|
||||
|
||||
onSendToDeviceFailure = () => {
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
|
||||
@@ -70,26 +73,32 @@ describe("onResumedSync", () => {
|
||||
onSendToDeviceSuccess = () => {
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(3);
|
||||
expect(store.removeToDeviceBatch).toHaveBeenCalled();
|
||||
done();
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
queue.start();
|
||||
return deferred.promise;
|
||||
});
|
||||
|
||||
it("does not resend queue if client sync still catching up", (done) => {
|
||||
it("does not resend queue if client sync still catching up", async () => {
|
||||
const deferred = defer();
|
||||
|
||||
onSendToDeviceFailure = () => {
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
|
||||
|
||||
resumeSync(SyncState.Catchup, SyncState.Catchup);
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
done();
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
queue.start();
|
||||
return deferred.promise;
|
||||
});
|
||||
|
||||
it("does not resend queue if connectivity restored after queue stopped", (done) => {
|
||||
it("does not resend queue if connectivity restored after queue stopped", async () => {
|
||||
const deferred = defer();
|
||||
|
||||
onSendToDeviceFailure = () => {
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
expect(store.removeToDeviceBatch).not.toHaveBeenCalled();
|
||||
@@ -98,9 +107,10 @@ describe("onResumedSync", () => {
|
||||
|
||||
resumeSync(SyncState.Syncing, SyncState.Catchup);
|
||||
expect(store.getOldestToDeviceBatch).toHaveBeenCalledTimes(1);
|
||||
done();
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
queue.start();
|
||||
return deferred.promise;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { REFERENCE_RELATION } from "matrix-events-sdk";
|
||||
|
||||
import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location";
|
||||
import { M_TOPIC } from "../../src/@types/topic";
|
||||
import {
|
||||
@@ -25,6 +23,7 @@ import {
|
||||
parseBeaconContent,
|
||||
parseTopicContent,
|
||||
} from "../../src/content-helpers";
|
||||
import { REFERENCE_RELATION } from "../../src/@types/extensible_events";
|
||||
|
||||
describe("Beacon content helpers", () => {
|
||||
describe("makeBeaconInfoContent()", () => {
|
||||
|
||||
+42
-28
@@ -254,6 +254,7 @@ describe("Crypto", function () {
|
||||
sendToDevice: jest.fn(),
|
||||
getKeyBackupVersion: jest.fn(),
|
||||
isGuest: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
} as unknown as MatrixClient;
|
||||
mockRoomList = {} as unknown as RoomList;
|
||||
|
||||
@@ -303,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();
|
||||
});
|
||||
@@ -400,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;
|
||||
@@ -549,7 +554,7 @@ describe("Crypto", function () {
|
||||
aliceClient.crypto!.outgoingRoomKeyRequestManager.sendQueuedRequests();
|
||||
jest.runAllTimers();
|
||||
await Promise.resolve();
|
||||
expect(aliceSendToDevice).toBeCalledTimes(1);
|
||||
expect(aliceSendToDevice).toHaveBeenCalledTimes(1);
|
||||
const txnId = aliceSendToDevice.mock.calls[0][2];
|
||||
|
||||
// give the room key request manager time to update the state
|
||||
@@ -563,21 +568,21 @@ describe("Crypto", function () {
|
||||
// cancelAndResend will call sendToDevice twice:
|
||||
// the first call to sendToDevice will be the cancellation
|
||||
// the second call to sendToDevice will be the key request
|
||||
expect(aliceSendToDevice).toBeCalledTimes(3);
|
||||
expect(aliceSendToDevice).toHaveBeenCalledTimes(3);
|
||||
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",
|
||||
@@ -613,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
|
||||
@@ -622,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;
|
||||
@@ -641,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) => {
|
||||
@@ -650,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,
|
||||
@@ -754,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",
|
||||
};
|
||||
@@ -808,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 () {
|
||||
@@ -1025,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) };
|
||||
|
||||
@@ -148,6 +148,10 @@ describe("DeviceList", function () {
|
||||
dl.invalidateUserDeviceList("@test1:sw1v.org");
|
||||
dl.refreshOutdatedDeviceLists();
|
||||
|
||||
// TODO: Fix this test so we actually await the call and assertions and remove
|
||||
// the eslint disable, https://github.com/matrix-org/matrix-js-sdk/issues/2977
|
||||
//
|
||||
// eslint-disable-next-line jest/valid-expect-in-promise
|
||||
dl.saveIfDirty()
|
||||
.then(() => {
|
||||
// the first request completes
|
||||
@@ -196,7 +200,7 @@ describe("DeviceList", function () {
|
||||
downloadSpy.mockReturnValueOnce(queryDefer2.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toBeCalledTimes(2);
|
||||
expect(downloadSpy).toHaveBeenCalledTimes(2);
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(1, ["@test1:sw1v.org"], {});
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(2, ["@test2:sw1v.org"], {});
|
||||
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 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.
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
|
||||
import type { DeviceInfoMap } from "../../../../src/crypto/DeviceList";
|
||||
import "../../../olm-loader";
|
||||
import type { OutboundGroupSession } from "@matrix-org/olm";
|
||||
import * as algorithms from "../../../../src/crypto/algorithms";
|
||||
@@ -33,6 +34,8 @@ 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")!;
|
||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
|
||||
@@ -58,6 +61,12 @@ describe("MegolmDecryption", function () {
|
||||
|
||||
beforeEach(async function () {
|
||||
mockCrypto = testUtils.mock(Crypto, "Crypto") as MockedObject<Crypto>;
|
||||
|
||||
// @ts-ignore assigning to readonly prop
|
||||
mockCrypto.backupManager = {
|
||||
backupGroupSession: () => {},
|
||||
};
|
||||
|
||||
mockBaseApis = {
|
||||
claimOneTimeKeys: jest.fn(),
|
||||
sendToDevice: jest.fn(),
|
||||
@@ -175,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(() => {
|
||||
@@ -203,7 +220,7 @@ describe("MegolmDecryption", function () {
|
||||
.then(() => {
|
||||
// check that it called encryptMessageForDevice with
|
||||
// appropriate args.
|
||||
expect(mockOlmLib.encryptMessageForDevice).toBeCalledTimes(1);
|
||||
expect(mockOlmLib.encryptMessageForDevice).toHaveBeenCalledTimes(1);
|
||||
|
||||
const call = mockOlmLib.encryptMessageForDevice.mock.calls[0];
|
||||
const payload = call[6];
|
||||
@@ -314,10 +331,6 @@ describe("MegolmDecryption", function () {
|
||||
let olmDevice: OlmDevice;
|
||||
|
||||
beforeEach(async () => {
|
||||
// @ts-ignore assigning to readonly prop
|
||||
mockCrypto.backupManager = {
|
||||
backupGroupSession: () => {},
|
||||
};
|
||||
const cryptoStore = new MemoryCryptoStore();
|
||||
|
||||
olmDevice = new OlmDevice(cryptoStore);
|
||||
@@ -353,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({
|
||||
@@ -387,8 +396,10 @@ describe("MegolmDecryption", function () {
|
||||
mockCrypto.baseApis = mockBaseApis;
|
||||
|
||||
mockRoom = {
|
||||
roomId: ROOM_ID,
|
||||
getEncryptionTargetMembers: jest.fn().mockReturnValue([{ userId: "@alice:home.server" }]),
|
||||
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
|
||||
shouldEncryptForInvitedMembers: jest.fn().mockReturnValue(false),
|
||||
} as unknown as Room;
|
||||
});
|
||||
|
||||
@@ -513,6 +524,87 @@ describe("MegolmDecryption", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("prepareToEncrypt", () => {
|
||||
let megolm: MegolmEncryptionClass;
|
||||
let room: jest.Mocked<Room>;
|
||||
|
||||
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>;
|
||||
room.getEncryptionTargetMembers.mockImplementation(async () => [
|
||||
new RoomMember(room.roomId, "@user:example.org"),
|
||||
]);
|
||||
room.getBlacklistUnverifiedDevices.mockReturnValue(false);
|
||||
|
||||
mockCrypto.downloadKeys.mockImplementation(async () => deviceMap);
|
||||
|
||||
mockCrypto.checkDeviceTrust.mockImplementation(() => new DeviceTrustLevel(true, true, true, true));
|
||||
|
||||
const olmDevice = new OlmDevice(new MemoryCryptoStore());
|
||||
megolm = new MegolmEncryptionClass({
|
||||
userId: "@user:id",
|
||||
deviceId: "12345",
|
||||
crypto: mockCrypto,
|
||||
olmDevice,
|
||||
baseApis: mockBaseApis,
|
||||
roomId: room.roomId,
|
||||
config: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
rotation_period_ms: 9_999_999,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("checks each device", async () => {
|
||||
megolm.prepareToEncrypt(room);
|
||||
//@ts-ignore private member access, gross
|
||||
await megolm.encryptionPreparation?.promise;
|
||||
|
||||
for (const [userId, devices] of deviceMap) {
|
||||
for (const deviceId of devices.keys()) {
|
||||
expect(mockCrypto.checkDeviceTrust).toHaveBeenCalledWith(userId, deviceId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("is cancellable", async () => {
|
||||
const stop = megolm.prepareToEncrypt(room);
|
||||
|
||||
const before = mockCrypto.checkDeviceTrust.mock.calls.length;
|
||||
stop();
|
||||
|
||||
// Ensure that no more devices were checked after cancellation.
|
||||
await sleep(10);
|
||||
expect(mockCrypto.checkDeviceTrust).toHaveBeenCalledTimes(before);
|
||||
});
|
||||
});
|
||||
|
||||
it("notifies devices that have been blocked", async function () {
|
||||
const aliceClient = new TestClient("@alice:example.com", "alicedevice").client;
|
||||
const bobClient1 = new TestClient("@bob:example.com", "bobdevice1").client;
|
||||
@@ -580,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",
|
||||
@@ -761,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));
|
||||
|
||||
|
||||
@@ -1148,6 +1148,6 @@ describe("userHasCrossSigningKeys", function () {
|
||||
|
||||
it("throws an error if crypto is disabled", () => {
|
||||
aliceClient["cryptoBackend"] = undefined;
|
||||
expect(() => aliceClient.userHasCrossSigningKeys()).toThrowError("encryption disabled");
|
||||
expect(() => aliceClient.userHasCrossSigningKeys()).toThrow("encryption disabled");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -215,7 +220,7 @@ describe("SAS verification", function () {
|
||||
]);
|
||||
|
||||
// make sure that it uses the preferred method
|
||||
expect(macMethod).toBe("org.matrix.msc3783.hkdf-hmac-sha256");
|
||||
expect(macMethod).toBe("hkdf-hmac-sha256.v2");
|
||||
expect(keyAgreement).toBe("curve25519-hkdf-sha256");
|
||||
|
||||
// make sure Alice and Bob verified each other
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 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.
|
||||
@@ -147,7 +147,7 @@ describe("EventTimelineSet", () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true);
|
||||
(client.supportsThreads as jest.Mock).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
@@ -179,7 +179,7 @@ describe("EventTimelineSet", () => {
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline2, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
}).toThrowError();
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it("should not add a threaded reply to the main room timeline", () => {
|
||||
@@ -206,7 +206,7 @@ describe("EventTimelineSet", () => {
|
||||
});
|
||||
|
||||
it("should allow edits to be added to thread timeline", async () => {
|
||||
jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true);
|
||||
jest.spyOn(client, "supportsThreads").mockReturnValue(true);
|
||||
jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
|
||||
Thread.hasServerSideSupport = FeatureSupport.Stable;
|
||||
|
||||
@@ -393,13 +393,13 @@ describe("EventTimelineSet", () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsExperimentalThreads as jest.Mock).mockReturnValue(true);
|
||||
(client.supportsThreads as jest.Mock).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
it("should throw if timeline set has no room", () => {
|
||||
const eventTimelineSet = new EventTimelineSet(undefined, {}, client);
|
||||
expect(() => eventTimelineSet.canContain(messageEvent)).toThrowError();
|
||||
expect(() => eventTimelineSet.canContain(messageEvent)).toThrow();
|
||||
});
|
||||
|
||||
it("should return false if timeline set is for thread but event is not threaded", () => {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2022 - 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 { ExtensibleEventType, IPartialEvent } from "../../../src/@types/extensible_events";
|
||||
import { ExtensibleEvent } from "../../../src/extensible_events_v1/ExtensibleEvent";
|
||||
|
||||
class MockEvent extends ExtensibleEvent<any> {
|
||||
public constructor(wireEvent: IPartialEvent<any>) {
|
||||
super(wireEvent);
|
||||
}
|
||||
|
||||
public serialize(): IPartialEvent<object> {
|
||||
throw new Error("Not implemented for tests");
|
||||
}
|
||||
|
||||
public isEquivalentTo(primaryEventType: ExtensibleEventType): boolean {
|
||||
throw new Error("Not implemented for tests");
|
||||
}
|
||||
}
|
||||
|
||||
describe("ExtensibleEvent", () => {
|
||||
it("should expose the wire event directly", () => {
|
||||
const input: IPartialEvent<any> = { type: "org.example.custom", content: { hello: "world" } };
|
||||
const event = new MockEvent(input);
|
||||
expect(event.wireFormat).toBe(input);
|
||||
expect(event.wireContent).toBe(input.content);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
Copyright 2022 - 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 {
|
||||
ExtensibleAnyMessageEventContent,
|
||||
IPartialEvent,
|
||||
M_HTML,
|
||||
M_MESSAGE,
|
||||
M_TEXT,
|
||||
} from "../../../src/@types/extensible_events";
|
||||
import { MessageEvent } from "../../../src/extensible_events_v1/MessageEvent";
|
||||
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
|
||||
|
||||
describe("MessageEvent", () => {
|
||||
it("should parse m.text", () => {
|
||||
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
|
||||
type: "org.example.message-like",
|
||||
content: {
|
||||
[M_TEXT.name]: "Text here",
|
||||
},
|
||||
};
|
||||
const message = new MessageEvent(input);
|
||||
expect(message.text).toBe("Text here");
|
||||
expect(message.html).toBeFalsy();
|
||||
expect(message.renderings.length).toBe(1);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
|
||||
});
|
||||
|
||||
it("should parse m.html", () => {
|
||||
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
|
||||
type: "org.example.message-like",
|
||||
content: {
|
||||
[M_TEXT.name]: "Text here",
|
||||
[M_HTML.name]: "HTML here",
|
||||
},
|
||||
};
|
||||
const message = new MessageEvent(input);
|
||||
expect(message.text).toBe("Text here");
|
||||
expect(message.html).toBe("HTML here");
|
||||
expect(message.renderings.length).toBe(2);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true);
|
||||
});
|
||||
|
||||
it("should parse m.message", () => {
|
||||
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
|
||||
type: "org.example.message-like",
|
||||
content: {
|
||||
[M_MESSAGE.name]: [
|
||||
{ body: "Text here", mimetype: "text/plain" },
|
||||
{ body: "HTML here", mimetype: "text/html" },
|
||||
{ body: "MD here", mimetype: "text/markdown" },
|
||||
],
|
||||
|
||||
// These should be ignored
|
||||
[M_TEXT.name]: "WRONG Text here",
|
||||
[M_HTML.name]: "WRONG HTML here",
|
||||
},
|
||||
};
|
||||
const message = new MessageEvent(input);
|
||||
expect(message.text).toBe("Text here");
|
||||
expect(message.html).toBe("HTML here");
|
||||
expect(message.renderings.length).toBe(3);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/markdown" && r.body === "MD here")).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail to parse missing text", () => {
|
||||
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
|
||||
type: "org.example.message-like",
|
||||
content: {
|
||||
hello: "world",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new MessageEvent(input)).toThrow(
|
||||
new InvalidEventError("Missing textual representation for event"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse missing plain text in m.message", () => {
|
||||
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
|
||||
type: "org.example.message-like",
|
||||
content: {
|
||||
[M_MESSAGE.name]: [{ body: "HTML here", mimetype: "text/html" }],
|
||||
},
|
||||
};
|
||||
expect(() => new MessageEvent(input)).toThrow(
|
||||
new InvalidEventError("m.message is missing a plain text representation"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse non-array m.message", () => {
|
||||
const input: IPartialEvent<ExtensibleAnyMessageEventContent> = {
|
||||
type: "org.example.message-like",
|
||||
content: {
|
||||
[M_MESSAGE.name]: "invalid",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new MessageEvent(input)).toThrow(new InvalidEventError("m.message contents must be an array"));
|
||||
});
|
||||
|
||||
describe("from & serialize", () => {
|
||||
it("should serialize to a legacy fallback", () => {
|
||||
const message = MessageEvent.from("Text here", "HTML here");
|
||||
expect(message.text).toBe("Text here");
|
||||
expect(message.html).toBe("HTML here");
|
||||
expect(message.renderings.length).toBe(2);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/html" && r.body === "HTML here")).toBe(true);
|
||||
|
||||
const serialized = message.serialize();
|
||||
expect(serialized.type).toBe("m.room.message");
|
||||
expect(serialized.content).toMatchObject({
|
||||
[M_MESSAGE.name]: [
|
||||
{ body: "Text here", mimetype: "text/plain" },
|
||||
{ body: "HTML here", mimetype: "text/html" },
|
||||
],
|
||||
body: "Text here",
|
||||
msgtype: "m.text",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "HTML here",
|
||||
});
|
||||
});
|
||||
|
||||
it("should serialize non-html content to a legacy fallback", () => {
|
||||
const message = MessageEvent.from("Text here");
|
||||
expect(message.text).toBe("Text here");
|
||||
expect(message.renderings.length).toBe(1);
|
||||
expect(message.renderings.some((r) => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
|
||||
|
||||
const serialized = message.serialize();
|
||||
expect(serialized.type).toBe("m.room.message");
|
||||
expect(serialized.content).toMatchObject({
|
||||
[M_TEXT.name]: "Text here",
|
||||
body: "Text here",
|
||||
msgtype: "m.text",
|
||||
format: undefined,
|
||||
formatted_body: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
Copyright 2022 - 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 { PollEndEventContent, M_POLL_END } from "../../../src/@types/polls";
|
||||
import { IPartialEvent, REFERENCE_RELATION, M_TEXT } from "../../../src/@types/extensible_events";
|
||||
import { PollEndEvent } from "../../../src/extensible_events_v1/PollEndEvent";
|
||||
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
|
||||
|
||||
describe("PollEndEvent", () => {
|
||||
// Note: throughout these tests we don't really bother testing that
|
||||
// MessageEvent is doing its job. It has its own tests to worry about.
|
||||
|
||||
it("should parse a poll closure", () => {
|
||||
const input: IPartialEvent<PollEndEventContent> = {
|
||||
type: M_POLL_END.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_END.name]: {},
|
||||
[M_TEXT.name]: "Poll closed",
|
||||
},
|
||||
};
|
||||
const event = new PollEndEvent(input);
|
||||
expect(event.pollEventId).toBe("$poll");
|
||||
expect(event.closingMessage.text).toBe("Poll closed");
|
||||
});
|
||||
|
||||
it("should fail to parse a missing relationship", () => {
|
||||
const input: IPartialEvent<PollEndEventContent> = {
|
||||
type: M_POLL_END.name,
|
||||
content: {
|
||||
[M_POLL_END.name]: {},
|
||||
[M_TEXT.name]: "Poll closed",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollEndEvent(input)).toThrow(
|
||||
new InvalidEventError("Relationship must be a reference to an event"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse a missing relationship event ID", () => {
|
||||
const input: IPartialEvent<PollEndEventContent> = {
|
||||
type: M_POLL_END.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
},
|
||||
[M_POLL_END.name]: {},
|
||||
[M_TEXT.name]: "Poll closed",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollEndEvent(input)).toThrow(
|
||||
new InvalidEventError("Relationship must be a reference to an event"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse an improper relationship", () => {
|
||||
const input: IPartialEvent<PollEndEventContent> = {
|
||||
type: M_POLL_END.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: "org.example.not-relationship",
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_END.name]: {},
|
||||
[M_TEXT.name]: "Poll closed",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollEndEvent(input)).toThrow(
|
||||
new InvalidEventError("Relationship must be a reference to an event"),
|
||||
);
|
||||
});
|
||||
|
||||
describe("from & serialize", () => {
|
||||
it("should serialize to a poll end event", () => {
|
||||
const event = PollEndEvent.from("$poll", "Poll closed");
|
||||
expect(event.pollEventId).toBe("$poll");
|
||||
expect(event.closingMessage.text).toBe("Poll closed");
|
||||
|
||||
const serialized = event.serialize();
|
||||
expect(M_POLL_END.matches(serialized.type)).toBe(true);
|
||||
expect(serialized.content).toMatchObject({
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_END.name]: {},
|
||||
[M_TEXT.name]: expect.any(String), // tested by MessageEvent tests
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
Copyright 2022 - 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 { M_TEXT, IPartialEvent, REFERENCE_RELATION } from "../../../src/@types/extensible_events";
|
||||
import {
|
||||
M_POLL_START,
|
||||
M_POLL_KIND_DISCLOSED,
|
||||
PollResponseEventContent,
|
||||
M_POLL_RESPONSE,
|
||||
} from "../../../src/@types/polls";
|
||||
import { PollStartEvent } from "../../../src/extensible_events_v1/PollStartEvent";
|
||||
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
|
||||
import { PollResponseEvent } from "../../../src/extensible_events_v1/PollResponseEvent";
|
||||
|
||||
const SAMPLE_POLL = new PollStartEvent({
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
{ id: "one", [M_TEXT.name]: "ONE" },
|
||||
{ id: "two", [M_TEXT.name]: "TWO" },
|
||||
{ id: "thr", [M_TEXT.name]: "THR" },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe("PollResponseEvent", () => {
|
||||
it("should parse a poll response", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["one"],
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(false);
|
||||
expect(response.answerIds).toMatchObject(["one"]);
|
||||
expect(response.pollEventId).toBe("$poll");
|
||||
});
|
||||
|
||||
it("should fail to parse a missing relationship", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["one"],
|
||||
},
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollResponseEvent(input)).toThrow(
|
||||
new InvalidEventError("Relationship must be a reference to an event"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse a missing relationship event ID", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["one"],
|
||||
},
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollResponseEvent(input)).toThrow(
|
||||
new InvalidEventError("Relationship must be a reference to an event"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse an improper relationship", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: "org.example.not-relationship",
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["one"],
|
||||
},
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollResponseEvent(input)).toThrow(
|
||||
new InvalidEventError("Relationship must be a reference to an event"),
|
||||
);
|
||||
});
|
||||
|
||||
describe("validateAgainst", () => {
|
||||
it("should spoil the vote when no answers", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {},
|
||||
} as any, // force invalid type
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(true);
|
||||
|
||||
response.validateAgainst(SAMPLE_POLL);
|
||||
expect(response.spoiled).toBe(true);
|
||||
});
|
||||
|
||||
it("should spoil the vote when answers are empty", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(true);
|
||||
|
||||
response.validateAgainst(SAMPLE_POLL);
|
||||
expect(response.spoiled).toBe(true);
|
||||
});
|
||||
|
||||
it("should spoil the vote when answers are not strings", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: [1, 2, 3],
|
||||
},
|
||||
} as any, // force invalid type
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(true);
|
||||
|
||||
response.validateAgainst(SAMPLE_POLL);
|
||||
expect(response.spoiled).toBe(true);
|
||||
});
|
||||
|
||||
describe("consumer usage", () => {
|
||||
it("should spoil the vote when invalid answers are given", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["A", "B", "C"],
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(false); // it won't know better
|
||||
|
||||
response.validateAgainst(SAMPLE_POLL);
|
||||
expect(response.spoiled).toBe(true);
|
||||
});
|
||||
|
||||
it("should truncate answers to the poll max selections", () => {
|
||||
const input: IPartialEvent<PollResponseEventContent> = {
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["one", "two", "thr"],
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = new PollResponseEvent(input);
|
||||
expect(response.spoiled).toBe(false); // it won't know better
|
||||
expect(response.answerIds).toMatchObject(["one", "two", "thr"]);
|
||||
|
||||
response.validateAgainst(SAMPLE_POLL);
|
||||
expect(response.spoiled).toBe(false);
|
||||
expect(response.answerIds).toMatchObject(["one", "two"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("from & serialize", () => {
|
||||
it("should serialize to a poll response event", () => {
|
||||
const response = PollResponseEvent.from(["A", "B", "C"], "$poll");
|
||||
expect(response.spoiled).toBe(false);
|
||||
expect(response.answerIds).toMatchObject(["A", "B", "C"]);
|
||||
expect(response.pollEventId).toBe("$poll");
|
||||
|
||||
const serialized = response.serialize();
|
||||
expect(M_POLL_RESPONSE.matches(serialized.type)).toBe(true);
|
||||
expect(serialized.content).toMatchObject({
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: ["A", "B", "C"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should serialize a spoiled vote", () => {
|
||||
const response = PollResponseEvent.from([], "$poll");
|
||||
expect(response.spoiled).toBe(true);
|
||||
expect(response.answerIds).toMatchObject([]);
|
||||
expect(response.pollEventId).toBe("$poll");
|
||||
|
||||
const serialized = response.serialize();
|
||||
expect(M_POLL_RESPONSE.matches(serialized.type)).toBe(true);
|
||||
expect(serialized.content).toMatchObject({
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: "$poll",
|
||||
},
|
||||
[M_POLL_RESPONSE.name]: {
|
||||
answers: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
Copyright 2022 - 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 { M_TEXT, IPartialEvent } from "../../../src/@types/extensible_events";
|
||||
import {
|
||||
M_POLL_START,
|
||||
M_POLL_KIND_DISCLOSED,
|
||||
PollAnswer,
|
||||
PollStartEventContent,
|
||||
M_POLL_KIND_UNDISCLOSED,
|
||||
} from "../../../src/@types/polls";
|
||||
import { PollStartEvent, PollAnswerSubevent } from "../../../src/extensible_events_v1/PollStartEvent";
|
||||
import { InvalidEventError } from "../../../src/extensible_events_v1/InvalidEventError";
|
||||
|
||||
describe("PollAnswerSubevent", () => {
|
||||
// Note: throughout these tests we don't really bother testing that
|
||||
// MessageEvent is doing its job. It has its own tests to worry about.
|
||||
|
||||
it("should parse an answer representation", () => {
|
||||
const input: IPartialEvent<PollAnswer> = {
|
||||
type: "org.matrix.sdk.poll.answer",
|
||||
content: {
|
||||
id: "one",
|
||||
[M_TEXT.name]: "ONE",
|
||||
},
|
||||
};
|
||||
const answer = new PollAnswerSubevent(input);
|
||||
expect(answer.id).toBe("one");
|
||||
expect(answer.text).toBe("ONE");
|
||||
});
|
||||
|
||||
it("should fail to parse answers without an ID", () => {
|
||||
const input: IPartialEvent<PollAnswer> = {
|
||||
type: "org.matrix.sdk.poll.answer",
|
||||
content: {
|
||||
[M_TEXT.name]: "ONE",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollAnswerSubevent(input)).toThrow(
|
||||
new InvalidEventError("Answer ID must be a non-empty string"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail to parse answers without text", () => {
|
||||
const input: IPartialEvent<PollAnswer> = {
|
||||
type: "org.matrix.sdk.poll.answer",
|
||||
content: {
|
||||
id: "one",
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollAnswerSubevent(input)).toThrow(); // we don't check message - that'll be MessageEvent's problem
|
||||
});
|
||||
|
||||
describe("from & serialize", () => {
|
||||
it("should serialize to a placeholder representation", () => {
|
||||
const answer = PollAnswerSubevent.from("one", "ONE");
|
||||
expect(answer.id).toBe("one");
|
||||
expect(answer.text).toBe("ONE");
|
||||
|
||||
const serialized = answer.serialize();
|
||||
expect(serialized.type).toBe("org.matrix.sdk.poll.answer");
|
||||
expect(serialized.content).toMatchObject({
|
||||
id: "one",
|
||||
[M_TEXT.name]: expect.any(String), // tested by MessageEvent
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("PollStartEvent", () => {
|
||||
// Note: throughout these tests we don't really bother testing that
|
||||
// MessageEvent is doing its job. It has its own tests to worry about.
|
||||
|
||||
it("should parse a poll", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
{ id: "one", [M_TEXT.name]: "ONE" },
|
||||
{ id: "two", [M_TEXT.name]: "TWO" },
|
||||
{ id: "thr", [M_TEXT.name]: "THR" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const poll = new PollStartEvent(input);
|
||||
expect(poll.question).toBeDefined();
|
||||
expect(poll.question.text).toBe("Question here");
|
||||
expect(poll.kind).toBe(M_POLL_KIND_DISCLOSED);
|
||||
expect(M_POLL_KIND_DISCLOSED.matches(poll.rawKind)).toBe(true);
|
||||
expect(poll.maxSelections).toBe(2);
|
||||
expect(poll.answers.length).toBe(3);
|
||||
expect(poll.answers.some((a) => a.id === "one" && a.text === "ONE")).toBe(true);
|
||||
expect(poll.answers.some((a) => a.id === "two" && a.text === "TWO")).toBe(true);
|
||||
expect(poll.answers.some((a) => a.id === "thr" && a.text === "THR")).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail to parse a missing question", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
{ id: "one", [M_TEXT.name]: "ONE" },
|
||||
{ id: "two", [M_TEXT.name]: "TWO" },
|
||||
{ id: "thr", [M_TEXT.name]: "THR" },
|
||||
],
|
||||
},
|
||||
} as any, // force invalid type
|
||||
};
|
||||
expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("A question is required"));
|
||||
});
|
||||
|
||||
it("should fail to parse non-array answers", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: "one",
|
||||
} as any, // force invalid type
|
||||
},
|
||||
};
|
||||
expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("Poll answers must be an array"));
|
||||
});
|
||||
|
||||
it("should fail to parse invalid answers", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [{ id: "one" }, { [M_TEXT.name]: "TWO" }],
|
||||
} as any, // force invalid type
|
||||
},
|
||||
};
|
||||
expect(() => new PollStartEvent(input)).toThrow(); // error tested by PollAnswerSubevent tests
|
||||
});
|
||||
|
||||
it("should fail to parse lack of answers", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [],
|
||||
} as any, // force invalid type
|
||||
},
|
||||
};
|
||||
expect(() => new PollStartEvent(input)).toThrow(new InvalidEventError("No answers available"));
|
||||
});
|
||||
|
||||
it("should truncate answers at 20", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
{ id: "01", [M_TEXT.name]: "A" },
|
||||
{ id: "02", [M_TEXT.name]: "B" },
|
||||
{ id: "03", [M_TEXT.name]: "C" },
|
||||
{ id: "04", [M_TEXT.name]: "D" },
|
||||
{ id: "05", [M_TEXT.name]: "E" },
|
||||
{ id: "06", [M_TEXT.name]: "F" },
|
||||
{ id: "07", [M_TEXT.name]: "G" },
|
||||
{ id: "08", [M_TEXT.name]: "H" },
|
||||
{ id: "09", [M_TEXT.name]: "I" },
|
||||
{ id: "10", [M_TEXT.name]: "J" },
|
||||
{ id: "11", [M_TEXT.name]: "K" },
|
||||
{ id: "12", [M_TEXT.name]: "L" },
|
||||
{ id: "13", [M_TEXT.name]: "M" },
|
||||
{ id: "14", [M_TEXT.name]: "N" },
|
||||
{ id: "15", [M_TEXT.name]: "O" },
|
||||
{ id: "16", [M_TEXT.name]: "P" },
|
||||
{ id: "17", [M_TEXT.name]: "Q" },
|
||||
{ id: "18", [M_TEXT.name]: "R" },
|
||||
{ id: "19", [M_TEXT.name]: "S" },
|
||||
{ id: "20", [M_TEXT.name]: "T" },
|
||||
{ id: "FAIL", [M_TEXT.name]: "U" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const poll = new PollStartEvent(input);
|
||||
expect(poll.answers.length).toBe(20);
|
||||
expect(poll.answers.some((a) => a.id === "FAIL")).toBe(false);
|
||||
});
|
||||
|
||||
it("should infer a kind from unknown kinds", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
kind: "org.example.custom.poll.kind",
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
{ id: "01", [M_TEXT.name]: "A" },
|
||||
{ id: "02", [M_TEXT.name]: "B" },
|
||||
{ id: "03", [M_TEXT.name]: "C" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const poll = new PollStartEvent(input);
|
||||
expect(poll.kind).toBe(M_POLL_KIND_UNDISCLOSED);
|
||||
expect(poll.rawKind).toBe("org.example.custom.poll.kind");
|
||||
});
|
||||
|
||||
it("should infer a kind from missing kinds", () => {
|
||||
const input: IPartialEvent<PollStartEventContent> = {
|
||||
type: M_POLL_START.name,
|
||||
content: {
|
||||
[M_TEXT.name]: "FALLBACK Question here",
|
||||
[M_POLL_START.name]: {
|
||||
question: { [M_TEXT.name]: "Question here" },
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
{ id: "01", [M_TEXT.name]: "A" },
|
||||
{ id: "02", [M_TEXT.name]: "B" },
|
||||
{ id: "03", [M_TEXT.name]: "C" },
|
||||
],
|
||||
} as any, // force invalid type
|
||||
},
|
||||
};
|
||||
const poll = new PollStartEvent(input);
|
||||
expect(poll.kind).toBe(M_POLL_KIND_UNDISCLOSED);
|
||||
expect(poll.rawKind).toBeFalsy();
|
||||
});
|
||||
|
||||
describe("from & serialize", () => {
|
||||
it("should serialize to a poll start event", () => {
|
||||
const poll = PollStartEvent.from("Question here", ["A", "B", "C"], M_POLL_KIND_DISCLOSED, 2);
|
||||
expect(poll.question.text).toBe("Question here");
|
||||
expect(poll.kind).toBe(M_POLL_KIND_DISCLOSED);
|
||||
expect(M_POLL_KIND_DISCLOSED.matches(poll.rawKind)).toBe(true);
|
||||
expect(poll.maxSelections).toBe(2);
|
||||
expect(poll.answers.length).toBe(3);
|
||||
expect(poll.answers.some((a) => a.text === "A")).toBe(true);
|
||||
expect(poll.answers.some((a) => a.text === "B")).toBe(true);
|
||||
expect(poll.answers.some((a) => a.text === "C")).toBe(true);
|
||||
|
||||
// Ids are non-empty and unique
|
||||
expect(poll.answers[0].id).toHaveLength(16);
|
||||
expect(poll.answers[1].id).toHaveLength(16);
|
||||
expect(poll.answers[2].id).toHaveLength(16);
|
||||
expect(poll.answers[0].id).not.toEqual(poll.answers[1].id);
|
||||
expect(poll.answers[0].id).not.toEqual(poll.answers[2].id);
|
||||
expect(poll.answers[1].id).not.toEqual(poll.answers[2].id);
|
||||
|
||||
const serialized = poll.serialize();
|
||||
expect(M_POLL_START.matches(serialized.type)).toBe(true);
|
||||
expect(serialized.content).toMatchObject({
|
||||
[M_TEXT.name]: "Question here\n1. A\n2. B\n3. C",
|
||||
[M_POLL_START.name]: {
|
||||
question: {
|
||||
[M_TEXT.name]: expect.any(String), // tested by MessageEvent tests
|
||||
},
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
// M_TEXT tested by MessageEvent tests
|
||||
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
|
||||
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
|
||||
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should serialize to a custom kind poll start event", () => {
|
||||
const poll = PollStartEvent.from("Question here", ["A", "B", "C"], "org.example.poll.kind", 2);
|
||||
expect(poll.question.text).toBe("Question here");
|
||||
expect(poll.kind).toBe(M_POLL_KIND_UNDISCLOSED);
|
||||
expect(poll.rawKind).toBe("org.example.poll.kind");
|
||||
expect(poll.maxSelections).toBe(2);
|
||||
expect(poll.answers.length).toBe(3);
|
||||
expect(poll.answers.some((a) => a.text === "A")).toBe(true);
|
||||
expect(poll.answers.some((a) => a.text === "B")).toBe(true);
|
||||
expect(poll.answers.some((a) => a.text === "C")).toBe(true);
|
||||
|
||||
const serialized = poll.serialize();
|
||||
expect(M_POLL_START.matches(serialized.type)).toBe(true);
|
||||
expect(serialized.content).toMatchObject({
|
||||
[M_TEXT.name]: "Question here\n1. A\n2. B\n3. C",
|
||||
[M_POLL_START.name]: {
|
||||
question: {
|
||||
[M_TEXT.name]: expect.any(String), // tested by MessageEvent tests
|
||||
},
|
||||
kind: "org.example.poll.kind",
|
||||
max_selections: 2,
|
||||
answers: [
|
||||
// M_MESSAGE tested by MessageEvent tests
|
||||
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
|
||||
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
|
||||
{ id: expect.any(String), [M_TEXT.name]: expect.any(String) },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
Copyright 2022 - 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 { NamespacedValue } from "matrix-events-sdk";
|
||||
|
||||
import { isEventTypeSame } from "../../../src/@types/extensible_events";
|
||||
|
||||
describe("isEventTypeSame", () => {
|
||||
it("should match string and string", () => {
|
||||
const a = "org.example.message-like";
|
||||
const b = "org.example.different";
|
||||
|
||||
expect(isEventTypeSame(a, b)).toBe(false);
|
||||
expect(isEventTypeSame(b, a)).toBe(false);
|
||||
|
||||
expect(isEventTypeSame(a, a)).toBe(true);
|
||||
expect(isEventTypeSame(b, b)).toBe(true);
|
||||
});
|
||||
|
||||
it("should match string and namespace", () => {
|
||||
const a = "org.example.message-like";
|
||||
const b = new NamespacedValue<string, string>("org.example.stable", "org.example.unstable");
|
||||
|
||||
expect(isEventTypeSame(a, b)).toBe(false);
|
||||
expect(isEventTypeSame(b, a)).toBe(false);
|
||||
|
||||
expect(isEventTypeSame(a, a)).toBe(true);
|
||||
expect(isEventTypeSame(b, b)).toBe(true);
|
||||
expect(isEventTypeSame(b.name, b)).toBe(true);
|
||||
expect(isEventTypeSame(b.altName, b)).toBe(true);
|
||||
expect(isEventTypeSame(b, b.name)).toBe(true);
|
||||
expect(isEventTypeSame(b, b.altName)).toBe(true);
|
||||
});
|
||||
|
||||
it("should match namespace and namespace", () => {
|
||||
const a = new NamespacedValue<string, string>("org.example.stable1", "org.example.unstable1");
|
||||
const b = new NamespacedValue<string, string>("org.example.stable2", "org.example.unstable2");
|
||||
|
||||
expect(isEventTypeSame(a, b)).toBe(false);
|
||||
expect(isEventTypeSame(b, a)).toBe(false);
|
||||
|
||||
expect(isEventTypeSame(a, a)).toBe(true);
|
||||
expect(isEventTypeSame(a.name, a)).toBe(true);
|
||||
expect(isEventTypeSame(a.altName, a)).toBe(true);
|
||||
expect(isEventTypeSame(a, a.name)).toBe(true);
|
||||
expect(isEventTypeSame(a, a.altName)).toBe(true);
|
||||
|
||||
expect(isEventTypeSame(b, b)).toBe(true);
|
||||
expect(isEventTypeSame(b.name, b)).toBe(true);
|
||||
expect(isEventTypeSame(b.altName, b)).toBe(true);
|
||||
expect(isEventTypeSame(b, b.name)).toBe(true);
|
||||
expect(isEventTypeSame(b, b.altName)).toBe(true);
|
||||
});
|
||||
|
||||
it("should match namespaces of different pointers", () => {
|
||||
const a = new NamespacedValue<string, string>("org.example.stable", "org.example.unstable");
|
||||
const b = new NamespacedValue<string, string>("org.example.stable", "org.example.unstable");
|
||||
|
||||
expect(isEventTypeSame(a, b)).toBe(true);
|
||||
expect(isEventTypeSame(b, a)).toBe(true);
|
||||
|
||||
expect(isEventTypeSame(a, a)).toBe(true);
|
||||
expect(isEventTypeSame(a.name, a)).toBe(true);
|
||||
expect(isEventTypeSame(a.altName, a)).toBe(true);
|
||||
expect(isEventTypeSame(a, a.name)).toBe(true);
|
||||
expect(isEventTypeSame(a, a.altName)).toBe(true);
|
||||
|
||||
expect(isEventTypeSame(b, b)).toBe(true);
|
||||
expect(isEventTypeSame(b.name, b)).toBe(true);
|
||||
expect(isEventTypeSame(b.altName, b)).toBe(true);
|
||||
expect(isEventTypeSame(b, b.name)).toBe(true);
|
||||
expect(isEventTypeSame(b, b.altName)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -78,11 +78,11 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
expect(doRequest).toHaveBeenCalledTimes(1);
|
||||
expect(stateUpdated).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should handle auth errcode presence ", async () => {
|
||||
it("should handle auth errcode presence", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
|
||||
@@ -128,8 +128,8 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
expect(doRequest).toHaveBeenCalledTimes(1);
|
||||
expect(stateUpdated).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should handle set emailSid for email flow", async () => {
|
||||
@@ -180,9 +180,9 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
expect(requestEmailToken).toBeCalledTimes(0);
|
||||
expect(doRequest).toHaveBeenCalledTimes(1);
|
||||
expect(stateUpdated).toHaveBeenCalledTimes(1);
|
||||
expect(requestEmailToken).toHaveBeenCalledTimes(0);
|
||||
expect(ia.getEmailSid()).toBe("myEmailSid");
|
||||
});
|
||||
|
||||
@@ -244,8 +244,8 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(2);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
expect(doRequest).toHaveBeenCalledTimes(2);
|
||||
expect(stateUpdated).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should make a request if authdata is null", async () => {
|
||||
@@ -306,8 +306,8 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(2);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
expect(doRequest).toHaveBeenCalledTimes(2);
|
||||
expect(stateUpdated).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should start an auth stage and reject if no auth flow", async () => {
|
||||
@@ -430,8 +430,8 @@ describe("InteractiveAuth", () => {
|
||||
|
||||
const res = await ia.attemptAuth();
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(0);
|
||||
expect(doRequest).toHaveBeenCalledTimes(1);
|
||||
expect(stateUpdated).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
describe("requestEmailToken", () => {
|
||||
@@ -464,35 +464,6 @@ describe("InteractiveAuth", () => {
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined);
|
||||
});
|
||||
|
||||
it("increases auth attempts", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
requestEmailToken.mockImplementation(async () => ({ sid: "" }));
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: getFakeClient(),
|
||||
doRequest,
|
||||
stateUpdated,
|
||||
requestEmailToken,
|
||||
});
|
||||
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 1, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 2, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 3, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 4, undefined);
|
||||
requestEmailToken.mockClear();
|
||||
await ia.requestEmailToken();
|
||||
expect(requestEmailToken).toHaveBeenLastCalledWith(undefined, ia.getClientSecret(), 5, undefined);
|
||||
});
|
||||
|
||||
it("passes errors through", async () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
@@ -508,7 +479,7 @@ describe("InteractiveAuth", () => {
|
||||
requestEmailToken,
|
||||
});
|
||||
|
||||
await expect(ia.requestEmailToken.bind(ia)).rejects.toThrowError("unspecific network error");
|
||||
await expect(ia.requestEmailToken.bind(ia)).rejects.toThrow("unspecific network error");
|
||||
});
|
||||
|
||||
it("only starts one request at a time", async () => {
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
M_TIMESTAMP,
|
||||
LocationEventWireContent,
|
||||
} from "../../src/@types/location";
|
||||
import { TEXT_NODE_TYPE } from "../../src/@types/extensible_events";
|
||||
import { M_TEXT } from "../../src/@types/extensible_events";
|
||||
import { MsgType } from "../../src/@types/event";
|
||||
|
||||
describe("Location", function () {
|
||||
@@ -32,7 +32,7 @@ describe("Location", function () {
|
||||
geo_uri: "geo:-36.24484561954707,175.46884959563613;u=10",
|
||||
[M_LOCATION.name]: { uri: "geo:-36.24484561954707,175.46884959563613;u=10", description: null },
|
||||
[M_ASSET.name]: { type: "m.self" },
|
||||
[TEXT_NODE_TYPE.name]: "Location geo:-36.24484561954707,175.46884959563613;u=10 at 2022-03-09T11:01:52.443Z",
|
||||
[M_TEXT.name]: "Location geo:-36.24484561954707,175.46884959563613;u=10 at 2022-03-09T11:01:52.443Z",
|
||||
[M_TIMESTAMP.name]: 1646823712443,
|
||||
} as any;
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("Location", function () {
|
||||
description: undefined,
|
||||
});
|
||||
expect(M_ASSET.findIn(loc)).toEqual({ type: LocationAssetType.Self });
|
||||
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual("User Location geo:foo at 1970-01-02T13:17:15.435Z");
|
||||
expect(M_TEXT.findIn(loc)).toEqual("User Location geo:foo at 1970-01-02T13:17:15.435Z");
|
||||
expect(M_TIMESTAMP.findIn(loc)).toEqual(134235435);
|
||||
});
|
||||
|
||||
@@ -74,7 +74,7 @@ describe("Location", function () {
|
||||
description: "desc",
|
||||
});
|
||||
expect(M_ASSET.findIn(loc)).toEqual({ type: LocationAssetType.Pin });
|
||||
expect(TEXT_NODE_TYPE.findIn(loc)).toEqual('Location "desc" geo:bar at 1970-01-02T13:17:15.436Z');
|
||||
expect(M_TEXT.findIn(loc)).toEqual('Location "desc" geo:bar at 1970-01-02T13:17:15.436Z');
|
||||
expect(M_TIMESTAMP.findIn(loc)).toEqual(134235436);
|
||||
});
|
||||
|
||||
|
||||
+532
-116
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 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.
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { logger } from "../../src/logger";
|
||||
import { ClientEvent, ITurnServerResponse, MatrixClient, Store } from "../../src/client";
|
||||
import { ClientEvent, IMatrixClientCreateOpts, ITurnServerResponse, MatrixClient, Store } from "../../src/client";
|
||||
import { Filter } from "../../src/filter";
|
||||
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
|
||||
import {
|
||||
@@ -276,7 +276,7 @@ describe("MatrixClient", function () {
|
||||
);
|
||||
}
|
||||
|
||||
function makeClient() {
|
||||
function makeClient(opts?: Partial<IMatrixClientCreateOpts>) {
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: identityServerUrl,
|
||||
@@ -285,6 +285,7 @@ describe("MatrixClient", function () {
|
||||
store: store,
|
||||
scheduler: scheduler,
|
||||
userId: userId,
|
||||
...(opts || {}),
|
||||
});
|
||||
// FIXME: We shouldn't be yanking http like this.
|
||||
client.http = (["authedRequest", "getContentUri", "request", "uploadContent"] as const).reduce((r, k) => {
|
||||
@@ -892,7 +893,7 @@ describe("MatrixClient", function () {
|
||||
describe("getOrCreateFilter", function () {
|
||||
it("should POST createFilter if no id is present in localStorage", function () {});
|
||||
it("should use an existing filter if id is present in localStorage", function () {});
|
||||
it("should handle localStorage filterId missing from the server", function (done) {
|
||||
it("should handle localStorage filterId missing from the server", async () => {
|
||||
function getFilterName(userId: string, suffix?: string) {
|
||||
// scope this on the user ID because people may login on many accounts
|
||||
// and they all need to be stored!
|
||||
@@ -918,10 +919,8 @@ describe("MatrixClient", function () {
|
||||
client.store.setFilterIdByName(filterName, invalidFilterId);
|
||||
const filter = new Filter(client.credentials.userId);
|
||||
|
||||
client.getOrCreateFilter(filterName, filter).then(function (filterId) {
|
||||
expect(filterId).toEqual(FILTER_RESPONSE.data?.filter_id);
|
||||
done();
|
||||
});
|
||||
const filterId = await client.getOrCreateFilter(filterName, filter);
|
||||
expect(filterId).toEqual(FILTER_RESPONSE.data?.filter_id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -932,7 +931,7 @@ describe("MatrixClient", function () {
|
||||
expect(client.retryImmediately()).toBe(false);
|
||||
});
|
||||
|
||||
it("should work on /filter", function (done) {
|
||||
it("should work on /filter", async () => {
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push({
|
||||
@@ -943,23 +942,26 @@ describe("MatrixClient", function () {
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(2);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener(ClientEvent.Sync, syncListener);
|
||||
done();
|
||||
} else {
|
||||
// unexpected state transition!
|
||||
expect(state).toEqual(null);
|
||||
}
|
||||
const wasPreparedPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(2);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener(ClientEvent.Sync, syncListener);
|
||||
resolve(null);
|
||||
} else {
|
||||
// unexpected state transition!
|
||||
expect(state).toEqual(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
client.startClient();
|
||||
await client.startClient();
|
||||
await wasPreparedPromise;
|
||||
});
|
||||
|
||||
it("should work on /sync", function (done) {
|
||||
it("should work on /sync", async () => {
|
||||
httpLookups.push({
|
||||
method: "GET",
|
||||
path: "/sync",
|
||||
@@ -971,22 +973,25 @@ describe("MatrixClient", function () {
|
||||
data: SYNC_DATA,
|
||||
});
|
||||
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(1);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
|
||||
jest.advanceTimersByTime(10000);
|
||||
} else if (state === "SYNCING" && httpLookups.length === 0) {
|
||||
client.removeListener(ClientEvent.Sync, syncListener);
|
||||
done();
|
||||
}
|
||||
const isSyncingPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(1);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
|
||||
jest.advanceTimersByTime(10000);
|
||||
} else if (state === "SYNCING" && httpLookups.length === 0) {
|
||||
client.removeListener(ClientEvent.Sync, syncListener);
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
client.startClient();
|
||||
await client.startClient();
|
||||
await isSyncingPromise;
|
||||
});
|
||||
|
||||
it("should work on /pushrules", function (done) {
|
||||
it("should work on /pushrules", async () => {
|
||||
httpLookups = [];
|
||||
httpLookups.push({
|
||||
method: "GET",
|
||||
@@ -997,20 +1002,23 @@ describe("MatrixClient", function () {
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(3);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener(ClientEvent.Sync, syncListener);
|
||||
done();
|
||||
} else {
|
||||
// unexpected state transition!
|
||||
expect(state).toEqual(null);
|
||||
}
|
||||
const wasPreparedPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, function syncListener(state) {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(3);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener(ClientEvent.Sync, syncListener);
|
||||
resolve(null);
|
||||
} else {
|
||||
// unexpected state transition!
|
||||
expect(state).toEqual(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
client.startClient();
|
||||
await client.startClient();
|
||||
await wasPreparedPromise;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1034,14 +1042,17 @@ describe("MatrixClient", function () {
|
||||
};
|
||||
}
|
||||
|
||||
it("should transition null -> PREPARED after the first /sync", function (done) {
|
||||
it("should transition null -> PREPARED after the first /sync", async () => {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
await didSyncPromise;
|
||||
});
|
||||
|
||||
it("should transition null -> ERROR after a failed /filter", function (done) {
|
||||
it("should transition null -> ERROR after a failed /filter", async () => {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
@@ -1051,14 +1062,17 @@ describe("MatrixClient", function () {
|
||||
error: { errcode: "NOPE_NOPE_NOPE" },
|
||||
});
|
||||
expectedStates.push(["ERROR", null]);
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
await didSyncPromise;
|
||||
});
|
||||
|
||||
// Disabled because now `startClient` makes a legit call to `/versions`
|
||||
// And those tests are really unhappy about it... Not possible to figure
|
||||
// out what a good resolution would look like
|
||||
xit("should transition ERROR -> CATCHUP after /sync if prev failed", function (done) {
|
||||
it.skip("should transition ERROR -> CATCHUP after /sync if prev failed", async () => {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
acceptKeepalives = false;
|
||||
httpLookups = [];
|
||||
@@ -1088,19 +1102,25 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["CATCHUP", "ERROR"]);
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
await didSyncPromise;
|
||||
});
|
||||
|
||||
it("should transition PREPARED -> SYNCING after /sync", function (done) {
|
||||
it("should transition PREPARED -> SYNCING after /sync", async () => {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
await didSyncPromise;
|
||||
});
|
||||
|
||||
xit("should transition SYNCING -> ERROR after a failed /sync", function (done) {
|
||||
it.skip("should transition SYNCING -> ERROR after a failed /sync", async () => {
|
||||
acceptKeepalives = false;
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups.push({
|
||||
@@ -1118,11 +1138,14 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["RECONNECTING", "SYNCING"]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
await didSyncPromise;
|
||||
});
|
||||
|
||||
xit("should transition ERROR -> SYNCING after /sync if prev failed", function (done) {
|
||||
it.skip("should transition ERROR -> SYNCING after /sync if prev failed", async () => {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups.push({
|
||||
method: "GET",
|
||||
@@ -1134,11 +1157,14 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
await didSyncPromise;
|
||||
});
|
||||
|
||||
it("should transition SYNCING -> SYNCING on subsequent /sync successes", function (done) {
|
||||
it("should transition SYNCING -> SYNCING on subsequent /sync successes", async () => {
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
@@ -1146,11 +1172,14 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["SYNCING", "SYNCING"]);
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
await didSyncPromise;
|
||||
});
|
||||
|
||||
xit("should transition ERROR -> ERROR if keepalive keeps failing", function (done) {
|
||||
it.skip("should transition ERROR -> ERROR if keepalive keeps failing", async () => {
|
||||
acceptKeepalives = false;
|
||||
const expectedStates: [string, string | null][] = [];
|
||||
httpLookups.push({
|
||||
@@ -1174,8 +1203,11 @@ describe("MatrixClient", function () {
|
||||
expectedStates.push(["RECONNECTING", "SYNCING"]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["ERROR", "ERROR"]);
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
const didSyncPromise = new Promise((resolve) => {
|
||||
client.on(ClientEvent.Sync, syncChecker(expectedStates, resolve));
|
||||
});
|
||||
await client.startClient();
|
||||
await didSyncPromise;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1213,7 +1245,7 @@ describe("MatrixClient", function () {
|
||||
expect(httpLookups.length).toBe(0);
|
||||
});
|
||||
|
||||
xit("should be able to peek into a room using peekInRoom", function (done) {});
|
||||
it.skip("should be able to peek into a room using peekInRoom", function () {});
|
||||
});
|
||||
|
||||
describe("getPresence", function () {
|
||||
@@ -1333,7 +1365,7 @@ describe("MatrixClient", function () {
|
||||
client.redactEvent(roomId, eventId, txnId, {
|
||||
with_relations: [RelationType.Reference],
|
||||
});
|
||||
}).toThrowError(
|
||||
}).toThrow(
|
||||
new Error(
|
||||
"Server does not support relation based redactions " +
|
||||
`roomId ${roomId} eventId ${eventId} txnId: ${txnId} threadId null`,
|
||||
@@ -1414,7 +1446,7 @@ describe("MatrixClient", function () {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
client.crypto = {
|
||||
client.crypto = client["cryptoBackend"] = {
|
||||
// mock crypto
|
||||
encryptEvent: () => new Promise(() => {}),
|
||||
stop: jest.fn(),
|
||||
@@ -1436,8 +1468,9 @@ describe("MatrixClient", function () {
|
||||
|
||||
it("should cancel an event which is encrypting", async () => {
|
||||
// @ts-ignore protected method access
|
||||
client.encryptAndSendEvent(null, event);
|
||||
client.encryptAndSendEvent(mockRoom, event);
|
||||
await testUtils.emitPromise(event, "Event.status");
|
||||
expect(event.status).toBe(EventStatus.ENCRYPTING);
|
||||
client.cancelPendingEvent(event);
|
||||
assertCancelled();
|
||||
});
|
||||
@@ -1456,9 +1489,20 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
|
||||
describe("threads", () => {
|
||||
it.each([
|
||||
{ startOpts: {}, hasThreadSupport: false },
|
||||
{ startOpts: { threadSupport: true }, hasThreadSupport: true },
|
||||
{ startOpts: { threadSupport: false }, hasThreadSupport: false },
|
||||
{ startOpts: { experimentalThreadSupport: true }, hasThreadSupport: true },
|
||||
{ startOpts: { experimentalThreadSupport: true, threadSupport: false }, hasThreadSupport: false },
|
||||
])("enabled thread support for the SDK instance", async ({ startOpts, hasThreadSupport }) => {
|
||||
await client.startClient(startOpts);
|
||||
expect(client.supportsThreads()).toBe(hasThreadSupport);
|
||||
});
|
||||
|
||||
it("partitions root events to room timeline and thread timeline", () => {
|
||||
const supportsExperimentalThreads = client.supportsExperimentalThreads;
|
||||
client.supportsExperimentalThreads = () => true;
|
||||
const supportsThreads = client.supportsThreads;
|
||||
client.supportsThreads = () => true;
|
||||
const room = new Room("!room1:matrix.org", client, userId);
|
||||
|
||||
const rootEvent = new MatrixEvent({
|
||||
@@ -1487,7 +1531,7 @@ describe("MatrixClient", function () {
|
||||
expect(threadEvents).toHaveLength(1);
|
||||
|
||||
// Restore method
|
||||
client.supportsExperimentalThreads = supportsExperimentalThreads;
|
||||
client.supportsThreads = supportsThreads;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2198,14 +2242,14 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getVisibleRooms", () => {
|
||||
describe("room lists and history", () => {
|
||||
function roomCreateEvent(newRoomId: string, predecessorRoomId: string): MatrixEvent {
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
"creator": "@daryl:alexandria.example.com",
|
||||
"m.federate": true,
|
||||
"predecessor": {
|
||||
event_id: "spec_is_not_clear_what_id_this_is",
|
||||
event_id: "id_of_last_event",
|
||||
room_id: predecessorRoomId,
|
||||
},
|
||||
"room_version": "9",
|
||||
@@ -2234,45 +2278,417 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
}
|
||||
|
||||
it("Returns an empty list if there are no rooms", () => {
|
||||
client.store = new StubStore();
|
||||
client.store.getRooms = () => [];
|
||||
const rooms = client.getVisibleRooms();
|
||||
expect(rooms).toHaveLength(0);
|
||||
function predecessorEvent(newRoomId: string, predecessorRoomId: string): MatrixEvent {
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
predecessor_room_id: predecessorRoomId,
|
||||
},
|
||||
event_id: `predecessor_event_id_pred_${predecessorRoomId}`,
|
||||
origin_server_ts: 1432735824653,
|
||||
room_id: newRoomId,
|
||||
sender: "@daryl:alexandria.example.com",
|
||||
state_key: "",
|
||||
type: "org.matrix.msc3946.room_predecessor",
|
||||
});
|
||||
}
|
||||
|
||||
describe("getVisibleRooms", () => {
|
||||
function setUpReplacedRooms(): {
|
||||
room1: Room;
|
||||
room2: Room;
|
||||
replacedByCreate1: Room;
|
||||
replacedByCreate2: Room;
|
||||
replacedByDynamicPredecessor1: Room;
|
||||
replacedByDynamicPredecessor2: Room;
|
||||
} {
|
||||
const room1 = new Room("room1", client, "@carol:alexandria.example.com");
|
||||
const replacedByCreate1 = new Room("replacedByCreate1", client, "@carol:alexandria.example.com");
|
||||
const replacedByCreate2 = new Room("replacedByCreate2", client, "@carol:alexandria.example.com");
|
||||
const replacedByDynamicPredecessor1 = new Room("dyn1", client, "@carol:alexandria.example.com");
|
||||
const replacedByDynamicPredecessor2 = new Room("dyn2", client, "@carol:alexandria.example.com");
|
||||
const room2 = new Room("room2", client, "@daryl:alexandria.example.com");
|
||||
client.store = new StubStore();
|
||||
client.store.getRooms = () => [
|
||||
room1,
|
||||
replacedByCreate1,
|
||||
replacedByCreate2,
|
||||
replacedByDynamicPredecessor1,
|
||||
replacedByDynamicPredecessor2,
|
||||
room2,
|
||||
];
|
||||
room1.addLiveEvents(
|
||||
[
|
||||
roomCreateEvent(room1.roomId, replacedByCreate1.roomId),
|
||||
predecessorEvent(room1.roomId, replacedByDynamicPredecessor1.roomId),
|
||||
],
|
||||
{},
|
||||
);
|
||||
room2.addLiveEvents(
|
||||
[
|
||||
roomCreateEvent(room2.roomId, replacedByCreate2.roomId),
|
||||
predecessorEvent(room2.roomId, replacedByDynamicPredecessor2.roomId),
|
||||
],
|
||||
{},
|
||||
);
|
||||
replacedByCreate1.addLiveEvents([tombstoneEvent(room1.roomId, replacedByCreate1.roomId)], {});
|
||||
replacedByCreate2.addLiveEvents([tombstoneEvent(room2.roomId, replacedByCreate2.roomId)], {});
|
||||
replacedByDynamicPredecessor1.addLiveEvents(
|
||||
[tombstoneEvent(room1.roomId, replacedByDynamicPredecessor1.roomId)],
|
||||
{},
|
||||
);
|
||||
replacedByDynamicPredecessor2.addLiveEvents(
|
||||
[tombstoneEvent(room2.roomId, replacedByDynamicPredecessor2.roomId)],
|
||||
{},
|
||||
);
|
||||
|
||||
return {
|
||||
room1,
|
||||
room2,
|
||||
replacedByCreate1,
|
||||
replacedByCreate2,
|
||||
replacedByDynamicPredecessor1,
|
||||
replacedByDynamicPredecessor2,
|
||||
};
|
||||
}
|
||||
it("Returns an empty list if there are no rooms", () => {
|
||||
client.store = new StubStore();
|
||||
client.store.getRooms = () => [];
|
||||
const rooms = client.getVisibleRooms();
|
||||
expect(rooms).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("Returns all non-replaced rooms", () => {
|
||||
const room1 = new Room("room1", client, "@carol:alexandria.example.com");
|
||||
const room2 = new Room("room2", client, "@daryl:alexandria.example.com");
|
||||
client.store = new StubStore();
|
||||
client.store.getRooms = () => [room1, room2];
|
||||
const rooms = client.getVisibleRooms();
|
||||
expect(rooms).toContain(room1);
|
||||
expect(rooms).toContain(room2);
|
||||
expect(rooms).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("Does not return replaced rooms", () => {
|
||||
// Given 4 rooms, 2 of which have been replaced
|
||||
const room1 = new Room("room1", client, "@carol:alexandria.example.com");
|
||||
const replacedRoom1 = new Room("replacedRoom1", client, "@carol:alexandria.example.com");
|
||||
const replacedRoom2 = new Room("replacedRoom2", client, "@carol:alexandria.example.com");
|
||||
const room2 = new Room("room2", client, "@daryl:alexandria.example.com");
|
||||
client.store = new StubStore();
|
||||
client.store.getRooms = () => [room1, replacedRoom1, replacedRoom2, room2];
|
||||
room1.addLiveEvents([roomCreateEvent(room1.roomId, replacedRoom1.roomId)], {});
|
||||
room2.addLiveEvents([roomCreateEvent(room2.roomId, replacedRoom2.roomId)], {});
|
||||
replacedRoom1.addLiveEvents([tombstoneEvent(room1.roomId, replacedRoom1.roomId)], {});
|
||||
replacedRoom2.addLiveEvents([tombstoneEvent(room2.roomId, replacedRoom2.roomId)], {});
|
||||
|
||||
// When we ask for the visible rooms
|
||||
const rooms = client.getVisibleRooms();
|
||||
|
||||
// Then we only get the ones that have not been replaced
|
||||
expect(rooms).not.toContain(replacedRoom1);
|
||||
expect(rooms).not.toContain(replacedRoom2);
|
||||
expect(rooms).toContain(room1);
|
||||
expect(rooms).toContain(room2);
|
||||
});
|
||||
|
||||
it("Ignores m.predecessor if we don't ask to use it", () => {
|
||||
// Given 6 rooms, 2 of which have been replaced, and 2 of which WERE
|
||||
// replaced by create events, but are now NOT replaced, because an
|
||||
// m.predecessor event has changed the room's predecessor.
|
||||
const {
|
||||
room1,
|
||||
room2,
|
||||
replacedByCreate1,
|
||||
replacedByCreate2,
|
||||
replacedByDynamicPredecessor1,
|
||||
replacedByDynamicPredecessor2,
|
||||
} = setUpReplacedRooms();
|
||||
|
||||
// When we ask for the visible rooms
|
||||
const rooms = client.getVisibleRooms(); // Don't supply msc3946ProcessDynamicPredecessor
|
||||
|
||||
// Then we only get the ones that have not been replaced
|
||||
expect(rooms).not.toContain(replacedByCreate1);
|
||||
expect(rooms).not.toContain(replacedByCreate2);
|
||||
expect(rooms).toContain(replacedByDynamicPredecessor1);
|
||||
expect(rooms).toContain(replacedByDynamicPredecessor2);
|
||||
expect(rooms).toContain(room1);
|
||||
expect(rooms).toContain(room2);
|
||||
});
|
||||
|
||||
it("Considers rooms replaced with m.predecessor events to be replaced", () => {
|
||||
// Given 6 rooms, 2 of which have been replaced, and 2 of which WERE
|
||||
// replaced by create events, but are now NOT replaced, because an
|
||||
// m.predecessor event has changed the room's predecessor.
|
||||
const {
|
||||
room1,
|
||||
room2,
|
||||
replacedByCreate1,
|
||||
replacedByCreate2,
|
||||
replacedByDynamicPredecessor1,
|
||||
replacedByDynamicPredecessor2,
|
||||
} = setUpReplacedRooms();
|
||||
|
||||
// When we ask for the visible rooms
|
||||
const useMsc3946 = true;
|
||||
const rooms = client.getVisibleRooms(useMsc3946);
|
||||
|
||||
// Then we only get the ones that have not been replaced
|
||||
expect(rooms).not.toContain(replacedByDynamicPredecessor1);
|
||||
expect(rooms).not.toContain(replacedByDynamicPredecessor2);
|
||||
expect(rooms).toContain(replacedByCreate1);
|
||||
expect(rooms).toContain(replacedByCreate2);
|
||||
expect(rooms).toContain(room1);
|
||||
expect(rooms).toContain(room2);
|
||||
});
|
||||
});
|
||||
|
||||
it("Returns all non-replaced rooms", () => {
|
||||
const room1 = new Room("room1", client, "@carol:alexandria.example.com");
|
||||
const room2 = new Room("room2", client, "@daryl:alexandria.example.com");
|
||||
client.store = new StubStore();
|
||||
client.store.getRooms = () => [room1, room2];
|
||||
const rooms = client.getVisibleRooms();
|
||||
expect(rooms).toContain(room1);
|
||||
expect(rooms).toContain(room2);
|
||||
expect(rooms).toHaveLength(2);
|
||||
});
|
||||
describe("getRoomUpgradeHistory", () => {
|
||||
/**
|
||||
* Create a chain of room history with create events and tombstones.
|
||||
*
|
||||
* @param creates include create events (default=true)
|
||||
* @param tombstones include tomstone events (default=true)
|
||||
* @returns 4 rooms chained together with tombstones and create
|
||||
* events, in order from oldest to latest.
|
||||
*/
|
||||
function createRoomHistory(creates = true, tombstones = true): [Room, Room, Room, Room] {
|
||||
const room1 = new Room("room1", client, "@carol:alexandria.example.com");
|
||||
const room2 = new Room("room2", client, "@daryl:alexandria.example.com");
|
||||
const room3 = new Room("room3", client, "@rick:helicopter.example.com");
|
||||
const room4 = new Room("room4", client, "@michonne:hawthorne.example.com");
|
||||
|
||||
it("Does not return replaced rooms", () => {
|
||||
// Given 4 rooms, 2 of which have been replaced
|
||||
const room1 = new Room("room1", client, "@carol:alexandria.example.com");
|
||||
const replacedRoom1 = new Room("replacedRoom1", client, "@carol:alexandria.example.com");
|
||||
const replacedRoom2 = new Room("replacedRoom2", client, "@carol:alexandria.example.com");
|
||||
const room2 = new Room("room2", client, "@daryl:alexandria.example.com");
|
||||
client.store = new StubStore();
|
||||
client.store.getRooms = () => [room1, replacedRoom1, replacedRoom2, room2];
|
||||
room1.addLiveEvents([roomCreateEvent(room1.roomId, replacedRoom1.roomId)], {});
|
||||
room2.addLiveEvents([roomCreateEvent(room2.roomId, replacedRoom2.roomId)], {});
|
||||
replacedRoom1.addLiveEvents([tombstoneEvent(room1.roomId, replacedRoom1.roomId)], {});
|
||||
replacedRoom2.addLiveEvents([tombstoneEvent(room2.roomId, replacedRoom2.roomId)], {});
|
||||
if (creates) {
|
||||
room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)]);
|
||||
room3.addLiveEvents([roomCreateEvent(room3.roomId, room2.roomId)]);
|
||||
room4.addLiveEvents([roomCreateEvent(room4.roomId, room3.roomId)]);
|
||||
}
|
||||
|
||||
// When we ask for the visible rooms
|
||||
const rooms = client.getVisibleRooms();
|
||||
if (tombstones) {
|
||||
room1.addLiveEvents([tombstoneEvent(room2.roomId, room1.roomId)], {});
|
||||
room2.addLiveEvents([tombstoneEvent(room3.roomId, room2.roomId)], {});
|
||||
room3.addLiveEvents([tombstoneEvent(room4.roomId, room3.roomId)], {});
|
||||
}
|
||||
|
||||
// Then we only get the ones that have not been replaced
|
||||
expect(rooms).not.toContain(replacedRoom1);
|
||||
expect(rooms).not.toContain(replacedRoom2);
|
||||
expect(rooms).toContain(room1);
|
||||
expect(rooms).toContain(room2);
|
||||
mocked(store.getRoom).mockImplementation((roomId: string) => {
|
||||
return { room1, room2, room3, room4 }[roomId] || null;
|
||||
});
|
||||
|
||||
return [room1, room2, room3, room4];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates 2 alternate chains of room history: one using create
|
||||
* events, and one using MSC2946 predecessor+tombstone events.
|
||||
*
|
||||
* Using create, history looks like:
|
||||
* room1->room2->room3->room4 (but note we do not create tombstones)
|
||||
*
|
||||
* Using predecessor+tombstone, history looks like:
|
||||
* dynRoom1->dynRoom2->room3->dynRoom4->dynRoom4
|
||||
*
|
||||
* @returns [room1, room2, room3, room4, dynRoom1, dynRoom2,
|
||||
* dynRoom4, dynRoom5].
|
||||
*/
|
||||
function createDynamicRoomHistory(): [Room, Room, Room, Room, Room, Room, Room, Room] {
|
||||
// Don't create tombstones for the old versions - we generally
|
||||
// expect only one tombstone in a room, and we are confused by
|
||||
// anything else.
|
||||
const creates = true;
|
||||
const tombstones = false;
|
||||
const [room1, room2, room3, room4] = createRoomHistory(creates, tombstones);
|
||||
const dynRoom1 = new Room("dynRoom1", client, "@rick:grimes.example.com");
|
||||
const dynRoom2 = new Room("dynRoom2", client, "@rick:grimes.example.com");
|
||||
const dynRoom4 = new Room("dynRoom4", client, "@rick:grimes.example.com");
|
||||
const dynRoom5 = new Room("dynRoom5", client, "@rick:grimes.example.com");
|
||||
|
||||
dynRoom1.addLiveEvents([tombstoneEvent(dynRoom2.roomId, dynRoom1.roomId)], {});
|
||||
dynRoom2.addLiveEvents([predecessorEvent(dynRoom2.roomId, dynRoom1.roomId)]);
|
||||
|
||||
dynRoom2.addLiveEvents([tombstoneEvent(room3.roomId, dynRoom2.roomId)], {});
|
||||
room3.addLiveEvents([predecessorEvent(room3.roomId, dynRoom2.roomId)]);
|
||||
|
||||
room3.addLiveEvents([tombstoneEvent(dynRoom4.roomId, room3.roomId)], {});
|
||||
dynRoom4.addLiveEvents([predecessorEvent(dynRoom4.roomId, room3.roomId)]);
|
||||
|
||||
dynRoom4.addLiveEvents([tombstoneEvent(dynRoom5.roomId, dynRoom4.roomId)], {});
|
||||
dynRoom5.addLiveEvents([predecessorEvent(dynRoom5.roomId, dynRoom4.roomId)]);
|
||||
|
||||
mocked(store.getRoom)
|
||||
.mockClear()
|
||||
.mockImplementation((roomId: string) => {
|
||||
return { room1, room2, room3, room4, dynRoom1, dynRoom2, dynRoom4, dynRoom5 }[roomId] || null;
|
||||
});
|
||||
|
||||
return [room1, room2, room3, room4, dynRoom1, dynRoom2, dynRoom4, dynRoom5];
|
||||
}
|
||||
|
||||
it("Returns an empty list if room does not exist", () => {
|
||||
const history = client.getRoomUpgradeHistory("roomthatdoesnotexist");
|
||||
expect(history).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("Returns just this room if there is no predecessor", () => {
|
||||
const mainRoom = new Room("mainRoom", client, "@carol:alexandria.example.com");
|
||||
mocked(store.getRoom).mockReturnValue(mainRoom);
|
||||
const history = client.getRoomUpgradeHistory(mainRoom.roomId);
|
||||
expect(history).toEqual([mainRoom]);
|
||||
});
|
||||
|
||||
it("Returns the predecessors of this room", () => {
|
||||
const [room1, room2, room3, room4] = createRoomHistory();
|
||||
const history = client.getRoomUpgradeHistory(room4.roomId);
|
||||
expect(history.map((room) => room.roomId)).toEqual([
|
||||
room1.roomId,
|
||||
room2.roomId,
|
||||
room3.roomId,
|
||||
room4.roomId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("Returns the predecessors of this room (with verify links)", () => {
|
||||
const [room1, room2, room3, room4] = createRoomHistory();
|
||||
const verifyLinks = true;
|
||||
const history = client.getRoomUpgradeHistory(room4.roomId, verifyLinks);
|
||||
expect(history.map((room) => room.roomId)).toEqual([
|
||||
room1.roomId,
|
||||
room2.roomId,
|
||||
room3.roomId,
|
||||
room4.roomId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("With verify links, rejects predecessors that don't point forwards", () => {
|
||||
// Given successors point back with create events, but
|
||||
// predecessors do not point forwards with tombstones
|
||||
const [, , , room4] = createRoomHistory(true, false);
|
||||
|
||||
// When I ask for history with verifyLinks on
|
||||
const verifyLinks = true;
|
||||
const history = client.getRoomUpgradeHistory(room4.roomId, verifyLinks);
|
||||
|
||||
// Then the predecessors are not included in the history
|
||||
expect(history.map((room) => room.roomId)).toEqual([room4.roomId]);
|
||||
});
|
||||
|
||||
it("Without verify links, includes predecessors that don't point forwards", () => {
|
||||
// Given successors point back with create events, but
|
||||
// predecessors do not point forwards with tombstones
|
||||
const [room1, room2, room3, room4] = createRoomHistory(true, false);
|
||||
|
||||
// When I ask for history with verifyLinks off
|
||||
const verifyLinks = false;
|
||||
const history = client.getRoomUpgradeHistory(room4.roomId, verifyLinks);
|
||||
|
||||
// Then the predecessors are included in the history
|
||||
expect(history.map((room) => room.roomId)).toEqual([
|
||||
room1.roomId,
|
||||
room2.roomId,
|
||||
room3.roomId,
|
||||
room4.roomId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("Returns the subsequent rooms", () => {
|
||||
const [room1, room2, room3, room4] = createRoomHistory();
|
||||
const history = client.getRoomUpgradeHistory(room1.roomId);
|
||||
expect(history.map((room) => room.roomId)).toEqual([
|
||||
room1.roomId,
|
||||
room2.roomId,
|
||||
room3.roomId,
|
||||
room4.roomId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("Returns the subsequent rooms (with verify links)", () => {
|
||||
const [room1, room2, room3, room4] = createRoomHistory();
|
||||
const verifyLinks = true;
|
||||
const history = client.getRoomUpgradeHistory(room1.roomId, verifyLinks);
|
||||
expect(history.map((room) => room.roomId)).toEqual([
|
||||
room1.roomId,
|
||||
room2.roomId,
|
||||
room3.roomId,
|
||||
room4.roomId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("With verify links, rejects successors that don't point backwards", () => {
|
||||
// Given predecessors point forwards with tombstones, but
|
||||
// successors do not point back with create events.
|
||||
const [room1, , ,] = createRoomHistory(false, true);
|
||||
|
||||
// When I ask for history with verifyLinks on
|
||||
const verifyLinks = true;
|
||||
const history = client.getRoomUpgradeHistory(room1.roomId, verifyLinks);
|
||||
|
||||
// Then the successors are not included in the history
|
||||
expect(history.map((room) => room.roomId)).toEqual([room1.roomId]);
|
||||
});
|
||||
|
||||
it("Without verify links, includes successors that don't point backwards", () => {
|
||||
// Given predecessors point forwards with tombstones, but
|
||||
// successors do not point back with create events.
|
||||
const [room1, room2, room3, room4] = createRoomHistory(false, true);
|
||||
|
||||
// When I ask for history with verifyLinks off
|
||||
const verifyLinks = false;
|
||||
const history = client.getRoomUpgradeHistory(room1.roomId, verifyLinks);
|
||||
|
||||
// Then the successors are included in the history
|
||||
expect(history.map((room) => room.roomId)).toEqual([
|
||||
room1.roomId,
|
||||
room2.roomId,
|
||||
room3.roomId,
|
||||
room4.roomId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("Returns the predecessors and subsequent rooms", () => {
|
||||
const [room1, room2, room3, room4] = createRoomHistory();
|
||||
const history = client.getRoomUpgradeHistory(room3.roomId);
|
||||
expect(history.map((room) => room.roomId)).toEqual([
|
||||
room1.roomId,
|
||||
room2.roomId,
|
||||
room3.roomId,
|
||||
room4.roomId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("Returns the predecessors and subsequent rooms (with verify links)", () => {
|
||||
const [room1, room2, room3, room4] = createRoomHistory();
|
||||
const verifyLinks = true;
|
||||
const history = client.getRoomUpgradeHistory(room3.roomId, verifyLinks);
|
||||
expect(history.map((room) => room.roomId)).toEqual([
|
||||
room1.roomId,
|
||||
room2.roomId,
|
||||
room3.roomId,
|
||||
room4.roomId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("Returns the predecessors and subsequent rooms using MSC3945 dynamic room predecessors", () => {
|
||||
const [, , room3, , dynRoom1, dynRoom2, dynRoom4, dynRoom5] = createDynamicRoomHistory();
|
||||
const useMsc3946 = true;
|
||||
const verifyLinks = false;
|
||||
const history = client.getRoomUpgradeHistory(room3.roomId, verifyLinks, useMsc3946);
|
||||
expect(history.map((room) => room.roomId)).toEqual([
|
||||
dynRoom1.roomId,
|
||||
dynRoom2.roomId,
|
||||
room3.roomId,
|
||||
dynRoom4.roomId,
|
||||
dynRoom5.roomId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("When not asking for MSC3946, verified history without tombstones is empty", () => {
|
||||
// There no tombstones to match the create events
|
||||
const [, , room3] = createDynamicRoomHistory();
|
||||
const useMsc3946 = false;
|
||||
const verifyLinks = true;
|
||||
const history = client.getRoomUpgradeHistory(room3.roomId, verifyLinks, useMsc3946);
|
||||
// So we get no history back
|
||||
expect(history.map((room) => room.roomId)).toEqual([room3.roomId]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { REFERENCE_RELATION } from "matrix-events-sdk";
|
||||
|
||||
import { REFERENCE_RELATION } from "../../../src/@types/extensible_events";
|
||||
import { MatrixEvent } from "../../../src";
|
||||
import { M_BEACON_INFO } from "../../../src/@types/beacon";
|
||||
import { isTimestampInDuration, Beacon, BeaconEvent } from "../../../src/models/beacon";
|
||||
|
||||
@@ -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", () => {
|
||||
@@ -126,12 +126,31 @@ describe("MatrixEvent", () => {
|
||||
expect(encryptedEvent.isEncrypted()).toBeTruthy();
|
||||
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
|
||||
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
|
||||
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeFalsy();
|
||||
expect(encryptedEvent.getContent()).toEqual({
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: "** Unable to decrypt: Error: test error **",
|
||||
});
|
||||
});
|
||||
|
||||
it(`should report "DecryptionError: The sender has disabled encrypting to unverified devices."`, async () => {
|
||||
const crypto = {
|
||||
decryptEvent: jest
|
||||
.fn()
|
||||
.mockRejectedValue("DecryptionError: The sender has disabled encrypting to unverified devices."),
|
||||
} as unknown as Crypto;
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
expect(encryptedEvent.isEncrypted()).toBeTruthy();
|
||||
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
|
||||
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
|
||||
expect(encryptedEvent.isEncryptedDisabledForUnverifiedDevices).toBeTruthy();
|
||||
expect(encryptedEvent.getContent()).toEqual({
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: "** Unable to decrypt: DecryptionError: The sender has disabled encrypting to unverified devices. **",
|
||||
});
|
||||
});
|
||||
|
||||
it("should retry decryption if a retry is queued", async () => {
|
||||
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, "attemptDecryption");
|
||||
|
||||
@@ -163,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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
/*
|
||||
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 { IEvent, MatrixEvent, PollEvent, Room } from "../../../src";
|
||||
import { REFERENCE_RELATION } from "../../../src/@types/extensible_events";
|
||||
import { M_POLL_END, M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE } from "../../../src/@types/polls";
|
||||
import { PollStartEvent } from "../../../src/extensible_events_v1/PollStartEvent";
|
||||
import { Poll } from "../../../src/models/poll";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils/client";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("Poll", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(true),
|
||||
relations: jest.fn(),
|
||||
});
|
||||
const roomId = "!room:server";
|
||||
const room = new Room(roomId, mockClient, userId);
|
||||
const maySendRedactionForEventSpy = jest.spyOn(room.currentState, "maySendRedactionForEvent");
|
||||
// 14.03.2022 16:15
|
||||
const now = 1647270879403;
|
||||
|
||||
const basePollStartEvent = new MatrixEvent({
|
||||
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
||||
room_id: roomId,
|
||||
sender: userId,
|
||||
});
|
||||
basePollStartEvent.event.event_id = "$12345";
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.setSystemTime(now);
|
||||
|
||||
mockClient.relations.mockReset().mockResolvedValue({ events: [] });
|
||||
|
||||
maySendRedactionForEventSpy.mockClear().mockReturnValue(true);
|
||||
});
|
||||
|
||||
let eventId = 1;
|
||||
const makeRelatedEvent = (eventProps: Partial<IEvent>, timestamp = now): MatrixEvent => {
|
||||
const event = new MatrixEvent({
|
||||
...eventProps,
|
||||
content: {
|
||||
...(eventProps.content || {}),
|
||||
"m.relates_to": {
|
||||
rel_type: REFERENCE_RELATION.name,
|
||||
event_id: basePollStartEvent.getId(),
|
||||
},
|
||||
},
|
||||
});
|
||||
event.event.origin_server_ts = timestamp;
|
||||
event.event.event_id = `${eventId++}`;
|
||||
return event;
|
||||
};
|
||||
|
||||
it("initialises with root event", () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
expect(poll.roomId).toEqual(roomId);
|
||||
expect(poll.pollId).toEqual(basePollStartEvent.getId());
|
||||
expect(poll.pollEvent).toEqual(basePollStartEvent.unstableExtensibleEvent);
|
||||
expect(poll.isEnded).toBe(false);
|
||||
expect(poll.endEventId).toBe(undefined);
|
||||
});
|
||||
|
||||
it("throws when poll start has no room id", () => {
|
||||
const pollStartEvent = new MatrixEvent(
|
||||
PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
||||
);
|
||||
expect(() => new Poll(pollStartEvent, mockClient, room)).toThrow("Invalid poll start event.");
|
||||
});
|
||||
|
||||
it("throws when poll start has no event id", () => {
|
||||
const pollStartEvent = new MatrixEvent({
|
||||
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
||||
room_id: roomId,
|
||||
});
|
||||
expect(() => new Poll(pollStartEvent, mockClient, room)).toThrow("Invalid poll start event.");
|
||||
});
|
||||
|
||||
describe("fetching responses", () => {
|
||||
it("calls relations api and emits", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const emitSpy = jest.spyOn(poll, "emit");
|
||||
const fetchResponsePromise = poll.getResponses();
|
||||
expect(poll.isFetchingResponses).toBe(true);
|
||||
const responses = await fetchResponsePromise;
|
||||
expect(poll.isFetchingResponses).toBe(false);
|
||||
expect(mockClient.relations).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
basePollStartEvent.getId(),
|
||||
"m.reference",
|
||||
undefined,
|
||||
{ from: undefined },
|
||||
);
|
||||
expect(emitSpy).toHaveBeenCalledWith(PollEvent.Responses, responses);
|
||||
});
|
||||
|
||||
it("returns existing responses object after initial fetch", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const responses = await poll.getResponses();
|
||||
const responses2 = await poll.getResponses();
|
||||
// only fetched relations once
|
||||
expect(mockClient.relations).toHaveBeenCalledTimes(1);
|
||||
// strictly equal
|
||||
expect(responses).toBe(responses2);
|
||||
});
|
||||
|
||||
it("waits for existing relations request to finish when getting responses", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const firstResponsePromise = poll.getResponses();
|
||||
const secondResponsePromise = poll.getResponses();
|
||||
await firstResponsePromise;
|
||||
expect(firstResponsePromise).toEqual(secondResponsePromise);
|
||||
await secondResponsePromise;
|
||||
expect(mockClient.relations).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("filters relations for relevent response events", async () => {
|
||||
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
|
||||
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
|
||||
const unstableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
||||
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [replyEvent, stableResponseEvent, unstableResponseEvent],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const responses = await poll.getResponses();
|
||||
expect(responses.getRelations()).toEqual([stableResponseEvent, unstableResponseEvent]);
|
||||
});
|
||||
|
||||
describe("with multiple pages of relations", () => {
|
||||
const makeResponses = (count = 1, timestamp = now): MatrixEvent[] =>
|
||||
new Array(count)
|
||||
.fill("x")
|
||||
.map((_x, index) =>
|
||||
makeRelatedEvent(
|
||||
{ type: M_POLL_RESPONSE.stable!, sender: "@bob@server.org" },
|
||||
timestamp + index,
|
||||
),
|
||||
);
|
||||
|
||||
it("page relations responses", async () => {
|
||||
const responseEvents = makeResponses(6);
|
||||
mockClient.relations
|
||||
.mockResolvedValueOnce({
|
||||
events: responseEvents.slice(0, 2),
|
||||
nextBatch: "test-next-1",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
events: responseEvents.slice(2, 4),
|
||||
nextBatch: "test-next-2",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
events: responseEvents.slice(4),
|
||||
});
|
||||
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
const responses = await poll.getResponses();
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(mockClient.relations.mock.calls).toEqual([
|
||||
[roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: undefined }],
|
||||
[roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: "test-next-1" }],
|
||||
[roomId, basePollStartEvent.getId(), "m.reference", undefined, { from: "test-next-2" }],
|
||||
]);
|
||||
|
||||
expect(poll.emit).toHaveBeenCalledTimes(3);
|
||||
expect(poll.isFetchingResponses).toBeFalsy();
|
||||
expect(responses.getRelations().length).toEqual(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe("undecryptable relations", () => {
|
||||
it("counts undecryptable relation events when getting responses", async () => {
|
||||
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
|
||||
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
|
||||
const undecryptableEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
||||
jest.spyOn(undecryptableEvent, "isDecryptionFailure").mockReturnValue(true);
|
||||
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [replyEvent, stableResponseEvent, undecryptableEvent],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
expect(poll.undecryptableRelationsCount).toBe(1);
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.UndecryptableRelations, 1);
|
||||
});
|
||||
|
||||
it("adds to undercryptable event count when new relation is undecryptable", async () => {
|
||||
const replyEvent = makeRelatedEvent({ type: "m.room.message" });
|
||||
const stableResponseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.stable! });
|
||||
const undecryptableEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
||||
const undecryptableEvent2 = makeRelatedEvent({ type: M_POLL_RESPONSE.unstable });
|
||||
jest.spyOn(undecryptableEvent, "isDecryptionFailure").mockReturnValue(true);
|
||||
jest.spyOn(undecryptableEvent2, "isDecryptionFailure").mockReturnValue(true);
|
||||
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [replyEvent, stableResponseEvent, undecryptableEvent],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
expect(poll.undecryptableRelationsCount).toBe(1);
|
||||
|
||||
await poll.onNewRelation(undecryptableEvent2);
|
||||
|
||||
expect(poll.undecryptableRelationsCount).toBe(2);
|
||||
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.UndecryptableRelations, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with poll end event", () => {
|
||||
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@bob@server.org" });
|
||||
const unstablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.unstable!, sender: "@bob@server.org" });
|
||||
const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000);
|
||||
const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
|
||||
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd, stablePollEndEvent],
|
||||
});
|
||||
});
|
||||
|
||||
it("sets poll end event with stable event type", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
|
||||
expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@bob@server.org");
|
||||
expect(poll.isEnded).toBe(true);
|
||||
expect(poll.endEventId).toBe(stablePollEndEvent.getId()!);
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
});
|
||||
|
||||
it("sets poll end event when endevent sender also created the poll, but does not have redaction rights", async () => {
|
||||
const pollStartEvent = new MatrixEvent({
|
||||
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
||||
room_id: roomId,
|
||||
sender: "@bob:domain.org",
|
||||
});
|
||||
pollStartEvent.event.event_id = "$6789";
|
||||
const poll = new Poll(pollStartEvent, mockClient, room);
|
||||
const pollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@bob:domain.org" });
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [pollEndEvent],
|
||||
});
|
||||
maySendRedactionForEventSpy.mockReturnValue(false);
|
||||
jest.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
|
||||
expect(maySendRedactionForEventSpy).not.toHaveBeenCalled();
|
||||
expect(poll.isEnded).toBe(true);
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
});
|
||||
|
||||
it("sets poll end event with unstable event type", async () => {
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [unstablePollEndEvent],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
await poll.getResponses();
|
||||
|
||||
expect(poll.isEnded).toBe(true);
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
});
|
||||
|
||||
it("filters out responses that were sent after poll end", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const responses = await poll.getResponses();
|
||||
|
||||
// just response type events
|
||||
// and response with ts after poll end event is excluded
|
||||
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("onNewRelation()", () => {
|
||||
it("discards response if poll responses have not been initialised", () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
const responseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
|
||||
|
||||
poll.onNewRelation(responseEvent);
|
||||
|
||||
// did not add response -> no emit
|
||||
expect(poll.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets poll end event when responses are not initialised", () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
jest.spyOn(poll, "emit");
|
||||
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: userId });
|
||||
|
||||
poll.onNewRelation(stablePollEndEvent);
|
||||
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
});
|
||||
|
||||
it("does not set poll end event when sent by invalid user", async () => {
|
||||
maySendRedactionForEventSpy.mockReturnValue(false);
|
||||
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: "@charlie:server.org" });
|
||||
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [responseEventAfterEnd],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
await poll.getResponses();
|
||||
jest.spyOn(poll, "emit");
|
||||
|
||||
poll.onNewRelation(stablePollEndEvent);
|
||||
|
||||
// didn't end, didn't refilter responses
|
||||
expect(poll.emit).not.toHaveBeenCalled();
|
||||
expect(poll.isEnded).toBeFalsy();
|
||||
expect(maySendRedactionForEventSpy).toHaveBeenCalledWith(basePollStartEvent, "@charlie:server.org");
|
||||
});
|
||||
|
||||
it("replaces poll end event and refilters when an older end event already exists", async () => {
|
||||
const earlierPollEndEvent = makeRelatedEvent(
|
||||
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
||||
now,
|
||||
);
|
||||
const laterPollEndEvent = makeRelatedEvent(
|
||||
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
||||
now + 2000,
|
||||
);
|
||||
const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000);
|
||||
const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
|
||||
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd, laterPollEndEvent],
|
||||
});
|
||||
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const responses = await poll.getResponses();
|
||||
|
||||
// all responses have a timestamp < laterPollEndEvent
|
||||
expect(responses.getRelations().length).toEqual(3);
|
||||
// first end event set correctly
|
||||
expect(poll.isEnded).toBeTruthy();
|
||||
|
||||
// reset spy count
|
||||
jest.spyOn(poll, "emit").mockClear();
|
||||
|
||||
// add a valid end event with earlier timestamp
|
||||
poll.onNewRelation(earlierPollEndEvent);
|
||||
|
||||
// emitted new end event
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
// filtered responses and emitted
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses);
|
||||
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
|
||||
});
|
||||
|
||||
it("does not set poll end event when an earlier end event already exists", async () => {
|
||||
const earlierPollEndEvent = makeRelatedEvent(
|
||||
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
||||
now,
|
||||
);
|
||||
const laterPollEndEvent = makeRelatedEvent(
|
||||
{ type: M_POLL_END.stable!, sender: "@valid:server.org" },
|
||||
now + 2000,
|
||||
);
|
||||
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
await poll.getResponses();
|
||||
|
||||
poll.onNewRelation(earlierPollEndEvent);
|
||||
|
||||
// first end event set correctly
|
||||
expect(poll.isEnded).toBeTruthy();
|
||||
|
||||
// reset spy count
|
||||
jest.spyOn(poll, "emit").mockClear();
|
||||
|
||||
poll.onNewRelation(laterPollEndEvent);
|
||||
// didn't set new end event, didn't refilter responses
|
||||
expect(poll.emit).not.toHaveBeenCalled();
|
||||
expect(poll.isEnded).toBeTruthy();
|
||||
});
|
||||
|
||||
it("sets poll end event and refilters responses based on timestamp", async () => {
|
||||
const stablePollEndEvent = makeRelatedEvent({ type: M_POLL_END.stable!, sender: userId });
|
||||
const responseEventBeforeEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now - 1000);
|
||||
const responseEventAtEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
|
||||
const responseEventAfterEnd = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now + 1000);
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [responseEventAfterEnd, responseEventAtEnd, responseEventBeforeEnd],
|
||||
});
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
const responses = await poll.getResponses();
|
||||
jest.spyOn(poll, "emit");
|
||||
|
||||
expect(responses.getRelations().length).toEqual(3);
|
||||
poll.onNewRelation(stablePollEndEvent);
|
||||
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.End);
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses);
|
||||
expect(responses.getRelations().length).toEqual(2);
|
||||
// after end timestamp event is removed
|
||||
expect(responses.getRelations()).toEqual([responseEventAtEnd, responseEventBeforeEnd]);
|
||||
});
|
||||
|
||||
it("filters out irrelevant relations", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
// init responses
|
||||
const responses = await poll.getResponses();
|
||||
jest.spyOn(poll, "emit");
|
||||
const replyEvent = new MatrixEvent({ type: "m.room.message" });
|
||||
|
||||
poll.onNewRelation(replyEvent);
|
||||
|
||||
// did not add response -> no emit
|
||||
expect(poll.emit).not.toHaveBeenCalled();
|
||||
expect(responses.getRelations().length).toEqual(0);
|
||||
});
|
||||
|
||||
it("adds poll response relations to responses", async () => {
|
||||
const poll = new Poll(basePollStartEvent, mockClient, room);
|
||||
// init responses
|
||||
const responses = await poll.getResponses();
|
||||
jest.spyOn(poll, "emit");
|
||||
const responseEvent = makeRelatedEvent({ type: M_POLL_RESPONSE.name }, now);
|
||||
|
||||
poll.onNewRelation(responseEvent);
|
||||
|
||||
// did not add response -> no emit
|
||||
expect(poll.emit).toHaveBeenCalledWith(PollEvent.Responses, responses);
|
||||
expect(responses.getRelations()).toEqual([responseEvent]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,7 @@ import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../../src/models/t
|
||||
import { mkThread } from "../../test-utils/thread";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { emitPromise, mkMessage, mock } from "../../test-utils/test-utils";
|
||||
import { EventStatus, MatrixEvent } from "../../../src";
|
||||
import { Direction, EventStatus, MatrixEvent } from "../../../src";
|
||||
import { ReceiptType } from "../../../src/@types/read_receipts";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils/client";
|
||||
import { ReEmitter } from "../../../src/ReEmitter";
|
||||
@@ -82,9 +82,10 @@ describe("Thread", () => {
|
||||
beforeEach(() => {
|
||||
client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||
getRoom: jest.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
|
||||
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
client.reEmitter = mock(ReEmitter, "ReEmitter");
|
||||
client.canSupport = new Map();
|
||||
@@ -193,9 +194,10 @@ describe("Thread", () => {
|
||||
beforeEach(() => {
|
||||
client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||
getRoom: jest.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
|
||||
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
client.reEmitter = mock(ReEmitter, "ReEmitter");
|
||||
client.canSupport = new Map();
|
||||
@@ -283,4 +285,143 @@ describe("Thread", () => {
|
||||
expect(thread2.getEventReadUpTo(myUserId)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resetLiveTimeline", () => {
|
||||
// ResetLiveTimeline is used when we have missing messages between the current live timeline's end and newly
|
||||
// received messages. In that case, we want to replace the existing live timeline. To ensure pagination
|
||||
// continues working correctly, new pagination tokens need to be set on both the old live timeline (which is
|
||||
// now a regular timeline) and the new live timeline.
|
||||
it("replaces the live timeline and correctly sets pagination tokens", async () => {
|
||||
const myUserId = "@bob:example.org";
|
||||
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
|
||||
const { thread } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: myUserId,
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
length: 3,
|
||||
});
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
expect(thread.length).toBe(2);
|
||||
|
||||
jest.spyOn(client, "createMessagesRequest").mockImplementation((_, token) =>
|
||||
Promise.resolve({
|
||||
chunk: [],
|
||||
start: `${token}-new`,
|
||||
end: `${token}-new`,
|
||||
}),
|
||||
);
|
||||
|
||||
function timelines(): [string | null, string | null][] {
|
||||
return thread.timelineSet
|
||||
.getTimelines()
|
||||
.map((it) => [it.getPaginationToken(Direction.Backward), it.getPaginationToken(Direction.Forward)]);
|
||||
}
|
||||
|
||||
expect(timelines()).toEqual([[null, null]]);
|
||||
const promise = thread.resetLiveTimeline("b1", "f1");
|
||||
expect(timelines()).toEqual([
|
||||
[null, "f1"],
|
||||
["b1", null],
|
||||
]);
|
||||
await promise;
|
||||
expect(timelines()).toEqual([
|
||||
[null, "f1-new"],
|
||||
["b1-new", null],
|
||||
]);
|
||||
});
|
||||
|
||||
// As the pagination tokens cannot be used right now, resetLiveTimeline needs to replace them before they can
|
||||
// be used. But if in the future the bug in synapse is fixed, and they can actually be used, we can get into a
|
||||
// state where the client has paginated (and changed the tokens) while resetLiveTimeline tries to set the
|
||||
// corrected tokens. To prevent such a race condition, we make sure that resetLiveTimeline respects any
|
||||
// changes done to the pagination tokens.
|
||||
it("replaces the live timeline but does not replace changed pagination tokens", async () => {
|
||||
const myUserId = "@bob:example.org";
|
||||
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
|
||||
const { thread } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: myUserId,
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
length: 3,
|
||||
});
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
expect(thread.length).toBe(2);
|
||||
|
||||
jest.spyOn(client, "createMessagesRequest").mockImplementation((_, token) =>
|
||||
Promise.resolve({
|
||||
chunk: [],
|
||||
start: `${token}-new`,
|
||||
end: `${token}-new`,
|
||||
}),
|
||||
);
|
||||
|
||||
function timelines(): [string | null, string | null][] {
|
||||
return thread.timelineSet
|
||||
.getTimelines()
|
||||
.map((it) => [it.getPaginationToken(Direction.Backward), it.getPaginationToken(Direction.Forward)]);
|
||||
}
|
||||
|
||||
expect(timelines()).toEqual([[null, null]]);
|
||||
const promise = thread.resetLiveTimeline("b1", "f1");
|
||||
expect(timelines()).toEqual([
|
||||
[null, "f1"],
|
||||
["b1", null],
|
||||
]);
|
||||
thread.timelineSet.getTimelines()[0].setPaginationToken("f2", Direction.Forward);
|
||||
thread.timelineSet.getTimelines()[1].setPaginationToken("b2", Direction.Backward);
|
||||
await promise;
|
||||
expect(timelines()).toEqual([
|
||||
[null, "f2"],
|
||||
["b2", null],
|
||||
]);
|
||||
});
|
||||
|
||||
it("is correctly called by the room", async () => {
|
||||
const myUserId = "@bob:example.org";
|
||||
const testClient = new TestClient(myUserId, "DEVICE", "ACCESS_TOKEN", undefined, {
|
||||
timelineSupport: false,
|
||||
});
|
||||
const client = testClient.client;
|
||||
const room = new Room("123", client, myUserId, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
|
||||
const { thread } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: myUserId,
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
length: 3,
|
||||
});
|
||||
await emitPromise(thread, ThreadEvent.Update);
|
||||
expect(thread.length).toBe(2);
|
||||
const mock = jest.spyOn(thread, "resetLiveTimeline");
|
||||
mock.mockReturnValue(Promise.resolve());
|
||||
|
||||
room.resetLiveTimeline("b1", "f1");
|
||||
expect(mock).toHaveBeenCalledWith("b1", "f1");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,10 +53,11 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
beforeEach(() => {
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||
getPushActionsForEvent: jest.fn().mockReturnValue(mkPushAction(true, true)),
|
||||
getRoom: jest.fn().mockImplementation(() => room),
|
||||
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
|
||||
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
mockClient.reEmitter = mock(ReEmitter, "ReEmitter");
|
||||
mockClient.canSupport = new Map();
|
||||
@@ -134,7 +135,7 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, event);
|
||||
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(2);
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(3);
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
|
||||
});
|
||||
|
||||
@@ -154,11 +155,11 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, threadEvent);
|
||||
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(2);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(1);
|
||||
});
|
||||
|
||||
it("does not change the room count when there's no unread count", () => {
|
||||
it("does not change the thread count when there's no unread count", () => {
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||
|
||||
@@ -192,6 +193,31 @@ describe("fixNotificationCountOnDecryption", () => {
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
|
||||
});
|
||||
|
||||
it("does not change the total room count when an event is marked as non-notifying", () => {
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 0);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
|
||||
|
||||
event.getPushActions = jest.fn().mockReturnValue(mkPushAction(true, false));
|
||||
mockClient.getPushActionsForEvent = jest.fn().mockReturnValue(mkPushAction(false, false));
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, event);
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0);
|
||||
});
|
||||
|
||||
it("does not change the total room count when a threaded event is marked as non-notifying", () => {
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||
|
||||
threadEvent.getPushActions = jest.fn().mockReturnValue(mkPushAction(true, false));
|
||||
mockClient.getPushActionsForEvent = jest.fn().mockReturnValue(mkPushAction(false, false));
|
||||
|
||||
fixNotificationCountOnDecryption(mockClient, event);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(0);
|
||||
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
|
||||
});
|
||||
|
||||
it("emits events", () => {
|
||||
const cb = jest.fn();
|
||||
room.on(RoomEvent.UnreadNotifications, cb);
|
||||
|
||||
+204
-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 } 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 () {
|
||||
@@ -163,26 +180,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 +289,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 +305,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 +341,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 +506,178 @@ 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);
|
||||
if (expected) {
|
||||
expect(actions?.notify).toBeTruthy();
|
||||
} else {
|
||||
expect(actions?.notify).toBeFalsy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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 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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,6 @@ import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import { MAIN_ROOM_TIMELINE, ReceiptType } from "../../src/@types/read_receipts";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { Feature, ServerSupport } from "../../src/feature";
|
||||
import { EventType } from "../../src/matrix";
|
||||
import { synthesizeReceipt } from "../../src/models/read-receipt";
|
||||
import { encodeUri } from "../../src/utils";
|
||||
@@ -70,10 +69,6 @@ const roomEvent = utils.mkEvent({
|
||||
},
|
||||
});
|
||||
|
||||
function mockServerSideSupport(client: MatrixClient, serverSideSupport: ServerSupport) {
|
||||
client.canSupport.set(Feature.ThreadUnreadNotifications, serverSideSupport);
|
||||
}
|
||||
|
||||
describe("Read receipt", () => {
|
||||
beforeEach(() => {
|
||||
httpBackend = new MockHttpBackend();
|
||||
@@ -101,7 +96,6 @@ describe("Read receipt", () => {
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, ServerSupport.Stable);
|
||||
client.sendReceipt(threadEvent, ReceiptType.Read, {});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
@@ -123,7 +117,6 @@ describe("Read receipt", () => {
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, ServerSupport.Stable);
|
||||
client.sendReadReceipt(threadEvent, ReceiptType.Read, true);
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
@@ -145,56 +138,11 @@ describe("Read receipt", () => {
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, ServerSupport.Stable);
|
||||
client.sendReceipt(roomEvent, ReceiptType.Read, {});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("sends a room read receipt when there's no server support", async () => {
|
||||
httpBackend
|
||||
.when(
|
||||
"POST",
|
||||
encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: threadEvent.getId()!,
|
||||
}),
|
||||
)
|
||||
.check((request) => {
|
||||
expect(request.data.thread_id).toBeUndefined();
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, ServerSupport.Unsupported);
|
||||
client.sendReceipt(threadEvent, ReceiptType.Read, {});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("sends a valid room read receipt even when body omitted", async () => {
|
||||
httpBackend
|
||||
.when(
|
||||
"POST",
|
||||
encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: ROOM_ID,
|
||||
$receiptType: ReceiptType.Read,
|
||||
$eventId: threadEvent.getId()!,
|
||||
}),
|
||||
)
|
||||
.check((request) => {
|
||||
expect(request.data).toEqual({});
|
||||
})
|
||||
.respond(200, {});
|
||||
|
||||
mockServerSideSupport(client, ServerSupport.Unsupported);
|
||||
client.sendReceipt(threadEvent, ReceiptType.Read, undefined);
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await flushPromises();
|
||||
});
|
||||
});
|
||||
|
||||
describe("synthesizeReceipt", () => {
|
||||
|
||||
@@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { M_POLL_START } from "matrix-events-sdk";
|
||||
|
||||
import { M_POLL_START } from "../../src/@types/polls";
|
||||
import { EventTimelineSet } from "../../src/models/event-timeline-set";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { Room } from "../../src/models/room";
|
||||
|
||||
@@ -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();
|
||||
@@ -146,7 +146,7 @@ describe("ECDHv1", function () {
|
||||
|
||||
// send a message without encryption
|
||||
await aliceTransport.send({ iv: "dummy", ciphertext: "dummy" });
|
||||
expect(bob.receive()).rejects.toThrowError();
|
||||
expect(bob.receive()).rejects.toThrow();
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
await bob.cancel(RendezvousFailureReason.Unknown);
|
||||
@@ -159,12 +159,12 @@ 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" });
|
||||
|
||||
expect(alice.receive()).rejects.toThrowError();
|
||||
expect(alice.receive()).rejects.toThrow();
|
||||
|
||||
await alice.cancel(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
@@ -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,
|
||||
@@ -220,7 +220,7 @@ describe("Rendezvous", function () {
|
||||
await bobStartPromise;
|
||||
});
|
||||
|
||||
it("new device declines protocol", async function () {
|
||||
it("new device declines protocol with outcome unsupported", async function () {
|
||||
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
|
||||
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
@@ -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,
|
||||
@@ -278,7 +278,7 @@ describe("Rendezvous", function () {
|
||||
expect(aliceOnFailure).toHaveBeenCalledWith(RendezvousFailureReason.UnsupportedAlgorithm);
|
||||
});
|
||||
|
||||
it("new device declines protocol", async function () {
|
||||
it("new device requests an invalid protocol", async function () {
|
||||
const aliceTransport = makeTransport("Alice", "https://test.rz/123456");
|
||||
const bobTransport = makeTransport("Bob", "https://test.rz/999999");
|
||||
transports.push(aliceTransport, bobTransport);
|
||||
@@ -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,
|
||||
@@ -570,7 +570,7 @@ describe("Rendezvous", function () {
|
||||
|
||||
it("device not online within timeout", async function () {
|
||||
const { aliceRz } = await completeLogin({});
|
||||
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError();
|
||||
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("device appears online within timeout", async function () {
|
||||
@@ -594,7 +594,7 @@ describe("Rendezvous", function () {
|
||||
getFingerprint: () => "bbbb",
|
||||
};
|
||||
}, 1500);
|
||||
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError();
|
||||
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("mismatched device key", async function () {
|
||||
@@ -603,6 +603,6 @@ describe("Rendezvous", function () {
|
||||
getFingerprint: () => "XXXX",
|
||||
},
|
||||
});
|
||||
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrowError(/different key/);
|
||||
expect(aliceRz.verifyNewDeviceOnExistingDevice(1000)).rejects.toThrow(/different key/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("SimpleHttpRendezvousTransport", function () {
|
||||
it("should throw an error when no server available", function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({ client, fetchFn });
|
||||
expect(simpleHttpTransport.send({})).rejects.toThrowError("Invalid rendezvous URI");
|
||||
expect(simpleHttpTransport.send({})).rejects.toThrow("Invalid rendezvous URI");
|
||||
});
|
||||
|
||||
it("POST to fallback server", async function () {
|
||||
@@ -130,7 +130,7 @@ describe("SimpleHttpRendezvousTransport", function () {
|
||||
fetchFn,
|
||||
});
|
||||
const prom = simpleHttpTransport.send({});
|
||||
expect(prom).rejects.toThrowError();
|
||||
expect(prom).rejects.toThrow();
|
||||
httpBackend.when("POST", "https://fallbackserver/rz").response = {
|
||||
body: null,
|
||||
response: {
|
||||
@@ -163,15 +163,6 @@ describe("SimpleHttpRendezvousTransport", function () {
|
||||
);
|
||||
});
|
||||
|
||||
it("POST with relative path response including parent", async function () {
|
||||
await postAndCheckLocation(
|
||||
false,
|
||||
"https://fallbackserver/rz/abc",
|
||||
"../xyz/123",
|
||||
"https://fallbackserver/rz/xyz/123",
|
||||
);
|
||||
});
|
||||
|
||||
it("POST to follow 307 to other server", async function () {
|
||||
const client = makeMockClient({ userId: "@alice:example.com", deviceId: "DEVICEID", msc3886Enabled: false });
|
||||
const simpleHttpTransport = new MSC3886SimpleHttpRendezvousTransport({
|
||||
@@ -373,7 +364,7 @@ describe("SimpleHttpRendezvousTransport", function () {
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
expect(simpleHttpTransport.details()).rejects.toThrowError();
|
||||
expect(simpleHttpTransport.details()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("send after cancelled", async function () {
|
||||
@@ -394,7 +385,7 @@ describe("SimpleHttpRendezvousTransport", function () {
|
||||
fallbackRzServer: "https://fallbackserver/rz",
|
||||
fetchFn,
|
||||
});
|
||||
expect(simpleHttpTransport.receive()).rejects.toThrowError();
|
||||
expect(simpleHttpTransport.receive()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("404 failure callback", async function () {
|
||||
@@ -416,7 +407,7 @@ describe("SimpleHttpRendezvousTransport", function () {
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("", 1);
|
||||
expect(onFailure).toBeCalledWith(RendezvousFailureReason.Unknown);
|
||||
expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Unknown);
|
||||
});
|
||||
|
||||
it("404 failure callback mapped to expired", async function () {
|
||||
@@ -456,7 +447,7 @@ describe("SimpleHttpRendezvousTransport", function () {
|
||||
},
|
||||
};
|
||||
await httpBackend.flush("");
|
||||
expect(onFailure).toBeCalledWith(RendezvousFailureReason.Expired);
|
||||
expect(onFailure).toHaveBeenCalledWith(RendezvousFailureReason.Expired);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+162
-14
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 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.
|
||||
@@ -19,6 +19,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, PollStartEvent } from "matrix-events-sdk";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
PendingEventOrdering,
|
||||
PollEvent,
|
||||
RelationType,
|
||||
RoomEvent,
|
||||
RoomMember,
|
||||
@@ -786,8 +788,12 @@ describe("Room", function () {
|
||||
});
|
||||
};
|
||||
|
||||
describe("resetLiveTimeline with timeline support enabled", resetTimelineTests.bind(null, true));
|
||||
describe("resetLiveTimeline with timeline support disabled", resetTimelineTests.bind(null, false));
|
||||
describe("resetLiveTimeline with timeline support enabled", () => {
|
||||
resetTimelineTests.bind(null, true);
|
||||
});
|
||||
describe("resetLiveTimeline with timeline support disabled", () => {
|
||||
resetTimelineTests.bind(null, false);
|
||||
});
|
||||
|
||||
describe("compareEventOrdering", function () {
|
||||
beforeEach(function () {
|
||||
@@ -1622,7 +1628,7 @@ describe("Room", function () {
|
||||
describe("addPendingEvent", function () {
|
||||
it("should add pending events to the pendingEventList if " + "pendingEventOrdering == 'detached'", function () {
|
||||
const client = new TestClient("@alice:example.com", "alicedevice").client;
|
||||
client.supportsExperimentalThreads = () => true;
|
||||
client.supportsThreads = () => true;
|
||||
const room = new Room(roomId, client, userA, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
@@ -2468,7 +2474,7 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
it("Edits update the lastReply event", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
room.client.supportsThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
|
||||
const randomMessage = mkMessage();
|
||||
@@ -2539,7 +2545,7 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
it("Redactions to thread responses decrement the length", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
room.client.supportsThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
|
||||
const threadRoot = mkMessage();
|
||||
@@ -2606,7 +2612,7 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
it("Redactions to reactions in threads do not decrement the length", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
room.client.supportsThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
|
||||
const threadRoot = mkMessage();
|
||||
@@ -2646,7 +2652,7 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
it("should not decrement the length when the thread root is redacted", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
room.client.supportsThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
|
||||
const threadRoot = mkMessage();
|
||||
@@ -2687,7 +2693,7 @@ describe("Room", function () {
|
||||
});
|
||||
|
||||
it("Redacting the lastEvent finds a new lastEvent", async () => {
|
||||
room.client.supportsExperimentalThreads = () => true;
|
||||
room.client.supportsThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
|
||||
@@ -2794,7 +2800,7 @@ describe("Room", function () {
|
||||
|
||||
describe("eventShouldLiveIn", () => {
|
||||
const client = new TestClient(userA).client;
|
||||
client.supportsExperimentalThreads = () => true;
|
||||
client.supportsThreads = () => true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
const room = new Room(roomId, client, userA);
|
||||
|
||||
@@ -3228,12 +3234,85 @@ describe("Room", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("processPollEvents()", () => {
|
||||
let room: Room;
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
client = getMockClientWithEventEmitter({
|
||||
decryptEventIfNeeded: jest.fn(),
|
||||
});
|
||||
room = new Room(roomId, client, userA);
|
||||
jest.spyOn(room, "emit").mockClear();
|
||||
});
|
||||
|
||||
const makePollStart = (id: string): MatrixEvent => {
|
||||
const event = new MatrixEvent({
|
||||
...PollStartEvent.from("What?", ["a", "b"], M_POLL_KIND_DISCLOSED.name).serialize(),
|
||||
room_id: roomId,
|
||||
});
|
||||
event.event.event_id = id;
|
||||
return event;
|
||||
};
|
||||
|
||||
it("adds poll models to room state for a poll start event", async () => {
|
||||
const pollStartEvent = makePollStart("1");
|
||||
const events = [pollStartEvent];
|
||||
|
||||
await room.processPollEvents(events);
|
||||
expect(client.decryptEventIfNeeded).toHaveBeenCalledWith(pollStartEvent);
|
||||
const pollInstance = room.polls.get(pollStartEvent.getId()!);
|
||||
expect(pollInstance).toBeTruthy();
|
||||
|
||||
expect(room.emit).toHaveBeenCalledWith(PollEvent.New, pollInstance);
|
||||
});
|
||||
|
||||
it("adds related events to poll models", async () => {
|
||||
const pollStartEvent = makePollStart("1");
|
||||
const pollStartEvent2 = makePollStart("2");
|
||||
const events = [pollStartEvent, pollStartEvent2];
|
||||
const pollResponseEvent = new MatrixEvent({
|
||||
type: M_POLL_RESPONSE.name,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Reference,
|
||||
event_id: pollStartEvent.getId(),
|
||||
},
|
||||
},
|
||||
});
|
||||
const messageEvent = new MatrixEvent({
|
||||
type: "m.room.messsage",
|
||||
content: {
|
||||
text: "hello",
|
||||
},
|
||||
});
|
||||
|
||||
// init poll
|
||||
await room.processPollEvents(events);
|
||||
|
||||
const poll = room.polls.get(pollStartEvent.getId()!)!;
|
||||
const poll2 = room.polls.get(pollStartEvent2.getId()!)!;
|
||||
jest.spyOn(poll, "onNewRelation");
|
||||
jest.spyOn(poll2, "onNewRelation");
|
||||
|
||||
await room.processPollEvents([pollResponseEvent, messageEvent]);
|
||||
|
||||
// only called for relevant event
|
||||
expect(poll.onNewRelation).toHaveBeenCalledTimes(1);
|
||||
expect(poll.onNewRelation).toHaveBeenCalledWith(pollResponseEvent);
|
||||
|
||||
// only called on poll with relation
|
||||
expect(poll2.onNewRelation).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("findPredecessorRoomId", () => {
|
||||
let client: MatrixClient | null = null;
|
||||
beforeEach(() => {
|
||||
client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
|
||||
isInitialSyncComplete: jest.fn().mockReturnValue(false),
|
||||
supportsThreads: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3266,21 +3345,90 @@ describe("Room", function () {
|
||||
});
|
||||
}
|
||||
|
||||
function predecessorEvent(
|
||||
newRoomId: string,
|
||||
predecessorRoomId: string,
|
||||
tombstoneEventId: string | null = null,
|
||||
): MatrixEvent {
|
||||
const content =
|
||||
tombstoneEventId === null
|
||||
? { predecessor_room_id: predecessorRoomId }
|
||||
: { predecessor_room_id: predecessorRoomId, last_known_event_id: tombstoneEventId };
|
||||
|
||||
return new MatrixEvent({
|
||||
content,
|
||||
event_id: `predecessor_event_id_pred_${predecessorRoomId}`,
|
||||
origin_server_ts: 1432735824653,
|
||||
room_id: newRoomId,
|
||||
sender: "@daryl:alexandria.example.com",
|
||||
state_key: "",
|
||||
type: "org.matrix.msc3946.room_predecessor",
|
||||
});
|
||||
}
|
||||
|
||||
it("Returns null if there is no create event", () => {
|
||||
const room = new Room("roomid", client!, "@u:example.com");
|
||||
expect(room.findPredecessorRoomId()).toBeNull();
|
||||
expect(room.findPredecessor()).toBeNull();
|
||||
});
|
||||
|
||||
it("Returns null if the create event has no predecessor", () => {
|
||||
const room = new Room("roomid", client!, "@u:example.com");
|
||||
room.addLiveEvents([roomCreateEvent("roomid", null)]);
|
||||
expect(room.findPredecessorRoomId()).toBeNull();
|
||||
expect(room.findPredecessor()).toBeNull();
|
||||
});
|
||||
|
||||
it("Returns the predecessor ID if one is provided via create event", () => {
|
||||
const room = new Room("roomid", client!, "@u:example.com");
|
||||
room.addLiveEvents([roomCreateEvent("roomid", "replacedroomid")]);
|
||||
expect(room.findPredecessorRoomId()).toBe("replacedroomid");
|
||||
expect(room.findPredecessor()).toEqual({ roomId: "replacedroomid", eventId: "id_of_last_known_event" });
|
||||
});
|
||||
|
||||
it("Prefers the m.predecessor event if one exists", () => {
|
||||
const room = new Room("roomid", client!, "@u:example.com");
|
||||
room.addLiveEvents([
|
||||
roomCreateEvent("roomid", "replacedroomid"),
|
||||
predecessorEvent("roomid", "otherreplacedroomid"),
|
||||
]);
|
||||
const useMsc3946 = true;
|
||||
expect(room.findPredecessor(useMsc3946)).toEqual({
|
||||
roomId: "otherreplacedroomid",
|
||||
eventId: undefined, // m.predecessor did not include an event_id
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the m.predecessor event ID if provided", () => {
|
||||
const room = new Room("roomid", client!, "@u:example.com");
|
||||
room.addLiveEvents([
|
||||
roomCreateEvent("roomid", "replacedroomid"),
|
||||
predecessorEvent("roomid", "otherreplacedroomid", "lstevtid"),
|
||||
]);
|
||||
const useMsc3946 = true;
|
||||
expect(room.findPredecessor(useMsc3946)).toEqual({
|
||||
roomId: "otherreplacedroomid",
|
||||
eventId: "lstevtid",
|
||||
});
|
||||
});
|
||||
|
||||
it("Ignores the m.predecessor event if we don't ask to use it", () => {
|
||||
const room = new Room("roomid", client!, "@u:example.com");
|
||||
room.addLiveEvents([
|
||||
roomCreateEvent("roomid", "replacedroomid"),
|
||||
predecessorEvent("roomid", "otherreplacedroomid"),
|
||||
]);
|
||||
// Don't provide an argument for msc3946ProcessDynamicPredecessor -
|
||||
// we should ignore the predecessor event.
|
||||
expect(room.findPredecessor()).toEqual({ roomId: "replacedroomid", eventId: "id_of_last_known_event" });
|
||||
});
|
||||
|
||||
it("Ignores the m.predecessor event and returns null if we don't ask to use it", () => {
|
||||
const room = new Room("roomid", client!, "@u:example.com");
|
||||
room.addLiveEvents([
|
||||
roomCreateEvent("roomid", null), // Create event has no predecessor
|
||||
predecessorEvent("roomid", "otherreplacedroomid", "lastevtid"),
|
||||
]);
|
||||
// Don't provide an argument for msc3946ProcessDynamicPredecessor -
|
||||
// we should ignore the predecessor event.
|
||||
expect(room.findPredecessor()).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
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 * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { Mocked } from "jest-mock";
|
||||
import { KeysClaimRequest, UserId } from "@matrix-org/matrix-sdk-crypto-js";
|
||||
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
import { KeyClaimManager } from "../../../src/rust-crypto/KeyClaimManager";
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi } from "../../../src";
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
describe("KeyClaimManager", () => {
|
||||
/* for these tests, we connect a KeyClaimManager to a mock OlmMachine, and a real OutgoingRequestProcessor
|
||||
* (which is connected to a mock fetch implementation)
|
||||
*/
|
||||
|
||||
/** the KeyClaimManager implementation under test */
|
||||
let keyClaimManager: KeyClaimManager;
|
||||
|
||||
/** a mocked-up OlmMachine which the OutgoingRequestProcessor and KeyClaimManager are connected to */
|
||||
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
|
||||
baseUrl: "https://example.com",
|
||||
prefix: "/_matrix",
|
||||
onlyData: true,
|
||||
});
|
||||
|
||||
olmMachine = {
|
||||
getMissingSessions: jest.fn(),
|
||||
markRequestAsSent: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
const outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, httpApi);
|
||||
|
||||
keyClaimManager = new KeyClaimManager(olmMachine, outgoingRequestProcessor);
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a promise which resolve once olmMachine.markRequestAsSent is called.
|
||||
*
|
||||
* The call itself will block initially.
|
||||
*
|
||||
* The promise returned by this function yields a callback function, which should be called to unblock the
|
||||
* markRequestAsSent call.
|
||||
*/
|
||||
function awaitCallToMarkRequestAsSent(): Promise<() => void> {
|
||||
return new Promise<() => void>((resolveCalledPromise, _reject) => {
|
||||
olmMachine.markRequestAsSent.mockImplementationOnce(async () => {
|
||||
// the mock implementation returns a promise...
|
||||
const completePromise = new Promise<void>((resolveCompletePromise, _reject) => {
|
||||
// ... and we now resolve the original promise with the resolver for that second promise.
|
||||
resolveCalledPromise(resolveCompletePromise);
|
||||
});
|
||||
return completePromise;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it("should claim missing keys", async () => {
|
||||
const u1 = new UserId("@alice:example.com");
|
||||
const u2 = new UserId("@bob:example.com");
|
||||
|
||||
// stub out olmMachine.getMissingSessions(), with a result indicating that it needs a keyclaim
|
||||
const keysClaimRequest = new KeysClaimRequest("1234", '{ "k1": "v1" }');
|
||||
olmMachine.getMissingSessions.mockResolvedValueOnce(keysClaimRequest);
|
||||
|
||||
// have the claim request return a 200
|
||||
fetchMock.postOnce("https://example.com/_matrix/client/v3/keys/claim", '{ "k": "v" }');
|
||||
|
||||
// also stub out olmMachine.markRequestAsSent
|
||||
olmMachine.markRequestAsSent.mockResolvedValueOnce(undefined);
|
||||
|
||||
// fire off the request
|
||||
await keyClaimManager.ensureSessionsForUsers([u1, u2]);
|
||||
|
||||
// check that all the calls were made
|
||||
expect(olmMachine.getMissingSessions).toHaveBeenCalledWith([u1, u2]);
|
||||
expect(fetchMock).toHaveFetched("https://example.com/_matrix/client/v3/keys/claim", {
|
||||
method: "POST",
|
||||
body: { k1: "v1" },
|
||||
});
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", keysClaimRequest.type, '{ "k": "v" }');
|
||||
});
|
||||
|
||||
it("should wait for previous claims to complete before making another", async () => {
|
||||
const u1 = new UserId("@alice:example.com");
|
||||
const u2 = new UserId("@bob:example.com");
|
||||
|
||||
// stub out olmMachine.getMissingSessions(), with a result indicating that it needs a keyclaim
|
||||
const keysClaimRequest = new KeysClaimRequest("1234", '{ "k1": "v1" }');
|
||||
olmMachine.getMissingSessions.mockResolvedValue(keysClaimRequest);
|
||||
|
||||
// have the claim request return a 200
|
||||
fetchMock.post("https://example.com/_matrix/client/v3/keys/claim", '{ "k": "v" }');
|
||||
|
||||
// stub out olmMachine.markRequestAsSent, and have it block
|
||||
let markRequestAsSentPromise = awaitCallToMarkRequestAsSent();
|
||||
|
||||
// fire off two requests, and keep track of whether their promises resolve
|
||||
let req1Resolved = false;
|
||||
keyClaimManager.ensureSessionsForUsers([u1]).then(() => {
|
||||
req1Resolved = true;
|
||||
});
|
||||
let req2Resolved = false;
|
||||
const req2 = keyClaimManager.ensureSessionsForUsers([u2]).then(() => {
|
||||
req2Resolved = true;
|
||||
});
|
||||
|
||||
// now: wait for the (first) call to OlmMachine.markRequestAsSent
|
||||
let resolveMarkRequestAsSentCallback = await markRequestAsSentPromise;
|
||||
|
||||
// at this point, there should have been a single call to getMissingSessions, and a single fetch; and neither
|
||||
// call to ensureSessionsAsUsers should have completed
|
||||
expect(olmMachine.getMissingSessions).toHaveBeenCalledWith([u1]);
|
||||
expect(olmMachine.getMissingSessions).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(req1Resolved).toBe(false);
|
||||
expect(req2Resolved).toBe(false);
|
||||
|
||||
// await the next call to markRequestAsSent, and release the first one
|
||||
markRequestAsSentPromise = awaitCallToMarkRequestAsSent();
|
||||
resolveMarkRequestAsSentCallback();
|
||||
resolveMarkRequestAsSentCallback = await markRequestAsSentPromise;
|
||||
|
||||
// the first request should now have completed, and we should have more calls and fetches
|
||||
expect(olmMachine.getMissingSessions).toHaveBeenCalledWith([u2]);
|
||||
expect(olmMachine.getMissingSessions).toHaveBeenCalledTimes(2);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
expect(req1Resolved).toBe(true);
|
||||
expect(req2Resolved).toBe(false);
|
||||
|
||||
// finally, release the second call to markRequestAsSent and check that the second request completes
|
||||
resolveMarkRequestAsSentCallback();
|
||||
await req2;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
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 MockHttpBackend from "matrix-mock-request";
|
||||
import { Mocked } from "jest-mock";
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import {
|
||||
KeysBackupRequest,
|
||||
KeysClaimRequest,
|
||||
KeysQueryRequest,
|
||||
KeysUploadRequest,
|
||||
RoomMessageRequest,
|
||||
SignatureUploadRequest,
|
||||
ToDeviceRequest,
|
||||
} from "@matrix-org/matrix-sdk-crypto-js";
|
||||
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi } from "../../../src";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
|
||||
describe("OutgoingRequestProcessor", () => {
|
||||
/** the OutgoingRequestProcessor implementation under test */
|
||||
let processor: OutgoingRequestProcessor;
|
||||
|
||||
/** A mock http backend which processor is connected to */
|
||||
let httpBackend: MockHttpBackend;
|
||||
|
||||
/** a mocked-up OlmMachine which processor is connected to */
|
||||
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
/** wait for a call to olmMachine.markRequestAsSent */
|
||||
function awaitCallToMarkAsSent(): Promise<void> {
|
||||
return new Promise((resolve, _reject) => {
|
||||
olmMachine.markRequestAsSent.mockImplementationOnce(async () => {
|
||||
resolve(undefined);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
httpBackend = new MockHttpBackend();
|
||||
|
||||
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
|
||||
baseUrl: "https://example.com",
|
||||
prefix: "/_matrix",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
onlyData: true,
|
||||
});
|
||||
|
||||
olmMachine = {
|
||||
markRequestAsSent: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
processor = new OutgoingRequestProcessor(olmMachine, httpApi);
|
||||
});
|
||||
|
||||
/* simple requests that map directly to the request body */
|
||||
const tests: Array<[string, any, "POST" | "PUT", string]> = [
|
||||
["KeysUploadRequest", KeysUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/upload"],
|
||||
["KeysQueryRequest", KeysQueryRequest, "POST", "https://example.com/_matrix/client/v3/keys/query"],
|
||||
["KeysClaimRequest", KeysClaimRequest, "POST", "https://example.com/_matrix/client/v3/keys/claim"],
|
||||
[
|
||||
"SignatureUploadRequest",
|
||||
SignatureUploadRequest,
|
||||
"POST",
|
||||
"https://example.com/_matrix/client/v3/keys/signatures/upload",
|
||||
],
|
||||
["KeysBackupRequest", KeysBackupRequest, "PUT", "https://example.com/_matrix/client/v3/room_keys/keys"],
|
||||
];
|
||||
|
||||
test.each(tests)(`should handle %ss`, async (_, RequestClass, expectedMethod, expectedPath) => {
|
||||
// first, mock up a request as we might expect to receive it from the Rust layer ...
|
||||
const testBody = '{ "foo": "bar" }';
|
||||
const outgoingRequest = new RequestClass("1234", testBody);
|
||||
|
||||
// ... then poke it into the OutgoingRequestProcessor under test.
|
||||
const reqProm = processor.makeOutgoingRequest(outgoingRequest);
|
||||
|
||||
// Now: check that it makes a matching HTTP request ...
|
||||
const testResponse = '{ "result": 1 }';
|
||||
httpBackend
|
||||
.when(expectedMethod, "/_matrix")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual(expectedPath);
|
||||
expect(req.rawData).toEqual(testBody);
|
||||
expect(req.headers["Accept"]).toEqual("application/json");
|
||||
expect(req.headers["Content-Type"]).toEqual("application/json");
|
||||
})
|
||||
.respond(200, testResponse, true);
|
||||
|
||||
// ... and that it calls OlmMachine.markAsSent.
|
||||
const markSentCallPromise = awaitCallToMarkAsSent();
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
await Promise.all([reqProm, markSentCallPromise]);
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
|
||||
it("should handle ToDeviceRequests", async () => {
|
||||
// first, mock up the ToDeviceRequest as we might expect to receive it from the Rust layer ...
|
||||
const testBody = '{ "foo": "bar" }';
|
||||
const outgoingRequest = new ToDeviceRequest("1234", "test/type", "test/txnid", testBody);
|
||||
|
||||
// ... then poke it into the OutgoingRequestProcessor under test.
|
||||
const reqProm = processor.makeOutgoingRequest(outgoingRequest);
|
||||
|
||||
// Now: check that it makes a matching HTTP request ...
|
||||
const testResponse = '{ "result": 1 }';
|
||||
httpBackend
|
||||
.when("PUT", "/_matrix")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://example.com/_matrix/client/v3/sendToDevice/test%2Ftype/test%2Ftxnid");
|
||||
expect(req.rawData).toEqual(testBody);
|
||||
expect(req.headers["Accept"]).toEqual("application/json");
|
||||
expect(req.headers["Content-Type"]).toEqual("application/json");
|
||||
})
|
||||
.respond(200, testResponse, true);
|
||||
|
||||
// ... and that it calls OlmMachine.markAsSent.
|
||||
const markSentCallPromise = awaitCallToMarkAsSent();
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
await Promise.all([reqProm, markSentCallPromise]);
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
|
||||
it("should handle RoomMessageRequests", async () => {
|
||||
// first, mock up the RoomMessageRequest as we might expect to receive it from the Rust layer ...
|
||||
const testBody = '{ "foo": "bar" }';
|
||||
const outgoingRequest = new RoomMessageRequest("1234", "test/room", "test/txnid", "test/type", testBody);
|
||||
|
||||
// ... then poke it into the OutgoingRequestProcessor under test.
|
||||
const reqProm = processor.makeOutgoingRequest(outgoingRequest);
|
||||
|
||||
// Now: check that it makes a matching HTTP request ...
|
||||
const testResponse = '{ "result": 1 }';
|
||||
httpBackend
|
||||
.when("PUT", "/_matrix")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual(
|
||||
"https://example.com/_matrix/client/v3/room/test%2Froom/send/test%2Ftype/test%2Ftxnid",
|
||||
);
|
||||
expect(req.rawData).toEqual(testBody);
|
||||
expect(req.headers["Accept"]).toEqual("application/json");
|
||||
expect(req.headers["Content-Type"]).toEqual("application/json");
|
||||
})
|
||||
.respond(200, testResponse, true);
|
||||
|
||||
// ... and that it calls OlmMachine.markAsSent.
|
||||
const markSentCallPromise = awaitCallToMarkAsSent();
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
await Promise.all([reqProm, markSentCallPromise]);
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
|
||||
it("does not explode with unknown requests", async () => {
|
||||
const outgoingRequest = { id: "5678", type: 987 };
|
||||
const markSentCallPromise = awaitCallToMarkAsSent();
|
||||
await Promise.all([processor.makeOutgoingRequest(outgoingRequest), markSentCallPromise]);
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("5678", 987, "");
|
||||
});
|
||||
});
|
||||
@@ -17,21 +17,16 @@ limitations under the License.
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import {
|
||||
KeysBackupRequest,
|
||||
KeysClaimRequest,
|
||||
KeysQueryRequest,
|
||||
KeysUploadRequest,
|
||||
OlmMachine,
|
||||
SignatureUploadRequest,
|
||||
} from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import { KeysQueryRequest, OlmMachine } from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import { Mocked } from "jest-mock";
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import { RustCrypto } from "../../src/rust-crypto/rust-crypto";
|
||||
import { initRustCrypto } from "../../src/rust-crypto";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../src";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
|
||||
import { initRustCrypto } from "../../../src/rust-crypto";
|
||||
import { IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../../src";
|
||||
import { mkEvent } from "../../test-utils/test-utils";
|
||||
import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
|
||||
import { IEventDecryptionResult } from "../../../src/@types/crypto";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@@ -103,8 +98,8 @@ describe("RustCrypto", () => {
|
||||
/** the RustCrypto implementation under test */
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
/** A mock http backend which rustCrypto is connected to */
|
||||
let httpBackend: MockHttpBackend;
|
||||
/** A mock OutgoingRequestProcessor which rustCrypto is connected to */
|
||||
let outgoingRequestProcessor: Mocked<OutgoingRequestProcessor>;
|
||||
|
||||
/** a mocked-up OlmMachine which rustCrypto is connected to */
|
||||
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
@@ -113,28 +108,25 @@ describe("RustCrypto", () => {
|
||||
* the front of the queue, until it is empty. */
|
||||
let outgoingRequestQueue: Array<Array<any>>;
|
||||
|
||||
/** wait for a call to olmMachine.markRequestAsSent */
|
||||
function awaitCallToMarkAsSent(): Promise<void> {
|
||||
return new Promise((resolve, _reject) => {
|
||||
olmMachine.markRequestAsSent.mockImplementationOnce(async () => {
|
||||
resolve(undefined);
|
||||
/** wait for a call to outgoingRequestProcessor.makeOutgoingRequest.
|
||||
*
|
||||
* The promise resolves to a callback: the makeOutgoingRequest call will not complete until the returned
|
||||
* callback is called.
|
||||
*/
|
||||
function awaitCallToMakeOutgoingRequest(): Promise<() => void> {
|
||||
return new Promise<() => void>((resolveCalledPromise, _reject) => {
|
||||
outgoingRequestProcessor.makeOutgoingRequest.mockImplementationOnce(async () => {
|
||||
const completePromise = new Promise<void>((resolveCompletePromise, _reject) => {
|
||||
resolveCalledPromise(resolveCompletePromise);
|
||||
});
|
||||
return completePromise;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
httpBackend = new MockHttpBackend();
|
||||
|
||||
await RustSdkCryptoJs.initAsync();
|
||||
|
||||
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
|
||||
baseUrl: "https://example.com",
|
||||
prefix: "/_matrix",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
onlyData: true,
|
||||
});
|
||||
|
||||
// for these tests we use a mock OlmMachine, with an implementation of outgoingRequests that
|
||||
// returns objects from outgoingRequestQueue
|
||||
outgoingRequestQueue = [];
|
||||
@@ -142,91 +134,55 @@ describe("RustCrypto", () => {
|
||||
outgoingRequests: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve(outgoingRequestQueue.shift() ?? []);
|
||||
}),
|
||||
markRequestAsSent: jest.fn(),
|
||||
close: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
rustCrypto = new RustCrypto(olmMachine, httpApi, TEST_USER, TEST_DEVICE_ID);
|
||||
outgoingRequestProcessor = {
|
||||
makeOutgoingRequest: jest.fn(),
|
||||
} as unknown as Mocked<OutgoingRequestProcessor>;
|
||||
|
||||
rustCrypto = new RustCrypto(olmMachine, {} as MatrixHttpApi<any>, TEST_USER, TEST_DEVICE_ID);
|
||||
rustCrypto["outgoingRequestProcessor"] = outgoingRequestProcessor;
|
||||
});
|
||||
|
||||
it("should poll for outgoing messages", () => {
|
||||
it("should poll for outgoing messages and send them", async () => {
|
||||
const testReq = new KeysQueryRequest("1234", "{}");
|
||||
outgoingRequestQueue.push([testReq]);
|
||||
|
||||
const makeRequestPromise = awaitCallToMakeOutgoingRequest();
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
await makeRequestPromise;
|
||||
expect(olmMachine.outgoingRequests).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
/* simple requests that map directly to the request body */
|
||||
const tests: Array<[any, "POST" | "PUT", string]> = [
|
||||
[KeysUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/upload"],
|
||||
[KeysQueryRequest, "POST", "https://example.com/_matrix/client/v3/keys/query"],
|
||||
[KeysClaimRequest, "POST", "https://example.com/_matrix/client/v3/keys/claim"],
|
||||
[SignatureUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/signatures/upload"],
|
||||
[KeysBackupRequest, "PUT", "https://example.com/_matrix/client/v3/room_keys/keys"],
|
||||
];
|
||||
|
||||
for (const [RequestClass, expectedMethod, expectedPath] of tests) {
|
||||
it(`should handle ${RequestClass.name}s`, async () => {
|
||||
const testBody = '{ "foo": "bar" }';
|
||||
const outgoingRequest = new RequestClass("1234", testBody);
|
||||
outgoingRequestQueue.push([outgoingRequest]);
|
||||
|
||||
const testResponse = '{ "result": 1 }';
|
||||
httpBackend
|
||||
.when(expectedMethod, "/_matrix")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual(expectedPath);
|
||||
expect(req.rawData).toEqual(testBody);
|
||||
expect(req.headers["Accept"]).toEqual("application/json");
|
||||
expect(req.headers["Content-Type"]).toEqual("application/json");
|
||||
})
|
||||
.respond(200, testResponse, true);
|
||||
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(1);
|
||||
|
||||
const markSentCallPromise = awaitCallToMarkAsSent();
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
await markSentCallPromise;
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
}
|
||||
|
||||
it("does not explode with unknown requests", async () => {
|
||||
const outgoingRequest = { id: "5678", type: 987 };
|
||||
outgoingRequestQueue.push([outgoingRequest]);
|
||||
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
await awaitCallToMarkAsSent();
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("5678", 987, "");
|
||||
expect(outgoingRequestProcessor.makeOutgoingRequest).toHaveBeenCalledWith(testReq);
|
||||
});
|
||||
|
||||
it("stops looping when stop() is called", async () => {
|
||||
const testResponse = '{ "result": 1 }';
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
outgoingRequestQueue.push([new KeysQueryRequest("1234", "{}")]);
|
||||
httpBackend.when("POST", "/_matrix").respond(200, testResponse, true);
|
||||
}
|
||||
|
||||
let makeRequestPromise = awaitCallToMakeOutgoingRequest();
|
||||
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
expect(rustCrypto["outgoingRequestLoopRunning"]).toBeTruthy();
|
||||
|
||||
// go a couple of times round the loop
|
||||
await httpBackend.flush("/_matrix", 1);
|
||||
await awaitCallToMarkAsSent();
|
||||
let resolveMakeRequest = await makeRequestPromise;
|
||||
makeRequestPromise = awaitCallToMakeOutgoingRequest();
|
||||
resolveMakeRequest();
|
||||
|
||||
await httpBackend.flush("/_matrix", 1);
|
||||
await awaitCallToMarkAsSent();
|
||||
resolveMakeRequest = await makeRequestPromise;
|
||||
makeRequestPromise = awaitCallToMakeOutgoingRequest();
|
||||
resolveMakeRequest();
|
||||
|
||||
// a second sync while this is going on shouldn't make any difference
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
await httpBackend.flush("/_matrix", 1);
|
||||
await awaitCallToMarkAsSent();
|
||||
resolveMakeRequest = await makeRequestPromise;
|
||||
outgoingRequestProcessor.makeOutgoingRequest.mockReset();
|
||||
resolveMakeRequest();
|
||||
|
||||
// now stop...
|
||||
rustCrypto.stop();
|
||||
@@ -238,11 +194,40 @@ describe("RustCrypto", () => {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
expect(rustCrypto["outgoingRequestLoopRunning"]).toBeFalsy();
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled();
|
||||
expect(olmMachine.outgoingRequests).not.toHaveBeenCalled();
|
||||
|
||||
// we sent three, so there should be 2 left
|
||||
expect(outgoingRequestQueue.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".getEventEncryptionInfo", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockHttpApi = {} as MatrixClient["http"];
|
||||
rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto;
|
||||
});
|
||||
|
||||
it("should handle unencrypted events", () => {
|
||||
const event = mkEvent({ event: true, type: "m.room.message", content: { body: "xyz" } });
|
||||
const res = rustCrypto.getEventEncryptionInfo(event);
|
||||
expect(res.encrypted).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should handle encrypted events", async () => {
|
||||
const event = mkEvent({ event: true, type: "m.room.encrypted", content: { algorithm: "fake_alg" } });
|
||||
const mockCryptoBackend = {
|
||||
decryptEvent: () =>
|
||||
({
|
||||
senderCurve25519Key: "1234",
|
||||
} as IEventDecryptionResult),
|
||||
} as unknown as CryptoBackend;
|
||||
await event.attemptDecryption(mockCryptoBackend);
|
||||
|
||||
const res = rustCrypto.getEventEncryptionInfo(event);
|
||||
expect(res.encrypted).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
+21
-23
@@ -112,7 +112,7 @@ describe("MatrixScheduler", function () {
|
||||
expect(procCount).toEqual(2);
|
||||
});
|
||||
|
||||
it("should give up if the retryFn on failure returns -1 and try the next event", async function () {
|
||||
it("should give up if the retryFn on failure returns -1", async function () {
|
||||
// Queue A & B.
|
||||
// Reject A and return -1 on retry.
|
||||
// Expect B to be tried next and the promise for A to be rejected.
|
||||
@@ -139,22 +139,18 @@ describe("MatrixScheduler", function () {
|
||||
return new Promise<Record<string, boolean>>(() => {});
|
||||
});
|
||||
|
||||
const globalA = scheduler.queueEvent(eventA);
|
||||
scheduler.queueEvent(eventB);
|
||||
const queuedA = scheduler.queueEvent(eventA);
|
||||
const queuedB = scheduler.queueEvent(eventB);
|
||||
await Promise.resolve();
|
||||
deferA.reject(new Error("Testerror"));
|
||||
// as queueing doesn't start processing synchronously anymore (see commit bbdb5ac)
|
||||
// wait just long enough before it does
|
||||
await Promise.resolve();
|
||||
await expect(queuedA).rejects.toThrow("Testerror");
|
||||
await expect(queuedB).rejects.toThrow("Testerror");
|
||||
expect(procCount).toEqual(1);
|
||||
deferA.reject({});
|
||||
try {
|
||||
await globalA;
|
||||
} catch (err) {
|
||||
await Promise.resolve();
|
||||
expect(procCount).toEqual(2);
|
||||
}
|
||||
});
|
||||
|
||||
it("should treat each queue separately", function (done) {
|
||||
it("should treat each queue separately", async () => {
|
||||
// Queue messages A B C D.
|
||||
// Bucket A&D into queue_A
|
||||
// Bucket B&C into queue_B
|
||||
@@ -179,13 +175,15 @@ describe("MatrixScheduler", function () {
|
||||
|
||||
const expectOrder = [eventA.getId(), eventB.getId(), eventD.getId()];
|
||||
const deferA = defer<Record<string, boolean>>();
|
||||
scheduler.setProcessFunction(function (event) {
|
||||
const id = expectOrder.shift();
|
||||
expect(id).toEqual(event.getId());
|
||||
if (expectOrder.length === 0) {
|
||||
done();
|
||||
}
|
||||
return id === eventA.getId() ? deferA.promise : deferred.promise;
|
||||
const allExpectedEventsSeenInOrderPromise = new Promise((resolve) => {
|
||||
scheduler.setProcessFunction(function (event) {
|
||||
const id = expectOrder.shift();
|
||||
expect(id).toEqual(event.getId());
|
||||
if (expectOrder.length === 0) {
|
||||
resolve(null);
|
||||
}
|
||||
return id === eventA.getId() ? deferA.promise : deferred.promise;
|
||||
});
|
||||
});
|
||||
scheduler.queueEvent(eventA);
|
||||
scheduler.queueEvent(eventB);
|
||||
@@ -197,6 +195,7 @@ describe("MatrixScheduler", function () {
|
||||
deferA.resolve({});
|
||||
}, 1000);
|
||||
jest.advanceTimersByTime(1000);
|
||||
await allExpectedEventsSeenInOrderPromise;
|
||||
});
|
||||
|
||||
describe("queueEvent", function () {
|
||||
@@ -294,7 +293,7 @@ describe("MatrixScheduler", function () {
|
||||
});
|
||||
|
||||
describe("setProcessFunction", function () {
|
||||
it("should call the processFn if there are queued events", function () {
|
||||
it("should call the processFn if there are queued events", async () => {
|
||||
queueFn = function () {
|
||||
return "yep";
|
||||
};
|
||||
@@ -307,9 +306,8 @@ describe("MatrixScheduler", function () {
|
||||
});
|
||||
// as queueing doesn't start processing synchronously anymore (see commit bbdb5ac)
|
||||
// wait just long enough before it does
|
||||
Promise.resolve().then(() => {
|
||||
expect(procCount).toEqual(1);
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(procCount).toEqual(1);
|
||||
});
|
||||
|
||||
it("should not call the processFn if there are no queued events", function () {
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("MemoryStore", () => {
|
||||
await store.deleteAllData();
|
||||
|
||||
// empty object
|
||||
expect(store.accountData).toEqual({});
|
||||
expect(store.accountData).toEqual(new Map());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
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 * as utils from "../../src/utils";
|
||||
import {
|
||||
alphabetPad,
|
||||
@@ -8,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";
|
||||
@@ -129,9 +148,11 @@ describe("utils", function () {
|
||||
describe("deepCompare", function () {
|
||||
const assert = {
|
||||
isTrue: function (x: any) {
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
expect(x).toBe(true);
|
||||
},
|
||||
isFalse: function (x: any) {
|
||||
// eslint-disable-next-line jest/no-standalone-expect
|
||||
expect(x).toBe(false);
|
||||
},
|
||||
};
|
||||
@@ -587,4 +608,121 @@ describe("utils", function () {
|
||||
expect(utils.isSupportedReceiptType("this is a receipt type")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it("resolves with the provided value", async () => {
|
||||
const expected = Symbol("hi");
|
||||
const result = await utils.sleep(0, expected);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("immediate", () => {
|
||||
it("resolves", async () => {
|
||||
await utils.immediate();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,7 +40,9 @@ import {
|
||||
MockMediaStreamTrack,
|
||||
installWebRTCMocks,
|
||||
MockRTCPeerConnection,
|
||||
MockRTCRtpTransceiver,
|
||||
SCREENSHARE_STREAM_ID,
|
||||
MockRTCRtpSender,
|
||||
} from "../../test-utils/webrtc";
|
||||
import { CallFeed } from "../../../src/webrtc/callFeed";
|
||||
import { EventType, IContent, ISendEventResponse, MatrixEvent, Room } from "../../../src";
|
||||
@@ -536,8 +538,15 @@ describe("Call", function () {
|
||||
it("if local video", async () => {
|
||||
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
|
||||
|
||||
// since this is testing for the presence of a local sender, we need to add a transciever
|
||||
// rather than just a source track
|
||||
const mockTrack = new MockMediaStreamTrack("track_id", "video");
|
||||
const mockTransceiver = new MockRTCRtpTransceiver(call.peerConn as unknown as MockRTCPeerConnection);
|
||||
mockTransceiver.sender = new MockRTCRtpSender(mockTrack) as unknown as RTCRtpSender;
|
||||
(call as any).transceivers.set("m.usermedia:video", mockTransceiver);
|
||||
|
||||
(call as any).pushNewLocalFeed(
|
||||
new MockMediaStream("remote_stream1", [new MockMediaStreamTrack("track_id", "video")]),
|
||||
new MockMediaStream("remote_stream1", [mockTrack]),
|
||||
SDPStreamMetadataPurpose.Usermedia,
|
||||
false,
|
||||
);
|
||||
@@ -829,6 +838,55 @@ describe("Call", function () {
|
||||
await startVideoCall(client, call);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("should not remove video sender on video mute", async () => {
|
||||
await call.setLocalVideoMuted(true);
|
||||
expect((call as any).hasUserMediaVideoSender).toBe(true);
|
||||
});
|
||||
|
||||
it("should release camera after short delay on video mute", async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
await call.setLocalVideoMuted(true);
|
||||
|
||||
jest.advanceTimersByTime(500);
|
||||
|
||||
expect(call.hasLocalUserMediaVideoTrack).toBe(false);
|
||||
});
|
||||
|
||||
it("should re-request video feed on video unmute if it doesn't have one", async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const mockGetUserMediaStream = jest
|
||||
.fn()
|
||||
.mockReturnValue(client.client.getMediaHandler().getUserMediaStream(true, true));
|
||||
|
||||
client.client.getMediaHandler().getUserMediaStream = mockGetUserMediaStream;
|
||||
|
||||
await call.setLocalVideoMuted(true);
|
||||
|
||||
jest.advanceTimersByTime(500);
|
||||
|
||||
await call.setLocalVideoMuted(false);
|
||||
|
||||
expect(mockGetUserMediaStream).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not release camera on fast mute and unmute", async () => {
|
||||
const mockGetUserMediaStream = jest.fn();
|
||||
|
||||
client.client.getMediaHandler().getUserMediaStream = mockGetUserMediaStream;
|
||||
|
||||
await call.setLocalVideoMuted(true);
|
||||
await call.setLocalVideoMuted(false);
|
||||
|
||||
expect(mockGetUserMediaStream).not.toHaveBeenCalled();
|
||||
expect(call.hasLocalUserMediaVideoTrack).toBe(true);
|
||||
});
|
||||
|
||||
describe("sending sdp_stream_metadata_changed events", () => {
|
||||
it("should send sdp_stream_metadata_changed when muting audio", async () => {
|
||||
await call.setMicrophoneMuted(true);
|
||||
@@ -1385,7 +1443,7 @@ describe("Call", function () {
|
||||
}),
|
||||
);
|
||||
// @ts-ignore Mock
|
||||
expect(call.terminate).toHaveBeenCalledWith(CallParty.Local, CallErrorCode.Transfered, true);
|
||||
expect(call.terminate).toHaveBeenCalledWith(CallParty.Local, CallErrorCode.Transferred, true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1521,7 +1579,7 @@ describe("Call", function () {
|
||||
hasAdvancedBy += advanceBy;
|
||||
|
||||
expect(lengthChangedListener).toHaveBeenCalledTimes(hasAdvancedBy);
|
||||
expect(lengthChangedListener).toBeCalledWith(hasAdvancedBy);
|
||||
expect(lengthChangedListener).toHaveBeenCalledWith(hasAdvancedBy);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ describe("CallFeed", () => {
|
||||
});
|
||||
|
||||
describe("muting after calling setAudioVideoMuted()", () => {
|
||||
it("should mute audio by default ", () => {
|
||||
it("should mute audio by default", () => {
|
||||
// @ts-ignore Mock
|
||||
feed.stream.addTrack(new MockMediaStreamTrack("track", "audio", true));
|
||||
feed.setAudioVideoMuted(true, false);
|
||||
|
||||
@@ -147,10 +147,17 @@ describe("Group Call", function () {
|
||||
async (state: GroupCallState) => {
|
||||
// @ts-ignore
|
||||
groupCall.state = state;
|
||||
await expect(groupCall.initLocalCallFeed()).rejects.toThrowError();
|
||||
await expect(groupCall.initLocalCallFeed()).rejects.toThrow();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([0, 3, 5, 10, 5000])("sets correct creation timestamp when creating a call", async (time: number) => {
|
||||
jest.spyOn(Date, "now").mockReturnValue(time);
|
||||
await groupCall.create();
|
||||
|
||||
expect(groupCall.creationTs).toBe(time);
|
||||
});
|
||||
|
||||
it("does not initialize local call feed, if it already is", async () => {
|
||||
await groupCall.initLocalCallFeed();
|
||||
jest.spyOn(groupCall, "initLocalCallFeed");
|
||||
@@ -161,6 +168,25 @@ describe("Group Call", function () {
|
||||
groupCall.leave();
|
||||
});
|
||||
|
||||
it("does not start initializing local call feed twice", () => {
|
||||
const promise1 = groupCall.initLocalCallFeed();
|
||||
// @ts-ignore Mock
|
||||
groupCall.state = GroupCallState.LocalCallFeedUninitialized;
|
||||
const promise2 = groupCall.initLocalCallFeed();
|
||||
|
||||
expect(promise1).toEqual(promise2);
|
||||
});
|
||||
|
||||
it("sets state to local call feed uninitialized when getUserMedia() fails", async () => {
|
||||
jest.spyOn(mockClient.getMediaHandler(), "getUserMediaStream").mockRejectedValue("Error");
|
||||
|
||||
try {
|
||||
await groupCall.initLocalCallFeed();
|
||||
} catch (e) {}
|
||||
|
||||
expect(groupCall.state).toBe(GroupCallState.LocalCallFeedUninitialized);
|
||||
});
|
||||
|
||||
it("stops initializing local call feed when leaving", async () => {
|
||||
const initPromise = groupCall.initLocalCallFeed();
|
||||
groupCall.leave();
|
||||
@@ -317,6 +343,20 @@ describe("Group Call", function () {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not throw when calling updateLocalUsermediaStream() without local usermedia stream", () => {
|
||||
expect(async () => await groupCall.updateLocalUsermediaStream({} as MediaStream)).not.toThrow();
|
||||
});
|
||||
|
||||
it.each([GroupCallState.Ended, GroupCallState.Entered, GroupCallState.InitializingLocalCallFeed])(
|
||||
"throws when entering call in the wrong state",
|
||||
async (state: GroupCallState) => {
|
||||
// @ts-ignore Mock
|
||||
groupCall.state = state;
|
||||
|
||||
await expect(groupCall.enter()).rejects.toThrow();
|
||||
},
|
||||
);
|
||||
|
||||
describe("hasLocalParticipant()", () => {
|
||||
it("should return false, if we don't have a local participant", () => {
|
||||
expect(groupCall.hasLocalParticipant()).toBeFalsy();
|
||||
@@ -349,7 +389,7 @@ describe("Group Call", function () {
|
||||
jest.spyOn(call, "getOpponentMember").mockReturnValue({ userId: undefined });
|
||||
|
||||
// @ts-ignore Mock
|
||||
expect(() => groupCall.onCallFeedsChanged(call)).toThrowError();
|
||||
expect(() => groupCall.onCallFeedsChanged(call)).toThrow();
|
||||
});
|
||||
|
||||
describe("usermedia feeds", () => {
|
||||
@@ -648,15 +688,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()]);
|
||||
}
|
||||
@@ -835,6 +875,34 @@ describe("Group Call", function () {
|
||||
|
||||
groupCall.terminate();
|
||||
});
|
||||
|
||||
it("returns false when unmuting audio with no audio device", async () => {
|
||||
const groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
jest.spyOn(mockClient.getMediaHandler(), "hasAudioDevice").mockResolvedValue(false);
|
||||
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", () => {
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("Media Handler", function () {
|
||||
} as unknown as MatrixClient);
|
||||
});
|
||||
|
||||
it("does not trigger update after restore media settings ", () => {
|
||||
it("does not trigger update after restore media settings", () => {
|
||||
mediaHandler.restoreMediaSettings(FAKE_AUDIO_INPUT_ID, FAKE_VIDEO_INPUT_ID);
|
||||
|
||||
expect(mockMediaDevices.getUserMedia).not.toHaveBeenCalled();
|
||||
@@ -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", () => {
|
||||
@@ -401,7 +411,7 @@ describe("Media Handler", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("stopUserMediaStream", () => {
|
||||
describe("stopScreensharingStream", () => {
|
||||
let stream: MediaStream;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -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,7 @@ export function isDmMemberCountCondition(condition: AnyMemberCountCondition): bo
|
||||
|
||||
export enum ConditionKind {
|
||||
EventMatch = "event_match",
|
||||
EventPropertyIs = "event_property_is",
|
||||
ContainsDisplayName = "contains_display_name",
|
||||
RoomMemberCount = "room_member_count",
|
||||
SenderNotificationPermission = "sender_notification_permission",
|
||||
@@ -77,9 +78,16 @@ 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 IContainsDisplayNameCondition extends IPushRuleCondition<ConditionKind.ContainsDisplayName> {
|
||||
// no additional fields
|
||||
}
|
||||
@@ -105,6 +113,7 @@ 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
|
||||
| IContainsDisplayNameCondition
|
||||
| IRoomMemberCountCondition
|
||||
| ISenderNotificationPermissionCondition
|
||||
@@ -133,6 +142,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 = {
|
||||
|
||||
@@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { RELATES_TO_RELATIONSHIP, REFERENCE_RELATION } from "matrix-events-sdk";
|
||||
|
||||
import { RelatesToRelationship, REFERENCE_RELATION } from "./extensible_events";
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { MAssetEvent, MLocationEvent, MTimestampEvent } from "./location";
|
||||
|
||||
@@ -138,4 +137,4 @@ export type MBeaconEventContent = MLocationEvent &
|
||||
// timestamp when location was taken
|
||||
MTimestampEvent &
|
||||
// relates to a beacon_info event
|
||||
RELATES_TO_RELATIONSHIP<typeof REFERENCE_RELATION>;
|
||||
RelatesToRelationship<typeof REFERENCE_RELATION>;
|
||||
|
||||
+28
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022-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.
|
||||
@@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IClearEvent } from "../models/event";
|
||||
import type { ISignatures } from "./signed";
|
||||
|
||||
export type OlmGroupSessionExtraData = {
|
||||
untrusted?: boolean;
|
||||
@@ -43,6 +44,10 @@ export interface IEventDecryptionResult {
|
||||
*/
|
||||
claimedEd25519Key?: string;
|
||||
untrusted?: boolean;
|
||||
/**
|
||||
* The sender doesn't authorize the unverified devices to decrypt his messages
|
||||
*/
|
||||
encryptedDisabledForUnverifiedDevices?: boolean;
|
||||
}
|
||||
|
||||
interface Extensible {
|
||||
@@ -70,3 +75,25 @@ export interface IMegolmSessionData extends Extensible {
|
||||
}
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
/** the type of the `device_keys` parameter on `/_matrix/client/v3/keys/upload`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.5/client-server-api/#post_matrixclientv3keysupload
|
||||
*/
|
||||
export interface IDeviceKeys {
|
||||
algorithms: Array<string>;
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
user_id: string; // eslint-disable-line camelcase
|
||||
keys: Record<string, string>;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
/** the type of the `one_time_keys` and `fallback_keys` parameters on `/_matrix/client/v3/keys/upload`
|
||||
*
|
||||
* @see https://spec.matrix.org/v1.5/client-server-api/#post_matrixclientv3keysupload
|
||||
*/
|
||||
export interface IOneTimeKey {
|
||||
key: string;
|
||||
fallback?: boolean;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export enum EventType {
|
||||
RoomGuestAccess = "m.room.guest_access",
|
||||
RoomServerAcl = "m.room.server_acl",
|
||||
RoomTombstone = "m.room.tombstone",
|
||||
RoomPredecessor = "org.matrix.msc3946.room_predecessor",
|
||||
|
||||
SpaceChild = "m.space.child",
|
||||
SpaceParent = "m.space.parent",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021 - 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.
|
||||
@@ -14,8 +14,138 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Types for MSC1767: Extensible events in Matrix
|
||||
import { EitherAnd, NamespacedValue, Optional, UnstableValue } from "matrix-events-sdk";
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { isProvided } from "../extensible_events_v1/utilities";
|
||||
|
||||
export const TEXT_NODE_TYPE = new UnstableValue("m.text", "org.matrix.msc1767.text");
|
||||
// Types and utilities for MSC1767: Extensible events (version 1) in Matrix
|
||||
|
||||
/**
|
||||
* Represents the stable and unstable values of a given namespace.
|
||||
*/
|
||||
export type TSNamespace<N> = N extends NamespacedValue<infer S, infer U>
|
||||
? TSNamespaceValue<S> | TSNamespaceValue<U>
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Represents a namespaced value, if the value is a string. Used to extract provided types
|
||||
* from a TSNamespace<N> (in cases where only stable *or* unstable is provided).
|
||||
*/
|
||||
export type TSNamespaceValue<V> = V extends string ? V : never;
|
||||
|
||||
/**
|
||||
* Creates a type which is V when T is `never`, otherwise T.
|
||||
*/
|
||||
// See https://github.com/microsoft/TypeScript/issues/23182#issuecomment-379091887 for details on the array syntax.
|
||||
export type DefaultNever<T, V> = [T] extends [never] ? V : T;
|
||||
|
||||
/**
|
||||
* The namespaced value for m.message
|
||||
*/
|
||||
export const M_MESSAGE = new UnstableValue("m.message", "org.matrix.msc1767.message");
|
||||
|
||||
/**
|
||||
* An m.message event rendering
|
||||
*/
|
||||
export interface IMessageRendering {
|
||||
body: string;
|
||||
mimetype?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The content for an m.message event
|
||||
*/
|
||||
export type ExtensibleMessageEventContent = EitherAnd<
|
||||
{ [M_MESSAGE.name]: IMessageRendering[] },
|
||||
{ [M_MESSAGE.altName]: IMessageRendering[] }
|
||||
>;
|
||||
|
||||
/**
|
||||
* The namespaced value for m.text
|
||||
*/
|
||||
export const M_TEXT = new UnstableValue("m.text", "org.matrix.msc1767.text");
|
||||
|
||||
/**
|
||||
* The content for an m.text event
|
||||
*/
|
||||
export type TextEventContent = EitherAnd<{ [M_TEXT.name]: string }, { [M_TEXT.altName]: string }>;
|
||||
|
||||
/**
|
||||
* The namespaced value for m.html
|
||||
*/
|
||||
export const M_HTML = new UnstableValue("m.html", "org.matrix.msc1767.html");
|
||||
|
||||
/**
|
||||
* The content for an m.html event
|
||||
*/
|
||||
export type HtmlEventContent = EitherAnd<{ [M_HTML.name]: string }, { [M_HTML.altName]: string }>;
|
||||
|
||||
/**
|
||||
* The content for an m.message, m.text, or m.html event
|
||||
*/
|
||||
export type ExtensibleAnyMessageEventContent = ExtensibleMessageEventContent | TextEventContent | HtmlEventContent;
|
||||
|
||||
/**
|
||||
* The namespaced value for an m.reference relation
|
||||
*/
|
||||
export const REFERENCE_RELATION = new NamespacedValue("m.reference");
|
||||
|
||||
/**
|
||||
* Represents any relation type
|
||||
*/
|
||||
export type AnyRelation = TSNamespace<typeof REFERENCE_RELATION> | string;
|
||||
|
||||
/**
|
||||
* An m.relates_to relationship
|
||||
*/
|
||||
export type RelatesToRelationship<R = never, C = never> = {
|
||||
"m.relates_to": {
|
||||
// See https://github.com/microsoft/TypeScript/issues/23182#issuecomment-379091887 for array syntax
|
||||
rel_type: [R] extends [never] ? AnyRelation : TSNamespace<R>;
|
||||
event_id: string;
|
||||
} & DefaultNever<C, {}>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Partial types for a Matrix Event.
|
||||
*/
|
||||
export interface IPartialEvent<TContent> {
|
||||
type: string;
|
||||
content: TContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a potentially namespaced event type.
|
||||
*/
|
||||
export type ExtensibleEventType = NamespacedValue<string, string> | string;
|
||||
|
||||
/**
|
||||
* Determines if two event types are the same, including namespaces.
|
||||
* @param given - The given event type. This will be compared
|
||||
* against the expected type.
|
||||
* @param expected - The expected event type.
|
||||
* @returns True if the given type matches the expected type.
|
||||
*/
|
||||
export function isEventTypeSame(
|
||||
given: Optional<ExtensibleEventType>,
|
||||
expected: Optional<ExtensibleEventType>,
|
||||
): boolean {
|
||||
if (typeof given === "string") {
|
||||
if (typeof expected === "string") {
|
||||
return expected === given;
|
||||
} else {
|
||||
return (expected as NamespacedValue<string, string>).matches(given as string);
|
||||
}
|
||||
} else {
|
||||
if (typeof expected === "string") {
|
||||
return (given as NamespacedValue<string, string>).matches(expected as string);
|
||||
} else {
|
||||
const expectedNs = expected as NamespacedValue<string, string>;
|
||||
const givenNs = given as NamespacedValue<string, string>;
|
||||
return (
|
||||
expectedNs.matches(givenNs.name) ||
|
||||
(isProvided(givenNs.altName) && expectedNs.matches(givenNs.altName!))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ limitations under the License.
|
||||
import { EitherAnd } from "matrix-events-sdk";
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { TEXT_NODE_TYPE } from "./extensible_events";
|
||||
import { M_TEXT } from "./extensible_events";
|
||||
|
||||
export enum LocationAssetType {
|
||||
Self = "m.self",
|
||||
@@ -50,7 +50,7 @@ export type MLocationEvent = EitherAnd<
|
||||
{ [M_LOCATION.altName]: MLocationContent }
|
||||
>;
|
||||
|
||||
export type MTextEvent = EitherAnd<{ [TEXT_NODE_TYPE.name]: string }, { [TEXT_NODE_TYPE.altName]: string }>;
|
||||
export type MTextEvent = EitherAnd<{ [M_TEXT.name]: string }, { [M_TEXT.altName]: string }>;
|
||||
|
||||
/* From the spec at:
|
||||
* https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
Copyright 2022 - 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 { EitherAnd, UnstableValue } from "matrix-events-sdk";
|
||||
|
||||
import {
|
||||
ExtensibleAnyMessageEventContent,
|
||||
REFERENCE_RELATION,
|
||||
RelatesToRelationship,
|
||||
TSNamespace,
|
||||
} from "./extensible_events";
|
||||
|
||||
/**
|
||||
* Identifier for a disclosed poll.
|
||||
*/
|
||||
export const M_POLL_KIND_DISCLOSED = new UnstableValue("m.poll.disclosed", "org.matrix.msc3381.poll.disclosed");
|
||||
|
||||
/**
|
||||
* Identifier for an undisclosed poll.
|
||||
*/
|
||||
export const M_POLL_KIND_UNDISCLOSED = new UnstableValue("m.poll.undisclosed", "org.matrix.msc3381.poll.undisclosed");
|
||||
|
||||
/**
|
||||
* Any poll kind.
|
||||
*/
|
||||
export type PollKind = TSNamespace<typeof M_POLL_KIND_DISCLOSED> | TSNamespace<typeof M_POLL_KIND_UNDISCLOSED> | string;
|
||||
|
||||
/**
|
||||
* Known poll kind namespaces.
|
||||
*/
|
||||
export type KnownPollKind = typeof M_POLL_KIND_DISCLOSED | typeof M_POLL_KIND_UNDISCLOSED;
|
||||
|
||||
/**
|
||||
* The namespaced value for m.poll.start
|
||||
*/
|
||||
export const M_POLL_START = new UnstableValue("m.poll.start", "org.matrix.msc3381.poll.start");
|
||||
|
||||
/**
|
||||
* The m.poll.start type within event content
|
||||
*/
|
||||
export type PollStartSubtype = {
|
||||
question: ExtensibleAnyMessageEventContent;
|
||||
kind: PollKind;
|
||||
max_selections?: number; // default 1, always positive
|
||||
answers: PollAnswer[];
|
||||
};
|
||||
|
||||
/**
|
||||
* A poll answer.
|
||||
*/
|
||||
export type PollAnswer = ExtensibleAnyMessageEventContent & { id: string };
|
||||
|
||||
/**
|
||||
* The event definition for an m.poll.start event (in content)
|
||||
*/
|
||||
export type PollStartEvent = EitherAnd<
|
||||
{ [M_POLL_START.name]: PollStartSubtype },
|
||||
{ [M_POLL_START.altName]: PollStartSubtype }
|
||||
>;
|
||||
|
||||
/**
|
||||
* The content for an m.poll.start event
|
||||
*/
|
||||
export type PollStartEventContent = PollStartEvent & ExtensibleAnyMessageEventContent;
|
||||
|
||||
/**
|
||||
* The namespaced value for m.poll.response
|
||||
*/
|
||||
export const M_POLL_RESPONSE = new UnstableValue("m.poll.response", "org.matrix.msc3381.poll.response");
|
||||
|
||||
/**
|
||||
* The m.poll.response type within event content
|
||||
*/
|
||||
export type PollResponseSubtype = {
|
||||
answers: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* The event definition for an m.poll.response event (in content)
|
||||
*/
|
||||
export type PollResponseEvent = EitherAnd<
|
||||
{ [M_POLL_RESPONSE.name]: PollResponseSubtype },
|
||||
{ [M_POLL_RESPONSE.altName]: PollResponseSubtype }
|
||||
>;
|
||||
|
||||
/**
|
||||
* The content for an m.poll.response event
|
||||
*/
|
||||
export type PollResponseEventContent = PollResponseEvent & RelatesToRelationship<typeof REFERENCE_RELATION>;
|
||||
|
||||
/**
|
||||
* The namespaced value for m.poll.end
|
||||
*/
|
||||
export const M_POLL_END = new UnstableValue("m.poll.end", "org.matrix.msc3381.poll.end");
|
||||
|
||||
/**
|
||||
* The event definition for an m.poll.end event (in content)
|
||||
*/
|
||||
export type PollEndEvent = EitherAnd<{ [M_POLL_END.name]: {} }, { [M_POLL_END.altName]: {} }>;
|
||||
|
||||
/**
|
||||
* The content for an m.poll.end event
|
||||
*/
|
||||
export type PollEndEventContent = PollEndEvent &
|
||||
RelatesToRelationship<typeof REFERENCE_RELATION> &
|
||||
ExtensibleAnyMessageEventContent;
|
||||
@@ -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;
|
||||
|
||||
+2
-1
@@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EitherAnd, IMessageRendering } from "matrix-events-sdk";
|
||||
import { EitherAnd } from "matrix-events-sdk";
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
import { IMessageRendering } from "./extensible_events";
|
||||
|
||||
/**
|
||||
* Extensible topic event type based on MSC3765
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -47,7 +47,7 @@ interface WellKnownConfig extends Omit<IWellKnownConfig, "error"> {
|
||||
error?: IWellKnownConfig["error"] | null;
|
||||
}
|
||||
|
||||
interface ClientConfig extends Omit<IClientWellKnown, "m.homeserver" | "m.identity_server"> {
|
||||
export interface ClientConfig extends Omit<IClientWellKnown, "m.homeserver" | "m.identity_server"> {
|
||||
"m.homeserver": WellKnownConfig;
|
||||
"m.identity_server": WellKnownConfig;
|
||||
}
|
||||
|
||||
+318
-174
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2015-2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015-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.
|
||||
@@ -20,7 +20,7 @@ limitations under the License.
|
||||
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
|
||||
import type { IMegolmSessionData } from "./@types/crypto";
|
||||
import type { IDeviceKeys, IMegolmSessionData, IOneTimeKey } from "./@types/crypto";
|
||||
import { ISyncStateData, SyncApi, SyncApiOptions, SyncState } from "./sync";
|
||||
import {
|
||||
EventStatus,
|
||||
@@ -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,19 +79,13 @@ 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";
|
||||
import { getHttpUriForMxc } from "./content-repo";
|
||||
import { SearchResult } from "./models/search-result";
|
||||
import {
|
||||
DEHYDRATION_ALGORITHM,
|
||||
IDehydratedDevice,
|
||||
IDehydratedDeviceKeyInfo,
|
||||
IDeviceKeys,
|
||||
IOneTimeKey,
|
||||
} from "./crypto/dehydration";
|
||||
import { DEHYDRATION_ALGORITHM, IDehydratedDevice, IDehydratedDeviceKeyInfo } from "./crypto/dehydration";
|
||||
import {
|
||||
IKeyBackupInfo,
|
||||
IKeyBackupPrepareOpts,
|
||||
@@ -209,10 +203,10 @@ import { ToDeviceBatch } from "./models/ToDeviceMessage";
|
||||
import { IgnoredInvites } from "./models/invites-ignorer";
|
||||
import { UIARequest, UIAResponse } from "./@types/uia";
|
||||
import { LocalNotificationSettings } from "./@types/local_notifications";
|
||||
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync";
|
||||
import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature";
|
||||
import { CryptoBackend } from "./common-crypto/CryptoBackend";
|
||||
import { RUST_SDK_STORE_PREFIX } from "./rust-crypto/constants";
|
||||
import { DeviceInfoMap } from "./crypto/DeviceList";
|
||||
|
||||
export type Store = IStore;
|
||||
|
||||
@@ -378,6 +372,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 {
|
||||
@@ -446,10 +447,16 @@ export interface IStartClientOpts {
|
||||
clientWellKnownPollPeriod?: number;
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* @deprecated use `threadSupport` instead
|
||||
*/
|
||||
experimentalThreadSupport?: boolean;
|
||||
|
||||
/**
|
||||
* Will organises events in threaded conversations when
|
||||
* a thread relation is encountered
|
||||
*/
|
||||
threadSupport?: boolean;
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
@@ -499,6 +506,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;
|
||||
@@ -568,6 +577,8 @@ export interface IWellKnownConfig {
|
||||
error?: Error | string;
|
||||
// eslint-disable-next-line
|
||||
base_url?: string | null;
|
||||
// XXX: this is undocumented
|
||||
server_name?: string;
|
||||
}
|
||||
|
||||
export interface IDelegatedAuthConfig {
|
||||
@@ -855,6 +866,7 @@ export enum ClientEvent {
|
||||
SyncUnexpectedError = "sync.unexpectedError",
|
||||
ClientWellKnown = "WellKnown.client",
|
||||
ReceivedVoipEvent = "received_voip_event",
|
||||
UndecryptableToDeviceEvent = "toDeviceEvent.undecryptable",
|
||||
TurnServers = "turnServers",
|
||||
TurnServersError = "turnServers.error",
|
||||
}
|
||||
@@ -1063,6 +1075,18 @@ export type ClientEventHandlerMap = {
|
||||
* ```
|
||||
*/
|
||||
[ClientEvent.ToDeviceEvent]: (event: MatrixEvent) => void;
|
||||
/**
|
||||
* Fires if a to-device event is received that cannot be decrypted.
|
||||
* Encrypted to-device events will (generally) use plain Olm encryption,
|
||||
* in which case decryption failures are fatal: the event will never be
|
||||
* decryptable, unlike Megolm encrypted events where the key may simply
|
||||
* arrive later.
|
||||
*
|
||||
* An undecryptable to-device event is therefore likley to indicate problems.
|
||||
*
|
||||
* @param event - The undecyptable to-device event
|
||||
*/
|
||||
[ClientEvent.UndecryptableToDeviceEvent]: (event: MatrixEvent) => void;
|
||||
/**
|
||||
* Fires whenever new user-scoped account_data is added.
|
||||
* @param event - The event describing the account_data just added
|
||||
@@ -1155,6 +1179,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.
|
||||
@@ -1287,6 +1312,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.on(ClientEvent.Sync, this.startCallEventHandler);
|
||||
}
|
||||
|
||||
this.on(ClientEvent.Sync, this.fixupRoomNotifications);
|
||||
|
||||
this.timelineSupport = Boolean(opts.timelineSupport);
|
||||
|
||||
this.cryptoStore = opts.cryptoStore;
|
||||
@@ -1297,6 +1324,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;
|
||||
|
||||
@@ -1435,6 +1463,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.syncApi = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
|
||||
}
|
||||
|
||||
if (this.clientOpts.hasOwnProperty("experimentalThreadSupport")) {
|
||||
logger.warn("`experimentalThreadSupport` has been deprecated, use `threadSupport` instead");
|
||||
}
|
||||
|
||||
// If `threadSupport` is omitted and the deprecated `experimentalThreadSupport` has been passed
|
||||
// We should fallback to that value for backwards compatibility purposes
|
||||
if (
|
||||
!this.clientOpts.hasOwnProperty("threadSupport") &&
|
||||
this.clientOpts.hasOwnProperty("experimentalThreadSupport")
|
||||
) {
|
||||
this.clientOpts.threadSupport = this.clientOpts.experimentalThreadSupport;
|
||||
}
|
||||
|
||||
this.syncApi.sync();
|
||||
|
||||
if (this.clientOpts.clientWellKnownPollPeriod !== undefined) {
|
||||
@@ -1690,12 +1731,16 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
resolve(0);
|
||||
};
|
||||
req.onerror = (e): void => {
|
||||
logger.error(`Failed to remove IndexedDB instance ${dbname}: ${e}`);
|
||||
reject(new Error(`Error clearing storage: ${e}`));
|
||||
// In private browsing, Firefox has a global.indexedDB, but attempts to delete an indexeddb
|
||||
// (even a non-existent one) fail with "DOMException: A mutation operation was attempted on a
|
||||
// database that did not allow mutations."
|
||||
//
|
||||
// it seems like the only thing we can really do is ignore the error.
|
||||
logger.warn(`Failed to remove IndexedDB instance ${dbname}:`, e);
|
||||
resolve(0);
|
||||
};
|
||||
req.onblocked = (e): void => {
|
||||
logger.info(`cannot yet remove IndexedDB instance ${dbname}`);
|
||||
//reject(new Error(`Error clearing storage: ${e}`));
|
||||
};
|
||||
});
|
||||
await prom;
|
||||
@@ -1847,6 +1892,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,
|
||||
@@ -1854,8 +1901,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
isPtt,
|
||||
intent,
|
||||
undefined,
|
||||
dataChannelsEnabled,
|
||||
dataChannelsEnabled || this.isVoipWithNoMediaAllowed,
|
||||
dataChannelOptions,
|
||||
this.isVoipWithNoMediaAllowed,
|
||||
).create();
|
||||
}
|
||||
|
||||
@@ -2148,7 +2196,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// importing rust-crypto will download the webassembly, so we delay it until we know it will be
|
||||
// needed.
|
||||
const RustCrypto = await import("./rust-crypto");
|
||||
this.cryptoBackend = await RustCrypto.initRustCrypto(this.http, userId, deviceId);
|
||||
const rustCrypto = await RustCrypto.initRustCrypto(this.http, userId, deviceId);
|
||||
this.cryptoBackend = rustCrypto;
|
||||
|
||||
// attach the event listeners needed by RustCrypto
|
||||
this.on(RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2194,7 +2246,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"));
|
||||
}
|
||||
@@ -2490,10 +2542,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns
|
||||
*/
|
||||
public checkUserTrust(userId: string): UserTrustLevel {
|
||||
if (!this.crypto) {
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this.crypto.checkUserTrust(userId);
|
||||
return this.cryptoBackend.checkUserTrust(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2505,10 +2557,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param deviceId - The ID of the device to check
|
||||
*/
|
||||
public checkDeviceTrust(userId: string, deviceId: string): DeviceTrustLevel {
|
||||
if (!this.crypto) {
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this.crypto.checkDeviceTrust(userId, deviceId);
|
||||
return this.cryptoBackend.checkDeviceTrust(userId, deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2571,10 +2623,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param room - the room the event is in
|
||||
*/
|
||||
public prepareToEncrypt(room: Room): void {
|
||||
if (!this.crypto) {
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
this.crypto.prepareToEncrypt(room);
|
||||
this.cryptoBackend.prepareToEncrypt(room);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2672,10 +2724,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns The event information.
|
||||
*/
|
||||
public getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo {
|
||||
if (!this.crypto) {
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this.crypto.getEventEncryptionInfo(event);
|
||||
return this.cryptoBackend.getEventEncryptionInfo(event);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3711,9 +3763,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
|
||||
@@ -3763,14 +3815,18 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* This is essentially getRooms() with some rooms filtered out, eg. old versions
|
||||
* of rooms that have been replaced or (in future) other rooms that have been
|
||||
* marked at the protocol level as not to be displayed to the user.
|
||||
*
|
||||
* @param msc3946ProcessDynamicPredecessor - if true, look for an
|
||||
* m.room.predecessor state event and
|
||||
* use it if found (MSC3946).
|
||||
* @returns A list of rooms, or an empty list if there is no data store.
|
||||
*/
|
||||
public getVisibleRooms(): Room[] {
|
||||
public getVisibleRooms(msc3946ProcessDynamicPredecessor = false): Room[] {
|
||||
const allRooms = this.store.getRooms();
|
||||
|
||||
const replacedRooms = new Set();
|
||||
for (const r of allRooms) {
|
||||
const predecessor = r.findPredecessorRoomId();
|
||||
const predecessor = r.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
|
||||
if (predecessor) {
|
||||
replacedRooms.add(predecessor);
|
||||
}
|
||||
@@ -4089,24 +4145,27 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
public setPowerLevel(
|
||||
roomId: string,
|
||||
userId: string | string[],
|
||||
powerLevel: number,
|
||||
event: MatrixEvent,
|
||||
powerLevel: number | undefined,
|
||||
event: MatrixEvent | null,
|
||||
): Promise<ISendEventResponse> {
|
||||
let content = {
|
||||
users: {} as Record<string, number>,
|
||||
};
|
||||
if (event.getType() === EventType.RoomPowerLevels) {
|
||||
if (event?.getType() === EventType.RoomPowerLevels) {
|
||||
// take a copy of the content to ensure we don't corrupt
|
||||
// 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,
|
||||
});
|
||||
@@ -4351,11 +4410,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.isRoomEncrypted(event.getRoomId()!)) {
|
||||
if (!room || !this.isRoomEncrypted(event.getRoomId()!)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.crypto && this.usingExternalCrypto) {
|
||||
if (!this.cryptoBackend && this.usingExternalCrypto) {
|
||||
// The client has opted to allow sending messages to encrypted
|
||||
// rooms even if the room is encrypted, and we haven't setup
|
||||
// crypto. This is useful for users of matrix-org/pantalaimon
|
||||
@@ -4376,13 +4435,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.crypto) {
|
||||
throw new Error(
|
||||
"This room is configured to use encryption, but your client does " + "not support encryption.",
|
||||
);
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("This room is configured to use encryption, but your client does not support encryption.");
|
||||
}
|
||||
|
||||
return this.crypto.encryptEvent(event, room);
|
||||
return this.cryptoBackend.encryptEvent(event, room);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4800,8 +4857,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
$eventId: event.getId()!,
|
||||
});
|
||||
|
||||
const supportsThreadRR = this.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported;
|
||||
if (supportsThreadRR && !unthreaded) {
|
||||
if (!unthreaded) {
|
||||
const isThread = !!event.threadRootId;
|
||||
body = {
|
||||
...body,
|
||||
@@ -4969,70 +5025,83 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* which can be proven to be linked. For example, rooms which have a create
|
||||
* event pointing to an old room which the client is not aware of or doesn't
|
||||
* have a matching tombstone would not be returned.
|
||||
* @param msc3946ProcessDynamicPredecessor - if true, look for
|
||||
* m.room.predecessor state events as well as create events, and prefer
|
||||
* predecessor events where they exist (MSC3946).
|
||||
* @returns An array of rooms representing the upgrade
|
||||
* history.
|
||||
*/
|
||||
public getRoomUpgradeHistory(roomId: string, verifyLinks = false): Room[] {
|
||||
let currentRoom = this.getRoom(roomId);
|
||||
public getRoomUpgradeHistory(
|
||||
roomId: string,
|
||||
verifyLinks = false,
|
||||
msc3946ProcessDynamicPredecessor = false,
|
||||
): Room[] {
|
||||
const currentRoom = this.getRoom(roomId);
|
||||
if (!currentRoom) return [];
|
||||
|
||||
const upgradeHistory = [currentRoom];
|
||||
const before = this.findPredecessorRooms(currentRoom, verifyLinks, msc3946ProcessDynamicPredecessor);
|
||||
const after = this.findSuccessorRooms(currentRoom, verifyLinks, msc3946ProcessDynamicPredecessor);
|
||||
|
||||
// Work backwards first, looking at create events.
|
||||
let createEvent = currentRoom.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
while (createEvent) {
|
||||
const predecessor = createEvent.getContent()["predecessor"];
|
||||
if (predecessor && predecessor["room_id"]) {
|
||||
const refRoom = this.getRoom(predecessor["room_id"]);
|
||||
if (!refRoom) break; // end of the chain
|
||||
return [...before, currentRoom, ...after];
|
||||
}
|
||||
|
||||
if (verifyLinks) {
|
||||
const tombstone = refRoom.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
private findPredecessorRooms(room: Room, verifyLinks: boolean, msc3946ProcessDynamicPredecessor: boolean): Room[] {
|
||||
const ret: Room[] = [];
|
||||
|
||||
if (!tombstone || tombstone.getContent()["replacement_room"] !== refRoom.roomId) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert at the front because we're working backwards from the currentRoom
|
||||
upgradeHistory.splice(0, 0, refRoom);
|
||||
createEvent = refRoom.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
} else {
|
||||
// No further create events to look at
|
||||
// Work backwards from newer to older rooms
|
||||
let predecessorRoomId = room.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
|
||||
while (predecessorRoomId !== null) {
|
||||
const predecessorRoom = this.getRoom(predecessorRoomId);
|
||||
if (predecessorRoom === null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (verifyLinks) {
|
||||
const tombstone = predecessorRoom.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
if (!tombstone || tombstone.getContent()["replacement_room"] !== room.roomId) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Work forwards next, looking at tombstone events
|
||||
let tombstoneEvent = currentRoom.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
// Insert at the front because we're working backwards from the currentRoom
|
||||
ret.splice(0, 0, predecessorRoom);
|
||||
|
||||
room = predecessorRoom;
|
||||
predecessorRoomId = room.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private findSuccessorRooms(room: Room, verifyLinks: boolean, msc3946ProcessDynamicPredecessor: boolean): Room[] {
|
||||
const ret: Room[] = [];
|
||||
|
||||
// Work forwards, looking at tombstone events
|
||||
let tombstoneEvent = room.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
while (tombstoneEvent) {
|
||||
const refRoom = this.getRoom(tombstoneEvent.getContent()["replacement_room"]);
|
||||
if (!refRoom) break; // end of the chain
|
||||
if (refRoom.roomId === currentRoom.roomId) break; // Tombstone is referencing it's own room
|
||||
const successorRoom = this.getRoom(tombstoneEvent.getContent()["replacement_room"]);
|
||||
if (!successorRoom) break; // end of the chain
|
||||
if (successorRoom.roomId === room.roomId) break; // Tombstone is referencing its own room
|
||||
|
||||
if (verifyLinks) {
|
||||
createEvent = refRoom.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
if (!createEvent || !createEvent.getContent()["predecessor"]) break;
|
||||
|
||||
const predecessor = createEvent.getContent()["predecessor"];
|
||||
if (predecessor["room_id"] !== currentRoom.roomId) break;
|
||||
const predecessorRoomId = successorRoom.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId;
|
||||
if (!predecessorRoomId || predecessorRoomId !== room.roomId) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Push to the end because we're looking forwards
|
||||
upgradeHistory.push(refRoom);
|
||||
const roomIds = new Set(upgradeHistory.map((ref) => ref.roomId));
|
||||
if (roomIds.size < upgradeHistory.length) {
|
||||
ret.push(successorRoom);
|
||||
const roomIds = new Set(ret.map((ref) => ref.roomId));
|
||||
if (roomIds.size < ret.length) {
|
||||
// The last room added to the list introduced a previous roomId
|
||||
// To avoid recursion, return the last rooms - 1
|
||||
return upgradeHistory.slice(0, upgradeHistory.length - 1);
|
||||
return ret.slice(0, ret.length - 1);
|
||||
}
|
||||
|
||||
// Set the current room to the reference room so we know where we're at
|
||||
currentRoom = refRoom;
|
||||
tombstoneEvent = currentRoom.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
room = successorRoom;
|
||||
tombstoneEvent = room.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||
}
|
||||
|
||||
return upgradeHistory;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5403,7 +5472,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents);
|
||||
|
||||
this.processBeaconEvents(room, timelineEvents);
|
||||
this.processAggregatedTimelineEvents(room, timelineEvents);
|
||||
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
|
||||
this.processThreadEvents(room, threadedEvents, true);
|
||||
|
||||
@@ -5464,7 +5533,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return timelineSet.getTimelineForEvent(eventId);
|
||||
}
|
||||
|
||||
if (timelineSet.thread && this.supportsExperimentalThreads()) {
|
||||
if (timelineSet.thread && this.supportsThreads()) {
|
||||
return this.getThreadTimeline(timelineSet, eventId);
|
||||
}
|
||||
|
||||
@@ -5518,7 +5587,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
|
||||
// The target event is not in a thread but process the contextual events, so we can show any threads around it.
|
||||
this.processThreadEvents(timelineSet.room, threadedEvents, true);
|
||||
this.processBeaconEvents(timelineSet.room, timelineEvents);
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, timelineEvents);
|
||||
|
||||
// There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring
|
||||
// timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up
|
||||
@@ -5531,7 +5600,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
public async getThreadTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<EventTimeline | undefined> {
|
||||
if (!this.supportsExperimentalThreads()) {
|
||||
if (!this.supportsThreads()) {
|
||||
throw new Error("could not get thread timeline: no client support");
|
||||
}
|
||||
|
||||
@@ -5613,7 +5682,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
|
||||
timeline.setPaginationToken(resNewer.next_batch ?? null, Direction.Forward);
|
||||
this.processBeaconEvents(timelineSet.room, events);
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, events);
|
||||
|
||||
// There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring
|
||||
// timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up
|
||||
@@ -5670,7 +5739,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
|
||||
timeline.setPaginationToken(null, Direction.Forward);
|
||||
this.processBeaconEvents(timelineSet.room, events);
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, events);
|
||||
|
||||
return timeline;
|
||||
}
|
||||
@@ -5911,6 +5980,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);
|
||||
@@ -5923,7 +5994,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
// in the notification timeline set
|
||||
const timelineSet = eventTimeline.getTimelineSet();
|
||||
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
|
||||
this.processBeaconEvents(timelineSet.room, matrixEvents);
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
|
||||
|
||||
// if we've hit the end of the timeline, we need to stop trying to
|
||||
// paginate. We need to keep the 'forwards' token though, to make sure
|
||||
@@ -5957,15 +6028,15 @@ 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);
|
||||
this.processBeaconEvents(room, matrixEvents);
|
||||
this.processAggregatedTimelineEvents(room, matrixEvents);
|
||||
this.processThreadRoots(room, matrixEvents, backwards);
|
||||
|
||||
// if we've hit the end of the timeline, we need to stop trying to
|
||||
@@ -5993,7 +6064,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()) {
|
||||
@@ -6012,7 +6083,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const originalEvent = await this.fetchRoomEvent(eventTimeline.getRoomId() ?? "", thread.id);
|
||||
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, eventTimeline, null);
|
||||
}
|
||||
this.processBeaconEvents(timelineSet.room, matrixEvents);
|
||||
this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
|
||||
|
||||
// if we've hit the end of the timeline, we need to stop trying to
|
||||
// paginate. We need to keep the 'forwards' token though, to make sure
|
||||
@@ -6041,16 +6112,16 @@ 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);
|
||||
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
||||
this.processBeaconEvents(room, timelineEvents);
|
||||
this.processAggregatedTimelineEvents(room, timelineEvents);
|
||||
this.processThreadRoots(
|
||||
room,
|
||||
timelineEvents.filter((it) => it.getServerAggregatedRelation(THREAD_RELATION_TYPE.name)),
|
||||
@@ -6762,6 +6833,31 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Once the client has been initialised, we want to clear notifications we
|
||||
* know for a fact should be here.
|
||||
* This issue should also be addressed on synapse's side and is tracked as part
|
||||
* of https://github.com/matrix-org/synapse/issues/14837
|
||||
*
|
||||
* We consider a room or a thread as fully read if the current user has sent
|
||||
* the last event in the live timeline of that context and if the read receipt
|
||||
* we have on record matches.
|
||||
*/
|
||||
private fixupRoomNotifications = (): void => {
|
||||
if (this.isInitialSyncComplete()) {
|
||||
const unreadRooms = (this.getRooms() ?? []).filter((room) => {
|
||||
return room.getUnreadNotificationCount(NotificationCountType.Total) > 0;
|
||||
});
|
||||
|
||||
for (const room of unreadRooms) {
|
||||
const currentUserId = this.getSafeUserId();
|
||||
room.fixupNotifications(currentUserId);
|
||||
}
|
||||
|
||||
this.off(ClientEvent.Sync, this.fixupRoomNotifications);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns Promise which resolves: ITurnServerResponse object
|
||||
* @returns Rejects: with an error response.
|
||||
@@ -6992,10 +7088,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const serverVersions = await this.serverVersionsPromise;
|
||||
this.canSupport = await buildFeatureSupportMap(serverVersions);
|
||||
|
||||
// We can set flag values to use their stable or unstable version
|
||||
const support = this.canSupport.get(Feature.ThreadUnreadNotifications);
|
||||
UNREAD_THREAD_NOTIFICATIONS.setPreferUnstable(support === ServerSupport.Unstable);
|
||||
|
||||
return this.serverVersionsPromise;
|
||||
}
|
||||
|
||||
@@ -8424,10 +8516,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);
|
||||
// Pre-calculate any necessary caches.
|
||||
this.pushProcessor.updateCachedPushRuleKeys(this.pushRules);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Promise which resolves: an empty object `{}`
|
||||
* @returns Rejects: with an error response.
|
||||
@@ -8674,18 +8778,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,
|
||||
@@ -8716,7 +8821,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
|
||||
*/
|
||||
@@ -8725,19 +8830,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,
|
||||
@@ -9028,24 +9134,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);
|
||||
@@ -9303,12 +9407,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* @deprecated use supportsThreads() instead
|
||||
*/
|
||||
public supportsExperimentalThreads(): boolean {
|
||||
logger.warn(`supportsExperimentalThreads() is deprecated, use supportThreads() instead`);
|
||||
return this.clientOpts?.experimentalThreadSupport || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper to determine thread support
|
||||
* @returns a boolean to determine if threads are enabled
|
||||
*/
|
||||
public supportsThreads(): boolean {
|
||||
return this.clientOpts?.threadSupport || 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
|
||||
@@ -9323,24 +9436,42 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Processes a list of threaded events and adds them to their respective timelines
|
||||
* @param room - the room the adds the threaded events
|
||||
* @param threadedEvents - an array of the threaded events
|
||||
* @param toStartOfTimeline - the direction in which we want to add the events
|
||||
*/
|
||||
public processThreadEvents(room: Room, threadedEvents: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
room.processThreadedEvents(threadedEvents, toStartOfTimeline);
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Processes a list of thread roots and creates a thread model
|
||||
* @param room - the room to create the threads in
|
||||
* @param threadedEvents - an array of thread roots
|
||||
* @param toStartOfTimeline - the direction
|
||||
*/
|
||||
public processThreadRoots(room: Room, threadedEvents: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
room.processThreadRoots(threadedEvents, toStartOfTimeline);
|
||||
}
|
||||
|
||||
public processBeaconEvents(room?: Room, events?: MatrixEvent[]): void {
|
||||
this.processAggregatedTimelineEvents(room, events);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls aggregation functions for event types that are aggregated
|
||||
* Polls and location beacons
|
||||
* @param room - room the events belong to
|
||||
* @param events - timeline events to be processed
|
||||
* @returns
|
||||
*/
|
||||
public processAggregatedTimelineEvents(room?: Room, events?: MatrixEvent[]): void {
|
||||
if (!events?.length) return;
|
||||
if (!room) return;
|
||||
|
||||
room.currentState.processBeaconEvents(events, this);
|
||||
room.processPollEvents(events);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9405,64 +9536,77 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* accurate notification_count
|
||||
*/
|
||||
export function fixNotificationCountOnDecryption(cli: MatrixClient, event: MatrixEvent): void {
|
||||
const ourUserId = cli.getUserId();
|
||||
const eventId = event.getId();
|
||||
|
||||
const room = cli.getRoom(event.getRoomId());
|
||||
if (!room || !ourUserId || !eventId) return;
|
||||
|
||||
const oldActions = event.getPushActions();
|
||||
const actions = cli.getPushActionsForEvent(event, true);
|
||||
|
||||
const room = cli.getRoom(event.getRoomId());
|
||||
if (!room || !cli.getUserId()) return;
|
||||
|
||||
const isThreadEvent = !!event.threadRootId && !event.isThreadRoot;
|
||||
|
||||
const currentCount = room.getUnreadCountForEventContext(NotificationCountType.Highlight, event);
|
||||
const currentHighlightCount = room.getUnreadCountForEventContext(NotificationCountType.Highlight, event);
|
||||
|
||||
// Ensure the unread counts are kept up to date if the event is encrypted
|
||||
// We also want to make sure that the notification count goes up if we already
|
||||
// have encrypted events to avoid other code from resetting 'highlight' to zero.
|
||||
const oldHighlight = !!oldActions?.tweaks?.highlight;
|
||||
const newHighlight = !!actions?.tweaks?.highlight;
|
||||
if (oldHighlight !== newHighlight || currentCount > 0) {
|
||||
|
||||
let hasReadEvent;
|
||||
if (isThreadEvent) {
|
||||
const thread = room.getThread(event.threadRootId);
|
||||
hasReadEvent = thread
|
||||
? thread.hasUserReadEvent(ourUserId, eventId)
|
||||
: // If the thread object does not exist in the room yet, we don't
|
||||
// want to calculate notification for this event yet. We have not
|
||||
// restored the read receipts yet and can't accurately calculate
|
||||
// notifications at this stage.
|
||||
//
|
||||
// This issue can likely go away when MSC3874 is implemented
|
||||
true;
|
||||
} else {
|
||||
hasReadEvent = room.hasUserReadEvent(ourUserId, eventId);
|
||||
}
|
||||
|
||||
if (hasReadEvent) {
|
||||
// If the event has been read, ignore it.
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldHighlight !== newHighlight || currentHighlightCount > 0) {
|
||||
// TODO: Handle mentions received while the client is offline
|
||||
// See also https://github.com/vector-im/element-web/issues/9069
|
||||
let hasReadEvent;
|
||||
let newCount = currentHighlightCount;
|
||||
if (newHighlight && !oldHighlight) newCount++;
|
||||
if (!newHighlight && oldHighlight) newCount--;
|
||||
|
||||
if (isThreadEvent) {
|
||||
const thread = room.getThread(event.threadRootId);
|
||||
hasReadEvent = thread
|
||||
? thread.hasUserReadEvent(cli.getUserId()!, event.getId()!)
|
||||
: // If the thread object does not exist in the room yet, we don't
|
||||
// want to calculate notification for this event yet. We have not
|
||||
// restored the read receipts yet and can't accurately calculate
|
||||
// highlight notifications at this stage.
|
||||
//
|
||||
// This issue can likely go away when MSC3874 is implemented
|
||||
true;
|
||||
room.setThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Highlight, newCount);
|
||||
} else {
|
||||
hasReadEvent = room.hasUserReadEvent(cli.getUserId()!, event.getId()!);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, newCount);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasReadEvent) {
|
||||
let newCount = currentCount;
|
||||
if (newHighlight && !oldHighlight) newCount++;
|
||||
if (!newHighlight && oldHighlight) newCount--;
|
||||
// Total count is used to typically increment a room notification counter, but not loudly highlight it.
|
||||
const currentTotalCount = room.getUnreadCountForEventContext(NotificationCountType.Total, event);
|
||||
|
||||
if (isThreadEvent) {
|
||||
room.setThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Highlight, newCount);
|
||||
} else {
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, newCount);
|
||||
}
|
||||
// `notify` is used in practice for incrementing the total count
|
||||
const newNotify = !!actions?.notify;
|
||||
|
||||
// Fix 'Mentions Only' rooms from not having the right badge count
|
||||
const totalCount =
|
||||
(isThreadEvent
|
||||
? room.getThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Total)
|
||||
: room.getRoomUnreadNotificationCount(NotificationCountType.Total)) ?? 0;
|
||||
|
||||
if (totalCount < newCount) {
|
||||
if (isThreadEvent) {
|
||||
room.setThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Total, newCount);
|
||||
} else {
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, newCount);
|
||||
}
|
||||
}
|
||||
// The room total count is NEVER incremented by the server for encrypted rooms. We basically ignore
|
||||
// the server here as it's always going to tell us to increment for encrypted events.
|
||||
if (newNotify) {
|
||||
if (isThreadEvent) {
|
||||
room.setThreadUnreadNotificationCount(
|
||||
event.threadRootId,
|
||||
NotificationCountType.Total,
|
||||
currentTotalCount + 1,
|
||||
);
|
||||
} else {
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, currentTotalCount + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@ 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 { IEncryptedEventInfo } from "../crypto/api";
|
||||
|
||||
/**
|
||||
* Common interface for the crypto implementations
|
||||
@@ -54,6 +57,45 @@ export interface CryptoBackend extends SyncCryptoCallbacks {
|
||||
*/
|
||||
userHasCrossSigningKeys(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get the verification level for a given user
|
||||
*
|
||||
* TODO: define this better
|
||||
*
|
||||
* @param userId - user to be checked
|
||||
*/
|
||||
checkUserTrust(userId: string): UserTrustLevel;
|
||||
|
||||
/**
|
||||
* Get the verification level for a given device
|
||||
*
|
||||
* TODO: define this better
|
||||
*
|
||||
* @param userId - user to be checked
|
||||
* @param deviceId - device to be checked
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param event - event to be sent
|
||||
*
|
||||
* @param room - destination room.
|
||||
*
|
||||
* @returns Promise which resolves when the event has been
|
||||
* encrypted, or null if nothing was needed
|
||||
*/
|
||||
encryptEvent(event: MatrixEvent, room: Room): Promise<void>;
|
||||
|
||||
/**
|
||||
* Decrypt a received event
|
||||
*
|
||||
@@ -62,6 +104,13 @@ export interface CryptoBackend extends SyncCryptoCallbacks {
|
||||
*/
|
||||
decryptEvent(event: MatrixEvent): Promise<IEventDecryptionResult>;
|
||||
|
||||
/**
|
||||
* Get information about the encryption of an event
|
||||
*
|
||||
* @param event - event to be checked
|
||||
*/
|
||||
getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo;
|
||||
|
||||
/**
|
||||
* Get a list containing all of the room keys
|
||||
*
|
||||
@@ -89,6 +138,20 @@ export interface SyncCryptoCallbacks {
|
||||
*/
|
||||
preprocessToDeviceMessages(events: IToDeviceEvent[]): Promise<IToDeviceEvent[]>;
|
||||
|
||||
/**
|
||||
* Called by the /sync loop whenever an m.room.encryption event is received.
|
||||
*
|
||||
* This is called before RoomStateEvents are emitted for any of the events in the /sync
|
||||
* response (even if the other events technically happened first). This works around a problem
|
||||
* if the client uses a RoomStateEvent (typically a membership event) as a trigger to send a message
|
||||
* in a new room (or one where encryption has been newly enabled): that would otherwise leave the
|
||||
* crypto layer confused because it expects crypto to be set up, but it has not yet been.
|
||||
*
|
||||
* @param room - in which the event was received
|
||||
* @param event - encryption event to be processed
|
||||
*/
|
||||
onCryptoEvent(room: Room, event: MatrixEvent): Promise<void>;
|
||||
|
||||
/**
|
||||
* Called by the /sync loop after each /sync response is processed.
|
||||
*
|
||||
|
||||
@@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { isProvided, REFERENCE_RELATION } from "matrix-events-sdk";
|
||||
|
||||
import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent } from "./@types/beacon";
|
||||
import { MsgType } from "./@types/event";
|
||||
import { TEXT_NODE_TYPE } from "./@types/extensible_events";
|
||||
import { M_TEXT, REFERENCE_RELATION } from "./@types/extensible_events";
|
||||
import { isProvided } from "./extensible_events_v1/utilities";
|
||||
import {
|
||||
M_ASSET,
|
||||
LocationAssetType,
|
||||
@@ -160,7 +159,7 @@ export const makeLocationContent = (
|
||||
[M_ASSET.name]: {
|
||||
type: assetType || LocationAssetType.Self,
|
||||
},
|
||||
[TEXT_NODE_TYPE.name]: defaultedText,
|
||||
[M_TEXT.name]: defaultedText,
|
||||
...timestampEvent,
|
||||
} as LegacyLocationEventContent & MLocationEventContent;
|
||||
};
|
||||
@@ -173,7 +172,7 @@ export const parseLocationEvent = (wireEventContent: LocationEventWireContent):
|
||||
const location = M_LOCATION.findIn<MLocationContent>(wireEventContent);
|
||||
const asset = M_ASSET.findIn<MAssetContent>(wireEventContent);
|
||||
const timestamp = M_TIMESTAMP.findIn<number>(wireEventContent);
|
||||
const text = TEXT_NODE_TYPE.findIn<string>(wireEventContent);
|
||||
const text = M_TEXT.findIn<string>(wireEventContent);
|
||||
|
||||
const geoUri = location?.uri ?? wireEventContent?.geo_uri;
|
||||
const description = location?.description;
|
||||
@@ -203,6 +202,9 @@ export type TopicState = {
|
||||
|
||||
export const parseTopicContent = (content: MRoomTopicEventContent): TopicState => {
|
||||
const mtopic = M_TOPIC.findIn<MTopicContent>(content);
|
||||
if (!Array.isArray(mtopic)) {
|
||||
return { text: content.topic };
|
||||
}
|
||||
const text = mtopic?.find((r) => !isProvided(r.mimetype) || r.mimetype === "text/plain")?.body ?? content.topic;
|
||||
const html = mtopic?.find((r) => r.mimetype === "text/html")?.body;
|
||||
return { text, html };
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -368,6 +368,11 @@ export class OlmDevice {
|
||||
*/
|
||||
private saveSession(deviceKey: string, sessionInfo: IUnpickledSessionInfo, txn: unknown): void {
|
||||
const sessionId = sessionInfo.session.session_id();
|
||||
logger.debug(`Saving Olm session ${sessionId} with device ${deviceKey}: ${sessionInfo.session.describe()}`);
|
||||
|
||||
// Why do we re-use the input object for this, overwriting the same key with a different
|
||||
// type? Is it because we want to erase the unpickled session to enforce that it's no longer
|
||||
// used? A comment would be great.
|
||||
const pickledSessionInfo = Object.assign(sessionInfo, {
|
||||
session: sessionInfo.session.pickle(this.pickleKey),
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
+12
-18
@@ -367,13 +367,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 +386,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 +465,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 +479,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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+689
-411
File diff suppressed because it is too large
Load Diff
@@ -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 });
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import anotherjson from "another-json";
|
||||
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../@types/crypto";
|
||||
import { decodeBase64, encodeBase64 } from "./olmlib";
|
||||
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store";
|
||||
import { decryptAES, encryptAES } from "./aes";
|
||||
@@ -23,7 +24,6 @@ import { logger } from "../logger";
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
import { Crypto } from "./index";
|
||||
import { Method } from "../http-api";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
|
||||
export interface IDehydratedDevice {
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
@@ -38,20 +38,6 @@ export interface IDehydratedDeviceKeyInfo {
|
||||
passphrase?: string;
|
||||
}
|
||||
|
||||
export interface IDeviceKeys {
|
||||
algorithms: Array<string>;
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
user_id: string; // eslint-disable-line camelcase
|
||||
keys: Record<string, string>;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
export interface IOneTimeKey {
|
||||
key: string;
|
||||
fallback?: boolean;
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
||||
export const DEHYDRATION_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle";
|
||||
|
||||
const oneweek = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
+36
-28
@@ -20,7 +20,7 @@ limitations under the License.
|
||||
import anotherjson from "another-json";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
|
||||
import type { IDeviceKeys, IEventDecryptionResult, IMegolmSessionData, IOneTimeKey } from "../@types/crypto";
|
||||
import type { PkDecryption, PkSigning } from "@matrix-org/olm";
|
||||
import { EventType, ToDeviceMessageId } from "../@types/event";
|
||||
import { TypedReEmitter } from "../ReEmitter";
|
||||
@@ -63,7 +63,7 @@ import { ToDeviceChannel, ToDeviceRequests, Request } from "./verification/reque
|
||||
import { IllegalMethod } from "./verification/IllegalMethod";
|
||||
import { KeySignatureUploadError } from "../errors";
|
||||
import { calculateKeyCheck, decryptAES, encryptAES } from "./aes";
|
||||
import { DehydrationManager, IDeviceKeys, IOneTimeKey } from "./dehydration";
|
||||
import { DehydrationManager } from "./dehydration";
|
||||
import { BackupManager } from "./backup";
|
||||
import { IStore } from "../store";
|
||||
import { Room, RoomEvent } from "../models/room";
|
||||
@@ -90,6 +90,7 @@ 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";
|
||||
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
@@ -151,7 +152,7 @@ export interface ICryptoCallbacks {
|
||||
requestId: string,
|
||||
secretName: string,
|
||||
deviceTrust: DeviceTrustLevel,
|
||||
) => Promise<string>;
|
||||
) => Promise<string | undefined>;
|
||||
getDehydrationKey?: (keyInfo: ISecretStorageKeyInfo, checkFunc: (key: Uint8Array) => void) => Promise<Uint8Array>;
|
||||
getBackupKey?: () => Promise<Uint8Array>;
|
||||
}
|
||||
@@ -399,7 +400,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
|
||||
@@ -1202,6 +1206,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
*/
|
||||
public async storeSessionBackupPrivateKey(key: ArrayLike<number>): Promise<void> {
|
||||
if (!(key instanceof Uint8Array)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||
throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`);
|
||||
}
|
||||
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
|
||||
@@ -2689,11 +2694,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) {
|
||||
@@ -2707,7 +2714,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
continue;
|
||||
}
|
||||
|
||||
devicesByUser[userId].push(deviceInfo);
|
||||
userDevices.push(deviceInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2808,11 +2815,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
* @returns Promise which resolves when the event has been
|
||||
* encrypted, or null if nothing was needed
|
||||
*/
|
||||
public async encryptEvent(event: MatrixEvent, room?: Room): Promise<void> {
|
||||
if (!room) {
|
||||
throw new Error("Cannot send encrypted messages in unknown rooms");
|
||||
}
|
||||
|
||||
public async encryptEvent(event: MatrixEvent, room: Room): Promise<void> {
|
||||
const roomId = event.getRoomId()!;
|
||||
|
||||
const alg = this.roomEncryptors.get(roomId);
|
||||
@@ -3149,7 +3152,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,
|
||||
@@ -3433,6 +3440,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
const algorithm = content.algorithm;
|
||||
const deviceKey = content.sender_key;
|
||||
|
||||
this.baseApis.emit(ClientEvent.UndecryptableToDeviceEvent, event);
|
||||
|
||||
// retry decryption for all events sent by the sender_key. This will
|
||||
// update the events to show a message indicating that the olm session was
|
||||
// wedged.
|
||||
@@ -3449,8 +3458,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 " +
|
||||
@@ -3483,11 +3492,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)
|
||||
@@ -3514,11 +3522,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
|
||||
@@ -3825,15 +3832,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;
|
||||
}
|
||||
}
|
||||
|
||||
+39
-38
@@ -21,15 +21,16 @@ limitations under the License.
|
||||
import anotherjson from "another-json";
|
||||
|
||||
import type { PkSigning } from "@matrix-org/olm";
|
||||
import type { IOneTimeKey } from "../@types/crypto";
|
||||
import { OlmDevice } from "./OlmDevice";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { logger } from "../logger";
|
||||
import { IOneTimeKey } from "./dehydration";
|
||||
import { IClaimOTKsResult, MatrixClient } from "../client";
|
||||
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;
|
||||
},
|
||||
),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user