You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-26 17:03:12 +03:00
Merge branch 'develop' into kegan/sync-v3
This commit is contained in:
22
.eslintrc.js
22
.eslintrc.js
@@ -1,14 +1,22 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
"matrix-org",
|
"matrix-org",
|
||||||
|
"import",
|
||||||
],
|
],
|
||||||
extends: [
|
extends: [
|
||||||
"plugin:matrix-org/babel",
|
"plugin:matrix-org/babel",
|
||||||
|
"plugin:import/typescript",
|
||||||
],
|
],
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
node: true,
|
node: true,
|
||||||
},
|
},
|
||||||
|
settings: {
|
||||||
|
"import/resolver": {
|
||||||
|
typescript: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
// NOTE: These rules are frozen and new rules should not be added here.
|
// NOTE: These rules are frozen and new rules should not be added here.
|
||||||
// New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
|
// New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
|
||||||
rules: {
|
rules: {
|
||||||
@@ -35,7 +43,19 @@ module.exports = {
|
|||||||
"no-console": "error",
|
"no-console": "error",
|
||||||
|
|
||||||
// restrict EventEmitters to force callers to use TypedEventEmitter
|
// restrict EventEmitters to force callers to use TypedEventEmitter
|
||||||
"no-restricted-imports": ["error", "events"],
|
"no-restricted-imports": ["error", {
|
||||||
|
name: "events",
|
||||||
|
message: "Please use TypedEventEmitter instead"
|
||||||
|
}],
|
||||||
|
|
||||||
|
"import/no-restricted-paths": ["error", {
|
||||||
|
"zones": [{
|
||||||
|
"target": "./src/",
|
||||||
|
"from": "./src/index.ts",
|
||||||
|
"message": "The package index is dynamic between src and lib depending on " +
|
||||||
|
"whether release or development, target the specific module or matrix.ts instead",
|
||||||
|
}],
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
overrides: [{
|
overrides: [{
|
||||||
files: [
|
files: [
|
||||||
|
|||||||
2
.github/workflows/backport.yml
vendored
2
.github/workflows/backport.yml
vendored
@@ -25,6 +25,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: tibdex/backport@v2
|
- uses: tibdex/backport@v2
|
||||||
with:
|
with:
|
||||||
labels_template: "<%= JSON.stringify(labels) %>"
|
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
|
||||||
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
|
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
|
||||||
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|||||||
41
.github/workflows/release-npm.yml
vendored
Normal file
41
.github/workflows/release-npm.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Must only be called from `release#published` triggers
|
||||||
|
name: Publish to npm
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
secrets:
|
||||||
|
NPM_TOKEN:
|
||||||
|
required: true
|
||||||
|
jobs:
|
||||||
|
npm:
|
||||||
|
name: Publish to npm
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: 🧮 Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: 🔧 Yarn cache
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: "yarn"
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: 🔨 Install dependencies
|
||||||
|
run: "yarn install --pure-lockfile"
|
||||||
|
|
||||||
|
- name: 🚀 Publish to npm
|
||||||
|
id: npm-publish
|
||||||
|
uses: JS-DevTools/npm-publish@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.NPM_TOKEN }}
|
||||||
|
access: public
|
||||||
|
tag: next
|
||||||
|
|
||||||
|
- name: 🎖️ Add `latest` dist-tag to final releases
|
||||||
|
if: github.event.release.prerelease == false
|
||||||
|
run: |
|
||||||
|
package=$(cat package.json | jq -er .name)
|
||||||
|
npm dist-tag add "$package@$release" latest
|
||||||
|
env:
|
||||||
|
# JS-DevTools/npm-publish overrides `NODE_AUTH_TOKEN` with `INPUT_TOKEN` in .npmrc
|
||||||
|
INPUT_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
release: ${{ steps.npm-publish.outputs.version }}
|
||||||
@@ -24,7 +24,6 @@ jobs:
|
|||||||
|
|
||||||
- name: 📋 Copy to temp
|
- name: 📋 Copy to temp
|
||||||
run: |
|
run: |
|
||||||
ls -lah
|
|
||||||
tag="${{ github.ref_name }}"
|
tag="${{ github.ref_name }}"
|
||||||
version="${tag#v}"
|
version="${tag#v}"
|
||||||
echo "VERSION=$version" >> $GITHUB_ENV
|
echo "VERSION=$version" >> $GITHUB_ENV
|
||||||
@@ -51,3 +50,9 @@ jobs:
|
|||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
keep_files: true
|
keep_files: true
|
||||||
publish_dir: .
|
publish_dir: .
|
||||||
|
|
||||||
|
npm:
|
||||||
|
name: Publish
|
||||||
|
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
|
||||||
|
secrets:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
21
.github/workflows/sonarcloud.yml
vendored
21
.github/workflows/sonarcloud.yml
vendored
@@ -10,7 +10,18 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event.workflow_run.conclusion == 'success'
|
if: github.event.workflow_run.conclusion == 'success'
|
||||||
steps:
|
steps:
|
||||||
|
# We create the status here and then update it to success/failure in the `report` stage
|
||||||
|
# This provides an easy link to this workflow_run from the PR before Cypress is done.
|
||||||
|
- uses: Sibz/github-status-action@v1
|
||||||
|
with:
|
||||||
|
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
state: pending
|
||||||
|
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||||
|
sha: ${{ github.event.workflow_run.head_sha }}
|
||||||
|
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
|
||||||
- name: "🩻 SonarCloud Scan"
|
- name: "🩻 SonarCloud Scan"
|
||||||
|
id: sonarcloud
|
||||||
uses: matrix-org/sonarcloud-workflow-action@v2.2
|
uses: matrix-org/sonarcloud-workflow-action@v2.2
|
||||||
with:
|
with:
|
||||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||||
@@ -22,3 +33,13 @@ jobs:
|
|||||||
coverage_run_id: ${{ github.event.workflow_run.id }}
|
coverage_run_id: ${{ github.event.workflow_run.id }}
|
||||||
coverage_workflow_name: tests.yml
|
coverage_workflow_name: tests.yml
|
||||||
coverage_extract_path: coverage
|
coverage_extract_path: coverage
|
||||||
|
|
||||||
|
|
||||||
|
- uses: Sibz/github-status-action@v1
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
state: ${{ steps.sonarcloud.outcome == 'success' && 'success' || 'failure' }}
|
||||||
|
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||||
|
sha: ${{ github.event.workflow_run.head_sha }}
|
||||||
|
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
|||||||
14
.github/workflows/static_analysis.yml
vendored
14
.github/workflows/static_analysis.yml
vendored
@@ -23,6 +23,16 @@ jobs:
|
|||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
run: "yarn run lint:types"
|
run: "yarn run lint:types"
|
||||||
|
|
||||||
|
- name: Switch js-sdk to release mode
|
||||||
|
run: |
|
||||||
|
scripts/switch_package_to_release.js
|
||||||
|
yarn install
|
||||||
|
yarn run build:compile
|
||||||
|
yarn run build:types
|
||||||
|
|
||||||
|
- name: Typecheck (release mode)
|
||||||
|
run: "yarn run lint:types"
|
||||||
|
|
||||||
js_lint:
|
js_lint:
|
||||||
name: "ESLint"
|
name: "ESLint"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -73,7 +83,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Detecting files changed
|
- name: Detecting files changed
|
||||||
id: files
|
id: files
|
||||||
uses: futuratrepadeira/changed-files@v3.2.1
|
uses: futuratrepadeira/changed-files@v4.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
pattern: '^.*\.tsx?$'
|
pattern: '^.*\.tsx?$'
|
||||||
@@ -81,7 +91,7 @@ jobs:
|
|||||||
- uses: t3chguy/typescript-check-action@main
|
- uses: t3chguy/typescript-check-action@main
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
use-check: true
|
use-check: false
|
||||||
check-fail-mode: added
|
check-fail-mode: added
|
||||||
output-behaviour: annotate
|
output-behaviour: annotate
|
||||||
ts-extra-args: '--strict'
|
ts-extra-args: '--strict'
|
||||||
|
|||||||
86
CHANGELOG.md
86
CHANGELOG.md
@@ -1,3 +1,89 @@
|
|||||||
|
Changes in [20.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.1.0) (2022-10-11)
|
||||||
|
============================================================================================================
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
* Add local notification settings capability ([\#2700](https://github.com/matrix-org/matrix-js-sdk/pull/2700)).
|
||||||
|
* Implementation of MSC3882 login token request ([\#2687](https://github.com/matrix-org/matrix-js-sdk/pull/2687)). Contributed by @hughns.
|
||||||
|
* Typings for MSC2965 OIDC provider discovery ([\#2424](https://github.com/matrix-org/matrix-js-sdk/pull/2424)). Contributed by @hughns.
|
||||||
|
* Support to remotely toggle push notifications ([\#2686](https://github.com/matrix-org/matrix-js-sdk/pull/2686)).
|
||||||
|
* Read receipts for threads ([\#2635](https://github.com/matrix-org/matrix-js-sdk/pull/2635)).
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
* Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374.
|
||||||
|
* Unexpected ignored self key request when it's not shared history ([\#2724](https://github.com/matrix-org/matrix-js-sdk/pull/2724)). Contributed by @mcalinghee.
|
||||||
|
* Fix IDB initial migration handling causing spurious lazy loading upgrade loops ([\#2718](https://github.com/matrix-org/matrix-js-sdk/pull/2718)). Fixes vector-im/element-web#23377.
|
||||||
|
* Fix backpagination at end logic being spec non-conforming ([\#2680](https://github.com/matrix-org/matrix-js-sdk/pull/2680)). Fixes vector-im/element-web#22784.
|
||||||
|
|
||||||
|
Changes in [20.0.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.2) (2022-09-30)
|
||||||
|
==================================================================================================
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
* Fix issue in sync when crypto is not supported by client ([\#2715](https://github.com/matrix-org/matrix-js-sdk/pull/2715)). Contributed by @stas-demydiuk.
|
||||||
|
|
||||||
|
Changes in [20.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.1) (2022-09-28)
|
||||||
|
==================================================================================================
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
* Fix missing return when receiving an invitation without shared history ([\#2710](https://github.com/matrix-org/matrix-js-sdk/pull/2710)).
|
||||||
|
|
||||||
|
Changes in [20.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.0) (2022-09-28)
|
||||||
|
==================================================================================================
|
||||||
|
|
||||||
|
## 🚨 BREAKING CHANGES
|
||||||
|
* Bump IDB crypto store version ([\#2705](https://github.com/matrix-org/matrix-js-sdk/pull/2705)).
|
||||||
|
|
||||||
|
Changes in [19.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.7.0) (2022-09-28)
|
||||||
|
==================================================================================================
|
||||||
|
|
||||||
|
## 🔒 Security
|
||||||
|
* Fix for [CVE-2022-39249](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39249)
|
||||||
|
* Fix for [CVE-2022-39250](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39250)
|
||||||
|
* Fix for [CVE-2022-39251](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39251)
|
||||||
|
* Fix for [CVE-2022-39236](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39236)
|
||||||
|
|
||||||
|
Changes in [19.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.6.0) (2022-09-27)
|
||||||
|
==================================================================================================
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
* Add a property aggregating all names of a NamespacedValue ([\#2656](https://github.com/matrix-org/matrix-js-sdk/pull/2656)).
|
||||||
|
* Implementation of MSC3824 to add action= param on SSO login ([\#2398](https://github.com/matrix-org/matrix-js-sdk/pull/2398)). Contributed by @hughns.
|
||||||
|
* Add invited_count and joined_count to sliding sync room responses. ([\#2628](https://github.com/matrix-org/matrix-js-sdk/pull/2628)).
|
||||||
|
* Base support for MSC3847: Ignore invites with policy rooms ([\#2626](https://github.com/matrix-org/matrix-js-sdk/pull/2626)). Contributed by @Yoric.
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
* Fix handling of remote echoes doubling up ([\#2639](https://github.com/matrix-org/matrix-js-sdk/pull/2639)). Fixes #2618.
|
||||||
|
|
||||||
|
Changes in [19.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.5.0) (2022-09-13)
|
||||||
|
==================================================================================================
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
* Fix bug in deepCompare which would incorrectly return objects with disjoint keys as equal ([\#2586](https://github.com/matrix-org/matrix-js-sdk/pull/2586)). Contributed by @3nprob.
|
||||||
|
* Refactor Sync and fix `initialSyncLimit` ([\#2587](https://github.com/matrix-org/matrix-js-sdk/pull/2587)).
|
||||||
|
* Use deep equality comparisons when searching for outgoing key requests by target ([\#2623](https://github.com/matrix-org/matrix-js-sdk/pull/2623)). Contributed by @duxovni.
|
||||||
|
* Fix room membership race with PREPARED event ([\#2613](https://github.com/matrix-org/matrix-js-sdk/pull/2613)). Contributed by @jotto.
|
||||||
|
|
||||||
|
Changes in [19.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.4.0) (2022-08-31)
|
||||||
|
==================================================================================================
|
||||||
|
|
||||||
|
## 🔒 Security
|
||||||
|
* Fix for [CVE-2022-36059](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D36059)
|
||||||
|
|
||||||
|
Find more details at https://matrix.org/blog/2022/08/31/security-releases-matrix-js-sdk-19-4-0-and-matrix-react-sdk-3-53-0
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
* Re-emit room state events on rooms ([\#2607](https://github.com/matrix-org/matrix-js-sdk/pull/2607)).
|
||||||
|
* Add ability to override built in room name generator for an i18n'able one ([\#2609](https://github.com/matrix-org/matrix-js-sdk/pull/2609)).
|
||||||
|
* Add txn_id support to sliding sync ([\#2567](https://github.com/matrix-org/matrix-js-sdk/pull/2567)).
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
* Refactor Sync and fix `initialSyncLimit` ([\#2587](https://github.com/matrix-org/matrix-js-sdk/pull/2587)).
|
||||||
|
* Use deep equality comparisons when searching for outgoing key requests by target ([\#2623](https://github.com/matrix-org/matrix-js-sdk/pull/2623)). Contributed by @duxovni.
|
||||||
|
* Fix room membership race with PREPARED event ([\#2613](https://github.com/matrix-org/matrix-js-sdk/pull/2613)). Contributed by @jotto.
|
||||||
|
* fixed a sliding sync bug which could cause the `roomIndexToRoomId` map to be incorrect when a new room is added in the middle of the list or when an existing room is deleted from the middle of the list. ([\#2610](https://github.com/matrix-org/matrix-js-sdk/pull/2610)).
|
||||||
|
* Fix: Handle parsing of a beacon info event without asset ([\#2591](https://github.com/matrix-org/matrix-js-sdk/pull/2591)). Fixes vector-im/element-web#23078. Contributed by @kerryarchibald.
|
||||||
|
* Fix finding event read up to if stable private read receipts is missing ([\#2585](https://github.com/matrix-org/matrix-js-sdk/pull/2585)). Fixes vector-im/element-web#23027.
|
||||||
|
* fixed a sliding sync issue where history could be interpreted as live events. ([\#2583](https://github.com/matrix-org/matrix-js-sdk/pull/2583)).
|
||||||
|
|
||||||
Changes in [19.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.3.0) (2022-08-16)
|
Changes in [19.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.3.0) (2022-08-16)
|
||||||
==================================================================================================
|
==================================================================================================
|
||||||
|
|
||||||
|
|||||||
281
CONTRIBUTING.md
281
CONTRIBUTING.md
@@ -1,284 +1,5 @@
|
|||||||
Contributing code to matrix-js-sdk
|
Contributing code to matrix-js-sdk
|
||||||
==================================
|
==================================
|
||||||
|
|
||||||
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
|
matrix-js-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md
|
||||||
willing to license their contributions under the same license as the project
|
|
||||||
itself. We follow a simple 'inbound=outbound' model for contributions: the act
|
|
||||||
of submitting an 'inbound' contribution means that the contributor agrees to
|
|
||||||
license the code under the same terms as the project's overall 'outbound'
|
|
||||||
license - in this case, Apache Software License v2 (see
|
|
||||||
[LICENSE](LICENSE)).
|
|
||||||
|
|
||||||
How to contribute
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
The preferred and easiest way to contribute changes to the project is to fork
|
|
||||||
it on github, and then create a pull request to ask us to pull your changes
|
|
||||||
into our repo (https://help.github.com/articles/using-pull-requests/)
|
|
||||||
|
|
||||||
We use GitHub's pull request workflow to review the contribution, and either
|
|
||||||
ask you to make any refinements needed or merge it and make them ourselves.
|
|
||||||
|
|
||||||
Things that should go into your PR description:
|
|
||||||
* A changelog entry in the `Notes` section (see below)
|
|
||||||
* References to any bugs fixed by the change (in GitHub's `Fixes` notation)
|
|
||||||
* Describe the why and what is changing in the PR description so it's easy for
|
|
||||||
onlookers and reviewers to onboard and context switch. This information is
|
|
||||||
also helpful when we come back to look at this in 6 months and ask "why did
|
|
||||||
we do it like that?" we have a chance of finding out.
|
|
||||||
* Why didn't it work before? Why does it work now? What use cases does it
|
|
||||||
unlock?
|
|
||||||
* If you find yourself adding information on how the code works or why you
|
|
||||||
chose to do it the way you did, make sure this information is instead
|
|
||||||
written as comments in the code itself.
|
|
||||||
* Sometimes a PR can change considerably as it is developed. In this case,
|
|
||||||
the description should be updated to reflect the most recent state of
|
|
||||||
the PR. (It can be helpful to retain the old content under a suitable
|
|
||||||
heading, for additional context.)
|
|
||||||
* Include both **before** and **after** screenshots to easily compare and discuss
|
|
||||||
what's changing.
|
|
||||||
* Include a step-by-step testing strategy so that a reviewer can check out the
|
|
||||||
code locally and easily get to the point of testing your change.
|
|
||||||
* Add comments to the diff for the reviewer that might help them to understand
|
|
||||||
why the change is necessary or how they might better understand and review it.
|
|
||||||
|
|
||||||
We rely on information in pull request to populate the information that goes
|
|
||||||
into the changelogs our users see, both for the JS SDK itself and also for some
|
|
||||||
projects based on it. This is picked up from both labels on the pull request and
|
|
||||||
the `Notes:` annotation in the description. By default, the PR title will be
|
|
||||||
used for the changelog entry, but you can specify more options, as follows.
|
|
||||||
|
|
||||||
To add a longer, more detailed description of the change for the changelog:
|
|
||||||
|
|
||||||
|
|
||||||
*Fix llama herding bug*
|
|
||||||
|
|
||||||
```
|
|
||||||
Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase
|
|
||||||
```
|
|
||||||
|
|
||||||
For some PRs, it's not useful to have an entry in the user-facing changelog (this is
|
|
||||||
the default for PRs labelled with `T-Task`):
|
|
||||||
|
|
||||||
*Remove outdated comment from `Ungulates.ts`*
|
|
||||||
```
|
|
||||||
Notes: none
|
|
||||||
```
|
|
||||||
|
|
||||||
Sometimes, you're fixing a bug in a downstream project, in which case you want
|
|
||||||
an entry in that project's changelog. You can do that too:
|
|
||||||
|
|
||||||
*Fix another herding bug*
|
|
||||||
```
|
|
||||||
Notes: Fix a bug where the `herd()` function would only work on Tuesdays
|
|
||||||
element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays
|
|
||||||
```
|
|
||||||
|
|
||||||
This example is for Element Web. You can specify:
|
|
||||||
* matrix-react-sdk
|
|
||||||
* element-web
|
|
||||||
* element-desktop
|
|
||||||
|
|
||||||
If your PR introduces a breaking change, use the `Notes` section in the same
|
|
||||||
way, additionally adding the `X-Breaking-Change` label (see below). There's no need
|
|
||||||
to specify in the notes that it's a breaking change - this will be added
|
|
||||||
automatically based on the label - but remember to tell the developer how to
|
|
||||||
migrate:
|
|
||||||
|
|
||||||
*Remove legacy class*
|
|
||||||
|
|
||||||
```
|
|
||||||
Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead.
|
|
||||||
```
|
|
||||||
|
|
||||||
Other metadata can be added using labels.
|
|
||||||
* `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a *major* version bump.
|
|
||||||
* `T-Enhancement`: A new feature - adding this label will mean the change causes a *minor* version bump.
|
|
||||||
* `T-Defect`: A bug fix (in either code or docs).
|
|
||||||
* `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one.
|
|
||||||
|
|
||||||
If you don't have permission to add labels, your PR reviewer(s) can work with you
|
|
||||||
to add them: ask in the PR description or comments.
|
|
||||||
|
|
||||||
We use continuous integration, and all pull requests get automatically tested:
|
|
||||||
if your change breaks the build, then the PR will show that there are failed
|
|
||||||
checks, so please check back after a few minutes.
|
|
||||||
|
|
||||||
Tests
|
|
||||||
-----
|
|
||||||
Your PR should include tests.
|
|
||||||
|
|
||||||
For new user facing features in `matrix-react-sdk` or `element-web`, you
|
|
||||||
must include:
|
|
||||||
|
|
||||||
1. Comprehensive unit tests written in Jest. These are located in `/test`.
|
|
||||||
2. "happy path" end-to-end tests.
|
|
||||||
These are located in `/test/end-to-end-tests` in `matrix-react-sdk`, and
|
|
||||||
are run using `element-web`. Ideally, you would also include tests for edge
|
|
||||||
and error cases.
|
|
||||||
|
|
||||||
Unit tests are expected even when the feature is in labs. It's good practice
|
|
||||||
to write tests alongside the code as it ensures the code is testable from
|
|
||||||
the start, and gives you a fast feedback loop while you're developing the
|
|
||||||
functionality. End-to-end tests should be added prior to the feature
|
|
||||||
leaving labs, but don't have to be present from the start (although it might
|
|
||||||
be beneficial to have some running early, so you can test things faster).
|
|
||||||
|
|
||||||
For bugs in those repos, your change must include at least one unit test or
|
|
||||||
end-to-end test; which is best depends on what sort of test most concisely
|
|
||||||
exercises the area.
|
|
||||||
|
|
||||||
Changes to `matrix-js-sdk` must be accompanied by unit tests written in Jest.
|
|
||||||
These are located in `/spec/`.
|
|
||||||
|
|
||||||
When writing unit tests, please aim for a high level of test coverage
|
|
||||||
for new code - 80% or greater. If you cannot achieve that, please document
|
|
||||||
why it's not possible in your PR.
|
|
||||||
|
|
||||||
Some sections of code are not sensible to add coverage for, such as those
|
|
||||||
which explicitly inhibit noisy logging for tests. Which can be hidden using
|
|
||||||
an istanbul magic comment as [documented here][1]. See example:
|
|
||||||
```javascript
|
|
||||||
/* istanbul ignore if */
|
|
||||||
if (process.env.NODE_ENV !== "test") {
|
|
||||||
logger.error("Log line that is noisy enough in tests to want to skip");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Tests validate that your change works as intended and also document
|
|
||||||
concisely what is being changed. Ideally, your new tests fail
|
|
||||||
prior to your change, and succeed once it has been applied. You may
|
|
||||||
find this simpler to achieve if you write the tests first.
|
|
||||||
|
|
||||||
If you're spiking some code that's experimental and not being used to support
|
|
||||||
production features, exceptions can be made to requirements for tests.
|
|
||||||
Note that tests will still be required in order to ship the feature, and it's
|
|
||||||
strongly encouraged to think about tests early in the process, as adding
|
|
||||||
tests later will become progressively more difficult.
|
|
||||||
|
|
||||||
If you're not sure how to approach writing tests for your change, ask for help
|
|
||||||
in [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
|
|
||||||
|
|
||||||
Code style
|
|
||||||
----------
|
|
||||||
The js-sdk aims to target TypeScript/ES6. All new files should be written in
|
|
||||||
TypeScript and existing files should use ES6 principles where possible.
|
|
||||||
|
|
||||||
Members should not be exported as a default export in general - it causes problems
|
|
||||||
with the architecture of the SDK (index file becomes less clear) and could
|
|
||||||
introduce naming problems (as default exports get aliased upon import). In
|
|
||||||
general, avoid using `export default`.
|
|
||||||
|
|
||||||
The remaining code-style for matrix-js-sdk is not formally documented, but
|
|
||||||
contributors are encouraged to read the
|
|
||||||
[code style document for matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md)
|
|
||||||
and follow the principles set out there.
|
|
||||||
|
|
||||||
Please ensure your changes match the cosmetic style of the existing project,
|
|
||||||
and ***never*** mix cosmetic and functional changes in the same commit, as it
|
|
||||||
makes it horribly hard to review otherwise.
|
|
||||||
|
|
||||||
Attribution
|
|
||||||
-----------
|
|
||||||
Everyone who contributes anything to Matrix is welcome to be listed in the
|
|
||||||
AUTHORS.rst file for the project in question. Please feel free to include a
|
|
||||||
change to AUTHORS.rst in your pull request to list yourself and a short
|
|
||||||
description of the area(s) you've worked on. Also, we sometimes have swag to
|
|
||||||
give away to contributors - if you feel that Matrix-branded apparel is missing
|
|
||||||
from your life, please mail us your shipping address to matrix at matrix.org
|
|
||||||
and we'll try to fix it :)
|
|
||||||
|
|
||||||
Sign off
|
|
||||||
--------
|
|
||||||
In order to have a concrete record that your contribution is intentional
|
|
||||||
and you agree to license it under the same terms as the project's license, we've
|
|
||||||
adopted the same lightweight approach that the Linux Kernel
|
|
||||||
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
|
|
||||||
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
|
||||||
projects use: the DCO (Developer Certificate of Origin:
|
|
||||||
http://developercertificate.org/). This is a simple declaration that you wrote
|
|
||||||
the contribution or otherwise have the right to contribute it to Matrix:
|
|
||||||
|
|
||||||
```
|
|
||||||
Developer Certificate of Origin
|
|
||||||
Version 1.1
|
|
||||||
|
|
||||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
|
||||||
660 York Street, Suite 102,
|
|
||||||
San Francisco, CA 94110 USA
|
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies of this
|
|
||||||
license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Developer's Certificate of Origin 1.1
|
|
||||||
|
|
||||||
By making a contribution to this project, I certify that:
|
|
||||||
|
|
||||||
(a) The contribution was created in whole or in part by me and I
|
|
||||||
have the right to submit it under the open source license
|
|
||||||
indicated in the file; or
|
|
||||||
|
|
||||||
(b) The contribution is based upon previous work that, to the best
|
|
||||||
of my knowledge, is covered under an appropriate open source
|
|
||||||
license and I have the right under that license to submit that
|
|
||||||
work with modifications, whether created in whole or in part
|
|
||||||
by me, under the same open source license (unless I am
|
|
||||||
permitted to submit under a different license), as indicated
|
|
||||||
in the file; or
|
|
||||||
|
|
||||||
(c) The contribution was provided directly to me by some other
|
|
||||||
person who certified (a), (b) or (c) and I have not modified
|
|
||||||
it.
|
|
||||||
|
|
||||||
(d) I understand and agree that this project and the contribution
|
|
||||||
are public and that a record of the contribution (including all
|
|
||||||
personal information I submit with it, including my sign-off) is
|
|
||||||
maintained indefinitely and may be redistributed consistent with
|
|
||||||
this project or the open source license(s) involved.
|
|
||||||
```
|
|
||||||
|
|
||||||
If you agree to this for your contribution, then all that's needed is to
|
|
||||||
include the line in your commit or pull request comment:
|
|
||||||
|
|
||||||
```
|
|
||||||
Signed-off-by: Your Name <your@email.example.org>
|
|
||||||
```
|
|
||||||
|
|
||||||
We accept contributions under a legally identifiable name, such as your name on
|
|
||||||
government documentation or common-law names (names claimed by legitimate usage
|
|
||||||
or repute). Unfortunately, we cannot accept anonymous contributions at this
|
|
||||||
time.
|
|
||||||
|
|
||||||
Git allows you to add this signoff automatically when using the `-s` flag to
|
|
||||||
`git commit`, which uses the name and email set in your `user.name` and
|
|
||||||
`user.email` git configs.
|
|
||||||
|
|
||||||
If you forgot to sign off your commits before making your pull request and are
|
|
||||||
on Git 2.17+ you can mass signoff using rebase:
|
|
||||||
|
|
||||||
```
|
|
||||||
git rebase --signoff origin/develop
|
|
||||||
```
|
|
||||||
|
|
||||||
Review expectations
|
|
||||||
===================
|
|
||||||
|
|
||||||
See https://github.com/vector-im/element-meta/wiki/Review-process
|
|
||||||
|
|
||||||
|
|
||||||
Merge Strategy
|
|
||||||
==============
|
|
||||||
|
|
||||||
The preferred method for merging pull requests is squash merging to keep the
|
|
||||||
commit history trim, but it is up to the discretion of the team member merging
|
|
||||||
the change. We do not support rebase merges due to `allchange` being unable to
|
|
||||||
handle them. When merging make sure to leave the default commit title, or
|
|
||||||
at least leave the PR number at the end in brackets like by default.
|
|
||||||
When stacking pull requests, you may wish to do the following:
|
|
||||||
|
|
||||||
1. Branch from develop to your branch (branch1), push commits onto it and open a pull request
|
|
||||||
2. Branch from your base branch (branch1) to your work branch (branch2), push commits and open a pull request configuring the base to be branch1, saying in the description that it is based on your other PR.
|
|
||||||
3. Merge the first PR using a merge commit otherwise your stacked PR will need a rebase. Github will automatically adjust the base branch of your other PR to be develop.
|
|
||||||
|
|
||||||
|
|
||||||
[1]: https://github.com/gotwarlost/istanbul/blob/master/ignoring-code-for-coverage.md
|
|
||||||
|
|||||||
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "matrix-js-sdk",
|
"name": "matrix-js-sdk",
|
||||||
"version": "19.3.0",
|
"version": "20.1.0",
|
||||||
"description": "Matrix Client-Server SDK for Javascript",
|
"description": "Matrix Client-Server SDK for Javascript",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.9.0"
|
"node": ">=12.9.0"
|
||||||
@@ -78,28 +78,30 @@
|
|||||||
"@babel/preset-env": "^7.12.11",
|
"@babel/preset-env": "^7.12.11",
|
||||||
"@babel/preset-typescript": "^7.12.7",
|
"@babel/preset-typescript": "^7.12.7",
|
||||||
"@babel/register": "^7.12.10",
|
"@babel/register": "^7.12.10",
|
||||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz",
|
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz",
|
||||||
"@types/bs58": "^4.0.1",
|
"@types/bs58": "^4.0.1",
|
||||||
"@types/content-type": "^1.1.5",
|
"@types/content-type": "^1.1.5",
|
||||||
"@types/jest": "^28.0.0",
|
"@types/jest": "^29.0.0",
|
||||||
"@types/node": "16",
|
"@types/node": "16",
|
||||||
"@types/request": "^2.48.5",
|
"@types/request": "^2.48.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||||
"@typescript-eslint/parser": "^5.6.0",
|
"@typescript-eslint/parser": "^5.6.0",
|
||||||
"allchange": "^1.0.6",
|
"allchange": "^1.0.6",
|
||||||
"babel-jest": "^28.0.0",
|
"babel-jest": "^29.0.0",
|
||||||
"babelify": "^10.0.0",
|
"babelify": "^10.0.0",
|
||||||
"better-docs": "^2.4.0-beta.9",
|
"better-docs": "^2.4.0-beta.9",
|
||||||
"browserify": "^17.0.0",
|
"browserify": "^17.0.0",
|
||||||
"docdash": "^1.2.0",
|
"docdash": "^1.2.0",
|
||||||
"eslint": "8.22.0",
|
"eslint": "8.24.0",
|
||||||
"eslint-config-google": "^0.14.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
"eslint-plugin-import": "^2.25.4",
|
"eslint-import-resolver-typescript": "^3.5.1",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-matrix-org": "^0.6.0",
|
"eslint-plugin-matrix-org": "^0.6.0",
|
||||||
"exorcist": "^2.0.0",
|
"exorcist": "^2.0.0",
|
||||||
"fake-indexeddb": "^4.0.0",
|
"fake-indexeddb": "^4.0.0",
|
||||||
"jest": "^28.0.0",
|
"jest": "^29.0.0",
|
||||||
"jest-localstorage-mock": "^2.4.6",
|
"jest-localstorage-mock": "^2.4.6",
|
||||||
|
"jest-mock": "^27.5.1",
|
||||||
"jest-sonar-reporter": "^2.0.0",
|
"jest-sonar-reporter": "^2.0.0",
|
||||||
"jsdoc": "^3.6.6",
|
"jsdoc": "^3.6.6",
|
||||||
"matrix-mock-request": "^2.1.2",
|
"matrix-mock-request": "^2.1.2",
|
||||||
|
|||||||
37
post-release.sh
Executable file
37
post-release.sh
Executable file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Script to perform a post-release steps of matrix-js-sdk.
|
||||||
|
#
|
||||||
|
# Requires:
|
||||||
|
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
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
|
||||||
|
do
|
||||||
|
# If a `lib` prefixed value is present, it means we adjusted the field
|
||||||
|
# earlier at publish time, so we should revert it now.
|
||||||
|
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
|
||||||
|
# If there's a `src` prefixed value, use that, otherwise delete.
|
||||||
|
# This is used to delete the `typings` field and reset `main` back
|
||||||
|
# to the TypeScript source.
|
||||||
|
src_value=$(jq -r ".matrix_src_$i" package.json)
|
||||||
|
if [ "$src_value" != "null" ]; then
|
||||||
|
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
|
||||||
|
else
|
||||||
|
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$(git ls-files --modified package.json)" ]; then
|
||||||
|
echo "Committing develop package.json"
|
||||||
|
git commit package.json -m "Resetting package fields for development"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git push origin develop
|
||||||
|
fi
|
||||||
173
release.sh
173
release.sh
@@ -3,19 +3,16 @@
|
|||||||
# Script to perform a release of matrix-js-sdk and downstream projects.
|
# Script to perform a release of matrix-js-sdk and downstream projects.
|
||||||
#
|
#
|
||||||
# Requires:
|
# Requires:
|
||||||
# github-changelog-generator; install via:
|
|
||||||
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
|
|
||||||
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
|
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
|
||||||
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
|
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
|
||||||
# npm; typically installed by Node.js
|
|
||||||
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
|
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
|
||||||
#
|
#
|
||||||
# Note: this script is also used to release matrix-react-sdk and element-web.
|
# Note: this script is also used to release matrix-react-sdk, element-web, and element-desktop.
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
||||||
if [[ `command -v hub` ]] && [[ `hub --version` =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
|
if [[ $(command -v hub) ]] && [[ $(hub --version) =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
|
||||||
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
|
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
|
||||||
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
|
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
|
||||||
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
|
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
|
||||||
@@ -26,7 +23,6 @@ else
|
|||||||
echo "hub is required: please install it"
|
echo "hub is required: please install it"
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
npm --version > /dev/null || (echo "npm is required: please install it"; kill $$)
|
|
||||||
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
|
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
|
||||||
|
|
||||||
USAGE="$0 [-x] [-c changelog_file] vX.Y.Z"
|
USAGE="$0 [-x] [-c changelog_file] vX.Y.Z"
|
||||||
@@ -37,17 +33,9 @@ $USAGE
|
|||||||
|
|
||||||
-c changelog_file: specify name of file containing changelog
|
-c changelog_file: specify name of file containing changelog
|
||||||
-x: skip updating the changelog
|
-x: skip updating the changelog
|
||||||
-n: skip publish to NPM
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
ret=0
|
|
||||||
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
|
|
||||||
if [ "$ret" -eq 0 ]; then
|
|
||||||
echo "package.json contains develop dependencies. Refusing to release."
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! git diff-index --quiet --cached HEAD; then
|
if ! git diff-index --quiet --cached HEAD; then
|
||||||
echo "this git checkout has staged (uncommitted) changes. Refusing to release."
|
echo "this git checkout has staged (uncommitted) changes. Refusing to release."
|
||||||
exit
|
exit
|
||||||
@@ -59,10 +47,8 @@ if ! git diff-files --quiet; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
skip_changelog=
|
skip_changelog=
|
||||||
skip_npm=
|
|
||||||
changelog_file="CHANGELOG.md"
|
changelog_file="CHANGELOG.md"
|
||||||
expected_npm_user="matrixdotorg"
|
while getopts hc:x f; do
|
||||||
while getopts hc:u:xzn f; do
|
|
||||||
case $f in
|
case $f in
|
||||||
h)
|
h)
|
||||||
help
|
help
|
||||||
@@ -74,21 +60,70 @@ while getopts hc:u:xzn f; do
|
|||||||
x)
|
x)
|
||||||
skip_changelog=1
|
skip_changelog=1
|
||||||
;;
|
;;
|
||||||
n)
|
|
||||||
skip_npm=1
|
|
||||||
;;
|
|
||||||
u)
|
|
||||||
expected_npm_user="$OPTARG"
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
shift `expr $OPTIND - 1`
|
shift $(expr $OPTIND - 1)
|
||||||
|
|
||||||
if [ $# -ne 1 ]; then
|
if [ $# -ne 1 ]; then
|
||||||
echo "Usage: $USAGE" >&2
|
echo "Usage: $USAGE" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
function check_dependency {
|
||||||
|
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
|
||||||
|
if [ "$depver" == "null" ]; then return 0; fi
|
||||||
|
|
||||||
|
echo "Checking version of $1..."
|
||||||
|
local latestver=$(yarn info -s "$1" dist-tags.next)
|
||||||
|
if [ "$depver" != "$latestver" ]
|
||||||
|
then
|
||||||
|
echo "The latest version of $1 is $latestver but package.json depends on $depver."
|
||||||
|
echo -n "Type 'u' to auto-upgrade, 'c' to continue anyway, or 'a' to abort:"
|
||||||
|
read resp
|
||||||
|
if [ "$resp" != "u" ] && [ "$resp" != "c" ]
|
||||||
|
then
|
||||||
|
echo "Aborting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$resp" == "u" ]
|
||||||
|
then
|
||||||
|
echo "Upgrading $1 to $latestver..."
|
||||||
|
yarn add -E "$1@$latestver"
|
||||||
|
git add -u
|
||||||
|
git commit -m "Upgrade $1 to $latestver"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset_dependency {
|
||||||
|
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
|
||||||
|
if [ "$depver" == "null" ]; then return 0; fi
|
||||||
|
|
||||||
|
echo "Resetting $1 to develop branch..."
|
||||||
|
yarn add "github:matrix-org/$1#develop"
|
||||||
|
git add -u
|
||||||
|
git commit -m "Reset $1 back to develop branch"
|
||||||
|
}
|
||||||
|
|
||||||
|
has_subprojects=0
|
||||||
|
if [ -f release_config.yaml ]; then
|
||||||
|
subprojects=$(cat release_config.yaml | python -c "import yaml; import sys; print(' '.join(list(yaml.load(sys.stdin)['subprojects'].keys())))" 2> /dev/null)
|
||||||
|
if [ "$?" -eq 0 ]; then
|
||||||
|
has_subprojects=1
|
||||||
|
echo "Checking subprojects for upgrades"
|
||||||
|
for proj in $subprojects; do
|
||||||
|
check_dependency "$proj"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
ret=0
|
||||||
|
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
|
||||||
|
if [ "$ret" -eq 0 ]; then
|
||||||
|
echo "package.json contains develop dependencies. Refusing to release."
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
# We use Git branch / commit dependencies for some packages, and Yarn seems
|
# We use Git branch / commit dependencies for some packages, and Yarn seems
|
||||||
# to have a hard time getting that right. See also
|
# to have a hard time getting that right. See also
|
||||||
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
|
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
|
||||||
@@ -97,20 +132,9 @@ yarn cache clean
|
|||||||
# Ensure all dependencies are updated
|
# Ensure all dependencies are updated
|
||||||
yarn install --ignore-scripts --pure-lockfile
|
yarn install --ignore-scripts --pure-lockfile
|
||||||
|
|
||||||
# Login and publish continues to use `npm`, as it seems to have more clearly
|
|
||||||
# defined options and semantics than `yarn` for writing to the registry.
|
|
||||||
if [ -z "$skip_npm" ]; then
|
|
||||||
actual_npm_user=`npm whoami`;
|
|
||||||
if [ $expected_npm_user != $actual_npm_user ]; then
|
|
||||||
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ignore leading v on release
|
# ignore leading v on release
|
||||||
release="${1#v}"
|
release="${1#v}"
|
||||||
tag="v${release}"
|
tag="v${release}"
|
||||||
rel_branch="release-$tag"
|
|
||||||
|
|
||||||
prerelease=0
|
prerelease=0
|
||||||
# We check if this build is a prerelease by looking to
|
# We check if this build is a prerelease by looking to
|
||||||
@@ -125,18 +149,7 @@ else
|
|||||||
read -p "Making a FINAL RELEASE, press enter to continue " REPLY
|
read -p "Making a FINAL RELEASE, press enter to continue " REPLY
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# We might already be on the release branch, in which case, yay
|
rel_branch=$(git symbolic-ref --short HEAD)
|
||||||
# If we're on any branch starting with 'release', or the staging branch
|
|
||||||
# we don't create a separate release branch (this allows us to use the same
|
|
||||||
# release branch for releases and release candidates).
|
|
||||||
curbranch=$(git symbolic-ref --short HEAD)
|
|
||||||
if [[ "$curbranch" != release* && "$curbranch" != "staging" ]]; then
|
|
||||||
echo "Creating release branch"
|
|
||||||
git checkout -b "$rel_branch"
|
|
||||||
else
|
|
||||||
echo "Using current branch ($curbranch) for release"
|
|
||||||
rel_branch=$curbranch
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$skip_changelog" ]; then
|
if [ -z "$skip_changelog" ]; then
|
||||||
echo "Generating changelog"
|
echo "Generating changelog"
|
||||||
@@ -148,8 +161,8 @@ if [ -z "$skip_changelog" ]; then
|
|||||||
git commit "$changelog_file" -m "Prepare changelog for $tag"
|
git commit "$changelog_file" -m "Prepare changelog for $tag"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
latest_changes=`mktemp`
|
latest_changes=$(mktemp)
|
||||||
cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_changes}"
|
cat "${changelog_file}" | "$(dirname "$0")/scripts/changelog_head.py" > "${latest_changes}"
|
||||||
|
|
||||||
set -x
|
set -x
|
||||||
|
|
||||||
@@ -176,7 +189,7 @@ do
|
|||||||
done
|
done
|
||||||
|
|
||||||
# commit yarn.lock if it exists, is versioned, and is modified
|
# commit yarn.lock if it exists, is versioned, and is modified
|
||||||
if [[ -f yarn.lock && `git status --porcelain yarn.lock | grep '^ M'` ]];
|
if [[ -f yarn.lock && $(git status --porcelain yarn.lock | grep '^ M') ]];
|
||||||
then
|
then
|
||||||
pkglock='yarn.lock'
|
pkglock='yarn.lock'
|
||||||
else
|
else
|
||||||
@@ -188,7 +201,7 @@ git commit package.json $pkglock -m "$tag"
|
|||||||
# figure out if we should be signing this release
|
# figure out if we should be signing this release
|
||||||
signing_id=
|
signing_id=
|
||||||
if [ -f release_config.yaml ]; then
|
if [ -f release_config.yaml ]; then
|
||||||
result=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']" 2> /dev/null || true`
|
result=$(cat release_config.yaml | python -c "import yaml; import sys; print(yaml.load(sys.stdin)['signing_id'])" 2> /dev/null || true)
|
||||||
if [ "$?" -eq 0 ]; then
|
if [ "$?" -eq 0 ]; then
|
||||||
signing_id=$result
|
signing_id=$result
|
||||||
fi
|
fi
|
||||||
@@ -206,8 +219,8 @@ assets=''
|
|||||||
dodist=0
|
dodist=0
|
||||||
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
|
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
|
||||||
if [ $dodist -eq 0 ]; then
|
if [ $dodist -eq 0 ]; then
|
||||||
projdir=`pwd`
|
projdir=$(pwd)
|
||||||
builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'`
|
builddir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
|
||||||
echo "Building distribution copy in $builddir"
|
echo "Building distribution copy in $builddir"
|
||||||
pushd "$builddir"
|
pushd "$builddir"
|
||||||
git clone "$projdir" .
|
git clone "$projdir" .
|
||||||
@@ -232,7 +245,7 @@ fi
|
|||||||
if [ -n "$signing_id" ]; then
|
if [ -n "$signing_id" ]; then
|
||||||
# make a signed tag
|
# make a signed tag
|
||||||
# gnupg seems to fail to get the right tty device unless we set it here
|
# gnupg seems to fail to get the right tty device unless we set it here
|
||||||
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
|
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=$(tty) git tag -u "$signing_id" -F "${latest_changes}" "$tag"
|
||||||
else
|
else
|
||||||
git tag -a -F "${latest_changes}" "$tag"
|
git tag -a -F "${latest_changes}" "$tag"
|
||||||
fi
|
fi
|
||||||
@@ -298,7 +311,7 @@ if [ $prerelease -eq 1 ]; then
|
|||||||
hubflags='-p'
|
hubflags='-p'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
release_text=`mktemp`
|
release_text=$(mktemp)
|
||||||
echo "$tag" > "${release_text}"
|
echo "$tag" > "${release_text}"
|
||||||
echo >> "${release_text}"
|
echo >> "${release_text}"
|
||||||
cat "${latest_changes}" >> "${release_text}"
|
cat "${latest_changes}" >> "${release_text}"
|
||||||
@@ -310,19 +323,6 @@ fi
|
|||||||
rm "${release_text}"
|
rm "${release_text}"
|
||||||
rm "${latest_changes}"
|
rm "${latest_changes}"
|
||||||
|
|
||||||
# Login and publish continues to use `npm`, as it seems to have more clearly
|
|
||||||
# defined options and semantics than `yarn` for writing to the registry.
|
|
||||||
# Tag both releases and prereleases as `next` so the last stable release remains
|
|
||||||
# the default.
|
|
||||||
if [ -z "$skip_npm" ]; then
|
|
||||||
npm publish --tag next
|
|
||||||
if [ $prerelease -eq 0 ]; then
|
|
||||||
# For a release, also add the default `latest` tag.
|
|
||||||
package=$(cat package.json | jq -er .name)
|
|
||||||
npm dist-tag add "$package@$release" latest
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# if it is a pre-release, leave it on the release branch for now.
|
# if it is a pre-release, leave it on the release branch for now.
|
||||||
if [ $prerelease -eq 1 ]; then
|
if [ $prerelease -eq 1 ]; then
|
||||||
git checkout "$rel_branch"
|
git checkout "$rel_branch"
|
||||||
@@ -339,34 +339,19 @@ git merge "$rel_branch" --no-edit
|
|||||||
git push origin master
|
git push origin master
|
||||||
|
|
||||||
# finally, merge master back onto develop (if it exists)
|
# finally, merge master back onto develop (if it exists)
|
||||||
if [ $(git branch -lr | grep origin/develop -c) -ge 1 ]; then
|
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
|
||||||
git checkout develop
|
git checkout develop
|
||||||
git pull
|
git pull
|
||||||
git merge master --no-edit
|
git merge master --no-edit
|
||||||
|
git push origin develop
|
||||||
# When merging to develop, we need revert the `main` and `typings` fields if
|
fi
|
||||||
# we adjusted them previously.
|
|
||||||
for i in main typings
|
[ -x ./post-release.sh ] && ./post-release.sh
|
||||||
do
|
|
||||||
# If a `lib` prefixed value is present, it means we adjusted the field
|
if [ $has_subprojects -eq 1 ] && [ $prerelease -eq 0 ]; then
|
||||||
# earlier at publish time, so we should revert it now.
|
echo "Resetting subprojects to develop"
|
||||||
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
|
for proj in $subprojects; do
|
||||||
# If there's a `src` prefixed value, use that, otherwise delete.
|
reset_dependency "$proj"
|
||||||
# This is used to delete the `typings` field and reset `main` back
|
done
|
||||||
# to the TypeScript source.
|
|
||||||
src_value=$(jq -r ".matrix_src_$i" package.json)
|
|
||||||
if [ "$src_value" != "null" ]; then
|
|
||||||
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
|
|
||||||
else
|
|
||||||
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -n "$(git ls-files --modified package.json)" ]; then
|
|
||||||
echo "Committing develop package.json"
|
|
||||||
git commit package.json -m "Resetting package fields for development"
|
|
||||||
fi
|
|
||||||
|
|
||||||
git push origin develop
|
git push origin develop
|
||||||
fi
|
fi
|
||||||
|
|||||||
17
scripts/switch_package_to_release.js
Executable file
17
scripts/switch_package_to_release.js
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const fsProm = require('fs/promises');
|
||||||
|
|
||||||
|
const PKGJSON = 'package.json';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, 'utf8'));
|
||||||
|
for (const field of ['main', 'typings']) {
|
||||||
|
if (pkgJson["matrix_lib_"+field] !== undefined) {
|
||||||
|
pkgJson[field] = pkgJson["matrix_lib_"+field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019, 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -17,31 +17,32 @@ limitations under the License.
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A mock implementation of the webstorage api
|
* A mock implementation of the webstorage api
|
||||||
* @constructor
|
|
||||||
*/
|
*/
|
||||||
export function MockStorageApi() {
|
export class MockStorageApi {
|
||||||
this.data = {};
|
public data: Record<string, string> = {};
|
||||||
this.keys = [];
|
public keys: string[] = [];
|
||||||
this.length = 0;
|
public length = 0;
|
||||||
|
|
||||||
|
public setItem(k: string, v: string): void {
|
||||||
|
this.data[k] = v;
|
||||||
|
this.recalc();
|
||||||
}
|
}
|
||||||
|
|
||||||
MockStorageApi.prototype = {
|
public getItem(k: string): string | null {
|
||||||
setItem: function(k, v) {
|
|
||||||
this.data[k] = v;
|
|
||||||
this._recalc();
|
|
||||||
},
|
|
||||||
getItem: function(k) {
|
|
||||||
return this.data[k] || null;
|
return this.data[k] || null;
|
||||||
},
|
}
|
||||||
removeItem: function(k) {
|
|
||||||
|
public removeItem(k: string): void {
|
||||||
delete this.data[k];
|
delete this.data[k];
|
||||||
this._recalc();
|
this.recalc();
|
||||||
},
|
}
|
||||||
key: function(index) {
|
|
||||||
|
public key(index: number): string {
|
||||||
return this.keys[index];
|
return this.keys[index];
|
||||||
},
|
}
|
||||||
_recalc: function() {
|
|
||||||
const keys = [];
|
private recalc(): void {
|
||||||
|
const keys: string[] = [];
|
||||||
for (const k in this.data) {
|
for (const k in this.data) {
|
||||||
if (!this.data.hasOwnProperty(k)) {
|
if (!this.data.hasOwnProperty(k)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -50,6 +51,5 @@ MockStorageApi.prototype = {
|
|||||||
}
|
}
|
||||||
this.keys = keys;
|
this.keys = keys;
|
||||||
this.length = keys.length;
|
this.length = keys.length;
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ export class TestClient {
|
|||||||
options?: Partial<ICreateClientOpts>,
|
options?: Partial<ICreateClientOpts>,
|
||||||
) {
|
) {
|
||||||
if (sessionStoreBackend === undefined) {
|
if (sessionStoreBackend === undefined) {
|
||||||
sessionStoreBackend = new MockStorageApi();
|
sessionStoreBackend = new MockStorageApi() as unknown as Storage;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.httpBackend = new MockHttpBackend();
|
this.httpBackend = new MockHttpBackend();
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// stub for browser-matrix browserify tests
|
// stub for browser-matrix browserify tests
|
||||||
|
// @ts-ignore
|
||||||
global.XMLHttpRequest = jest.fn();
|
global.XMLHttpRequest = jest.fn();
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
// clean up XMLHttpRequest mock
|
// clean up XMLHttpRequest mock
|
||||||
|
// @ts-ignore
|
||||||
global.XMLHttpRequest = undefined;
|
global.XMLHttpRequest = undefined;
|
||||||
});
|
});
|
||||||
@@ -290,8 +290,9 @@ describe("DeviceList management:", function() {
|
|||||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||||
|
|
||||||
|
// Alice should be tracking bob's device list
|
||||||
expect(bobStat).toBeGreaterThan(
|
expect(bobStat).toBeGreaterThan(
|
||||||
0, "Alice should be tracking bob's device list",
|
0,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -326,8 +327,9 @@ describe("DeviceList management:", function() {
|
|||||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||||
|
|
||||||
|
// Alice should have marked bob's device list as untracked
|
||||||
expect(bobStat).toEqual(
|
expect(bobStat).toEqual(
|
||||||
0, "Alice should have marked bob's device list as untracked",
|
0,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -362,8 +364,9 @@ describe("DeviceList management:", function() {
|
|||||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||||
|
|
||||||
|
// Alice should have marked bob's device list as untracked
|
||||||
expect(bobStat).toEqual(
|
expect(bobStat).toEqual(
|
||||||
0, "Alice should have marked bob's device list as untracked",
|
0,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -378,13 +381,15 @@ describe("DeviceList management:", function() {
|
|||||||
anotherTestClient.httpBackend.when('GET', '/sync').respond(
|
anotherTestClient.httpBackend.when('GET', '/sync').respond(
|
||||||
200, getSyncResponse([]));
|
200, getSyncResponse([]));
|
||||||
await anotherTestClient.flushSync();
|
await anotherTestClient.flushSync();
|
||||||
await anotherTestClient.client.crypto.deviceList.saveIfDirty();
|
await anotherTestClient.client?.crypto?.deviceList?.saveIfDirty();
|
||||||
|
|
||||||
|
// @ts-ignore accessing private property
|
||||||
anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
const bobStat = data!.trackingStatus['@bob:xyz'];
|
||||||
|
|
||||||
|
// Alice should have marked bob's device list as untracked
|
||||||
expect(bobStat).toEqual(
|
expect(bobStat).toEqual(
|
||||||
0, "Alice should have marked bob's device list as untracked",
|
0,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@@ -31,8 +31,9 @@ import '../olm-loader';
|
|||||||
import { logger } from '../../src/logger';
|
import { logger } from '../../src/logger';
|
||||||
import * as testUtils from "../test-utils/test-utils";
|
import * as testUtils from "../test-utils/test-utils";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
import { CRYPTO_ENABLED } from "../../src/client";
|
import { CRYPTO_ENABLED, IUploadKeysRequest } from "../../src/client";
|
||||||
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
|
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
|
||||||
|
import { DeviceInfo } from '../../src/crypto/deviceinfo';
|
||||||
|
|
||||||
let aliTestClient: TestClient;
|
let aliTestClient: TestClient;
|
||||||
const roomId = "!room:localhost";
|
const roomId = "!room:localhost";
|
||||||
@@ -71,12 +72,12 @@ function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise<num
|
|||||||
expect(uploader.deviceKeys).toBeTruthy();
|
expect(uploader.deviceKeys).toBeTruthy();
|
||||||
|
|
||||||
const uploaderKeys = {};
|
const uploaderKeys = {};
|
||||||
uploaderKeys[uploader.deviceId] = uploader.deviceKeys;
|
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys;
|
||||||
querier.httpBackend.when("POST", "/keys/query")
|
querier.httpBackend.when("POST", "/keys/query")
|
||||||
.respond(200, function(_path, content) {
|
.respond(200, function(_path, content: IUploadKeysRequest) {
|
||||||
expect(content.device_keys[uploader.userId]).toEqual([]);
|
expect(content.device_keys![uploader.userId!]).toEqual([]);
|
||||||
const result = {};
|
const result = {};
|
||||||
result[uploader.userId] = uploaderKeys;
|
result[uploader.userId!] = uploaderKeys;
|
||||||
return { device_keys: result };
|
return { device_keys: result };
|
||||||
});
|
});
|
||||||
return querier.httpBackend.flush("/keys/query", 1);
|
return querier.httpBackend.flush("/keys/query", 1);
|
||||||
@@ -93,10 +94,10 @@ async function expectAliClaimKeys(): Promise<void> {
|
|||||||
const keys = await bobTestClient.awaitOneTimeKeyUpload();
|
const keys = await bobTestClient.awaitOneTimeKeyUpload();
|
||||||
aliTestClient.httpBackend.when(
|
aliTestClient.httpBackend.when(
|
||||||
"POST", "/keys/claim",
|
"POST", "/keys/claim",
|
||||||
).respond(200, function(_path, content) {
|
).respond(200, function(_path, content: IUploadKeysRequest) {
|
||||||
const claimType = content.one_time_keys[bobUserId][bobDeviceId];
|
const claimType = content.one_time_keys![bobUserId][bobDeviceId];
|
||||||
expect(claimType).toEqual("signed_curve25519");
|
expect(claimType).toEqual("signed_curve25519");
|
||||||
let keyId = null;
|
let keyId = '';
|
||||||
for (keyId in keys) {
|
for (keyId in keys) {
|
||||||
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
|
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
|
||||||
if (keyId.indexOf(claimType + ":") === 0) {
|
if (keyId.indexOf(claimType + ":") === 0) {
|
||||||
@@ -132,13 +133,13 @@ async function aliDownloadsKeys(): Promise<void> {
|
|||||||
// check that the localStorage is updated as we expect (not sure this is
|
// check that the localStorage is updated as we expect (not sure this is
|
||||||
// an integration test, but meh)
|
// an integration test, but meh)
|
||||||
await Promise.all([p1(), p2()]);
|
await Promise.all([p1(), p2()]);
|
||||||
await aliTestClient.client.crypto.deviceList.saveIfDirty();
|
await aliTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||||
// @ts-ignore - protected
|
// @ts-ignore - protected
|
||||||
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||||
const devices = data.devices[bobUserId];
|
const devices = data!.devices[bobUserId]!;
|
||||||
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
|
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
|
||||||
expect(devices[bobDeviceId].verified).
|
expect(devices[bobDeviceId].verified).
|
||||||
toBe(0); // DeviceVerification.UNVERIFIED
|
toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +238,7 @@ function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
|
|||||||
|
|
||||||
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
|
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
|
||||||
const path = "/send/m.room.encrypted/";
|
const path = "/send/m.room.encrypted/";
|
||||||
const prom = new Promise((resolve) => {
|
const prom = new Promise<IContent>((resolve) => {
|
||||||
httpBackend.when("PUT", path).respond(200, function(_path, content) {
|
httpBackend.when("PUT", path).respond(200, function(_path, content) {
|
||||||
resolve(content);
|
resolve(content);
|
||||||
return {
|
return {
|
||||||
@@ -252,14 +253,14 @@ async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]):
|
|||||||
}
|
}
|
||||||
|
|
||||||
function aliRecvMessage(): Promise<void> {
|
function aliRecvMessage(): Promise<void> {
|
||||||
const message = bobMessages.shift();
|
const message = bobMessages.shift()!;
|
||||||
return recvMessage(
|
return recvMessage(
|
||||||
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
|
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function bobRecvMessage(): Promise<void> {
|
function bobRecvMessage(): Promise<void> {
|
||||||
const message = aliMessages.shift();
|
const message = aliMessages.shift()!;
|
||||||
return recvMessage(
|
return recvMessage(
|
||||||
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
|
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
|
||||||
);
|
);
|
||||||
@@ -494,6 +495,7 @@ describe("MatrixClient crypto", () => {
|
|||||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||||
await aliTestClient.start();
|
await aliTestClient.start();
|
||||||
await bobTestClient.start();
|
await bobTestClient.start();
|
||||||
|
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
await firstSync(aliTestClient);
|
await firstSync(aliTestClient);
|
||||||
await aliEnablesEncryption();
|
await aliEnablesEncryption();
|
||||||
await aliSendsFirstMessage();
|
await aliSendsFirstMessage();
|
||||||
@@ -504,10 +506,11 @@ describe("MatrixClient crypto", () => {
|
|||||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||||
await aliTestClient.start();
|
await aliTestClient.start();
|
||||||
await bobTestClient.start();
|
await bobTestClient.start();
|
||||||
|
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
await firstSync(aliTestClient);
|
await firstSync(aliTestClient);
|
||||||
await aliEnablesEncryption();
|
await aliEnablesEncryption();
|
||||||
await aliSendsFirstMessage();
|
await aliSendsFirstMessage();
|
||||||
const message = aliMessages.shift();
|
const message = aliMessages.shift()!;
|
||||||
const syncData = {
|
const syncData = {
|
||||||
next_batch: "x",
|
next_batch: "x",
|
||||||
rooms: {
|
rooms: {
|
||||||
@@ -567,6 +570,7 @@ describe("MatrixClient crypto", () => {
|
|||||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||||
await aliTestClient.start();
|
await aliTestClient.start();
|
||||||
await bobTestClient.start();
|
await bobTestClient.start();
|
||||||
|
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
await firstSync(aliTestClient);
|
await firstSync(aliTestClient);
|
||||||
await aliEnablesEncryption();
|
await aliEnablesEncryption();
|
||||||
await aliSendsFirstMessage();
|
await aliSendsFirstMessage();
|
||||||
@@ -584,6 +588,9 @@ describe("MatrixClient crypto", () => {
|
|||||||
await firstSync(bobTestClient);
|
await firstSync(bobTestClient);
|
||||||
await aliEnablesEncryption();
|
await aliEnablesEncryption();
|
||||||
await aliSendsFirstMessage();
|
await aliSendsFirstMessage();
|
||||||
|
bobTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||||
|
200, {},
|
||||||
|
);
|
||||||
await bobRecvMessage();
|
await bobRecvMessage();
|
||||||
await bobEnablesEncryption();
|
await bobEnablesEncryption();
|
||||||
const ciphertext = await bobSendsReplyMessage();
|
const ciphertext = await bobSendsReplyMessage();
|
||||||
@@ -658,11 +665,10 @@ describe("MatrixClient crypto", () => {
|
|||||||
]);
|
]);
|
||||||
logger.log(aliTestClient + ': started');
|
logger.log(aliTestClient + ': started');
|
||||||
httpBackend.when("POST", "/keys/upload")
|
httpBackend.when("POST", "/keys/upload")
|
||||||
.respond(200, (_path, content) => {
|
.respond(200, (_path, content: IUploadKeysRequest) => {
|
||||||
expect(content.one_time_keys).toBeTruthy();
|
expect(content.one_time_keys).toBeTruthy();
|
||||||
expect(content.one_time_keys).not.toEqual({});
|
expect(content.one_time_keys).not.toEqual({});
|
||||||
expect(Object.keys(content.one_time_keys).length).toBeGreaterThanOrEqual(1);
|
expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1);
|
||||||
logger.log('received %i one-time keys', Object.keys(content.one_time_keys).length);
|
|
||||||
// cancel futher calls by telling the client
|
// cancel futher calls by telling the client
|
||||||
// we have more than we need
|
// we have more than we need
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,25 +1,59 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 HttpBackend from "matrix-mock-request";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ClientEvent,
|
||||||
|
HttpApiEvent,
|
||||||
|
IEvent,
|
||||||
|
MatrixClient,
|
||||||
|
RoomEvent,
|
||||||
|
RoomMemberEvent,
|
||||||
|
RoomStateEvent,
|
||||||
|
UserEvent,
|
||||||
|
} from "../../src";
|
||||||
import * as utils from "../test-utils/test-utils";
|
import * as utils from "../test-utils/test-utils";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
|
|
||||||
describe("MatrixClient events", function() {
|
describe("MatrixClient events", function() {
|
||||||
let client;
|
|
||||||
let httpBackend;
|
|
||||||
const selfUserId = "@alice:localhost";
|
const selfUserId = "@alice:localhost";
|
||||||
const selfAccessToken = "aseukfgwef";
|
const selfAccessToken = "aseukfgwef";
|
||||||
|
let client: MatrixClient | undefined;
|
||||||
|
let httpBackend: HttpBackend | undefined;
|
||||||
|
|
||||||
|
const setupTests = (): [MatrixClient, HttpBackend] => {
|
||||||
|
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||||
|
const client = testClient.client;
|
||||||
|
const httpBackend = testClient.httpBackend;
|
||||||
|
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(function() {
|
beforeEach(function() {
|
||||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
[client!, httpBackend] = setupTests();
|
||||||
client = testClient.client;
|
|
||||||
httpBackend = testClient.httpBackend;
|
|
||||||
httpBackend.when("GET", "/versions").respond(200, {});
|
|
||||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
|
||||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function() {
|
afterEach(function() {
|
||||||
httpBackend.verifyNoOutstandingExpectation();
|
httpBackend?.verifyNoOutstandingExpectation();
|
||||||
client.stopClient();
|
client?.stopClient();
|
||||||
return httpBackend.stop();
|
return httpBackend?.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("emissions", function() {
|
describe("emissions", function() {
|
||||||
@@ -93,10 +127,10 @@ describe("MatrixClient events", function() {
|
|||||||
|
|
||||||
it("should emit events from both the first and subsequent /sync calls",
|
it("should emit events from both the first and subsequent /sync calls",
|
||||||
function() {
|
function() {
|
||||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||||
|
|
||||||
let expectedEvents = [];
|
let expectedEvents: Partial<IEvent>[] = [];
|
||||||
expectedEvents = expectedEvents.concat(
|
expectedEvents = expectedEvents.concat(
|
||||||
SYNC_DATA.presence.events,
|
SYNC_DATA.presence.events,
|
||||||
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||||
@@ -105,7 +139,7 @@ describe("MatrixClient events", function() {
|
|||||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
|
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
|
||||||
);
|
);
|
||||||
|
|
||||||
client.on("event", function(event) {
|
client!.on(ClientEvent.Event, function(event) {
|
||||||
let found = false;
|
let found = false;
|
||||||
for (let i = 0; i < expectedEvents.length; i++) {
|
for (let i = 0; i < expectedEvents.length; i++) {
|
||||||
if (expectedEvents[i].event_id === event.getId()) {
|
if (expectedEvents[i].event_id === event.getId()) {
|
||||||
@@ -114,31 +148,27 @@ describe("MatrixClient events", function() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
expect(found).toBe(
|
expect(found).toBe(true);
|
||||||
true, "Unexpected 'event' emitted: " + event.getType(),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
client.startClient();
|
client!.startClient();
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
// wait for two SYNCING events
|
// wait for two SYNCING events
|
||||||
utils.syncPromise(client).then(() => {
|
utils.syncPromise(client!).then(() => {
|
||||||
return utils.syncPromise(client);
|
return utils.syncPromise(client!);
|
||||||
}),
|
}),
|
||||||
httpBackend.flushAllExpected(),
|
httpBackend!.flushAllExpected(),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
expect(expectedEvents.length).toEqual(
|
expect(expectedEvents.length).toEqual(0);
|
||||||
0, "Failed to see all events from /sync calls",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit User events", function(done) {
|
it("should emit User events", function(done) {
|
||||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||||
let fired = false;
|
let fired = false;
|
||||||
client.on("User.presence", function(event, user) {
|
client!.on(UserEvent.Presence, function(event, user) {
|
||||||
fired = true;
|
fired = true;
|
||||||
expect(user).toBeTruthy();
|
expect(user).toBeTruthy();
|
||||||
expect(event).toBeTruthy();
|
expect(event).toBeTruthy();
|
||||||
@@ -146,58 +176,52 @@ describe("MatrixClient events", function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(event.event).toMatch(SYNC_DATA.presence.events[0]);
|
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
|
||||||
expect(user.presence).toEqual(
|
expect(user.presence).toEqual(
|
||||||
SYNC_DATA.presence.events[0].content.presence,
|
SYNC_DATA.presence.events[0]?.content?.presence,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
client.startClient();
|
client!.startClient();
|
||||||
|
|
||||||
httpBackend.flushAllExpected().then(function() {
|
httpBackend!.flushAllExpected().then(function() {
|
||||||
expect(fired).toBe(true, "User.presence didn't fire.");
|
expect(fired).toBe(true);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit Room events", function() {
|
it("should emit Room events", function() {
|
||||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||||
let roomInvokeCount = 0;
|
let roomInvokeCount = 0;
|
||||||
let roomNameInvokeCount = 0;
|
let roomNameInvokeCount = 0;
|
||||||
let timelineFireCount = 0;
|
let timelineFireCount = 0;
|
||||||
client.on("Room", function(room) {
|
client!.on(ClientEvent.Room, function(room) {
|
||||||
roomInvokeCount++;
|
roomInvokeCount++;
|
||||||
expect(room.roomId).toEqual("!erufh:bar");
|
expect(room.roomId).toEqual("!erufh:bar");
|
||||||
});
|
});
|
||||||
client.on("Room.timeline", function(event, room) {
|
client!.on(RoomEvent.Timeline, function(event, room) {
|
||||||
timelineFireCount++;
|
timelineFireCount++;
|
||||||
expect(room.roomId).toEqual("!erufh:bar");
|
expect(room.roomId).toEqual("!erufh:bar");
|
||||||
});
|
});
|
||||||
client.on("Room.name", function(room) {
|
client!.on(RoomEvent.Name, function(room) {
|
||||||
roomNameInvokeCount++;
|
roomNameInvokeCount++;
|
||||||
});
|
});
|
||||||
|
|
||||||
client.startClient();
|
client!.startClient();
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
httpBackend.flushAllExpected(),
|
httpBackend!.flushAllExpected(),
|
||||||
utils.syncPromise(client, 2),
|
utils.syncPromise(client!, 2),
|
||||||
]).then(function() {
|
]).then(function() {
|
||||||
expect(roomInvokeCount).toEqual(
|
expect(roomInvokeCount).toEqual(1);
|
||||||
1, "Room fired wrong number of times.",
|
expect(roomNameInvokeCount).toEqual(1);
|
||||||
);
|
expect(timelineFireCount).toEqual(3);
|
||||||
expect(roomNameInvokeCount).toEqual(
|
|
||||||
1, "Room.name fired wrong number of times.",
|
|
||||||
);
|
|
||||||
expect(timelineFireCount).toEqual(
|
|
||||||
3, "Room.timeline fired the wrong number of times",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit RoomState events", function() {
|
it("should emit RoomState events", function() {
|
||||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||||
|
|
||||||
const roomStateEventTypes = [
|
const roomStateEventTypes = [
|
||||||
"m.room.member", "m.room.create",
|
"m.room.member", "m.room.create",
|
||||||
@@ -205,126 +229,106 @@ describe("MatrixClient events", function() {
|
|||||||
let eventsInvokeCount = 0;
|
let eventsInvokeCount = 0;
|
||||||
let membersInvokeCount = 0;
|
let membersInvokeCount = 0;
|
||||||
let newMemberInvokeCount = 0;
|
let newMemberInvokeCount = 0;
|
||||||
client.on("RoomState.events", function(event, state) {
|
client!.on(RoomStateEvent.Events, function(event, state) {
|
||||||
eventsInvokeCount++;
|
eventsInvokeCount++;
|
||||||
const index = roomStateEventTypes.indexOf(event.getType());
|
const index = roomStateEventTypes.indexOf(event.getType());
|
||||||
expect(index).not.toEqual(
|
expect(index).not.toEqual(-1);
|
||||||
-1, "Unexpected room state event type: " + event.getType(),
|
|
||||||
);
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
roomStateEventTypes.splice(index, 1);
|
roomStateEventTypes.splice(index, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
client.on("RoomState.members", function(event, state, member) {
|
client!.on(RoomStateEvent.Members, function(event, state, member) {
|
||||||
membersInvokeCount++;
|
membersInvokeCount++;
|
||||||
expect(member.roomId).toEqual("!erufh:bar");
|
expect(member.roomId).toEqual("!erufh:bar");
|
||||||
expect(member.userId).toEqual("@foo:bar");
|
expect(member.userId).toEqual("@foo:bar");
|
||||||
expect(member.membership).toEqual("join");
|
expect(member.membership).toEqual("join");
|
||||||
});
|
});
|
||||||
client.on("RoomState.newMember", function(event, state, member) {
|
client!.on(RoomStateEvent.NewMember, function(event, state, member) {
|
||||||
newMemberInvokeCount++;
|
newMemberInvokeCount++;
|
||||||
expect(member.roomId).toEqual("!erufh:bar");
|
expect(member.roomId).toEqual("!erufh:bar");
|
||||||
expect(member.userId).toEqual("@foo:bar");
|
expect(member.userId).toEqual("@foo:bar");
|
||||||
expect(member.membership).toBeFalsy();
|
expect(member.membership).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
client.startClient();
|
client!.startClient();
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
httpBackend.flushAllExpected(),
|
httpBackend!.flushAllExpected(),
|
||||||
utils.syncPromise(client, 2),
|
utils.syncPromise(client!, 2),
|
||||||
]).then(function() {
|
]).then(function() {
|
||||||
expect(membersInvokeCount).toEqual(
|
expect(membersInvokeCount).toEqual(1);
|
||||||
1, "RoomState.members fired wrong number of times",
|
expect(newMemberInvokeCount).toEqual(1);
|
||||||
);
|
expect(eventsInvokeCount).toEqual(2);
|
||||||
expect(newMemberInvokeCount).toEqual(
|
|
||||||
1, "RoomState.newMember fired wrong number of times",
|
|
||||||
);
|
|
||||||
expect(eventsInvokeCount).toEqual(
|
|
||||||
2, "RoomState.events fired wrong number of times",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit RoomMember events", function() {
|
it("should emit RoomMember events", function() {
|
||||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||||
|
|
||||||
let typingInvokeCount = 0;
|
let typingInvokeCount = 0;
|
||||||
let powerLevelInvokeCount = 0;
|
let powerLevelInvokeCount = 0;
|
||||||
let nameInvokeCount = 0;
|
let nameInvokeCount = 0;
|
||||||
let membershipInvokeCount = 0;
|
let membershipInvokeCount = 0;
|
||||||
client.on("RoomMember.name", function(event, member) {
|
client!.on(RoomMemberEvent.Name, function(event, member) {
|
||||||
nameInvokeCount++;
|
nameInvokeCount++;
|
||||||
});
|
});
|
||||||
client.on("RoomMember.typing", function(event, member) {
|
client!.on(RoomMemberEvent.Typing, function(event, member) {
|
||||||
typingInvokeCount++;
|
typingInvokeCount++;
|
||||||
expect(member.typing).toBe(true);
|
expect(member.typing).toBe(true);
|
||||||
});
|
});
|
||||||
client.on("RoomMember.powerLevel", function(event, member) {
|
client!.on(RoomMemberEvent.PowerLevel, function(event, member) {
|
||||||
powerLevelInvokeCount++;
|
powerLevelInvokeCount++;
|
||||||
});
|
});
|
||||||
client.on("RoomMember.membership", function(event, member) {
|
client!.on(RoomMemberEvent.Membership, function(event, member) {
|
||||||
membershipInvokeCount++;
|
membershipInvokeCount++;
|
||||||
expect(member.membership).toEqual("join");
|
expect(member.membership).toEqual("join");
|
||||||
});
|
});
|
||||||
|
|
||||||
client.startClient();
|
client!.startClient();
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
httpBackend.flushAllExpected(),
|
httpBackend!.flushAllExpected(),
|
||||||
utils.syncPromise(client, 2),
|
utils.syncPromise(client!, 2),
|
||||||
]).then(function() {
|
]).then(function() {
|
||||||
expect(typingInvokeCount).toEqual(
|
expect(typingInvokeCount).toEqual(1);
|
||||||
1, "RoomMember.typing fired wrong number of times",
|
expect(powerLevelInvokeCount).toEqual(0);
|
||||||
);
|
expect(nameInvokeCount).toEqual(0);
|
||||||
expect(powerLevelInvokeCount).toEqual(
|
expect(membershipInvokeCount).toEqual(1);
|
||||||
0, "RoomMember.powerLevel fired wrong number of times",
|
|
||||||
);
|
|
||||||
expect(nameInvokeCount).toEqual(
|
|
||||||
0, "RoomMember.name fired wrong number of times",
|
|
||||||
);
|
|
||||||
expect(membershipInvokeCount).toEqual(
|
|
||||||
1, "RoomMember.membership fired wrong number of times",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
|
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
|
||||||
const error = { errcode: 'M_UNKNOWN_TOKEN' };
|
const error = { errcode: 'M_UNKNOWN_TOKEN' };
|
||||||
httpBackend.when("GET", "/sync").respond(401, error);
|
httpBackend!.when("GET", "/sync").respond(401, error);
|
||||||
|
|
||||||
let sessionLoggedOutCount = 0;
|
let sessionLoggedOutCount = 0;
|
||||||
client.on("Session.logged_out", function(errObj) {
|
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
|
||||||
sessionLoggedOutCount++;
|
sessionLoggedOutCount++;
|
||||||
expect(errObj.data).toEqual(error);
|
expect(errObj.data).toEqual(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.startClient();
|
client!.startClient();
|
||||||
|
|
||||||
return httpBackend.flushAllExpected().then(function() {
|
return httpBackend!.flushAllExpected().then(function() {
|
||||||
expect(sessionLoggedOutCount).toEqual(
|
expect(sessionLoggedOutCount).toEqual(1);
|
||||||
1, "Session.logged_out fired wrong number of times",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() {
|
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() {
|
||||||
const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true };
|
const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true };
|
||||||
httpBackend.when("GET", "/sync").respond(401, error);
|
httpBackend!.when("GET", "/sync").respond(401, error);
|
||||||
|
|
||||||
let sessionLoggedOutCount = 0;
|
let sessionLoggedOutCount = 0;
|
||||||
client.on("Session.logged_out", function(errObj) {
|
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
|
||||||
sessionLoggedOutCount++;
|
sessionLoggedOutCount++;
|
||||||
expect(errObj.data).toEqual(error);
|
expect(errObj.data).toEqual(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.startClient();
|
client!.startClient();
|
||||||
|
|
||||||
return httpBackend.flushAllExpected().then(function() {
|
return httpBackend!.flushAllExpected().then(function() {
|
||||||
expect(sessionLoggedOutCount).toEqual(
|
expect(sessionLoggedOutCount).toEqual(1);
|
||||||
1, "Session.logged_out fired wrong number of times",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -13,68 +13,84 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
import HttpBackend from "matrix-mock-request";
|
||||||
|
|
||||||
import * as utils from "../test-utils/test-utils";
|
import * as utils from "../test-utils/test-utils";
|
||||||
import { CRYPTO_ENABLED } from "../../src/client";
|
import { CRYPTO_ENABLED, MatrixClient, IStoredClientOpts } from "../../src/client";
|
||||||
import { MatrixEvent } from "../../src/models/event";
|
import { MatrixEvent } from "../../src/models/event";
|
||||||
import { Filter, MemoryStore, Room } from "../../src/matrix";
|
import { Filter, MemoryStore, Room } from "../../src/matrix";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
|
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
|
||||||
|
import { IFilterDefinition } from "../../src/filter";
|
||||||
|
import { FileType } from "../../src/http-api";
|
||||||
|
import { ISearchResults } from "../../src/@types/search";
|
||||||
|
import { IStore } from "../../src/store";
|
||||||
|
|
||||||
describe("MatrixClient", function() {
|
describe("MatrixClient", function() {
|
||||||
let client = null;
|
|
||||||
let httpBackend = null;
|
|
||||||
let store = null;
|
|
||||||
const userId = "@alice:localhost";
|
const userId = "@alice:localhost";
|
||||||
const accessToken = "aseukfgwef";
|
const accessToken = "aseukfgwef";
|
||||||
const idServerDomain = "identity.localhost"; // not a real server
|
const idServerDomain = "identity.localhost"; // not a real server
|
||||||
const identityAccessToken = "woop-i-am-a-secret";
|
const identityAccessToken = "woop-i-am-a-secret";
|
||||||
|
let client: MatrixClient | undefined;
|
||||||
|
let httpBackend: HttpBackend | undefined;
|
||||||
|
let store: MemoryStore | undefined;
|
||||||
|
|
||||||
beforeEach(function() {
|
const defaultClientOpts: IStoredClientOpts = {
|
||||||
store = new MemoryStore();
|
canResetEntireTimeline: roomId => false,
|
||||||
|
experimentalThreadSupport: false,
|
||||||
|
crypto: {} as unknown as IStoredClientOpts['crypto'],
|
||||||
|
};
|
||||||
|
const setupTests = (): [MatrixClient, HttpBackend, MemoryStore] => {
|
||||||
|
const store = new MemoryStore();
|
||||||
|
|
||||||
const testClient = new TestClient(userId, "aliceDevice", accessToken, undefined, {
|
const testClient = new TestClient(userId, "aliceDevice", accessToken, undefined, {
|
||||||
store,
|
store: store as IStore,
|
||||||
identityServer: {
|
identityServer: {
|
||||||
getAccessToken: () => Promise.resolve(identityAccessToken),
|
getAccessToken: () => Promise.resolve(identityAccessToken),
|
||||||
},
|
},
|
||||||
idBaseUrl: `https://${idServerDomain}`,
|
idBaseUrl: `https://${idServerDomain}`,
|
||||||
});
|
});
|
||||||
httpBackend = testClient.httpBackend;
|
|
||||||
client = testClient.client;
|
return [testClient.client, testClient.httpBackend, store];
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
[client, httpBackend, store] = setupTests();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function() {
|
afterEach(function() {
|
||||||
httpBackend.verifyNoOutstandingExpectation();
|
httpBackend!.verifyNoOutstandingExpectation();
|
||||||
return httpBackend.stop();
|
return httpBackend!.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("uploadContent", function() {
|
describe("uploadContent", function() {
|
||||||
const buf = Buffer.from('hello world');
|
const buf = Buffer.from('hello world');
|
||||||
it("should upload the file", function() {
|
it("should upload the file", function() {
|
||||||
httpBackend.when(
|
httpBackend!.when(
|
||||||
"POST", "/_matrix/media/r0/upload",
|
"POST", "/_matrix/media/r0/upload",
|
||||||
).check(function(req) {
|
).check(function(req) {
|
||||||
expect(req.rawData).toEqual(buf);
|
expect(req.rawData).toEqual(buf);
|
||||||
expect(req.queryParams.filename).toEqual("hi.txt");
|
expect(req.queryParams?.filename).toEqual("hi.txt");
|
||||||
if (!(req.queryParams.access_token == accessToken ||
|
if (!(req.queryParams?.access_token == accessToken ||
|
||||||
req.headers["Authorization"] == "Bearer " + accessToken)) {
|
req.headers["Authorization"] == "Bearer " + accessToken)) {
|
||||||
expect(true).toBe(false);
|
expect(true).toBe(false);
|
||||||
}
|
}
|
||||||
expect(req.headers["Content-Type"]).toEqual("text/plain");
|
expect(req.headers["Content-Type"]).toEqual("text/plain");
|
||||||
|
// @ts-ignore private property
|
||||||
expect(req.opts.json).toBeFalsy();
|
expect(req.opts.json).toBeFalsy();
|
||||||
|
// @ts-ignore private property
|
||||||
expect(req.opts.timeout).toBe(undefined);
|
expect(req.opts.timeout).toBe(undefined);
|
||||||
}).respond(200, "content", true);
|
}).respond(200, "content", true);
|
||||||
|
|
||||||
const prom = client.uploadContent({
|
const prom = client!.uploadContent({
|
||||||
stream: buf,
|
stream: buf,
|
||||||
name: "hi.txt",
|
name: "hi.txt",
|
||||||
type: "text/plain",
|
type: "text/plain",
|
||||||
});
|
} as unknown as FileType);
|
||||||
|
|
||||||
expect(prom).toBeTruthy();
|
expect(prom).toBeTruthy();
|
||||||
|
|
||||||
const uploads = client.getCurrentUploads();
|
const uploads = client!.getCurrentUploads();
|
||||||
expect(uploads.length).toEqual(1);
|
expect(uploads.length).toEqual(1);
|
||||||
expect(uploads[0].promise).toBe(prom);
|
expect(uploads[0].promise).toBe(prom);
|
||||||
expect(uploads[0].loaded).toEqual(0);
|
expect(uploads[0].loaded).toEqual(0);
|
||||||
@@ -83,51 +99,53 @@ describe("MatrixClient", function() {
|
|||||||
// for backwards compatibility, we return the raw JSON
|
// for backwards compatibility, we return the raw JSON
|
||||||
expect(response).toEqual("content");
|
expect(response).toEqual("content");
|
||||||
|
|
||||||
const uploads = client.getCurrentUploads();
|
const uploads = client!.getCurrentUploads();
|
||||||
expect(uploads.length).toEqual(0);
|
expect(uploads.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
httpBackend.flush();
|
httpBackend!.flush('');
|
||||||
return prom2;
|
return prom2;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should parse the response if rawResponse=false", function() {
|
it("should parse the response if rawResponse=false", function() {
|
||||||
httpBackend.when(
|
httpBackend!.when(
|
||||||
"POST", "/_matrix/media/r0/upload",
|
"POST", "/_matrix/media/r0/upload",
|
||||||
).check(function(req) {
|
).check(function(req) {
|
||||||
|
// @ts-ignore private property
|
||||||
expect(req.opts.json).toBeFalsy();
|
expect(req.opts.json).toBeFalsy();
|
||||||
}).respond(200, { "content_uri": "uri" });
|
}).respond(200, { "content_uri": "uri" });
|
||||||
|
|
||||||
const prom = client.uploadContent({
|
const prom = client!.uploadContent({
|
||||||
stream: buf,
|
stream: buf,
|
||||||
name: "hi.txt",
|
name: "hi.txt",
|
||||||
type: "text/plain",
|
type: "text/plain",
|
||||||
}, {
|
} as unknown as FileType, {
|
||||||
rawResponse: false,
|
rawResponse: false,
|
||||||
}).then(function(response) {
|
}).then(function(response) {
|
||||||
expect(response.content_uri).toEqual("uri");
|
expect(response.content_uri).toEqual("uri");
|
||||||
});
|
});
|
||||||
|
|
||||||
httpBackend.flush();
|
httpBackend!.flush('');
|
||||||
return prom;
|
return prom;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should parse errors into a MatrixError", function() {
|
it("should parse errors into a MatrixError", function() {
|
||||||
httpBackend.when(
|
httpBackend!.when(
|
||||||
"POST", "/_matrix/media/r0/upload",
|
"POST", "/_matrix/media/r0/upload",
|
||||||
).check(function(req) {
|
).check(function(req) {
|
||||||
expect(req.rawData).toEqual(buf);
|
expect(req.rawData).toEqual(buf);
|
||||||
|
// @ts-ignore private property
|
||||||
expect(req.opts.json).toBeFalsy();
|
expect(req.opts.json).toBeFalsy();
|
||||||
}).respond(400, {
|
}).respond(400, {
|
||||||
"errcode": "M_SNAFU",
|
"errcode": "M_SNAFU",
|
||||||
"error": "broken",
|
"error": "broken",
|
||||||
});
|
});
|
||||||
|
|
||||||
const prom = client.uploadContent({
|
const prom = client!.uploadContent({
|
||||||
stream: buf,
|
stream: buf,
|
||||||
name: "hi.txt",
|
name: "hi.txt",
|
||||||
type: "text/plain",
|
type: "text/plain",
|
||||||
}).then(function(response) {
|
} as unknown as FileType).then(function(response) {
|
||||||
throw Error("request not failed");
|
throw Error("request not failed");
|
||||||
}, function(error) {
|
}, function(error) {
|
||||||
expect(error.httpStatus).toEqual(400);
|
expect(error.httpStatus).toEqual(400);
|
||||||
@@ -135,18 +153,18 @@ describe("MatrixClient", function() {
|
|||||||
expect(error.message).toEqual("broken");
|
expect(error.message).toEqual("broken");
|
||||||
});
|
});
|
||||||
|
|
||||||
httpBackend.flush();
|
httpBackend!.flush('');
|
||||||
return prom;
|
return prom;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return a promise which can be cancelled", function() {
|
it("should return a promise which can be cancelled", function() {
|
||||||
const prom = client.uploadContent({
|
const prom = client!.uploadContent({
|
||||||
stream: buf,
|
stream: buf,
|
||||||
name: "hi.txt",
|
name: "hi.txt",
|
||||||
type: "text/plain",
|
type: "text/plain",
|
||||||
});
|
} as unknown as FileType);
|
||||||
|
|
||||||
const uploads = client.getCurrentUploads();
|
const uploads = client!.getCurrentUploads();
|
||||||
expect(uploads.length).toEqual(1);
|
expect(uploads.length).toEqual(1);
|
||||||
expect(uploads[0].promise).toBe(prom);
|
expect(uploads[0].promise).toBe(prom);
|
||||||
expect(uploads[0].loaded).toEqual(0);
|
expect(uploads[0].loaded).toEqual(0);
|
||||||
@@ -156,11 +174,11 @@ describe("MatrixClient", function() {
|
|||||||
}, function(error) {
|
}, function(error) {
|
||||||
expect(error).toEqual("aborted");
|
expect(error).toEqual("aborted");
|
||||||
|
|
||||||
const uploads = client.getCurrentUploads();
|
const uploads = client!.getCurrentUploads();
|
||||||
expect(uploads.length).toEqual(0);
|
expect(uploads.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
const r = client.cancelUpload(prom);
|
const r = client!.cancelUpload(prom);
|
||||||
expect(r).toBe(true);
|
expect(r).toBe(true);
|
||||||
return prom2;
|
return prom2;
|
||||||
});
|
});
|
||||||
@@ -169,17 +187,20 @@ describe("MatrixClient", function() {
|
|||||||
describe("joinRoom", function() {
|
describe("joinRoom", function() {
|
||||||
it("should no-op if you've already joined a room", function() {
|
it("should no-op if you've already joined a room", function() {
|
||||||
const roomId = "!foo:bar";
|
const roomId = "!foo:bar";
|
||||||
const room = new Room(roomId, client, userId);
|
const room = new Room(roomId, client!, userId);
|
||||||
client.fetchRoomEvent = () => Promise.resolve({});
|
client!.fetchRoomEvent = () => Promise.resolve({
|
||||||
|
type: 'test',
|
||||||
|
content: {},
|
||||||
|
});
|
||||||
room.addLiveEvents([
|
room.addLiveEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
user: userId, room: roomId, mship: "join", event: true,
|
user: userId, room: roomId, mship: "join", event: true,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
httpBackend.verifyNoOutstandingRequests();
|
httpBackend!.verifyNoOutstandingRequests();
|
||||||
store.storeRoom(room);
|
store!.storeRoom(room);
|
||||||
client.joinRoom(roomId);
|
client!.joinRoom(roomId);
|
||||||
httpBackend.verifyNoOutstandingRequests();
|
httpBackend!.verifyNoOutstandingRequests();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -190,12 +211,12 @@ describe("MatrixClient", function() {
|
|||||||
const filter = Filter.fromJson(userId, filterId, {
|
const filter = Filter.fromJson(userId, filterId, {
|
||||||
event_format: "client",
|
event_format: "client",
|
||||||
});
|
});
|
||||||
store.storeFilter(filter);
|
store!.storeFilter(filter);
|
||||||
client.getFilter(userId, filterId, true).then(function(gotFilter) {
|
client!.getFilter(userId, filterId, true).then(function(gotFilter) {
|
||||||
expect(gotFilter).toEqual(filter);
|
expect(gotFilter).toEqual(filter);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
httpBackend.verifyNoOutstandingRequests();
|
httpBackend!.verifyNoOutstandingRequests();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should do an HTTP request if !allowCached even if one exists",
|
it("should do an HTTP request if !allowCached even if one exists",
|
||||||
@@ -204,20 +225,20 @@ describe("MatrixClient", function() {
|
|||||||
event_format: "federation",
|
event_format: "federation",
|
||||||
};
|
};
|
||||||
|
|
||||||
httpBackend.when(
|
httpBackend!.when(
|
||||||
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
|
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
|
||||||
).respond(200, httpFilterDefinition);
|
).respond(200, httpFilterDefinition);
|
||||||
|
|
||||||
const storeFilter = Filter.fromJson(userId, filterId, {
|
const storeFilter = Filter.fromJson(userId, filterId, {
|
||||||
event_format: "client",
|
event_format: "client",
|
||||||
});
|
});
|
||||||
store.storeFilter(storeFilter);
|
store!.storeFilter(storeFilter);
|
||||||
client.getFilter(userId, filterId, false).then(function(gotFilter) {
|
client!.getFilter(userId, filterId, false).then(function(gotFilter) {
|
||||||
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
httpBackend.flush();
|
httpBackend!.flush('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should do an HTTP request if nothing is in the cache and then store it",
|
it("should do an HTTP request if nothing is in the cache and then store it",
|
||||||
@@ -225,18 +246,18 @@ describe("MatrixClient", function() {
|
|||||||
const httpFilterDefinition = {
|
const httpFilterDefinition = {
|
||||||
event_format: "federation",
|
event_format: "federation",
|
||||||
};
|
};
|
||||||
expect(store.getFilter(userId, filterId)).toBe(null);
|
expect(store!.getFilter(userId, filterId)).toBe(null);
|
||||||
|
|
||||||
httpBackend.when(
|
httpBackend!.when(
|
||||||
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
|
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
|
||||||
).respond(200, httpFilterDefinition);
|
).respond(200, httpFilterDefinition);
|
||||||
client.getFilter(userId, filterId, true).then(function(gotFilter) {
|
client!.getFilter(userId, filterId, true).then(function(gotFilter) {
|
||||||
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
||||||
expect(store.getFilter(userId, filterId)).toBeTruthy();
|
expect(store!.getFilter(userId, filterId)).toBeTruthy();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
httpBackend.flush();
|
httpBackend!.flush('');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -244,13 +265,13 @@ describe("MatrixClient", function() {
|
|||||||
const filterId = "f1llllllerid";
|
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", function(done) {
|
||||||
expect(store.getFilter(userId, filterId)).toBe(null);
|
expect(store!.getFilter(userId, filterId)).toBe(null);
|
||||||
|
|
||||||
const filterDefinition = {
|
const filterDefinition = {
|
||||||
event_format: "client",
|
event_format: "client" as IFilterDefinition['event_format'],
|
||||||
};
|
};
|
||||||
|
|
||||||
httpBackend.when(
|
httpBackend!.when(
|
||||||
"POST", "/user/" + encodeURIComponent(userId) + "/filter",
|
"POST", "/user/" + encodeURIComponent(userId) + "/filter",
|
||||||
).check(function(req) {
|
).check(function(req) {
|
||||||
expect(req.data).toEqual(filterDefinition);
|
expect(req.data).toEqual(filterDefinition);
|
||||||
@@ -258,13 +279,13 @@ describe("MatrixClient", function() {
|
|||||||
filter_id: filterId,
|
filter_id: filterId,
|
||||||
});
|
});
|
||||||
|
|
||||||
client.createFilter(filterDefinition).then(function(gotFilter) {
|
client!.createFilter(filterDefinition).then(function(gotFilter) {
|
||||||
expect(gotFilter.getDefinition()).toEqual(filterDefinition);
|
expect(gotFilter.getDefinition()).toEqual(filterDefinition);
|
||||||
expect(store.getFilter(userId, filterId)).toEqual(gotFilter);
|
expect(store!.getFilter(userId, filterId)).toEqual(gotFilter);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
httpBackend.flush();
|
httpBackend!.flush('');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -291,10 +312,10 @@ describe("MatrixClient", function() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
client.searchMessageText({
|
client!.searchMessageText({
|
||||||
query: "monkeys",
|
query: "monkeys",
|
||||||
});
|
});
|
||||||
httpBackend.when("POST", "/search").check(function(req) {
|
httpBackend!.when("POST", "/search").check(function(req) {
|
||||||
expect(req.data).toEqual({
|
expect(req.data).toEqual({
|
||||||
search_categories: {
|
search_categories: {
|
||||||
room_events: {
|
room_events: {
|
||||||
@@ -304,7 +325,7 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
}).respond(200, response);
|
}).respond(200, response);
|
||||||
|
|
||||||
return httpBackend.flush();
|
return httpBackend!.flush('');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("should filter out context from different timelines (threads)", () => {
|
describe("should filter out context from different timelines (threads)", () => {
|
||||||
@@ -313,11 +334,14 @@ describe("MatrixClient", function() {
|
|||||||
search_categories: {
|
search_categories: {
|
||||||
room_events: {
|
room_events: {
|
||||||
count: 24,
|
count: 24,
|
||||||
|
highlights: [],
|
||||||
results: [{
|
results: [{
|
||||||
rank: 0.1,
|
rank: 0.1,
|
||||||
result: {
|
result: {
|
||||||
event_id: "$flibble:localhost",
|
event_id: "$flibble:localhost",
|
||||||
type: "m.room.message",
|
type: "m.room.message",
|
||||||
|
sender: '@test:locahost',
|
||||||
|
origin_server_ts: 123,
|
||||||
user_id: "@alice:localhost",
|
user_id: "@alice:localhost",
|
||||||
room_id: "!feuiwhf:localhost",
|
room_id: "!feuiwhf:localhost",
|
||||||
content: {
|
content: {
|
||||||
@@ -326,9 +350,12 @@ describe("MatrixClient", function() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
|
profile_info: {},
|
||||||
events_after: [{
|
events_after: [{
|
||||||
event_id: "$ev-after:server",
|
event_id: "$ev-after:server",
|
||||||
type: "m.room.message",
|
type: "m.room.message",
|
||||||
|
sender: '@test:locahost',
|
||||||
|
origin_server_ts: 123,
|
||||||
user_id: "@alice:localhost",
|
user_id: "@alice:localhost",
|
||||||
room_id: "!feuiwhf:localhost",
|
room_id: "!feuiwhf:localhost",
|
||||||
content: {
|
content: {
|
||||||
@@ -343,6 +370,8 @@ describe("MatrixClient", function() {
|
|||||||
events_before: [{
|
events_before: [{
|
||||||
event_id: "$ev-before:server",
|
event_id: "$ev-before:server",
|
||||||
type: "m.room.message",
|
type: "m.room.message",
|
||||||
|
sender: '@test:locahost',
|
||||||
|
origin_server_ts: 123,
|
||||||
user_id: "@alice:localhost",
|
user_id: "@alice:localhost",
|
||||||
room_id: "!feuiwhf:localhost",
|
room_id: "!feuiwhf:localhost",
|
||||||
content: {
|
content: {
|
||||||
@@ -356,15 +385,17 @@ describe("MatrixClient", function() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = {
|
const data: ISearchResults = {
|
||||||
results: [],
|
results: [],
|
||||||
highlights: [],
|
highlights: [],
|
||||||
};
|
};
|
||||||
client.processRoomEventsSearch(data, response);
|
client!.processRoomEventsSearch(data, response);
|
||||||
|
|
||||||
expect(data.results).toHaveLength(1);
|
expect(data.results).toHaveLength(1);
|
||||||
expect(data.results[0].context.timeline).toHaveLength(2);
|
expect(data.results[0].context.getTimeline()).toHaveLength(2);
|
||||||
expect(data.results[0].context.timeline.find(e => e.getId() === "$ev-after:server")).toBeFalsy();
|
expect(
|
||||||
|
data.results[0].context.getTimeline().find(e => e.getId() === "$ev-after:server"),
|
||||||
|
).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("filters out thread replies from threads other than the thread the result replied to", () => {
|
it("filters out thread replies from threads other than the thread the result replied to", () => {
|
||||||
@@ -372,11 +403,14 @@ describe("MatrixClient", function() {
|
|||||||
search_categories: {
|
search_categories: {
|
||||||
room_events: {
|
room_events: {
|
||||||
count: 24,
|
count: 24,
|
||||||
|
highlights: [],
|
||||||
results: [{
|
results: [{
|
||||||
rank: 0.1,
|
rank: 0.1,
|
||||||
result: {
|
result: {
|
||||||
event_id: "$flibble:localhost",
|
event_id: "$flibble:localhost",
|
||||||
type: "m.room.message",
|
type: "m.room.message",
|
||||||
|
sender: '@test:locahost',
|
||||||
|
origin_server_ts: 123,
|
||||||
user_id: "@alice:localhost",
|
user_id: "@alice:localhost",
|
||||||
room_id: "!feuiwhf:localhost",
|
room_id: "!feuiwhf:localhost",
|
||||||
content: {
|
content: {
|
||||||
@@ -389,9 +423,12 @@ describe("MatrixClient", function() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
|
profile_info: {},
|
||||||
events_after: [{
|
events_after: [{
|
||||||
event_id: "$ev-after:server",
|
event_id: "$ev-after:server",
|
||||||
type: "m.room.message",
|
type: "m.room.message",
|
||||||
|
sender: '@test:locahost',
|
||||||
|
origin_server_ts: 123,
|
||||||
user_id: "@alice:localhost",
|
user_id: "@alice:localhost",
|
||||||
room_id: "!feuiwhf:localhost",
|
room_id: "!feuiwhf:localhost",
|
||||||
content: {
|
content: {
|
||||||
@@ -410,15 +447,17 @@ describe("MatrixClient", function() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = {
|
const data: ISearchResults = {
|
||||||
results: [],
|
results: [],
|
||||||
highlights: [],
|
highlights: [],
|
||||||
};
|
};
|
||||||
client.processRoomEventsSearch(data, response);
|
client!.processRoomEventsSearch(data, response);
|
||||||
|
|
||||||
expect(data.results).toHaveLength(1);
|
expect(data.results).toHaveLength(1);
|
||||||
expect(data.results[0].context.timeline).toHaveLength(1);
|
expect(data.results[0].context.getTimeline()).toHaveLength(1);
|
||||||
expect(data.results[0].context.timeline.find(e => e.getId() === "$flibble:localhost")).toBeTruthy();
|
expect(
|
||||||
|
data.results[0].context.getTimeline().find(e => e.getId() === "$flibble:localhost"),
|
||||||
|
).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("filters out main timeline events when result is a thread reply", () => {
|
it("filters out main timeline events when result is a thread reply", () => {
|
||||||
@@ -426,10 +465,13 @@ describe("MatrixClient", function() {
|
|||||||
search_categories: {
|
search_categories: {
|
||||||
room_events: {
|
room_events: {
|
||||||
count: 24,
|
count: 24,
|
||||||
|
highlights: [],
|
||||||
results: [{
|
results: [{
|
||||||
rank: 0.1,
|
rank: 0.1,
|
||||||
result: {
|
result: {
|
||||||
event_id: "$flibble:localhost",
|
event_id: "$flibble:localhost",
|
||||||
|
sender: '@test:locahost',
|
||||||
|
origin_server_ts: 123,
|
||||||
type: "m.room.message",
|
type: "m.room.message",
|
||||||
user_id: "@alice:localhost",
|
user_id: "@alice:localhost",
|
||||||
room_id: "!feuiwhf:localhost",
|
room_id: "!feuiwhf:localhost",
|
||||||
@@ -445,6 +487,8 @@ describe("MatrixClient", function() {
|
|||||||
context: {
|
context: {
|
||||||
events_after: [{
|
events_after: [{
|
||||||
event_id: "$ev-after:server",
|
event_id: "$ev-after:server",
|
||||||
|
sender: '@test:locahost',
|
||||||
|
origin_server_ts: 123,
|
||||||
type: "m.room.message",
|
type: "m.room.message",
|
||||||
user_id: "@alice:localhost",
|
user_id: "@alice:localhost",
|
||||||
room_id: "!feuiwhf:localhost",
|
room_id: "!feuiwhf:localhost",
|
||||||
@@ -454,21 +498,24 @@ describe("MatrixClient", function() {
|
|||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
events_before: [],
|
events_before: [],
|
||||||
|
profile_info: {},
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = {
|
const data: ISearchResults = {
|
||||||
results: [],
|
results: [],
|
||||||
highlights: [],
|
highlights: [],
|
||||||
};
|
};
|
||||||
client.processRoomEventsSearch(data, response);
|
client!.processRoomEventsSearch(data, response);
|
||||||
|
|
||||||
expect(data.results).toHaveLength(1);
|
expect(data.results).toHaveLength(1);
|
||||||
expect(data.results[0].context.timeline).toHaveLength(1);
|
expect(data.results[0].context.getTimeline()).toHaveLength(1);
|
||||||
expect(data.results[0].context.timeline.find(e => e.getId() === "$flibble:localhost")).toBeTruthy();
|
expect(
|
||||||
|
data.results[0].context.getTimeline().find(e => e.getId() === "$flibble:localhost"),
|
||||||
|
).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -479,16 +526,16 @@ describe("MatrixClient", function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
return client.initCrypto();
|
return client!.initCrypto();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
client.stopClient();
|
client!.stopClient();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should do an HTTP request and then store the keys", function() {
|
it("should do an HTTP request and then store the keys", function() {
|
||||||
const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
|
const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
|
||||||
// ed25519key = client.getDeviceEd25519Key();
|
// ed25519key = client!.getDeviceEd25519Key();
|
||||||
const borisKeys = {
|
const borisKeys = {
|
||||||
dev1: {
|
dev1: {
|
||||||
algorithms: ["1"],
|
algorithms: ["1"],
|
||||||
@@ -528,7 +575,7 @@ describe("MatrixClient", function() {
|
|||||||
var b = JSON.parse(JSON.stringify(o));
|
var b = JSON.parse(JSON.stringify(o));
|
||||||
delete(b.signatures);
|
delete(b.signatures);
|
||||||
delete(b.unsigned);
|
delete(b.unsigned);
|
||||||
return client.crypto.olmDevice.sign(anotherjson.stringify(b));
|
return client!.crypto.olmDevice.sign(anotherjson.stringify(b));
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.log("Ed25519: " + ed25519key);
|
logger.log("Ed25519: " + ed25519key);
|
||||||
@@ -536,7 +583,7 @@ describe("MatrixClient", function() {
|
|||||||
logger.log("chaz:", sign(chazKeys.dev2));
|
logger.log("chaz:", sign(chazKeys.dev2));
|
||||||
*/
|
*/
|
||||||
|
|
||||||
httpBackend.when("POST", "/keys/query").check(function(req) {
|
httpBackend!.when("POST", "/keys/query").check(function(req) {
|
||||||
expect(req.data).toEqual({ device_keys: {
|
expect(req.data).toEqual({ device_keys: {
|
||||||
'boris': [],
|
'boris': [],
|
||||||
'chaz': [],
|
'chaz': [],
|
||||||
@@ -548,7 +595,7 @@ describe("MatrixClient", function() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const prom = client.downloadKeys(["boris", "chaz"]).then(function(res) {
|
const prom = client!.downloadKeys(["boris", "chaz"]).then(function(res) {
|
||||||
assertObjectContains(res.boris.dev1, {
|
assertObjectContains(res.boris.dev1, {
|
||||||
verified: 0, // DeviceVerification.UNVERIFIED
|
verified: 0, // DeviceVerification.UNVERIFIED
|
||||||
keys: { "ed25519:dev1": ed25519key },
|
keys: { "ed25519:dev1": ed25519key },
|
||||||
@@ -564,23 +611,23 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
httpBackend.flush();
|
httpBackend!.flush('');
|
||||||
return prom;
|
return prom;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("deleteDevice", function() {
|
describe("deleteDevice", function() {
|
||||||
const auth = { a: 1 };
|
const auth = { identifier: 1 };
|
||||||
it("should pass through an auth dict", function() {
|
it("should pass through an auth dict", function() {
|
||||||
httpBackend.when(
|
httpBackend!.when(
|
||||||
"DELETE", "/_matrix/client/r0/devices/my_device",
|
"DELETE", "/_matrix/client/r0/devices/my_device",
|
||||||
).check(function(req) {
|
).check(function(req) {
|
||||||
expect(req.data).toEqual({ auth: auth });
|
expect(req.data).toEqual({ auth: auth });
|
||||||
}).respond(200);
|
}).respond(200);
|
||||||
|
|
||||||
const prom = client.deleteDevice("my_device", auth);
|
const prom = client!.deleteDevice("my_device", auth);
|
||||||
|
|
||||||
httpBackend.flush();
|
httpBackend!.flush('');
|
||||||
return prom;
|
return prom;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -588,7 +635,7 @@ describe("MatrixClient", function() {
|
|||||||
describe("partitionThreadedEvents", function() {
|
describe("partitionThreadedEvents", function() {
|
||||||
let room;
|
let room;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client, userId);
|
room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client!, userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns empty arrays when given an empty arrays", function() {
|
it("returns empty arrays when given an empty arrays", function() {
|
||||||
@@ -599,7 +646,11 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("copies pre-thread in-timeline vote events onto both timelines", function() {
|
it("copies pre-thread in-timeline vote events onto both timelines", function() {
|
||||||
client.clientOpts = { experimentalThreadSupport: true };
|
// @ts-ignore setting private property
|
||||||
|
client!.clientOpts = {
|
||||||
|
...defaultClientOpts,
|
||||||
|
experimentalThreadSupport: true,
|
||||||
|
};
|
||||||
|
|
||||||
const eventPollResponseReference = buildEventPollResponseReference();
|
const eventPollResponseReference = buildEventPollResponseReference();
|
||||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||||
@@ -611,6 +662,7 @@ describe("MatrixClient", function() {
|
|||||||
eventPollResponseReference,
|
eventPollResponseReference,
|
||||||
];
|
];
|
||||||
// Vote has no threadId yet
|
// Vote has no threadId yet
|
||||||
|
// @ts-ignore private property
|
||||||
expect(eventPollResponseReference.threadId).toBeFalsy();
|
expect(eventPollResponseReference.threadId).toBeFalsy();
|
||||||
|
|
||||||
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||||
@@ -634,7 +686,11 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("copies pre-thread in-timeline reactions onto both timelines", function() {
|
it("copies pre-thread in-timeline reactions onto both timelines", function() {
|
||||||
client.clientOpts = { experimentalThreadSupport: true };
|
// @ts-ignore setting private property
|
||||||
|
client!.clientOpts = {
|
||||||
|
...defaultClientOpts,
|
||||||
|
experimentalThreadSupport: true,
|
||||||
|
};
|
||||||
|
|
||||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||||
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
|
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
|
||||||
@@ -661,7 +717,11 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("copies post-thread in-timeline vote events onto both timelines", function() {
|
it("copies post-thread in-timeline vote events onto both timelines", function() {
|
||||||
client.clientOpts = { experimentalThreadSupport: true };
|
// @ts-ignore setting private property
|
||||||
|
client!.clientOpts = {
|
||||||
|
...defaultClientOpts,
|
||||||
|
experimentalThreadSupport: true,
|
||||||
|
};
|
||||||
|
|
||||||
const eventPollResponseReference = buildEventPollResponseReference();
|
const eventPollResponseReference = buildEventPollResponseReference();
|
||||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||||
@@ -688,7 +748,11 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("copies post-thread in-timeline reactions onto both timelines", function() {
|
it("copies post-thread in-timeline reactions onto both timelines", function() {
|
||||||
client.clientOpts = { experimentalThreadSupport: true };
|
// @ts-ignore setting private property
|
||||||
|
client!.clientOpts = {
|
||||||
|
...defaultClientOpts,
|
||||||
|
experimentalThreadSupport: true,
|
||||||
|
};
|
||||||
|
|
||||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||||
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
|
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
|
||||||
@@ -715,7 +779,11 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("sends room state events to the main timeline only", function() {
|
it("sends room state events to the main timeline only", function() {
|
||||||
client.clientOpts = { experimentalThreadSupport: true };
|
// @ts-ignore setting private property
|
||||||
|
client!.clientOpts = {
|
||||||
|
...defaultClientOpts,
|
||||||
|
experimentalThreadSupport: true,
|
||||||
|
};
|
||||||
// This is based on recording the events in a real room:
|
// This is based on recording the events in a real room:
|
||||||
|
|
||||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||||
@@ -768,7 +836,11 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("sends redactions of reactions to thread responses to thread timeline only", () => {
|
it("sends redactions of reactions to thread responses to thread timeline only", () => {
|
||||||
client.clientOpts = { experimentalThreadSupport: true };
|
// @ts-ignore setting private property
|
||||||
|
client!.clientOpts = {
|
||||||
|
...defaultClientOpts,
|
||||||
|
experimentalThreadSupport: true,
|
||||||
|
};
|
||||||
|
|
||||||
const threadRootEvent = buildEventPollStartThreadRoot();
|
const threadRootEvent = buildEventPollStartThreadRoot();
|
||||||
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
|
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
|
||||||
@@ -797,7 +869,11 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("sends reply to reply to thread root outside of thread to main timeline only", () => {
|
it("sends reply to reply to thread root outside of thread to main timeline only", () => {
|
||||||
client.clientOpts = { experimentalThreadSupport: true };
|
// @ts-ignore setting private property
|
||||||
|
client!.clientOpts = {
|
||||||
|
...defaultClientOpts,
|
||||||
|
experimentalThreadSupport: true,
|
||||||
|
};
|
||||||
|
|
||||||
const threadRootEvent = buildEventPollStartThreadRoot();
|
const threadRootEvent = buildEventPollStartThreadRoot();
|
||||||
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
|
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
|
||||||
@@ -826,7 +902,11 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("sends reply to thread responses to main timeline only", () => {
|
it("sends reply to thread responses to main timeline only", () => {
|
||||||
client.clientOpts = { experimentalThreadSupport: true };
|
// @ts-ignore setting private property
|
||||||
|
client!.clientOpts = {
|
||||||
|
...defaultClientOpts,
|
||||||
|
experimentalThreadSupport: true,
|
||||||
|
};
|
||||||
|
|
||||||
const threadRootEvent = buildEventPollStartThreadRoot();
|
const threadRootEvent = buildEventPollStartThreadRoot();
|
||||||
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
|
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
|
||||||
@@ -860,9 +940,9 @@ describe("MatrixClient", function() {
|
|||||||
fields: {},
|
fields: {},
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const prom = client.getThirdpartyUser("irc", {});
|
const prom = client!.getThirdpartyUser("irc", {});
|
||||||
httpBackend.when("GET", "/thirdparty/user/irc").respond(200, response);
|
httpBackend!.when("GET", "/thirdparty/user/irc").respond(200, response);
|
||||||
await httpBackend.flush();
|
await httpBackend!.flush('');
|
||||||
expect(await prom).toStrictEqual(response);
|
expect(await prom).toStrictEqual(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -875,9 +955,9 @@ describe("MatrixClient", function() {
|
|||||||
fields: {},
|
fields: {},
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const prom = client.getThirdpartyLocation("irc", {});
|
const prom = client!.getThirdpartyLocation("irc", {});
|
||||||
httpBackend.when("GET", "/thirdparty/location/irc").respond(200, response);
|
httpBackend!.when("GET", "/thirdparty/location/irc").respond(200, response);
|
||||||
await httpBackend.flush();
|
await httpBackend!.flush('');
|
||||||
expect(await prom).toStrictEqual(response);
|
expect(await prom).toStrictEqual(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -888,9 +968,10 @@ describe("MatrixClient", function() {
|
|||||||
pushers: [],
|
pushers: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const prom = client.getPushers();
|
const prom = client!.getPushers();
|
||||||
httpBackend.when("GET", "/pushers").respond(200, response);
|
httpBackend!.when("GET", "/_matrix/client/versions").respond(200, {});
|
||||||
await httpBackend.flush();
|
httpBackend!.when("GET", "/pushers").respond(200, response);
|
||||||
|
await httpBackend!.flush('');
|
||||||
expect(await prom).toStrictEqual(response);
|
expect(await prom).toStrictEqual(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -902,12 +983,12 @@ describe("MatrixClient", function() {
|
|||||||
left: [],
|
left: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const prom = client.getKeyChanges("old", "new");
|
const prom = client!.getKeyChanges("old", "new");
|
||||||
httpBackend.when("GET", "/keys/changes").check((req) => {
|
httpBackend!.when("GET", "/keys/changes").check((req) => {
|
||||||
expect(req.queryParams.from).toEqual("old");
|
expect(req.queryParams?.from).toEqual("old");
|
||||||
expect(req.queryParams.to).toEqual("new");
|
expect(req.queryParams?.to).toEqual("new");
|
||||||
}).respond(200, response);
|
}).respond(200, response);
|
||||||
await httpBackend.flush();
|
await httpBackend!.flush('');
|
||||||
expect(await prom).toStrictEqual(response);
|
expect(await prom).toStrictEqual(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -918,9 +999,9 @@ describe("MatrixClient", function() {
|
|||||||
devices: [],
|
devices: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const prom = client.getDevices();
|
const prom = client!.getDevices();
|
||||||
httpBackend.when("GET", "/devices").respond(200, response);
|
httpBackend!.when("GET", "/devices").respond(200, response);
|
||||||
await httpBackend.flush();
|
await httpBackend!.flush('');
|
||||||
expect(await prom).toStrictEqual(response);
|
expect(await prom).toStrictEqual(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -934,9 +1015,9 @@ describe("MatrixClient", function() {
|
|||||||
last_seen_ts: 1,
|
last_seen_ts: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const prom = client.getDevice("DEADBEEF");
|
const prom = client!.getDevice("DEADBEEF");
|
||||||
httpBackend.when("GET", "/devices/DEADBEEF").respond(200, response);
|
httpBackend!.when("GET", "/devices/DEADBEEF").respond(200, response);
|
||||||
await httpBackend.flush();
|
await httpBackend!.flush('');
|
||||||
expect(await prom).toStrictEqual(response);
|
expect(await prom).toStrictEqual(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -947,9 +1028,9 @@ describe("MatrixClient", function() {
|
|||||||
threepids: [],
|
threepids: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const prom = client.getThreePids();
|
const prom = client!.getThreePids();
|
||||||
httpBackend.when("GET", "/account/3pid").respond(200, response);
|
httpBackend!.when("GET", "/account/3pid").respond(200, response);
|
||||||
await httpBackend.flush();
|
await httpBackend!.flush('');
|
||||||
expect(await prom).toStrictEqual(response);
|
expect(await prom).toStrictEqual(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -957,9 +1038,9 @@ describe("MatrixClient", function() {
|
|||||||
describe("deleteAlias", () => {
|
describe("deleteAlias", () => {
|
||||||
it("should hit the expected API endpoint", async () => {
|
it("should hit the expected API endpoint", async () => {
|
||||||
const response = {};
|
const response = {};
|
||||||
const prom = client.deleteAlias("#foo:bar");
|
const prom = client!.deleteAlias("#foo:bar");
|
||||||
httpBackend.when("DELETE", "/directory/room/" + encodeURIComponent("#foo:bar")).respond(200, response);
|
httpBackend!.when("DELETE", "/directory/room/" + encodeURIComponent("#foo:bar")).respond(200, response);
|
||||||
await httpBackend.flush();
|
await httpBackend!.flush('');
|
||||||
expect(await prom).toStrictEqual(response);
|
expect(await prom).toStrictEqual(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -967,10 +1048,10 @@ describe("MatrixClient", function() {
|
|||||||
describe("deleteRoomTag", () => {
|
describe("deleteRoomTag", () => {
|
||||||
it("should hit the expected API endpoint", async () => {
|
it("should hit the expected API endpoint", async () => {
|
||||||
const response = {};
|
const response = {};
|
||||||
const prom = client.deleteRoomTag("!roomId:server", "u.tag");
|
const prom = client!.deleteRoomTag("!roomId:server", "u.tag");
|
||||||
const url = `/user/${encodeURIComponent(userId)}/rooms/${encodeURIComponent("!roomId:server")}/tags/u.tag`;
|
const url = `/user/${encodeURIComponent(userId)}/rooms/${encodeURIComponent("!roomId:server")}/tags/u.tag`;
|
||||||
httpBackend.when("DELETE", url).respond(200, response);
|
httpBackend!.when("DELETE", url).respond(200, response);
|
||||||
await httpBackend.flush();
|
await httpBackend!.flush('');
|
||||||
expect(await prom).toStrictEqual(response);
|
expect(await prom).toStrictEqual(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -985,10 +1066,10 @@ describe("MatrixClient", function() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const prom = client.getRoomTags("!roomId:server");
|
const prom = client!.getRoomTags("!roomId:server");
|
||||||
const url = `/user/${encodeURIComponent(userId)}/rooms/${encodeURIComponent("!roomId:server")}/tags`;
|
const url = `/user/${encodeURIComponent(userId)}/rooms/${encodeURIComponent("!roomId:server")}/tags`;
|
||||||
httpBackend.when("GET", url).respond(200, response);
|
httpBackend!.when("GET", url).respond(200, response);
|
||||||
await httpBackend.flush();
|
await httpBackend!.flush('');
|
||||||
expect(await prom).toStrictEqual(response);
|
expect(await prom).toStrictEqual(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1000,19 +1081,19 @@ describe("MatrixClient", function() {
|
|||||||
submit_url: "https://foobar.matrix/_matrix/matrix",
|
submit_url: "https://foobar.matrix/_matrix/matrix",
|
||||||
};
|
};
|
||||||
|
|
||||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
httpBackend!.when("GET", "/_matrix/client/versions").respond(200, {
|
||||||
versions: ["r0.6.0"],
|
versions: ["r0.6.0"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const prom = client.requestRegisterEmailToken("bob@email", "secret", 1);
|
const prom = client!.requestRegisterEmailToken("bob@email", "secret", 1);
|
||||||
httpBackend.when("POST", "/register/email/requestToken").check(req => {
|
httpBackend!.when("POST", "/register/email/requestToken").check(req => {
|
||||||
expect(req.data).toStrictEqual({
|
expect(req.data).toStrictEqual({
|
||||||
email: "bob@email",
|
email: "bob@email",
|
||||||
client_secret: "secret",
|
client_secret: "secret",
|
||||||
send_attempt: 1,
|
send_attempt: 1,
|
||||||
});
|
});
|
||||||
}).respond(200, response);
|
}).respond(200, response);
|
||||||
await httpBackend.flush();
|
await httpBackend!.flush('');
|
||||||
expect(await prom).toStrictEqual(response);
|
expect(await prom).toStrictEqual(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1021,11 +1102,11 @@ describe("MatrixClient", function() {
|
|||||||
it("should supply an id_access_token", async () => {
|
it("should supply an id_access_token", async () => {
|
||||||
const targetEmail = "gerald@example.org";
|
const targetEmail = "gerald@example.org";
|
||||||
|
|
||||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
httpBackend!.when("GET", "/_matrix/client/versions").respond(200, {
|
||||||
versions: ["r0.6.0"],
|
versions: ["r0.6.0"],
|
||||||
});
|
});
|
||||||
|
|
||||||
httpBackend.when("POST", "/invite").check(req => {
|
httpBackend!.when("POST", "/invite").check(req => {
|
||||||
expect(req.data).toStrictEqual({
|
expect(req.data).toStrictEqual({
|
||||||
id_server: idServerDomain,
|
id_server: idServerDomain,
|
||||||
id_access_token: identityAccessToken,
|
id_access_token: identityAccessToken,
|
||||||
@@ -1034,8 +1115,8 @@ describe("MatrixClient", function() {
|
|||||||
});
|
});
|
||||||
}).respond(200, {});
|
}).respond(200, {});
|
||||||
|
|
||||||
const prom = client.inviteByThreePid("!room:example.org", "email", targetEmail);
|
const prom = client!.inviteByThreePid("!room:example.org", "email", targetEmail);
|
||||||
await httpBackend.flush();
|
await httpBackend!.flush('');
|
||||||
await prom; // returns empty object, so no validation needed
|
await prom; // returns empty object, so no validation needed
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1055,11 +1136,11 @@ describe("MatrixClient", function() {
|
|||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
httpBackend!.when("GET", "/_matrix/client/versions").respond(200, {
|
||||||
versions: ["r0.6.0"],
|
versions: ["r0.6.0"],
|
||||||
});
|
});
|
||||||
|
|
||||||
httpBackend.when("POST", "/createRoom").check(req => {
|
httpBackend!.when("POST", "/createRoom").check(req => {
|
||||||
expect(req.data).toMatchObject({
|
expect(req.data).toMatchObject({
|
||||||
invite_3pid: expect.arrayContaining([{
|
invite_3pid: expect.arrayContaining([{
|
||||||
...input.invite_3pid[0],
|
...input.invite_3pid[0],
|
||||||
@@ -1069,8 +1150,31 @@ describe("MatrixClient", function() {
|
|||||||
expect(req.data.invite_3pid.length).toBe(1);
|
expect(req.data.invite_3pid.length).toBe(1);
|
||||||
}).respond(200, response);
|
}).respond(200, response);
|
||||||
|
|
||||||
const prom = client.createRoom(input);
|
const prom = client!.createRoom(input);
|
||||||
await httpBackend.flush();
|
await httpBackend!.flush('');
|
||||||
|
expect(await prom).toStrictEqual(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("requestLoginToken", () => {
|
||||||
|
it("should hit the expected API endpoint with UIA", async () => {
|
||||||
|
const response = {};
|
||||||
|
const uiaData = {};
|
||||||
|
const prom = client!.requestLoginToken(uiaData);
|
||||||
|
httpBackend!
|
||||||
|
.when("POST", "/unstable/org.matrix.msc3882/login/token", { auth: uiaData })
|
||||||
|
.respond(200, response);
|
||||||
|
await httpBackend!.flush('');
|
||||||
|
expect(await prom).toStrictEqual(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should hit the expected API endpoint without UIA", async () => {
|
||||||
|
const response = {};
|
||||||
|
const prom = client!.requestLoginToken();
|
||||||
|
httpBackend!
|
||||||
|
.when("POST", "/unstable/org.matrix.msc3882/login/token", {})
|
||||||
|
.respond(200, response);
|
||||||
|
await httpBackend!.flush('');
|
||||||
expect(await prom).toStrictEqual(response);
|
expect(await prom).toStrictEqual(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -5,10 +5,12 @@ import { MatrixClient } from "../../src/matrix";
|
|||||||
import { MatrixScheduler } from "../../src/scheduler";
|
import { MatrixScheduler } from "../../src/scheduler";
|
||||||
import { MemoryStore } from "../../src/store/memory";
|
import { MemoryStore } from "../../src/store/memory";
|
||||||
import { MatrixError } from "../../src/http-api";
|
import { MatrixError } from "../../src/http-api";
|
||||||
|
import { ICreateClientOpts } from "../../src/client";
|
||||||
|
import { IStore } from "../../src/store";
|
||||||
|
|
||||||
describe("MatrixClient opts", function() {
|
describe("MatrixClient opts", function() {
|
||||||
const baseUrl = "http://localhost.or.something";
|
const baseUrl = "http://localhost.or.something";
|
||||||
let httpBackend = null;
|
let httpBackend = new HttpBackend();
|
||||||
const userId = "@alice:localhost";
|
const userId = "@alice:localhost";
|
||||||
const userB = "@bob:localhost";
|
const userB = "@bob:localhost";
|
||||||
const accessToken = "aseukfgwef";
|
const accessToken = "aseukfgwef";
|
||||||
@@ -67,7 +69,7 @@ describe("MatrixClient opts", function() {
|
|||||||
let client;
|
let client;
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
client = new MatrixClient({
|
client = new MatrixClient({
|
||||||
request: httpBackend.requestFn,
|
request: httpBackend.requestFn as unknown as ICreateClientOpts['request'],
|
||||||
store: undefined,
|
store: undefined,
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
@@ -99,7 +101,7 @@ describe("MatrixClient opts", function() {
|
|||||||
];
|
];
|
||||||
client.on("event", function(event) {
|
client.on("event", function(event) {
|
||||||
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
|
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
|
||||||
-1, "Recv unexpected event type: " + event.getType(),
|
-1,
|
||||||
);
|
);
|
||||||
expectedEventTypes.splice(
|
expectedEventTypes.splice(
|
||||||
expectedEventTypes.indexOf(event.getType()), 1,
|
expectedEventTypes.indexOf(event.getType()), 1,
|
||||||
@@ -118,7 +120,7 @@ describe("MatrixClient opts", function() {
|
|||||||
utils.syncPromise(client),
|
utils.syncPromise(client),
|
||||||
]);
|
]);
|
||||||
expect(expectedEventTypes.length).toEqual(
|
expect(expectedEventTypes.length).toEqual(
|
||||||
0, "Expected to see event types: " + expectedEventTypes,
|
0,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -127,8 +129,8 @@ describe("MatrixClient opts", function() {
|
|||||||
let client;
|
let client;
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
client = new MatrixClient({
|
client = new MatrixClient({
|
||||||
request: httpBackend.requestFn,
|
request: httpBackend.requestFn as unknown as ICreateClientOpts['request'],
|
||||||
store: new MemoryStore(),
|
store: new MemoryStore() as IStore,
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
accessToken: accessToken,
|
accessToken: accessToken,
|
||||||
@@ -146,7 +148,7 @@ describe("MatrixClient opts", function() {
|
|||||||
error: "Ruh roh",
|
error: "Ruh roh",
|
||||||
}));
|
}));
|
||||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
|
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
|
||||||
expect(false).toBe(true, "sendTextMessage resolved but shouldn't");
|
expect(false).toBe(true);
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
expect(err.errcode).toEqual("M_SOMETHING");
|
expect(err.errcode).toEqual("M_SOMETHING");
|
||||||
done();
|
done();
|
||||||
127
spec/integ/matrix-client-relations.spec.ts
Normal file
127
spec/integ/matrix-client-relations.spec.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 Dominik Henneke
|
||||||
|
Copyright 2022 Nordeck IT + Consulting GmbH.
|
||||||
|
|
||||||
|
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 HttpBackend from "matrix-mock-request";
|
||||||
|
|
||||||
|
import { Direction, MatrixClient, MatrixScheduler } from "../../src/matrix";
|
||||||
|
import { TestClient } from "../TestClient";
|
||||||
|
|
||||||
|
describe("MatrixClient relations", () => {
|
||||||
|
const userId = "@alice:localhost";
|
||||||
|
const accessToken = "aseukfgwef";
|
||||||
|
const roomId = "!room:here";
|
||||||
|
let client: MatrixClient | undefined;
|
||||||
|
let httpBackend: HttpBackend | undefined;
|
||||||
|
|
||||||
|
const setupTests = (): [MatrixClient, HttpBackend] => {
|
||||||
|
const scheduler = new MatrixScheduler();
|
||||||
|
const testClient = new TestClient(
|
||||||
|
userId,
|
||||||
|
"DEVICE",
|
||||||
|
accessToken,
|
||||||
|
undefined,
|
||||||
|
{ scheduler },
|
||||||
|
);
|
||||||
|
const httpBackend = testClient.httpBackend;
|
||||||
|
const client = testClient.client;
|
||||||
|
|
||||||
|
return [client, httpBackend];
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
[client, httpBackend] = setupTests();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
httpBackend!.verifyNoOutstandingExpectation();
|
||||||
|
return httpBackend!.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should read related events with the default options", async () => {
|
||||||
|
const response = client!.relations(roomId, '$event-0', null, null);
|
||||||
|
|
||||||
|
httpBackend!
|
||||||
|
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0?dir=b")
|
||||||
|
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||||
|
|
||||||
|
await httpBackend!.flushAllExpected();
|
||||||
|
|
||||||
|
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should read related events with relation type", async () => {
|
||||||
|
const response = client!.relations(roomId, '$event-0', 'm.reference', null);
|
||||||
|
|
||||||
|
httpBackend!
|
||||||
|
.when("GET", "/rooms/!room%3Ahere/relations/%24event-0/m.reference?dir=b")
|
||||||
|
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||||
|
|
||||||
|
await httpBackend!.flushAllExpected();
|
||||||
|
|
||||||
|
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should read related events with relation type and event type", async () => {
|
||||||
|
const response = client!.relations(roomId, '$event-0', 'm.reference', 'm.room.message');
|
||||||
|
|
||||||
|
httpBackend!
|
||||||
|
.when(
|
||||||
|
"GET",
|
||||||
|
"/rooms/!room%3Ahere/relations/%24event-0/m.reference/m.room.message?dir=b",
|
||||||
|
)
|
||||||
|
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||||
|
|
||||||
|
await httpBackend!.flushAllExpected();
|
||||||
|
|
||||||
|
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should read related events with custom options", async () => {
|
||||||
|
const response = client!.relations(roomId, '$event-0', null, null, {
|
||||||
|
dir: Direction.Forward,
|
||||||
|
from: 'FROM',
|
||||||
|
limit: 10,
|
||||||
|
to: 'TO',
|
||||||
|
});
|
||||||
|
|
||||||
|
httpBackend!
|
||||||
|
.when(
|
||||||
|
"GET",
|
||||||
|
"/rooms/!room%3Ahere/relations/%24event-0?dir=f&from=FROM&limit=10&to=TO",
|
||||||
|
)
|
||||||
|
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||||
|
|
||||||
|
await httpBackend!.flushAllExpected();
|
||||||
|
|
||||||
|
expect(await response).toEqual({ "events": [], "nextBatch": "NEXT" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default direction in the fetchRelations endpoint', async () => {
|
||||||
|
const response = client!.fetchRelations(roomId, '$event-0', null, null);
|
||||||
|
|
||||||
|
httpBackend!
|
||||||
|
.when(
|
||||||
|
"GET",
|
||||||
|
"/rooms/!room%3Ahere/relations/%24event-0?dir=b",
|
||||||
|
)
|
||||||
|
.respond(200, { chunk: [], next_batch: 'NEXT' });
|
||||||
|
|
||||||
|
await httpBackend!.flushAllExpected();
|
||||||
|
|
||||||
|
expect(await response).toEqual({ "chunk": [], "next_batch": "NEXT" });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventStatus, RoomEvent, MatrixClient } from "../../src/matrix";
|
import HttpBackend from "matrix-mock-request";
|
||||||
import { MatrixScheduler } from "../../src/scheduler";
|
|
||||||
|
import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src/matrix";
|
||||||
import { Room } from "../../src/models/room";
|
import { Room } from "../../src/models/room";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
|
|
||||||
describe("MatrixClient retrying", function() {
|
describe("MatrixClient retrying", function() {
|
||||||
let client: MatrixClient = null;
|
|
||||||
let httpBackend: TestClient["httpBackend"] = null;
|
|
||||||
let scheduler;
|
|
||||||
const userId = "@alice:localhost";
|
const userId = "@alice:localhost";
|
||||||
const accessToken = "aseukfgwef";
|
const accessToken = "aseukfgwef";
|
||||||
const roomId = "!room:here";
|
const roomId = "!room:here";
|
||||||
let room: Room;
|
let client: MatrixClient | undefined;
|
||||||
|
let httpBackend: HttpBackend | undefined;
|
||||||
|
let room: Room | undefined;
|
||||||
|
|
||||||
beforeEach(function() {
|
const setupTests = (): [MatrixClient, HttpBackend, Room] => {
|
||||||
scheduler = new MatrixScheduler();
|
const scheduler = new MatrixScheduler();
|
||||||
const testClient = new TestClient(
|
const testClient = new TestClient(
|
||||||
userId,
|
userId,
|
||||||
"DEVICE",
|
"DEVICE",
|
||||||
@@ -37,15 +37,21 @@ describe("MatrixClient retrying", function() {
|
|||||||
undefined,
|
undefined,
|
||||||
{ scheduler },
|
{ scheduler },
|
||||||
);
|
);
|
||||||
httpBackend = testClient.httpBackend;
|
const httpBackend = testClient.httpBackend;
|
||||||
client = testClient.client;
|
const client = testClient.client;
|
||||||
room = new Room(roomId, client, userId);
|
const room = new Room(roomId, client, userId);
|
||||||
client.store.storeRoom(room);
|
client!.store.storeRoom(room);
|
||||||
|
|
||||||
|
return [client, httpBackend, room];
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
[client, httpBackend, room] = setupTests();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function() {
|
afterEach(function() {
|
||||||
httpBackend.verifyNoOutstandingExpectation();
|
httpBackend!.verifyNoOutstandingExpectation();
|
||||||
return httpBackend.stop();
|
return httpBackend!.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("should retry according to MatrixScheduler.retryFn", function() {
|
xit("should retry according to MatrixScheduler.retryFn", function() {
|
||||||
@@ -66,7 +72,7 @@ describe("MatrixClient retrying", function() {
|
|||||||
|
|
||||||
it("should mark events as EventStatus.CANCELLED when cancelled", function() {
|
it("should mark events as EventStatus.CANCELLED when cancelled", function() {
|
||||||
// send a couple of events; the second will be queued
|
// send a couple of events; the second will be queued
|
||||||
const p1 = client.sendMessage(roomId, {
|
const p1 = client!.sendMessage(roomId, {
|
||||||
"msgtype": "m.text",
|
"msgtype": "m.text",
|
||||||
"body": "m1",
|
"body": "m1",
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
@@ -79,13 +85,13 @@ describe("MatrixClient retrying", function() {
|
|||||||
// XXX: it turns out that the promise returned by this message
|
// XXX: it turns out that the promise returned by this message
|
||||||
// never gets resolved.
|
// never gets resolved.
|
||||||
// https://github.com/matrix-org/matrix-js-sdk/issues/496
|
// https://github.com/matrix-org/matrix-js-sdk/issues/496
|
||||||
client.sendMessage(roomId, {
|
client!.sendMessage(roomId, {
|
||||||
"msgtype": "m.text",
|
"msgtype": "m.text",
|
||||||
"body": "m2",
|
"body": "m2",
|
||||||
});
|
});
|
||||||
|
|
||||||
// both events should be in the timeline at this point
|
// both events should be in the timeline at this point
|
||||||
const tl = room.getLiveTimeline().getEvents();
|
const tl = room!.getLiveTimeline().getEvents();
|
||||||
expect(tl.length).toEqual(2);
|
expect(tl.length).toEqual(2);
|
||||||
const ev1 = tl[0];
|
const ev1 = tl[0];
|
||||||
const ev2 = tl[1];
|
const ev2 = tl[1];
|
||||||
@@ -94,24 +100,24 @@ describe("MatrixClient retrying", function() {
|
|||||||
expect(ev2.status).toEqual(EventStatus.SENDING);
|
expect(ev2.status).toEqual(EventStatus.SENDING);
|
||||||
|
|
||||||
// the first message should get sent, and the second should get queued
|
// the first message should get sent, and the second should get queued
|
||||||
httpBackend.when("PUT", "/send/m.room.message/").check(function() {
|
httpBackend!.when("PUT", "/send/m.room.message/").check(function() {
|
||||||
// ev2 should now have been queued
|
// ev2 should now have been queued
|
||||||
expect(ev2.status).toEqual(EventStatus.QUEUED);
|
expect(ev2.status).toEqual(EventStatus.QUEUED);
|
||||||
|
|
||||||
// now we can cancel the second and check everything looks sane
|
// now we can cancel the second and check everything looks sane
|
||||||
client.cancelPendingEvent(ev2);
|
client!.cancelPendingEvent(ev2);
|
||||||
expect(ev2.status).toEqual(EventStatus.CANCELLED);
|
expect(ev2.status).toEqual(EventStatus.CANCELLED);
|
||||||
expect(tl.length).toEqual(1);
|
expect(tl.length).toEqual(1);
|
||||||
|
|
||||||
// shouldn't be able to cancel the first message yet
|
// shouldn't be able to cancel the first message yet
|
||||||
expect(function() {
|
expect(function() {
|
||||||
client.cancelPendingEvent(ev1);
|
client!.cancelPendingEvent(ev1);
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
}).respond(400); // fail the first message
|
}).respond(400); // fail the first message
|
||||||
|
|
||||||
// wait for the localecho of ev1 to be updated
|
// wait for the localecho of ev1 to be updated
|
||||||
const p3 = new Promise<void>((resolve, reject) => {
|
const p3 = new Promise<void>((resolve, reject) => {
|
||||||
room.on(RoomEvent.LocalEchoUpdated, (ev0) => {
|
room!.on(RoomEvent.LocalEchoUpdated, (ev0) => {
|
||||||
if (ev0 === ev1) {
|
if (ev0 === ev1) {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
@@ -121,7 +127,7 @@ describe("MatrixClient retrying", function() {
|
|||||||
expect(tl.length).toEqual(1);
|
expect(tl.length).toEqual(1);
|
||||||
|
|
||||||
// cancel the first message
|
// cancel the first message
|
||||||
client.cancelPendingEvent(ev1);
|
client!.cancelPendingEvent(ev1);
|
||||||
expect(ev1.status).toEqual(EventStatus.CANCELLED);
|
expect(ev1.status).toEqual(EventStatus.CANCELLED);
|
||||||
expect(tl.length).toEqual(0);
|
expect(tl.length).toEqual(0);
|
||||||
});
|
});
|
||||||
@@ -129,7 +135,7 @@ describe("MatrixClient retrying", function() {
|
|||||||
return Promise.all([
|
return Promise.all([
|
||||||
p1,
|
p1,
|
||||||
p3,
|
p3,
|
||||||
httpBackend.flushAllExpected(),
|
httpBackend!.flushAllExpected(),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,35 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 HttpBackend from "matrix-mock-request";
|
||||||
|
|
||||||
import * as utils from "../test-utils/test-utils";
|
import * as utils from "../test-utils/test-utils";
|
||||||
import { EventStatus } from "../../src/models/event";
|
import { EventStatus } from "../../src/models/event";
|
||||||
import { RoomEvent } from "../../src";
|
import { ClientEvent, IEvent, MatrixClient, RoomEvent } from "../../src";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
|
|
||||||
describe("MatrixClient room timelines", function() {
|
describe("MatrixClient room timelines", function() {
|
||||||
let client = null;
|
|
||||||
let httpBackend = null;
|
|
||||||
const userId = "@alice:localhost";
|
const userId = "@alice:localhost";
|
||||||
const userName = "Alice";
|
const userName = "Alice";
|
||||||
const accessToken = "aseukfgwef";
|
const accessToken = "aseukfgwef";
|
||||||
const roomId = "!foo:bar";
|
const roomId = "!foo:bar";
|
||||||
const otherUserId = "@bob:localhost";
|
const otherUserId = "@bob:localhost";
|
||||||
|
let client: MatrixClient | undefined;
|
||||||
|
let httpBackend: HttpBackend | undefined;
|
||||||
|
|
||||||
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||||
room: roomId, mship: "join", user: userId, name: userName,
|
room: roomId, mship: "join", user: userId, name: userName,
|
||||||
});
|
});
|
||||||
@@ -55,8 +74,7 @@ describe("MatrixClient room timelines", function() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function setNextSyncData(events) {
|
function setNextSyncData(events: Partial<IEvent>[] = []) {
|
||||||
events = events || [];
|
|
||||||
NEXT_SYNC_DATA = {
|
NEXT_SYNC_DATA = {
|
||||||
next_batch: "n",
|
next_batch: "n",
|
||||||
presence: { events: [] },
|
presence: { events: [] },
|
||||||
@@ -77,19 +95,9 @@ describe("MatrixClient room timelines", function() {
|
|||||||
throw new Error("setNextSyncData only works with one room id");
|
throw new Error("setNextSyncData only works with one room id");
|
||||||
}
|
}
|
||||||
if (e.state_key) {
|
if (e.state_key) {
|
||||||
if (e.__prev_event === undefined) {
|
|
||||||
throw new Error(
|
|
||||||
"setNextSyncData needs the prev state set to '__prev_event' " +
|
|
||||||
"for " + e.type,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (e.__prev_event !== null) {
|
|
||||||
// push the previous state for this event type
|
|
||||||
NEXT_SYNC_DATA.rooms.join[roomId].state.events.push(e.__prev_event);
|
|
||||||
}
|
|
||||||
// push the current
|
// push the current
|
||||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
|
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
|
||||||
} else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) {
|
} else if (["m.typing", "m.receipt"].indexOf(e.type!) !== -1) {
|
||||||
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
|
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
|
||||||
} else {
|
} else {
|
||||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
|
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
|
||||||
@@ -97,7 +105,7 @@ describe("MatrixClient room timelines", function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(async function() {
|
const setupTestClient = (): [MatrixClient, HttpBackend] => {
|
||||||
// these tests should work with or without timelineSupport
|
// these tests should work with or without timelineSupport
|
||||||
const testClient = new TestClient(
|
const testClient = new TestClient(
|
||||||
userId,
|
userId,
|
||||||
@@ -106,41 +114,46 @@ describe("MatrixClient room timelines", function() {
|
|||||||
undefined,
|
undefined,
|
||||||
{ timelineSupport: true },
|
{ timelineSupport: true },
|
||||||
);
|
);
|
||||||
httpBackend = testClient.httpBackend;
|
const httpBackend = testClient.httpBackend;
|
||||||
client = testClient.client;
|
const client = testClient.client;
|
||||||
|
|
||||||
setNextSyncData();
|
setNextSyncData();
|
||||||
httpBackend.when("GET", "/versions").respond(200, {});
|
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
httpBackend!.when("GET", "/sync").respond(200, function() {
|
||||||
return NEXT_SYNC_DATA;
|
return NEXT_SYNC_DATA;
|
||||||
});
|
});
|
||||||
client.startClient();
|
client!.startClient();
|
||||||
|
|
||||||
|
return [client!, httpBackend];
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async function() {
|
||||||
|
[client!, httpBackend] = setupTestClient();
|
||||||
await httpBackend.flush("/versions");
|
await httpBackend.flush("/versions");
|
||||||
await httpBackend.flush("/pushrules");
|
await httpBackend.flush("/pushrules");
|
||||||
await httpBackend.flush("/filter");
|
await httpBackend.flush("/filter");
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function() {
|
afterEach(function() {
|
||||||
httpBackend.verifyNoOutstandingExpectation();
|
httpBackend!.verifyNoOutstandingExpectation();
|
||||||
client.stopClient();
|
client!.stopClient();
|
||||||
return httpBackend.stop();
|
return httpBackend!.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("local echo events", function() {
|
describe("local echo events", function() {
|
||||||
it("should be added immediately after calling MatrixClient.sendEvent " +
|
it("should be added immediately after calling MatrixClient.sendEvent " +
|
||||||
"with EventStatus.SENDING and the right event.sender", function(done) {
|
"with EventStatus.SENDING and the right event.sender", function(done) {
|
||||||
client.on("sync", function(state) {
|
client!.on(ClientEvent.Sync, function(state) {
|
||||||
if (state !== "PREPARED") {
|
if (state !== "PREPARED") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const room = client.getRoom(roomId);
|
const room = client!.getRoom(roomId)!;
|
||||||
expect(room.timeline.length).toEqual(1);
|
expect(room.timeline.length).toEqual(1);
|
||||||
|
|
||||||
client.sendTextMessage(roomId, "I am a fish", "txn1");
|
client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||||
// check it was added
|
// check it was added
|
||||||
expect(room.timeline.length).toEqual(2);
|
expect(room.timeline.length).toEqual(2);
|
||||||
// check status
|
// check status
|
||||||
@@ -150,68 +163,68 @@ describe("MatrixClient room timelines", function() {
|
|||||||
expect(member.userId).toEqual(userId);
|
expect(member.userId).toEqual(userId);
|
||||||
expect(member.name).toEqual(userName);
|
expect(member.name).toEqual(userName);
|
||||||
|
|
||||||
httpBackend.flush("/sync", 1).then(function() {
|
httpBackend!.flush("/sync", 1).then(function() {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
httpBackend.flush("/sync", 1);
|
httpBackend!.flush("/sync", 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be updated correctly when the send request finishes " +
|
it("should be updated correctly when the send request finishes " +
|
||||||
"BEFORE the event comes down the event stream", function(done) {
|
"BEFORE the event comes down the event stream", function(done) {
|
||||||
const eventId = "$foo:bar";
|
const eventId = "$foo:bar";
|
||||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
httpBackend!.when("PUT", "/txn1").respond(200, {
|
||||||
event_id: eventId,
|
event_id: eventId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ev = utils.mkMessage({
|
const ev = utils.mkMessage({
|
||||||
body: "I am a fish", user: userId, room: roomId,
|
msg: "I am a fish", user: userId, room: roomId,
|
||||||
});
|
});
|
||||||
ev.event_id = eventId;
|
ev.event_id = eventId;
|
||||||
ev.unsigned = { transaction_id: "txn1" };
|
ev.unsigned = { transaction_id: "txn1" };
|
||||||
setNextSyncData([ev]);
|
setNextSyncData([ev]);
|
||||||
|
|
||||||
client.on("sync", function(state) {
|
client!.on(ClientEvent.Sync, function(state) {
|
||||||
if (state !== "PREPARED") {
|
if (state !== "PREPARED") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const room = client.getRoom(roomId);
|
const room = client!.getRoom(roomId)!;
|
||||||
client.sendTextMessage(roomId, "I am a fish", "txn1").then(
|
client!.sendTextMessage(roomId, "I am a fish", "txn1").then(
|
||||||
function() {
|
function() {
|
||||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||||
httpBackend.flush("/sync", 1).then(function() {
|
httpBackend!.flush("/sync", 1).then(function() {
|
||||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
httpBackend.flush("/txn1", 1);
|
httpBackend!.flush("/txn1", 1);
|
||||||
});
|
});
|
||||||
httpBackend.flush("/sync", 1);
|
httpBackend!.flush("/sync", 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be updated correctly when the send request finishes " +
|
it("should be updated correctly when the send request finishes " +
|
||||||
"AFTER the event comes down the event stream", function(done) {
|
"AFTER the event comes down the event stream", function(done) {
|
||||||
const eventId = "$foo:bar";
|
const eventId = "$foo:bar";
|
||||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
httpBackend!.when("PUT", "/txn1").respond(200, {
|
||||||
event_id: eventId,
|
event_id: eventId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ev = utils.mkMessage({
|
const ev = utils.mkMessage({
|
||||||
body: "I am a fish", user: userId, room: roomId,
|
msg: "I am a fish", user: userId, room: roomId,
|
||||||
});
|
});
|
||||||
ev.event_id = eventId;
|
ev.event_id = eventId;
|
||||||
ev.unsigned = { transaction_id: "txn1" };
|
ev.unsigned = { transaction_id: "txn1" };
|
||||||
setNextSyncData([ev]);
|
setNextSyncData([ev]);
|
||||||
|
|
||||||
client.on("sync", function(state) {
|
client!.on(ClientEvent.Sync, function(state) {
|
||||||
if (state !== "PREPARED") {
|
if (state !== "PREPARED") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const room = client.getRoom(roomId);
|
const room = client!.getRoom(roomId)!;
|
||||||
const promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
|
const promise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||||
httpBackend.flush("/sync", 1).then(function() {
|
httpBackend!.flush("/sync", 1).then(function() {
|
||||||
expect(room.timeline.length).toEqual(2);
|
expect(room.timeline.length).toEqual(2);
|
||||||
httpBackend.flush("/txn1", 1);
|
httpBackend!.flush("/txn1", 1);
|
||||||
promise.then(function() {
|
promise.then(function() {
|
||||||
expect(room.timeline.length).toEqual(2);
|
expect(room.timeline.length).toEqual(2);
|
||||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||||
@@ -219,7 +232,7 @@ describe("MatrixClient room timelines", function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
httpBackend.flush("/sync", 1);
|
httpBackend!.flush("/sync", 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -229,7 +242,7 @@ describe("MatrixClient room timelines", function() {
|
|||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
sbEvents = [];
|
sbEvents = [];
|
||||||
httpBackend.when("GET", "/messages").respond(200, function() {
|
httpBackend!.when("GET", "/messages").respond(200, function() {
|
||||||
return {
|
return {
|
||||||
chunk: sbEvents,
|
chunk: sbEvents,
|
||||||
start: "pagin_start",
|
start: "pagin_start",
|
||||||
@@ -240,26 +253,26 @@ describe("MatrixClient room timelines", function() {
|
|||||||
|
|
||||||
it("should set Room.oldState.paginationToken to null at the start" +
|
it("should set Room.oldState.paginationToken to null at the start" +
|
||||||
" of the timeline.", function(done) {
|
" of the timeline.", function(done) {
|
||||||
client.on("sync", function(state) {
|
client!.on(ClientEvent.Sync, function(state) {
|
||||||
if (state !== "PREPARED") {
|
if (state !== "PREPARED") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const room = client.getRoom(roomId);
|
const room = client!.getRoom(roomId)!;
|
||||||
expect(room.timeline.length).toEqual(1);
|
expect(room.timeline.length).toEqual(1);
|
||||||
|
|
||||||
client.scrollback(room).then(function() {
|
client!.scrollback(room).then(function() {
|
||||||
expect(room.timeline.length).toEqual(1);
|
expect(room.timeline.length).toEqual(1);
|
||||||
expect(room.oldState.paginationToken).toBe(null);
|
expect(room.oldState.paginationToken).toBe(null);
|
||||||
|
|
||||||
// still have a sync to flush
|
// still have a sync to flush
|
||||||
httpBackend.flush("/sync", 1).then(() => {
|
httpBackend!.flush("/sync", 1).then(() => {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
httpBackend.flush("/messages", 1);
|
httpBackend!.flush("/messages", 1);
|
||||||
});
|
});
|
||||||
httpBackend.flush("/sync", 1);
|
httpBackend!.flush("/sync", 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set the right event.sender values", function(done) {
|
it("should set the right event.sender values", function(done) {
|
||||||
@@ -275,7 +288,7 @@ describe("MatrixClient room timelines", function() {
|
|||||||
// make an m.room.member event for alice's join
|
// make an m.room.member event for alice's join
|
||||||
const joinMshipEvent = utils.mkMembership({
|
const joinMshipEvent = utils.mkMembership({
|
||||||
mship: "join", user: userId, room: roomId, name: "Old Alice",
|
mship: "join", user: userId, room: roomId, name: "Old Alice",
|
||||||
url: null,
|
url: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
// make an m.room.member event with prev_content for alice's nick
|
// make an m.room.member event with prev_content for alice's nick
|
||||||
@@ -286,7 +299,7 @@ describe("MatrixClient room timelines", function() {
|
|||||||
});
|
});
|
||||||
oldMshipEvent.prev_content = {
|
oldMshipEvent.prev_content = {
|
||||||
displayname: "Old Alice",
|
displayname: "Old Alice",
|
||||||
avatar_url: null,
|
avatar_url: undefined,
|
||||||
membership: "join",
|
membership: "join",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -303,15 +316,15 @@ describe("MatrixClient room timelines", function() {
|
|||||||
joinMshipEvent,
|
joinMshipEvent,
|
||||||
];
|
];
|
||||||
|
|
||||||
client.on("sync", function(state) {
|
client!.on(ClientEvent.Sync, function(state) {
|
||||||
if (state !== "PREPARED") {
|
if (state !== "PREPARED") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const room = client.getRoom(roomId);
|
const room = client!.getRoom(roomId)!;
|
||||||
// sync response
|
// sync response
|
||||||
expect(room.timeline.length).toEqual(1);
|
expect(room.timeline.length).toEqual(1);
|
||||||
|
|
||||||
client.scrollback(room).then(function() {
|
client!.scrollback(room).then(function() {
|
||||||
expect(room.timeline.length).toEqual(5);
|
expect(room.timeline.length).toEqual(5);
|
||||||
const joinMsg = room.timeline[0];
|
const joinMsg = room.timeline[0];
|
||||||
expect(joinMsg.sender.name).toEqual("Old Alice");
|
expect(joinMsg.sender.name).toEqual("Old Alice");
|
||||||
@@ -321,14 +334,14 @@ describe("MatrixClient room timelines", function() {
|
|||||||
expect(newMsg.sender.name).toEqual(userName);
|
expect(newMsg.sender.name).toEqual(userName);
|
||||||
|
|
||||||
// still have a sync to flush
|
// still have a sync to flush
|
||||||
httpBackend.flush("/sync", 1).then(() => {
|
httpBackend!.flush("/sync", 1).then(() => {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
httpBackend.flush("/messages", 1);
|
httpBackend!.flush("/messages", 1);
|
||||||
});
|
});
|
||||||
httpBackend.flush("/sync", 1);
|
httpBackend!.flush("/sync", 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
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", function(done) {
|
||||||
@@ -342,27 +355,27 @@ describe("MatrixClient room timelines", function() {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
client.on("sync", function(state) {
|
client!.on(ClientEvent.Sync, function(state) {
|
||||||
if (state !== "PREPARED") {
|
if (state !== "PREPARED") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const room = client.getRoom(roomId);
|
const room = client!.getRoom(roomId)!;
|
||||||
expect(room.timeline.length).toEqual(1);
|
expect(room.timeline.length).toEqual(1);
|
||||||
|
|
||||||
client.scrollback(room).then(function() {
|
client!.scrollback(room).then(function() {
|
||||||
expect(room.timeline.length).toEqual(3);
|
expect(room.timeline.length).toEqual(3);
|
||||||
expect(room.timeline[0].event).toEqual(sbEvents[1]);
|
expect(room.timeline[0].event).toEqual(sbEvents[1]);
|
||||||
expect(room.timeline[1].event).toEqual(sbEvents[0]);
|
expect(room.timeline[1].event).toEqual(sbEvents[0]);
|
||||||
|
|
||||||
// still have a sync to flush
|
// still have a sync to flush
|
||||||
httpBackend.flush("/sync", 1).then(() => {
|
httpBackend!.flush("/sync", 1).then(() => {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
httpBackend.flush("/messages", 1);
|
httpBackend!.flush("/messages", 1);
|
||||||
});
|
});
|
||||||
httpBackend.flush("/sync", 1);
|
httpBackend!.flush("/sync", 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should use 'end' as the next pagination token", function(done) {
|
it("should use 'end' as the next pagination token", function(done) {
|
||||||
@@ -373,25 +386,25 @@ describe("MatrixClient room timelines", function() {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
client.on("sync", function(state) {
|
client!.on(ClientEvent.Sync, function(state) {
|
||||||
if (state !== "PREPARED") {
|
if (state !== "PREPARED") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const room = client.getRoom(roomId);
|
const room = client!.getRoom(roomId)!;
|
||||||
expect(room.oldState.paginationToken).toBeTruthy();
|
expect(room.oldState.paginationToken).toBeTruthy();
|
||||||
|
|
||||||
client.scrollback(room, 1).then(function() {
|
client!.scrollback(room, 1).then(function() {
|
||||||
expect(room.oldState.paginationToken).toEqual(sbEndTok);
|
expect(room.oldState.paginationToken).toEqual(sbEndTok);
|
||||||
});
|
});
|
||||||
|
|
||||||
httpBackend.flush("/messages", 1).then(function() {
|
httpBackend!.flush("/messages", 1).then(function() {
|
||||||
// still have a sync to flush
|
// still have a sync to flush
|
||||||
httpBackend.flush("/sync", 1).then(() => {
|
httpBackend!.flush("/sync", 1).then(() => {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
httpBackend.flush("/sync", 1);
|
httpBackend!.flush("/sync", 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -404,23 +417,23 @@ describe("MatrixClient room timelines", function() {
|
|||||||
setNextSyncData(eventData);
|
setNextSyncData(eventData);
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
httpBackend.flush("/sync", 1),
|
httpBackend!.flush("/sync", 1),
|
||||||
utils.syncPromise(client),
|
utils.syncPromise(client!),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
const room = client.getRoom(roomId);
|
const room = client!.getRoom(roomId)!;
|
||||||
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
client.on("Room.timeline", function(event, rm, toStart) {
|
client!.on(RoomEvent.Timeline, function(event, rm, toStart) {
|
||||||
expect(toStart).toBe(false);
|
expect(toStart).toBe(false);
|
||||||
expect(rm).toEqual(room);
|
expect(rm).toEqual(room);
|
||||||
expect(event.event).toEqual(eventData[index]);
|
expect(event.event).toEqual(eventData[index]);
|
||||||
index += 1;
|
index += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
httpBackend.flush("/messages", 1);
|
httpBackend!.flush("/messages", 1);
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
httpBackend.flush("/sync", 1),
|
httpBackend!.flush("/sync", 1),
|
||||||
utils.syncPromise(client),
|
utils.syncPromise(client!),
|
||||||
]).then(function() {
|
]).then(function() {
|
||||||
expect(index).toEqual(2);
|
expect(index).toEqual(2);
|
||||||
expect(room.timeline.length).toEqual(3);
|
expect(room.timeline.length).toEqual(3);
|
||||||
@@ -442,17 +455,16 @@ describe("MatrixClient room timelines", function() {
|
|||||||
}),
|
}),
|
||||||
utils.mkMessage({ user: userId, room: roomId }),
|
utils.mkMessage({ user: userId, room: roomId }),
|
||||||
];
|
];
|
||||||
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
|
|
||||||
setNextSyncData(eventData);
|
setNextSyncData(eventData);
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
httpBackend.flush("/sync", 1),
|
httpBackend!.flush("/sync", 1),
|
||||||
utils.syncPromise(client),
|
utils.syncPromise(client!),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
const room = client.getRoom(roomId);
|
const room = client!.getRoom(roomId)!;
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
httpBackend.flush("/sync", 1),
|
httpBackend!.flush("/sync", 1),
|
||||||
utils.syncPromise(client),
|
utils.syncPromise(client!),
|
||||||
]).then(function() {
|
]).then(function() {
|
||||||
const preNameEvent = room.timeline[room.timeline.length - 3];
|
const preNameEvent = room.timeline[room.timeline.length - 3];
|
||||||
const postNameEvent = room.timeline[room.timeline.length - 1];
|
const postNameEvent = room.timeline[room.timeline.length - 1];
|
||||||
@@ -468,22 +480,21 @@ describe("MatrixClient room timelines", function() {
|
|||||||
name: "Room 2",
|
name: "Room 2",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
|
|
||||||
setNextSyncData([secondRoomNameEvent]);
|
setNextSyncData([secondRoomNameEvent]);
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
httpBackend.flush("/sync", 1),
|
httpBackend!.flush("/sync", 1),
|
||||||
utils.syncPromise(client),
|
utils.syncPromise(client!),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
const room = client.getRoom(roomId);
|
const room = client!.getRoom(roomId)!;
|
||||||
let nameEmitCount = 0;
|
let nameEmitCount = 0;
|
||||||
client.on("Room.name", function(rm) {
|
client!.on(RoomEvent.Name, function(rm) {
|
||||||
nameEmitCount += 1;
|
nameEmitCount += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
httpBackend.flush("/sync", 1),
|
httpBackend!.flush("/sync", 1),
|
||||||
utils.syncPromise(client),
|
utils.syncPromise(client!),
|
||||||
]).then(function() {
|
]).then(function() {
|
||||||
expect(nameEmitCount).toEqual(1);
|
expect(nameEmitCount).toEqual(1);
|
||||||
expect(room.name).toEqual("Room 2");
|
expect(room.name).toEqual("Room 2");
|
||||||
@@ -493,12 +504,11 @@ describe("MatrixClient room timelines", function() {
|
|||||||
name: "Room 3",
|
name: "Room 3",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
|
|
||||||
setNextSyncData([thirdRoomNameEvent]);
|
setNextSyncData([thirdRoomNameEvent]);
|
||||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
httpBackend.flush("/sync", 1),
|
httpBackend!.flush("/sync", 1),
|
||||||
utils.syncPromise(client),
|
utils.syncPromise(client!),
|
||||||
]);
|
]);
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
expect(nameEmitCount).toEqual(2);
|
expect(nameEmitCount).toEqual(2);
|
||||||
@@ -518,26 +528,24 @@ describe("MatrixClient room timelines", function() {
|
|||||||
user: userC, room: roomId, mship: "invite", skey: userD,
|
user: userC, room: roomId, mship: "invite", skey: userD,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
eventData[0].__prev_event = null;
|
|
||||||
eventData[1].__prev_event = null;
|
|
||||||
setNextSyncData(eventData);
|
setNextSyncData(eventData);
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
httpBackend.flush("/sync", 1),
|
httpBackend!.flush("/sync", 1),
|
||||||
utils.syncPromise(client),
|
utils.syncPromise(client!),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
const room = client.getRoom(roomId);
|
const room = client!.getRoom(roomId)!;
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
httpBackend.flush("/sync", 1),
|
httpBackend!.flush("/sync", 1),
|
||||||
utils.syncPromise(client),
|
utils.syncPromise(client!),
|
||||||
]).then(function() {
|
]).then(function() {
|
||||||
expect(room.currentState.getMembers().length).toEqual(4);
|
expect(room.currentState.getMembers().length).toEqual(4);
|
||||||
expect(room.currentState.getMember(userC).name).toEqual("C");
|
expect(room.currentState.getMember(userC)!.name).toEqual("C");
|
||||||
expect(room.currentState.getMember(userC).membership).toEqual(
|
expect(room.currentState.getMember(userC)!.membership).toEqual(
|
||||||
"join",
|
"join",
|
||||||
);
|
);
|
||||||
expect(room.currentState.getMember(userD).name).toEqual(userD);
|
expect(room.currentState.getMember(userD)!.name).toEqual(userD);
|
||||||
expect(room.currentState.getMember(userD).membership).toEqual(
|
expect(room.currentState.getMember(userD)!.membership).toEqual(
|
||||||
"invite",
|
"invite",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -554,26 +562,26 @@ describe("MatrixClient room timelines", function() {
|
|||||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
httpBackend.flush("/versions", 1),
|
httpBackend!.flush("/versions", 1),
|
||||||
httpBackend.flush("/sync", 1),
|
httpBackend!.flush("/sync", 1),
|
||||||
utils.syncPromise(client),
|
utils.syncPromise(client!),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
const room = client.getRoom(roomId);
|
const room = client!.getRoom(roomId)!;
|
||||||
|
|
||||||
httpBackend.flush("/messages", 1);
|
httpBackend!.flush("/messages", 1);
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
httpBackend.flush("/sync", 1),
|
httpBackend!.flush("/sync", 1),
|
||||||
utils.syncPromise(client),
|
utils.syncPromise(client!),
|
||||||
]).then(function() {
|
]).then(function() {
|
||||||
expect(room.timeline.length).toEqual(1);
|
expect(room.timeline.length).toEqual(1);
|
||||||
expect(room.timeline[0].event).toEqual(eventData[0]);
|
expect(room.timeline[0].event).toEqual(eventData[0]);
|
||||||
expect(room.currentState.getMembers().length).toEqual(2);
|
expect(room.currentState.getMembers().length).toEqual(2);
|
||||||
expect(room.currentState.getMember(userId).name).toEqual(userName);
|
expect(room.currentState.getMember(userId)!.name).toEqual(userName);
|
||||||
expect(room.currentState.getMember(userId).membership).toEqual(
|
expect(room.currentState.getMember(userId)!.membership).toEqual(
|
||||||
"join",
|
"join",
|
||||||
);
|
);
|
||||||
expect(room.currentState.getMember(otherUserId).name).toEqual("Bob");
|
expect(room.currentState.getMember(otherUserId)!.name).toEqual("Bob");
|
||||||
expect(room.currentState.getMember(otherUserId).membership).toEqual(
|
expect(room.currentState.getMember(otherUserId)!.membership).toEqual(
|
||||||
"join",
|
"join",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -588,21 +596,21 @@ describe("MatrixClient room timelines", function() {
|
|||||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
httpBackend.flush("/sync", 1),
|
httpBackend!.flush("/sync", 1),
|
||||||
utils.syncPromise(client),
|
utils.syncPromise(client!),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
const room = client.getRoom(roomId);
|
const room = client!.getRoom(roomId)!;
|
||||||
|
|
||||||
let emitCount = 0;
|
let emitCount = 0;
|
||||||
client.on("Room.timelineReset", function(emitRoom) {
|
client!.on(RoomEvent.TimelineReset, function(emitRoom) {
|
||||||
expect(emitRoom).toEqual(room);
|
expect(emitRoom).toEqual(room);
|
||||||
emitCount++;
|
emitCount++;
|
||||||
});
|
});
|
||||||
|
|
||||||
httpBackend.flush("/messages", 1);
|
httpBackend!.flush("/messages", 1);
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
httpBackend.flush("/sync", 1),
|
httpBackend!.flush("/sync", 1),
|
||||||
utils.syncPromise(client),
|
utils.syncPromise(client!),
|
||||||
]).then(function() {
|
]).then(function() {
|
||||||
expect(emitCount).toEqual(1);
|
expect(emitCount).toEqual(1);
|
||||||
});
|
});
|
||||||
@@ -618,7 +626,7 @@ describe("MatrixClient room timelines", function() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const contextUrl = `/rooms/${encodeURIComponent(roomId)}/context/` +
|
const contextUrl = `/rooms/${encodeURIComponent(roomId)}/context/` +
|
||||||
`${encodeURIComponent(initialSyncEventData[2].event_id)}`;
|
`${encodeURIComponent(initialSyncEventData[2].event_id!)}`;
|
||||||
const contextResponse = {
|
const contextResponse = {
|
||||||
start: "start_token",
|
start: "start_token",
|
||||||
events_before: [initialSyncEventData[1], initialSyncEventData[0]],
|
events_before: [initialSyncEventData[1], initialSyncEventData[0]],
|
||||||
@@ -636,19 +644,19 @@ describe("MatrixClient room timelines", function() {
|
|||||||
|
|
||||||
// Create a room from the sync
|
// Create a room from the sync
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
httpBackend.flushAllExpected(),
|
httpBackend!.flushAllExpected(),
|
||||||
utils.syncPromise(client, 1),
|
utils.syncPromise(client!, 1),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Get the room after the first sync so the room is created
|
// Get the room after the first sync so the room is created
|
||||||
room = client.getRoom(roomId);
|
room = client!.getRoom(roomId)!;
|
||||||
expect(room).toBeTruthy();
|
expect(room).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should clear and refresh messages in timeline', async () => {
|
it('should clear and refresh messages in timeline', async () => {
|
||||||
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
|
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
|
||||||
// to construct a new timeline from.
|
// to construct a new timeline from.
|
||||||
httpBackend.when("GET", contextUrl)
|
httpBackend!.when("GET", contextUrl)
|
||||||
.respond(200, function() {
|
.respond(200, function() {
|
||||||
// The timeline should be cleared at this point in the refresh
|
// The timeline should be cleared at this point in the refresh
|
||||||
expect(room.timeline.length).toEqual(0);
|
expect(room.timeline.length).toEqual(0);
|
||||||
@@ -659,7 +667,7 @@ describe("MatrixClient room timelines", function() {
|
|||||||
// Refresh the timeline.
|
// Refresh the timeline.
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
room.refreshLiveTimeline(),
|
room.refreshLiveTimeline(),
|
||||||
httpBackend.flushAllExpected(),
|
httpBackend!.flushAllExpected(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Make sure the message are visible
|
// Make sure the message are visible
|
||||||
@@ -681,7 +689,7 @@ describe("MatrixClient room timelines", function() {
|
|||||||
// middle of all of this refresh timeline logic. We want to make
|
// middle of all of this refresh timeline logic. We want to make
|
||||||
// sure the sync pagination still works as expected after messing
|
// sure the sync pagination still works as expected after messing
|
||||||
// the refresh timline logic messes with the pagination tokens.
|
// the refresh timline logic messes with the pagination tokens.
|
||||||
httpBackend.when("GET", contextUrl)
|
httpBackend!.when("GET", contextUrl)
|
||||||
.respond(200, () => {
|
.respond(200, () => {
|
||||||
// Now finally return and make the `/context` request respond
|
// Now finally return and make the `/context` request respond
|
||||||
return contextResponse;
|
return contextResponse;
|
||||||
@@ -700,7 +708,7 @@ describe("MatrixClient room timelines", function() {
|
|||||||
const racingSyncEventData = [
|
const racingSyncEventData = [
|
||||||
utils.mkMessage({ user: userId, room: roomId }),
|
utils.mkMessage({ user: userId, room: roomId }),
|
||||||
];
|
];
|
||||||
const waitForRaceySyncAfterResetPromise = new Promise((resolve, reject) => {
|
const waitForRaceySyncAfterResetPromise = new Promise<void>((resolve, reject) => {
|
||||||
let eventFired = false;
|
let eventFired = false;
|
||||||
// Throw a more descriptive error if this part of the test times out.
|
// Throw a more descriptive error if this part of the test times out.
|
||||||
const failTimeout = setTimeout(() => {
|
const failTimeout = setTimeout(() => {
|
||||||
@@ -726,12 +734,12 @@ describe("MatrixClient room timelines", function() {
|
|||||||
// Then make a `/sync` happen by sending a message and seeing that it
|
// Then make a `/sync` happen by sending a message and seeing that it
|
||||||
// shows up (simulate a /sync naturally racing with us).
|
// shows up (simulate a /sync naturally racing with us).
|
||||||
setNextSyncData(racingSyncEventData);
|
setNextSyncData(racingSyncEventData);
|
||||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
httpBackend!.when("GET", "/sync").respond(200, function() {
|
||||||
return NEXT_SYNC_DATA;
|
return NEXT_SYNC_DATA;
|
||||||
});
|
});
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
httpBackend.flush("/sync", 1),
|
httpBackend!.flush("/sync", 1),
|
||||||
utils.syncPromise(client, 1),
|
utils.syncPromise(client!, 1),
|
||||||
]);
|
]);
|
||||||
// Make sure the timeline has the racey sync data
|
// Make sure the timeline has the racey sync data
|
||||||
const afterRaceySyncTimelineEvents = room
|
const afterRaceySyncTimelineEvents = room
|
||||||
@@ -761,7 +769,7 @@ describe("MatrixClient room timelines", function() {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
refreshLiveTimelinePromise,
|
refreshLiveTimelinePromise,
|
||||||
// Then flush the remaining `/context` to left the refresh logic complete
|
// Then flush the remaining `/context` to left the refresh logic complete
|
||||||
httpBackend.flushAllExpected(),
|
httpBackend!.flushAllExpected(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Make sure sync pagination still works by seeing a new message show up
|
// Make sure sync pagination still works by seeing a new message show up
|
||||||
@@ -770,12 +778,12 @@ describe("MatrixClient room timelines", function() {
|
|||||||
utils.mkMessage({ user: userId, room: roomId }),
|
utils.mkMessage({ user: userId, room: roomId }),
|
||||||
];
|
];
|
||||||
setNextSyncData(afterRefreshEventData);
|
setNextSyncData(afterRefreshEventData);
|
||||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
httpBackend!.when("GET", "/sync").respond(200, function() {
|
||||||
return NEXT_SYNC_DATA;
|
return NEXT_SYNC_DATA;
|
||||||
});
|
});
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
httpBackend.flushAllExpected(),
|
httpBackend!.flushAllExpected(),
|
||||||
utils.syncPromise(client, 1),
|
utils.syncPromise(client!, 1),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Make sure the timeline includes the the events from the `/sync`
|
// Make sure the timeline includes the the events from the `/sync`
|
||||||
@@ -794,7 +802,7 @@ describe("MatrixClient room timelines", function() {
|
|||||||
it('Timeline recovers after `/context` request to generate new timeline fails', async () => {
|
it('Timeline recovers after `/context` request to generate new timeline fails', async () => {
|
||||||
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
|
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
|
||||||
// to construct a new timeline from.
|
// to construct a new timeline from.
|
||||||
httpBackend.when("GET", contextUrl)
|
httpBackend!.when("GET", contextUrl)
|
||||||
.respond(500, function() {
|
.respond(500, function() {
|
||||||
// The timeline should be cleared at this point in the refresh
|
// The timeline should be cleared at this point in the refresh
|
||||||
expect(room.timeline.length).toEqual(0);
|
expect(room.timeline.length).toEqual(0);
|
||||||
@@ -809,7 +817,7 @@ describe("MatrixClient room timelines", function() {
|
|||||||
// Refresh the timeline and expect it to fail
|
// Refresh the timeline and expect it to fail
|
||||||
const settledFailedRefreshPromises = await Promise.allSettled([
|
const settledFailedRefreshPromises = await Promise.allSettled([
|
||||||
room.refreshLiveTimeline(),
|
room.refreshLiveTimeline(),
|
||||||
httpBackend.flushAllExpected(),
|
httpBackend!.flushAllExpected(),
|
||||||
]);
|
]);
|
||||||
// We only expect `TEST_FAKE_ERROR` here. Anything else is
|
// We only expect `TEST_FAKE_ERROR` here. Anything else is
|
||||||
// unexpected and should fail the test.
|
// unexpected and should fail the test.
|
||||||
@@ -825,7 +833,7 @@ describe("MatrixClient room timelines", function() {
|
|||||||
|
|
||||||
// `/messages` request for `refreshLiveTimeline()` ->
|
// `/messages` request for `refreshLiveTimeline()` ->
|
||||||
// `getLatestTimeline()` to construct a new timeline from.
|
// `getLatestTimeline()` to construct a new timeline from.
|
||||||
httpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`)
|
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`)
|
||||||
.respond(200, function() {
|
.respond(200, function() {
|
||||||
return {
|
return {
|
||||||
chunk: [{
|
chunk: [{
|
||||||
@@ -837,7 +845,7 @@ describe("MatrixClient room timelines", function() {
|
|||||||
// `/context` request for `refreshLiveTimeline()` ->
|
// `/context` request for `refreshLiveTimeline()` ->
|
||||||
// `getLatestTimeline()` -> `getEventTimeline()` to construct a new
|
// `getLatestTimeline()` -> `getEventTimeline()` to construct a new
|
||||||
// timeline from.
|
// timeline from.
|
||||||
httpBackend.when("GET", contextUrl)
|
httpBackend!.when("GET", contextUrl)
|
||||||
.respond(200, function() {
|
.respond(200, function() {
|
||||||
// The timeline should be cleared at this point in the refresh
|
// The timeline should be cleared at this point in the refresh
|
||||||
expect(room.timeline.length).toEqual(0);
|
expect(room.timeline.length).toEqual(0);
|
||||||
@@ -848,7 +856,7 @@ describe("MatrixClient room timelines", function() {
|
|||||||
// Refresh the timeline again but this time it should pass
|
// Refresh the timeline again but this time it should pass
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
room.refreshLiveTimeline(),
|
room.refreshLiveTimeline(),
|
||||||
httpBackend.flushAllExpected(),
|
httpBackend!.flushAllExpected(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Make sure sync pagination still works by seeing a new message show up
|
// Make sure sync pagination still works by seeing a new message show up
|
||||||
@@ -857,12 +865,12 @@ describe("MatrixClient room timelines", function() {
|
|||||||
utils.mkMessage({ user: userId, room: roomId }),
|
utils.mkMessage({ user: userId, room: roomId }),
|
||||||
];
|
];
|
||||||
setNextSyncData(afterRefreshEventData);
|
setNextSyncData(afterRefreshEventData);
|
||||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
httpBackend!.when("GET", "/sync").respond(200, function() {
|
||||||
return NEXT_SYNC_DATA;
|
return NEXT_SYNC_DATA;
|
||||||
});
|
});
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
httpBackend.flushAllExpected(),
|
httpBackend!.flushAllExpected(),
|
||||||
utils.syncPromise(client, 1),
|
utils.syncPromise(client!, 1),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Make sure the message are visible
|
// Make sure the message are visible
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -95,26 +95,31 @@ describe("megolm key backups", function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const Olm = global.Olm;
|
const Olm = global.Olm;
|
||||||
|
let testOlmAccount: Olm.Account;
|
||||||
let testOlmAccount: Account;
|
|
||||||
let aliceTestClient: TestClient;
|
let aliceTestClient: TestClient;
|
||||||
|
|
||||||
|
const setupTestClient = (): [Account, TestClient] => {
|
||||||
|
const aliceTestClient = new TestClient(
|
||||||
|
"@alice:localhost", "xzcvb", "akjgkrgjs",
|
||||||
|
);
|
||||||
|
const testOlmAccount = new Olm.Account();
|
||||||
|
testOlmAccount!.create();
|
||||||
|
|
||||||
|
return [testOlmAccount, aliceTestClient];
|
||||||
|
};
|
||||||
|
|
||||||
beforeAll(function() {
|
beforeAll(function() {
|
||||||
return Olm.init();
|
return Olm.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
aliceTestClient = new TestClient(
|
[testOlmAccount, aliceTestClient] = setupTestClient();
|
||||||
"@alice:localhost", "xzcvb", "akjgkrgjs",
|
await aliceTestClient!.client.initCrypto();
|
||||||
);
|
aliceTestClient!.client.crypto!.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
|
||||||
testOlmAccount = new Olm.Account();
|
|
||||||
testOlmAccount.create();
|
|
||||||
await aliceTestClient.client.initCrypto();
|
|
||||||
aliceTestClient.client.crypto.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function() {
|
afterEach(function() {
|
||||||
return aliceTestClient.stop();
|
return aliceTestClient!.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Alice checks key backups when receiving a message she can't decrypt", function() {
|
it("Alice checks key backups when receiving a message she can't decrypt", function() {
|
||||||
@@ -130,22 +135,22 @@ describe("megolm key backups", function() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return aliceTestClient.start().then(() => {
|
return aliceTestClient!.start().then(() => {
|
||||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const privkey = decodeRecoveryKey(RECOVERY_KEY);
|
const privkey = decodeRecoveryKey(RECOVERY_KEY);
|
||||||
return aliceTestClient.client.crypto.storeSessionBackupPrivateKey(privkey);
|
return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||||
aliceTestClient.expectKeyBackupQuery(
|
aliceTestClient!.expectKeyBackupQuery(
|
||||||
ROOM_ID,
|
ROOM_ID,
|
||||||
SESSION_ID,
|
SESSION_ID,
|
||||||
200,
|
200,
|
||||||
CURVE25519_KEY_BACKUP_DATA,
|
CURVE25519_KEY_BACKUP_DATA,
|
||||||
);
|
);
|
||||||
return aliceTestClient.httpBackend.flushAllExpected();
|
return aliceTestClient!.httpBackend.flushAllExpected();
|
||||||
}).then(function(): Promise<MatrixEvent> {
|
}).then(function(): Promise<MatrixEvent> {
|
||||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
const room = aliceTestClient!.client.getRoom(ROOM_ID)!;
|
||||||
const event = room.getLiveTimeline().getEvents()[0];
|
const event = room.getLiveTimeline().getEvents()[0];
|
||||||
|
|
||||||
if (event.getContent()) {
|
if (event.getContent()) {
|
||||||
|
|||||||
@@ -29,8 +29,11 @@ import {
|
|||||||
IDownloadKeyResult,
|
IDownloadKeyResult,
|
||||||
MatrixEvent,
|
MatrixEvent,
|
||||||
MatrixEventEvent,
|
MatrixEventEvent,
|
||||||
|
IndexedDBCryptoStore,
|
||||||
|
Room,
|
||||||
} from "../../src/matrix";
|
} from "../../src/matrix";
|
||||||
import { IDeviceKeys } from "../../src/crypto/dehydration";
|
import { IDeviceKeys } from "../../src/crypto/dehydration";
|
||||||
|
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||||
|
|
||||||
const ROOM_ID = "!room:id";
|
const ROOM_ID = "!room:id";
|
||||||
|
|
||||||
@@ -204,9 +207,11 @@ describe("megolm", () => {
|
|||||||
}
|
}
|
||||||
const Olm = global.Olm;
|
const Olm = global.Olm;
|
||||||
|
|
||||||
let testOlmAccount: Olm.Account;
|
let testOlmAccount = {} as unknown as Olm.Account;
|
||||||
let testSenderKey: string;
|
let testSenderKey = '';
|
||||||
let aliceTestClient: TestClient;
|
let aliceTestClient = new TestClient(
|
||||||
|
"@alice:localhost", "device2", "access_token2",
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the device keys for testOlmAccount in a format suitable for a
|
* Get the device keys for testOlmAccount in a format suitable for a
|
||||||
@@ -280,10 +285,13 @@ describe("megolm", () => {
|
|||||||
|
|
||||||
it("Alice receives a megolm message", async () => {
|
it("Alice receives a megolm message", async () => {
|
||||||
await aliceTestClient.start();
|
await aliceTestClient.start();
|
||||||
|
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||||
const groupSession = new Olm.OutboundGroupSession();
|
const groupSession = new Olm.OutboundGroupSession();
|
||||||
groupSession.create();
|
groupSession.create();
|
||||||
|
|
||||||
|
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||||
|
|
||||||
// make the room_key event
|
// make the room_key event
|
||||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||||
senderKey: testSenderKey,
|
senderKey: testSenderKey,
|
||||||
@@ -316,7 +324,7 @@ describe("megolm", () => {
|
|||||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||||
await aliceTestClient.flushSync();
|
await aliceTestClient.flushSync();
|
||||||
|
|
||||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||||
const event = room.getLiveTimeline().getEvents()[0];
|
const event = room.getLiveTimeline().getEvents()[0];
|
||||||
expect(event.isEncrypted()).toBe(true);
|
expect(event.isEncrypted()).toBe(true);
|
||||||
const decryptedEvent = await testUtils.awaitDecryption(event);
|
const decryptedEvent = await testUtils.awaitDecryption(event);
|
||||||
@@ -326,10 +334,13 @@ describe("megolm", () => {
|
|||||||
it("Alice receives a megolm message before the session keys", async () => {
|
it("Alice receives a megolm message before the session keys", async () => {
|
||||||
// https://github.com/vector-im/element-web/issues/2273
|
// https://github.com/vector-im/element-web/issues/2273
|
||||||
await aliceTestClient.start();
|
await aliceTestClient.start();
|
||||||
|
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||||
const groupSession = new Olm.OutboundGroupSession();
|
const groupSession = new Olm.OutboundGroupSession();
|
||||||
groupSession.create();
|
groupSession.create();
|
||||||
|
|
||||||
|
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||||
|
|
||||||
// make the room_key event, but don't send it yet
|
// make the room_key event, but don't send it yet
|
||||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||||
senderKey: testSenderKey,
|
senderKey: testSenderKey,
|
||||||
@@ -353,7 +364,7 @@ describe("megolm", () => {
|
|||||||
});
|
});
|
||||||
await aliceTestClient.flushSync();
|
await aliceTestClient.flushSync();
|
||||||
|
|
||||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||||
expect(room.getLiveTimeline().getEvents()[0].getContent().msgtype).toEqual('m.bad.encrypted');
|
expect(room.getLiveTimeline().getEvents()[0].getContent().msgtype).toEqual('m.bad.encrypted');
|
||||||
|
|
||||||
// now she gets the room_key event
|
// now she gets the room_key event
|
||||||
@@ -383,10 +394,13 @@ describe("megolm", () => {
|
|||||||
|
|
||||||
it("Alice gets a second room_key message", async () => {
|
it("Alice gets a second room_key message", async () => {
|
||||||
await aliceTestClient.start();
|
await aliceTestClient.start();
|
||||||
|
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||||
const groupSession = new Olm.OutboundGroupSession();
|
const groupSession = new Olm.OutboundGroupSession();
|
||||||
groupSession.create();
|
groupSession.create();
|
||||||
|
|
||||||
|
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||||
|
|
||||||
// make the room_key event
|
// make the room_key event
|
||||||
const roomKeyEncrypted1 = encryptGroupSessionKey({
|
const roomKeyEncrypted1 = encryptGroupSessionKey({
|
||||||
senderKey: testSenderKey,
|
senderKey: testSenderKey,
|
||||||
@@ -439,7 +453,7 @@ describe("megolm", () => {
|
|||||||
await aliceTestClient.flushSync();
|
await aliceTestClient.flushSync();
|
||||||
await aliceTestClient.flushSync();
|
await aliceTestClient.flushSync();
|
||||||
|
|
||||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||||
await room.decryptCriticalEvents();
|
await room.decryptCriticalEvents();
|
||||||
const event = room.getLiveTimeline().getEvents()[0];
|
const event = room.getLiveTimeline().getEvents()[0];
|
||||||
expect(event.getContent().body).toEqual('42');
|
expect(event.getContent().body).toEqual('42');
|
||||||
@@ -468,6 +482,9 @@ describe("megolm", () => {
|
|||||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||||
);
|
);
|
||||||
|
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||||
|
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||||
|
);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
|
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
|
||||||
@@ -484,7 +501,7 @@ describe("megolm", () => {
|
|||||||
let inboundGroupSession: Olm.InboundGroupSession;
|
let inboundGroupSession: Olm.InboundGroupSession;
|
||||||
aliceTestClient.httpBackend.when(
|
aliceTestClient.httpBackend.when(
|
||||||
'PUT', '/sendToDevice/m.room.encrypted/',
|
'PUT', '/sendToDevice/m.room.encrypted/',
|
||||||
).respond(200, function(_path, content) {
|
).respond(200, function(_path, content: any) {
|
||||||
const m = content.messages['@bob:xyz'].DEVICE_ID;
|
const m = content.messages['@bob:xyz'].DEVICE_ID;
|
||||||
const ct = m.ciphertext[testSenderKey];
|
const ct = m.ciphertext[testSenderKey];
|
||||||
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
|
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
|
||||||
@@ -510,7 +527,7 @@ describe("megolm", () => {
|
|||||||
return { event_id: '$event_id' };
|
return { event_id: '$event_id' };
|
||||||
});
|
});
|
||||||
|
|
||||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||||
const pendingMsg = room.getPendingEvents()[0];
|
const pendingMsg = room.getPendingEvents()[0];
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -541,13 +558,16 @@ describe("megolm", () => {
|
|||||||
|
|
||||||
logger.log('Forcing alice to download our device keys');
|
logger.log('Forcing alice to download our device keys');
|
||||||
|
|
||||||
|
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||||
|
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||||
|
);
|
||||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||||
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
aliceTestClient.httpBackend.flush('/keys/query', 2),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
logger.log('Telling alice to block our device');
|
logger.log('Telling alice to block our device');
|
||||||
@@ -592,6 +612,9 @@ describe("megolm", () => {
|
|||||||
|
|
||||||
logger.log("Fetching bob's devices and marking known");
|
logger.log("Fetching bob's devices and marking known");
|
||||||
|
|
||||||
|
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||||
|
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||||
|
);
|
||||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||||
);
|
);
|
||||||
@@ -607,7 +630,7 @@ describe("megolm", () => {
|
|||||||
let megolmSessionId: string;
|
let megolmSessionId: string;
|
||||||
aliceTestClient.httpBackend.when(
|
aliceTestClient.httpBackend.when(
|
||||||
'PUT', '/sendToDevice/m.room.encrypted/',
|
'PUT', '/sendToDevice/m.room.encrypted/',
|
||||||
).respond(200, function(_path, content) {
|
).respond(200, function(_path, content: any) {
|
||||||
logger.log('sendToDevice: ', content);
|
logger.log('sendToDevice: ', content);
|
||||||
const m = content.messages['@bob:xyz'].DEVICE_ID;
|
const m = content.messages['@bob:xyz'].DEVICE_ID;
|
||||||
const ct = m.ciphertext[testSenderKey];
|
const ct = m.ciphertext[testSenderKey];
|
||||||
@@ -685,7 +708,7 @@ describe("megolm", () => {
|
|||||||
// invalidate the device cache for all members in e2e rooms (ie,
|
// invalidate the device cache for all members in e2e rooms (ie,
|
||||||
// herself), and do a key query.
|
// herself), and do a key query.
|
||||||
aliceTestClient.expectKeyQuery(
|
aliceTestClient.expectKeyQuery(
|
||||||
getTestKeysQueryResponse(aliceTestClient.userId),
|
getTestKeysQueryResponse(aliceTestClient.userId!),
|
||||||
);
|
);
|
||||||
|
|
||||||
await aliceTestClient.httpBackend.flushAllExpected();
|
await aliceTestClient.httpBackend.flushAllExpected();
|
||||||
@@ -695,28 +718,30 @@ describe("megolm", () => {
|
|||||||
await aliceTestClient.client.sendTextMessage(ROOM_ID, 'test');
|
await aliceTestClient.client.sendTextMessage(ROOM_ID, 'test');
|
||||||
throw new Error("sendTextMessage succeeded on an unknown device");
|
throw new Error("sendTextMessage succeeded on an unknown device");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.name).toEqual("UnknownDeviceError");
|
expect((e as any).name).toEqual("UnknownDeviceError");
|
||||||
expect(Object.keys(e.devices)).toEqual([aliceTestClient.userId]);
|
expect(Object.keys((e as any).devices)).toEqual([aliceTestClient.userId!]);
|
||||||
expect(Object.keys(e.devices[aliceTestClient.userId])).
|
expect(Object.keys((e as any)?.devices[aliceTestClient.userId!])).
|
||||||
toEqual(['DEVICE_ID']);
|
toEqual(['DEVICE_ID']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// mark the device as known, and resend.
|
// mark the device as known, and resend.
|
||||||
aliceTestClient.client.setDeviceKnown(aliceTestClient.userId, 'DEVICE_ID');
|
aliceTestClient.client.setDeviceKnown(aliceTestClient.userId!, 'DEVICE_ID');
|
||||||
aliceTestClient.httpBackend.when('POST', '/keys/claim').respond(
|
aliceTestClient.httpBackend.when('POST', '/keys/claim').respond(
|
||||||
200, function(_path, content) {
|
200, function(_path, content: IClaimOTKsResult) {
|
||||||
expect(content.one_time_keys[aliceTestClient.userId].DEVICE_ID)
|
expect(content.one_time_keys[aliceTestClient.userId!].DEVICE_ID)
|
||||||
.toEqual("signed_curve25519");
|
.toEqual("signed_curve25519");
|
||||||
return getTestKeysClaimResponse(aliceTestClient.userId);
|
return getTestKeysClaimResponse(aliceTestClient.userId!);
|
||||||
});
|
});
|
||||||
|
|
||||||
let p2pSession: Olm.Session;
|
let p2pSession: Olm.Session;
|
||||||
let inboundGroupSession: Olm.InboundGroupSession;
|
let inboundGroupSession: Olm.InboundGroupSession;
|
||||||
aliceTestClient.httpBackend.when(
|
aliceTestClient.httpBackend.when(
|
||||||
'PUT', '/sendToDevice/m.room.encrypted/',
|
'PUT', '/sendToDevice/m.room.encrypted/',
|
||||||
).respond(200, function(_path, content) {
|
).respond(200, function(_path, content: {
|
||||||
|
messages: { [userId: string]: { [deviceId: string]: Record<string, any> }};
|
||||||
|
}) {
|
||||||
logger.log("sendToDevice: ", content);
|
logger.log("sendToDevice: ", content);
|
||||||
const m = content.messages[aliceTestClient.userId].DEVICE_ID;
|
const m = content.messages[aliceTestClient.userId!].DEVICE_ID;
|
||||||
const ct = m.ciphertext[testSenderKey];
|
const ct = m.ciphertext[testSenderKey];
|
||||||
expect(ct.type).toEqual(0); // pre-key message
|
expect(ct.type).toEqual(0); // pre-key message
|
||||||
|
|
||||||
@@ -730,7 +755,7 @@ describe("megolm", () => {
|
|||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
|
|
||||||
let decrypted: IEvent;
|
let decrypted: Partial<IEvent> = {};
|
||||||
aliceTestClient.httpBackend.when(
|
aliceTestClient.httpBackend.when(
|
||||||
'PUT', '/send/',
|
'PUT', '/send/',
|
||||||
).respond(200, function(_path, content: IContent) {
|
).respond(200, function(_path, content: IContent) {
|
||||||
@@ -745,7 +770,7 @@ describe("megolm", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Grab the event that we'll need to resend
|
// Grab the event that we'll need to resend
|
||||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||||
const pendingEvents = room.getPendingEvents();
|
const pendingEvents = room.getPendingEvents();
|
||||||
expect(pendingEvents.length).toEqual(1);
|
expect(pendingEvents.length).toEqual(1);
|
||||||
const unsentEvent = pendingEvents[0];
|
const unsentEvent = pendingEvents[0];
|
||||||
@@ -760,7 +785,7 @@ describe("megolm", () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
expect(decrypted.type).toEqual('m.room.message');
|
expect(decrypted.type).toEqual('m.room.message');
|
||||||
expect(decrypted.content.body).toEqual('test');
|
expect(decrypted.content?.body).toEqual('test');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Alice should wait for device list to complete when sending a megolm message', async () => {
|
it('Alice should wait for device list to complete when sending a megolm message', async () => {
|
||||||
@@ -786,6 +811,10 @@ describe("megolm", () => {
|
|||||||
logger.log('Forcing alice to download our device keys');
|
logger.log('Forcing alice to download our device keys');
|
||||||
const downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
const downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||||
|
|
||||||
|
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||||
|
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||||
|
);
|
||||||
|
|
||||||
// so will this.
|
// so will this.
|
||||||
const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test')
|
const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -805,9 +834,12 @@ describe("megolm", () => {
|
|||||||
it("Alice exports megolm keys and imports them to a new device", async () => {
|
it("Alice exports megolm keys and imports them to a new device", async () => {
|
||||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} }, failures: {} });
|
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} }, failures: {} });
|
||||||
await aliceTestClient.start();
|
await aliceTestClient.start();
|
||||||
|
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
// establish an olm session with alice
|
// establish an olm session with alice
|
||||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||||
|
|
||||||
|
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||||
|
|
||||||
const groupSession = new Olm.OutboundGroupSession();
|
const groupSession = new Olm.OutboundGroupSession();
|
||||||
groupSession.create();
|
groupSession.create();
|
||||||
|
|
||||||
@@ -839,7 +871,7 @@ describe("megolm", () => {
|
|||||||
});
|
});
|
||||||
await aliceTestClient.flushSync();
|
await aliceTestClient.flushSync();
|
||||||
|
|
||||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||||
await room.decryptCriticalEvents();
|
await room.decryptCriticalEvents();
|
||||||
expect(room.getLiveTimeline().getEvents()[0].getContent().body).toEqual('42');
|
expect(room.getLiveTimeline().getEvents()[0].getContent().body).toEqual('42');
|
||||||
|
|
||||||
@@ -855,6 +887,8 @@ describe("megolm", () => {
|
|||||||
await aliceTestClient.client.importRoomKeys(exported);
|
await aliceTestClient.client.importRoomKeys(exported);
|
||||||
await aliceTestClient.start();
|
await aliceTestClient.start();
|
||||||
|
|
||||||
|
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||||
|
|
||||||
const syncResponse = {
|
const syncResponse = {
|
||||||
next_batch: 1,
|
next_batch: 1,
|
||||||
rooms: {
|
rooms: {
|
||||||
@@ -897,7 +931,7 @@ describe("megolm", () => {
|
|||||||
...rawEvent,
|
...rawEvent,
|
||||||
room: ROOM_ID,
|
room: ROOM_ID,
|
||||||
});
|
});
|
||||||
await event1.attemptDecryption(testClient.client.crypto, { isRetry: true });
|
await event1.attemptDecryption(testClient.client.crypto!, { isRetry: true });
|
||||||
expect(event1.isKeySourceUntrusted()).toBeTruthy();
|
expect(event1.isKeySourceUntrusted()).toBeTruthy();
|
||||||
|
|
||||||
const event2 = testUtils.mkEvent({
|
const event2 = testUtils.mkEvent({
|
||||||
@@ -913,24 +947,27 @@ describe("megolm", () => {
|
|||||||
// @ts-ignore - private
|
// @ts-ignore - private
|
||||||
event2.senderCurve25519Key = testSenderKey;
|
event2.senderCurve25519Key = testSenderKey;
|
||||||
// @ts-ignore - private
|
// @ts-ignore - private
|
||||||
testClient.client.crypto.onRoomKeyEvent(event2);
|
testClient.client.crypto!.onRoomKeyEvent(event2);
|
||||||
|
|
||||||
const event3 = testUtils.mkEvent({
|
const event3 = testUtils.mkEvent({
|
||||||
event: true,
|
event: true,
|
||||||
...rawEvent,
|
...rawEvent,
|
||||||
room: ROOM_ID,
|
room: ROOM_ID,
|
||||||
});
|
});
|
||||||
await event3.attemptDecryption(testClient.client.crypto, { isRetry: true });
|
await event3.attemptDecryption(testClient.client.crypto!, { isRetry: true });
|
||||||
expect(event3.isKeySourceUntrusted()).toBeFalsy();
|
expect(event3.isKeySourceUntrusted()).toBeFalsy();
|
||||||
testClient.stop();
|
testClient.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Alice can decrypt a message with falsey content", async () => {
|
it("Alice can decrypt a message with falsey content", async () => {
|
||||||
await aliceTestClient.start();
|
await aliceTestClient.start();
|
||||||
|
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||||
const groupSession = new Olm.OutboundGroupSession();
|
const groupSession = new Olm.OutboundGroupSession();
|
||||||
groupSession.create();
|
groupSession.create();
|
||||||
|
|
||||||
|
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||||
|
|
||||||
// make the room_key event
|
// make the room_key event
|
||||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||||
senderKey: testSenderKey,
|
senderKey: testSenderKey,
|
||||||
@@ -972,7 +1009,7 @@ describe("megolm", () => {
|
|||||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||||
await aliceTestClient.flushSync();
|
await aliceTestClient.flushSync();
|
||||||
|
|
||||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||||
const event = room.getLiveTimeline().getEvents()[0];
|
const event = room.getLiveTimeline().getEvents()[0];
|
||||||
expect(event.isEncrypted()).toBe(true);
|
expect(event.isEncrypted()).toBe(true);
|
||||||
const decryptedEvent = await testUtils.awaitDecryption(event);
|
const decryptedEvent = await testUtils.awaitDecryption(event);
|
||||||
@@ -985,10 +1022,13 @@ describe("megolm", () => {
|
|||||||
"should successfully decrypt bundled redaction events that don't include a room_id in their /sync data",
|
"should successfully decrypt bundled redaction events that don't include a room_id in their /sync data",
|
||||||
async () => {
|
async () => {
|
||||||
await aliceTestClient.start();
|
await aliceTestClient.start();
|
||||||
|
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||||
const groupSession = new Olm.OutboundGroupSession();
|
const groupSession = new Olm.OutboundGroupSession();
|
||||||
groupSession.create();
|
groupSession.create();
|
||||||
|
|
||||||
|
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||||
|
|
||||||
// make the room_key event
|
// make the room_key event
|
||||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||||
senderKey: testSenderKey,
|
senderKey: testSenderKey,
|
||||||
@@ -1036,13 +1076,292 @@ describe("megolm", () => {
|
|||||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||||
await aliceTestClient.flushSync();
|
await aliceTestClient.flushSync();
|
||||||
|
|
||||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||||
const event = room.getLiveTimeline().getEvents()[0];
|
const event = room.getLiveTimeline().getEvents()[0];
|
||||||
expect(event.isEncrypted()).toBe(true);
|
expect(event.isEncrypted()).toBe(true);
|
||||||
await event.attemptDecryption(aliceTestClient.client.crypto);
|
await event.attemptDecryption(aliceTestClient.client.crypto!);
|
||||||
expect(event.getContent()).toEqual({});
|
expect(event.getContent()).toEqual({});
|
||||||
const redactionEvent: any = event.getRedactionEvent();
|
const redactionEvent: any = event.getRedactionEvent();
|
||||||
expect(redactionEvent.content.reason).toEqual("redaction test");
|
expect(redactionEvent.content.reason).toEqual("redaction test");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it("Alice receives shared history before being invited to a room by the sharer", async () => {
|
||||||
|
const beccaTestClient = new TestClient(
|
||||||
|
"@becca:localhost", "foobar", "bazquux",
|
||||||
|
);
|
||||||
|
await beccaTestClient.client.initCrypto();
|
||||||
|
|
||||||
|
await aliceTestClient.start();
|
||||||
|
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
|
await beccaTestClient.start();
|
||||||
|
|
||||||
|
const beccaRoom = new Room(ROOM_ID, beccaTestClient.client, "@becca:localhost", {});
|
||||||
|
beccaTestClient.client.store.storeRoom(beccaRoom);
|
||||||
|
await beccaTestClient.client.setRoomEncryption(ROOM_ID, { "algorithm": "m.megolm.v1.aes-sha2" });
|
||||||
|
|
||||||
|
const event = new MatrixEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: "@becca:localhost",
|
||||||
|
room_id: ROOM_ID,
|
||||||
|
event_id: "$1",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "test message",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await beccaTestClient.client.crypto!.encryptEvent(event, beccaRoom);
|
||||||
|
// remove keys from the event
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.clearEvent = undefined;
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.senderCurve25519Key = null;
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.claimedEd25519Key = null;
|
||||||
|
|
||||||
|
const device = new DeviceInfo(beccaTestClient.client.deviceId!);
|
||||||
|
aliceTestClient.client.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||||
|
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => beccaTestClient.client.getUserId()!;
|
||||||
|
|
||||||
|
// Create an olm session for Becca and Alice's devices
|
||||||
|
const aliceOtks = await aliceTestClient.awaitOneTimeKeyUpload();
|
||||||
|
const aliceOtkId = Object.keys(aliceOtks)[0];
|
||||||
|
const aliceOtk = aliceOtks[aliceOtkId];
|
||||||
|
const p2pSession = new global.Olm.Session();
|
||||||
|
await beccaTestClient.client.crypto!.cryptoStore.doTxn(
|
||||||
|
'readonly',
|
||||||
|
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||||
|
(txn) => {
|
||||||
|
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string) => {
|
||||||
|
const account = new global.Olm.Account();
|
||||||
|
try {
|
||||||
|
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount);
|
||||||
|
p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
|
||||||
|
} finally {
|
||||||
|
account.free();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = event.getWireContent();
|
||||||
|
const groupSessionKey = await beccaTestClient.client.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||||
|
ROOM_ID,
|
||||||
|
content.sender_key,
|
||||||
|
content.session_id,
|
||||||
|
);
|
||||||
|
const encryptedForwardedKey = encryptOlmEvent({
|
||||||
|
sender: "@becca:localhost",
|
||||||
|
senderKey: beccaTestClient.getDeviceKey(),
|
||||||
|
recipient: aliceTestClient,
|
||||||
|
p2pSession: p2pSession,
|
||||||
|
plaincontent: {
|
||||||
|
"algorithm": 'm.megolm.v1.aes-sha2',
|
||||||
|
"room_id": ROOM_ID,
|
||||||
|
"sender_key": content.sender_key,
|
||||||
|
"sender_claimed_ed25519_key": groupSessionKey.sender_claimed_ed25519_key,
|
||||||
|
"session_id": content.session_id,
|
||||||
|
"session_key": groupSessionKey.key,
|
||||||
|
"chain_index": groupSessionKey.chain_index,
|
||||||
|
"forwarding_curve25519_key_chain": groupSessionKey.forwarding_curve25519_key_chain,
|
||||||
|
"org.matrix.msc3061.shared_history": true,
|
||||||
|
},
|
||||||
|
plaintype: 'm.forwarded_room_key',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Alice receives shared history
|
||||||
|
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||||
|
next_batch: 1,
|
||||||
|
to_device: { events: [encryptedForwardedKey] },
|
||||||
|
});
|
||||||
|
await aliceTestClient.flushSync();
|
||||||
|
|
||||||
|
// Alice is invited to the room by Becca
|
||||||
|
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||||
|
next_batch: 2,
|
||||||
|
rooms: { invite: { [ROOM_ID]: { invite_state: { events: [
|
||||||
|
{
|
||||||
|
sender: '@becca:localhost',
|
||||||
|
type: 'm.room.encryption',
|
||||||
|
state_key: '',
|
||||||
|
content: {
|
||||||
|
algorithm: 'm.megolm.v1.aes-sha2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sender: '@becca:localhost',
|
||||||
|
type: 'm.room.member',
|
||||||
|
state_key: '@alice:localhost',
|
||||||
|
content: {
|
||||||
|
membership: 'invite',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] } } } },
|
||||||
|
});
|
||||||
|
await aliceTestClient.flushSync();
|
||||||
|
|
||||||
|
// Alice has joined the room
|
||||||
|
aliceTestClient.httpBackend.when("GET", "/sync").respond(
|
||||||
|
200, getSyncResponse(["@alice:localhost", "@becca:localhost"]),
|
||||||
|
);
|
||||||
|
await aliceTestClient.flushSync();
|
||||||
|
|
||||||
|
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||||
|
next_batch: 4,
|
||||||
|
rooms: {
|
||||||
|
join: {
|
||||||
|
[ROOM_ID]: { timeline: { events: [event.event] } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await aliceTestClient.flushSync();
|
||||||
|
|
||||||
|
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||||
|
const roomEvent = room.getLiveTimeline().getEvents()[0];
|
||||||
|
expect(roomEvent.isEncrypted()).toBe(true);
|
||||||
|
const decryptedEvent = await testUtils.awaitDecryption(roomEvent);
|
||||||
|
expect(decryptedEvent.getContent().body).toEqual('test message');
|
||||||
|
|
||||||
|
await beccaTestClient.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Alice receives shared history before being invited to a room by someone else", async () => {
|
||||||
|
const beccaTestClient = new TestClient(
|
||||||
|
"@becca:localhost", "foobar", "bazquux",
|
||||||
|
);
|
||||||
|
await beccaTestClient.client.initCrypto();
|
||||||
|
|
||||||
|
await aliceTestClient.start();
|
||||||
|
await beccaTestClient.start();
|
||||||
|
|
||||||
|
const beccaRoom = new Room(ROOM_ID, beccaTestClient.client, "@becca:localhost", {});
|
||||||
|
beccaTestClient.client.store.storeRoom(beccaRoom);
|
||||||
|
await beccaTestClient.client.setRoomEncryption(ROOM_ID, { "algorithm": "m.megolm.v1.aes-sha2" });
|
||||||
|
|
||||||
|
const event = new MatrixEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: "@becca:localhost",
|
||||||
|
room_id: ROOM_ID,
|
||||||
|
event_id: "$1",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "test message",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await beccaTestClient.client.crypto!.encryptEvent(event, beccaRoom);
|
||||||
|
// remove keys from the event
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.clearEvent = undefined;
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.senderCurve25519Key = null;
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.claimedEd25519Key = null;
|
||||||
|
|
||||||
|
const device = new DeviceInfo(beccaTestClient.client.deviceId!);
|
||||||
|
aliceTestClient.client.crypto!.deviceList.getDeviceByIdentityKey = () => device;
|
||||||
|
|
||||||
|
// Create an olm session for Becca and Alice's devices
|
||||||
|
const aliceOtks = await aliceTestClient.awaitOneTimeKeyUpload();
|
||||||
|
const aliceOtkId = Object.keys(aliceOtks)[0];
|
||||||
|
const aliceOtk = aliceOtks[aliceOtkId];
|
||||||
|
const p2pSession = new global.Olm.Session();
|
||||||
|
await beccaTestClient.client.crypto!.cryptoStore.doTxn(
|
||||||
|
'readonly',
|
||||||
|
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||||
|
(txn) => {
|
||||||
|
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string) => {
|
||||||
|
const account = new global.Olm.Account();
|
||||||
|
try {
|
||||||
|
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount);
|
||||||
|
p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
|
||||||
|
} finally {
|
||||||
|
account.free();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = event.getWireContent();
|
||||||
|
const groupSessionKey = await beccaTestClient.client.crypto!.olmDevice.getInboundGroupSessionKey(
|
||||||
|
ROOM_ID,
|
||||||
|
content.sender_key,
|
||||||
|
content.session_id,
|
||||||
|
);
|
||||||
|
const encryptedForwardedKey = encryptOlmEvent({
|
||||||
|
sender: "@becca:localhost",
|
||||||
|
senderKey: beccaTestClient.getDeviceKey(),
|
||||||
|
recipient: aliceTestClient,
|
||||||
|
p2pSession: p2pSession,
|
||||||
|
plaincontent: {
|
||||||
|
"algorithm": 'm.megolm.v1.aes-sha2',
|
||||||
|
"room_id": ROOM_ID,
|
||||||
|
"sender_key": content.sender_key,
|
||||||
|
"sender_claimed_ed25519_key": groupSessionKey.sender_claimed_ed25519_key,
|
||||||
|
"session_id": content.session_id,
|
||||||
|
"session_key": groupSessionKey.key,
|
||||||
|
"chain_index": groupSessionKey.chain_index,
|
||||||
|
"forwarding_curve25519_key_chain": groupSessionKey.forwarding_curve25519_key_chain,
|
||||||
|
"org.matrix.msc3061.shared_history": true,
|
||||||
|
},
|
||||||
|
plaintype: 'm.forwarded_room_key',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Alice receives forwarded history from Becca
|
||||||
|
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||||
|
next_batch: 1,
|
||||||
|
to_device: { events: [encryptedForwardedKey] },
|
||||||
|
});
|
||||||
|
await aliceTestClient.flushSync();
|
||||||
|
|
||||||
|
// Alice is invited to the room by Charlie
|
||||||
|
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||||
|
next_batch: 2,
|
||||||
|
rooms: { invite: { [ROOM_ID]: { invite_state: { events: [
|
||||||
|
{
|
||||||
|
sender: '@becca:localhost',
|
||||||
|
type: 'm.room.encryption',
|
||||||
|
state_key: '',
|
||||||
|
content: {
|
||||||
|
algorithm: 'm.megolm.v1.aes-sha2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sender: '@charlie:localhost',
|
||||||
|
type: 'm.room.member',
|
||||||
|
state_key: '@alice:localhost',
|
||||||
|
content: {
|
||||||
|
membership: 'invite',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] } } } },
|
||||||
|
});
|
||||||
|
await aliceTestClient.flushSync();
|
||||||
|
|
||||||
|
// Alice has joined the room
|
||||||
|
aliceTestClient.httpBackend.when("GET", "/sync").respond(
|
||||||
|
200, getSyncResponse(["@alice:localhost", "@becca:localhost", "@charlie:localhost"]),
|
||||||
|
);
|
||||||
|
await aliceTestClient.flushSync();
|
||||||
|
|
||||||
|
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||||
|
next_batch: 4,
|
||||||
|
rooms: {
|
||||||
|
join: {
|
||||||
|
[ROOM_ID]: { timeline: { events: [event.event] } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await aliceTestClient.flushSync();
|
||||||
|
|
||||||
|
// Decryption should fail, because Alice hasn't received any keys she can trust
|
||||||
|
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||||
|
const roomEvent = room.getLiveTimeline().getEvents()[0];
|
||||||
|
expect(roomEvent.isEncrypted()).toBe(true);
|
||||||
|
const decryptedEvent = await testUtils.awaitDecryption(roomEvent);
|
||||||
|
expect(decryptedEvent.isDecryptionFailure()).toBe(true);
|
||||||
|
|
||||||
|
await beccaTestClient.stop();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ import { IStoredClientOpts } from "../../src/client";
|
|||||||
import { logger } from "../../src/logger";
|
import { logger } from "../../src/logger";
|
||||||
|
|
||||||
describe("SlidingSyncSdk", () => {
|
describe("SlidingSyncSdk", () => {
|
||||||
let client: MatrixClient = null;
|
let client: MatrixClient | undefined;
|
||||||
let httpBackend: MockHttpBackend = null;
|
let httpBackend: MockHttpBackend | undefined;
|
||||||
let sdk: SlidingSyncSdk = null;
|
let sdk: SlidingSyncSdk | undefined;
|
||||||
let mockSlidingSync: SlidingSync = null;
|
let mockSlidingSync: SlidingSync | undefined;
|
||||||
const selfUserId = "@alice:localhost";
|
const selfUserId = "@alice:localhost";
|
||||||
const selfAccessToken = "aseukfgwef";
|
const selfAccessToken = "aseukfgwef";
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
event_id: "$" + eventIdCounter,
|
event_id: "$" + eventIdCounter,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const mkOwnStateEvent = (evType: string, content: object, stateKey?: string): IStateEvent => {
|
const mkOwnStateEvent = (evType: string, content: object, stateKey = ''): IStateEvent => {
|
||||||
eventIdCounter++;
|
eventIdCounter++;
|
||||||
return {
|
return {
|
||||||
type: evType,
|
type: evType,
|
||||||
@@ -103,24 +103,24 @@ describe("SlidingSyncSdk", () => {
|
|||||||
client = testClient.client;
|
client = testClient.client;
|
||||||
mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0));
|
mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0));
|
||||||
if (testOpts.withCrypto) {
|
if (testOpts.withCrypto) {
|
||||||
httpBackend.when("GET", "/room_keys/version").respond(404, {});
|
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
|
||||||
await client.initCrypto();
|
await client!.initCrypto();
|
||||||
testOpts.crypto = client.crypto;
|
testOpts.crypto = client!.crypto;
|
||||||
}
|
}
|
||||||
httpBackend.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
|
httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
|
||||||
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts);
|
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts);
|
||||||
};
|
};
|
||||||
|
|
||||||
// tear down client/httpBackend globals
|
// tear down client/httpBackend globals
|
||||||
const teardownClient = () => {
|
const teardownClient = () => {
|
||||||
client.stopClient();
|
client!.stopClient();
|
||||||
return httpBackend.stop();
|
return httpBackend!.stop();
|
||||||
};
|
};
|
||||||
|
|
||||||
// find an extension on a SlidingSyncSdk instance
|
// find an extension on a SlidingSyncSdk instance
|
||||||
const findExtension = (name: string): Extension => {
|
const findExtension = (name: string): Extension => {
|
||||||
expect(mockSlidingSync.registerExtension).toHaveBeenCalled();
|
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
|
||||||
const mockFn = mockSlidingSync.registerExtension as jest.Mock;
|
const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
|
||||||
// find the extension
|
// find the extension
|
||||||
for (let i = 0; i < mockFn.mock.calls.length; i++) {
|
for (let i = 0; i < mockFn.mock.calls.length; i++) {
|
||||||
const calledExtension = mockFn.mock.calls[i][0] as Extension;
|
const calledExtension = mockFn.mock.calls[i][0] as Extension;
|
||||||
@@ -137,14 +137,14 @@ describe("SlidingSyncSdk", () => {
|
|||||||
});
|
});
|
||||||
afterAll(teardownClient);
|
afterAll(teardownClient);
|
||||||
it("can sync()", async () => {
|
it("can sync()", async () => {
|
||||||
const hasSynced = sdk.sync();
|
const hasSynced = sdk!.sync();
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await hasSynced;
|
await hasSynced;
|
||||||
expect(mockSlidingSync.start).toBeCalled();
|
expect(mockSlidingSync!.start).toBeCalled();
|
||||||
});
|
});
|
||||||
it("can stop()", async () => {
|
it("can stop()", async () => {
|
||||||
sdk.stop();
|
sdk!.stop();
|
||||||
expect(mockSlidingSync.stop).toBeCalled();
|
expect(mockSlidingSync!.stop).toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -156,8 +156,8 @@ describe("SlidingSyncSdk", () => {
|
|||||||
|
|
||||||
describe("initial", () => {
|
describe("initial", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const hasSynced = sdk.sync();
|
const hasSynced = sdk!.sync();
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await hasSynced;
|
await hasSynced;
|
||||||
});
|
});
|
||||||
// inject some rooms with different fields set.
|
// inject some rooms with different fields set.
|
||||||
@@ -168,6 +168,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
const roomD = "!d_with_notif_count:localhost";
|
const roomD = "!d_with_notif_count:localhost";
|
||||||
const roomE = "!e_with_invite:localhost";
|
const roomE = "!e_with_invite:localhost";
|
||||||
const roomF = "!f_calc_room_name:localhost";
|
const roomF = "!f_calc_room_name:localhost";
|
||||||
|
const roomG = "!g_join_invite_counts:localhost";
|
||||||
const data: Record<string, MSC3575RoomData> = {
|
const data: Record<string, MSC3575RoomData> = {
|
||||||
[roomA]: {
|
[roomA]: {
|
||||||
name: "A",
|
name: "A",
|
||||||
@@ -261,56 +262,83 @@ describe("SlidingSyncSdk", () => {
|
|||||||
],
|
],
|
||||||
initial: true,
|
initial: true,
|
||||||
},
|
},
|
||||||
|
[roomG]: {
|
||||||
|
name: "G",
|
||||||
|
required_state: [],
|
||||||
|
timeline: [
|
||||||
|
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||||
|
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||||
|
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||||
|
],
|
||||||
|
joined_count: 5,
|
||||||
|
invited_count: 2,
|
||||||
|
initial: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
it("can be created with required_state and timeline", () => {
|
it("can be created with required_state and timeline", () => {
|
||||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
|
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
|
||||||
const gotRoom = client.getRoom(roomA);
|
const gotRoom = client!.getRoom(roomA);
|
||||||
expect(gotRoom).toBeDefined();
|
expect(gotRoom).toBeDefined();
|
||||||
|
if (gotRoom == null) { return; }
|
||||||
expect(gotRoom.name).toEqual(data[roomA].name);
|
expect(gotRoom.name).toEqual(data[roomA].name);
|
||||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
|
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can be created with timeline only", () => {
|
it("can be created with timeline only", () => {
|
||||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
|
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
|
||||||
const gotRoom = client.getRoom(roomB);
|
const gotRoom = client!.getRoom(roomB);
|
||||||
expect(gotRoom).toBeDefined();
|
expect(gotRoom).toBeDefined();
|
||||||
|
if (gotRoom == null) { return; }
|
||||||
expect(gotRoom.name).toEqual(data[roomB].name);
|
expect(gotRoom.name).toEqual(data[roomB].name);
|
||||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
|
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can be created with a highlight_count", () => {
|
it("can be created with a highlight_count", () => {
|
||||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
|
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
|
||||||
const gotRoom = client.getRoom(roomC);
|
const gotRoom = client!.getRoom(roomC);
|
||||||
expect(gotRoom).toBeDefined();
|
expect(gotRoom).toBeDefined();
|
||||||
|
if (gotRoom == null) { return; }
|
||||||
expect(
|
expect(
|
||||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
|
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
|
||||||
).toEqual(data[roomC].highlight_count);
|
).toEqual(data[roomC].highlight_count);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can be created with a notification_count", () => {
|
it("can be created with a notification_count", () => {
|
||||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
|
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
|
||||||
const gotRoom = client.getRoom(roomD);
|
const gotRoom = client!.getRoom(roomD);
|
||||||
expect(gotRoom).toBeDefined();
|
expect(gotRoom).toBeDefined();
|
||||||
|
if (gotRoom == null) { return; }
|
||||||
expect(
|
expect(
|
||||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
|
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
|
||||||
).toEqual(data[roomD].notification_count);
|
).toEqual(data[roomD].notification_count);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can be created with invite_state", () => {
|
it("can be created with an invited/joined_count", () => {
|
||||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
|
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
|
||||||
const gotRoom = client.getRoom(roomE);
|
const gotRoom = client!.getRoom(roomG);
|
||||||
expect(gotRoom).toBeDefined();
|
expect(gotRoom).toBeDefined();
|
||||||
|
if (gotRoom == null) { return; }
|
||||||
|
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
|
||||||
|
expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can be created with invite_state", () => {
|
||||||
|
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
|
||||||
|
const gotRoom = client!.getRoom(roomE);
|
||||||
|
expect(gotRoom).toBeDefined();
|
||||||
|
if (gotRoom == null) { return; }
|
||||||
expect(gotRoom.getMyMembership()).toEqual("invite");
|
expect(gotRoom.getMyMembership()).toEqual("invite");
|
||||||
expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite);
|
expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses the 'name' field to caluclate the room name", () => {
|
it("uses the 'name' field to caluclate the room name", () => {
|
||||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
|
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
|
||||||
const gotRoom = client.getRoom(roomF);
|
const gotRoom = client!.getRoom(roomF);
|
||||||
expect(gotRoom).toBeDefined();
|
expect(gotRoom).toBeDefined();
|
||||||
|
if (gotRoom == null) { return; }
|
||||||
expect(
|
expect(
|
||||||
gotRoom.name,
|
gotRoom.name,
|
||||||
).toEqual(data[roomF].name);
|
).toEqual(data[roomF].name);
|
||||||
@@ -319,61 +347,80 @@ describe("SlidingSyncSdk", () => {
|
|||||||
describe("updating", () => {
|
describe("updating", () => {
|
||||||
it("can update with a new timeline event", async () => {
|
it("can update with a new timeline event", async () => {
|
||||||
const newEvent = mkOwnEvent(EventType.RoomMessage, { body: "new event A" });
|
const newEvent = mkOwnEvent(EventType.RoomMessage, { body: "new event A" });
|
||||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, {
|
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, {
|
||||||
timeline: [newEvent],
|
timeline: [newEvent],
|
||||||
required_state: [],
|
required_state: [],
|
||||||
name: data[roomA].name,
|
name: data[roomA].name,
|
||||||
});
|
});
|
||||||
const gotRoom = client.getRoom(roomA);
|
const gotRoom = client!.getRoom(roomA);
|
||||||
expect(gotRoom).toBeDefined();
|
expect(gotRoom).toBeDefined();
|
||||||
|
if (gotRoom == null) { return; }
|
||||||
const newTimeline = data[roomA].timeline;
|
const newTimeline = data[roomA].timeline;
|
||||||
newTimeline.push(newEvent);
|
newTimeline.push(newEvent);
|
||||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline);
|
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can update with a new required_state event", async () => {
|
it("can update with a new required_state event", async () => {
|
||||||
let gotRoom = client.getRoom(roomB);
|
let gotRoom = client!.getRoom(roomB);
|
||||||
|
expect(gotRoom).toBeDefined();
|
||||||
|
if (gotRoom == null) { return; }
|
||||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
|
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
|
||||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, {
|
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, {
|
||||||
required_state: [
|
required_state: [
|
||||||
mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, ""),
|
mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, ""),
|
||||||
],
|
],
|
||||||
timeline: [],
|
timeline: [],
|
||||||
name: data[roomB].name,
|
name: data[roomB].name,
|
||||||
});
|
});
|
||||||
gotRoom = client.getRoom(roomB);
|
gotRoom = client!.getRoom(roomB);
|
||||||
expect(gotRoom).toBeDefined();
|
expect(gotRoom).toBeDefined();
|
||||||
|
if (gotRoom == null) { return; }
|
||||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
|
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can update with a new highlight_count", async () => {
|
it("can update with a new highlight_count", async () => {
|
||||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, {
|
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, {
|
||||||
name: data[roomC].name,
|
name: data[roomC].name,
|
||||||
required_state: [],
|
required_state: [],
|
||||||
timeline: [],
|
timeline: [],
|
||||||
highlight_count: 1,
|
highlight_count: 1,
|
||||||
});
|
});
|
||||||
const gotRoom = client.getRoom(roomC);
|
const gotRoom = client!.getRoom(roomC);
|
||||||
expect(gotRoom).toBeDefined();
|
expect(gotRoom).toBeDefined();
|
||||||
|
if (gotRoom == null) { return; }
|
||||||
expect(
|
expect(
|
||||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
|
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
|
||||||
).toEqual(1);
|
).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can update with a new notification_count", async () => {
|
it("can update with a new notification_count", async () => {
|
||||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, {
|
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, {
|
||||||
name: data[roomD].name,
|
name: data[roomD].name,
|
||||||
required_state: [],
|
required_state: [],
|
||||||
timeline: [],
|
timeline: [],
|
||||||
notification_count: 1,
|
notification_count: 1,
|
||||||
});
|
});
|
||||||
const gotRoom = client.getRoom(roomD);
|
const gotRoom = client!.getRoom(roomD);
|
||||||
expect(gotRoom).toBeDefined();
|
expect(gotRoom).toBeDefined();
|
||||||
|
if (gotRoom == null) { return; }
|
||||||
expect(
|
expect(
|
||||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
|
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
|
||||||
).toEqual(1);
|
).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("can update with a new joined_count", () => {
|
||||||
|
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, {
|
||||||
|
name: data[roomD].name,
|
||||||
|
required_state: [],
|
||||||
|
timeline: [],
|
||||||
|
joined_count: 1,
|
||||||
|
});
|
||||||
|
const gotRoom = client!.getRoom(roomG);
|
||||||
|
expect(gotRoom).toBeDefined();
|
||||||
|
if (gotRoom == null) { return; }
|
||||||
|
expect(gotRoom.getJoinedMemberCount()).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
// Regression test for a bug which caused the timeline entries to be out-of-order
|
// Regression test for a bug which caused the timeline entries to be out-of-order
|
||||||
// when the same room appears twice with different timeline limits. E.g appears in
|
// when the same room appears twice with different timeline limits. E.g appears in
|
||||||
// the list with timeline_limit:1 then appears again as a room subscription with
|
// the list with timeline_limit:1 then appears again as a room subscription with
|
||||||
@@ -386,14 +433,15 @@ describe("SlidingSyncSdk", () => {
|
|||||||
mkOwnEvent(EventType.RoomMessage, { body: "old event C" }),
|
mkOwnEvent(EventType.RoomMessage, { body: "old event C" }),
|
||||||
...timeline,
|
...timeline,
|
||||||
];
|
];
|
||||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, {
|
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, {
|
||||||
timeline: oldTimeline,
|
timeline: oldTimeline,
|
||||||
required_state: [],
|
required_state: [],
|
||||||
name: data[roomA].name,
|
name: data[roomA].name,
|
||||||
initial: true, // e.g requested via room subscription
|
initial: true, // e.g requested via room subscription
|
||||||
});
|
});
|
||||||
const gotRoom = client.getRoom(roomA);
|
const gotRoom = client!.getRoom(roomA);
|
||||||
expect(gotRoom).toBeDefined();
|
expect(gotRoom).toBeDefined();
|
||||||
|
if (gotRoom == null) { return; }
|
||||||
|
|
||||||
logger.log("want:", oldTimeline.map((e) => (e.type + " : " + (e.content || {}).body)));
|
logger.log("want:", oldTimeline.map((e) => (e.type + " : " + (e.content || {}).body)));
|
||||||
logger.log("got:", gotRoom.getLiveTimeline().getEvents().map(
|
logger.log("got:", gotRoom.getLiveTimeline().getEvents().map(
|
||||||
@@ -410,50 +458,50 @@ describe("SlidingSyncSdk", () => {
|
|||||||
describe("lifecycle", () => {
|
describe("lifecycle", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await setupClient();
|
await setupClient();
|
||||||
const hasSynced = sdk.sync();
|
const hasSynced = sdk!.sync();
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await hasSynced;
|
await hasSynced;
|
||||||
});
|
});
|
||||||
const FAILED_SYNC_ERROR_THRESHOLD = 3; // would be nice to export the const in the actual class...
|
const FAILED_SYNC_ERROR_THRESHOLD = 3; // would be nice to export the const in the actual class...
|
||||||
|
|
||||||
it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => {
|
it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => {
|
||||||
mockSlidingSync.emit(
|
mockSlidingSync!.emit(
|
||||||
SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete,
|
SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete,
|
||||||
{ pos: "h", lists: [], rooms: {}, extensions: {} }, null,
|
{ pos: "h", lists: [], rooms: {}, extensions: {} }, null,
|
||||||
);
|
);
|
||||||
expect(sdk.getSyncState()).toEqual(SyncState.Syncing);
|
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
|
||||||
|
|
||||||
mockSlidingSync.emit(
|
mockSlidingSync!.emit(
|
||||||
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
|
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
|
||||||
);
|
);
|
||||||
expect(sdk.getSyncState()).toEqual(SyncState.Reconnecting);
|
expect(sdk!.getSyncState()).toEqual(SyncState.Reconnecting);
|
||||||
|
|
||||||
for (let i = 0; i < FAILED_SYNC_ERROR_THRESHOLD; i++) {
|
for (let i = 0; i < FAILED_SYNC_ERROR_THRESHOLD; i++) {
|
||||||
mockSlidingSync.emit(
|
mockSlidingSync!.emit(
|
||||||
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
|
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
expect(sdk.getSyncState()).toEqual(SyncState.Error);
|
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("emits SyncState.Syncing after a previous SyncState.Error", async () => {
|
it("emits SyncState.Syncing after a previous SyncState.Error", async () => {
|
||||||
mockSlidingSync.emit(
|
mockSlidingSync!.emit(
|
||||||
SlidingSyncEvent.Lifecycle,
|
SlidingSyncEvent.Lifecycle,
|
||||||
SlidingSyncState.Complete,
|
SlidingSyncState.Complete,
|
||||||
{ pos: "i", lists: [], rooms: {}, extensions: {} },
|
{ pos: "i", lists: [], rooms: {}, extensions: {} },
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
expect(sdk.getSyncState()).toEqual(SyncState.Syncing);
|
expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("emits SyncState.Error immediately when receiving M_UNKNOWN_TOKEN and stops syncing", async () => {
|
it("emits SyncState.Error immediately when receiving M_UNKNOWN_TOKEN and stops syncing", async () => {
|
||||||
expect(mockSlidingSync.stop).not.toBeCalled();
|
expect(mockSlidingSync!.stop).not.toBeCalled();
|
||||||
mockSlidingSync.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({
|
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({
|
||||||
errcode: "M_UNKNOWN_TOKEN",
|
errcode: "M_UNKNOWN_TOKEN",
|
||||||
message: "Oh no your access token is no longer valid",
|
message: "Oh no your access token is no longer valid",
|
||||||
}));
|
}));
|
||||||
expect(sdk.getSyncState()).toEqual(SyncState.Error);
|
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
|
||||||
expect(mockSlidingSync.stop).toBeCalled();
|
expect(mockSlidingSync!.stop).toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -469,8 +517,8 @@ describe("SlidingSyncSdk", () => {
|
|||||||
avatar_url: "mxc://foobar",
|
avatar_url: "mxc://foobar",
|
||||||
displayname: "The Invitee",
|
displayname: "The Invitee",
|
||||||
};
|
};
|
||||||
httpBackend.when("GET", "/profile").respond(200, inviteeProfile);
|
httpBackend!.when("GET", "/profile").respond(200, inviteeProfile);
|
||||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomId, {
|
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||||
initial: true,
|
initial: true,
|
||||||
name: "Room with Invite",
|
name: "Room with Invite",
|
||||||
required_state: [],
|
required_state: [],
|
||||||
@@ -481,10 +529,10 @@ describe("SlidingSyncSdk", () => {
|
|||||||
mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee),
|
mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
await httpBackend.flush("/profile", 1, 1000);
|
await httpBackend!.flush("/profile", 1, 1000);
|
||||||
const room = client.getRoom(roomId);
|
const room = client!.getRoom(roomId)!;
|
||||||
expect(room).toBeDefined();
|
expect(room).toBeDefined();
|
||||||
const inviteeMember = room.getMember(invitee);
|
const inviteeMember = room.getMember(invitee)!;
|
||||||
expect(inviteeMember).toBeDefined();
|
expect(inviteeMember).toBeDefined();
|
||||||
expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url);
|
expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url);
|
||||||
expect(inviteeMember.name).toEqual(inviteeProfile.displayname);
|
expect(inviteeMember.name).toEqual(inviteeProfile.displayname);
|
||||||
@@ -497,8 +545,8 @@ describe("SlidingSyncSdk", () => {
|
|||||||
await setupClient({
|
await setupClient({
|
||||||
withCrypto: true,
|
withCrypto: true,
|
||||||
});
|
});
|
||||||
const hasSynced = sdk.sync();
|
const hasSynced = sdk!.sync();
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await hasSynced;
|
await hasSynced;
|
||||||
ext = findExtension("e2ee");
|
ext = findExtension("e2ee");
|
||||||
});
|
});
|
||||||
@@ -506,7 +554,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
// needed else we do some async operations in the background which can cause Jest to whine:
|
// needed else we do some async operations in the background which can cause Jest to whine:
|
||||||
// "Cannot log after tests are done. Did you forget to wait for something async in your test?"
|
// "Cannot log after tests are done. Did you forget to wait for something async in your test?"
|
||||||
// Attempted to log "Saving device tracking data null"."
|
// Attempted to log "Saving device tracking data null"."
|
||||||
client.crypto.stop();
|
client!.crypto!.stop();
|
||||||
});
|
});
|
||||||
it("gets enabled on the initial request only", () => {
|
it("gets enabled on the initial request only", () => {
|
||||||
expect(ext.onRequest(true)).toEqual({
|
expect(ext.onRequest(true)).toEqual({
|
||||||
@@ -524,38 +572,38 @@ describe("SlidingSyncSdk", () => {
|
|||||||
// TODO: more assertions?
|
// TODO: more assertions?
|
||||||
});
|
});
|
||||||
it("can update OTK counts", () => {
|
it("can update OTK counts", () => {
|
||||||
client.crypto.updateOneTimeKeyCount = jest.fn();
|
client!.crypto!.updateOneTimeKeyCount = jest.fn();
|
||||||
ext.onResponse({
|
ext.onResponse({
|
||||||
device_one_time_keys_count: {
|
device_one_time_keys_count: {
|
||||||
signed_curve25519: 42,
|
signed_curve25519: 42,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(client.crypto.updateOneTimeKeyCount).toHaveBeenCalledWith(42);
|
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(42);
|
||||||
ext.onResponse({
|
ext.onResponse({
|
||||||
device_one_time_keys_count: {
|
device_one_time_keys_count: {
|
||||||
not_signed_curve25519: 42,
|
not_signed_curve25519: 42,
|
||||||
// missing field -> default to 0
|
// missing field -> default to 0
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(client.crypto.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
|
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
|
||||||
});
|
});
|
||||||
it("can update fallback keys", () => {
|
it("can update fallback keys", () => {
|
||||||
ext.onResponse({
|
ext.onResponse({
|
||||||
device_unused_fallback_key_types: ["signed_curve25519"],
|
device_unused_fallback_key_types: ["signed_curve25519"],
|
||||||
});
|
});
|
||||||
expect(client.crypto.getNeedsNewFallback()).toEqual(false);
|
expect(client!.crypto!.getNeedsNewFallback()).toEqual(false);
|
||||||
ext.onResponse({
|
ext.onResponse({
|
||||||
device_unused_fallback_key_types: ["not_signed_curve25519"],
|
device_unused_fallback_key_types: ["not_signed_curve25519"],
|
||||||
});
|
});
|
||||||
expect(client.crypto.getNeedsNewFallback()).toEqual(true);
|
expect(client!.crypto!.getNeedsNewFallback()).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe("ExtensionAccountData", () => {
|
describe("ExtensionAccountData", () => {
|
||||||
let ext: Extension;
|
let ext: Extension;
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await setupClient();
|
await setupClient();
|
||||||
const hasSynced = sdk.sync();
|
const hasSynced = sdk!.sync();
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await hasSynced;
|
await hasSynced;
|
||||||
ext = findExtension("account_data");
|
ext = findExtension("account_data");
|
||||||
});
|
});
|
||||||
@@ -570,7 +618,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
const globalContent = {
|
const globalContent = {
|
||||||
info: "here",
|
info: "here",
|
||||||
};
|
};
|
||||||
let globalData = client.getAccountData(globalType);
|
let globalData = client!.getAccountData(globalType);
|
||||||
expect(globalData).toBeUndefined();
|
expect(globalData).toBeUndefined();
|
||||||
ext.onResponse({
|
ext.onResponse({
|
||||||
global: [
|
global: [
|
||||||
@@ -580,13 +628,13 @@ describe("SlidingSyncSdk", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
globalData = client.getAccountData(globalType);
|
globalData = client!.getAccountData(globalType)!;
|
||||||
expect(globalData).toBeDefined();
|
expect(globalData).toBeDefined();
|
||||||
expect(globalData.getContent()).toEqual(globalContent);
|
expect(globalData.getContent()).toEqual(globalContent);
|
||||||
});
|
});
|
||||||
it("processes rooms account data", async () => {
|
it("processes rooms account data", async () => {
|
||||||
const roomId = "!room:id";
|
const roomId = "!room:id";
|
||||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomId, {
|
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||||
name: "Room with account data",
|
name: "Room with account data",
|
||||||
required_state: [],
|
required_state: [],
|
||||||
timeline: [
|
timeline: [
|
||||||
@@ -612,9 +660,9 @@ describe("SlidingSyncSdk", () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const room = client.getRoom(roomId);
|
const room = client!.getRoom(roomId)!;
|
||||||
expect(room).toBeDefined();
|
expect(room).toBeDefined();
|
||||||
const event = room.getAccountData(roomType);
|
const event = room.getAccountData(roomType)!;
|
||||||
expect(event).toBeDefined();
|
expect(event).toBeDefined();
|
||||||
expect(event.getContent()).toEqual(roomContent);
|
expect(event.getContent()).toEqual(roomContent);
|
||||||
});
|
});
|
||||||
@@ -633,9 +681,9 @@ describe("SlidingSyncSdk", () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const room = client.getRoom(unknownRoomId);
|
const room = client!.getRoom(unknownRoomId);
|
||||||
expect(room).toBeNull();
|
expect(room).toBeNull();
|
||||||
expect(client.getAccountData(roomType)).toBeUndefined();
|
expect(client!.getAccountData(roomType)).toBeUndefined();
|
||||||
});
|
});
|
||||||
it("can update push rules via account data", async () => {
|
it("can update push rules via account data", async () => {
|
||||||
const roomId = "!foo:bar";
|
const roomId = "!foo:bar";
|
||||||
@@ -655,7 +703,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let pushRule = client.getRoomPushRule("global", roomId);
|
let pushRule = client!.getRoomPushRule("global", roomId);
|
||||||
expect(pushRule).toBeUndefined();
|
expect(pushRule).toBeUndefined();
|
||||||
ext.onResponse({
|
ext.onResponse({
|
||||||
global: [
|
global: [
|
||||||
@@ -665,16 +713,16 @@ describe("SlidingSyncSdk", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
pushRule = client.getRoomPushRule("global", roomId);
|
pushRule = client!.getRoomPushRule("global", roomId)!;
|
||||||
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific][0]);
|
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific]![0]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe("ExtensionToDevice", () => {
|
describe("ExtensionToDevice", () => {
|
||||||
let ext: Extension;
|
let ext: Extension;
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await setupClient();
|
await setupClient();
|
||||||
const hasSynced = sdk.sync();
|
const hasSynced = sdk!.sync();
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await hasSynced;
|
await hasSynced;
|
||||||
ext = findExtension("to_device");
|
ext = findExtension("to_device");
|
||||||
});
|
});
|
||||||
@@ -705,7 +753,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
foo: "bar",
|
foo: "bar",
|
||||||
};
|
};
|
||||||
let called = false;
|
let called = false;
|
||||||
client.once(ClientEvent.ToDeviceEvent, (ev) => {
|
client!.once(ClientEvent.ToDeviceEvent, (ev) => {
|
||||||
expect(ev.getContent()).toEqual(toDeviceContent);
|
expect(ev.getContent()).toEqual(toDeviceContent);
|
||||||
expect(ev.getType()).toEqual(toDeviceType);
|
expect(ev.getType()).toEqual(toDeviceType);
|
||||||
called = true;
|
called = true;
|
||||||
@@ -723,7 +771,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
});
|
});
|
||||||
it("can cancel key verification requests", async () => {
|
it("can cancel key verification requests", async () => {
|
||||||
const seen: Record<string, boolean> = {};
|
const seen: Record<string, boolean> = {};
|
||||||
client.on(ClientEvent.ToDeviceEvent, (ev) => {
|
client!.on(ClientEvent.ToDeviceEvent, (ev) => {
|
||||||
const evType = ev.getType();
|
const evType = ev.getType();
|
||||||
expect(seen[evType]).toBeFalsy();
|
expect(seen[evType]).toBeFalsy();
|
||||||
seen[evType] = true;
|
seen[evType] = true;
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ import { sleep } from "../../src/utils";
|
|||||||
* Each test will call different functions on SlidingSync which may depend on state from previous tests.
|
* Each test will call different functions on SlidingSync which may depend on state from previous tests.
|
||||||
*/
|
*/
|
||||||
describe("SlidingSync", () => {
|
describe("SlidingSync", () => {
|
||||||
let client: MatrixClient = null;
|
let client: MatrixClient | undefined;
|
||||||
let httpBackend: MockHttpBackend = null;
|
let httpBackend: MockHttpBackend | undefined;
|
||||||
const selfUserId = "@alice:localhost";
|
const selfUserId = "@alice:localhost";
|
||||||
const selfAccessToken = "aseukfgwef";
|
const selfAccessToken = "aseukfgwef";
|
||||||
const proxyBaseUrl = "http://localhost:8008";
|
const proxyBaseUrl = "http://localhost:8008";
|
||||||
@@ -46,9 +46,9 @@ describe("SlidingSync", () => {
|
|||||||
|
|
||||||
// tear down client/httpBackend globals
|
// tear down client/httpBackend globals
|
||||||
const teardownClient = () => {
|
const teardownClient = () => {
|
||||||
httpBackend.verifyNoOutstandingExpectation();
|
httpBackend!.verifyNoOutstandingExpectation();
|
||||||
client.stopClient();
|
client!.stopClient();
|
||||||
return httpBackend.stop();
|
return httpBackend!.stop();
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("start/stop", () => {
|
describe("start/stop", () => {
|
||||||
@@ -57,14 +57,14 @@ describe("SlidingSync", () => {
|
|||||||
let slidingSync: SlidingSync;
|
let slidingSync: SlidingSync;
|
||||||
|
|
||||||
it("should start the sync loop upon calling start()", async () => {
|
it("should start the sync loop upon calling start()", async () => {
|
||||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
|
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||||
const fakeResp = {
|
const fakeResp = {
|
||||||
pos: "a",
|
pos: "a",
|
||||||
lists: [],
|
lists: [],
|
||||||
rooms: {},
|
rooms: {},
|
||||||
extensions: {},
|
extensions: {},
|
||||||
};
|
};
|
||||||
httpBackend.when("POST", syncUrl).respond(200, fakeResp);
|
httpBackend!.when("POST", syncUrl).respond(200, fakeResp);
|
||||||
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => {
|
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => {
|
||||||
expect(state).toEqual(SlidingSyncState.RequestFinished);
|
expect(state).toEqual(SlidingSyncState.RequestFinished);
|
||||||
expect(resp).toEqual(fakeResp);
|
expect(resp).toEqual(fakeResp);
|
||||||
@@ -72,13 +72,13 @@ describe("SlidingSync", () => {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
slidingSync.start();
|
slidingSync.start();
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await p;
|
await p;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should stop the sync loop upon calling stop()", () => {
|
it("should stop the sync loop upon calling stop()", () => {
|
||||||
slidingSync.stop();
|
slidingSync.stop();
|
||||||
httpBackend.verifyNoOutstandingExpectation();
|
httpBackend!.verifyNoOutstandingExpectation();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reset the connection on HTTP 400 and send everything again", async () => {
|
it("should reset the connection on HTTP 400 and send everything again", async () => {
|
||||||
@@ -203,9 +203,9 @@ describe("SlidingSync", () => {
|
|||||||
|
|
||||||
it("should be able to subscribe to a room", async () => {
|
it("should be able to subscribe to a room", async () => {
|
||||||
// add the subscription
|
// add the subscription
|
||||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client, 1);
|
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
|
||||||
slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
||||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||||
const body = req.data;
|
const body = req.data;
|
||||||
logger.log("room sub", body);
|
logger.log("room sub", body);
|
||||||
expect(body.room_subscriptions).toBeTruthy();
|
expect(body.room_subscriptions).toBeTruthy();
|
||||||
@@ -225,7 +225,7 @@ describe("SlidingSync", () => {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
slidingSync.start();
|
slidingSync.start();
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await p;
|
await p;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -237,7 +237,7 @@ describe("SlidingSync", () => {
|
|||||||
["m.room.member", "*"],
|
["m.room.member", "*"],
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||||
const body = req.data;
|
const body = req.data;
|
||||||
logger.log("adjusted sub", body);
|
logger.log("adjusted sub", body);
|
||||||
expect(body.room_subscriptions).toBeTruthy();
|
expect(body.room_subscriptions).toBeTruthy();
|
||||||
@@ -258,7 +258,7 @@ describe("SlidingSync", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
slidingSync.modifyRoomSubscriptionInfo(newSubInfo);
|
slidingSync.modifyRoomSubscriptionInfo(newSubInfo);
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await p;
|
await p;
|
||||||
// need to set what the new subscription info is for subsequent tests
|
// need to set what the new subscription info is for subsequent tests
|
||||||
roomSubInfo = newSubInfo;
|
roomSubInfo = newSubInfo;
|
||||||
@@ -279,7 +279,7 @@ describe("SlidingSync", () => {
|
|||||||
required_state: [],
|
required_state: [],
|
||||||
timeline: [],
|
timeline: [],
|
||||||
};
|
};
|
||||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||||
const body = req.data;
|
const body = req.data;
|
||||||
logger.log("new subs", body);
|
logger.log("new subs", body);
|
||||||
expect(body.room_subscriptions).toBeTruthy();
|
expect(body.room_subscriptions).toBeTruthy();
|
||||||
@@ -304,12 +304,12 @@ describe("SlidingSync", () => {
|
|||||||
const subs = slidingSync.getRoomSubscriptions();
|
const subs = slidingSync.getRoomSubscriptions();
|
||||||
subs.add(anotherRoomID);
|
subs.add(anotherRoomID);
|
||||||
slidingSync.modifyRoomSubscriptions(subs);
|
slidingSync.modifyRoomSubscriptions(subs);
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await p;
|
await p;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to unsubscribe from a room", async () => {
|
it("should be able to unsubscribe from a room", async () => {
|
||||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||||
const body = req.data;
|
const body = req.data;
|
||||||
logger.log("unsub request", body);
|
logger.log("unsub request", body);
|
||||||
expect(body.room_subscriptions).toBeFalsy();
|
expect(body.room_subscriptions).toBeFalsy();
|
||||||
@@ -326,7 +326,7 @@ describe("SlidingSync", () => {
|
|||||||
// remove the subscription for the first room
|
// remove the subscription for the first room
|
||||||
slidingSync.modifyRoomSubscriptions(new Set([anotherRoomID]));
|
slidingSync.modifyRoomSubscriptions(new Set([anotherRoomID]));
|
||||||
|
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await p;
|
await p;
|
||||||
|
|
||||||
slidingSync.stop();
|
slidingSync.stop();
|
||||||
@@ -373,8 +373,8 @@ describe("SlidingSync", () => {
|
|||||||
is_dm: true,
|
is_dm: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client, 1);
|
slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client!, 1);
|
||||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||||
const body = req.data;
|
const body = req.data;
|
||||||
logger.log("list", body);
|
logger.log("list", body);
|
||||||
expect(body.lists).toBeTruthy();
|
expect(body.lists).toBeTruthy();
|
||||||
@@ -401,7 +401,7 @@ describe("SlidingSync", () => {
|
|||||||
return state === SlidingSyncState.Complete;
|
return state === SlidingSyncState.Complete;
|
||||||
});
|
});
|
||||||
slidingSync.start();
|
slidingSync.start();
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await responseProcessed;
|
await responseProcessed;
|
||||||
|
|
||||||
expect(listenerData[roomA]).toEqual(rooms[roomA]);
|
expect(listenerData[roomA]).toEqual(rooms[roomA]);
|
||||||
@@ -427,7 +427,7 @@ describe("SlidingSync", () => {
|
|||||||
|
|
||||||
it("should be possible to adjust list ranges", async () => {
|
it("should be possible to adjust list ranges", async () => {
|
||||||
// modify the list ranges
|
// modify the list ranges
|
||||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||||
const body = req.data;
|
const body = req.data;
|
||||||
logger.log("next ranges", body.lists[0].ranges);
|
logger.log("next ranges", body.lists[0].ranges);
|
||||||
expect(body.lists).toBeTruthy();
|
expect(body.lists).toBeTruthy();
|
||||||
@@ -451,7 +451,7 @@ describe("SlidingSync", () => {
|
|||||||
return state === SlidingSyncState.RequestFinished;
|
return state === SlidingSyncState.RequestFinished;
|
||||||
});
|
});
|
||||||
slidingSync.setListRanges(0, newRanges);
|
slidingSync.setListRanges(0, newRanges);
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await responseProcessed;
|
await responseProcessed;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -464,7 +464,7 @@ describe("SlidingSync", () => {
|
|||||||
"is_dm": true,
|
"is_dm": true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||||
const body = req.data;
|
const body = req.data;
|
||||||
logger.log("extra list", body);
|
logger.log("extra list", body);
|
||||||
expect(body.lists).toBeTruthy();
|
expect(body.lists).toBeTruthy();
|
||||||
@@ -503,13 +503,13 @@ describe("SlidingSync", () => {
|
|||||||
return state === SlidingSyncState.Complete;
|
return state === SlidingSyncState.Complete;
|
||||||
});
|
});
|
||||||
slidingSync.setList(1, extraListReq);
|
slidingSync.setList(1, extraListReq);
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await responseProcessed;
|
await responseProcessed;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be possible to get list DELETE/INSERTs", async () => {
|
it("should be possible to get list DELETE/INSERTs", async () => {
|
||||||
// move C (2) to A (0)
|
// move C (2) to A (0)
|
||||||
httpBackend.when("POST", syncUrl).respond(200, {
|
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||||
pos: "e",
|
pos: "e",
|
||||||
lists: [{
|
lists: [{
|
||||||
count: 500,
|
count: 500,
|
||||||
@@ -540,12 +540,12 @@ describe("SlidingSync", () => {
|
|||||||
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||||
return state === SlidingSyncState.Complete;
|
return state === SlidingSyncState.Complete;
|
||||||
});
|
});
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await responseProcessed;
|
await responseProcessed;
|
||||||
await listPromise;
|
await listPromise;
|
||||||
|
|
||||||
// move C (0) back to A (2)
|
// move C (0) back to A (2)
|
||||||
httpBackend.when("POST", syncUrl).respond(200, {
|
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||||
pos: "f",
|
pos: "f",
|
||||||
lists: [{
|
lists: [{
|
||||||
count: 500,
|
count: 500,
|
||||||
@@ -576,13 +576,13 @@ describe("SlidingSync", () => {
|
|||||||
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||||
return state === SlidingSyncState.Complete;
|
return state === SlidingSyncState.Complete;
|
||||||
});
|
});
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await responseProcessed;
|
await responseProcessed;
|
||||||
await listPromise;
|
await listPromise;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should ignore invalid list indexes", async () => {
|
it("should ignore invalid list indexes", async () => {
|
||||||
httpBackend.when("POST", syncUrl).respond(200, {
|
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||||
pos: "e",
|
pos: "e",
|
||||||
lists: [{
|
lists: [{
|
||||||
count: 500,
|
count: 500,
|
||||||
@@ -609,13 +609,13 @@ describe("SlidingSync", () => {
|
|||||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||||
return state === SlidingSyncState.Complete;
|
return state === SlidingSyncState.Complete;
|
||||||
});
|
});
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await responseProcessed;
|
await responseProcessed;
|
||||||
await listPromise;
|
await listPromise;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be possible to update a list", async () => {
|
it("should be possible to update a list", async () => {
|
||||||
httpBackend.when("POST", syncUrl).respond(200, {
|
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||||
pos: "g",
|
pos: "g",
|
||||||
lists: [{
|
lists: [{
|
||||||
count: 42,
|
count: 42,
|
||||||
@@ -655,7 +655,7 @@ describe("SlidingSync", () => {
|
|||||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||||
return state === SlidingSyncState.Complete;
|
return state === SlidingSyncState.Complete;
|
||||||
});
|
});
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await responseProcessed;
|
await responseProcessed;
|
||||||
await listPromise;
|
await listPromise;
|
||||||
});
|
});
|
||||||
@@ -667,7 +667,7 @@ describe("SlidingSync", () => {
|
|||||||
1: roomC,
|
1: roomC,
|
||||||
};
|
};
|
||||||
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual(indexToRoomId);
|
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual(indexToRoomId);
|
||||||
httpBackend.when("POST", syncUrl).respond(200, {
|
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||||
pos: "f",
|
pos: "f",
|
||||||
// currently the list is [B,C] so we will insert D then immediately delete it
|
// currently the list is [B,C] so we will insert D then immediately delete it
|
||||||
lists: [{
|
lists: [{
|
||||||
@@ -698,7 +698,7 @@ describe("SlidingSync", () => {
|
|||||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||||
return state === SlidingSyncState.Complete;
|
return state === SlidingSyncState.Complete;
|
||||||
});
|
});
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await responseProcessed;
|
await responseProcessed;
|
||||||
await listPromise;
|
await listPromise;
|
||||||
});
|
});
|
||||||
@@ -708,7 +708,7 @@ describe("SlidingSync", () => {
|
|||||||
0: roomB,
|
0: roomB,
|
||||||
1: roomC,
|
1: roomC,
|
||||||
});
|
});
|
||||||
httpBackend.when("POST", syncUrl).respond(200, {
|
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||||
pos: "g",
|
pos: "g",
|
||||||
lists: [{
|
lists: [{
|
||||||
count: 499,
|
count: 499,
|
||||||
@@ -734,7 +734,7 @@ describe("SlidingSync", () => {
|
|||||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||||
return state === SlidingSyncState.Complete;
|
return state === SlidingSyncState.Complete;
|
||||||
});
|
});
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await responseProcessed;
|
await responseProcessed;
|
||||||
await listPromise;
|
await listPromise;
|
||||||
});
|
});
|
||||||
@@ -743,7 +743,7 @@ describe("SlidingSync", () => {
|
|||||||
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
|
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
|
||||||
0: roomC,
|
0: roomC,
|
||||||
});
|
});
|
||||||
httpBackend.when("POST", syncUrl).respond(200, {
|
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||||
pos: "h",
|
pos: "h",
|
||||||
lists: [{
|
lists: [{
|
||||||
count: 500,
|
count: 500,
|
||||||
@@ -770,11 +770,11 @@ describe("SlidingSync", () => {
|
|||||||
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||||
return state === SlidingSyncState.Complete;
|
return state === SlidingSyncState.Complete;
|
||||||
});
|
});
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await responseProcessed;
|
await responseProcessed;
|
||||||
await listPromise;
|
await listPromise;
|
||||||
|
|
||||||
httpBackend.when("POST", syncUrl).respond(200, {
|
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||||
pos: "h",
|
pos: "h",
|
||||||
lists: [{
|
lists: [{
|
||||||
count: 501,
|
count: 501,
|
||||||
@@ -802,7 +802,7 @@ describe("SlidingSync", () => {
|
|||||||
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||||
return state === SlidingSyncState.Complete;
|
return state === SlidingSyncState.Complete;
|
||||||
});
|
});
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await responseProcessed;
|
await responseProcessed;
|
||||||
await listPromise;
|
await listPromise;
|
||||||
slidingSync.stop();
|
slidingSync.stop();
|
||||||
@@ -825,11 +825,11 @@ describe("SlidingSync", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
// add the subscription
|
// add the subscription
|
||||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client, 1);
|
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
|
||||||
// modification before SlidingSync.start()
|
// modification before SlidingSync.start()
|
||||||
const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
||||||
let txnId;
|
let txnId;
|
||||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||||
const body = req.data;
|
const body = req.data;
|
||||||
logger.debug("got ", body);
|
logger.debug("got ", body);
|
||||||
expect(body.room_subscriptions).toBeTruthy();
|
expect(body.room_subscriptions).toBeTruthy();
|
||||||
@@ -852,7 +852,7 @@ describe("SlidingSync", () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
slidingSync.start();
|
slidingSync.start();
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await subscribePromise;
|
await subscribePromise;
|
||||||
});
|
});
|
||||||
it("should resolve setList during a connection", async () => {
|
it("should resolve setList during a connection", async () => {
|
||||||
@@ -861,7 +861,7 @@ describe("SlidingSync", () => {
|
|||||||
};
|
};
|
||||||
const promise = slidingSync.setList(0, newList);
|
const promise = slidingSync.setList(0, newList);
|
||||||
let txnId;
|
let txnId;
|
||||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||||
const body = req.data;
|
const body = req.data;
|
||||||
logger.debug("got ", body);
|
logger.debug("got ", body);
|
||||||
expect(body.room_subscriptions).toBeFalsy();
|
expect(body.room_subscriptions).toBeFalsy();
|
||||||
@@ -876,14 +876,14 @@ describe("SlidingSync", () => {
|
|||||||
extensions: {},
|
extensions: {},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await promise;
|
await promise;
|
||||||
expect(txnId).toBeDefined();
|
expect(txnId).toBeDefined();
|
||||||
});
|
});
|
||||||
it("should resolve setListRanges during a connection", async () => {
|
it("should resolve setListRanges during a connection", async () => {
|
||||||
const promise = slidingSync.setListRanges(0, [[20, 40]]);
|
const promise = slidingSync.setListRanges(0, [[20, 40]]);
|
||||||
let txnId;
|
let txnId;
|
||||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||||
const body = req.data;
|
const body = req.data;
|
||||||
logger.debug("got ", body);
|
logger.debug("got ", body);
|
||||||
expect(body.room_subscriptions).toBeFalsy();
|
expect(body.room_subscriptions).toBeFalsy();
|
||||||
@@ -900,7 +900,7 @@ describe("SlidingSync", () => {
|
|||||||
extensions: {},
|
extensions: {},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await promise;
|
await promise;
|
||||||
expect(txnId).toBeDefined();
|
expect(txnId).toBeDefined();
|
||||||
});
|
});
|
||||||
@@ -909,7 +909,7 @@ describe("SlidingSync", () => {
|
|||||||
timeline_limit: 99,
|
timeline_limit: 99,
|
||||||
});
|
});
|
||||||
let txnId;
|
let txnId;
|
||||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||||
const body = req.data;
|
const body = req.data;
|
||||||
logger.debug("got ", body);
|
logger.debug("got ", body);
|
||||||
expect(body.room_subscriptions).toBeTruthy();
|
expect(body.room_subscriptions).toBeTruthy();
|
||||||
@@ -925,22 +925,22 @@ describe("SlidingSync", () => {
|
|||||||
extensions: {},
|
extensions: {},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await promise;
|
await promise;
|
||||||
expect(txnId).toBeDefined();
|
expect(txnId).toBeDefined();
|
||||||
});
|
});
|
||||||
it("should reject earlier pending promises if a later transaction is acknowledged", async () => {
|
it("should reject earlier pending promises if a later transaction is acknowledged", async () => {
|
||||||
// i.e if we have [A,B,C] and see txn_id=C then A,B should be rejected.
|
// i.e if we have [A,B,C] and see txn_id=C then A,B should be rejected.
|
||||||
const gotTxnIds = [];
|
const gotTxnIds: any[] = [];
|
||||||
const pushTxn = function(req) {
|
const pushTxn = function(req) {
|
||||||
gotTxnIds.push(req.data.txn_id);
|
gotTxnIds.push(req.data.txn_id);
|
||||||
};
|
};
|
||||||
const failPromise = slidingSync.setListRanges(0, [[20, 40]]);
|
const failPromise = slidingSync.setListRanges(0, [[20, 40]]);
|
||||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id
|
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
const failPromise2 = slidingSync.setListRanges(0, [[60, 70]]);
|
const failPromise2 = slidingSync.setListRanges(0, [[60, 70]]);
|
||||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
|
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
|
|
||||||
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
|
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
|
||||||
// which is a fail.
|
// which is a fail.
|
||||||
@@ -949,7 +949,7 @@ describe("SlidingSync", () => {
|
|||||||
|
|
||||||
const okPromise = slidingSync.setListRanges(0, [[0, 20]]);
|
const okPromise = slidingSync.setListRanges(0, [[0, 20]]);
|
||||||
let txnId;
|
let txnId;
|
||||||
httpBackend.when("POST", syncUrl).check((req) => {
|
httpBackend!.when("POST", syncUrl).check((req) => {
|
||||||
txnId = req.data.txn_id;
|
txnId = req.data.txn_id;
|
||||||
}).respond(200, () => {
|
}).respond(200, () => {
|
||||||
// include the txn_id, earlier requests should now be reject()ed.
|
// include the txn_id, earlier requests should now be reject()ed.
|
||||||
@@ -958,23 +958,23 @@ describe("SlidingSync", () => {
|
|||||||
txn_id: txnId,
|
txn_id: txnId,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await okPromise;
|
await okPromise;
|
||||||
|
|
||||||
expect(txnId).toBeDefined();
|
expect(txnId).toBeDefined();
|
||||||
});
|
});
|
||||||
it("should not reject later pending promises if an earlier transaction is acknowledged", async () => {
|
it("should not reject later pending promises if an earlier transaction is acknowledged", async () => {
|
||||||
// i.e if we have [A,B,C] and see txn_id=B then C should not be rejected but A should.
|
// i.e if we have [A,B,C] and see txn_id=B then C should not be rejected but A should.
|
||||||
const gotTxnIds = [];
|
const gotTxnIds: any[] = [];
|
||||||
const pushTxn = function(req) {
|
const pushTxn = function(req) {
|
||||||
gotTxnIds.push(req.data.txn_id);
|
gotTxnIds.push(req.data?.txn_id);
|
||||||
};
|
};
|
||||||
const A = slidingSync.setListRanges(0, [[20, 40]]);
|
const A = slidingSync.setListRanges(0, [[20, 40]]);
|
||||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" });
|
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" });
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
const B = slidingSync.setListRanges(0, [[60, 70]]);
|
const B = slidingSync.setListRanges(0, [[60, 70]]);
|
||||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id
|
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
|
|
||||||
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
|
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
|
||||||
// which is a fail.
|
// which is a fail.
|
||||||
@@ -985,14 +985,14 @@ describe("SlidingSync", () => {
|
|||||||
C.finally(() => {
|
C.finally(() => {
|
||||||
pendingC = false;
|
pendingC = false;
|
||||||
});
|
});
|
||||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, () => {
|
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, () => {
|
||||||
// include the txn_id for B, so C's promise is outstanding
|
// include the txn_id for B, so C's promise is outstanding
|
||||||
return {
|
return {
|
||||||
pos: "C",
|
pos: "C",
|
||||||
txn_id: gotTxnIds[1],
|
txn_id: gotTxnIds[1],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
// A is rejected, see above
|
// A is rejected, see above
|
||||||
expect(B).resolves.toEqual(gotTxnIds[1]); // B is resolved
|
expect(B).resolves.toEqual(gotTxnIds[1]); // B is resolved
|
||||||
expect(pendingC).toBe(true); // C is pending still
|
expect(pendingC).toBe(true); // C is pending still
|
||||||
@@ -1004,7 +1004,7 @@ describe("SlidingSync", () => {
|
|||||||
pending = false;
|
pending = false;
|
||||||
});
|
});
|
||||||
let txnId;
|
let txnId;
|
||||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||||
const body = req.data;
|
const body = req.data;
|
||||||
logger.debug("got ", body);
|
logger.debug("got ", body);
|
||||||
expect(body.room_subscriptions).toBeFalsy();
|
expect(body.room_subscriptions).toBeFalsy();
|
||||||
@@ -1021,7 +1021,7 @@ describe("SlidingSync", () => {
|
|||||||
extensions: {},
|
extensions: {},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
expect(txnId).toBeDefined();
|
expect(txnId).toBeDefined();
|
||||||
expect(pending).toBe(true);
|
expect(pending).toBe(true);
|
||||||
slidingSync.stop();
|
slidingSync.stop();
|
||||||
@@ -1063,10 +1063,10 @@ describe("SlidingSync", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it("should be able to register an extension", async () => {
|
it("should be able to register an extension", async () => {
|
||||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
|
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||||
slidingSync.registerExtension(extPre);
|
slidingSync.registerExtension(extPre);
|
||||||
|
|
||||||
const callbackOrder = [];
|
const callbackOrder: string[] = [];
|
||||||
let extensionOnResponseCalled = false;
|
let extensionOnResponseCalled = false;
|
||||||
onPreExtensionRequest = () => {
|
onPreExtensionRequest = () => {
|
||||||
return extReq;
|
return extReq;
|
||||||
@@ -1077,7 +1077,7 @@ describe("SlidingSync", () => {
|
|||||||
expect(resp).toEqual(extResp);
|
expect(resp).toEqual(extResp);
|
||||||
};
|
};
|
||||||
|
|
||||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||||
const body = req.data;
|
const body = req.data;
|
||||||
logger.log("ext req", body);
|
logger.log("ext req", body);
|
||||||
expect(body.extensions).toBeTruthy();
|
expect(body.extensions).toBeTruthy();
|
||||||
@@ -1098,7 +1098,7 @@ describe("SlidingSync", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
slidingSync.start();
|
slidingSync.start();
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await p;
|
await p;
|
||||||
expect(extensionOnResponseCalled).toBe(true);
|
expect(extensionOnResponseCalled).toBe(true);
|
||||||
expect(callbackOrder).toEqual(["onPreExtensionResponse", "Lifecycle"]);
|
expect(callbackOrder).toEqual(["onPreExtensionResponse", "Lifecycle"]);
|
||||||
@@ -1112,7 +1112,7 @@ describe("SlidingSync", () => {
|
|||||||
onPreExtensionResponse = (resp) => {
|
onPreExtensionResponse = (resp) => {
|
||||||
responseCalled = true;
|
responseCalled = true;
|
||||||
};
|
};
|
||||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||||
const body = req.data;
|
const body = req.data;
|
||||||
logger.log("ext req nothing", body);
|
logger.log("ext req nothing", body);
|
||||||
expect(body.extensions).toBeTruthy();
|
expect(body.extensions).toBeTruthy();
|
||||||
@@ -1130,7 +1130,7 @@ describe("SlidingSync", () => {
|
|||||||
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => {
|
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => {
|
||||||
return state === SlidingSyncState.Complete;
|
return state === SlidingSyncState.Complete;
|
||||||
});
|
});
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await p;
|
await p;
|
||||||
expect(responseCalled).toBe(false);
|
expect(responseCalled).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -1141,13 +1141,13 @@ describe("SlidingSync", () => {
|
|||||||
return extReq;
|
return extReq;
|
||||||
};
|
};
|
||||||
let responseCalled = false;
|
let responseCalled = false;
|
||||||
const callbackOrder = [];
|
const callbackOrder: string[] = [];
|
||||||
onPostExtensionResponse = (resp) => {
|
onPostExtensionResponse = (resp) => {
|
||||||
expect(resp).toEqual(extResp);
|
expect(resp).toEqual(extResp);
|
||||||
responseCalled = true;
|
responseCalled = true;
|
||||||
callbackOrder.push("onPostExtensionResponse");
|
callbackOrder.push("onPostExtensionResponse");
|
||||||
};
|
};
|
||||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||||
const body = req.data;
|
const body = req.data;
|
||||||
logger.log("ext req after start", body);
|
logger.log("ext req after start", body);
|
||||||
expect(body.extensions).toBeTruthy();
|
expect(body.extensions).toBeTruthy();
|
||||||
@@ -1171,7 +1171,7 @@ describe("SlidingSync", () => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await httpBackend.flushAllExpected();
|
await httpBackend!.flushAllExpected();
|
||||||
await p;
|
await p;
|
||||||
expect(responseCalled).toBe(true);
|
expect(responseCalled).toBe(true);
|
||||||
expect(callbackOrder).toEqual(["Lifecycle", "onPostExtensionResponse"]);
|
expect(callbackOrder).toEqual(["Lifecycle", "onPostExtensionResponse"]);
|
||||||
@@ -1179,7 +1179,7 @@ describe("SlidingSync", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("is not possible to register the same extension name twice", async () => {
|
it("is not possible to register the same extension name twice", async () => {
|
||||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
|
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||||
slidingSync.registerExtension(extPre);
|
slidingSync.registerExtension(extPre);
|
||||||
expect(() => { slidingSync.registerExtension(extPre); }).toThrow();
|
expect(() => { slidingSync.registerExtension(extPre); }).toThrow();
|
||||||
});
|
});
|
||||||
@@ -1206,7 +1206,7 @@ function listenUntil<T>(
|
|||||||
callback: (...args: any[]) => T,
|
callback: (...args: any[]) => T,
|
||||||
timeoutMs = 500,
|
timeoutMs = 500,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const trace = new Error().stack.split(`\n`)[2];
|
const trace = new Error().stack?.split(`\n`)[2];
|
||||||
return Promise.race([new Promise<T>((resolve, reject) => {
|
return Promise.race([new Promise<T>((resolve, reject) => {
|
||||||
const wrapper = (...args) => {
|
const wrapper = (...args) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import * as utils from "../src/utils";
|
|||||||
|
|
||||||
// try to load the olm library.
|
// try to load the olm library.
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
global.Olm = require('@matrix-org/olm');
|
global.Olm = require('@matrix-org/olm');
|
||||||
logger.log('loaded libolm');
|
logger.log('loaded libolm');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -28,6 +29,7 @@ try {
|
|||||||
|
|
||||||
// also try to set node crypto
|
// also try to set node crypto
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
utils.setCrypto(crypto);
|
utils.setCrypto(crypto);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
94
spec/test-utils/client.ts
Normal file
94
spec/test-utils/client.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 { MethodKeysOf, mocked, MockedObject } from "jest-mock";
|
||||||
|
|
||||||
|
import { ClientEventHandlerMap, EmittedEvents, MatrixClient } from "../../src/client";
|
||||||
|
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||||
|
import { User } from "../../src/models/user";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock client with real event emitter
|
||||||
|
* useful for testing code that listens
|
||||||
|
* to MatrixClient events
|
||||||
|
*/
|
||||||
|
export class MockClientWithEventEmitter extends TypedEventEmitter<EmittedEvents, ClientEventHandlerMap> {
|
||||||
|
constructor(mockProperties: Partial<Record<MethodKeysOf<MatrixClient>, unknown>> = {}) {
|
||||||
|
super();
|
||||||
|
Object.assign(this, mockProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - make a mock client
|
||||||
|
* - cast the type to mocked(MatrixClient)
|
||||||
|
* - spy on MatrixClientPeg.get to return the mock
|
||||||
|
* eg
|
||||||
|
* ```
|
||||||
|
* const mockClient = getMockClientWithEventEmitter({
|
||||||
|
getUserId: jest.fn().mockReturnValue(aliceId),
|
||||||
|
});
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const getMockClientWithEventEmitter = (
|
||||||
|
mockProperties: Partial<Record<MethodKeysOf<MatrixClient>, unknown>>,
|
||||||
|
): MockedObject<MatrixClient> => {
|
||||||
|
const mock = mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
|
||||||
|
return mock;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns basic mocked client methods related to the current user
|
||||||
|
* ```
|
||||||
|
* const mockClient = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsUser('@mytestuser:domain'),
|
||||||
|
});
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const mockClientMethodsUser = (userId = '@alice:domain') => ({
|
||||||
|
getUserId: jest.fn().mockReturnValue(userId),
|
||||||
|
getUser: jest.fn().mockReturnValue(new User(userId)),
|
||||||
|
isGuest: jest.fn().mockReturnValue(false),
|
||||||
|
mxcUrlToHttp: jest.fn().mockReturnValue('mock-mxcUrlToHttp'),
|
||||||
|
credentials: { userId },
|
||||||
|
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
|
||||||
|
getAccessToken: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns basic mocked client methods related to rendering events
|
||||||
|
* ```
|
||||||
|
* const mockClient = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsUser('@mytestuser:domain'),
|
||||||
|
});
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const mockClientMethodsEvents = () => ({
|
||||||
|
decryptEventIfNeeded: jest.fn(),
|
||||||
|
getPushActionsForEvent: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns basic mocked client methods related to server support
|
||||||
|
*/
|
||||||
|
export const mockClientMethodsServer = (): Partial<Record<MethodKeysOf<MatrixClient>, unknown>> => ({
|
||||||
|
doesServerSupportSeparateAddAndBind: jest.fn(),
|
||||||
|
getIdentityServerUrl: jest.fn(),
|
||||||
|
getHomeserverUrl: jest.fn(),
|
||||||
|
getCapabilities: jest.fn().mockReturnValue({}),
|
||||||
|
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
|
||||||
|
});
|
||||||
|
|
||||||
@@ -24,5 +24,5 @@ limitations under the License.
|
|||||||
* expect(beaconLivenessEmits.length).toBe(1);
|
* expect(beaconLivenessEmits.length).toBe(1);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, unknown[]>) =>
|
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, any[]>) =>
|
||||||
spy.mock.calls.filter((args) => args[0] === eventType);
|
spy.mock.calls.filter((args) => args[0] === eventType);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import '../olm-loader';
|
|||||||
|
|
||||||
import { logger } from '../../src/logger';
|
import { logger } from '../../src/logger';
|
||||||
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||||
import { ClientEvent, EventType, MatrixClient, MsgType } from "../../src";
|
import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src";
|
||||||
import { SyncState } from "../../src/sync";
|
import { SyncState } from "../../src/sync";
|
||||||
import { eventMapperFor } from "../../src/event-mapper";
|
import { eventMapperFor } from "../../src/event-mapper";
|
||||||
|
|
||||||
@@ -74,6 +74,7 @@ interface IEventOpts {
|
|||||||
sender?: string;
|
sender?: string;
|
||||||
skey?: string;
|
skey?: string;
|
||||||
content: IContent;
|
content: IContent;
|
||||||
|
prev_content?: IContent;
|
||||||
user?: string;
|
user?: string;
|
||||||
unsigned?: IUnsigned;
|
unsigned?: IUnsigned;
|
||||||
redacts?: string;
|
redacts?: string;
|
||||||
@@ -103,6 +104,7 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
|
|||||||
room_id: opts.room,
|
room_id: opts.room,
|
||||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||||
content: opts.content,
|
content: opts.content,
|
||||||
|
prev_content: opts.prev_content,
|
||||||
unsigned: opts.unsigned || {},
|
unsigned: opts.unsigned || {},
|
||||||
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
|
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
|
||||||
txn_id: "~" + Math.random(),
|
txn_id: "~" + Math.random(),
|
||||||
@@ -147,9 +149,9 @@ export function mkEventCustom<T>(base: T): T & GeneratedMetadata {
|
|||||||
interface IPresenceOpts {
|
interface IPresenceOpts {
|
||||||
user?: string;
|
user?: string;
|
||||||
sender?: string;
|
sender?: string;
|
||||||
url: string;
|
url?: string;
|
||||||
name: string;
|
name?: string;
|
||||||
ago: number;
|
ago?: number;
|
||||||
presence?: string;
|
presence?: string;
|
||||||
event?: boolean;
|
event?: boolean;
|
||||||
}
|
}
|
||||||
@@ -371,3 +373,14 @@ export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent>
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));
|
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));
|
||||||
|
|
||||||
|
export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
|
||||||
|
app_display_name: "app",
|
||||||
|
app_id: "123",
|
||||||
|
data: {},
|
||||||
|
device_display_name: "name",
|
||||||
|
kind: "http",
|
||||||
|
lang: "en",
|
||||||
|
pushkey: "pushpush",
|
||||||
|
...extra,
|
||||||
|
});
|
||||||
|
|||||||
@@ -21,18 +21,21 @@ describe("NamespacedValue", () => {
|
|||||||
const ns = new NamespacedValue("stable", "unstable");
|
const ns = new NamespacedValue("stable", "unstable");
|
||||||
expect(ns.name).toBe(ns.stable);
|
expect(ns.name).toBe(ns.stable);
|
||||||
expect(ns.altName).toBe(ns.unstable);
|
expect(ns.altName).toBe(ns.unstable);
|
||||||
|
expect(ns.names).toEqual([ns.stable, ns.unstable]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return unstable if there is no stable", () => {
|
it("should return unstable if there is no stable", () => {
|
||||||
const ns = new NamespacedValue(null, "unstable");
|
const ns = new NamespacedValue(null, "unstable");
|
||||||
expect(ns.name).toBe(ns.unstable);
|
expect(ns.name).toBe(ns.unstable);
|
||||||
expect(ns.altName).toBeFalsy();
|
expect(ns.altName).toBeFalsy();
|
||||||
|
expect(ns.names).toEqual([ns.unstable]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have a falsey unstable if needed", () => {
|
it("should have a falsey unstable if needed", () => {
|
||||||
const ns = new NamespacedValue("stable", null);
|
const ns = new NamespacedValue("stable", null);
|
||||||
expect(ns.name).toBe(ns.stable);
|
expect(ns.name).toBe(ns.stable);
|
||||||
expect(ns.altName).toBeFalsy();
|
expect(ns.altName).toBeFalsy();
|
||||||
|
expect(ns.names).toEqual([ns.stable]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should match against either stable or unstable", () => {
|
it("should match against either stable or unstable", () => {
|
||||||
@@ -58,12 +61,14 @@ describe("UnstableValue", () => {
|
|||||||
const ns = new UnstableValue("stable", "unstable");
|
const ns = new UnstableValue("stable", "unstable");
|
||||||
expect(ns.name).toBe(ns.unstable);
|
expect(ns.name).toBe(ns.unstable);
|
||||||
expect(ns.altName).toBe(ns.stable);
|
expect(ns.altName).toBe(ns.stable);
|
||||||
|
expect(ns.names).toEqual([ns.unstable, ns.stable]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return unstable if there is no stable", () => {
|
it("should return unstable if there is no stable", () => {
|
||||||
const ns = new UnstableValue(null, "unstable");
|
const ns = new UnstableValue(null, "unstable");
|
||||||
expect(ns.name).toBe(ns.unstable);
|
expect(ns.name).toBe(ns.unstable);
|
||||||
expect(ns.altName).toBeFalsy();
|
expect(ns.altName).toBeFalsy();
|
||||||
|
expect(ns.names).toEqual([ns.unstable]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not permit falsey unstable values", () => {
|
it("should not permit falsey unstable values", () => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
makeBeaconContent,
|
makeBeaconContent,
|
||||||
makeBeaconInfoContent,
|
makeBeaconInfoContent,
|
||||||
makeTopicContent,
|
makeTopicContent,
|
||||||
|
parseBeaconContent,
|
||||||
parseTopicContent,
|
parseTopicContent,
|
||||||
} from "../../src/content-helpers";
|
} from "../../src/content-helpers";
|
||||||
|
|
||||||
@@ -127,6 +128,66 @@ describe('Beacon content helpers', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("parseBeaconContent()", () => {
|
||||||
|
it("should not explode when parsing an invalid beacon", () => {
|
||||||
|
// deliberate cast to simulate wire content being invalid
|
||||||
|
const result = parseBeaconContent({} as any);
|
||||||
|
expect(result).toEqual({
|
||||||
|
description: undefined,
|
||||||
|
uri: undefined,
|
||||||
|
timestamp: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse unstable values", () => {
|
||||||
|
const uri = "urigoeshere";
|
||||||
|
const description = "descriptiongoeshere";
|
||||||
|
const timestamp = 1234;
|
||||||
|
const result = parseBeaconContent({
|
||||||
|
"org.matrix.msc3488.location": {
|
||||||
|
uri,
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
"org.matrix.msc3488.ts": timestamp,
|
||||||
|
|
||||||
|
// relationship not used - just here to satisfy types
|
||||||
|
"m.relates_to": {
|
||||||
|
rel_type: "m.reference",
|
||||||
|
event_id: "$unused",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
description,
|
||||||
|
uri,
|
||||||
|
timestamp,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse stable values", () => {
|
||||||
|
const uri = "urigoeshere";
|
||||||
|
const description = "descriptiongoeshere";
|
||||||
|
const timestamp = 1234;
|
||||||
|
const result = parseBeaconContent({
|
||||||
|
"m.location": {
|
||||||
|
uri,
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
"m.ts": timestamp,
|
||||||
|
|
||||||
|
// relationship not used - just here to satisfy types
|
||||||
|
"m.relates_to": {
|
||||||
|
rel_type: "m.reference",
|
||||||
|
event_id: "$unused",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
description,
|
||||||
|
uri,
|
||||||
|
timestamp,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Topic content helpers', () => {
|
describe('Topic content helpers', () => {
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import { CRYPTO_ENABLED } from "../../src/client";
|
|||||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||||
import { logger } from '../../src/logger';
|
import { logger } from '../../src/logger';
|
||||||
import { MemoryStore } from "../../src";
|
import { MemoryStore } from "../../src";
|
||||||
|
import { RoomKeyRequestState } from '../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||||
|
import { RoomMember } from '../../src/models/room-member';
|
||||||
|
import { IStore } from '../../src/store';
|
||||||
|
|
||||||
const Olm = global.Olm;
|
const Olm = global.Olm;
|
||||||
|
|
||||||
@@ -39,20 +42,52 @@ async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent>
|
|||||||
type: "m.forwarded_room_key",
|
type: "m.forwarded_room_key",
|
||||||
sender: client.getUserId(),
|
sender: client.getUserId(),
|
||||||
content: {
|
content: {
|
||||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||||
room_id: roomId,
|
"room_id": roomId,
|
||||||
sender_key: eventContent.sender_key,
|
"sender_key": eventContent.sender_key,
|
||||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
|
||||||
session_id: eventContent.session_id,
|
"session_id": eventContent.session_id,
|
||||||
session_key: key.key,
|
"session_key": key.key,
|
||||||
chain_index: key.chain_index,
|
"chain_index": key.chain_index,
|
||||||
forwarding_curve25519_key_chain:
|
"forwarding_curve25519_key_chain": key.forwarding_curve_key_chain,
|
||||||
key.forwarding_curve_key_chain,
|
"org.matrix.msc3061.shared_history": true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// make onRoomKeyEvent think this was an encrypted event
|
// make onRoomKeyEvent think this was an encrypted event
|
||||||
// @ts-ignore private property
|
// @ts-ignore private property
|
||||||
ksEvent.senderCurve25519Key = "akey";
|
ksEvent.senderCurve25519Key = "akey";
|
||||||
|
ksEvent.getWireType = () => "m.room.encrypted";
|
||||||
|
ksEvent.getWireContent = () => {
|
||||||
|
return {
|
||||||
|
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return ksEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function roomKeyEventForEvent(client: MatrixClient, event: MatrixEvent): MatrixEvent {
|
||||||
|
const roomId = event.getRoomId();
|
||||||
|
const eventContent = event.getWireContent();
|
||||||
|
const key = client.crypto.olmDevice.getOutboundGroupSessionKey(eventContent.session_id);
|
||||||
|
const ksEvent = new MatrixEvent({
|
||||||
|
type: "m.room_key",
|
||||||
|
sender: client.getUserId(),
|
||||||
|
content: {
|
||||||
|
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||||
|
"room_id": roomId,
|
||||||
|
"session_id": eventContent.session_id,
|
||||||
|
"session_key": key.key,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// make onRoomKeyEvent think this was an encrypted event
|
||||||
|
// @ts-ignore private property
|
||||||
|
ksEvent.senderCurve25519Key = event.getSenderKey();
|
||||||
|
ksEvent.getWireType = () => "m.room.encrypted";
|
||||||
|
ksEvent.getWireContent = () => {
|
||||||
|
return {
|
||||||
|
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||||
|
};
|
||||||
|
};
|
||||||
return ksEvent;
|
return ksEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +129,7 @@ describe("Crypto", function() {
|
|||||||
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||||
event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };};
|
event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };};
|
||||||
event.getForwardingCurve25519KeyChain = () => ["not empty"];
|
event.getForwardingCurve25519KeyChain = () => ["not empty"];
|
||||||
event.isKeySourceUntrusted = () => false;
|
event.isKeySourceUntrusted = () => true;
|
||||||
event.getClaimedEd25519Key =
|
event.getClaimedEd25519Key =
|
||||||
() => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
() => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||||
|
|
||||||
@@ -158,8 +193,8 @@ describe("Crypto", function() {
|
|||||||
let fakeEmitter;
|
let fakeEmitter;
|
||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
const mockStorage = new MockStorageApi();
|
const mockStorage = new MockStorageApi() as unknown as Storage;
|
||||||
const clientStore = new MemoryStore({ localStorage: mockStorage });
|
const clientStore = new MemoryStore({ localStorage: mockStorage }) as unknown as IStore;
|
||||||
const cryptoStore = new MemoryCryptoStore();
|
const cryptoStore = new MemoryCryptoStore();
|
||||||
|
|
||||||
cryptoStore.storeEndToEndDeviceData({
|
cryptoStore.storeEndToEndDeviceData({
|
||||||
@@ -232,6 +267,7 @@ describe("Crypto", function() {
|
|||||||
describe('Key requests', function() {
|
describe('Key requests', function() {
|
||||||
let aliceClient: MatrixClient;
|
let aliceClient: MatrixClient;
|
||||||
let bobClient: MatrixClient;
|
let bobClient: MatrixClient;
|
||||||
|
let claraClient: MatrixClient;
|
||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
aliceClient = (new TestClient(
|
aliceClient = (new TestClient(
|
||||||
@@ -240,22 +276,35 @@ describe("Crypto", function() {
|
|||||||
bobClient = (new TestClient(
|
bobClient = (new TestClient(
|
||||||
"@bob:example.com", "bobdevice",
|
"@bob:example.com", "bobdevice",
|
||||||
)).client;
|
)).client;
|
||||||
|
claraClient = (new TestClient(
|
||||||
|
"@clara:example.com", "claradevice",
|
||||||
|
)).client;
|
||||||
await aliceClient.initCrypto();
|
await aliceClient.initCrypto();
|
||||||
await bobClient.initCrypto();
|
await bobClient.initCrypto();
|
||||||
|
await claraClient.initCrypto();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async function() {
|
afterEach(async function() {
|
||||||
aliceClient.stopClient();
|
aliceClient.stopClient();
|
||||||
bobClient.stopClient();
|
bobClient.stopClient();
|
||||||
|
claraClient.stopClient();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not cancel keyshare requests if some messages are not decrypted", async function() {
|
it("does not cancel keyshare requests until all messages are decrypted with trusted keys", async function() {
|
||||||
const encryptionCfg = {
|
const encryptionCfg = {
|
||||||
"algorithm": "m.megolm.v1.aes-sha2",
|
"algorithm": "m.megolm.v1.aes-sha2",
|
||||||
};
|
};
|
||||||
const roomId = "!someroom";
|
const roomId = "!someroom";
|
||||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||||
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||||
|
// Make Bob invited by Alice so Bob will accept Alice's forwarded keys
|
||||||
|
bobRoom.currentState.setStateEvents([new MatrixEvent({
|
||||||
|
type: "m.room.member",
|
||||||
|
sender: "@alice:example.com",
|
||||||
|
room_id: roomId,
|
||||||
|
content: { membership: "invite" },
|
||||||
|
state_key: "@bob:example.com",
|
||||||
|
})]);
|
||||||
aliceClient.store.storeRoom(aliceRoom);
|
aliceClient.store.storeRoom(aliceRoom);
|
||||||
bobClient.store.storeRoom(bobRoom);
|
bobClient.store.storeRoom(bobRoom);
|
||||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||||
@@ -301,6 +350,9 @@ describe("Crypto", function() {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const device = new DeviceInfo(aliceClient.deviceId);
|
||||||
|
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||||
|
|
||||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||||
);
|
);
|
||||||
@@ -313,6 +365,8 @@ describe("Crypto", function() {
|
|||||||
// the first message can't be decrypted yet, but the second one
|
// the first message can't be decrypted yet, but the second one
|
||||||
// can
|
// can
|
||||||
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
|
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
|
||||||
|
bobClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
|
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||||
await decryptEventsPromise;
|
await decryptEventsPromise;
|
||||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||||
@@ -339,8 +393,24 @@ describe("Crypto", function() {
|
|||||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||||
await decryptEventPromise;
|
await decryptEventPromise;
|
||||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||||
|
expect(events[0].isKeySourceUntrusted()).toBeTruthy();
|
||||||
await sleep(1);
|
await sleep(1);
|
||||||
// the room key request should be gone since we've now decrypted everything
|
// the room key request should still be there, since we've
|
||||||
|
// decrypted everything with an untrusted key
|
||||||
|
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeDefined();
|
||||||
|
|
||||||
|
// Now share a trusted room key event so Bob will re-decrypt the messages.
|
||||||
|
// Bob will backfill trust when they receive a trusted session with a higher
|
||||||
|
// index that connects to an untrusted session with a lower index.
|
||||||
|
const roomKeyEvent = roomKeyEventForEvent(aliceClient, events[1]);
|
||||||
|
const trustedDecryptEventPromise = awaitEvent(events[0], "Event.decrypted");
|
||||||
|
await bobDecryptor.onRoomKeyEvent(roomKeyEvent);
|
||||||
|
await trustedDecryptEventPromise;
|
||||||
|
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||||
|
expect(events[0].isKeySourceUntrusted()).toBeFalsy();
|
||||||
|
await sleep(1);
|
||||||
|
// now the room key request should be gone, since there's
|
||||||
|
// no better key to wait for
|
||||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy();
|
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -382,6 +452,9 @@ describe("Crypto", function() {
|
|||||||
// decryption keys yet
|
// decryption keys yet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const device = new DeviceInfo(aliceClient.deviceId);
|
||||||
|
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||||
|
|
||||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||||
);
|
);
|
||||||
@@ -461,6 +534,420 @@ describe("Crypto", function() {
|
|||||||
expect(aliceSendToDevice).toBeCalledTimes(3);
|
expect(aliceSendToDevice).toBeCalledTimes(3);
|
||||||
expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId);
|
expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should accept forwarded keys which it requested", 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", {});
|
||||||
|
aliceClient.store.storeRoom(aliceRoom);
|
||||||
|
bobClient.store.storeRoom(bobRoom);
|
||||||
|
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||||
|
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||||
|
const events = [
|
||||||
|
new MatrixEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: "@alice:example.com",
|
||||||
|
room_id: roomId,
|
||||||
|
event_id: "$1",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "1",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new MatrixEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: "@alice:example.com",
|
||||||
|
room_id: roomId,
|
||||||
|
event_id: "$2",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "2",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
await Promise.all(events.map(async (event) => {
|
||||||
|
// alice encrypts each event, and then bob tries to decrypt
|
||||||
|
// them without any keys, so that they'll be in pending
|
||||||
|
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||||
|
// remove keys from the event
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.clearEvent = undefined;
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.senderCurve25519Key = null;
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.claimedEd25519Key = null;
|
||||||
|
try {
|
||||||
|
await bobClient.crypto.decryptEvent(event);
|
||||||
|
} catch (e) {
|
||||||
|
// we expect this to fail because we don't have the
|
||||||
|
// decryption keys yet
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const device = new DeviceInfo(aliceClient.deviceId);
|
||||||
|
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||||
|
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept forwarded keys from the user who invited it to the room", 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 claraRoom = new Room(roomId, claraClient, "@clara:example.com", {});
|
||||||
|
// Make Bob invited by Clara
|
||||||
|
bobRoom.currentState.setStateEvents([new MatrixEvent({
|
||||||
|
type: "m.room.member",
|
||||||
|
sender: "@clara:example.com",
|
||||||
|
room_id: roomId,
|
||||||
|
content: { membership: "invite" },
|
||||||
|
state_key: "@bob:example.com",
|
||||||
|
})]);
|
||||||
|
aliceClient.store.storeRoom(aliceRoom);
|
||||||
|
bobClient.store.storeRoom(bobRoom);
|
||||||
|
claraClient.store.storeRoom(claraRoom);
|
||||||
|
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||||
|
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||||
|
await claraClient.setRoomEncryption(roomId, encryptionCfg);
|
||||||
|
const events = [
|
||||||
|
new MatrixEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: "@alice:example.com",
|
||||||
|
room_id: roomId,
|
||||||
|
event_id: "$1",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "1",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new MatrixEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: "@alice:example.com",
|
||||||
|
room_id: roomId,
|
||||||
|
event_id: "$2",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "2",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
await Promise.all(events.map(async (event) => {
|
||||||
|
// alice encrypts each event, and then bob tries to decrypt
|
||||||
|
// them without any keys, so that they'll be in pending
|
||||||
|
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||||
|
// remove keys from the event
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.clearEvent = undefined;
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.senderCurve25519Key = null;
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.claimedEd25519Key = null;
|
||||||
|
try {
|
||||||
|
await bobClient.crypto.decryptEvent(event);
|
||||||
|
} catch (e) {
|
||||||
|
// we expect this to fail because we don't have the
|
||||||
|
// decryption keys yet
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const device = new DeviceInfo(claraClient.deviceId);
|
||||||
|
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||||
|
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@clara: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 = claraClient.getUserId(),
|
||||||
|
ksEvent.sender = new RoomMember(roomId, claraClient.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");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept forwarded keys 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", {});
|
||||||
|
aliceClient.store.storeRoom(aliceRoom);
|
||||||
|
bobClient.store.storeRoom(bobRoom);
|
||||||
|
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||||
|
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||||
|
const events = [
|
||||||
|
new MatrixEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: "@alice:example.com",
|
||||||
|
room_id: roomId,
|
||||||
|
event_id: "$1",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "1",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new MatrixEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: "@alice:example.com",
|
||||||
|
room_id: roomId,
|
||||||
|
event_id: "$2",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "2",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
await Promise.all(events.map(async (event) => {
|
||||||
|
// alice encrypts each event, and then bob tries to decrypt
|
||||||
|
// them without any keys, so that they'll be in pending
|
||||||
|
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||||
|
// remove keys from the event
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.clearEvent = undefined;
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.senderCurve25519Key = null;
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.claimedEd25519Key = null;
|
||||||
|
try {
|
||||||
|
await bobClient.crypto.decryptEvent(event);
|
||||||
|
} catch (e) {
|
||||||
|
// we expect this to fail because we don't have the
|
||||||
|
// decryption keys yet
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const device = new DeviceInfo(claraClient.deviceId);
|
||||||
|
device.verified = DeviceInfo.DeviceVerification.VERIFIED;
|
||||||
|
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||||
|
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@bob: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());
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not accept unexpected forwarded keys for a room it's in", 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 claraRoom = new Room(roomId, claraClient, "@clara:example.com", {});
|
||||||
|
aliceClient.store.storeRoom(aliceRoom);
|
||||||
|
bobClient.store.storeRoom(bobRoom);
|
||||||
|
claraClient.store.storeRoom(claraRoom);
|
||||||
|
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||||
|
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||||
|
await claraClient.setRoomEncryption(roomId, encryptionCfg);
|
||||||
|
const events = [
|
||||||
|
new MatrixEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: "@alice:example.com",
|
||||||
|
room_id: roomId,
|
||||||
|
event_id: "$1",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "1",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new MatrixEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: "@alice:example.com",
|
||||||
|
room_id: roomId,
|
||||||
|
event_id: "$2",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "2",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
await Promise.all(events.map(async (event) => {
|
||||||
|
// alice encrypts each event, and then bob tries to decrypt
|
||||||
|
// them without any keys, so that they'll be in pending
|
||||||
|
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||||
|
// remove keys from the event
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.clearEvent = undefined;
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.senderCurve25519Key = null;
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.claimedEd25519Key = null;
|
||||||
|
try {
|
||||||
|
await bobClient.crypto.decryptEvent(event);
|
||||||
|
} catch (e) {
|
||||||
|
// we expect this to fail because we don't have the
|
||||||
|
// decryption keys yet
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const device = new DeviceInfo(claraClient.deviceId);
|
||||||
|
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||||
|
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||||
|
|
||||||
|
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||||
|
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||||
|
ksEvent.event.sender = claraClient.getUserId(),
|
||||||
|
ksEvent.sender = new RoomMember(roomId, claraClient.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).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should park forwarded keys for a room it's not in", async function() {
|
||||||
|
const encryptionCfg = {
|
||||||
|
"algorithm": "m.megolm.v1.aes-sha2",
|
||||||
|
};
|
||||||
|
const roomId = "!someroom";
|
||||||
|
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||||
|
aliceClient.store.storeRoom(aliceRoom);
|
||||||
|
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||||
|
const events = [
|
||||||
|
new MatrixEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: "@alice:example.com",
|
||||||
|
room_id: roomId,
|
||||||
|
event_id: "$1",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "1",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new MatrixEvent({
|
||||||
|
type: "m.room.message",
|
||||||
|
sender: "@alice:example.com",
|
||||||
|
room_id: roomId,
|
||||||
|
event_id: "$2",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "2",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
await Promise.all(events.map(async (event) => {
|
||||||
|
// alice encrypts each event, and then bob tries to decrypt
|
||||||
|
// them without any keys, so that they'll be in pending
|
||||||
|
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||||
|
// remove keys from the event
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.clearEvent = undefined;
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.senderCurve25519Key = null;
|
||||||
|
// @ts-ignore private properties
|
||||||
|
event.claimedEd25519Key = null;
|
||||||
|
}));
|
||||||
|
|
||||||
|
const device = new DeviceInfo(aliceClient.deviceId);
|
||||||
|
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||||
|
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||||
|
|
||||||
|
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||||
|
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = events[0].getWireContent();
|
||||||
|
|
||||||
|
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
|
||||||
|
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||||
|
const bobKey = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
|
||||||
|
roomId,
|
||||||
|
content.sender_key,
|
||||||
|
content.session_id,
|
||||||
|
);
|
||||||
|
expect(bobKey).toBeNull();
|
||||||
|
|
||||||
|
const aliceKey = await aliceClient.crypto.olmDevice.getInboundGroupSessionKey(
|
||||||
|
roomId,
|
||||||
|
content.sender_key,
|
||||||
|
content.session_id,
|
||||||
|
);
|
||||||
|
const parked = await bobClient.crypto.cryptoStore.takeParkedSharedHistory(roomId);
|
||||||
|
expect(parked).toEqual([{
|
||||||
|
senderId: aliceClient.getUserId(),
|
||||||
|
senderKey: content.sender_key,
|
||||||
|
sessionId: content.session_id,
|
||||||
|
sessionKey: aliceKey.key,
|
||||||
|
keysClaimed: { ed25519: aliceKey.sender_claimed_ed25519_key },
|
||||||
|
forwardingCurve25519KeyChain: ["akey"],
|
||||||
|
}]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Secret storage', function() {
|
describe('Secret storage', function() {
|
||||||
@@ -469,12 +956,12 @@ describe("Crypto", function() {
|
|||||||
jest.setTimeout(10000);
|
jest.setTimeout(10000);
|
||||||
const client = (new TestClient("@a:example.com", "dev")).client;
|
const client = (new TestClient("@a:example.com", "dev")).client;
|
||||||
await client.initCrypto();
|
await client.initCrypto();
|
||||||
client.crypto.getSecretStorageKey = async () => null;
|
client.crypto.getSecretStorageKey = jest.fn().mockResolvedValue(null);
|
||||||
client.crypto.isCrossSigningReady = async () => false;
|
client.crypto.isCrossSigningReady = async () => false;
|
||||||
client.crypto.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
|
client.crypto.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
|
||||||
client.crypto.baseApis.setAccountData = () => null;
|
client.crypto.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
|
||||||
client.crypto.baseApis.uploadKeySignatures = () => null;
|
client.crypto.baseApis.uploadKeySignatures = jest.fn();
|
||||||
client.crypto.baseApis.http.authedRequest = () => null;
|
client.crypto.baseApis.http.authedRequest = jest.fn();
|
||||||
const createSecretStorageKey = async () => {
|
const createSecretStorageKey = async () => {
|
||||||
return {
|
return {
|
||||||
keyInfo: undefined, // Returning undefined here used to cause a crash
|
keyInfo: undefined, // Returning undefined here used to cause a crash
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ import { ClientEvent, MatrixClient, RoomMember } from '../../../../src';
|
|||||||
import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo';
|
import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo';
|
||||||
import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning';
|
import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning';
|
||||||
|
|
||||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||||
|
|
||||||
const ROOM_ID = '!ROOM:ID';
|
const ROOM_ID = '!ROOM:ID';
|
||||||
|
|
||||||
@@ -110,6 +110,12 @@ describe("MegolmDecryption", function() {
|
|||||||
senderCurve25519Key: "SENDER_CURVE25519",
|
senderCurve25519Key: "SENDER_CURVE25519",
|
||||||
claimedEd25519Key: "SENDER_ED25519",
|
claimedEd25519Key: "SENDER_ED25519",
|
||||||
};
|
};
|
||||||
|
event.getWireType = () => "m.room.encrypted";
|
||||||
|
event.getWireContent = () => {
|
||||||
|
return {
|
||||||
|
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const mockCrypto = {
|
const mockCrypto = {
|
||||||
decryptEvent: function() {
|
decryptEvent: function() {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import { IAbortablePromise, MatrixScheduler } from '../../../src';
|
|||||||
|
|
||||||
const Olm = global.Olm;
|
const Olm = global.Olm;
|
||||||
|
|
||||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||||
|
|
||||||
const ROOM_ID = '!ROOM:ID';
|
const ROOM_ID = '!ROOM:ID';
|
||||||
|
|
||||||
@@ -214,6 +214,12 @@ describe("MegolmBackup", function() {
|
|||||||
const event = new MatrixEvent({
|
const event = new MatrixEvent({
|
||||||
type: 'm.room.encrypted',
|
type: 'm.room.encrypted',
|
||||||
});
|
});
|
||||||
|
event.getWireType = () => "m.room.encrypted";
|
||||||
|
event.getWireContent = () => {
|
||||||
|
return {
|
||||||
|
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||||
|
};
|
||||||
|
};
|
||||||
const decryptedData = {
|
const decryptedData = {
|
||||||
clearEvent: {
|
clearEvent: {
|
||||||
type: 'm.room_key',
|
type: 'm.room_key',
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { CryptoStore } from '../../../src/crypto/store/base';
|
||||||
IndexedDBCryptoStore,
|
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||||
} from '../../../src/crypto/store/indexeddb-crypto-store';
|
import { LocalStorageCryptoStore } from '../../../src/crypto/store/localStorage-crypto-store';
|
||||||
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
|
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
|
||||||
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
|
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||||
|
|
||||||
@@ -26,36 +26,39 @@ import 'jest-localstorage-mock';
|
|||||||
const requests = [
|
const requests = [
|
||||||
{
|
{
|
||||||
requestId: "A",
|
requestId: "A",
|
||||||
requestBody: { session_id: "A", room_id: "A" },
|
requestBody: { session_id: "A", room_id: "A", sender_key: "A", algorithm: "m.megolm.v1.aes-sha2" },
|
||||||
state: RoomKeyRequestState.Sent,
|
state: RoomKeyRequestState.Sent,
|
||||||
|
recipients: [
|
||||||
|
{ userId: "@alice:example.com", deviceId: "*" },
|
||||||
|
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
requestId: "B",
|
requestId: "B",
|
||||||
requestBody: { session_id: "B", room_id: "B" },
|
requestBody: { session_id: "B", room_id: "B", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" },
|
||||||
state: RoomKeyRequestState.Sent,
|
state: RoomKeyRequestState.Sent,
|
||||||
|
recipients: [
|
||||||
|
{ userId: "@alice:example.com", deviceId: "*" },
|
||||||
|
{ userId: "@carrie:example.com", deviceId: "barbazquux" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
requestId: "C",
|
requestId: "C",
|
||||||
requestBody: { session_id: "C", room_id: "C" },
|
requestBody: { session_id: "C", room_id: "C", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" },
|
||||||
state: RoomKeyRequestState.Unsent,
|
state: RoomKeyRequestState.Unsent,
|
||||||
|
recipients: [
|
||||||
|
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
["IndexedDBCryptoStore",
|
["IndexedDBCryptoStore",
|
||||||
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||||
["LocalStorageCryptoStore",
|
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
|
||||||
() => new IndexedDBCryptoStore(undefined, "tests")],
|
["MemoryCryptoStore", () => new MemoryCryptoStore()],
|
||||||
["MemoryCryptoStore", () => {
|
|
||||||
const store = new IndexedDBCryptoStore(undefined, "tests");
|
|
||||||
// @ts-ignore set private properties
|
|
||||||
store.backend = new MemoryCryptoStore();
|
|
||||||
// @ts-ignore
|
|
||||||
store.backendPromise = Promise.resolve(store.backend);
|
|
||||||
return store;
|
|
||||||
}],
|
|
||||||
])("Outgoing room key requests [%s]", function(name, dbFactory) {
|
])("Outgoing room key requests [%s]", function(name, dbFactory) {
|
||||||
let store;
|
let store: CryptoStore;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
store = dbFactory();
|
store = dbFactory();
|
||||||
@@ -75,6 +78,15 @@ describe.each([
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("getOutgoingRoomKeyRequestsByTarget retrieves all entries with a given target",
|
||||||
|
async () => {
|
||||||
|
const r = await store.getOutgoingRoomKeyRequestsByTarget(
|
||||||
|
"@becca:example.com", "foobarbaz", [RoomKeyRequestState.Sent],
|
||||||
|
);
|
||||||
|
expect(r).toHaveLength(1);
|
||||||
|
expect(r[0]).toEqual(requests[0]);
|
||||||
|
});
|
||||||
|
|
||||||
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
|
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
|
||||||
async () => {
|
async () => {
|
||||||
const r =
|
const r =
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { logger } from '../../../src/logger';
|
|||||||
import * as utils from "../../../src/utils";
|
import * as utils from "../../../src/utils";
|
||||||
import { ICreateClientOpts } from '../../../src/client';
|
import { ICreateClientOpts } from '../../../src/client';
|
||||||
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
|
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
|
||||||
|
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
@@ -250,20 +251,20 @@ describe("Secrets", function() {
|
|||||||
|
|
||||||
osborne2.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
osborne2.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||||
"VAX": {
|
"VAX": {
|
||||||
user_id: "@alice:example.com",
|
known: false,
|
||||||
device_id: "VAX",
|
|
||||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||||
keys: {
|
keys: {
|
||||||
"ed25519:VAX": vaxDevice.deviceEd25519Key,
|
"ed25519:VAX": vaxDevice.deviceEd25519Key,
|
||||||
"curve25519:VAX": vaxDevice.deviceCurve25519Key,
|
"curve25519:VAX": vaxDevice.deviceCurve25519Key,
|
||||||
},
|
},
|
||||||
|
verified: DeviceInfo.DeviceVerification.VERIFIED,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||||
"Osborne2": {
|
"Osborne2": {
|
||||||
user_id: "@alice:example.com",
|
|
||||||
device_id: "Osborne2",
|
|
||||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||||
|
verified: 0,
|
||||||
|
known: false,
|
||||||
keys: {
|
keys: {
|
||||||
"ed25519:Osborne2": osborne2Device.deviceEd25519Key,
|
"ed25519:Osborne2": osborne2Device.deviceEd25519Key,
|
||||||
"curve25519:Osborne2": osborne2Device.deviceCurve25519Key,
|
"curve25519:Osborne2": osborne2Device.deviceCurve25519Key,
|
||||||
@@ -280,10 +281,12 @@ describe("Secrets", function() {
|
|||||||
Object.values(otks)[0],
|
Object.values(otks)[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
const request = await secretStorage.request("foo", ["VAX"]);
|
osborne2.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||||
const secret = await request.promise;
|
osborne2.client.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||||
|
|
||||||
|
const request = await secretStorage.request("foo", ["VAX"]);
|
||||||
|
await request.promise; // return value not used
|
||||||
|
|
||||||
expect(secret).toBe("bar");
|
|
||||||
osborne2.stop();
|
osborne2.stop();
|
||||||
vax.stop();
|
vax.stop();
|
||||||
clearTestClientTimeouts();
|
clearTestClientTimeouts();
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
import { MatrixClient } from "../../../../src/client";
|
||||||
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
|
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||||
import { MatrixEvent } from "../../../../src/models/event";
|
import { MatrixEvent } from "../../../../src/models/event";
|
||||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
|
||||||
|
|
||||||
describe("InRoomChannel tests", function() {
|
describe("InRoomChannel tests", function() {
|
||||||
const ALICE = "@alice:hs.tld";
|
const ALICE = "@alice:hs.tld";
|
||||||
@@ -23,7 +23,7 @@ describe("InRoomChannel tests", function() {
|
|||||||
const MALORY = "@malory:hs.tld";
|
const MALORY = "@malory:hs.tld";
|
||||||
const client = {
|
const client = {
|
||||||
getUserId() { return ALICE; },
|
getUserId() { return ALICE; },
|
||||||
};
|
} as unknown as MatrixClient;
|
||||||
|
|
||||||
it("getEventType only returns .request for a message with a msgtype", function() {
|
it("getEventType only returns .request for a message with a msgtype", function() {
|
||||||
const invalidEvent = new MatrixEvent({
|
const invalidEvent = new MatrixEvent({
|
||||||
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import "../../../olm-loader";
|
import "../../../olm-loader";
|
||||||
import { verificationMethods } from "../../../../src/crypto";
|
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
|
||||||
import { logger } from "../../../../src/logger";
|
import { logger } from "../../../../src/logger";
|
||||||
import { SAS } from "../../../../src/crypto/verification/SAS";
|
import { SAS } from "../../../../src/crypto/verification/SAS";
|
||||||
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
|
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
|
||||||
@@ -52,23 +52,22 @@ describe("verification request integration tests with crypto layer", function()
|
|||||||
alice.client.crypto.deviceList.getRawStoredDevicesForUser = function() {
|
alice.client.crypto.deviceList.getRawStoredDevicesForUser = function() {
|
||||||
return {
|
return {
|
||||||
Dynabook: {
|
Dynabook: {
|
||||||
|
algorithms: [],
|
||||||
|
verified: 0,
|
||||||
|
known: false,
|
||||||
keys: {
|
keys: {
|
||||||
"ed25519:Dynabook": "bob+base64+ed25519+key",
|
"ed25519:Dynabook": "bob+base64+ed25519+key",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
alice.client.downloadKeys = () => {
|
alice.client.downloadKeys = jest.fn().mockResolvedValue({});
|
||||||
return Promise.resolve();
|
bob.client.downloadKeys = jest.fn().mockResolvedValue({});
|
||||||
};
|
bob.client.on(CryptoEvent.VerificationRequest, (request) => {
|
||||||
bob.client.downloadKeys = () => {
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
bob.client.on("crypto.verification.request", (request) => {
|
|
||||||
const bobVerifier = request.beginKeyVerification(verificationMethods.SAS);
|
const bobVerifier = request.beginKeyVerification(verificationMethods.SAS);
|
||||||
bobVerifier.verify();
|
bobVerifier.verify();
|
||||||
|
|
||||||
// XXX: Private function access (but it's a test, so we're okay)
|
// @ts-ignore Private function access (but it's a test, so we're okay)
|
||||||
bobVerifier.endTimer();
|
bobVerifier.endTimer();
|
||||||
});
|
});
|
||||||
const aliceRequest = await alice.client.requestVerification("@bob:example.com");
|
const aliceRequest = await alice.client.requestVerification("@bob:example.com");
|
||||||
@@ -76,7 +75,7 @@ describe("verification request integration tests with crypto layer", function()
|
|||||||
const aliceVerifier = aliceRequest.verifier;
|
const aliceVerifier = aliceRequest.verifier;
|
||||||
expect(aliceVerifier).toBeInstanceOf(SAS);
|
expect(aliceVerifier).toBeInstanceOf(SAS);
|
||||||
|
|
||||||
// XXX: Private function access (but it's a test, so we're okay)
|
// @ts-ignore Private function access (but it's a test, so we're okay)
|
||||||
aliceVerifier.endTimer();
|
aliceVerifier.endTimer();
|
||||||
|
|
||||||
alice.stop();
|
alice.stop();
|
||||||
@@ -19,10 +19,14 @@ import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
|
|||||||
import { MatrixEvent } from "../../../../src/models/event";
|
import { MatrixEvent } from "../../../../src/models/event";
|
||||||
import { SAS } from "../../../../src/crypto/verification/SAS";
|
import { SAS } from "../../../../src/crypto/verification/SAS";
|
||||||
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
|
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
|
||||||
import { verificationMethods } from "../../../../src/crypto";
|
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
|
||||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||||
import { logger } from "../../../../src/logger";
|
import { logger } from "../../../../src/logger";
|
||||||
import { resetCrossSigningKeys } from "../crypto-utils";
|
import { resetCrossSigningKeys } from "../crypto-utils";
|
||||||
|
import { VerificationBase } from "../../../../src/crypto/verification/Base";
|
||||||
|
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
|
||||||
|
import { MatrixClient } from "../../../../src";
|
||||||
|
import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest";
|
||||||
|
|
||||||
const Olm = global.Olm;
|
const Olm = global.Olm;
|
||||||
|
|
||||||
@@ -48,13 +52,15 @@ describe("SAS verification", function() {
|
|||||||
//channel, baseApis, userId, deviceId, startEvent, request
|
//channel, baseApis, userId, deviceId, startEvent, request
|
||||||
const request = {
|
const request = {
|
||||||
onVerifierCancelled: function() {},
|
onVerifierCancelled: function() {},
|
||||||
};
|
} as VerificationRequest;
|
||||||
const channel = {
|
const channel = {
|
||||||
send: function() {
|
send: function() {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
};
|
} as unknown as IVerificationChannel;
|
||||||
const sas = new SAS(channel, {}, "@alice:example.com", "ABCDEFG", null, request);
|
const mockClient = {} as unknown as MatrixClient;
|
||||||
|
const event = new MatrixEvent({ type: 'test' });
|
||||||
|
const sas = new SAS(channel, mockClient, "@alice:example.com", "ABCDEFG", event, request);
|
||||||
sas.handleEvent(new MatrixEvent({
|
sas.handleEvent(new MatrixEvent({
|
||||||
sender: "@alice:example.com",
|
sender: "@alice:example.com",
|
||||||
type: "es.inquisition",
|
type: "es.inquisition",
|
||||||
@@ -65,7 +71,7 @@ describe("SAS verification", function() {
|
|||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
|
|
||||||
// Cancel the SAS for cleanup (we started a verification, so abort)
|
// Cancel the SAS for cleanup (we started a verification, so abort)
|
||||||
sas.cancel();
|
sas.cancel(new Error('error'));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("verification", () => {
|
describe("verification", () => {
|
||||||
@@ -403,16 +409,12 @@ describe("SAS verification", function() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
alice.client.setDeviceVerified = jest.fn();
|
alice.client.setDeviceVerified = jest.fn();
|
||||||
alice.client.downloadKeys = () => {
|
alice.client.downloadKeys = jest.fn().mockResolvedValue({});
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
bob.client.setDeviceVerified = jest.fn();
|
bob.client.setDeviceVerified = jest.fn();
|
||||||
bob.client.downloadKeys = () => {
|
bob.client.downloadKeys = jest.fn().mockResolvedValue({});
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
const bobPromise = new Promise((resolve, reject) => {
|
const bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
|
||||||
bob.client.on("crypto.verification.request", request => {
|
bob.client.on(CryptoEvent.VerificationRequest, request => {
|
||||||
request.verifier.on("show_sas", (e) => {
|
request.verifier.on("show_sas", (e) => {
|
||||||
e.mismatch();
|
e.mismatch();
|
||||||
});
|
});
|
||||||
@@ -421,7 +423,7 @@ describe("SAS verification", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const aliceVerifier = alice.client.beginKeyVerification(
|
const aliceVerifier = alice.client.beginKeyVerification(
|
||||||
verificationMethods.SAS, bob.client.getUserId(), bob.client.deviceId,
|
verificationMethods.SAS, bob.client.getUserId()!, bob.client.deviceId!,
|
||||||
);
|
);
|
||||||
|
|
||||||
const aliceSpy = jest.fn();
|
const aliceSpy = jest.fn();
|
||||||
@@ -462,7 +464,7 @@ describe("SAS verification", function() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
alice.client.setDeviceVerified = jest.fn();
|
alice.client.crypto.setDeviceVerification = jest.fn();
|
||||||
alice.client.getDeviceEd25519Key = () => {
|
alice.client.getDeviceEd25519Key = () => {
|
||||||
return "alice+base64+ed25519+key";
|
return "alice+base64+ed25519+key";
|
||||||
};
|
};
|
||||||
@@ -480,7 +482,7 @@ describe("SAS verification", function() {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
bob.client.setDeviceVerified = jest.fn();
|
bob.client.crypto.setDeviceVerification = jest.fn();
|
||||||
bob.client.getStoredDevice = () => {
|
bob.client.getStoredDevice = () => {
|
||||||
return DeviceInfo.fromStorage(
|
return DeviceInfo.fromStorage(
|
||||||
{
|
{
|
||||||
@@ -501,7 +503,7 @@ describe("SAS verification", function() {
|
|||||||
aliceSasEvent = null;
|
aliceSasEvent = null;
|
||||||
bobSasEvent = null;
|
bobSasEvent = null;
|
||||||
|
|
||||||
bobPromise = new Promise((resolve, reject) => {
|
bobPromise = new Promise<void>((resolve, reject) => {
|
||||||
bob.client.on("crypto.verification.request", async (request) => {
|
bob.client.on("crypto.verification.request", async (request) => {
|
||||||
const verifier = request.beginKeyVerification(SAS.NAME);
|
const verifier = request.beginKeyVerification(SAS.NAME);
|
||||||
verifier.on("show_sas", (e) => {
|
verifier.on("show_sas", (e) => {
|
||||||
@@ -563,10 +565,24 @@ describe("SAS verification", function() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// make sure Alice and Bob verified each other
|
// make sure Alice and Bob verified each other
|
||||||
expect(alice.client.setDeviceVerified)
|
expect(alice.client.crypto.setDeviceVerification)
|
||||||
.toHaveBeenCalledWith(bob.client.getUserId(), bob.client.deviceId);
|
.toHaveBeenCalledWith(
|
||||||
expect(bob.client.setDeviceVerified)
|
bob.client.getUserId(),
|
||||||
.toHaveBeenCalledWith(alice.client.getUserId(), alice.client.deviceId);
|
bob.client.deviceId,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{ "ed25519:Dynabook": "bob+base64+ed25519+key" },
|
||||||
|
);
|
||||||
|
expect(bob.client.crypto.setDeviceVerification)
|
||||||
|
.toHaveBeenCalledWith(
|
||||||
|
alice.client.getUserId(),
|
||||||
|
alice.client.deviceId,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{ "ed25519:Osborne2": "alice+base64+ed25519+key" },
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -18,6 +18,9 @@ import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
|
|||||||
import { encodeBase64 } from "../../../../src/crypto/olmlib";
|
import { encodeBase64 } from "../../../../src/crypto/olmlib";
|
||||||
import { setupWebcrypto, teardownWebcrypto } from './util';
|
import { setupWebcrypto, teardownWebcrypto } from './util';
|
||||||
import { VerificationBase } from '../../../../src/crypto/verification/Base';
|
import { VerificationBase } from '../../../../src/crypto/verification/Base';
|
||||||
|
import { MatrixClient, MatrixEvent } from '../../../../src';
|
||||||
|
import { VerificationRequest } from '../../../../src/crypto/verification/request/VerificationRequest';
|
||||||
|
import { IVerificationChannel } from '../../../../src/crypto/verification/request/Channel';
|
||||||
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
@@ -54,9 +57,21 @@ describe("self-verifications", () => {
|
|||||||
cacheCallbacks,
|
cacheCallbacks,
|
||||||
);
|
);
|
||||||
crossSigningInfo.keys = {
|
crossSigningInfo.keys = {
|
||||||
master: { keys: { X: testKeyPub } },
|
master: {
|
||||||
self_signing: { keys: { X: testKeyPub } },
|
keys: { X: testKeyPub },
|
||||||
user_signing: { keys: { X: testKeyPub } },
|
usage: [],
|
||||||
|
user_id: 'user-id',
|
||||||
|
},
|
||||||
|
self_signing: {
|
||||||
|
keys: { X: testKeyPub },
|
||||||
|
usage: [],
|
||||||
|
user_id: 'user-id',
|
||||||
|
},
|
||||||
|
user_signing: {
|
||||||
|
keys: { X: testKeyPub },
|
||||||
|
usage: [],
|
||||||
|
user_id: 'user-id',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const secretStorage = {
|
const secretStorage = {
|
||||||
@@ -79,20 +94,22 @@ describe("self-verifications", () => {
|
|||||||
getUserId: () => userId,
|
getUserId: () => userId,
|
||||||
getKeyBackupVersion: () => Promise.resolve({}),
|
getKeyBackupVersion: () => Promise.resolve({}),
|
||||||
restoreKeyBackupWithCache,
|
restoreKeyBackupWithCache,
|
||||||
};
|
} as unknown as MatrixClient;
|
||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
onVerifierFinished: () => undefined,
|
onVerifierFinished: () => undefined,
|
||||||
};
|
} as unknown as VerificationRequest;
|
||||||
|
|
||||||
const verification = new VerificationBase(
|
const verification = new VerificationBase(
|
||||||
undefined, // channel
|
undefined as unknown as IVerificationChannel, // channel
|
||||||
client, // baseApis
|
client, // baseApis
|
||||||
userId,
|
userId,
|
||||||
"ABC", // deviceId
|
"ABC", // deviceId
|
||||||
undefined, // startEvent
|
undefined as unknown as MatrixEvent, // startEvent
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// @ts-ignore set private property
|
||||||
verification.resolve = () => undefined;
|
verification.resolve = () => undefined;
|
||||||
|
|
||||||
const result = await verification.done();
|
const result = await verification.done();
|
||||||
@@ -19,20 +19,23 @@ import nodeCrypto from "crypto";
|
|||||||
|
|
||||||
import { TestClient } from '../../../TestClient';
|
import { TestClient } from '../../../TestClient';
|
||||||
import { MatrixEvent } from "../../../../src/models/event";
|
import { 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 { logger } from '../../../../src/logger';
|
||||||
|
import { MatrixClient, ClientEvent } from '../../../../src/client';
|
||||||
|
|
||||||
export async function makeTestClients(userInfos, options) {
|
export async function makeTestClients(userInfos, options): Promise<[TestClient[], () => void]> {
|
||||||
const clients = [];
|
const clients: TestClient[] = [];
|
||||||
const timeouts = [];
|
const timeouts: ReturnType<typeof setTimeout>[] = [];
|
||||||
const clientMap = {};
|
const clientMap: Record<string, Record<string, MatrixClient>> = {};
|
||||||
const sendToDevice = function(type, map) {
|
const makeSendToDevice = (matrixClient: MatrixClient): MatrixClient['sendToDevice'] => async (type, map) => {
|
||||||
// logger.log(this.getUserId(), "sends", type, map);
|
// logger.log(this.getUserId(), "sends", type, map);
|
||||||
for (const [userId, devMap] of Object.entries(map)) {
|
for (const [userId, devMap] of Object.entries(map)) {
|
||||||
if (userId in clientMap) {
|
if (userId in clientMap) {
|
||||||
for (const [deviceId, msg] of Object.entries(devMap)) {
|
for (const [deviceId, msg] of Object.entries(devMap)) {
|
||||||
if (deviceId in clientMap[userId]) {
|
if (deviceId in clientMap[userId]) {
|
||||||
const event = new MatrixEvent({
|
const event = new MatrixEvent({
|
||||||
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
|
sender: matrixClient.getUserId()!,
|
||||||
type: type,
|
type: type,
|
||||||
content: msg,
|
content: msg,
|
||||||
});
|
});
|
||||||
@@ -42,18 +45,19 @@ export async function makeTestClients(userInfos, options) {
|
|||||||
Promise.resolve();
|
Promise.resolve();
|
||||||
|
|
||||||
decryptionPromise.then(
|
decryptionPromise.then(
|
||||||
() => client.emit("toDeviceEvent", event),
|
() => client.emit(ClientEvent.ToDeviceEvent, event),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return {};
|
||||||
};
|
};
|
||||||
const sendEvent = function(room, type, content) {
|
const makeSendEvent = (matrixClient: MatrixClient) => (room, type, content) => {
|
||||||
// make up a unique ID as the event ID
|
// make up a unique ID as the event ID
|
||||||
const eventId = "$" + this.makeTxnId(); // eslint-disable-line @babel/no-invalid-this
|
const eventId = "$" + matrixClient.makeTxnId();
|
||||||
const rawEvent = {
|
const rawEvent = {
|
||||||
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
|
sender: matrixClient.getUserId()!,
|
||||||
type: type,
|
type: type,
|
||||||
content: content,
|
content: content,
|
||||||
room_id: room,
|
room_id: room,
|
||||||
@@ -63,22 +67,24 @@ export async function makeTestClients(userInfos, options) {
|
|||||||
const event = new MatrixEvent(rawEvent);
|
const event = new MatrixEvent(rawEvent);
|
||||||
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
|
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
|
||||||
unsigned: {
|
unsigned: {
|
||||||
transaction_id: this.makeTxnId(), // eslint-disable-line @babel/no-invalid-this
|
transaction_id: matrixClient.makeTxnId(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
for (const tc of clients) {
|
for (const tc of clients) {
|
||||||
if (tc.client === this) { // eslint-disable-line @babel/no-invalid-this
|
const room = new Room('test', tc.client, tc.client.getUserId()!);
|
||||||
|
const roomTimelineData = {} as unknown as IRoomTimelineData;
|
||||||
|
if (tc.client === matrixClient) {
|
||||||
logger.log("sending remote echo!!");
|
logger.log("sending remote echo!!");
|
||||||
tc.client.emit("Room.timeline", remoteEcho);
|
tc.client.emit(RoomEvent.Timeline, remoteEcho, room, false, false, roomTimelineData);
|
||||||
} else {
|
} else {
|
||||||
tc.client.emit("Room.timeline", event);
|
tc.client.emit(RoomEvent.Timeline, event, room, false, false, roomTimelineData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
timeouts.push(timeout);
|
timeouts.push(timeout as unknown as ReturnType<typeof setTimeout>);
|
||||||
|
|
||||||
return Promise.resolve({ event_id: eventId });
|
return Promise.resolve({ event_id: eventId });
|
||||||
};
|
};
|
||||||
@@ -99,8 +105,8 @@ export async function makeTestClients(userInfos, options) {
|
|||||||
clientMap[userInfo.userId] = {};
|
clientMap[userInfo.userId] = {};
|
||||||
}
|
}
|
||||||
clientMap[userInfo.userId][userInfo.deviceId] = testClient.client;
|
clientMap[userInfo.userId][userInfo.deviceId] = testClient.client;
|
||||||
testClient.client.sendToDevice = sendToDevice;
|
testClient.client.sendToDevice = makeSendToDevice(testClient.client);
|
||||||
testClient.client.sendEvent = sendEvent;
|
testClient.client.sendEvent = makeSendEvent(testClient.client);
|
||||||
clients.push(testClient);
|
clients.push(testClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,11 +122,12 @@ export async function makeTestClients(userInfos, options) {
|
|||||||
export function setupWebcrypto() {
|
export function setupWebcrypto() {
|
||||||
global.crypto = {
|
global.crypto = {
|
||||||
getRandomValues: (buf) => {
|
getRandomValues: (buf) => {
|
||||||
return nodeCrypto.randomFillSync(buf);
|
return nodeCrypto.randomFillSync(buf as any);
|
||||||
},
|
},
|
||||||
};
|
} as unknown as Crypto;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function teardownWebcrypto() {
|
export function teardownWebcrypto() {
|
||||||
|
// @ts-ignore undefined != Crypto
|
||||||
global.crypto = undefined;
|
global.crypto = undefined;
|
||||||
}
|
}
|
||||||
@@ -19,11 +19,18 @@ import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoo
|
|||||||
import { ToDeviceChannel } from
|
import { ToDeviceChannel } from
|
||||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||||
import { MatrixEvent } from "../../../../src/models/event";
|
import { MatrixEvent } from "../../../../src/models/event";
|
||||||
|
import { MatrixClient } from "../../../../src/client";
|
||||||
import { setupWebcrypto, teardownWebcrypto } from "./util";
|
import { setupWebcrypto, teardownWebcrypto } from "./util";
|
||||||
|
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
|
||||||
|
import { VerificationBase } from "../../../../src/crypto/verification/Base";
|
||||||
|
|
||||||
function makeMockClient(userId, deviceId) {
|
type MockClient = MatrixClient & {
|
||||||
|
popEvents: () => MatrixEvent[];
|
||||||
|
popDeviceEvents: (userId: string, deviceId: string) => MatrixEvent[];
|
||||||
|
};
|
||||||
|
function makeMockClient(userId: string, deviceId: string): MockClient {
|
||||||
let counter = 1;
|
let counter = 1;
|
||||||
let events = [];
|
let events: MatrixEvent[] = [];
|
||||||
const deviceEvents = {};
|
const deviceEvents = {};
|
||||||
return {
|
return {
|
||||||
getUserId() { return userId; },
|
getUserId() { return userId; },
|
||||||
@@ -54,16 +61,18 @@ function makeMockClient(userId, deviceId) {
|
|||||||
deviceEvents[userId][deviceId].push(event);
|
deviceEvents[userId][deviceId].push(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve({});
|
||||||
},
|
},
|
||||||
|
|
||||||
popEvents() {
|
// @ts-ignore special testing fn
|
||||||
|
popEvents(): MatrixEvent[] {
|
||||||
const e = events;
|
const e = events;
|
||||||
events = [];
|
events = [];
|
||||||
return e;
|
return e;
|
||||||
},
|
},
|
||||||
|
|
||||||
popDeviceEvents(userId, deviceId) {
|
// @ts-ignore special testing fn
|
||||||
|
popDeviceEvents(userId: string, deviceId: string): MatrixEvent[] {
|
||||||
const forDevice = deviceEvents[userId];
|
const forDevice = deviceEvents[userId];
|
||||||
const events = forDevice && forDevice[deviceId];
|
const events = forDevice && forDevice[deviceId];
|
||||||
const result = events || [];
|
const result = events || [];
|
||||||
@@ -72,12 +81,21 @@ function makeMockClient(userId, deviceId) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
};
|
} as unknown as MockClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MOCK_METHOD = "mock-verify";
|
const MOCK_METHOD = "mock-verify";
|
||||||
class MockVerifier {
|
class MockVerifier extends VerificationBase<'', any> {
|
||||||
constructor(channel, client, userId, deviceId, startEvent) {
|
public _channel;
|
||||||
|
public _startEvent;
|
||||||
|
constructor(
|
||||||
|
channel: IVerificationChannel,
|
||||||
|
client: MatrixClient,
|
||||||
|
userId: string,
|
||||||
|
deviceId: string,
|
||||||
|
startEvent: MatrixEvent,
|
||||||
|
) {
|
||||||
|
super(channel, client, userId, deviceId, startEvent, {} as unknown as VerificationRequest);
|
||||||
this._channel = channel;
|
this._channel = channel;
|
||||||
this._startEvent = startEvent;
|
this._startEvent = startEvent;
|
||||||
}
|
}
|
||||||
@@ -115,7 +133,10 @@ function makeRemoteEcho(event) {
|
|||||||
|
|
||||||
async function distributeEvent(ownRequest, theirRequest, event) {
|
async function distributeEvent(ownRequest, theirRequest, event) {
|
||||||
await ownRequest.channel.handleEvent(
|
await ownRequest.channel.handleEvent(
|
||||||
makeRemoteEcho(event), ownRequest, true);
|
makeRemoteEcho(event),
|
||||||
|
ownRequest,
|
||||||
|
true,
|
||||||
|
);
|
||||||
await theirRequest.channel.handleEvent(event, theirRequest, true);
|
await theirRequest.channel.handleEvent(event, theirRequest, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,12 +154,19 @@ describe("verification request unit tests", function() {
|
|||||||
it("transition from UNSENT to DONE through happy path", async function() {
|
it("transition from UNSENT to DONE through happy path", async function() {
|
||||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||||
|
const verificationMethods = new Map(
|
||||||
|
[[MOCK_METHOD, MockVerifier]],
|
||||||
|
) as unknown as Map<string, typeof VerificationBase>;
|
||||||
const aliceRequest = new VerificationRequest(
|
const aliceRequest = new VerificationRequest(
|
||||||
new InRoomChannel(alice, "!room", bob.getUserId()),
|
new InRoomChannel(alice, "!room", bob.getUserId()!),
|
||||||
new Map([[MOCK_METHOD, MockVerifier]]), alice);
|
verificationMethods,
|
||||||
|
alice,
|
||||||
|
);
|
||||||
const bobRequest = new VerificationRequest(
|
const bobRequest = new VerificationRequest(
|
||||||
new InRoomChannel(bob, "!room"),
|
new InRoomChannel(bob, "!room"),
|
||||||
new Map([[MOCK_METHOD, MockVerifier]]), bob);
|
verificationMethods,
|
||||||
|
bob,
|
||||||
|
);
|
||||||
expect(aliceRequest.invalid).toBe(true);
|
expect(aliceRequest.invalid).toBe(true);
|
||||||
expect(bobRequest.invalid).toBe(true);
|
expect(bobRequest.invalid).toBe(true);
|
||||||
|
|
||||||
@@ -157,7 +185,7 @@ describe("verification request unit tests", function() {
|
|||||||
expect(aliceRequest.ready).toBe(true);
|
expect(aliceRequest.ready).toBe(true);
|
||||||
|
|
||||||
const verifier = aliceRequest.beginKeyVerification(MOCK_METHOD);
|
const verifier = aliceRequest.beginKeyVerification(MOCK_METHOD);
|
||||||
await verifier.start();
|
await (verifier as MockVerifier).start();
|
||||||
const [startEvent] = alice.popEvents();
|
const [startEvent] = alice.popEvents();
|
||||||
expect(startEvent.getType()).toBe(START_TYPE);
|
expect(startEvent.getType()).toBe(START_TYPE);
|
||||||
await distributeEvent(aliceRequest, bobRequest, startEvent);
|
await distributeEvent(aliceRequest, bobRequest, startEvent);
|
||||||
@@ -165,8 +193,7 @@ describe("verification request unit tests", function() {
|
|||||||
expect(aliceRequest.verifier).toBeInstanceOf(MockVerifier);
|
expect(aliceRequest.verifier).toBeInstanceOf(MockVerifier);
|
||||||
expect(bobRequest.started).toBe(true);
|
expect(bobRequest.started).toBe(true);
|
||||||
expect(bobRequest.verifier).toBeInstanceOf(MockVerifier);
|
expect(bobRequest.verifier).toBeInstanceOf(MockVerifier);
|
||||||
|
await (bobRequest.verifier as MockVerifier).start();
|
||||||
await bobRequest.verifier.start();
|
|
||||||
const [bobDoneEvent] = bob.popEvents();
|
const [bobDoneEvent] = bob.popEvents();
|
||||||
expect(bobDoneEvent.getType()).toBe(DONE_TYPE);
|
expect(bobDoneEvent.getType()).toBe(DONE_TYPE);
|
||||||
await distributeEvent(bobRequest, aliceRequest, bobDoneEvent);
|
await distributeEvent(bobRequest, aliceRequest, bobDoneEvent);
|
||||||
@@ -180,12 +207,20 @@ describe("verification request unit tests", function() {
|
|||||||
it("methods only contains common methods", async function() {
|
it("methods only contains common methods", async function() {
|
||||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||||
|
const aliceVerificationMethods = new Map(
|
||||||
|
[["c", function() {}], ["a", function() {}]],
|
||||||
|
) as unknown as Map<string, typeof VerificationBase>;
|
||||||
|
const bobVerificationMethods = new Map(
|
||||||
|
[["c", function() {}], ["b", function() {}]],
|
||||||
|
) as unknown as Map<string, typeof VerificationBase>;
|
||||||
const aliceRequest = new VerificationRequest(
|
const aliceRequest = new VerificationRequest(
|
||||||
new InRoomChannel(alice, "!room", bob.getUserId()),
|
new InRoomChannel(alice, "!room", bob.getUserId()!),
|
||||||
new Map([["c", function() {}], ["a", function() {}]]), alice);
|
aliceVerificationMethods, alice);
|
||||||
const bobRequest = new VerificationRequest(
|
const bobRequest = new VerificationRequest(
|
||||||
new InRoomChannel(bob, "!room"),
|
new InRoomChannel(bob, "!room"),
|
||||||
new Map([["c", function() {}], ["b", function() {}]]), bob);
|
bobVerificationMethods,
|
||||||
|
bob,
|
||||||
|
);
|
||||||
await aliceRequest.sendRequest();
|
await aliceRequest.sendRequest();
|
||||||
const [requestEvent] = alice.popEvents();
|
const [requestEvent] = alice.popEvents();
|
||||||
await distributeEvent(aliceRequest, bobRequest, requestEvent);
|
await distributeEvent(aliceRequest, bobRequest, requestEvent);
|
||||||
@@ -201,13 +236,22 @@ describe("verification request unit tests", function() {
|
|||||||
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
|
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
|
||||||
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
|
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
|
||||||
const aliceRequest = new VerificationRequest(
|
const aliceRequest = new VerificationRequest(
|
||||||
new InRoomChannel(alice, "!room", bob1.getUserId()), new Map(), alice);
|
new InRoomChannel(alice, "!room", bob1.getUserId()!),
|
||||||
|
new Map(),
|
||||||
|
alice,
|
||||||
|
);
|
||||||
await aliceRequest.sendRequest();
|
await aliceRequest.sendRequest();
|
||||||
const [requestEvent] = alice.popEvents();
|
const [requestEvent] = alice.popEvents();
|
||||||
const bob1Request = new VerificationRequest(
|
const bob1Request = new VerificationRequest(
|
||||||
new InRoomChannel(bob1, "!room"), new Map(), bob1);
|
new InRoomChannel(bob1, "!room"),
|
||||||
|
new Map(),
|
||||||
|
bob1,
|
||||||
|
);
|
||||||
const bob2Request = new VerificationRequest(
|
const bob2Request = new VerificationRequest(
|
||||||
new InRoomChannel(bob2, "!room"), new Map(), bob2);
|
new InRoomChannel(bob2, "!room"),
|
||||||
|
new Map(),
|
||||||
|
bob2,
|
||||||
|
);
|
||||||
|
|
||||||
await bob1Request.channel.handleEvent(requestEvent, bob1Request, true);
|
await bob1Request.channel.handleEvent(requestEvent, bob1Request, true);
|
||||||
await bob2Request.channel.handleEvent(requestEvent, bob2Request, true);
|
await bob2Request.channel.handleEvent(requestEvent, bob2Request, true);
|
||||||
@@ -222,22 +266,34 @@ describe("verification request unit tests", function() {
|
|||||||
it("verify own device with to_device messages", async function() {
|
it("verify own device with to_device messages", async function() {
|
||||||
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
|
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
|
||||||
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
|
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
|
||||||
|
const verificationMethods = new Map(
|
||||||
|
[[MOCK_METHOD, MockVerifier]],
|
||||||
|
) as unknown as Map<string, typeof VerificationBase>;
|
||||||
const bob1Request = new VerificationRequest(
|
const bob1Request = new VerificationRequest(
|
||||||
new ToDeviceChannel(bob1, bob1.getUserId(), ["device1", "device2"],
|
new ToDeviceChannel(
|
||||||
ToDeviceChannel.makeTransactionId(), "device2"),
|
bob1,
|
||||||
new Map([[MOCK_METHOD, MockVerifier]]), bob1);
|
bob1.getUserId()!,
|
||||||
|
["device1", "device2"],
|
||||||
|
ToDeviceChannel.makeTransactionId(),
|
||||||
|
"device2",
|
||||||
|
),
|
||||||
|
verificationMethods,
|
||||||
|
bob1,
|
||||||
|
);
|
||||||
const to = { userId: "@bob:matrix.tld", deviceId: "device2" };
|
const to = { userId: "@bob:matrix.tld", deviceId: "device2" };
|
||||||
const verifier = bob1Request.beginKeyVerification(MOCK_METHOD, to);
|
const verifier = bob1Request.beginKeyVerification(MOCK_METHOD, to);
|
||||||
expect(verifier).toBeInstanceOf(MockVerifier);
|
expect(verifier).toBeInstanceOf(MockVerifier);
|
||||||
await verifier.start();
|
await (verifier as MockVerifier).start();
|
||||||
const [startEvent] = bob1.popDeviceEvents(to.userId, to.deviceId);
|
const [startEvent] = bob1.popDeviceEvents(to.userId, to.deviceId);
|
||||||
expect(startEvent.getType()).toBe(START_TYPE);
|
expect(startEvent.getType()).toBe(START_TYPE);
|
||||||
const bob2Request = new VerificationRequest(
|
const bob2Request = new VerificationRequest(
|
||||||
new ToDeviceChannel(bob2, bob2.getUserId(), ["device1"]),
|
new ToDeviceChannel(bob2, bob2.getUserId()!, ["device1"]),
|
||||||
new Map([[MOCK_METHOD, MockVerifier]]), bob2);
|
verificationMethods,
|
||||||
|
bob2,
|
||||||
|
);
|
||||||
|
|
||||||
await bob2Request.channel.handleEvent(startEvent, bob2Request, true);
|
await bob2Request.channel.handleEvent(startEvent, bob2Request, true);
|
||||||
await bob2Request.verifier.start();
|
await (bob2Request.verifier as MockVerifier).start();
|
||||||
const [doneEvent1] = bob2.popDeviceEvents("@bob:matrix.tld", "device1");
|
const [doneEvent1] = bob2.popDeviceEvents("@bob:matrix.tld", "device1");
|
||||||
expect(doneEvent1.getType()).toBe(DONE_TYPE);
|
expect(doneEvent1.getType()).toBe(DONE_TYPE);
|
||||||
await bob1Request.channel.handleEvent(doneEvent1, bob1Request, true);
|
await bob1Request.channel.handleEvent(doneEvent1, bob1Request, true);
|
||||||
@@ -253,11 +309,13 @@ describe("verification request unit tests", function() {
|
|||||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||||
const aliceRequest = new VerificationRequest(
|
const aliceRequest = new VerificationRequest(
|
||||||
new InRoomChannel(alice, "!room", bob.getUserId()), new Map(), alice);
|
new InRoomChannel(alice, "!room", bob.getUserId()!),
|
||||||
|
new Map(),
|
||||||
|
alice,
|
||||||
|
);
|
||||||
await aliceRequest.sendRequest();
|
await aliceRequest.sendRequest();
|
||||||
const [requestEvent] = alice.popEvents();
|
const [requestEvent] = alice.popEvents();
|
||||||
await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true,
|
await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true);
|
||||||
true, true);
|
|
||||||
|
|
||||||
expect(aliceRequest.cancelled).toBe(false);
|
expect(aliceRequest.cancelled).toBe(false);
|
||||||
expect(aliceRequest._cancellingUserId).toBe(undefined);
|
expect(aliceRequest._cancellingUserId).toBe(undefined);
|
||||||
@@ -269,11 +327,17 @@ describe("verification request unit tests", function() {
|
|||||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||||
const aliceRequest = new VerificationRequest(
|
const aliceRequest = new VerificationRequest(
|
||||||
new InRoomChannel(alice, "!room", bob.getUserId()), new Map(), alice);
|
new InRoomChannel(alice, "!room", bob.getUserId()!),
|
||||||
|
new Map(),
|
||||||
|
alice,
|
||||||
|
);
|
||||||
await aliceRequest.sendRequest();
|
await aliceRequest.sendRequest();
|
||||||
const [requestEvent] = alice.popEvents();
|
const [requestEvent] = alice.popEvents();
|
||||||
const bobRequest = new VerificationRequest(
|
const bobRequest = new VerificationRequest(
|
||||||
new InRoomChannel(bob, "!room"), new Map(), bob);
|
new InRoomChannel(bob, "!room"),
|
||||||
|
new Map(),
|
||||||
|
bob,
|
||||||
|
);
|
||||||
|
|
||||||
await bobRequest.channel.handleEvent(requestEvent, bobRequest, true);
|
await bobRequest.channel.handleEvent(requestEvent, bobRequest, true);
|
||||||
|
|
||||||
@@ -16,14 +16,15 @@ limitations under the License.
|
|||||||
|
|
||||||
import * as utils from "../test-utils/test-utils";
|
import * as utils from "../test-utils/test-utils";
|
||||||
import {
|
import {
|
||||||
|
DuplicateStrategy,
|
||||||
EventTimeline,
|
EventTimeline,
|
||||||
EventTimelineSet,
|
EventTimelineSet,
|
||||||
EventType,
|
EventType,
|
||||||
|
Filter,
|
||||||
MatrixClient,
|
MatrixClient,
|
||||||
MatrixEvent,
|
MatrixEvent,
|
||||||
MatrixEventEvent,
|
MatrixEventEvent,
|
||||||
Room,
|
Room,
|
||||||
DuplicateStrategy,
|
|
||||||
} from '../../src';
|
} from '../../src';
|
||||||
import { Thread } from "../../src/models/thread";
|
import { Thread } from "../../src/models/thread";
|
||||||
import { ReEmitter } from "../../src/ReEmitter";
|
import { ReEmitter } from "../../src/ReEmitter";
|
||||||
@@ -291,4 +292,34 @@ describe('EventTimelineSet', () => {
|
|||||||
expect(eventTimelineSet.canContain(event)).toBeTruthy();
|
expect(eventTimelineSet.canContain(event)).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("handleRemoteEcho", () => {
|
||||||
|
it("should add to liveTimeline only if the event matches the filter", () => {
|
||||||
|
const filter = new Filter(client.getUserId()!, "test_filter");
|
||||||
|
filter.setDefinition({
|
||||||
|
room: {
|
||||||
|
timeline: {
|
||||||
|
types: [EventType.RoomMessage],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const eventTimelineSet = new EventTimelineSet(room, { filter }, client);
|
||||||
|
|
||||||
|
const roomMessageEvent = new MatrixEvent({
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
content: { body: "test" },
|
||||||
|
event_id: "!test1:server",
|
||||||
|
});
|
||||||
|
eventTimelineSet.handleRemoteEcho(roomMessageEvent, "~!local-event-id:server", roomMessageEvent.getId());
|
||||||
|
expect(eventTimelineSet.getLiveTimeline().getEvents()).toContain(roomMessageEvent);
|
||||||
|
|
||||||
|
const roomFilteredEvent = new MatrixEvent({
|
||||||
|
type: "other_event_type",
|
||||||
|
content: { body: "test" },
|
||||||
|
event_id: "!test2:server",
|
||||||
|
});
|
||||||
|
eventTimelineSet.handleRemoteEcho(roomFilteredEvent, "~!local-event-id:server", roomFilteredEvent.getId());
|
||||||
|
expect(eventTimelineSet.getLiveTimeline().getEvents()).not.toContain(roomFilteredEvent);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
62
spec/unit/feature.spec.ts
Normal file
62
spec/unit/feature.spec.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 { buildFeatureSupportMap, Feature, ServerSupport } from "../../src/feature";
|
||||||
|
|
||||||
|
describe("Feature detection", () => {
|
||||||
|
it("checks the matrix version", async () => {
|
||||||
|
const support = await buildFeatureSupportMap({
|
||||||
|
versions: ["v1.3"],
|
||||||
|
unstable_features: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(support.get(Feature.Thread)).toBe(ServerSupport.Stable);
|
||||||
|
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unsupported);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("checks the matrix msc number", async () => {
|
||||||
|
const support = await buildFeatureSupportMap({
|
||||||
|
versions: ["v1.2"],
|
||||||
|
unstable_features: {
|
||||||
|
"org.matrix.msc3771": true,
|
||||||
|
"org.matrix.msc3773": true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unstable);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires two MSCs to pass", async () => {
|
||||||
|
const support = await buildFeatureSupportMap({
|
||||||
|
versions: ["v1.2"],
|
||||||
|
unstable_features: {
|
||||||
|
"org.matrix.msc3771": false,
|
||||||
|
"org.matrix.msc3773": true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unsupported);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires two MSCs OR matrix versions to pass", async () => {
|
||||||
|
const support = await buildFeatureSupportMap({
|
||||||
|
versions: ["v1.4"],
|
||||||
|
unstable_features: {
|
||||||
|
"org.matrix.msc3771": false,
|
||||||
|
"org.matrix.msc3773": true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Stable);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
|
||||||
import { Filter, IFilterDefinition } from "../../src/filter";
|
import { Filter, IFilterDefinition } from "../../src/filter";
|
||||||
|
|
||||||
describe("Filter", function() {
|
describe("Filter", function() {
|
||||||
@@ -43,4 +44,17 @@ describe("Filter", function() {
|
|||||||
expect(filter.getDefinition()).toEqual(definition);
|
expect(filter.getDefinition()).toEqual(definition);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("setUnreadThreadNotifications", function() {
|
||||||
|
it("setUnreadThreadNotifications", function() {
|
||||||
|
filter.setUnreadThreadNotifications(true);
|
||||||
|
expect(filter.getDefinition()).toEqual({
|
||||||
|
room: {
|
||||||
|
timeline: {
|
||||||
|
[UNREAD_THREAD_NOTIFICATIONS.name]: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
43
spec/unit/local_notifications.spec.ts
Normal file
43
spec/unit/local_notifications.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 { LocalNotificationSettings } from "../../src/@types/local_notifications";
|
||||||
|
import { LOCAL_NOTIFICATION_SETTINGS_PREFIX, MatrixClient } from "../../src/matrix";
|
||||||
|
import { TestClient } from '../TestClient';
|
||||||
|
|
||||||
|
let client: MatrixClient;
|
||||||
|
|
||||||
|
describe("Local notification settings", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
client = (new TestClient(
|
||||||
|
"@alice:matrix.org", "123", undefined, undefined, undefined,
|
||||||
|
)).client;
|
||||||
|
client.setAccountData = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Lets you set local notification settings", () => {
|
||||||
|
it("stores settings in account data", () => {
|
||||||
|
const deviceId = "device";
|
||||||
|
const settings: LocalNotificationSettings = { is_silenced: true };
|
||||||
|
client.setLocalNotificationSettings(deviceId, settings);
|
||||||
|
|
||||||
|
expect(client.setAccountData).toHaveBeenCalledWith(
|
||||||
|
`${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`,
|
||||||
|
settings,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { SSOAction } from '../../src/@types/auth';
|
||||||
import { TestClient } from '../TestClient';
|
import { TestClient } from '../TestClient';
|
||||||
|
|
||||||
describe('Login request', function() {
|
describe('Login request', function() {
|
||||||
@@ -22,3 +23,37 @@ describe('Login request', function() {
|
|||||||
expect(client.client.getUserId()).toBe(response.user_id);
|
expect(client.client.getUserId()).toBe(response.user_id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('SSO login URL', function() {
|
||||||
|
let client: TestClient;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
client = new TestClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
client.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SSOAction', function() {
|
||||||
|
const redirectUri = "https://test.com/foo";
|
||||||
|
|
||||||
|
it('No action', function() {
|
||||||
|
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, undefined);
|
||||||
|
const url = new URL(urlString);
|
||||||
|
expect(url.searchParams.has('org.matrix.msc3824.action')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('register', function() {
|
||||||
|
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.REGISTER);
|
||||||
|
const url = new URL(urlString);
|
||||||
|
expect(url.searchParams.get('org.matrix.msc3824.action')).toEqual('register');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('login', function() {
|
||||||
|
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.LOGIN);
|
||||||
|
const url = new URL(urlString);
|
||||||
|
expect(url.searchParams.get('org.matrix.msc3824.action')).toEqual('login');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -36,9 +36,14 @@ import { ReceiptType } from "../../src/@types/read_receipts";
|
|||||||
import * as testUtils from "../test-utils/test-utils";
|
import * as testUtils from "../test-utils/test-utils";
|
||||||
import { makeBeaconInfoContent } from "../../src/content-helpers";
|
import { makeBeaconInfoContent } from "../../src/content-helpers";
|
||||||
import { M_BEACON_INFO } from "../../src/@types/beacon";
|
import { M_BEACON_INFO } from "../../src/@types/beacon";
|
||||||
import { ContentHelpers, Room } from "../../src";
|
import { ContentHelpers, EventTimeline, Room } from "../../src";
|
||||||
import { supportsMatrixCall } from "../../src/webrtc/call";
|
import { supportsMatrixCall } from "../../src/webrtc/call";
|
||||||
import { makeBeaconEvent } from "../test-utils/beacon";
|
import { makeBeaconEvent } from "../test-utils/beacon";
|
||||||
|
import {
|
||||||
|
IGNORE_INVITES_ACCOUNT_EVENT_KEY,
|
||||||
|
POLICIES_ACCOUNT_EVENT_TYPE,
|
||||||
|
PolicyScope,
|
||||||
|
} from "../../src/models/invites-ignorer";
|
||||||
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
@@ -427,7 +432,7 @@ describe("MatrixClient", function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await client.startClient();
|
await client.startClient({ filter });
|
||||||
await syncPromise;
|
await syncPromise;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1412,4 +1417,301 @@ describe("MatrixClient", function() {
|
|||||||
expect(client.crypto.encryptAndSendToDevices).toHaveBeenLastCalledWith(deviceInfos, payload);
|
expect(client.crypto.encryptAndSendToDevices).toHaveBeenLastCalledWith(deviceInfos, payload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("support for ignoring invites", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mockup `getAccountData`/`setAccountData`.
|
||||||
|
const dataStore = new Map();
|
||||||
|
client.setAccountData = function(eventType, content) {
|
||||||
|
dataStore.set(eventType, content);
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
client.getAccountData = function(eventType) {
|
||||||
|
const data = dataStore.get(eventType);
|
||||||
|
return new MatrixEvent({
|
||||||
|
content: data,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mockup `createRoom`/`getRoom`/`joinRoom`, including state.
|
||||||
|
const rooms = new Map();
|
||||||
|
client.createRoom = function(options = {}) {
|
||||||
|
const roomId = options["_roomId"] || `!room-${rooms.size}:example.org`;
|
||||||
|
const state = new Map();
|
||||||
|
const room = {
|
||||||
|
roomId,
|
||||||
|
_options: options,
|
||||||
|
_state: state,
|
||||||
|
getUnfilteredTimelineSet: function() {
|
||||||
|
return {
|
||||||
|
getLiveTimeline: function() {
|
||||||
|
return {
|
||||||
|
getState: function(direction) {
|
||||||
|
expect(direction).toBe(EventTimeline.FORWARDS);
|
||||||
|
return {
|
||||||
|
getStateEvents: function(type) {
|
||||||
|
const store = state.get(type) || {};
|
||||||
|
return Object.keys(store).map(key => store[key]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
rooms.set(roomId, room);
|
||||||
|
return Promise.resolve({ room_id: roomId });
|
||||||
|
};
|
||||||
|
client.getRoom = function(roomId) {
|
||||||
|
return rooms.get(roomId);
|
||||||
|
};
|
||||||
|
client.joinRoom = function(roomId) {
|
||||||
|
return this.getRoom(roomId) || this.createRoom({ _roomId: roomId });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mockup state events
|
||||||
|
client.sendStateEvent = function(roomId, type, content) {
|
||||||
|
const room = this.getRoom(roomId);
|
||||||
|
const state: Map<string, any> = room._state;
|
||||||
|
let store = state.get(type);
|
||||||
|
if (!store) {
|
||||||
|
store = {};
|
||||||
|
state.set(type, store);
|
||||||
|
}
|
||||||
|
const eventId = `$event-${Math.random()}:example.org`;
|
||||||
|
store[eventId] = {
|
||||||
|
getId: function() {
|
||||||
|
return eventId;
|
||||||
|
},
|
||||||
|
getRoomId: function() {
|
||||||
|
return roomId;
|
||||||
|
},
|
||||||
|
getContent: function() {
|
||||||
|
return content;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return { event_id: eventId };
|
||||||
|
};
|
||||||
|
client.redactEvent = function(roomId, eventId) {
|
||||||
|
const room = this.getRoom(roomId);
|
||||||
|
const state: Map<string, any> = room._state;
|
||||||
|
for (const store of state.values()) {
|
||||||
|
delete store[eventId];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should initialize and return the same `target` consistently", async () => {
|
||||||
|
const target1 = await client.ignoredInvites.getOrCreateTargetRoom();
|
||||||
|
const target2 = await client.ignoredInvites.getOrCreateTargetRoom();
|
||||||
|
expect(target1).toBeTruthy();
|
||||||
|
expect(target1).toBe(target2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should initialize and return the same `sources` consistently", async () => {
|
||||||
|
const sources1 = await client.ignoredInvites.getOrCreateSourceRooms();
|
||||||
|
const sources2 = await client.ignoredInvites.getOrCreateSourceRooms();
|
||||||
|
expect(sources1).toBeTruthy();
|
||||||
|
expect(sources1).toHaveLength(1);
|
||||||
|
expect(sources1).toEqual(sources2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should initially not reject any invite", async () => {
|
||||||
|
const rule = await client.ignoredInvites.getRuleForInvite({
|
||||||
|
sender: "@foobar:example.org",
|
||||||
|
roomId: "!snafu:somewhere.org",
|
||||||
|
});
|
||||||
|
expect(rule).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject invites once we have added a matching rule in the target room (scope: user)", async () => {
|
||||||
|
await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
|
||||||
|
|
||||||
|
// We should reject this invite.
|
||||||
|
const ruleMatch = await client.ignoredInvites.getRuleForInvite({
|
||||||
|
sender: "@foobar:example.org",
|
||||||
|
roomId: "!snafu:somewhere.org",
|
||||||
|
});
|
||||||
|
expect(ruleMatch).toBeTruthy();
|
||||||
|
expect(ruleMatch.getContent()).toMatchObject({
|
||||||
|
recommendation: "m.ban",
|
||||||
|
reason: "just a test",
|
||||||
|
});
|
||||||
|
|
||||||
|
// We should let these invites go through.
|
||||||
|
const ruleWrongServer = await client.ignoredInvites.getRuleForInvite({
|
||||||
|
sender: "@foobar:somewhere.org",
|
||||||
|
roomId: "!snafu:somewhere.org",
|
||||||
|
});
|
||||||
|
expect(ruleWrongServer).toBeFalsy();
|
||||||
|
|
||||||
|
const ruleWrongServerRoom = await client.ignoredInvites.getRuleForInvite({
|
||||||
|
sender: "@foobar:somewhere.org",
|
||||||
|
roomId: "!snafu:example.org",
|
||||||
|
});
|
||||||
|
expect(ruleWrongServerRoom).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject invites once we have added a matching rule in the target room (scope: server)", async () => {
|
||||||
|
const REASON = `Just a test ${Math.random()}`;
|
||||||
|
await client.ignoredInvites.addRule(PolicyScope.Server, "example.org", REASON);
|
||||||
|
|
||||||
|
// We should reject these invites.
|
||||||
|
const ruleSenderMatch = await client.ignoredInvites.getRuleForInvite({
|
||||||
|
sender: "@foobar:example.org",
|
||||||
|
roomId: "!snafu:somewhere.org",
|
||||||
|
});
|
||||||
|
expect(ruleSenderMatch).toBeTruthy();
|
||||||
|
expect(ruleSenderMatch.getContent()).toMatchObject({
|
||||||
|
recommendation: "m.ban",
|
||||||
|
reason: REASON,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ruleRoomMatch = await client.ignoredInvites.getRuleForInvite({
|
||||||
|
sender: "@foobar:somewhere.org",
|
||||||
|
roomId: "!snafu:example.org",
|
||||||
|
});
|
||||||
|
expect(ruleRoomMatch).toBeTruthy();
|
||||||
|
expect(ruleRoomMatch.getContent()).toMatchObject({
|
||||||
|
recommendation: "m.ban",
|
||||||
|
reason: REASON,
|
||||||
|
});
|
||||||
|
|
||||||
|
// We should let these invites go through.
|
||||||
|
const ruleWrongServer = await client.ignoredInvites.getRuleForInvite({
|
||||||
|
sender: "@foobar:somewhere.org",
|
||||||
|
roomId: "!snafu:somewhere.org",
|
||||||
|
});
|
||||||
|
expect(ruleWrongServer).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject invites once we have added a matching rule in the target room (scope: room)", async () => {
|
||||||
|
const REASON = `Just a test ${Math.random()}`;
|
||||||
|
const BAD_ROOM_ID = "!bad:example.org";
|
||||||
|
const GOOD_ROOM_ID = "!good:example.org";
|
||||||
|
await client.ignoredInvites.addRule(PolicyScope.Room, BAD_ROOM_ID, REASON);
|
||||||
|
|
||||||
|
// We should reject this invite.
|
||||||
|
const ruleSenderMatch = await client.ignoredInvites.getRuleForInvite({
|
||||||
|
sender: "@foobar:example.org",
|
||||||
|
roomId: BAD_ROOM_ID,
|
||||||
|
});
|
||||||
|
expect(ruleSenderMatch).toBeTruthy();
|
||||||
|
expect(ruleSenderMatch.getContent()).toMatchObject({
|
||||||
|
recommendation: "m.ban",
|
||||||
|
reason: REASON,
|
||||||
|
});
|
||||||
|
|
||||||
|
// We should let these invites go through.
|
||||||
|
const ruleWrongRoom = await client.ignoredInvites.getRuleForInvite({
|
||||||
|
sender: BAD_ROOM_ID,
|
||||||
|
roomId: GOOD_ROOM_ID,
|
||||||
|
});
|
||||||
|
expect(ruleWrongRoom).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject invites once we have added a matching rule in a non-target source room", async () => {
|
||||||
|
const NEW_SOURCE_ROOM_ID = "!another-source:example.org";
|
||||||
|
|
||||||
|
// Make sure that everything is initialized.
|
||||||
|
await client.ignoredInvites.getOrCreateSourceRooms();
|
||||||
|
await client.joinRoom(NEW_SOURCE_ROOM_ID);
|
||||||
|
await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
|
||||||
|
|
||||||
|
// Add a rule in the new source room.
|
||||||
|
await client.sendStateEvent(NEW_SOURCE_ROOM_ID, PolicyScope.User, {
|
||||||
|
entity: "*:example.org",
|
||||||
|
reason: "just a test",
|
||||||
|
recommendation: "m.ban",
|
||||||
|
});
|
||||||
|
|
||||||
|
// We should reject this invite.
|
||||||
|
const ruleMatch = await client.ignoredInvites.getRuleForInvite({
|
||||||
|
sender: "@foobar:example.org",
|
||||||
|
roomId: "!snafu:somewhere.org",
|
||||||
|
});
|
||||||
|
expect(ruleMatch).toBeTruthy();
|
||||||
|
expect(ruleMatch.getContent()).toMatchObject({
|
||||||
|
recommendation: "m.ban",
|
||||||
|
reason: "just a test",
|
||||||
|
});
|
||||||
|
|
||||||
|
// We should let these invites go through.
|
||||||
|
const ruleWrongServer = await client.ignoredInvites.getRuleForInvite({
|
||||||
|
sender: "@foobar:somewhere.org",
|
||||||
|
roomId: "!snafu:somewhere.org",
|
||||||
|
});
|
||||||
|
expect(ruleWrongServer).toBeFalsy();
|
||||||
|
|
||||||
|
const ruleWrongServerRoom = await client.ignoredInvites.getRuleForInvite({
|
||||||
|
sender: "@foobar:somewhere.org",
|
||||||
|
roomId: "!snafu:example.org",
|
||||||
|
});
|
||||||
|
expect(ruleWrongServerRoom).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not reject invites anymore once we have removed a rule", async () => {
|
||||||
|
await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
|
||||||
|
|
||||||
|
// We should reject this invite.
|
||||||
|
const ruleMatch = await client.ignoredInvites.getRuleForInvite({
|
||||||
|
sender: "@foobar:example.org",
|
||||||
|
roomId: "!snafu:somewhere.org",
|
||||||
|
});
|
||||||
|
expect(ruleMatch).toBeTruthy();
|
||||||
|
expect(ruleMatch.getContent()).toMatchObject({
|
||||||
|
recommendation: "m.ban",
|
||||||
|
reason: "just a test",
|
||||||
|
});
|
||||||
|
|
||||||
|
// After removing the invite, we shouldn't reject it anymore.
|
||||||
|
await client.ignoredInvites.removeRule(ruleMatch);
|
||||||
|
const ruleMatch2 = await client.ignoredInvites.getRuleForInvite({
|
||||||
|
sender: "@foobar:example.org",
|
||||||
|
roomId: "!snafu:somewhere.org",
|
||||||
|
});
|
||||||
|
expect(ruleMatch2).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add new rules in the target room, rather than any other source room", async () => {
|
||||||
|
const NEW_SOURCE_ROOM_ID = "!another-source:example.org";
|
||||||
|
|
||||||
|
// Make sure that everything is initialized.
|
||||||
|
await client.ignoredInvites.getOrCreateSourceRooms();
|
||||||
|
await client.joinRoom(NEW_SOURCE_ROOM_ID);
|
||||||
|
const newSourceRoom = client.getRoom(NEW_SOURCE_ROOM_ID);
|
||||||
|
|
||||||
|
// Fetch the list of sources and check that we do not have the new room yet.
|
||||||
|
const policies = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent();
|
||||||
|
expect(policies).toBeTruthy();
|
||||||
|
const ignoreInvites = policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name];
|
||||||
|
expect(ignoreInvites).toBeTruthy();
|
||||||
|
expect(ignoreInvites.sources).toBeTruthy();
|
||||||
|
expect(ignoreInvites.sources).not.toContain(NEW_SOURCE_ROOM_ID);
|
||||||
|
|
||||||
|
// Add a source.
|
||||||
|
const added = await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
|
||||||
|
expect(added).toBe(true);
|
||||||
|
const added2 = await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
|
||||||
|
expect(added2).toBe(false);
|
||||||
|
|
||||||
|
// Fetch the list of sources and check that we have added the new room.
|
||||||
|
const policies2 = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent();
|
||||||
|
expect(policies2).toBeTruthy();
|
||||||
|
const ignoreInvites2 = policies2[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name];
|
||||||
|
expect(ignoreInvites2).toBeTruthy();
|
||||||
|
expect(ignoreInvites2.sources).toBeTruthy();
|
||||||
|
expect(ignoreInvites2.sources).toContain(NEW_SOURCE_ROOM_ID);
|
||||||
|
|
||||||
|
// Add a rule.
|
||||||
|
const eventId = await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
|
||||||
|
|
||||||
|
// Check where it shows up.
|
||||||
|
const targetRoomId = ignoreInvites2.target;
|
||||||
|
const targetRoom = client.getRoom(targetRoomId);
|
||||||
|
expect(targetRoom._state.get(PolicyScope.User)[eventId]).toBeTruthy();
|
||||||
|
expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -565,7 +565,7 @@ describe("MSC3089TreeSpace", () => {
|
|||||||
rooms = {};
|
rooms = {};
|
||||||
rooms[tree.roomId] = parentRoom;
|
rooms[tree.roomId] = parentRoom;
|
||||||
(<any>tree).room = parentRoom; // override readonly
|
(<any>tree).room = parentRoom; // override readonly
|
||||||
client.getRoom = (r) => rooms[r];
|
client.getRoom = (r) => rooms[r ?? ""];
|
||||||
|
|
||||||
clientSendStateFn = jest.fn()
|
clientSendStateFn = jest.fn()
|
||||||
.mockImplementation((roomId: string, eventType: EventType, content: any, stateKey: string) => {
|
.mockImplementation((roomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { REFERENCE_RELATION } from "matrix-events-sdk";
|
||||||
|
|
||||||
import { MatrixEvent } from "../../../src";
|
import { MatrixEvent } from "../../../src";
|
||||||
import { M_BEACON_INFO } from "../../../src/@types/beacon";
|
import { M_BEACON_INFO } from "../../../src/@types/beacon";
|
||||||
import {
|
import {
|
||||||
@@ -431,6 +433,27 @@ describe('Beacon', () => {
|
|||||||
expect(emitSpy).not.toHaveBeenCalled();
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should ignore invalid beacon events", () => {
|
||||||
|
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
|
||||||
|
const emitSpy = jest.spyOn(beacon, 'emit');
|
||||||
|
|
||||||
|
const ev = new MatrixEvent({
|
||||||
|
type: M_BEACON_INFO.name,
|
||||||
|
sender: userId,
|
||||||
|
room_id: roomId,
|
||||||
|
content: {
|
||||||
|
"m.relates_to": {
|
||||||
|
rel_type: REFERENCE_RELATION.name,
|
||||||
|
event_id: beacon.beaconInfoId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
beacon.addLocations([ev]);
|
||||||
|
|
||||||
|
expect(beacon.latestLocationEvent).toBeFalsy();
|
||||||
|
expect(emitSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
describe('when beacon is live with a start timestamp is in the future', () => {
|
describe('when beacon is live with a start timestamp is in the future', () => {
|
||||||
it('ignores locations before the beacon start timestamp', () => {
|
it('ignores locations before the beacon start timestamp', () => {
|
||||||
const startTimestamp = now + 60000;
|
const startTimestamp = now + 60000;
|
||||||
|
|||||||
114
spec/unit/notifications.spec.ts
Normal file
114
spec/unit/notifications.spec.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 {
|
||||||
|
EventType,
|
||||||
|
fixNotificationCountOnDecryption,
|
||||||
|
MatrixClient,
|
||||||
|
MatrixEvent,
|
||||||
|
MsgType,
|
||||||
|
NotificationCountType,
|
||||||
|
RelationType,
|
||||||
|
Room,
|
||||||
|
} from "../../src/matrix";
|
||||||
|
import { IActionsObject } from "../../src/pushprocessor";
|
||||||
|
import { ReEmitter } from "../../src/ReEmitter";
|
||||||
|
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../test-utils/client";
|
||||||
|
import { mkEvent, mock } from "../test-utils/test-utils";
|
||||||
|
|
||||||
|
let mockClient: MatrixClient;
|
||||||
|
let room: Room;
|
||||||
|
let event: MatrixEvent;
|
||||||
|
let threadEvent: MatrixEvent;
|
||||||
|
|
||||||
|
const ROOM_ID = "!roomId:example.org";
|
||||||
|
let THREAD_ID;
|
||||||
|
|
||||||
|
function mkPushAction(notify, highlight): IActionsObject {
|
||||||
|
return {
|
||||||
|
notify,
|
||||||
|
tweaks: {
|
||||||
|
highlight,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("fixNotificationCountOnDecryption", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockClient = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsUser(),
|
||||||
|
getPushActionsForEvent: jest.fn().mockReturnValue(mkPushAction(true, true)),
|
||||||
|
getRoom: jest.fn().mockImplementation(() => room),
|
||||||
|
decryptEventIfNeeded: jest.fn().mockResolvedValue(void 0),
|
||||||
|
supportsExperimentalThreads: jest.fn().mockReturnValue(true),
|
||||||
|
});
|
||||||
|
mockClient.reEmitter = mock(ReEmitter, 'ReEmitter');
|
||||||
|
|
||||||
|
room = new Room(ROOM_ID, mockClient, mockClient.getUserId());
|
||||||
|
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
||||||
|
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
|
||||||
|
|
||||||
|
event = mkEvent({
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
content: {
|
||||||
|
msgtype: MsgType.Text,
|
||||||
|
body: "Hello world!",
|
||||||
|
},
|
||||||
|
event: true,
|
||||||
|
}, mockClient);
|
||||||
|
|
||||||
|
THREAD_ID = event.getId();
|
||||||
|
threadEvent = mkEvent({
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
content: {
|
||||||
|
"m.relates_to": {
|
||||||
|
rel_type: RelationType.Thread,
|
||||||
|
event_id: THREAD_ID,
|
||||||
|
},
|
||||||
|
"msgtype": MsgType.Text,
|
||||||
|
"body": "Thread reply",
|
||||||
|
},
|
||||||
|
event: true,
|
||||||
|
});
|
||||||
|
room.createThread(THREAD_ID, event, [threadEvent], false);
|
||||||
|
|
||||||
|
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 1);
|
||||||
|
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||||
|
|
||||||
|
event.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false));
|
||||||
|
threadEvent.getPushActions = jest.fn().mockReturnValue(mkPushAction(false, false));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("changes the room count to highlight on decryption", () => {
|
||||||
|
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(1);
|
||||||
|
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(0);
|
||||||
|
|
||||||
|
fixNotificationCountOnDecryption(mockClient, event);
|
||||||
|
|
||||||
|
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(1);
|
||||||
|
expect(room.getUnreadNotificationCount(NotificationCountType.Highlight)).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("changes the thread count to highlight on decryption", () => {
|
||||||
|
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1);
|
||||||
|
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(0);
|
||||||
|
|
||||||
|
fixNotificationCountOnDecryption(mockClient, threadEvent);
|
||||||
|
|
||||||
|
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total)).toBe(1);
|
||||||
|
expect(room.getThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight)).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
69
spec/unit/pusher.spec.ts
Normal file
69
spec/unit/pusher.spec.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 { IHttpOpts, MatrixClient, PUSHER_ENABLED } from "../../src/matrix";
|
||||||
|
import { mkPusher } from '../test-utils/test-utils';
|
||||||
|
|
||||||
|
const realSetTimeout = setTimeout;
|
||||||
|
function flushPromises() {
|
||||||
|
return new Promise(r => {
|
||||||
|
realSetTimeout(r, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let client: MatrixClient;
|
||||||
|
let httpBackend: MockHttpBackend;
|
||||||
|
|
||||||
|
describe("Pushers", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
httpBackend = new MockHttpBackend();
|
||||||
|
client = new MatrixClient({
|
||||||
|
baseUrl: "https://my.home.server",
|
||||||
|
accessToken: "my.access.token",
|
||||||
|
request: httpBackend.requestFn as unknown as IHttpOpts["request"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("supports remotely toggling push notifications", () => {
|
||||||
|
it("migration support when connecting to a legacy homeserver", async () => {
|
||||||
|
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
|
||||||
|
unstable_features: {
|
||||||
|
"org.matrix.msc3881": false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
httpBackend.when("GET", "/pushers").respond(200, {
|
||||||
|
pushers: [
|
||||||
|
mkPusher(),
|
||||||
|
mkPusher({ [PUSHER_ENABLED.name]: true }),
|
||||||
|
mkPusher({ [PUSHER_ENABLED.name]: false }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const promise = client.getPushers();
|
||||||
|
|
||||||
|
await httpBackend.flushAllExpected();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
const response = await promise;
|
||||||
|
|
||||||
|
expect(response.pushers[0][PUSHER_ENABLED.name]).toBe(true);
|
||||||
|
expect(response.pushers[1][PUSHER_ENABLED.name]).toBe(true);
|
||||||
|
expect(response.pushers[2][PUSHER_ENABLED.name]).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as utils from "../test-utils/test-utils";
|
import * as utils from "../test-utils/test-utils";
|
||||||
import { PushProcessor } from "../../src/pushprocessor";
|
import { IActionsObject, PushProcessor } from "../../src/pushprocessor";
|
||||||
import { EventType, MatrixClient, MatrixEvent } from "../../src";
|
import { EventType, IContent, MatrixClient, MatrixEvent } from "../../src";
|
||||||
|
|
||||||
describe('NotificationService', function() {
|
describe('NotificationService', function() {
|
||||||
const testUserId = "@ali:matrix.org";
|
const testUserId = "@ali:matrix.org";
|
||||||
@@ -336,4 +336,102 @@ describe('NotificationService', function() {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
}, testEvent)).toBe(true);
|
}, testEvent)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("performCustomEventHandling()", () => {
|
||||||
|
const getActionsForEvent = (prevContent: IContent, content: IContent): IActionsObject => {
|
||||||
|
testEvent = utils.mkEvent({
|
||||||
|
type: "org.matrix.msc3401.call",
|
||||||
|
room: testRoomId,
|
||||||
|
user: "@alice:foo",
|
||||||
|
skey: "state_key",
|
||||||
|
event: true,
|
||||||
|
content: content,
|
||||||
|
prev_content: prevContent,
|
||||||
|
});
|
||||||
|
|
||||||
|
return pushProcessor.actionsForEvent(testEvent);
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertDoesNotify = (actions: IActionsObject): void => {
|
||||||
|
expect(actions.notify).toBeTruthy();
|
||||||
|
expect(actions.tweaks.sound).toBeTruthy();
|
||||||
|
expect(actions.tweaks.highlight).toBeFalsy();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertDoesNotNotify = (actions: IActionsObject): void => {
|
||||||
|
expect(actions.notify).toBeFalsy();
|
||||||
|
expect(actions.tweaks.sound).toBeFalsy();
|
||||||
|
expect(actions.tweaks.highlight).toBeFalsy();
|
||||||
|
};
|
||||||
|
|
||||||
|
it.each(
|
||||||
|
["m.ring", "m.prompt"],
|
||||||
|
)("should notify when new group call event appears with %s intent", (intent: string) => {
|
||||||
|
assertDoesNotify(getActionsForEvent({}, {
|
||||||
|
"m.intent": intent,
|
||||||
|
"m.type": "m.voice",
|
||||||
|
"m.name": "Call",
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should notify when a call is un-terminated", () => {
|
||||||
|
assertDoesNotify(getActionsForEvent({
|
||||||
|
"m.intent": "m.ring",
|
||||||
|
"m.type": "m.voice",
|
||||||
|
"m.name": "Call",
|
||||||
|
"m.terminated": "All users left",
|
||||||
|
}, {
|
||||||
|
"m.intent": "m.ring",
|
||||||
|
"m.type": "m.voice",
|
||||||
|
"m.name": "Call",
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not notify when call is terminated", () => {
|
||||||
|
assertDoesNotNotify(getActionsForEvent({
|
||||||
|
"m.intent": "m.ring",
|
||||||
|
"m.type": "m.voice",
|
||||||
|
"m.name": "Call",
|
||||||
|
}, {
|
||||||
|
"m.intent": "m.ring",
|
||||||
|
"m.type": "m.voice",
|
||||||
|
"m.name": "Call",
|
||||||
|
"m.terminated": "All users left",
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore with m.room intent", () => {
|
||||||
|
assertDoesNotNotify(getActionsForEvent({}, {
|
||||||
|
"m.intent": "m.room",
|
||||||
|
"m.type": "m.voice",
|
||||||
|
"m.name": "Call",
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ignoring non-relevant state changes", () => {
|
||||||
|
it("should ignore intent changes", () => {
|
||||||
|
assertDoesNotNotify(getActionsForEvent({
|
||||||
|
"m.intent": "m.ring",
|
||||||
|
"m.type": "m.voice",
|
||||||
|
"m.name": "Call",
|
||||||
|
}, {
|
||||||
|
"m.intent": "m.ring",
|
||||||
|
"m.type": "m.video",
|
||||||
|
"m.name": "Call",
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore name changes", () => {
|
||||||
|
assertDoesNotNotify(getActionsForEvent({
|
||||||
|
"m.intent": "m.ring",
|
||||||
|
"m.type": "m.voice",
|
||||||
|
"m.name": "Call",
|
||||||
|
}, {
|
||||||
|
"m.intent": "m.ring",
|
||||||
|
"m.type": "m.voice",
|
||||||
|
"m.name": "New call",
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
150
spec/unit/read-receipt.spec.ts
Normal file
150
spec/unit/read-receipt.spec.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 { ReceiptType } from '../../src/@types/read_receipts';
|
||||||
|
import { MatrixClient } from "../../src/client";
|
||||||
|
import { IHttpOpts } from '../../src/http-api';
|
||||||
|
import { EventType } from '../../src/matrix';
|
||||||
|
import { MAIN_ROOM_TIMELINE } from '../../src/models/read-receipt';
|
||||||
|
import { encodeUri } from '../../src/utils';
|
||||||
|
import * as utils from "../test-utils/test-utils";
|
||||||
|
|
||||||
|
// Jest now uses @sinonjs/fake-timers which exposes tickAsync() and a number of
|
||||||
|
// other async methods which break the event loop, letting scheduled promise
|
||||||
|
// callbacks run. Unfortunately, Jest doesn't expose these, so we have to do
|
||||||
|
// it manually (this is what sinon does under the hood). We do both in a loop
|
||||||
|
// until the thing we expect happens: hopefully this is the least flakey way
|
||||||
|
// and avoids assuming anything about the app's behaviour.
|
||||||
|
const realSetTimeout = setTimeout;
|
||||||
|
function flushPromises() {
|
||||||
|
return new Promise(r => {
|
||||||
|
realSetTimeout(r, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let client: MatrixClient;
|
||||||
|
let httpBackend: MockHttpBackend;
|
||||||
|
|
||||||
|
const THREAD_ID = "$thread_event_id";
|
||||||
|
const ROOM_ID = "!123:matrix.org";
|
||||||
|
|
||||||
|
const threadEvent = utils.mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
user: "@bob:matrix.org",
|
||||||
|
room: ROOM_ID,
|
||||||
|
content: {
|
||||||
|
"body": "Hello from a thread",
|
||||||
|
"m.relates_to": {
|
||||||
|
"event_id": THREAD_ID,
|
||||||
|
"m.in_reply_to": {
|
||||||
|
"event_id": THREAD_ID,
|
||||||
|
},
|
||||||
|
"rel_type": "m.thread",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const roomEvent = utils.mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
user: "@bob:matrix.org",
|
||||||
|
room: ROOM_ID,
|
||||||
|
content: {
|
||||||
|
"body": "Hello from a room",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function mockServerSideSupport(client, hasServerSideSupport) {
|
||||||
|
const doesServerSupportUnstableFeature = client.doesServerSupportUnstableFeature;
|
||||||
|
client.doesServerSupportUnstableFeature = (unstableFeature) => {
|
||||||
|
if (unstableFeature === "org.matrix.msc3771") {
|
||||||
|
return Promise.resolve(hasServerSideSupport);
|
||||||
|
} else {
|
||||||
|
return doesServerSupportUnstableFeature(unstableFeature);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Read receipt", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
httpBackend = new MockHttpBackend();
|
||||||
|
client = new MatrixClient({
|
||||||
|
baseUrl: "https://my.home.server",
|
||||||
|
accessToken: "my.access.token",
|
||||||
|
request: httpBackend.requestFn as unknown as IHttpOpts["request"],
|
||||||
|
});
|
||||||
|
client.isGuest = () => false;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sendReceipt", () => {
|
||||||
|
it("sends a thread read receipt", 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).toEqual(THREAD_ID);
|
||||||
|
}).respond(200, {});
|
||||||
|
|
||||||
|
mockServerSideSupport(client, true);
|
||||||
|
client.sendReceipt(threadEvent, ReceiptType.Read, {});
|
||||||
|
|
||||||
|
await httpBackend.flushAllExpected();
|
||||||
|
await flushPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends a room read receipt", async () => {
|
||||||
|
httpBackend.when(
|
||||||
|
"POST", encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||||
|
$roomId: ROOM_ID,
|
||||||
|
$receiptType: ReceiptType.Read,
|
||||||
|
$eventId: roomEvent.getId(),
|
||||||
|
}),
|
||||||
|
).check((request) => {
|
||||||
|
expect(request.data.thread_id).toEqual(MAIN_ROOM_TIMELINE);
|
||||||
|
}).respond(200, {});
|
||||||
|
|
||||||
|
mockServerSideSupport(client, true);
|
||||||
|
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, false);
|
||||||
|
client.sendReceipt(threadEvent, ReceiptType.Read, {});
|
||||||
|
|
||||||
|
await httpBackend.flushAllExpected();
|
||||||
|
await flushPromises();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,12 +1,29 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 "../test-utils/test-utils";
|
import * as utils from "../test-utils/test-utils";
|
||||||
import { RoomMember } from "../../src/models/room-member";
|
import { RoomMember, RoomMemberEvent } from "../../src/models/room-member";
|
||||||
|
import { RoomState } from "../../src";
|
||||||
|
|
||||||
describe("RoomMember", function() {
|
describe("RoomMember", function() {
|
||||||
const roomId = "!foo:bar";
|
const roomId = "!foo:bar";
|
||||||
const userA = "@alice:bar";
|
const userA = "@alice:bar";
|
||||||
const userB = "@bertha:bar";
|
const userB = "@bertha:bar";
|
||||||
const userC = "@clarissa:bar";
|
const userC = "@clarissa:bar";
|
||||||
let member;
|
let member = new RoomMember(roomId, userA);
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
member = new RoomMember(roomId, userA);
|
member = new RoomMember(roomId, userA);
|
||||||
@@ -27,15 +44,15 @@ describe("RoomMember", function() {
|
|||||||
avatar_url: "mxc://flibble/wibble",
|
avatar_url: "mxc://flibble/wibble",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const url = member.getAvatarUrl(hsUrl);
|
const url = member.getAvatarUrl(hsUrl, 1, 1, '', false, false);
|
||||||
// we don't care about how the mxc->http conversion is done, other
|
// we don't care about how the mxc->http conversion is done, other
|
||||||
// than it contains the mxc body.
|
// than it contains the mxc body.
|
||||||
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
|
expect(url?.indexOf("flibble/wibble")).not.toEqual(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return nothing if there is no m.room.member and allowDefault=false",
|
it("should return nothing if there is no m.room.member and allowDefault=false",
|
||||||
function() {
|
function() {
|
||||||
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false);
|
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false, false);
|
||||||
expect(url).toEqual(null);
|
expect(url).toEqual(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -82,7 +99,7 @@ describe("RoomMember", function() {
|
|||||||
});
|
});
|
||||||
let emitCount = 0;
|
let emitCount = 0;
|
||||||
|
|
||||||
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
|
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
|
||||||
emitCount += 1;
|
emitCount += 1;
|
||||||
expect(emitMember).toEqual(member);
|
expect(emitMember).toEqual(member);
|
||||||
expect(emitEvent).toEqual(event);
|
expect(emitEvent).toEqual(event);
|
||||||
@@ -113,7 +130,7 @@ describe("RoomMember", function() {
|
|||||||
// set the power level to something other than zero or we
|
// set the power level to something other than zero or we
|
||||||
// won't get an event
|
// won't get an event
|
||||||
member.powerLevel = 1;
|
member.powerLevel = 1;
|
||||||
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
|
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
|
||||||
emitCount += 1;
|
emitCount += 1;
|
||||||
expect(emitMember.userId).toEqual('@alice:bar');
|
expect(emitMember.userId).toEqual('@alice:bar');
|
||||||
expect(emitMember.powerLevel).toEqual(0);
|
expect(emitMember.powerLevel).toEqual(0);
|
||||||
@@ -141,7 +158,7 @@ describe("RoomMember", function() {
|
|||||||
});
|
});
|
||||||
let emitCount = 0;
|
let emitCount = 0;
|
||||||
|
|
||||||
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
|
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
|
||||||
emitCount += 1;
|
emitCount += 1;
|
||||||
expect(emitMember.userId).toEqual('@alice:bar');
|
expect(emitMember.userId).toEqual('@alice:bar');
|
||||||
expect(emitMember.powerLevel).toEqual(20);
|
expect(emitMember.powerLevel).toEqual(20);
|
||||||
@@ -195,7 +212,7 @@ describe("RoomMember", function() {
|
|||||||
event: true,
|
event: true,
|
||||||
});
|
});
|
||||||
let emitCount = 0;
|
let emitCount = 0;
|
||||||
member.on("RoomMember.typing", function(ev, mem) {
|
member.on(RoomMemberEvent.Typing, function(ev, mem) {
|
||||||
expect(mem).toEqual(member);
|
expect(mem).toEqual(member);
|
||||||
expect(ev).toEqual(event);
|
expect(ev).toEqual(event);
|
||||||
emitCount += 1;
|
emitCount += 1;
|
||||||
@@ -210,7 +227,7 @@ describe("RoomMember", function() {
|
|||||||
|
|
||||||
describe("isOutOfBand", function() {
|
describe("isOutOfBand", function() {
|
||||||
it("should be set by markOutOfBand", function() {
|
it("should be set by markOutOfBand", function() {
|
||||||
const member = new RoomMember();
|
const member = new RoomMember(roomId, userA);
|
||||||
expect(member.isOutOfBand()).toEqual(false);
|
expect(member.isOutOfBand()).toEqual(false);
|
||||||
member.markOutOfBand();
|
member.markOutOfBand();
|
||||||
expect(member.isOutOfBand()).toEqual(true);
|
expect(member.isOutOfBand()).toEqual(true);
|
||||||
@@ -266,7 +283,7 @@ describe("RoomMember", function() {
|
|||||||
getUserIdsWithDisplayName: function(displayName) {
|
getUserIdsWithDisplayName: function(displayName) {
|
||||||
return [userA, userC];
|
return [userA, userC];
|
||||||
},
|
},
|
||||||
};
|
} as unknown as RoomState;
|
||||||
expect(member.name).toEqual(userA); // default = user_id
|
expect(member.name).toEqual(userA); // default = user_id
|
||||||
member.setMembershipEvent(joinEvent);
|
member.setMembershipEvent(joinEvent);
|
||||||
expect(member.name).toEqual("Alice"); // prefer displayname
|
expect(member.name).toEqual("Alice"); // prefer displayname
|
||||||
@@ -278,7 +295,7 @@ describe("RoomMember", function() {
|
|||||||
|
|
||||||
it("should emit 'RoomMember.membership' if the membership changes", function() {
|
it("should emit 'RoomMember.membership' if the membership changes", function() {
|
||||||
let emitCount = 0;
|
let emitCount = 0;
|
||||||
member.on("RoomMember.membership", function(ev, mem) {
|
member.on(RoomMemberEvent.Membership, function(ev, mem) {
|
||||||
emitCount += 1;
|
emitCount += 1;
|
||||||
expect(mem).toEqual(member);
|
expect(mem).toEqual(member);
|
||||||
expect(ev).toEqual(inviteEvent);
|
expect(ev).toEqual(inviteEvent);
|
||||||
@@ -291,7 +308,7 @@ describe("RoomMember", function() {
|
|||||||
|
|
||||||
it("should emit 'RoomMember.name' if the name changes", function() {
|
it("should emit 'RoomMember.name' if the name changes", function() {
|
||||||
let emitCount = 0;
|
let emitCount = 0;
|
||||||
member.on("RoomMember.name", function(ev, mem) {
|
member.on(RoomMemberEvent.Name, function(ev, mem) {
|
||||||
emitCount += 1;
|
emitCount += 1;
|
||||||
expect(mem).toEqual(member);
|
expect(mem).toEqual(member);
|
||||||
expect(ev).toEqual(joinEvent);
|
expect(ev).toEqual(joinEvent);
|
||||||
@@ -341,7 +358,7 @@ describe("RoomMember", function() {
|
|||||||
getUserIdsWithDisplayName: function(displayName) {
|
getUserIdsWithDisplayName: function(displayName) {
|
||||||
return [userA, userC];
|
return [userA, userC];
|
||||||
},
|
},
|
||||||
};
|
} as unknown as RoomState;
|
||||||
expect(member.name).toEqual(userA); // default = user_id
|
expect(member.name).toEqual(userA); // default = user_id
|
||||||
member.setMembershipEvent(joinEvent, roomState);
|
member.setMembershipEvent(joinEvent, roomState);
|
||||||
expect(member.name).not.toEqual("Alíce"); // it should disambig.
|
expect(member.name).not.toEqual("Alíce"); // it should disambig.
|
||||||
@@ -1,14 +1,37 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 { MockedObject } from 'jest-mock';
|
||||||
|
|
||||||
import * as utils from "../test-utils/test-utils";
|
import * as utils from "../test-utils/test-utils";
|
||||||
import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
|
import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
|
||||||
import { filterEmitCallsByEventType } from "../test-utils/emitter";
|
import { filterEmitCallsByEventType } from "../test-utils/emitter";
|
||||||
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
|
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
|
||||||
import { BeaconEvent, getBeaconInfoIdentifier } from "../../src/models/beacon";
|
import {
|
||||||
|
Beacon,
|
||||||
|
BeaconEvent,
|
||||||
|
getBeaconInfoIdentifier,
|
||||||
|
} from "../../src/models/beacon";
|
||||||
import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@types/event";
|
import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@types/event";
|
||||||
import {
|
import {
|
||||||
MatrixEvent,
|
MatrixEvent,
|
||||||
MatrixEventEvent,
|
MatrixEventEvent,
|
||||||
} from "../../src/models/event";
|
} from "../../src/models/event";
|
||||||
import { M_BEACON } from "../../src/@types/beacon";
|
import { M_BEACON } from "../../src/@types/beacon";
|
||||||
|
import { MatrixClient } from "../../src/client";
|
||||||
|
|
||||||
describe("RoomState", function() {
|
describe("RoomState", function() {
|
||||||
const roomId = "!foo:bar";
|
const roomId = "!foo:bar";
|
||||||
@@ -17,7 +40,7 @@ describe("RoomState", function() {
|
|||||||
const userC = "@cleo:bar";
|
const userC = "@cleo:bar";
|
||||||
const userLazy = "@lazy:bar";
|
const userLazy = "@lazy:bar";
|
||||||
|
|
||||||
let state;
|
let state = new RoomState(roomId);
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
state = new RoomState(roomId);
|
state = new RoomState(roomId);
|
||||||
@@ -67,8 +90,8 @@ describe("RoomState", function() {
|
|||||||
|
|
||||||
it("should return a member which changes as state changes", function() {
|
it("should return a member which changes as state changes", function() {
|
||||||
const member = state.getMember(userB);
|
const member = state.getMember(userB);
|
||||||
expect(member.membership).toEqual("join");
|
expect(member?.membership).toEqual("join");
|
||||||
expect(member.name).toEqual(userB);
|
expect(member?.name).toEqual(userB);
|
||||||
|
|
||||||
state.setStateEvents([
|
state.setStateEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
@@ -77,14 +100,14 @@ describe("RoomState", function() {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(member.membership).toEqual("leave");
|
expect(member?.membership).toEqual("leave");
|
||||||
expect(member.name).toEqual("BobGone");
|
expect(member?.name).toEqual("BobGone");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getSentinelMember", function() {
|
describe("getSentinelMember", function() {
|
||||||
it("should return a member with the user id as name", function() {
|
it("should return a member with the user id as name", function() {
|
||||||
expect(state.getSentinelMember("@no-one:here").name).toEqual("@no-one:here");
|
expect(state.getSentinelMember("@no-one:here")?.name).toEqual("@no-one:here");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return a member which doesn't change when the state is updated",
|
it("should return a member which doesn't change when the state is updated",
|
||||||
@@ -98,11 +121,11 @@ describe("RoomState", function() {
|
|||||||
]);
|
]);
|
||||||
const postLeaveUser = state.getSentinelMember(userA);
|
const postLeaveUser = state.getSentinelMember(userA);
|
||||||
|
|
||||||
expect(preLeaveUser.membership).toEqual("join");
|
expect(preLeaveUser?.membership).toEqual("join");
|
||||||
expect(preLeaveUser.name).toEqual(userA);
|
expect(preLeaveUser?.name).toEqual(userA);
|
||||||
|
|
||||||
expect(postLeaveUser.membership).toEqual("leave");
|
expect(postLeaveUser?.membership).toEqual("leave");
|
||||||
expect(postLeaveUser.name).toEqual("AliceIsGone");
|
expect(postLeaveUser?.name).toEqual("AliceIsGone");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -122,8 +145,8 @@ describe("RoomState", function() {
|
|||||||
const events = state.getStateEvents("m.room.member");
|
const events = state.getStateEvents("m.room.member");
|
||||||
expect(events.length).toEqual(2);
|
expect(events.length).toEqual(2);
|
||||||
// ordering unimportant
|
// ordering unimportant
|
||||||
expect([userA, userB].indexOf(events[0].getStateKey())).not.toEqual(-1);
|
expect([userA, userB].indexOf(events[0].getStateKey() as string)).not.toEqual(-1);
|
||||||
expect([userA, userB].indexOf(events[1].getStateKey())).not.toEqual(-1);
|
expect([userA, userB].indexOf(events[1].getStateKey() as string)).not.toEqual(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return a single MatrixEvent if a state_key was specified",
|
it("should return a single MatrixEvent if a state_key was specified",
|
||||||
@@ -146,7 +169,7 @@ describe("RoomState", function() {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
let emitCount = 0;
|
let emitCount = 0;
|
||||||
state.on("RoomState.members", function(ev, st, mem) {
|
state.on(RoomStateEvent.Members, function(ev, st, mem) {
|
||||||
expect(ev).toEqual(memberEvents[emitCount]);
|
expect(ev).toEqual(memberEvents[emitCount]);
|
||||||
expect(st).toEqual(state);
|
expect(st).toEqual(state);
|
||||||
expect(mem).toEqual(state.getMember(ev.getSender()));
|
expect(mem).toEqual(state.getMember(ev.getSender()));
|
||||||
@@ -166,7 +189,7 @@ describe("RoomState", function() {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
let emitCount = 0;
|
let emitCount = 0;
|
||||||
state.on("RoomState.newMember", function(ev, st, mem) {
|
state.on(RoomStateEvent.NewMember, function(ev, st, mem) {
|
||||||
expect(state.getMember(mem.userId)).toEqual(mem);
|
expect(state.getMember(mem.userId)).toEqual(mem);
|
||||||
expect(mem.userId).toEqual(memberEvents[emitCount].getSender());
|
expect(mem.userId).toEqual(memberEvents[emitCount].getSender());
|
||||||
expect(mem.membership).toBeFalsy(); // not defined yet
|
expect(mem.membership).toBeFalsy(); // not defined yet
|
||||||
@@ -192,7 +215,7 @@ describe("RoomState", function() {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
let emitCount = 0;
|
let emitCount = 0;
|
||||||
state.on("RoomState.events", function(ev, st) {
|
state.on(RoomStateEvent.Events, function(ev, st) {
|
||||||
expect(ev).toEqual(events[emitCount]);
|
expect(ev).toEqual(events[emitCount]);
|
||||||
expect(st).toEqual(state);
|
expect(st).toEqual(state);
|
||||||
emitCount += 1;
|
emitCount += 1;
|
||||||
@@ -272,7 +295,7 @@ describe("RoomState", function() {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
let emitCount = 0;
|
let emitCount = 0;
|
||||||
state.on("RoomState.Marker", function(markerEvent, markerFoundOptions) {
|
state.on(RoomStateEvent.Marker, function(markerEvent, markerFoundOptions) {
|
||||||
expect(markerEvent).toEqual(events[emitCount]);
|
expect(markerEvent).toEqual(events[emitCount]);
|
||||||
expect(markerFoundOptions).toEqual({ timelineWasEmpty: true });
|
expect(markerFoundOptions).toEqual({ timelineWasEmpty: true });
|
||||||
emitCount += 1;
|
emitCount += 1;
|
||||||
@@ -296,7 +319,7 @@ describe("RoomState", function() {
|
|||||||
|
|
||||||
it('does not add redacted beacon info events to state', () => {
|
it('does not add redacted beacon info events to state', () => {
|
||||||
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
|
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
|
||||||
const redactionEvent = { event: { type: 'm.room.redaction' } };
|
const redactionEvent = new MatrixEvent({ type: 'm.room.redaction' });
|
||||||
redactedBeaconEvent.makeRedacted(redactionEvent);
|
redactedBeaconEvent.makeRedacted(redactionEvent);
|
||||||
const emitSpy = jest.spyOn(state, 'emit');
|
const emitSpy = jest.spyOn(state, 'emit');
|
||||||
|
|
||||||
@@ -316,27 +339,27 @@ describe("RoomState", function() {
|
|||||||
|
|
||||||
state.setStateEvents([beaconEvent]);
|
state.setStateEvents([beaconEvent]);
|
||||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
||||||
expect(beaconInstance.isLive).toEqual(true);
|
expect(beaconInstance?.isLive).toEqual(true);
|
||||||
|
|
||||||
state.setStateEvents([updatedBeaconEvent]);
|
state.setStateEvents([updatedBeaconEvent]);
|
||||||
|
|
||||||
// same Beacon
|
// same Beacon
|
||||||
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance);
|
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance);
|
||||||
// updated liveness
|
// updated liveness
|
||||||
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent)).isLive).toEqual(false);
|
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))?.isLive).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('destroys and removes redacted beacon events', () => {
|
it('destroys and removes redacted beacon events', () => {
|
||||||
const beaconId = '$beacon1';
|
const beaconId = '$beacon1';
|
||||||
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
||||||
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
||||||
const redactionEvent = { event: { type: 'm.room.redaction', redacts: beaconEvent.getId() } };
|
const redactionEvent = new MatrixEvent({ type: 'm.room.redaction', redacts: beaconEvent.getId() });
|
||||||
redactedBeaconEvent.makeRedacted(redactionEvent);
|
redactedBeaconEvent.makeRedacted(redactionEvent);
|
||||||
|
|
||||||
state.setStateEvents([beaconEvent]);
|
state.setStateEvents([beaconEvent]);
|
||||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
||||||
const destroySpy = jest.spyOn(beaconInstance, 'destroy');
|
const destroySpy = jest.spyOn(beaconInstance as Beacon, 'destroy');
|
||||||
expect(beaconInstance.isLive).toEqual(true);
|
expect(beaconInstance?.isLive).toEqual(true);
|
||||||
|
|
||||||
state.setStateEvents([redactedBeaconEvent]);
|
state.setStateEvents([redactedBeaconEvent]);
|
||||||
|
|
||||||
@@ -357,7 +380,7 @@ describe("RoomState", function() {
|
|||||||
|
|
||||||
// live beacon is now not live
|
// live beacon is now not live
|
||||||
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
|
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
|
||||||
userA, roomId, { isLive: false }, liveBeaconEvent.getId(), '$beacon1',
|
userA, roomId, { isLive: false }, liveBeaconEvent.getId(),
|
||||||
);
|
);
|
||||||
|
|
||||||
state.setStateEvents([updatedLiveBeaconEvent]);
|
state.setStateEvents([updatedLiveBeaconEvent]);
|
||||||
@@ -377,8 +400,8 @@ describe("RoomState", function() {
|
|||||||
state.markOutOfBandMembersStarted();
|
state.markOutOfBandMembersStarted();
|
||||||
state.setOutOfBandMembers([oobMemberEvent]);
|
state.setOutOfBandMembers([oobMemberEvent]);
|
||||||
const member = state.getMember(userLazy);
|
const member = state.getMember(userLazy);
|
||||||
expect(member.userId).toEqual(userLazy);
|
expect(member?.userId).toEqual(userLazy);
|
||||||
expect(member.isOutOfBand()).toEqual(true);
|
expect(member?.isOutOfBand()).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have no effect when not in correct status", function() {
|
it("should have no effect when not in correct status", function() {
|
||||||
@@ -394,7 +417,7 @@ describe("RoomState", function() {
|
|||||||
user: userLazy, mship: "join", room: roomId, event: true,
|
user: userLazy, mship: "join", room: roomId, event: true,
|
||||||
});
|
});
|
||||||
let eventReceived = false;
|
let eventReceived = false;
|
||||||
state.once('RoomState.newMember', (_, __, member) => {
|
state.once(RoomStateEvent.NewMember, (_event, _state, member) => {
|
||||||
expect(member.userId).toEqual(userLazy);
|
expect(member.userId).toEqual(userLazy);
|
||||||
eventReceived = true;
|
eventReceived = true;
|
||||||
});
|
});
|
||||||
@@ -410,8 +433,8 @@ describe("RoomState", function() {
|
|||||||
state.markOutOfBandMembersStarted();
|
state.markOutOfBandMembersStarted();
|
||||||
state.setOutOfBandMembers([oobMemberEvent]);
|
state.setOutOfBandMembers([oobMemberEvent]);
|
||||||
const memberA = state.getMember(userA);
|
const memberA = state.getMember(userA);
|
||||||
expect(memberA.events.member.getId()).not.toEqual(oobMemberEvent.getId());
|
expect(memberA?.events?.member?.getId()).not.toEqual(oobMemberEvent.getId());
|
||||||
expect(memberA.isOutOfBand()).toEqual(false);
|
expect(memberA?.isOutOfBand()).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit members when updating a member", function() {
|
it("should emit members when updating a member", function() {
|
||||||
@@ -420,7 +443,7 @@ describe("RoomState", function() {
|
|||||||
user: doesntExistYetUserId, mship: "join", room: roomId, event: true,
|
user: doesntExistYetUserId, mship: "join", room: roomId, event: true,
|
||||||
});
|
});
|
||||||
let eventReceived = false;
|
let eventReceived = false;
|
||||||
state.once('RoomState.members', (_, __, member) => {
|
state.once(RoomStateEvent.Members, (_event, _state, member) => {
|
||||||
expect(member.userId).toEqual(doesntExistYetUserId);
|
expect(member.userId).toEqual(doesntExistYetUserId);
|
||||||
eventReceived = true;
|
eventReceived = true;
|
||||||
});
|
});
|
||||||
@@ -443,8 +466,8 @@ describe("RoomState", function() {
|
|||||||
[userA, userB, userLazy].forEach((userId) => {
|
[userA, userB, userLazy].forEach((userId) => {
|
||||||
const member = state.getMember(userId);
|
const member = state.getMember(userId);
|
||||||
const memberCopy = copy.getMember(userId);
|
const memberCopy = copy.getMember(userId);
|
||||||
expect(member.name).toEqual(memberCopy.name);
|
expect(member?.name).toEqual(memberCopy?.name);
|
||||||
expect(member.isOutOfBand()).toEqual(memberCopy.isOutOfBand());
|
expect(member?.isOutOfBand()).toEqual(memberCopy?.isOutOfBand());
|
||||||
});
|
});
|
||||||
// check member keys
|
// check member keys
|
||||||
expect(Object.keys(state.members)).toEqual(Object.keys(copy.members));
|
expect(Object.keys(state.members)).toEqual(Object.keys(copy.members));
|
||||||
@@ -503,19 +526,20 @@ describe("RoomState", function() {
|
|||||||
it("should say members with power >=50 may send state with power level event " +
|
it("should say members with power >=50 may send state with power level event " +
|
||||||
"but no state default",
|
"but no state default",
|
||||||
function() {
|
function() {
|
||||||
const powerLevelEvent = {
|
const powerLevelEvent = new MatrixEvent({
|
||||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
type: "m.room.power_levels", room_id: roomId, sender: userA,
|
||||||
|
state_key: "",
|
||||||
content: {
|
content: {
|
||||||
users_default: 10,
|
users_default: 10,
|
||||||
// state_default: 50, "intentionally left blank"
|
// state_default: 50, "intentionally left blank"
|
||||||
events_default: 25,
|
events_default: 25,
|
||||||
users: {
|
users: {
|
||||||
|
[userA]: 50,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
powerLevelEvent.content.users[userA] = 50;
|
|
||||||
|
|
||||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
state.setStateEvents([powerLevelEvent]);
|
||||||
|
|
||||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||||
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
|
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
|
||||||
@@ -523,20 +547,21 @@ describe("RoomState", function() {
|
|||||||
|
|
||||||
it("should obey state_default",
|
it("should obey state_default",
|
||||||
function() {
|
function() {
|
||||||
const powerLevelEvent = {
|
const powerLevelEvent = new MatrixEvent({
|
||||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
type: "m.room.power_levels", room_id: roomId, sender: userA,
|
||||||
|
state_key: "",
|
||||||
content: {
|
content: {
|
||||||
users_default: 10,
|
users_default: 10,
|
||||||
state_default: 30,
|
state_default: 30,
|
||||||
events_default: 25,
|
events_default: 25,
|
||||||
users: {
|
users: {
|
||||||
|
[userA]: 30,
|
||||||
|
[userB]: 29,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
powerLevelEvent.content.users[userA] = 30;
|
|
||||||
powerLevelEvent.content.users[userB] = 29;
|
|
||||||
|
|
||||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
state.setStateEvents([powerLevelEvent]);
|
||||||
|
|
||||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||||
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
|
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
|
||||||
@@ -544,9 +569,9 @@ describe("RoomState", function() {
|
|||||||
|
|
||||||
it("should honour explicit event power levels in the power_levels event",
|
it("should honour explicit event power levels in the power_levels event",
|
||||||
function() {
|
function() {
|
||||||
const powerLevelEvent = {
|
const powerLevelEvent = new MatrixEvent({
|
||||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
type: "m.room.power_levels", room_id: roomId, sender: userA,
|
||||||
content: {
|
state_key: "", content: {
|
||||||
events: {
|
events: {
|
||||||
"m.room.other_thing": 76,
|
"m.room.other_thing": 76,
|
||||||
},
|
},
|
||||||
@@ -554,13 +579,13 @@ describe("RoomState", function() {
|
|||||||
state_default: 50,
|
state_default: 50,
|
||||||
events_default: 25,
|
events_default: 25,
|
||||||
users: {
|
users: {
|
||||||
|
[userA]: 80,
|
||||||
|
[userB]: 50,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
powerLevelEvent.content.users[userA] = 80;
|
|
||||||
powerLevelEvent.content.users[userB] = 50;
|
|
||||||
|
|
||||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
state.setStateEvents([powerLevelEvent]);
|
||||||
|
|
||||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||||
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(true);
|
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(true);
|
||||||
@@ -689,20 +714,21 @@ describe("RoomState", function() {
|
|||||||
|
|
||||||
it("should obey events_default",
|
it("should obey events_default",
|
||||||
function() {
|
function() {
|
||||||
const powerLevelEvent = {
|
const powerLevelEvent = new MatrixEvent({
|
||||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
type: "m.room.power_levels", room_id: roomId, sender: userA,
|
||||||
|
state_key: "",
|
||||||
content: {
|
content: {
|
||||||
users_default: 10,
|
users_default: 10,
|
||||||
state_default: 30,
|
state_default: 30,
|
||||||
events_default: 25,
|
events_default: 25,
|
||||||
users: {
|
users: {
|
||||||
|
[userA]: 26,
|
||||||
|
[userB]: 24,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
powerLevelEvent.content.users[userA] = 26;
|
|
||||||
powerLevelEvent.content.users[userB] = 24;
|
|
||||||
|
|
||||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
state.setStateEvents([powerLevelEvent]);
|
||||||
|
|
||||||
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
||||||
expect(state.maySendEvent('m.room.message', userB)).toEqual(false);
|
expect(state.maySendEvent('m.room.message', userB)).toEqual(false);
|
||||||
@@ -713,8 +739,9 @@ describe("RoomState", function() {
|
|||||||
|
|
||||||
it("should honour explicit event power levels in the power_levels event",
|
it("should honour explicit event power levels in the power_levels event",
|
||||||
function() {
|
function() {
|
||||||
const powerLevelEvent = {
|
const powerLevelEvent = new MatrixEvent({
|
||||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
type: "m.room.power_levels", room_id: roomId, sender: userA,
|
||||||
|
state_key: "",
|
||||||
content: {
|
content: {
|
||||||
events: {
|
events: {
|
||||||
"m.room.other_thing": 33,
|
"m.room.other_thing": 33,
|
||||||
@@ -723,13 +750,13 @@ describe("RoomState", function() {
|
|||||||
state_default: 50,
|
state_default: 50,
|
||||||
events_default: 25,
|
events_default: 25,
|
||||||
users: {
|
users: {
|
||||||
|
[userA]: 40,
|
||||||
|
[userB]: 30,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
powerLevelEvent.content.users[userA] = 40;
|
|
||||||
powerLevelEvent.content.users[userB] = 30;
|
|
||||||
|
|
||||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
state.setStateEvents([powerLevelEvent]);
|
||||||
|
|
||||||
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
||||||
expect(state.maySendEvent('m.room.message', userB)).toEqual(true);
|
expect(state.maySendEvent('m.room.message', userB)).toEqual(true);
|
||||||
@@ -743,10 +770,10 @@ describe("RoomState", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('processBeaconEvents', () => {
|
describe('processBeaconEvents', () => {
|
||||||
const beacon1 = makeBeaconInfoEvent(userA, roomId, {}, '$beacon1', '$beacon1');
|
const beacon1 = makeBeaconInfoEvent(userA, roomId, {}, '$beacon1');
|
||||||
const beacon2 = makeBeaconInfoEvent(userB, roomId, {}, '$beacon2', '$beacon2');
|
const beacon2 = makeBeaconInfoEvent(userB, roomId, {}, '$beacon2');
|
||||||
|
|
||||||
const mockClient = { decryptEventIfNeeded: jest.fn() };
|
const mockClient = { decryptEventIfNeeded: jest.fn() } as unknown as MockedObject<MatrixClient>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockClient.decryptEventIfNeeded.mockClear();
|
mockClient.decryptEventIfNeeded.mockClear();
|
||||||
@@ -816,11 +843,11 @@ describe("RoomState", function() {
|
|||||||
beaconInfoId: 'some-other-beacon',
|
beaconInfoId: 'some-other-beacon',
|
||||||
});
|
});
|
||||||
|
|
||||||
state.setStateEvents([beacon1, beacon2], mockClient);
|
state.setStateEvents([beacon1, beacon2]);
|
||||||
|
|
||||||
expect(state.beacons.size).toEqual(2);
|
expect(state.beacons.size).toEqual(2);
|
||||||
|
|
||||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
|
||||||
const addLocationsSpy = jest.spyOn(beaconInstance, 'addLocations');
|
const addLocationsSpy = jest.spyOn(beaconInstance, 'addLocations');
|
||||||
|
|
||||||
state.processBeaconEvents([location1, location2, location3], mockClient);
|
state.processBeaconEvents([location1, location2, location3], mockClient);
|
||||||
@@ -885,7 +912,7 @@ describe("RoomState", function() {
|
|||||||
});
|
});
|
||||||
state.setStateEvents([beacon1, beacon2]);
|
state.setStateEvents([beacon1, beacon2]);
|
||||||
|
|
||||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
|
||||||
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
|
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
|
||||||
state.processBeaconEvents([location, otherRelatedEvent], mockClient);
|
state.processBeaconEvents([location, otherRelatedEvent], mockClient);
|
||||||
expect(addLocationsSpy).not.toHaveBeenCalled();
|
expect(addLocationsSpy).not.toHaveBeenCalled();
|
||||||
@@ -945,13 +972,13 @@ describe("RoomState", function() {
|
|||||||
});
|
});
|
||||||
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
||||||
state.setStateEvents([beacon1, beacon2]);
|
state.setStateEvents([beacon1, beacon2]);
|
||||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
|
||||||
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
|
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
|
||||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||||
|
|
||||||
// this event is a message after decryption
|
// this event is a message after decryption
|
||||||
decryptingRelatedEvent.type = EventType.RoomMessage;
|
decryptingRelatedEvent.event.type = EventType.RoomMessage;
|
||||||
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted);
|
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted, decryptingRelatedEvent);
|
||||||
|
|
||||||
expect(addLocationsSpy).not.toHaveBeenCalled();
|
expect(addLocationsSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -967,14 +994,14 @@ describe("RoomState", function() {
|
|||||||
});
|
});
|
||||||
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
||||||
state.setStateEvents([beacon1, beacon2]);
|
state.setStateEvents([beacon1, beacon2]);
|
||||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
|
||||||
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
|
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
|
||||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||||
|
|
||||||
// update type after '''decryption'''
|
// update type after '''decryption'''
|
||||||
decryptingRelatedEvent.event.type = M_BEACON.name;
|
decryptingRelatedEvent.event.type = M_BEACON.name;
|
||||||
decryptingRelatedEvent.event.content = locationEvent.content;
|
decryptingRelatedEvent.event.content = locationEvent.event.content;
|
||||||
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted);
|
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted, decryptingRelatedEvent);
|
||||||
|
|
||||||
expect(addLocationsSpy).toHaveBeenCalledWith([decryptingRelatedEvent]);
|
expect(addLocationsSpy).toHaveBeenCalledWith([decryptingRelatedEvent]);
|
||||||
});
|
});
|
||||||
@@ -32,13 +32,14 @@ import {
|
|||||||
RoomEvent,
|
RoomEvent,
|
||||||
} from "../../src";
|
} from "../../src";
|
||||||
import { EventTimeline } from "../../src/models/event-timeline";
|
import { EventTimeline } from "../../src/models/event-timeline";
|
||||||
import { IWrappedReceipt, Room } from "../../src/models/room";
|
import { NotificationCountType, Room } from "../../src/models/room";
|
||||||
import { RoomState } from "../../src/models/room-state";
|
import { RoomState } from "../../src/models/room-state";
|
||||||
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
import { emitPromise } from "../test-utils/test-utils";
|
import { emitPromise } from "../test-utils/test-utils";
|
||||||
import { ReceiptType } from "../../src/@types/read_receipts";
|
import { ReceiptType } from "../../src/@types/read_receipts";
|
||||||
import { Thread, ThreadEvent } from "../../src/models/thread";
|
import { FeatureSupport, Thread, ThreadEvent } from "../../src/models/thread";
|
||||||
|
import { WrappedReceipt } from "../../src/models/read-receipt";
|
||||||
|
|
||||||
describe("Room", function() {
|
describe("Room", function() {
|
||||||
const roomId = "!foo:bar";
|
const roomId = "!foo:bar";
|
||||||
@@ -288,11 +289,11 @@ describe("Room", function() {
|
|||||||
room.addLiveEvents(events);
|
room.addLiveEvents(events);
|
||||||
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
|
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
|
||||||
[events[0]],
|
[events[0]],
|
||||||
{ timelineWasEmpty: undefined },
|
{ timelineWasEmpty: false },
|
||||||
);
|
);
|
||||||
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
|
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
|
||||||
[events[1]],
|
[events[1]],
|
||||||
{ timelineWasEmpty: undefined },
|
{ timelineWasEmpty: false },
|
||||||
);
|
);
|
||||||
expect(events[0].forwardLooking).toBe(true);
|
expect(events[0].forwardLooking).toBe(true);
|
||||||
expect(events[1].forwardLooking).toBe(true);
|
expect(events[1].forwardLooking).toBe(true);
|
||||||
@@ -426,6 +427,17 @@ describe("Room", function() {
|
|||||||
// but without the event ID matching we will still have the local event in pending events
|
// but without the event ID matching we will still have the local event in pending events
|
||||||
expect(room.getEventForTxnId(txnId)).toBeUndefined();
|
expect(room.getEventForTxnId(txnId)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should correctly handle remote echoes from other devices", () => {
|
||||||
|
const remoteEvent = utils.mkMessage({
|
||||||
|
room: roomId, user: userA, event: true,
|
||||||
|
});
|
||||||
|
remoteEvent.event.unsigned = { transaction_id: "TXN_ID" };
|
||||||
|
|
||||||
|
// add the remoteEvent
|
||||||
|
room.addLiveEvents([remoteEvent]);
|
||||||
|
expect(room.timeline.length).toEqual(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('addEphemeralEvents', () => {
|
describe('addEphemeralEvents', () => {
|
||||||
@@ -1419,6 +1431,19 @@ describe("Room", function() {
|
|||||||
expect(room.getUsersReadUpTo(eventToAck)).toEqual([userB]);
|
expect(room.getUsersReadUpTo(eventToAck)).toEqual([userB]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("hasUserReadUpTo", function() {
|
||||||
|
it("should acknowledge if an event has been read", function() {
|
||||||
|
const ts = 13787898424;
|
||||||
|
room.addReceipt(mkReceipt(roomId, [
|
||||||
|
mkRecord(eventToAck.getId(), "m.read", userB, ts),
|
||||||
|
]));
|
||||||
|
expect(room.hasUserReadEvent(userB, eventToAck.getId())).toEqual(true);
|
||||||
|
});
|
||||||
|
it("return false for an unknown event", function() {
|
||||||
|
expect(room.hasUserReadEvent(userB, "unknown_event")).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("tags", function() {
|
describe("tags", function() {
|
||||||
@@ -2383,7 +2408,7 @@ describe("Room", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should aggregate relations in thread event timeline set", () => {
|
it("should aggregate relations in thread event timeline set", () => {
|
||||||
Thread.setServerSideSupport(true, true);
|
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||||
const threadRoot = mkMessage();
|
const threadRoot = mkMessage();
|
||||||
const rootReaction = mkReaction(threadRoot);
|
const rootReaction = mkReaction(threadRoot);
|
||||||
const threadResponse = mkThreadResponse(threadRoot);
|
const threadResponse = mkThreadResponse(threadRoot);
|
||||||
@@ -2428,8 +2453,8 @@ describe("Room", function() {
|
|||||||
const room = new Room(roomId, client, userA);
|
const room = new Room(roomId, client, userA);
|
||||||
|
|
||||||
it("handles missing receipt type", () => {
|
it("handles missing receipt type", () => {
|
||||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
||||||
return receiptType === ReceiptType.ReadPrivate ? { eventId: "eventId" } as IWrappedReceipt : null;
|
return receiptType === ReceiptType.ReadPrivate ? { eventId: "eventId" } as WrappedReceipt : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(room.getEventReadUpTo(userA)).toEqual("eventId");
|
expect(room.getEventReadUpTo(userA)).toEqual("eventId");
|
||||||
@@ -2437,19 +2462,17 @@ describe("Room", function() {
|
|||||||
|
|
||||||
describe("prefers newer receipt", () => {
|
describe("prefers newer receipt", () => {
|
||||||
it("should compare correctly using timelines", () => {
|
it("should compare correctly using timelines", () => {
|
||||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
||||||
if (receiptType === ReceiptType.ReadPrivate) {
|
if (receiptType === ReceiptType.ReadPrivate) {
|
||||||
return { eventId: "eventId1" } as IWrappedReceipt;
|
return { eventId: "eventId1" } as WrappedReceipt;
|
||||||
}
|
|
||||||
if (receiptType === ReceiptType.UnstableReadPrivate) {
|
|
||||||
return { eventId: "eventId2" } as IWrappedReceipt;
|
|
||||||
}
|
}
|
||||||
if (receiptType === ReceiptType.Read) {
|
if (receiptType === ReceiptType.Read) {
|
||||||
return { eventId: "eventId3" } as IWrappedReceipt;
|
return { eventId: "eventId2" } as WrappedReceipt;
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 1; i <= 3; i++) {
|
for (let i = 1; i <= 2; i++) {
|
||||||
room.getUnfilteredTimelineSet = () => ({ compareEventOrdering: (event1, event2) => {
|
room.getUnfilteredTimelineSet = () => ({ compareEventOrdering: (event1, event2) => {
|
||||||
return (event1 === `eventId${i}`) ? 1 : -1;
|
return (event1 === `eventId${i}`) ? 1 : -1;
|
||||||
} } as EventTimelineSet);
|
} } as EventTimelineSet);
|
||||||
@@ -2460,20 +2483,18 @@ describe("Room", function() {
|
|||||||
|
|
||||||
describe("correctly compares by timestamp", () => {
|
describe("correctly compares by timestamp", () => {
|
||||||
it("should correctly compare, if we have all receipts", () => {
|
it("should correctly compare, if we have all receipts", () => {
|
||||||
for (let i = 1; i <= 3; i++) {
|
for (let i = 1; i <= 2; i++) {
|
||||||
room.getUnfilteredTimelineSet = () => ({
|
room.getUnfilteredTimelineSet = () => ({
|
||||||
compareEventOrdering: (_1, _2) => null,
|
compareEventOrdering: (_1, _2) => null,
|
||||||
} as EventTimelineSet);
|
} as EventTimelineSet);
|
||||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
||||||
if (receiptType === ReceiptType.ReadPrivate) {
|
if (receiptType === ReceiptType.ReadPrivate) {
|
||||||
return { eventId: "eventId1", data: { ts: i === 1 ? 1 : 0 } } as IWrappedReceipt;
|
return { eventId: "eventId1", data: { ts: i === 1 ? 2 : 1 } } as WrappedReceipt;
|
||||||
}
|
|
||||||
if (receiptType === ReceiptType.UnstableReadPrivate) {
|
|
||||||
return { eventId: "eventId2", data: { ts: i === 2 ? 1 : 0 } } as IWrappedReceipt;
|
|
||||||
}
|
}
|
||||||
if (receiptType === ReceiptType.Read) {
|
if (receiptType === ReceiptType.Read) {
|
||||||
return { eventId: "eventId3", data: { ts: i === 3 ? 1 : 0 } } as IWrappedReceipt;
|
return { eventId: "eventId2", data: { ts: i === 2 ? 2 : 1 } } as WrappedReceipt;
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(room.getEventReadUpTo(userA)).toEqual(`eventId${i}`);
|
expect(room.getEventReadUpTo(userA)).toEqual(`eventId${i}`);
|
||||||
@@ -2484,13 +2505,11 @@ describe("Room", function() {
|
|||||||
room.getUnfilteredTimelineSet = () => ({
|
room.getUnfilteredTimelineSet = () => ({
|
||||||
compareEventOrdering: (_1, _2) => null,
|
compareEventOrdering: (_1, _2) => null,
|
||||||
} as EventTimelineSet);
|
} as EventTimelineSet);
|
||||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
||||||
if (receiptType === ReceiptType.UnstableReadPrivate) {
|
|
||||||
return { eventId: "eventId1", data: { ts: 0 } } as IWrappedReceipt;
|
|
||||||
}
|
|
||||||
if (receiptType === ReceiptType.Read) {
|
if (receiptType === ReceiptType.Read) {
|
||||||
return { eventId: "eventId2", data: { ts: 1 } } as IWrappedReceipt;
|
return { eventId: "eventId2", data: { ts: 1 } } as WrappedReceipt;
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(room.getEventReadUpTo(userA)).toEqual(`eventId2`);
|
expect(room.getEventReadUpTo(userA)).toEqual(`eventId2`);
|
||||||
@@ -2505,39 +2524,25 @@ describe("Room", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should give precedence to m.read.private", () => {
|
it("should give precedence to m.read.private", () => {
|
||||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
||||||
if (receiptType === ReceiptType.ReadPrivate) {
|
if (receiptType === ReceiptType.ReadPrivate) {
|
||||||
return { eventId: "eventId1" } as IWrappedReceipt;
|
return { eventId: "eventId1" } as WrappedReceipt;
|
||||||
}
|
|
||||||
if (receiptType === ReceiptType.UnstableReadPrivate) {
|
|
||||||
return { eventId: "eventId2" } as IWrappedReceipt;
|
|
||||||
}
|
}
|
||||||
if (receiptType === ReceiptType.Read) {
|
if (receiptType === ReceiptType.Read) {
|
||||||
return { eventId: "eventId3" } as IWrappedReceipt;
|
return { eventId: "eventId2" } as WrappedReceipt;
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(room.getEventReadUpTo(userA)).toEqual(`eventId1`);
|
expect(room.getEventReadUpTo(userA)).toEqual(`eventId1`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should give precedence to org.matrix.msc2285.read.private", () => {
|
|
||||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
|
||||||
if (receiptType === ReceiptType.UnstableReadPrivate) {
|
|
||||||
return { eventId: "eventId2" } as IWrappedReceipt;
|
|
||||||
}
|
|
||||||
if (receiptType === ReceiptType.Read) {
|
|
||||||
return { eventId: "eventId2" } as IWrappedReceipt;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(room.getEventReadUpTo(userA)).toEqual(`eventId2`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should give precedence to m.read", () => {
|
it("should give precedence to m.read", () => {
|
||||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
||||||
if (receiptType === ReceiptType.Read) {
|
if (receiptType === ReceiptType.Read) {
|
||||||
return { eventId: "eventId3" } as IWrappedReceipt;
|
return { eventId: "eventId3" } as WrappedReceipt;
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(room.getEventReadUpTo(userA)).toEqual(`eventId3`);
|
expect(room.getEventReadUpTo(userA)).toEqual(`eventId3`);
|
||||||
@@ -2557,4 +2562,40 @@ describe("Room", function() {
|
|||||||
expect(client.roomNameGenerator).toHaveBeenCalled();
|
expect(client.roomNameGenerator).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("thread notifications", () => {
|
||||||
|
let room;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const client = new TestClient(userA).client;
|
||||||
|
room = new Room(roomId, client, userA);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults to undefined", () => {
|
||||||
|
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBeUndefined();
|
||||||
|
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets you set values", () => {
|
||||||
|
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 1);
|
||||||
|
|
||||||
|
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(1);
|
||||||
|
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBeUndefined();
|
||||||
|
|
||||||
|
room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 10);
|
||||||
|
|
||||||
|
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(1);
|
||||||
|
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets you reset threads notifications", () => {
|
||||||
|
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666);
|
||||||
|
room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 123);
|
||||||
|
|
||||||
|
room.resetThreadUnreadNotificationCount();
|
||||||
|
|
||||||
|
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBeUndefined();
|
||||||
|
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import 'jest-localstorage-mock';
|
|||||||
import { IndexedDBStore, IStateEventWithRoomId, MemoryStore } from "../../../src";
|
import { IndexedDBStore, IStateEventWithRoomId, MemoryStore } from "../../../src";
|
||||||
import { emitPromise } from "../../test-utils/test-utils";
|
import { emitPromise } from "../../test-utils/test-utils";
|
||||||
import { LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend";
|
import { LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend";
|
||||||
|
import { defer } from "../../../src/utils";
|
||||||
|
|
||||||
describe("IndexedDBStore", () => {
|
describe("IndexedDBStore", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -111,4 +112,57 @@ describe("IndexedDBStore", () => {
|
|||||||
await store.setPendingEvents(roomId, []);
|
await store.setPendingEvents(roomId, []);
|
||||||
expect(localStorage.getItem("mx_pending_events_" + roomId)).toBeNull();
|
expect(localStorage.getItem("mx_pending_events_" + roomId)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should resolve isNewlyCreated to true if no database existed initially", async () => {
|
||||||
|
const store = new IndexedDBStore({
|
||||||
|
indexedDB,
|
||||||
|
dbName: "db1",
|
||||||
|
localStorage,
|
||||||
|
});
|
||||||
|
await store.startup();
|
||||||
|
|
||||||
|
await expect(store.isNewlyCreated()).resolves.toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should resolve isNewlyCreated to false if database existed already", async () => {
|
||||||
|
let store = new IndexedDBStore({
|
||||||
|
indexedDB,
|
||||||
|
dbName: "db2",
|
||||||
|
localStorage,
|
||||||
|
});
|
||||||
|
await store.startup();
|
||||||
|
|
||||||
|
store = new IndexedDBStore({
|
||||||
|
indexedDB,
|
||||||
|
dbName: "db2",
|
||||||
|
localStorage,
|
||||||
|
});
|
||||||
|
await store.startup();
|
||||||
|
|
||||||
|
await expect(store.isNewlyCreated()).resolves.toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should resolve isNewlyCreated to false if database existed already but needs upgrade", async () => {
|
||||||
|
const deferred = defer<Event>();
|
||||||
|
// seed db3 to Version 1 so it forces a migration
|
||||||
|
const req = indexedDB.open("matrix-js-sdk:db3", 1);
|
||||||
|
req.onupgradeneeded = () => {
|
||||||
|
const db = req.result;
|
||||||
|
db.createObjectStore("users", { keyPath: ["userId"] });
|
||||||
|
db.createObjectStore("accountData", { keyPath: ["type"] });
|
||||||
|
db.createObjectStore("sync", { keyPath: ["clobber"] });
|
||||||
|
};
|
||||||
|
req.onsuccess = deferred.resolve;
|
||||||
|
await deferred.promise;
|
||||||
|
req.result.close();
|
||||||
|
|
||||||
|
const store = new IndexedDBStore({
|
||||||
|
indexedDB,
|
||||||
|
dbName: "db3",
|
||||||
|
localStorage,
|
||||||
|
});
|
||||||
|
await store.startup();
|
||||||
|
|
||||||
|
await expect(store.isNewlyCreated()).resolves.toBeFalsy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ const RES_WITH_AGE = {
|
|||||||
account_data: { events: [] },
|
account_data: { events: [] },
|
||||||
ephemeral: { events: [] },
|
ephemeral: { events: [] },
|
||||||
unread_notifications: {},
|
unread_notifications: {},
|
||||||
|
unread_thread_notifications: {
|
||||||
|
"$143273582443PhrSn:example.org": {
|
||||||
|
highlight_count: 0,
|
||||||
|
notification_count: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
timeline: {
|
timeline: {
|
||||||
events: [
|
events: [
|
||||||
Object.freeze({
|
Object.freeze({
|
||||||
@@ -302,9 +308,6 @@ describe("SyncAccumulator", function() {
|
|||||||
[ReceiptType.ReadPrivate]: {
|
[ReceiptType.ReadPrivate]: {
|
||||||
"@dan:localhost": { ts: 4 },
|
"@dan:localhost": { ts: 4 },
|
||||||
},
|
},
|
||||||
[ReceiptType.UnstableReadPrivate]: {
|
|
||||||
"@matthew:localhost": { ts: 5 },
|
|
||||||
},
|
|
||||||
"some.other.receipt.type": {
|
"some.other.receipt.type": {
|
||||||
"@should_be_ignored:localhost": { key: "val" },
|
"@should_be_ignored:localhost": { key: "val" },
|
||||||
},
|
},
|
||||||
@@ -350,9 +353,6 @@ describe("SyncAccumulator", function() {
|
|||||||
[ReceiptType.ReadPrivate]: {
|
[ReceiptType.ReadPrivate]: {
|
||||||
"@dan:localhost": { ts: 4 },
|
"@dan:localhost": { ts: 4 },
|
||||||
},
|
},
|
||||||
[ReceiptType.UnstableReadPrivate]: {
|
|
||||||
"@matthew:localhost": { ts: 5 },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"$event2:localhost": {
|
"$event2:localhost": {
|
||||||
[ReceiptType.Read]: {
|
[ReceiptType.Read]: {
|
||||||
@@ -445,6 +445,13 @@ describe("SyncAccumulator", function() {
|
|||||||
Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]),
|
Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should retrieve unread thread notifications", () => {
|
||||||
|
sa.accumulate(RES_WITH_AGE);
|
||||||
|
const output = sa.getJSON();
|
||||||
|
expect(output.roomsData.join["!foo:bar"]
|
||||||
|
.unread_thread_notifications["$143273582443PhrSn:example.org"]).not.toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -152,6 +152,9 @@ describe("utils", function() {
|
|||||||
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 2 }));
|
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 2 }));
|
||||||
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { b: 2, a: 1 }));
|
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { b: 2, a: 1 }));
|
||||||
assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 3 }));
|
assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 3 }));
|
||||||
|
assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1 }));
|
||||||
|
assert.isFalse(utils.deepCompare({ a: 1 }, { a: 1, b: 2 }));
|
||||||
|
assert.isFalse(utils.deepCompare({ a: 1 }, { b: 1 }));
|
||||||
|
|
||||||
assert.isTrue(utils.deepCompare({
|
assert.isTrue(utils.deepCompare({
|
||||||
1: { name: "mhc", age: 28 },
|
1: { name: "mhc", age: 28 },
|
||||||
@@ -525,38 +528,6 @@ describe("utils", function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getPrivateReadReceiptField', () => {
|
|
||||||
it('should return m.read.private if server supports stable', async () => {
|
|
||||||
expect(await utils.getPrivateReadReceiptField({
|
|
||||||
doesServerSupportUnstableFeature: jest.fn().mockImplementation((feature) => {
|
|
||||||
return feature === "org.matrix.msc2285.stable";
|
|
||||||
}),
|
|
||||||
} as any)).toBe(ReceiptType.ReadPrivate);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return m.read.private if server supports stable and unstable', async () => {
|
|
||||||
expect(await utils.getPrivateReadReceiptField({
|
|
||||||
doesServerSupportUnstableFeature: jest.fn().mockImplementation((feature) => {
|
|
||||||
return ["org.matrix.msc2285.stable", "org.matrix.msc2285"].includes(feature);
|
|
||||||
}),
|
|
||||||
} as any)).toBe(ReceiptType.ReadPrivate);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return org.matrix.msc2285.read.private if server supports unstable', async () => {
|
|
||||||
expect(await utils.getPrivateReadReceiptField({
|
|
||||||
doesServerSupportUnstableFeature: jest.fn().mockImplementation((feature) => {
|
|
||||||
return feature === "org.matrix.msc2285";
|
|
||||||
}),
|
|
||||||
} as any)).toBe(ReceiptType.UnstableReadPrivate);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return none if server does not support either', async () => {
|
|
||||||
expect(await utils.getPrivateReadReceiptField({
|
|
||||||
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
|
|
||||||
} as any)).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isSupportedReceiptType', () => {
|
describe('isSupportedReceiptType', () => {
|
||||||
it('should support m.read', () => {
|
it('should support m.read', () => {
|
||||||
expect(utils.isSupportedReceiptType(ReceiptType.Read)).toBeTruthy();
|
expect(utils.isSupportedReceiptType(ReceiptType.Read)).toBeTruthy();
|
||||||
@@ -566,10 +537,6 @@ describe("utils", function() {
|
|||||||
expect(utils.isSupportedReceiptType(ReceiptType.ReadPrivate)).toBeTruthy();
|
expect(utils.isSupportedReceiptType(ReceiptType.ReadPrivate)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support org.matrix.msc2285.read.private', () => {
|
|
||||||
expect(utils.isSupportedReceiptType(ReceiptType.UnstableReadPrivate)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not support other receipt types', () => {
|
it('should not support other receipt types', () => {
|
||||||
expect(utils.isSupportedReceiptType("this is a receipt type")).toBeFalsy();
|
expect(utils.isSupportedReceiptType("this is a receipt type")).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -156,9 +156,13 @@ export interface IPusher {
|
|||||||
lang: string;
|
lang: string;
|
||||||
profile_tag?: string;
|
profile_tag?: string;
|
||||||
pushkey: string;
|
pushkey: string;
|
||||||
|
enabled?: boolean | null | undefined;
|
||||||
|
"org.matrix.msc3881.enabled"?: boolean | null | undefined;
|
||||||
|
device_id?: string | null;
|
||||||
|
"org.matrix.msc3881.device_id"?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPusherRequest extends IPusher {
|
export interface IPusherRequest extends Omit<IPusher, "device_id" | "org.matrix.msc3881.device_id"> {
|
||||||
append?: boolean;
|
append?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { UnstableValue } from "../NamespacedValue";
|
||||||
|
|
||||||
// disable lint because these are wire responses
|
// disable lint because these are wire responses
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
@@ -27,3 +29,89 @@ export interface IRefreshTokenResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response to GET login flows as per https://spec.matrix.org/v1.3/client-server-api/#get_matrixclientv3login
|
||||||
|
*/
|
||||||
|
export interface ILoginFlowsResponse {
|
||||||
|
flows: LoginFlow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoginFlow = ISSOFlow | IPasswordFlow | ILoginFlow;
|
||||||
|
|
||||||
|
export interface ILoginFlow {
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPasswordFlow extends ILoginFlow {
|
||||||
|
type: "m.login.password";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DELEGATED_OIDC_COMPATIBILITY = new UnstableValue(
|
||||||
|
"delegated_oidc_compatibility",
|
||||||
|
"org.matrix.msc3824.delegated_oidc_compatibility",
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of SSO flow as per https://spec.matrix.org/v1.3/client-server-api/#client-login-via-sso
|
||||||
|
*/
|
||||||
|
export interface ISSOFlow extends ILoginFlow {
|
||||||
|
type: "m.login.sso" | "m.login.cas";
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
identity_providers?: IIdentityProvider[];
|
||||||
|
[DELEGATED_OIDC_COMPATIBILITY.name]?: boolean;
|
||||||
|
[DELEGATED_OIDC_COMPATIBILITY.altName]?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum IdentityProviderBrand {
|
||||||
|
Gitlab = "gitlab",
|
||||||
|
Github = "github",
|
||||||
|
Apple = "apple",
|
||||||
|
Google = "google",
|
||||||
|
Facebook = "facebook",
|
||||||
|
Twitter = "twitter",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IIdentityProvider {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon?: string;
|
||||||
|
brand?: IdentityProviderBrand | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters to login request as per https://spec.matrix.org/v1.3/client-server-api/#login
|
||||||
|
*/
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
export interface ILoginParams {
|
||||||
|
identifier?: object;
|
||||||
|
password?: string;
|
||||||
|
token?: string;
|
||||||
|
device_id?: string;
|
||||||
|
initial_device_display_name?: string;
|
||||||
|
}
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
export enum SSOAction {
|
||||||
|
/** The user intends to login to an existing account */
|
||||||
|
LOGIN = "login",
|
||||||
|
|
||||||
|
/** The user intends to register for a new account */
|
||||||
|
REGISTER = "register",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of a successful [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882)
|
||||||
|
* `m.login.token` issuance request.
|
||||||
|
* Note that this is UNSTABLE and subject to breaking changes without notice.
|
||||||
|
*/
|
||||||
|
export interface LoginTokenPostResponse {
|
||||||
|
/**
|
||||||
|
* The token to use with `m.login.token` to authenticate.
|
||||||
|
*/
|
||||||
|
login_token: string;
|
||||||
|
/**
|
||||||
|
* Expiration in seconds.
|
||||||
|
*/
|
||||||
|
expires_in: number;
|
||||||
|
}
|
||||||
|
|||||||
20
src/@types/crypto.ts
Normal file
20
src/@types/crypto.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type OlmGroupSessionExtraData = {
|
||||||
|
untrusted?: boolean;
|
||||||
|
sharedHistory?: boolean;
|
||||||
|
};
|
||||||
@@ -191,6 +191,33 @@ export const EVENT_VISIBILITY_CHANGE_TYPE = new UnstableValue(
|
|||||||
"m.visibility",
|
"m.visibility",
|
||||||
"org.matrix.msc3531.visibility");
|
"org.matrix.msc3531.visibility");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/matrix-org/matrix-doc/pull/3881
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export const PUSHER_ENABLED = new UnstableValue(
|
||||||
|
"enabled",
|
||||||
|
"org.matrix.msc3881.enabled");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/matrix-org/matrix-doc/pull/3881
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export const PUSHER_DEVICE_ID = new UnstableValue(
|
||||||
|
"device_id",
|
||||||
|
"org.matrix.msc3881.device_id");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/matrix-org/matrix-doc/pull/3890
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export const LOCAL_NOTIFICATION_SETTINGS_PREFIX = new UnstableValue(
|
||||||
|
"m.local_notification_settings",
|
||||||
|
"org.matrix.msc3890.local_notification_settings");
|
||||||
|
|
||||||
export interface IEncryptedFile {
|
export interface IEncryptedFile {
|
||||||
url: string;
|
url: string;
|
||||||
mimetype?: string;
|
mimetype?: string;
|
||||||
|
|||||||
19
src/@types/local_notifications.ts
Normal file
19
src/@types/local_notifications.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface LocalNotificationSettings {
|
||||||
|
is_silenced: boolean;
|
||||||
|
}
|
||||||
@@ -18,8 +18,4 @@ export enum ReceiptType {
|
|||||||
Read = "m.read",
|
Read = "m.read",
|
||||||
FullyRead = "m.fully_read",
|
FullyRead = "m.fully_read",
|
||||||
ReadPrivate = "m.read.private",
|
ReadPrivate = "m.read.private",
|
||||||
/**
|
|
||||||
* @deprecated Please use the ReadPrivate type when possible. This value may be removed at any time without notice.
|
|
||||||
*/
|
|
||||||
UnstableReadPrivate = "org.matrix.msc2285.read.private",
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { IRoomEventFilter } from "../filter";
|
|||||||
import { Direction } from "../models/event-timeline";
|
import { Direction } from "../models/event-timeline";
|
||||||
import { PushRuleAction } from "./PushRules";
|
import { PushRuleAction } from "./PushRules";
|
||||||
import { IRoomEvent } from "../sync-accumulator";
|
import { IRoomEvent } from "../sync-accumulator";
|
||||||
import { RoomType } from "./event";
|
import { EventType, RoomType } from "./event";
|
||||||
|
|
||||||
// allow camelcase as these are things that go onto the wire
|
// allow camelcase as these are things that go onto the wire
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
@@ -98,7 +98,18 @@ export interface ICreateRoomOpts {
|
|||||||
name?: string;
|
name?: string;
|
||||||
topic?: string;
|
topic?: string;
|
||||||
preset?: Preset;
|
preset?: Preset;
|
||||||
power_level_content_override?: object;
|
power_level_content_override?: {
|
||||||
|
ban?: number;
|
||||||
|
events?: Record<EventType | string, number>;
|
||||||
|
events_default?: number;
|
||||||
|
invite?: number;
|
||||||
|
kick?: number;
|
||||||
|
notifications?: Record<string, number>;
|
||||||
|
redact?: number;
|
||||||
|
state_default?: number;
|
||||||
|
users?: Record<string, number>;
|
||||||
|
users_default?: number;
|
||||||
|
};
|
||||||
creation_content?: object;
|
creation_content?: object;
|
||||||
initial_state?: ICreateRoomStateEvent[];
|
initial_state?: ICreateRoomStateEvent[];
|
||||||
invite?: string[];
|
invite?: string[];
|
||||||
@@ -149,7 +160,7 @@ export interface IRelationsRequestOpts {
|
|||||||
from?: string;
|
from?: string;
|
||||||
to?: string;
|
to?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
direction?: Direction;
|
dir?: Direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRelationsResponse {
|
export interface IRelationsResponse {
|
||||||
|
|||||||
26
src/@types/sync.ts
Normal file
26
src/@types/sync.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 { ServerControlledNamespacedValue } from "../NamespacedValue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/matrix-org/matrix-doc/pull/3773
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export const UNREAD_THREAD_NOTIFICATIONS = new ServerControlledNamespacedValue(
|
||||||
|
"unread_thread_notifications",
|
||||||
|
"org.matrix.msc3773.unread_thread_notifications");
|
||||||
29
src/@types/uia.ts
Normal file
29
src/@types/uia.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 { IAuthData } from "../interactive-auth";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper type to represent HTTP request body for a UIA enabled endpoint
|
||||||
|
*/
|
||||||
|
export type UIARequest<T> = T & {
|
||||||
|
auth?: IAuthData;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper type to represent HTTP response body for a UIA enabled endpoint
|
||||||
|
*/
|
||||||
|
export type UIAResponse<T> = T | IAuthData;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Optional } from "matrix-events-sdk/lib/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a simple Matrix namespaced value. This will assume that if a stable prefix
|
* Represents a simple Matrix namespaced value. This will assume that if a stable prefix
|
||||||
* is provided that the stable prefix should be used when representing the identifier.
|
* is provided that the stable prefix should be used when representing the identifier.
|
||||||
@@ -41,13 +43,20 @@ export class NamespacedValue<S extends string, U extends string> {
|
|||||||
return this.unstable;
|
return this.unstable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get names(): (U | S)[] {
|
||||||
|
const names = [this.name];
|
||||||
|
const altName = this.altName;
|
||||||
|
if (altName) names.push(altName);
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
public matches(val: string): boolean {
|
public matches(val: string): boolean {
|
||||||
return this.name === val || this.altName === val;
|
return this.name === val || this.altName === val;
|
||||||
}
|
}
|
||||||
|
|
||||||
// this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class
|
// this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class
|
||||||
// so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace.
|
// so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace.
|
||||||
public findIn<T>(obj: any): T {
|
public findIn<T>(obj: any): Optional<T> {
|
||||||
let val: T;
|
let val: T;
|
||||||
if (this.name) {
|
if (this.name) {
|
||||||
val = obj?.[this.name];
|
val = obj?.[this.name];
|
||||||
|
|||||||
523
src/client.ts
523
src/client.ts
@@ -19,7 +19,7 @@ limitations under the License.
|
|||||||
* @module client
|
* @module client
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent } from "matrix-events-sdk";
|
import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent, Optional } from "matrix-events-sdk";
|
||||||
|
|
||||||
import { ISyncStateData, SyncApi, SyncState } from "./sync";
|
import { ISyncStateData, SyncApi, SyncState } from "./sync";
|
||||||
import {
|
import {
|
||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
} from "./models/event";
|
} from "./models/event";
|
||||||
import { StubStore } from "./store/stub";
|
import { StubStore } from "./store/stub";
|
||||||
import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, supportsMatrixCall } from "./webrtc/call";
|
import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, supportsMatrixCall } from "./webrtc/call";
|
||||||
import { Filter, IFilterDefinition } from "./filter";
|
import { Filter, IFilterDefinition, IRoomEventFilter } from "./filter";
|
||||||
import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler';
|
import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler';
|
||||||
import * as utils from './utils';
|
import * as utils from './utils';
|
||||||
import { sleep } from './utils';
|
import { sleep } from './utils';
|
||||||
@@ -158,7 +158,9 @@ import {
|
|||||||
} from "./@types/requests";
|
} from "./@types/requests";
|
||||||
import {
|
import {
|
||||||
EventType,
|
EventType,
|
||||||
|
LOCAL_NOTIFICATION_SETTINGS_PREFIX,
|
||||||
MsgType,
|
MsgType,
|
||||||
|
PUSHER_ENABLED,
|
||||||
RelationType,
|
RelationType,
|
||||||
RoomCreateTypeField,
|
RoomCreateTypeField,
|
||||||
RoomType,
|
RoomType,
|
||||||
@@ -188,15 +190,22 @@ import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, Rule
|
|||||||
import { IThreepid } from "./@types/threepids";
|
import { IThreepid } from "./@types/threepids";
|
||||||
import { CryptoStore } from "./crypto/store/base";
|
import { CryptoStore } from "./crypto/store/base";
|
||||||
import { MediaHandler } from "./webrtc/mediaHandler";
|
import { MediaHandler } from "./webrtc/mediaHandler";
|
||||||
import { IRefreshTokenResponse } from "./@types/auth";
|
import { LoginTokenPostResponse, ILoginFlowsResponse, IRefreshTokenResponse, SSOAction } from "./@types/auth";
|
||||||
import { TypedEventEmitter } from "./models/typed-event-emitter";
|
import { TypedEventEmitter } from "./models/typed-event-emitter";
|
||||||
import { ReceiptType } from "./@types/read_receipts";
|
import { ReceiptType } from "./@types/read_receipts";
|
||||||
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
|
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
|
||||||
import { SlidingSyncSdk } from "./sliding-sync-sdk";
|
import { SlidingSyncSdk } from "./sliding-sync-sdk";
|
||||||
import { Thread, THREAD_RELATION_TYPE } from "./models/thread";
|
import { FeatureSupport, Thread, THREAD_RELATION_TYPE, determineFeatureSupport } from "./models/thread";
|
||||||
import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
|
import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
|
||||||
|
import { UnstableValue } from "./NamespacedValue";
|
||||||
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
|
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
|
||||||
import { ToDeviceBatch } from "./models/ToDeviceMessage";
|
import { ToDeviceBatch } from "./models/ToDeviceMessage";
|
||||||
|
import { MAIN_ROOM_TIMELINE } from "./models/read-receipt";
|
||||||
|
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";
|
||||||
|
|
||||||
export type Store = IStore;
|
export type Store = IStore;
|
||||||
|
|
||||||
@@ -208,6 +217,11 @@ export const CRYPTO_ENABLED: boolean = isCryptoAvailable();
|
|||||||
const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
|
const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
|
||||||
const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes
|
const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes
|
||||||
|
|
||||||
|
export const UNSTABLE_MSC3852_LAST_SEEN_UA = new UnstableValue(
|
||||||
|
"last_seen_user_agent",
|
||||||
|
"org.matrix.msc3852.last_seen_user_agent",
|
||||||
|
);
|
||||||
|
|
||||||
interface IExportedDevice {
|
interface IExportedDevice {
|
||||||
olmDevice: IExportedOlmDevice;
|
olmDevice: IExportedOlmDevice;
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -396,8 +410,7 @@ export interface IStartClientOpts {
|
|||||||
pollTimeout?: number;
|
pollTimeout?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The filter to apply to /sync calls. This will override the opts.initialSyncLimit, which would
|
* The filter to apply to /sync calls.
|
||||||
* normally result in a timeline limit filter.
|
|
||||||
*/
|
*/
|
||||||
filter?: Filter;
|
filter?: Filter;
|
||||||
|
|
||||||
@@ -517,15 +530,21 @@ export interface ITurnServer {
|
|||||||
credential: string;
|
credential: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IServerVersions {
|
export interface IServerVersions {
|
||||||
versions: string[];
|
versions: string[];
|
||||||
unstable_features: Record<string, boolean>;
|
unstable_features: Record<string, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const M_AUTHENTICATION = new UnstableValue(
|
||||||
|
"m.authentication",
|
||||||
|
"org.matrix.msc2965.authentication",
|
||||||
|
);
|
||||||
|
|
||||||
export interface IClientWellKnown {
|
export interface IClientWellKnown {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
"m.homeserver"?: IWellKnownConfig;
|
"m.homeserver"?: IWellKnownConfig;
|
||||||
"m.identity_server"?: IWellKnownConfig;
|
"m.identity_server"?: IWellKnownConfig;
|
||||||
|
[M_AUTHENTICATION.name]?: IDelegatedAuthConfig; // MSC2965
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWellKnownConfig {
|
export interface IWellKnownConfig {
|
||||||
@@ -537,6 +556,13 @@ export interface IWellKnownConfig {
|
|||||||
base_url?: string | null;
|
base_url?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IDelegatedAuthConfig { // MSC2965
|
||||||
|
/** The OIDC Provider/issuer the client should use */
|
||||||
|
issuer: string;
|
||||||
|
/** The optional URL of the web UI where the user can manage their account */
|
||||||
|
account?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface IKeyBackupPath {
|
interface IKeyBackupPath {
|
||||||
path: string;
|
path: string;
|
||||||
queryData?: {
|
queryData?: {
|
||||||
@@ -572,6 +598,13 @@ interface IMessagesResponse {
|
|||||||
state: IStateEvent[];
|
state: IStateEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IThreadedMessagesResponse {
|
||||||
|
prev_batch: string;
|
||||||
|
next_batch: string;
|
||||||
|
chunk: IRoomEvent[];
|
||||||
|
state: IStateEvent[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface IRequestTokenResponse {
|
export interface IRequestTokenResponse {
|
||||||
sid: string;
|
sid: string;
|
||||||
submit_url?: string;
|
submit_url?: string;
|
||||||
@@ -669,6 +702,8 @@ export interface IMyDevice {
|
|||||||
display_name?: string;
|
display_name?: string;
|
||||||
last_seen_ip?: string;
|
last_seen_ip?: string;
|
||||||
last_seen_ts?: number;
|
last_seen_ts?: number;
|
||||||
|
[UNSTABLE_MSC3852_LAST_SEEN_UA.stable]?: string;
|
||||||
|
[UNSTABLE_MSC3852_LAST_SEEN_UA.unstable]?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDownloadKeyResult {
|
export interface IDownloadKeyResult {
|
||||||
@@ -846,7 +881,7 @@ type UserEvents = UserEvent.AvatarUrl
|
|||||||
| UserEvent.CurrentlyActive
|
| UserEvent.CurrentlyActive
|
||||||
| UserEvent.LastPresenceTs;
|
| UserEvent.LastPresenceTs;
|
||||||
|
|
||||||
type EmittedEvents = ClientEvent
|
export type EmittedEvents = ClientEvent
|
||||||
| RoomEvents
|
| RoomEvents
|
||||||
| RoomStateEvents
|
| RoomStateEvents
|
||||||
| CryptoEvents
|
| CryptoEvents
|
||||||
@@ -881,6 +916,8 @@ export type ClientEventHandlerMap = {
|
|||||||
& HttpApiEventHandlerMap
|
& HttpApiEventHandlerMap
|
||||||
& BeaconEventHandlerMap;
|
& BeaconEventHandlerMap;
|
||||||
|
|
||||||
|
const SSO_ACTION_PARAM = new UnstableValue("action", "org.matrix.msc3824.action");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a Matrix Client. Only directly construct this if you want to use
|
* Represents a Matrix Client. Only directly construct this if you want to use
|
||||||
* custom modules. Normally, {@link createClient} should be used
|
* custom modules. Normally, {@link createClient} should be used
|
||||||
@@ -902,7 +939,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
public urlPreviewCache: { [key: string]: Promise<IPreviewUrlResponse> } = {};
|
public urlPreviewCache: { [key: string]: Promise<IPreviewUrlResponse> } = {};
|
||||||
public identityServer: IIdentityServerProvider;
|
public identityServer: IIdentityServerProvider;
|
||||||
public http: MatrixHttpApi; // XXX: Intended private, used in code.
|
public http: MatrixHttpApi; // XXX: Intended private, used in code.
|
||||||
public crypto: Crypto; // XXX: Intended private, used in code.
|
public crypto?: Crypto; // XXX: Intended private, used in code.
|
||||||
public cryptoCallbacks: ICryptoCallbacks; // XXX: Intended private, used in code.
|
public cryptoCallbacks: ICryptoCallbacks; // XXX: Intended private, used in code.
|
||||||
public callEventHandler: CallEventHandler; // XXX: Intended private, used in code.
|
public callEventHandler: CallEventHandler; // XXX: Intended private, used in code.
|
||||||
public supportsCallTransfer = false; // XXX: Intended private, used in code.
|
public supportsCallTransfer = false; // XXX: Intended private, used in code.
|
||||||
@@ -932,6 +969,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
protected clientWellKnownIntervalID: ReturnType<typeof setInterval>;
|
protected clientWellKnownIntervalID: ReturnType<typeof setInterval>;
|
||||||
protected canResetTimelineCallback: ResetTimelineCallback;
|
protected canResetTimelineCallback: ResetTimelineCallback;
|
||||||
|
|
||||||
|
public canSupport = new Map<Feature, ServerSupport>();
|
||||||
|
|
||||||
// The pushprocessor caches useful things, so keep one and re-use it
|
// The pushprocessor caches useful things, so keep one and re-use it
|
||||||
protected pushProcessor = new PushProcessor(this);
|
protected pushProcessor = new PushProcessor(this);
|
||||||
|
|
||||||
@@ -955,6 +994,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
|
|
||||||
private toDeviceMessageQueue: ToDeviceMessageQueue;
|
private toDeviceMessageQueue: ToDeviceMessageQueue;
|
||||||
|
|
||||||
|
// A manager for determining which invites should be ignored.
|
||||||
|
public readonly ignoredInvites: IgnoredInvites;
|
||||||
|
|
||||||
constructor(opts: IMatrixClientCreateOpts) {
|
constructor(opts: IMatrixClientCreateOpts) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@@ -1057,35 +1099,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
// We do this so that push rules are correctly executed on events in their decrypted
|
// We do this so that push rules are correctly executed on events in their decrypted
|
||||||
// state, such as highlights when the user's name is mentioned.
|
// state, such as highlights when the user's name is mentioned.
|
||||||
this.on(MatrixEventEvent.Decrypted, (event) => {
|
this.on(MatrixEventEvent.Decrypted, (event) => {
|
||||||
const oldActions = event.getPushActions();
|
fixNotificationCountOnDecryption(this, event);
|
||||||
const actions = this.getPushActionsForEvent(event, true);
|
|
||||||
|
|
||||||
const room = this.getRoom(event.getRoomId());
|
|
||||||
if (!room) return;
|
|
||||||
|
|
||||||
const currentCount = room.getUnreadNotificationCount(NotificationCountType.Highlight);
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
// TODO: Handle mentions received while the client is offline
|
|
||||||
// See also https://github.com/vector-im/element-web/issues/9069
|
|
||||||
if (!room.hasUserReadEvent(this.getUserId(), event.getId())) {
|
|
||||||
let newCount = currentCount;
|
|
||||||
if (newHighlight && !oldHighlight) newCount++;
|
|
||||||
if (!newHighlight && oldHighlight) newCount--;
|
|
||||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, newCount);
|
|
||||||
|
|
||||||
// Fix 'Mentions Only' rooms from not having the right badge count
|
|
||||||
const totalCount = room.getUnreadNotificationCount(NotificationCountType.Total);
|
|
||||||
if (totalCount < newCount) {
|
|
||||||
room.setUnreadNotificationCount(NotificationCountType.Total, newCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Like above, we have to listen for read receipts from ourselves in order to
|
// Like above, we have to listen for read receipts from ourselves in order to
|
||||||
@@ -1136,6 +1150,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, highlightCount);
|
room.setUnreadNotificationCount(NotificationCountType.Highlight, highlightCount);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.ignoredInvites = new IgnoredInvites(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1185,14 +1201,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
this.syncApi.stop();
|
this.syncApi.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const serverVersions = await this.getVersions();
|
||||||
const { serverSupport, stable } = await this.doesServerSupportThread();
|
this.canSupport = await buildFeatureSupportMap(serverVersions);
|
||||||
Thread.setServerSideSupport(serverSupport, stable);
|
|
||||||
} catch (e) {
|
const support = this.canSupport.get(Feature.ThreadUnreadNotifications);
|
||||||
// Most likely cause is that `doesServerSupportThread` returned `null` (as it
|
UNREAD_THREAD_NOTIFICATIONS.setPreferUnstable(support === ServerSupport.Unstable);
|
||||||
// is allowed to do) and thus we enter "degraded mode" on threads.
|
|
||||||
Thread.setServerSideSupport(false, true);
|
const { threads, list } = await this.doesServerSupportThread();
|
||||||
}
|
Thread.setServerSideSupport(threads);
|
||||||
|
Thread.setServerSideListSupport(list);
|
||||||
|
|
||||||
// shallow-copy the opts dict before modifying and storing it
|
// shallow-copy the opts dict before modifying and storing it
|
||||||
this.clientOpts = Object.assign({}, opts) as IStoredClientOpts;
|
this.clientOpts = Object.assign({}, opts) as IStoredClientOpts;
|
||||||
@@ -2667,7 +2684,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
try {
|
try {
|
||||||
res = await this.http.authedRequest<IKeyBackupInfo>(
|
res = await this.http.authedRequest<IKeyBackupInfo>(
|
||||||
undefined, Method.Get, "/room_keys/version", undefined, undefined,
|
undefined, Method.Get, "/room_keys/version", undefined, undefined,
|
||||||
{ prefix: PREFIX_UNSTABLE },
|
{ prefix: PREFIX_V3 },
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.errcode === 'M_NOT_FOUND') {
|
if (e.errcode === 'M_NOT_FOUND') {
|
||||||
@@ -2823,7 +2840,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
|
|
||||||
const res = await this.http.authedRequest<IKeyBackupInfo>(
|
const res = await this.http.authedRequest<IKeyBackupInfo>(
|
||||||
undefined, Method.Post, "/room_keys/version", undefined, data,
|
undefined, Method.Post, "/room_keys/version", undefined, data,
|
||||||
{ prefix: PREFIX_UNSTABLE },
|
{ prefix: PREFIX_V3 },
|
||||||
);
|
);
|
||||||
|
|
||||||
// We could assume everything's okay and enable directly, but this ensures
|
// We could assume everything's okay and enable directly, but this ensures
|
||||||
@@ -2855,7 +2872,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
|
|
||||||
return this.http.authedRequest(
|
return this.http.authedRequest(
|
||||||
undefined, Method.Delete, path, undefined, undefined,
|
undefined, Method.Delete, path, undefined, undefined,
|
||||||
{ prefix: PREFIX_UNSTABLE },
|
{ prefix: PREFIX_V3 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3324,7 +3341,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* @param {string} roomId The room ID
|
* @param {string} roomId The room ID
|
||||||
* @return {Room|null} The Room or null if it doesn't exist or there is no data store.
|
* @return {Room|null} The Room or null if it doesn't exist or there is no data store.
|
||||||
*/
|
*/
|
||||||
public getRoom(roomId: string): Room | null {
|
public getRoom(roomId: string | undefined): Room | null {
|
||||||
|
if (!roomId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return this.store.getRoom(roomId);
|
return this.store.getRoom(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3413,7 +3433,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* @param {string} eventType The event type
|
* @param {string} eventType The event type
|
||||||
* @return {?object} The contents of the given account data event
|
* @return {?object} The contents of the given account data event
|
||||||
*/
|
*/
|
||||||
public getAccountData(eventType: string): MatrixEvent {
|
public getAccountData(eventType: string): MatrixEvent | undefined {
|
||||||
return this.store.getAccountData(eventType);
|
return this.store.getAccountData(eventType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4582,7 +4602,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* @return {Promise} Resolves: to an empty object {}
|
* @return {Promise} Resolves: to an empty object {}
|
||||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||||
*/
|
*/
|
||||||
public sendReceipt(event: MatrixEvent, receiptType: ReceiptType, body: any, callback?: Callback): Promise<{}> {
|
public async sendReceipt(
|
||||||
|
event: MatrixEvent,
|
||||||
|
receiptType: ReceiptType,
|
||||||
|
body: any,
|
||||||
|
callback?: Callback,
|
||||||
|
): Promise<{}> {
|
||||||
if (typeof (body) === 'function') {
|
if (typeof (body) === 'function') {
|
||||||
callback = body as any as Callback; // legacy
|
callback = body as any as Callback; // legacy
|
||||||
body = {};
|
body = {};
|
||||||
@@ -4597,10 +4622,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
$receiptType: receiptType,
|
$receiptType: receiptType,
|
||||||
$eventId: event.getId(),
|
$eventId: event.getId(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: Add a check for which spec version this will be released in
|
||||||
|
if (await this.doesServerSupportUnstableFeature("org.matrix.msc3771")) {
|
||||||
|
const isThread = !!event.threadRootId;
|
||||||
|
body.thread_id = isThread
|
||||||
|
? event.threadRootId
|
||||||
|
: MAIN_ROOM_TIMELINE;
|
||||||
|
}
|
||||||
|
|
||||||
const promise = this.http.authedRequest(callback, Method.Post, path, undefined, body || {});
|
const promise = this.http.authedRequest(callback, Method.Post, path, undefined, body || {});
|
||||||
|
|
||||||
const room = this.getRoom(event.getRoomId());
|
const room = this.getRoom(event.getRoomId());
|
||||||
if (room) {
|
if (room && this.credentials.userId) {
|
||||||
room.addLocalEchoReceipt(this.credentials.userId, event, receiptType);
|
room.addLocalEchoReceipt(this.credentials.userId, event, receiptType);
|
||||||
}
|
}
|
||||||
return promise;
|
return promise;
|
||||||
@@ -4614,7 +4648,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* @return {Promise} Resolves: to an empty object {}
|
* @return {Promise} Resolves: to an empty object {}
|
||||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||||
*/
|
*/
|
||||||
public async sendReadReceipt(event: MatrixEvent, receiptType = ReceiptType.Read, callback?: Callback): Promise<{}> {
|
public async sendReadReceipt(
|
||||||
|
event: MatrixEvent | null,
|
||||||
|
receiptType = ReceiptType.Read,
|
||||||
|
callback?: Callback,
|
||||||
|
): Promise<{} | undefined> {
|
||||||
|
if (!event) return;
|
||||||
const eventId = event.getId();
|
const eventId = event.getId();
|
||||||
const room = this.getRoom(event.getRoomId());
|
const room = this.getRoom(event.getRoomId());
|
||||||
if (room && room.hasPendingEvent(eventId)) {
|
if (room && room.hasPendingEvent(eventId)) {
|
||||||
@@ -5279,6 +5318,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* @param {object} [options]
|
* @param {object} [options]
|
||||||
* @param {boolean} options.preventReEmit don't re-emit events emitted on an event mapped by this mapper on the client
|
* @param {boolean} options.preventReEmit don't re-emit events emitted on an event mapped by this mapper on the client
|
||||||
* @param {boolean} options.decrypt decrypt event proactively
|
* @param {boolean} options.decrypt decrypt event proactively
|
||||||
|
* @param {boolean} options.toDevice the event is a to_device event
|
||||||
* @return {Function}
|
* @return {Function}
|
||||||
*/
|
*/
|
||||||
public getEventMapper(options?: MapperOpts): EventMapper {
|
public getEventMapper(options?: MapperOpts): EventMapper {
|
||||||
@@ -5299,13 +5339,17 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* @return {Promise} Resolves:
|
* @return {Promise} Resolves:
|
||||||
* {@link module:models/event-timeline~EventTimeline} including the given event
|
* {@link module:models/event-timeline~EventTimeline} including the given event
|
||||||
*/
|
*/
|
||||||
public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<EventTimeline | undefined> {
|
public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<Optional<EventTimeline>> {
|
||||||
// don't allow any timeline support unless it's been enabled.
|
// don't allow any timeline support unless it's been enabled.
|
||||||
if (!this.timelineSupport) {
|
if (!this.timelineSupport) {
|
||||||
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
|
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
|
||||||
" parameter to true when creating MatrixClient to enable it.");
|
" parameter to true when creating MatrixClient to enable it.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!timelineSet?.room) {
|
||||||
|
throw new Error("getEventTimeline only supports room timelines");
|
||||||
|
}
|
||||||
|
|
||||||
if (timelineSet.getTimelineForEvent(eventId)) {
|
if (timelineSet.getTimelineForEvent(eventId)) {
|
||||||
return timelineSet.getTimelineForEvent(eventId);
|
return timelineSet.getTimelineForEvent(eventId);
|
||||||
}
|
}
|
||||||
@@ -5317,7 +5361,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let params: Record<string, string | string[]> = undefined;
|
let params: Record<string, string | string[]> | undefined = undefined;
|
||||||
if (this.clientOpts.lazyLoadMembers) {
|
if (this.clientOpts.lazyLoadMembers) {
|
||||||
params = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) };
|
params = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) };
|
||||||
}
|
}
|
||||||
@@ -5355,12 +5399,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
if (Thread.hasServerSideSupport && timelineSet.thread) {
|
if (Thread.hasServerSideSupport && timelineSet.thread) {
|
||||||
const thread = timelineSet.thread;
|
const thread = timelineSet.thread;
|
||||||
const opts: IRelationsRequestOpts = {
|
const opts: IRelationsRequestOpts = {
|
||||||
direction: Direction.Backward,
|
dir: Direction.Backward,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
};
|
};
|
||||||
|
|
||||||
await thread.fetchInitialEvents();
|
await thread.fetchInitialEvents();
|
||||||
let nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward);
|
let nextBatch: string | null | undefined = thread.liveTimeline.getPaginationToken(Direction.Backward);
|
||||||
|
|
||||||
// Fetch events until we find the one we were asked for, or we run out of pages
|
// Fetch events until we find the one we were asked for, or we run out of pages
|
||||||
while (!thread.findEventById(eventId)) {
|
while (!thread.findEventById(eventId)) {
|
||||||
@@ -5410,27 +5454,36 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* @return {Promise} Resolves:
|
* @return {Promise} Resolves:
|
||||||
* {@link module:models/event-timeline~EventTimeline} timeline with the latest events in the room
|
* {@link module:models/event-timeline~EventTimeline} timeline with the latest events in the room
|
||||||
*/
|
*/
|
||||||
public async getLatestTimeline(timelineSet: EventTimelineSet): Promise<EventTimeline> {
|
public async getLatestTimeline(timelineSet: EventTimelineSet): Promise<Optional<EventTimeline>> {
|
||||||
// don't allow any timeline support unless it's been enabled.
|
// don't allow any timeline support unless it's been enabled.
|
||||||
if (!this.timelineSupport) {
|
if (!this.timelineSupport) {
|
||||||
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
|
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
|
||||||
" parameter to true when creating MatrixClient to enable it.");
|
" parameter to true when creating MatrixClient to enable it.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const messagesPath = utils.encodeUri(
|
if (!timelineSet.room) {
|
||||||
"/rooms/$roomId/messages", {
|
throw new Error("getLatestTimeline only supports room timelines");
|
||||||
$roomId: timelineSet.room.roomId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const params: Record<string, string | string[]> = {
|
|
||||||
dir: 'b',
|
|
||||||
};
|
|
||||||
if (this.clientOpts.lazyLoadMembers) {
|
|
||||||
params.filter = JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await this.http.authedRequest<IMessagesResponse>(undefined, Method.Get, messagesPath, params);
|
let res: IMessagesResponse;
|
||||||
|
const roomId = timelineSet.room.roomId;
|
||||||
|
if (timelineSet.isThreadTimeline) {
|
||||||
|
res = await this.createThreadListMessagesRequest(
|
||||||
|
roomId,
|
||||||
|
null,
|
||||||
|
1,
|
||||||
|
Direction.Backward,
|
||||||
|
timelineSet.getFilter(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
res = await this.createMessagesRequest(
|
||||||
|
roomId,
|
||||||
|
null,
|
||||||
|
1,
|
||||||
|
Direction.Backward,
|
||||||
|
timelineSet.getFilter(),
|
||||||
|
);
|
||||||
|
}
|
||||||
const event = res.chunk?.[0];
|
const event = res.chunk?.[0];
|
||||||
if (!event) {
|
if (!event) {
|
||||||
throw new Error("No message returned from /messages when trying to construct getLatestTimeline");
|
throw new Error("No message returned from /messages when trying to construct getLatestTimeline");
|
||||||
@@ -5470,7 +5523,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
params.from = fromToken;
|
params.from = fromToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = null;
|
let filter: IRoomEventFilter | null = null;
|
||||||
if (this.clientOpts.lazyLoadMembers) {
|
if (this.clientOpts.lazyLoadMembers) {
|
||||||
// create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
|
// create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
|
||||||
// so the timelineFilter doesn't get written into it below
|
// so the timelineFilter doesn't get written into it below
|
||||||
@@ -5488,6 +5541,72 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
return this.http.authedRequest(undefined, Method.Get, path, params);
|
return this.http.authedRequest(undefined, Method.Get, path, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a request to /messages with the appropriate lazy loading filter set.
|
||||||
|
* XXX: if we do get rid of scrollback (as it's not used at the moment),
|
||||||
|
* we could inline this method again in paginateEventTimeline as that would
|
||||||
|
* then be the only call-site
|
||||||
|
* @param {string} roomId
|
||||||
|
* @param {string} fromToken
|
||||||
|
* @param {number} limit the maximum amount of events the retrieve
|
||||||
|
* @param {string} dir 'f' or 'b'
|
||||||
|
* @param {Filter} timelineFilter the timeline filter to pass
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
// XXX: Intended private, used by room.fetchRoomThreads
|
||||||
|
public createThreadListMessagesRequest(
|
||||||
|
roomId: string,
|
||||||
|
fromToken: string | null,
|
||||||
|
limit = 30,
|
||||||
|
dir = Direction.Backward,
|
||||||
|
timelineFilter?: Filter,
|
||||||
|
): Promise<IMessagesResponse> {
|
||||||
|
const path = utils.encodeUri("/rooms/$roomId/threads", { $roomId: roomId });
|
||||||
|
|
||||||
|
const params: Record<string, string> = {
|
||||||
|
limit: limit.toString(),
|
||||||
|
dir: dir,
|
||||||
|
include: 'all',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fromToken) {
|
||||||
|
params.from = fromToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
let filter: IRoomEventFilter | null = null;
|
||||||
|
if (this.clientOpts.lazyLoadMembers) {
|
||||||
|
// create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
|
||||||
|
// so the timelineFilter doesn't get written into it below
|
||||||
|
filter = {
|
||||||
|
...filter,
|
||||||
|
...Filter.LAZY_LOADING_MESSAGES_FILTER,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (timelineFilter) {
|
||||||
|
// XXX: it's horrific that /messages' filter parameter doesn't match
|
||||||
|
// /sync's one - see https://matrix.org/jira/browse/SPEC-451
|
||||||
|
filter = {
|
||||||
|
...filter,
|
||||||
|
...timelineFilter.getRoomTimelineFilterComponent()?.toJSON(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (filter) {
|
||||||
|
params.filter = JSON.stringify(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts: { prefix?: string } = {};
|
||||||
|
if (Thread.hasServerSideListSupport === FeatureSupport.Experimental) {
|
||||||
|
opts.prefix = "/_matrix/client/unstable/org.matrix.msc3856";
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.authedRequest<IThreadedMessagesResponse>(undefined, Method.Get, path, params, undefined, opts)
|
||||||
|
.then(res => ({
|
||||||
|
...res,
|
||||||
|
start: res.prev_batch,
|
||||||
|
end: res.next_batch,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Take an EventTimeline, and back/forward-fill results.
|
* Take an EventTimeline, and back/forward-fill results.
|
||||||
*
|
*
|
||||||
@@ -5503,6 +5622,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
*/
|
*/
|
||||||
public paginateEventTimeline(eventTimeline: EventTimeline, opts: IPaginateOpts): Promise<boolean> {
|
public paginateEventTimeline(eventTimeline: EventTimeline, opts: IPaginateOpts): Promise<boolean> {
|
||||||
const isNotifTimeline = (eventTimeline.getTimelineSet() === this.notifTimelineSet);
|
const isNotifTimeline = (eventTimeline.getTimelineSet() === this.notifTimelineSet);
|
||||||
|
const room = this.getRoom(eventTimeline.getRoomId());
|
||||||
|
const isThreadTimeline = eventTimeline.getTimelineSet().isThreadTimeline;
|
||||||
|
|
||||||
// TODO: we should implement a backoff (as per scrollback()) to deal more
|
// TODO: we should implement a backoff (as per scrollback()) to deal more
|
||||||
// nicely with HTTP errors.
|
// nicely with HTTP errors.
|
||||||
@@ -5536,7 +5657,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
only: 'highlight',
|
only: 'highlight',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (token !== "end") {
|
if (token && token !== "end") {
|
||||||
params.from = token;
|
params.from = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5544,7 +5665,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
undefined, Method.Get, path, params,
|
undefined, Method.Get, path, params,
|
||||||
).then(async (res) => {
|
).then(async (res) => {
|
||||||
const token = res.next_token;
|
const token = res.next_token;
|
||||||
const matrixEvents = [];
|
const matrixEvents: MatrixEvent[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < res.notifications.length; i++) {
|
for (let i = 0; i < res.notifications.length; i++) {
|
||||||
const notification = res.notifications[i];
|
const notification = res.notifications[i];
|
||||||
@@ -5568,13 +5689,48 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
if (backwards && !res.next_token) {
|
if (backwards && !res.next_token) {
|
||||||
eventTimeline.setPaginationToken(null, dir);
|
eventTimeline.setPaginationToken(null, dir);
|
||||||
}
|
}
|
||||||
return res.next_token ? true : false;
|
return Boolean(res.next_token);
|
||||||
|
}).finally(() => {
|
||||||
|
eventTimeline.paginationRequests[dir] = null;
|
||||||
|
});
|
||||||
|
eventTimeline.paginationRequests[dir] = promise;
|
||||||
|
} else if (isThreadTimeline) {
|
||||||
|
if (!room) {
|
||||||
|
throw new Error("Unknown room " + eventTimeline.getRoomId());
|
||||||
|
}
|
||||||
|
|
||||||
|
promise = this.createThreadListMessagesRequest(
|
||||||
|
eventTimeline.getRoomId(),
|
||||||
|
token,
|
||||||
|
opts.limit,
|
||||||
|
dir,
|
||||||
|
eventTimeline.getFilter(),
|
||||||
|
).then((res) => {
|
||||||
|
if (res.state) {
|
||||||
|
const roomState = eventTimeline.getState(dir);
|
||||||
|
const stateEvents = res.state.map(this.getEventMapper());
|
||||||
|
roomState.setUnknownStateEvents(stateEvents);
|
||||||
|
}
|
||||||
|
const token = res.end;
|
||||||
|
const matrixEvents = res.chunk.map(this.getEventMapper());
|
||||||
|
|
||||||
|
const timelineSet = eventTimeline.getTimelineSet();
|
||||||
|
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
|
||||||
|
this.processBeaconEvents(room, matrixEvents);
|
||||||
|
this.processThreadRoots(room, matrixEvents, backwards);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// we can recover from gappy syncs.
|
||||||
|
if (backwards && res.end == res.start) {
|
||||||
|
eventTimeline.setPaginationToken(null, dir);
|
||||||
|
}
|
||||||
|
return res.end !== res.start;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
eventTimeline.paginationRequests[dir] = null;
|
eventTimeline.paginationRequests[dir] = null;
|
||||||
});
|
});
|
||||||
eventTimeline.paginationRequests[dir] = promise;
|
eventTimeline.paginationRequests[dir] = promise;
|
||||||
} else {
|
} else {
|
||||||
const room = this.getRoom(eventTimeline.getRoomId());
|
|
||||||
if (!room) {
|
if (!room) {
|
||||||
throw new Error("Unknown room " + eventTimeline.getRoomId());
|
throw new Error("Unknown room " + eventTimeline.getRoomId());
|
||||||
}
|
}
|
||||||
@@ -5595,18 +5751,20 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
const matrixEvents = res.chunk.map(this.getEventMapper());
|
const matrixEvents = res.chunk.map(this.getEventMapper());
|
||||||
|
|
||||||
const timelineSet = eventTimeline.getTimelineSet();
|
const timelineSet = eventTimeline.getTimelineSet();
|
||||||
const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(matrixEvents);
|
const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents);
|
||||||
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
||||||
this.processBeaconEvents(timelineSet.room, timelineEvents);
|
this.processBeaconEvents(room, timelineEvents);
|
||||||
this.processThreadEvents(room, threadedEvents, backwards);
|
this.processThreadEvents(room, threadedEvents, backwards);
|
||||||
|
|
||||||
|
const atEnd = res.end === undefined || res.end === res.start;
|
||||||
|
|
||||||
// if we've hit the end of the timeline, we need to stop trying to
|
// 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
|
// paginate. We need to keep the 'forwards' token though, to make sure
|
||||||
// we can recover from gappy syncs.
|
// we can recover from gappy syncs.
|
||||||
if (backwards && res.end == res.start) {
|
if (backwards && atEnd) {
|
||||||
eventTimeline.setPaginationToken(null, dir);
|
eventTimeline.setPaginationToken(null, dir);
|
||||||
}
|
}
|
||||||
return res.end != res.start;
|
return !atEnd;
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
eventTimeline.paginationRequests[dir] = null;
|
eventTimeline.paginationRequests[dir] = null;
|
||||||
});
|
});
|
||||||
@@ -6684,23 +6842,28 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async doesServerSupportThread(): Promise<{
|
public async doesServerSupportThread(): Promise<{
|
||||||
serverSupport: boolean;
|
threads: FeatureSupport;
|
||||||
stable: boolean;
|
list: FeatureSupport;
|
||||||
} | null> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const hasUnstableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440");
|
const [threadUnstable, threadStable, listUnstable, listStable] = await Promise.all([
|
||||||
const hasStableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable");
|
this.doesServerSupportUnstableFeature("org.matrix.msc3440"),
|
||||||
|
this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable"),
|
||||||
|
this.doesServerSupportUnstableFeature("org.matrix.msc3856"),
|
||||||
|
this.doesServerSupportUnstableFeature("org.matrix.msc3856.stable"),
|
||||||
|
]);
|
||||||
|
|
||||||
// TODO: Use `this.isVersionSupported("v1.3")` for whatever spec version includes MSC3440 formally.
|
// TODO: Use `this.isVersionSupported("v1.3")` for whatever spec version includes MSC3440 formally.
|
||||||
|
|
||||||
return {
|
return {
|
||||||
serverSupport: hasUnstableSupport || hasStableSupport,
|
threads: determineFeatureSupport(threadStable, threadUnstable),
|
||||||
stable: hasStableSupport,
|
list: determineFeatureSupport(listStable, listUnstable),
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Assume server support and stability aren't available: null/no data return.
|
return {
|
||||||
// XXX: This should just return an object with `false` booleans instead.
|
threads: FeatureSupport.None,
|
||||||
return null;
|
list: FeatureSupport.None,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6757,7 +6920,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
eventId: string,
|
eventId: string,
|
||||||
relationType?: RelationType | string | null,
|
relationType?: RelationType | string | null,
|
||||||
eventType?: EventType | string | null,
|
eventType?: EventType | string | null,
|
||||||
opts: IRelationsRequestOpts = { direction: Direction.Backward },
|
opts: IRelationsRequestOpts = { dir: Direction.Backward },
|
||||||
): Promise<{
|
): Promise<{
|
||||||
originalEvent: MatrixEvent;
|
originalEvent: MatrixEvent;
|
||||||
events: MatrixEvent[];
|
events: MatrixEvent[];
|
||||||
@@ -7072,10 +7235,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {module:client.callback} callback Optional.
|
* @param {module:client.callback} callback Optional.
|
||||||
* @return {Promise} Resolves: TODO
|
* @return {Promise<ILoginFlowsResponse>} Resolves to the available login flows
|
||||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||||
*/
|
*/
|
||||||
public loginFlows(callback?: Callback): Promise<any> { // TODO: Types
|
public loginFlows(callback?: Callback): Promise<ILoginFlowsResponse> {
|
||||||
return this.http.request(callback, Method.Get, "/login");
|
return this.http.request(callback, Method.Get, "/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7151,15 +7314,26 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* @param {string} loginType The type of SSO login we are doing (sso or cas).
|
* @param {string} loginType The type of SSO login we are doing (sso or cas).
|
||||||
* Defaults to 'sso'.
|
* Defaults to 'sso'.
|
||||||
* @param {string} idpId The ID of the Identity Provider being targeted, optional.
|
* @param {string} idpId The ID of the Identity Provider being targeted, optional.
|
||||||
|
* @param {SSOAction} action the SSO flow to indicate to the IdP, optional.
|
||||||
* @return {string} The HS URL to hit to begin the SSO login process.
|
* @return {string} The HS URL to hit to begin the SSO login process.
|
||||||
*/
|
*/
|
||||||
public getSsoLoginUrl(redirectUrl: string, loginType = "sso", idpId?: string): string {
|
public getSsoLoginUrl(
|
||||||
|
redirectUrl: string,
|
||||||
|
loginType = "sso",
|
||||||
|
idpId?: string,
|
||||||
|
action?: SSOAction,
|
||||||
|
): string {
|
||||||
let url = "/login/" + loginType + "/redirect";
|
let url = "/login/" + loginType + "/redirect";
|
||||||
if (idpId) {
|
if (idpId) {
|
||||||
url += "/" + idpId;
|
url += "/" + idpId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.http.getUrl(url, { redirectUrl }, PREFIX_R0);
|
const params = {
|
||||||
|
redirectUrl,
|
||||||
|
[SSO_ACTION_PARAM.unstable!]: action,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.http.getUrl(url, params, PREFIX_R0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7233,6 +7407,27 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
return this.http.authedRequest(undefined, Method.Post, '/account/deactivate', undefined, body);
|
return this.http.authedRequest(undefined, Method.Post, '/account/deactivate', undefined, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a request for an `m.login.token` to be issued as per
|
||||||
|
* [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882).
|
||||||
|
* The server may require User-Interactive auth.
|
||||||
|
* Note that this is UNSTABLE and subject to breaking changes without notice.
|
||||||
|
* @param {IAuthData} auth Optional. Auth data to supply for User-Interactive auth.
|
||||||
|
* @return {Promise<UIAResponse<LoginTokenPostResponse>>} Resolves: On success, the token response
|
||||||
|
* or UIA auth data.
|
||||||
|
*/
|
||||||
|
public requestLoginToken(auth?: IAuthData): Promise<UIAResponse<LoginTokenPostResponse>> {
|
||||||
|
const body: UIARequest<{}> = { auth };
|
||||||
|
return this.http.authedRequest(
|
||||||
|
undefined, // no callback support
|
||||||
|
Method.Post,
|
||||||
|
"/org.matrix.msc3882/login/token",
|
||||||
|
undefined, // no query params
|
||||||
|
body,
|
||||||
|
{ prefix: PREFIX_UNSTABLE },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the fallback URL to use for unknown interactive-auth stages.
|
* Get the fallback URL to use for unknown interactive-auth stages.
|
||||||
*
|
*
|
||||||
@@ -7303,7 +7498,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
eventId: string,
|
eventId: string,
|
||||||
relationType?: RelationType | string | null,
|
relationType?: RelationType | string | null,
|
||||||
eventType?: EventType | string | null,
|
eventType?: EventType | string | null,
|
||||||
opts: IRelationsRequestOpts = { direction: Direction.Backward },
|
opts: IRelationsRequestOpts = { dir: Direction.Backward },
|
||||||
): Promise<IRelationsResponse> {
|
): Promise<IRelationsResponse> {
|
||||||
const queryString = utils.encodeParams(opts as Record<string, string | number>);
|
const queryString = utils.encodeParams(opts as Record<string, string | number>);
|
||||||
|
|
||||||
@@ -7327,7 +7522,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
$eventType: eventType,
|
$eventType: eventType,
|
||||||
});
|
});
|
||||||
return this.http.authedRequest(
|
return this.http.authedRequest(
|
||||||
undefined, Method.Get, path, null, null, {
|
undefined, Method.Get, path, undefined, undefined, {
|
||||||
prefix: PREFIX_UNSTABLE,
|
prefix: PREFIX_UNSTABLE,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -7524,9 +7719,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
[ReceiptType.Read]: rrEventId,
|
[ReceiptType.Read]: rrEventId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const privateField = await utils.getPrivateReadReceiptField(this);
|
if (
|
||||||
if (privateField) {
|
(await this.doesServerSupportUnstableFeature("org.matrix.msc2285.stable"))
|
||||||
content[privateField] = rpEventId;
|
|| (await this.isVersionSupported("v1.4"))
|
||||||
|
) {
|
||||||
|
content[ReceiptType.ReadPrivate] = rpEventId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.http.authedRequest(undefined, Method.Post, path, undefined, content);
|
return this.http.authedRequest(undefined, Method.Post, path, undefined, content);
|
||||||
@@ -7633,7 +7830,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
public getLocalAliases(roomId: string): Promise<{ aliases: string[] }> {
|
public getLocalAliases(roomId: string): Promise<{ aliases: string[] }> {
|
||||||
const path = utils.encodeUri("/rooms/$roomId/aliases", { $roomId: roomId });
|
const path = utils.encodeUri("/rooms/$roomId/aliases", { $roomId: roomId });
|
||||||
const prefix = PREFIX_V3;
|
const prefix = PREFIX_V3;
|
||||||
return this.http.authedRequest(undefined, Method.Get, path, null, null, { prefix });
|
return this.http.authedRequest(undefined, Method.Get, path, undefined, undefined, { prefix });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7870,7 +8067,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
'bind': bind,
|
'bind': bind,
|
||||||
};
|
};
|
||||||
return this.http.authedRequest(
|
return this.http.authedRequest(
|
||||||
callback, Method.Post, path, null, data,
|
callback, Method.Post, path, undefined, data,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7889,7 +8086,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
public async addThreePidOnly(data: IAddThreePidOnlyBody): Promise<{}> {
|
public async addThreePidOnly(data: IAddThreePidOnlyBody): Promise<{}> {
|
||||||
const path = "/account/3pid/add";
|
const path = "/account/3pid/add";
|
||||||
const prefix = await this.isVersionSupported("r0.6.0") ? PREFIX_R0 : PREFIX_UNSTABLE;
|
const prefix = await this.isVersionSupported("r0.6.0") ? PREFIX_R0 : PREFIX_UNSTABLE;
|
||||||
return this.http.authedRequest(undefined, Method.Post, path, null, data, { prefix });
|
return this.http.authedRequest(undefined, Method.Post, path, undefined, data, { prefix });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7911,7 +8108,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
const prefix = await this.isVersionSupported("r0.6.0") ?
|
const prefix = await this.isVersionSupported("r0.6.0") ?
|
||||||
PREFIX_R0 : PREFIX_UNSTABLE;
|
PREFIX_R0 : PREFIX_UNSTABLE;
|
||||||
return this.http.authedRequest(
|
return this.http.authedRequest(
|
||||||
undefined, Method.Post, path, null, data, { prefix },
|
undefined, Method.Post, path, undefined, data, { prefix },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7938,7 +8135,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
id_server: this.getIdentityServerUrl(true),
|
id_server: this.getIdentityServerUrl(true),
|
||||||
};
|
};
|
||||||
const prefix = await this.isVersionSupported("r0.6.0") ? PREFIX_R0 : PREFIX_UNSTABLE;
|
const prefix = await this.isVersionSupported("r0.6.0") ? PREFIX_R0 : PREFIX_UNSTABLE;
|
||||||
return this.http.authedRequest(undefined, Method.Post, path, null, data, { prefix });
|
return this.http.authedRequest(undefined, Method.Post, path, undefined, data, { prefix });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7955,7 +8152,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
): Promise<{ id_server_unbind_result: IdServerUnbindResult }> {
|
): Promise<{ id_server_unbind_result: IdServerUnbindResult }> {
|
||||||
const path = "/account/3pid/delete";
|
const path = "/account/3pid/delete";
|
||||||
return this.http.authedRequest(undefined, Method.Post, path, null, { medium, address });
|
return this.http.authedRequest(undefined, Method.Post, path, undefined, { medium, address });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8001,7 +8198,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
};
|
};
|
||||||
|
|
||||||
return this.http.authedRequest<{}>(
|
return this.http.authedRequest<{}>(
|
||||||
callback, Method.Post, path, null, data,
|
callback, Method.Post, path, undefined, data,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8092,8 +8289,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
* @return {Promise} Resolves: Array of objects representing pushers
|
* @return {Promise} Resolves: Array of objects representing pushers
|
||||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||||
*/
|
*/
|
||||||
public getPushers(callback?: Callback): Promise<{ pushers: IPusher[] }> {
|
public async getPushers(callback?: Callback): Promise<{ pushers: IPusher[] }> {
|
||||||
return this.http.authedRequest(callback, Method.Get, "/pushers");
|
const response = await this.http.authedRequest(callback, Method.Get, "/pushers");
|
||||||
|
|
||||||
|
// Migration path for clients that connect to a homeserver that does not support
|
||||||
|
// MSC3881 yet, see https://github.com/matrix-org/matrix-spec-proposals/blob/kerry/remote-push-toggle/proposals/3881-remote-push-notification-toggling.md#migration
|
||||||
|
if (!await this.doesServerSupportUnstableFeature("org.matrix.msc3881")) {
|
||||||
|
response.pushers = response.pushers.map(pusher => {
|
||||||
|
if (!pusher.hasOwnProperty(PUSHER_ENABLED.name)) {
|
||||||
|
pusher[PUSHER_ENABLED.name] = true;
|
||||||
|
}
|
||||||
|
return pusher;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8106,7 +8316,22 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
*/
|
*/
|
||||||
public setPusher(pusher: IPusherRequest, callback?: Callback): Promise<{}> {
|
public setPusher(pusher: IPusherRequest, callback?: Callback): Promise<{}> {
|
||||||
const path = "/pushers/set";
|
const path = "/pushers/set";
|
||||||
return this.http.authedRequest(callback, Method.Post, path, null, pusher);
|
return this.http.authedRequest(callback, Method.Post, path, undefined, pusher);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists local notification settings
|
||||||
|
* @param {string} deviceId
|
||||||
|
* @param {LocalNotificationSettings} notificationSettings
|
||||||
|
* @return {Promise} Resolves: an empty object
|
||||||
|
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||||
|
*/
|
||||||
|
public setLocalNotificationSettings(
|
||||||
|
deviceId: string,
|
||||||
|
notificationSettings: LocalNotificationSettings,
|
||||||
|
): Promise<{}> {
|
||||||
|
const key = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
|
||||||
|
return this.setAccountData(key, notificationSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8893,7 +9118,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
$eventId: eventId,
|
$eventId: eventId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.http.authedRequest(undefined, Method.Post, path, null, { score, reason });
|
return this.http.authedRequest(undefined, Method.Post, path, undefined, { score, reason });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -9055,7 +9280,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
*/
|
*/
|
||||||
public async getRoomSummary(roomIdOrAlias: string, via?: string[]): Promise<IRoomSummary> {
|
public async getRoomSummary(roomIdOrAlias: string, via?: string[]): Promise<IRoomSummary> {
|
||||||
const path = utils.encodeUri("/rooms/$roomid/summary", { $roomid: roomIdOrAlias });
|
const path = utils.encodeUri("/rooms/$roomid/summary", { $roomid: roomIdOrAlias });
|
||||||
return this.http.authedRequest(undefined, Method.Get, path, { via }, null, {
|
return this.http.authedRequest(undefined, Method.Get, path, { via }, undefined, {
|
||||||
qsStringifyOptions: { arrayFormat: 'repeat' },
|
qsStringifyOptions: { arrayFormat: 'repeat' },
|
||||||
prefix: "/_matrix/client/unstable/im.nheko.summary",
|
prefix: "/_matrix/client/unstable/im.nheko.summary",
|
||||||
});
|
});
|
||||||
@@ -9068,6 +9293,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
room.processThreadedEvents(threadedEvents, toStartOfTimeline);
|
room.processThreadedEvents(threadedEvents, toStartOfTimeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
public processThreadRoots(room: Room, threadedEvents: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||||
|
room.processThreadRoots(threadedEvents, toStartOfTimeline);
|
||||||
|
}
|
||||||
|
|
||||||
public processBeaconEvents(
|
public processBeaconEvents(
|
||||||
room?: Room,
|
room?: Room,
|
||||||
events?: MatrixEvent[],
|
events?: MatrixEvent[],
|
||||||
@@ -9116,6 +9348,73 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* recalculates an accurate notifications count on event decryption.
|
||||||
|
* Servers do not have enough knowledge about encrypted events to calculate an
|
||||||
|
* accurate notification_count
|
||||||
|
*/
|
||||||
|
export function fixNotificationCountOnDecryption(cli: MatrixClient, event: MatrixEvent): void {
|
||||||
|
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 = (isThreadEvent
|
||||||
|
? room.getThreadUnreadNotificationCount(
|
||||||
|
event.threadRootId,
|
||||||
|
NotificationCountType.Highlight,
|
||||||
|
)
|
||||||
|
: room.getUnreadNotificationCount(NotificationCountType.Highlight)) ?? 0;
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// TODO: Handle mentions received while the client is offline
|
||||||
|
// See also https://github.com/vector-im/element-web/issues/9069
|
||||||
|
const hasReadEvent = isThreadEvent
|
||||||
|
? room.getThread(event.threadRootId).hasUserReadEvent(cli.getUserId(), event.getId())
|
||||||
|
: room.hasUserReadEvent(cli.getUserId(), event.getId());
|
||||||
|
|
||||||
|
if (!hasReadEvent) {
|
||||||
|
let newCount = currentCount;
|
||||||
|
if (newHighlight && !oldHighlight) newCount++;
|
||||||
|
if (!newHighlight && oldHighlight) newCount--;
|
||||||
|
|
||||||
|
if (isThreadEvent) {
|
||||||
|
room.setThreadUnreadNotificationCount(
|
||||||
|
event.threadRootId,
|
||||||
|
NotificationCountType.Highlight,
|
||||||
|
newCount,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
room.setUnreadNotificationCount(NotificationCountType.Highlight, newCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix 'Mentions Only' rooms from not having the right badge count
|
||||||
|
const totalCount = (isThreadEvent
|
||||||
|
? room.getThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Total)
|
||||||
|
: room.getUnreadNotificationCount(NotificationCountType.Total)) ?? 0;
|
||||||
|
|
||||||
|
if (totalCount < newCount) {
|
||||||
|
if (isThreadEvent) {
|
||||||
|
room.setThreadUnreadNotificationCount(
|
||||||
|
event.threadRootId,
|
||||||
|
NotificationCountType.Total,
|
||||||
|
newCount,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
room.setUnreadNotificationCount(NotificationCountType.Total, newCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fires whenever the SDK receives a new event.
|
* Fires whenever the SDK receives a new event.
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
@@ -292,16 +292,17 @@ export const makeBeaconContent: MakeBeaconContent = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type BeaconLocationState = MLocationContent & {
|
export type BeaconLocationState = MLocationContent & {
|
||||||
timestamp: number;
|
uri?: string; // override from MLocationContent to allow optionals
|
||||||
|
timestamp?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseBeaconContent = (content: MBeaconEventContent): BeaconLocationState => {
|
export const parseBeaconContent = (content: MBeaconEventContent): BeaconLocationState => {
|
||||||
const { description, uri } = M_LOCATION.findIn<MLocationContent>(content);
|
const location = M_LOCATION.findIn<MLocationContent>(content);
|
||||||
const timestamp = M_TIMESTAMP.findIn<number>(content);
|
const timestamp = M_TIMESTAMP.findIn<number>(content);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
description,
|
description: location?.description,
|
||||||
uri,
|
uri: location?.uri,
|
||||||
timestamp,
|
timestamp,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { logger } from "../logger";
|
|||||||
import { MatrixEvent } from "../models/event";
|
import { MatrixEvent } from "../models/event";
|
||||||
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
|
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
|
||||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||||
import { Method, PREFIX_UNSTABLE } from "../http-api";
|
import { Method, PREFIX_V3 } from "../http-api";
|
||||||
import { Crypto, IBootstrapCrossSigningOpts } from "./index";
|
import { Crypto, IBootstrapCrossSigningOpts } from "./index";
|
||||||
import {
|
import {
|
||||||
ClientEvent,
|
ClientEvent,
|
||||||
@@ -246,14 +246,14 @@ export class EncryptionSetupOperation {
|
|||||||
algorithm: this.keyBackupInfo.algorithm,
|
algorithm: this.keyBackupInfo.algorithm,
|
||||||
auth_data: this.keyBackupInfo.auth_data,
|
auth_data: this.keyBackupInfo.auth_data,
|
||||||
},
|
},
|
||||||
{ prefix: PREFIX_UNSTABLE },
|
{ prefix: PREFIX_V3 },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// add new key backup
|
// add new key backup
|
||||||
await baseApis.http.authedRequest(
|
await baseApis.http.authedRequest(
|
||||||
undefined, Method.Post, "/room_keys/version",
|
undefined, Method.Post, "/room_keys/version",
|
||||||
undefined, this.keyBackupInfo,
|
undefined, this.keyBackupInfo,
|
||||||
{ prefix: PREFIX_UNSTABLE },
|
{ prefix: PREFIX_V3 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import * as algorithms from './algorithms';
|
|||||||
import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
|
import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
|
||||||
import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
|
import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
|
||||||
import { IMegolmSessionData } from "./index";
|
import { IMegolmSessionData } from "./index";
|
||||||
|
import { OlmGroupSessionExtraData } from "../@types/crypto";
|
||||||
|
|
||||||
// The maximum size of an event is 65K, and we base64 the content, so this is a
|
// The maximum size of an event is 65K, and we base64 the content, so this is a
|
||||||
// reasonable approximation to the biggest plaintext we can encrypt.
|
// reasonable approximation to the biggest plaintext we can encrypt.
|
||||||
@@ -122,6 +123,7 @@ interface IInboundGroupSessionKey {
|
|||||||
forwarding_curve25519_key_chain: string[];
|
forwarding_curve25519_key_chain: string[];
|
||||||
sender_claimed_ed25519_key: string;
|
sender_claimed_ed25519_key: string;
|
||||||
shared_history: boolean;
|
shared_history: boolean;
|
||||||
|
untrusted: boolean;
|
||||||
}
|
}
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
@@ -1101,7 +1103,7 @@ export class OlmDevice {
|
|||||||
sessionKey: string,
|
sessionKey: string,
|
||||||
keysClaimed: Record<string, string>,
|
keysClaimed: Record<string, string>,
|
||||||
exportFormat: boolean,
|
exportFormat: boolean,
|
||||||
extraSessionData: Record<string, any> = {},
|
extraSessionData: OlmGroupSessionExtraData = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.cryptoStore.doTxn(
|
await this.cryptoStore.doTxn(
|
||||||
'readwrite', [
|
'readwrite', [
|
||||||
@@ -1133,18 +1135,43 @@ export class OlmDevice {
|
|||||||
"Update for megolm session "
|
"Update for megolm session "
|
||||||
+ senderKey + "/" + sessionId,
|
+ senderKey + "/" + sessionId,
|
||||||
);
|
);
|
||||||
if (existingSession.first_known_index()
|
if (existingSession.first_known_index() <= session.first_known_index()) {
|
||||||
<= session.first_known_index()
|
if (!existingSessionData.untrusted || extraSessionData.untrusted) {
|
||||||
&& !(existingSession.first_known_index() == session.first_known_index()
|
// existing session has less-than-or-equal index
|
||||||
&& !extraSessionData.untrusted
|
// (i.e. can decrypt at least as much), and the
|
||||||
&& existingSessionData.untrusted)) {
|
// new session's trust does not win over the old
|
||||||
// existing session has lower index (i.e. can
|
// session's trust, so keep it
|
||||||
// decrypt more), or they have the same index and
|
|
||||||
// the new sessions trust does not win over the old
|
|
||||||
// sessions trust, so keep it
|
|
||||||
logger.log(`Keeping existing megolm session ${sessionId}`);
|
logger.log(`Keeping existing megolm session ${sessionId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (existingSession.first_known_index() < session.first_known_index()) {
|
||||||
|
// We want to upgrade the existing session's trust,
|
||||||
|
// but we can't just use the new session because we'll
|
||||||
|
// lose the lower index. Check that the sessions connect
|
||||||
|
// properly, and then manually set the existing session
|
||||||
|
// as trusted.
|
||||||
|
if (
|
||||||
|
existingSession.export_session(session.first_known_index())
|
||||||
|
=== session.export_session(session.first_known_index())
|
||||||
|
) {
|
||||||
|
logger.info(
|
||||||
|
"Upgrading trust of existing megolm session " +
|
||||||
|
sessionId + " based on newly-received trusted session",
|
||||||
|
);
|
||||||
|
existingSessionData.untrusted = false;
|
||||||
|
this.cryptoStore.storeEndToEndInboundGroupSession(
|
||||||
|
senderKey, sessionId, existingSessionData, txn,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
"Newly-received megolm session " + sessionId +
|
||||||
|
" does not match existing session! Keeping existing session",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If the sessions have the same index, go ahead and store the new trusted one.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -1427,13 +1454,23 @@ export class OlmDevice {
|
|||||||
const claimedKeys = sessionData.keysClaimed || {};
|
const claimedKeys = sessionData.keysClaimed || {};
|
||||||
const senderEd25519Key = claimedKeys.ed25519 || null;
|
const senderEd25519Key = claimedKeys.ed25519 || null;
|
||||||
|
|
||||||
|
const forwardingKeyChain = sessionData.forwardingCurve25519KeyChain || [];
|
||||||
|
// older forwarded keys didn't set the "untrusted"
|
||||||
|
// property, but can be identified by having a
|
||||||
|
// non-empty forwarding key chain. These keys should
|
||||||
|
// be marked as untrusted since we don't know that they
|
||||||
|
// can be trusted
|
||||||
|
const untrusted = "untrusted" in sessionData
|
||||||
|
? sessionData.untrusted
|
||||||
|
: forwardingKeyChain.length > 0;
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"chain_index": chainIndex,
|
"chain_index": chainIndex,
|
||||||
"key": exportedSession,
|
"key": exportedSession,
|
||||||
"forwarding_curve25519_key_chain":
|
"forwarding_curve25519_key_chain": forwardingKeyChain,
|
||||||
sessionData.forwardingCurve25519KeyChain || [],
|
|
||||||
"sender_claimed_ed25519_key": senderEd25519Key,
|
"sender_claimed_ed25519_key": senderEd25519Key,
|
||||||
"shared_history": sessionData.sharedHistory || false,
|
"shared_history": sessionData.sharedHistory || false,
|
||||||
|
"untrusted": untrusted,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -539,7 +539,23 @@ export class SecretStorage {
|
|||||||
// because someone could be trying to send us bogus data
|
// because someone could be trying to send us bogus data
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!olmlib.isOlmEncrypted(event)) {
|
||||||
|
logger.error("secret event not properly encrypted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const content = event.getContent();
|
const content = event.getContent();
|
||||||
|
|
||||||
|
const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(
|
||||||
|
olmlib.OLM_ALGORITHM,
|
||||||
|
event.getSenderKey() || "",
|
||||||
|
);
|
||||||
|
if (senderKeyUser !== event.getSender()) {
|
||||||
|
logger.error("sending device does not belong to the user it claims to be from");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
logger.log("got secret share for request", content.request_id);
|
logger.log("got secret share for request", content.request_id);
|
||||||
const requestControl = this.requests.get(content.request_id);
|
const requestControl = this.requests.get(content.request_id);
|
||||||
if (requestControl) {
|
if (requestControl) {
|
||||||
@@ -559,6 +575,14 @@ export class SecretStorage {
|
|||||||
logger.log("unsolicited secret share from device", deviceInfo.deviceId);
|
logger.log("unsolicited secret share from device", deviceInfo.deviceId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// unsure that the sender is trusted. In theory, this check is
|
||||||
|
// unnecessary since we only accept secret shares from devices that
|
||||||
|
// we requested from, but it doesn't hurt.
|
||||||
|
const deviceTrust = this.baseApis.crypto.checkDeviceInfoTrust(event.getSender(), deviceInfo);
|
||||||
|
if (!deviceTrust.isVerified()) {
|
||||||
|
logger.log("secret share from unverified device");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
logger.log(
|
logger.log(
|
||||||
`Successfully received secret ${requestControl.name} ` +
|
`Successfully received secret ${requestControl.name} ` +
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ limitations under the License.
|
|||||||
import { MatrixClient } from "../../client";
|
import { MatrixClient } from "../../client";
|
||||||
import { Room } from "../../models/room";
|
import { Room } from "../../models/room";
|
||||||
import { OlmDevice } from "../OlmDevice";
|
import { OlmDevice } from "../OlmDevice";
|
||||||
import { MatrixEvent, RoomMember } from "../..";
|
import { MatrixEvent, RoomMember } from "../../matrix";
|
||||||
import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "..";
|
import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "..";
|
||||||
import { DeviceInfo } from "../deviceinfo";
|
import { DeviceInfo } from "../deviceinfo";
|
||||||
import { IRoomEncryption } from "../RoomList";
|
import { IRoomEncryption } from "../RoomList";
|
||||||
@@ -34,7 +34,7 @@ import { IRoomEncryption } from "../RoomList";
|
|||||||
*
|
*
|
||||||
* @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>}
|
* @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>}
|
||||||
*/
|
*/
|
||||||
export const ENCRYPTION_CLASSES: Record<string, new (params: IParams) => EncryptionAlgorithm> = {};
|
export const ENCRYPTION_CLASSES = new Map<string, new (params: IParams) => EncryptionAlgorithm>();
|
||||||
|
|
||||||
type DecryptionClassParams = Omit<IParams, "deviceId" | "config">;
|
type DecryptionClassParams = Omit<IParams, "deviceId" | "config">;
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ type DecryptionClassParams = Omit<IParams, "deviceId" | "config">;
|
|||||||
*
|
*
|
||||||
* @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
|
* @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
|
||||||
*/
|
*/
|
||||||
export const DECRYPTION_CLASSES: Record<string, new (params: DecryptionClassParams) => DecryptionAlgorithm> = {};
|
export const DECRYPTION_CLASSES = new Map<string, new (params: DecryptionClassParams) => DecryptionAlgorithm>();
|
||||||
|
|
||||||
export interface IParams {
|
export interface IParams {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -297,6 +297,6 @@ export function registerAlgorithm(
|
|||||||
encryptor: new (params: IParams) => EncryptionAlgorithm,
|
encryptor: new (params: IParams) => EncryptionAlgorithm,
|
||||||
decryptor: new (params: Omit<IParams, "deviceId">) => DecryptionAlgorithm,
|
decryptor: new (params: Omit<IParams, "deviceId">) => DecryptionAlgorithm,
|
||||||
): void {
|
): void {
|
||||||
ENCRYPTION_CLASSES[algorithm] = encryptor;
|
ENCRYPTION_CLASSES.set(algorithm, encryptor);
|
||||||
DECRYPTION_CLASSES[algorithm] = decryptor;
|
DECRYPTION_CLASSES.set(algorithm, decryptor);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,10 @@ import { Room } from '../../models/room';
|
|||||||
import { DeviceInfo } from "../deviceinfo";
|
import { DeviceInfo } from "../deviceinfo";
|
||||||
import { IOlmSessionResult } from "../olmlib";
|
import { IOlmSessionResult } from "../olmlib";
|
||||||
import { DeviceInfoMap } from "../DeviceList";
|
import { DeviceInfoMap } from "../DeviceList";
|
||||||
import { MatrixEvent } from "../..";
|
import { MatrixEvent } from "../../models/event";
|
||||||
import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
|
import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
|
||||||
|
import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager';
|
||||||
|
import { OlmGroupSessionExtraData } from "../../@types/crypto";
|
||||||
|
|
||||||
// determine whether the key can be shared with invitees
|
// determine whether the key can be shared with invitees
|
||||||
export function isRoomSharedHistory(room: Room): boolean {
|
export function isRoomSharedHistory(room: Room): boolean {
|
||||||
@@ -1189,9 +1191,10 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
|||||||
* {@link module:crypto/algorithms/DecryptionAlgorithm}
|
* {@link module:crypto/algorithms/DecryptionAlgorithm}
|
||||||
*/
|
*/
|
||||||
class MegolmDecryption extends DecryptionAlgorithm {
|
class MegolmDecryption extends DecryptionAlgorithm {
|
||||||
// events which we couldn't decrypt due to unknown sessions / indexes: map from
|
// events which we couldn't decrypt due to unknown sessions /
|
||||||
// senderKey|sessionId to Set of MatrixEvents
|
// indexes, or which we could only decrypt with untrusted keys:
|
||||||
private pendingEvents: Record<string, Map<string, Set<MatrixEvent>>> = {};
|
// map from senderKey|sessionId to Set of MatrixEvents
|
||||||
|
private pendingEvents = new Map<string, Map<string, Set<MatrixEvent>>>();
|
||||||
|
|
||||||
// this gets stubbed out by the unit tests.
|
// this gets stubbed out by the unit tests.
|
||||||
private olmlib = olmlib;
|
private olmlib = olmlib;
|
||||||
@@ -1294,9 +1297,13 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// success. We can remove the event from the pending list, if that hasn't
|
// Success. We can remove the event from the pending list, if
|
||||||
// already happened.
|
// that hasn't already happened. However, if the event was
|
||||||
|
// decrypted with an untrusted key, leave it on the pending
|
||||||
|
// list so it will be retried if we find a trusted key later.
|
||||||
|
if (!res.untrusted) {
|
||||||
this.removeEventFromPendingList(event);
|
this.removeEventFromPendingList(event);
|
||||||
|
}
|
||||||
|
|
||||||
const payload = JSON.parse(res.result);
|
const payload = JSON.parse(res.result);
|
||||||
|
|
||||||
@@ -1343,10 +1350,10 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
const content = event.getWireContent();
|
const content = event.getWireContent();
|
||||||
const senderKey = content.sender_key;
|
const senderKey = content.sender_key;
|
||||||
const sessionId = content.session_id;
|
const sessionId = content.session_id;
|
||||||
if (!this.pendingEvents[senderKey]) {
|
if (!this.pendingEvents.has(senderKey)) {
|
||||||
this.pendingEvents[senderKey] = new Map();
|
this.pendingEvents.set(senderKey, new Map<string, Set<MatrixEvent>>());
|
||||||
}
|
}
|
||||||
const senderPendingEvents = this.pendingEvents[senderKey];
|
const senderPendingEvents = this.pendingEvents.get(senderKey);
|
||||||
if (!senderPendingEvents.has(sessionId)) {
|
if (!senderPendingEvents.has(sessionId)) {
|
||||||
senderPendingEvents.set(sessionId, new Set());
|
senderPendingEvents.set(sessionId, new Set());
|
||||||
}
|
}
|
||||||
@@ -1364,7 +1371,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
const content = event.getWireContent();
|
const content = event.getWireContent();
|
||||||
const senderKey = content.sender_key;
|
const senderKey = content.sender_key;
|
||||||
const sessionId = content.session_id;
|
const sessionId = content.session_id;
|
||||||
const senderPendingEvents = this.pendingEvents[senderKey];
|
const senderPendingEvents = this.pendingEvents.get(senderKey);
|
||||||
const pendingEvents = senderPendingEvents?.get(sessionId);
|
const pendingEvents = senderPendingEvents?.get(sessionId);
|
||||||
if (!pendingEvents) {
|
if (!pendingEvents) {
|
||||||
return;
|
return;
|
||||||
@@ -1375,7 +1382,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
senderPendingEvents.delete(sessionId);
|
senderPendingEvents.delete(sessionId);
|
||||||
}
|
}
|
||||||
if (senderPendingEvents.size === 0) {
|
if (senderPendingEvents.size === 0) {
|
||||||
delete this.pendingEvents[senderKey];
|
this.pendingEvents.delete(senderKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1391,6 +1398,8 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
let exportFormat = false;
|
let exportFormat = false;
|
||||||
let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>;
|
let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>;
|
||||||
|
|
||||||
|
const extraSessionData: OlmGroupSessionExtraData = {};
|
||||||
|
|
||||||
if (!content.room_id ||
|
if (!content.room_id ||
|
||||||
!content.session_key ||
|
!content.session_key ||
|
||||||
!content.session_id ||
|
!content.session_id ||
|
||||||
@@ -1400,12 +1409,59 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!senderKey) {
|
if (!olmlib.isOlmEncrypted(event)) {
|
||||||
logger.error("key event has no sender key (not encrypted?)");
|
logger.error("key event not properly encrypted");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (content["org.matrix.msc3061.shared_history"]) {
|
||||||
|
extraSessionData.sharedHistory = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.getType() == "m.forwarded_room_key") {
|
if (event.getType() == "m.forwarded_room_key") {
|
||||||
|
const deviceInfo = this.crypto.deviceList.getDeviceByIdentityKey(
|
||||||
|
olmlib.OLM_ALGORITHM,
|
||||||
|
senderKey,
|
||||||
|
);
|
||||||
|
const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(
|
||||||
|
olmlib.OLM_ALGORITHM,
|
||||||
|
senderKey,
|
||||||
|
);
|
||||||
|
if (senderKeyUser !== event.getSender()) {
|
||||||
|
logger.error("sending device does not belong to the user it claims to be from");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const outgoingRequests = deviceInfo ? await this.crypto.cryptoStore.getOutgoingRoomKeyRequestsByTarget(
|
||||||
|
event.getSender(), deviceInfo.deviceId, [RoomKeyRequestState.Sent],
|
||||||
|
) : [];
|
||||||
|
const weRequested = outgoingRequests.some((req) => (
|
||||||
|
req.requestBody.room_id === content.room_id && req.requestBody.session_id === content.session_id
|
||||||
|
));
|
||||||
|
const room = this.baseApis.getRoom(content.room_id);
|
||||||
|
const memberEvent = room?.getMember(this.userId)?.events.member;
|
||||||
|
const fromInviter = memberEvent?.getSender() === event.getSender() ||
|
||||||
|
(memberEvent?.getUnsigned()?.prev_sender === event.getSender() &&
|
||||||
|
memberEvent?.getPrevContent()?.membership === "invite");
|
||||||
|
const fromUs = event.getSender() === this.baseApis.getUserId();
|
||||||
|
|
||||||
|
if (!weRequested && !fromUs) {
|
||||||
|
// If someone sends us an unsolicited key and they're
|
||||||
|
// not one of our other devices and it's not shared
|
||||||
|
// history, ignore it
|
||||||
|
if (!extraSessionData.sharedHistory) {
|
||||||
|
logger.log("forwarded key not shared history - ignoring");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If someone sends us an unsolicited key for a room
|
||||||
|
// we're already in, and they're not one of our other
|
||||||
|
// devices or the one who invited us, ignore it
|
||||||
|
if (room && !fromInviter) {
|
||||||
|
logger.log("forwarded key not from inviter or from us - ignoring");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exportFormat = true;
|
exportFormat = true;
|
||||||
forwardingKeyChain = Array.isArray(content.forwarding_curve25519_key_chain) ?
|
forwardingKeyChain = Array.isArray(content.forwarding_curve25519_key_chain) ?
|
||||||
content.forwarding_curve25519_key_chain : [];
|
content.forwarding_curve25519_key_chain : [];
|
||||||
@@ -1418,7 +1474,6 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
logger.error("forwarded_room_key event is missing sender_key field");
|
logger.error("forwarded_room_key event is missing sender_key field");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
senderKey = content.sender_key;
|
|
||||||
|
|
||||||
const ed25519Key = content.sender_claimed_ed25519_key;
|
const ed25519Key = content.sender_claimed_ed25519_key;
|
||||||
if (!ed25519Key) {
|
if (!ed25519Key) {
|
||||||
@@ -1431,11 +1486,45 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
keysClaimed = {
|
keysClaimed = {
|
||||||
ed25519: ed25519Key,
|
ed25519: ed25519Key,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If this is a key for a room we're not in, don't load it
|
||||||
|
// yet, just park it in case *this sender* invites us to
|
||||||
|
// that room later
|
||||||
|
if (!room) {
|
||||||
|
const parkedData = {
|
||||||
|
senderId: event.getSender(),
|
||||||
|
senderKey: content.sender_key,
|
||||||
|
sessionId: content.session_id,
|
||||||
|
sessionKey: content.session_key,
|
||||||
|
keysClaimed,
|
||||||
|
forwardingCurve25519KeyChain: forwardingKeyChain,
|
||||||
|
};
|
||||||
|
await this.crypto.cryptoStore.doTxn(
|
||||||
|
'readwrite',
|
||||||
|
['parked_shared_history'],
|
||||||
|
(txn) => this.crypto.cryptoStore.addParkedSharedHistory(content.room_id, parkedData, txn),
|
||||||
|
logger.withPrefix("[addParkedSharedHistory]"),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendingDevice = this.crypto.deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, senderKey);
|
||||||
|
const deviceTrust = this.crypto.checkDeviceInfoTrust(event.getSender(), sendingDevice);
|
||||||
|
|
||||||
|
if (fromUs && !deviceTrust.isVerified()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// forwarded keys are always untrusted
|
||||||
|
extraSessionData.untrusted = true;
|
||||||
|
|
||||||
|
// replace the sender key with the sender key of the session
|
||||||
|
// creator for storage
|
||||||
|
senderKey = content.sender_key;
|
||||||
} else {
|
} else {
|
||||||
keysClaimed = event.getKeysClaimed();
|
keysClaimed = event.getKeysClaimed();
|
||||||
}
|
}
|
||||||
|
|
||||||
const extraSessionData: any = {};
|
|
||||||
if (content["org.matrix.msc3061.shared_history"]) {
|
if (content["org.matrix.msc3061.shared_history"]) {
|
||||||
extraSessionData.sharedHistory = true;
|
extraSessionData.sharedHistory = true;
|
||||||
}
|
}
|
||||||
@@ -1453,7 +1542,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// have another go at decrypting events sent with this session.
|
// have another go at decrypting events sent with this session.
|
||||||
if (await this.retryDecryption(senderKey, content.session_id)) {
|
if (await this.retryDecryption(senderKey, content.session_id, !extraSessionData.untrusted)) {
|
||||||
// cancel any outstanding room key requests for this session.
|
// cancel any outstanding room key requests for this session.
|
||||||
// Only do this if we managed to decrypt every message in the
|
// Only do this if we managed to decrypt every message in the
|
||||||
// session, because if we didn't, we leave the other key
|
// session, because if we didn't, we leave the other key
|
||||||
@@ -1668,7 +1757,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
session: IMegolmSessionData,
|
session: IMegolmSessionData,
|
||||||
opts: { untrusted?: boolean, source?: string } = {},
|
opts: { untrusted?: boolean, source?: string } = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const extraSessionData: any = {};
|
const extraSessionData: OlmGroupSessionExtraData = {};
|
||||||
if (opts.untrusted || session.untrusted) {
|
if (opts.untrusted || session.untrusted) {
|
||||||
extraSessionData.untrusted = true;
|
extraSessionData.untrusted = true;
|
||||||
}
|
}
|
||||||
@@ -1696,7 +1785,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
// have another go at decrypting events sent with this session.
|
// have another go at decrypting events sent with this session.
|
||||||
this.retryDecryption(session.sender_key, session.session_id);
|
this.retryDecryption(session.sender_key, session.session_id, !extraSessionData.untrusted);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1707,11 +1796,18 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
* @private
|
* @private
|
||||||
* @param {String} senderKey
|
* @param {String} senderKey
|
||||||
* @param {String} sessionId
|
* @param {String} sessionId
|
||||||
|
* @param {Boolean} forceRedecryptIfUntrusted whether messages that were already
|
||||||
|
* successfully decrypted using untrusted keys should be re-decrypted
|
||||||
*
|
*
|
||||||
* @return {Boolean} whether all messages were successfully decrypted
|
* @return {Boolean} whether all messages were successfully
|
||||||
|
* decrypted with trusted keys
|
||||||
*/
|
*/
|
||||||
private async retryDecryption(senderKey: string, sessionId: string): Promise<boolean> {
|
private async retryDecryption(
|
||||||
const senderPendingEvents = this.pendingEvents[senderKey];
|
senderKey: string,
|
||||||
|
sessionId: string,
|
||||||
|
forceRedecryptIfUntrusted?: boolean,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const senderPendingEvents = this.pendingEvents.get(senderKey);
|
||||||
if (!senderPendingEvents) {
|
if (!senderPendingEvents) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1725,23 +1821,24 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
|
|
||||||
await Promise.all([...pending].map(async (ev) => {
|
await Promise.all([...pending].map(async (ev) => {
|
||||||
try {
|
try {
|
||||||
await ev.attemptDecryption(this.crypto, { isRetry: true });
|
await ev.attemptDecryption(this.crypto, { isRetry: true, forceRedecryptIfUntrusted });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// don't die if something goes wrong
|
// don't die if something goes wrong
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// If decrypted successfully, they'll have been removed from pendingEvents
|
// If decrypted successfully with trusted keys, they'll have
|
||||||
return !this.pendingEvents[senderKey]?.has(sessionId);
|
// been removed from pendingEvents
|
||||||
|
return !this.pendingEvents.get(senderKey)?.has(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async retryDecryptionFromSender(senderKey: string): Promise<boolean> {
|
public async retryDecryptionFromSender(senderKey: string): Promise<boolean> {
|
||||||
const senderPendingEvents = this.pendingEvents[senderKey];
|
const senderPendingEvents = this.pendingEvents.get(senderKey);
|
||||||
if (!senderPendingEvents) {
|
if (!senderPendingEvents) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
delete this.pendingEvents[senderKey];
|
this.pendingEvents.delete(senderKey);
|
||||||
|
|
||||||
await Promise.all([...senderPendingEvents].map(async ([_sessionId, pending]) => {
|
await Promise.all([...senderPendingEvents].map(async ([_sessionId, pending]) => {
|
||||||
await Promise.all([...pending].map(async (ev) => {
|
await Promise.all([...pending].map(async (ev) => {
|
||||||
@@ -1753,7 +1850,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
}));
|
}));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return !this.pendingEvents[senderKey];
|
return !this.pendingEvents.has(senderKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendSharedHistoryInboundSessions(devicesByUser: Record<string, DeviceInfo[]>): Promise<void> {
|
public async sendSharedHistoryInboundSessions(devicesByUser: Record<string, DeviceInfo[]>): Promise<void> {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
registerAlgorithm,
|
registerAlgorithm,
|
||||||
} from "./base";
|
} from "./base";
|
||||||
import { Room } from '../../models/room';
|
import { Room } from '../../models/room';
|
||||||
import { MatrixEvent } from "../..";
|
import { MatrixEvent } from "../../models/event";
|
||||||
import { IEventDecryptionResult } from "../index";
|
import { IEventDecryptionResult } from "../index";
|
||||||
import { IInboundSession } from "../OlmDevice";
|
import { IInboundSession } from "../OlmDevice";
|
||||||
|
|
||||||
@@ -222,6 +222,26 @@ class OlmDecryption extends DecryptionAlgorithm {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check that the device that encrypted the event belongs to the user
|
||||||
|
// that the event claims it's from. We need to make sure that our
|
||||||
|
// device list is up-to-date. If the device is unknown, we can only
|
||||||
|
// assume that the device logged out. Some event handlers, such as
|
||||||
|
// secret sharing, may be more strict and reject events that come from
|
||||||
|
// unknown devices.
|
||||||
|
await this.crypto.deviceList.downloadKeys([event.getSender()], false);
|
||||||
|
const senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(
|
||||||
|
olmlib.OLM_ALGORITHM,
|
||||||
|
deviceKey,
|
||||||
|
);
|
||||||
|
if (senderKeyUser !== event.getSender() && senderKeyUser !== undefined) {
|
||||||
|
throw new DecryptionError(
|
||||||
|
"OLM_BAD_SENDER",
|
||||||
|
"Message claimed to be from " + event.getSender(), {
|
||||||
|
real_sender: senderKeyUser,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// check that the original sender matches what the homeserver told us, to
|
// check that the original sender matches what the homeserver told us, to
|
||||||
// avoid people masquerading as others.
|
// avoid people masquerading as others.
|
||||||
// (this check is also provided via the sender's embedded ed25519 key,
|
// (this check is also provided via the sender's embedded ed25519 key,
|
||||||
|
|||||||
@@ -431,7 +431,6 @@ export class BackupManager {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
ret.usable = ret.usable || ret.trusted_locally;
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -278,9 +278,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
private oneTimeKeyCheckInProgress = false;
|
private oneTimeKeyCheckInProgress = false;
|
||||||
|
|
||||||
// EncryptionAlgorithm instance for each room
|
// EncryptionAlgorithm instance for each room
|
||||||
private roomEncryptors: Record<string, EncryptionAlgorithm> = {};
|
private roomEncryptors = new Map<string, EncryptionAlgorithm>();
|
||||||
// map from algorithm to DecryptionAlgorithm instance, for each room
|
// map from algorithm to DecryptionAlgorithm instance, for each room
|
||||||
private roomDecryptors: Record<string, Record<string, DecryptionAlgorithm>> = {};
|
private roomDecryptors = new Map<string, Map<string, DecryptionAlgorithm>>();
|
||||||
|
|
||||||
private deviceKeys: Record<string, string> = {}; // type: key
|
private deviceKeys: Record<string, string> = {}; // type: key
|
||||||
|
|
||||||
@@ -422,7 +422,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
this.deviceList.on(CryptoEvent.UserCrossSigningUpdated, this.onDeviceListUserCrossSigningUpdated);
|
this.deviceList.on(CryptoEvent.UserCrossSigningUpdated, this.onDeviceListUserCrossSigningUpdated);
|
||||||
this.reEmitter.reEmit(this.deviceList, [CryptoEvent.DevicesUpdated, CryptoEvent.WillUpdateDevices]);
|
this.reEmitter.reEmit(this.deviceList, [CryptoEvent.DevicesUpdated, CryptoEvent.WillUpdateDevices]);
|
||||||
|
|
||||||
this.supportedAlgorithms = Object.keys(algorithms.DECRYPTION_CLASSES);
|
this.supportedAlgorithms = Array.from(algorithms.DECRYPTION_CLASSES.keys());
|
||||||
|
|
||||||
this.outgoingRoomKeyRequestManager = new OutgoingRoomKeyRequestManager(
|
this.outgoingRoomKeyRequestManager = new OutgoingRoomKeyRequestManager(
|
||||||
baseApis, this.deviceId, this.cryptoStore,
|
baseApis, this.deviceId, this.cryptoStore,
|
||||||
@@ -2105,6 +2105,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
* @param {?boolean} known whether to mark that the user has been made aware of
|
* @param {?boolean} known whether to mark that the user has been made aware of
|
||||||
* the existence of this device. Null to leave unchanged
|
* the existence of this device. Null to leave unchanged
|
||||||
*
|
*
|
||||||
|
* @param {?Record<string, any>} keys The list of keys that was present
|
||||||
|
* during the device verification. This will be double checked with the list
|
||||||
|
* of keys the given device has currently.
|
||||||
|
*
|
||||||
* @return {Promise<module:crypto/deviceinfo>} updated DeviceInfo
|
* @return {Promise<module:crypto/deviceinfo>} updated DeviceInfo
|
||||||
*/
|
*/
|
||||||
public async setDeviceVerification(
|
public async setDeviceVerification(
|
||||||
@@ -2113,6 +2117,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
verified?: boolean,
|
verified?: boolean,
|
||||||
blocked?: boolean,
|
blocked?: boolean,
|
||||||
known?: boolean,
|
known?: boolean,
|
||||||
|
keys?: Record<string, string>,
|
||||||
): Promise<DeviceInfo | CrossSigningInfo> {
|
): Promise<DeviceInfo | CrossSigningInfo> {
|
||||||
// get rid of any `undefined`s here so we can just check
|
// get rid of any `undefined`s here so we can just check
|
||||||
// for null rather than null or undefined
|
// for null rather than null or undefined
|
||||||
@@ -2131,6 +2136,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
if (!verified) {
|
if (!verified) {
|
||||||
throw new Error("Cannot set a cross-signing key as unverified");
|
throw new Error("Cannot set a cross-signing key as unverified");
|
||||||
}
|
}
|
||||||
|
const gotKeyId = keys ? Object.values(keys)[0] : null;
|
||||||
|
if (keys && (Object.values(keys).length !== 1 || gotKeyId !== xsk.getId())) {
|
||||||
|
throw new Error(`Key did not match expected value: expected ${xsk.getId()}, got ${gotKeyId}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) {
|
if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) {
|
||||||
this.storeTrustedSelfKeys(xsk.keys);
|
this.storeTrustedSelfKeys(xsk.keys);
|
||||||
@@ -2191,6 +2200,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
let verificationStatus = dev.verified;
|
let verificationStatus = dev.verified;
|
||||||
|
|
||||||
if (verified) {
|
if (verified) {
|
||||||
|
if (keys) {
|
||||||
|
for (const [keyId, key] of Object.entries(keys)) {
|
||||||
|
if (dev.keys[keyId] !== key) {
|
||||||
|
throw new Error(`Key did not match expected value: expected ${key}, got ${dev.keys[keyId]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
verificationStatus = DeviceVerification.VERIFIED;
|
verificationStatus = DeviceVerification.VERIFIED;
|
||||||
} else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) {
|
} else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) {
|
||||||
verificationStatus = DeviceVerification.UNVERIFIED;
|
verificationStatus = DeviceVerification.UNVERIFIED;
|
||||||
@@ -2400,13 +2416,6 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const forwardingChain = event.getForwardingCurve25519KeyChain();
|
|
||||||
if (forwardingChain.length > 0) {
|
|
||||||
// we got the key this event from somewhere else
|
|
||||||
// TODO: check if we can trust the forwarders.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.isKeySourceUntrusted()) {
|
if (event.isKeySourceUntrusted()) {
|
||||||
// we got the key for this event from a source that we consider untrusted
|
// we got the key for this event from a source that we consider untrusted
|
||||||
return null;
|
return null;
|
||||||
@@ -2478,8 +2487,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
}
|
}
|
||||||
ret.encrypted = true;
|
ret.encrypted = true;
|
||||||
|
|
||||||
const forwardingChain = event.getForwardingCurve25519KeyChain();
|
if (event.isKeySourceUntrusted()) {
|
||||||
if (forwardingChain.length > 0 || event.isKeySourceUntrusted()) {
|
|
||||||
// we got the key this event from somewhere else
|
// we got the key this event from somewhere else
|
||||||
// TODO: check if we can trust the forwarders.
|
// TODO: check if we can trust the forwarders.
|
||||||
ret.authenticated = false;
|
ret.authenticated = false;
|
||||||
@@ -2527,7 +2535,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
* This should not normally be necessary.
|
* This should not normally be necessary.
|
||||||
*/
|
*/
|
||||||
public forceDiscardSession(roomId: string): void {
|
public forceDiscardSession(roomId: string): void {
|
||||||
const alg = this.roomEncryptors[roomId];
|
const alg = this.roomEncryptors.get(roomId);
|
||||||
if (alg === undefined) throw new Error("Room not encrypted");
|
if (alg === undefined) throw new Error("Room not encrypted");
|
||||||
if (alg.forceDiscardSession === undefined) {
|
if (alg.forceDiscardSession === undefined) {
|
||||||
throw new Error("Room encryption algorithm doesn't support session discarding");
|
throw new Error("Room encryption algorithm doesn't support session discarding");
|
||||||
@@ -2580,7 +2588,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
// the encryption event would appear in both.
|
// the encryption event would appear in both.
|
||||||
// If it's called more than twice though,
|
// If it's called more than twice though,
|
||||||
// it signals a bug on client or server.
|
// it signals a bug on client or server.
|
||||||
const existingAlg = this.roomEncryptors[roomId];
|
const existingAlg = this.roomEncryptors.get(roomId);
|
||||||
if (existingAlg) {
|
if (existingAlg) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2594,7 +2602,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
storeConfigPromise = this.roomList.setRoomEncryption(roomId, config);
|
storeConfigPromise = this.roomList.setRoomEncryption(roomId, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
const AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm];
|
const AlgClass = algorithms.ENCRYPTION_CLASSES.get(config.algorithm);
|
||||||
if (!AlgClass) {
|
if (!AlgClass) {
|
||||||
throw new Error("Unable to encrypt with " + config.algorithm);
|
throw new Error("Unable to encrypt with " + config.algorithm);
|
||||||
}
|
}
|
||||||
@@ -2608,7 +2616,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
roomId,
|
roomId,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
this.roomEncryptors[roomId] = alg;
|
this.roomEncryptors.set(roomId, alg);
|
||||||
|
|
||||||
if (storeConfigPromise) {
|
if (storeConfigPromise) {
|
||||||
await storeConfigPromise;
|
await storeConfigPromise;
|
||||||
@@ -2640,7 +2648,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
public trackRoomDevices(roomId: string): Promise<void> {
|
public trackRoomDevices(roomId: string): Promise<void> {
|
||||||
const trackMembers = async () => {
|
const trackMembers = async () => {
|
||||||
// not an encrypted room
|
// not an encrypted room
|
||||||
if (!this.roomEncryptors[roomId]) {
|
if (!this.roomEncryptors.has(roomId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const room = this.clientStore.getRoom(roomId);
|
const room = this.clientStore.getRoom(roomId);
|
||||||
@@ -2785,7 +2793,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
* @param {module:models/room} room the room the event is in
|
* @param {module:models/room} room the room the event is in
|
||||||
*/
|
*/
|
||||||
public prepareToEncrypt(room: Room): void {
|
public prepareToEncrypt(room: Room): void {
|
||||||
const alg = this.roomEncryptors[room.roomId];
|
const alg = this.roomEncryptors.get(room.roomId);
|
||||||
if (alg) {
|
if (alg) {
|
||||||
alg.prepareToEncrypt(room);
|
alg.prepareToEncrypt(room);
|
||||||
}
|
}
|
||||||
@@ -2808,7 +2816,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
|
|
||||||
const roomId = event.getRoomId();
|
const roomId = event.getRoomId();
|
||||||
|
|
||||||
const alg = this.roomEncryptors[roomId];
|
const alg = this.roomEncryptors.get(roomId);
|
||||||
if (!alg) {
|
if (!alg) {
|
||||||
// MatrixClient has already checked that this room should be encrypted,
|
// MatrixClient has already checked that this room should be encrypted,
|
||||||
// so this is an unexpected situation.
|
// so this is an unexpected situation.
|
||||||
@@ -3097,7 +3105,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
private getTrackedE2eRooms(): Room[] {
|
private getTrackedE2eRooms(): Room[] {
|
||||||
return this.clientStore.getRooms().filter((room) => {
|
return this.clientStore.getRooms().filter((room) => {
|
||||||
// check for rooms with encryption enabled
|
// check for rooms with encryption enabled
|
||||||
const alg = this.roomEncryptors[room.roomId];
|
const alg = this.roomEncryptors.get(room.roomId);
|
||||||
if (!alg) {
|
if (!alg) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -3533,7 +3541,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
|
|
||||||
const roomId = member.roomId;
|
const roomId = member.roomId;
|
||||||
|
|
||||||
const alg = this.roomEncryptors[roomId];
|
const alg = this.roomEncryptors.get(roomId);
|
||||||
if (!alg) {
|
if (!alg) {
|
||||||
// not encrypting in this room
|
// not encrypting in this room
|
||||||
return;
|
return;
|
||||||
@@ -3634,11 +3642,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
` for ${roomId} / ${body.session_id} (id ${req.requestId})`);
|
` for ${roomId} / ${body.session_id} (id ${req.requestId})`);
|
||||||
|
|
||||||
if (userId !== this.userId) {
|
if (userId !== this.userId) {
|
||||||
if (!this.roomEncryptors[roomId]) {
|
if (!this.roomEncryptors.get(roomId)) {
|
||||||
logger.debug(`room key request for unencrypted room ${roomId}`);
|
logger.debug(`room key request for unencrypted room ${roomId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const encryptor = this.roomEncryptors[roomId];
|
const encryptor = this.roomEncryptors.get(roomId);
|
||||||
const device = this.deviceList.getStoredDevice(userId, deviceId);
|
const device = this.deviceList.getStoredDevice(userId, deviceId);
|
||||||
if (!device) {
|
if (!device) {
|
||||||
logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`);
|
logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`);
|
||||||
@@ -3674,12 +3682,12 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
|
|
||||||
// if we don't have a decryptor for this room/alg, we don't have
|
// if we don't have a decryptor for this room/alg, we don't have
|
||||||
// the keys for the requested events, and can drop the requests.
|
// the keys for the requested events, and can drop the requests.
|
||||||
if (!this.roomDecryptors[roomId]) {
|
if (!this.roomDecryptors.has(roomId)) {
|
||||||
logger.log(`room key request for unencrypted room ${roomId}`);
|
logger.log(`room key request for unencrypted room ${roomId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptor = this.roomDecryptors[roomId][alg];
|
const decryptor = this.roomDecryptors.get(roomId).get(alg);
|
||||||
if (!decryptor) {
|
if (!decryptor) {
|
||||||
logger.log(`room key request for unknown alg ${alg} in room ${roomId}`);
|
logger.log(`room key request for unknown alg ${alg} in room ${roomId}`);
|
||||||
return;
|
return;
|
||||||
@@ -3745,23 +3753,24 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
* unknown
|
* unknown
|
||||||
*/
|
*/
|
||||||
public getRoomDecryptor(roomId: string, algorithm: string): DecryptionAlgorithm {
|
public getRoomDecryptor(roomId: string, algorithm: string): DecryptionAlgorithm {
|
||||||
let decryptors: Record<string, DecryptionAlgorithm>;
|
let decryptors: Map<string, DecryptionAlgorithm>;
|
||||||
let alg: DecryptionAlgorithm;
|
let alg: DecryptionAlgorithm;
|
||||||
|
|
||||||
roomId = roomId || null;
|
roomId = roomId || null;
|
||||||
if (roomId) {
|
if (roomId) {
|
||||||
decryptors = this.roomDecryptors[roomId];
|
decryptors = this.roomDecryptors.get(roomId);
|
||||||
if (!decryptors) {
|
if (!decryptors) {
|
||||||
this.roomDecryptors[roomId] = decryptors = {};
|
decryptors = new Map<string, DecryptionAlgorithm>();
|
||||||
|
this.roomDecryptors.set(roomId, decryptors);
|
||||||
}
|
}
|
||||||
|
|
||||||
alg = decryptors[algorithm];
|
alg = decryptors.get(algorithm);
|
||||||
if (alg) {
|
if (alg) {
|
||||||
return alg;
|
return alg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const AlgClass = algorithms.DECRYPTION_CLASSES[algorithm];
|
const AlgClass = algorithms.DECRYPTION_CLASSES.get(algorithm);
|
||||||
if (!AlgClass) {
|
if (!AlgClass) {
|
||||||
throw new algorithms.DecryptionError(
|
throw new algorithms.DecryptionError(
|
||||||
'UNKNOWN_ENCRYPTION_ALGORITHM',
|
'UNKNOWN_ENCRYPTION_ALGORITHM',
|
||||||
@@ -3777,7 +3786,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (decryptors) {
|
if (decryptors) {
|
||||||
decryptors[algorithm] = alg;
|
decryptors.set(algorithm, alg);
|
||||||
}
|
}
|
||||||
return alg;
|
return alg;
|
||||||
}
|
}
|
||||||
@@ -3791,9 +3800,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
*/
|
*/
|
||||||
private getRoomDecryptors(algorithm: string): DecryptionAlgorithm[] {
|
private getRoomDecryptors(algorithm: string): DecryptionAlgorithm[] {
|
||||||
const decryptors = [];
|
const decryptors = [];
|
||||||
for (const d of Object.values(this.roomDecryptors)) {
|
for (const d of this.roomDecryptors.values()) {
|
||||||
if (algorithm in d) {
|
if (d.has(algorithm)) {
|
||||||
decryptors.push(d[algorithm]);
|
decryptors.push(d.get(algorithm));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return decryptors;
|
return decryptors;
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import { logger } from '../logger';
|
|||||||
import { IOneTimeKey } from "./dehydration";
|
import { IOneTimeKey } from "./dehydration";
|
||||||
import { IClaimOTKsResult, MatrixClient } from "../client";
|
import { IClaimOTKsResult, MatrixClient } from "../client";
|
||||||
import { ISignatures } from "../@types/signed";
|
import { ISignatures } from "../@types/signed";
|
||||||
|
import { MatrixEvent } from "../models/event";
|
||||||
|
import { EventType } from "../@types/event";
|
||||||
|
|
||||||
enum Algorithm {
|
enum Algorithm {
|
||||||
Olm = "m.olm.v1.curve25519-aes-sha2",
|
Olm = "m.olm.v1.curve25519-aes-sha2",
|
||||||
@@ -554,6 +556,22 @@ export function pkVerify(obj: IObject, pubKey: string, userId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that an event was encrypted using olm.
|
||||||
|
*/
|
||||||
|
export function isOlmEncrypted(event: MatrixEvent): boolean {
|
||||||
|
if (!event.getSenderKey()) {
|
||||||
|
logger.error("Event has no sender key (not encrypted?)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (event.getWireType() !== EventType.RoomMessageEncrypted ||
|
||||||
|
!(["m.olm.v1.curve25519-aes-sha2"].includes(event.getWireContent().algorithm))) {
|
||||||
|
logger.error("Event was not encrypted using an appropriate algorithm");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode a typed array of uint8 as base64.
|
* Encode a typed array of uint8 as base64.
|
||||||
* @param {Uint8Array} uint8Array The data to encode.
|
* @param {Uint8Array} uint8Array The data to encode.
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { ICrossSigningInfo } from "../CrossSigning";
|
|||||||
import { PrefixedLogger } from "../../logger";
|
import { PrefixedLogger } from "../../logger";
|
||||||
import { InboundGroupSessionData } from "../OlmDevice";
|
import { InboundGroupSessionData } from "../OlmDevice";
|
||||||
import { IEncryptedPayload } from "../aes";
|
import { IEncryptedPayload } from "../aes";
|
||||||
|
import { MatrixEvent } from "../../models/event";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal module. Definitions for storage for the crypto module
|
* Internal module. Definitions for storage for the crypto module
|
||||||
@@ -127,6 +128,8 @@ export interface CryptoStore {
|
|||||||
roomId: string,
|
roomId: string,
|
||||||
txn?: unknown,
|
txn?: unknown,
|
||||||
): Promise<[senderKey: string, sessionId: string][]>;
|
): Promise<[senderKey: string, sessionId: string][]>;
|
||||||
|
addParkedSharedHistory(roomId: string, data: ParkedSharedHistory, txn?: unknown): void;
|
||||||
|
takeParkedSharedHistory(roomId: string, txn?: unknown): Promise<ParkedSharedHistory[]>;
|
||||||
|
|
||||||
// Session key backups
|
// Session key backups
|
||||||
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>;
|
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>;
|
||||||
@@ -203,3 +206,12 @@ export interface OutgoingRoomKeyRequest {
|
|||||||
requestBody: IRoomKeyRequestBody;
|
requestBody: IRoomKeyRequestBody;
|
||||||
state: RoomKeyRequestState;
|
state: RoomKeyRequestState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ParkedSharedHistory {
|
||||||
|
senderId: string;
|
||||||
|
senderKey: string;
|
||||||
|
sessionId: string;
|
||||||
|
sessionKey: string;
|
||||||
|
keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>; // XXX: Less type dependence on MatrixEvent
|
||||||
|
forwardingCurve25519KeyChain: string[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,15 +25,15 @@ import {
|
|||||||
IWithheld,
|
IWithheld,
|
||||||
Mode,
|
Mode,
|
||||||
OutgoingRoomKeyRequest,
|
OutgoingRoomKeyRequest,
|
||||||
|
ParkedSharedHistory,
|
||||||
} from "./base";
|
} from "./base";
|
||||||
import { IRoomKeyRequestBody } from "../index";
|
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
|
||||||
import { ICrossSigningKey } from "../../client";
|
import { ICrossSigningKey } from "../../client";
|
||||||
import { IOlmDevice } from "../algorithms/megolm";
|
import { IOlmDevice } from "../algorithms/megolm";
|
||||||
import { IRoomEncryption } from "../RoomList";
|
import { IRoomEncryption } from "../RoomList";
|
||||||
import { InboundGroupSessionData } from "../OlmDevice";
|
import { InboundGroupSessionData } from "../OlmDevice";
|
||||||
import { IEncryptedPayload } from "../aes";
|
import { IEncryptedPayload } from "../aes";
|
||||||
|
|
||||||
export const VERSION = 10;
|
|
||||||
const PROFILE_TRANSACTIONS = false;
|
const PROFILE_TRANSACTIONS = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -261,7 +261,9 @@ export class Backend implements CryptoStore {
|
|||||||
const cursor = this.result;
|
const cursor = this.result;
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
const keyReq = cursor.value;
|
const keyReq = cursor.value;
|
||||||
if (keyReq.recipients.includes({ userId, deviceId })) {
|
if (keyReq.recipients.some((recipient: IRoomKeyRequestRecipient) =>
|
||||||
|
recipient.userId === userId && recipient.deviceId === deviceId,
|
||||||
|
)) {
|
||||||
results.push(keyReq);
|
results.push(keyReq);
|
||||||
}
|
}
|
||||||
cursor.continue();
|
cursor.continue();
|
||||||
@@ -871,6 +873,50 @@ export class Backend implements CryptoStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addParkedSharedHistory(
|
||||||
|
roomId: string,
|
||||||
|
parkedData: ParkedSharedHistory,
|
||||||
|
txn?: IDBTransaction,
|
||||||
|
): void {
|
||||||
|
if (!txn) {
|
||||||
|
txn = this.db.transaction(
|
||||||
|
"parked_shared_history", "readwrite",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const objectStore = txn.objectStore("parked_shared_history");
|
||||||
|
const req = objectStore.get([roomId]);
|
||||||
|
req.onsuccess = () => {
|
||||||
|
const { parked } = req.result || { parked: [] };
|
||||||
|
parked.push(parkedData);
|
||||||
|
objectStore.put({ roomId, parked });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public takeParkedSharedHistory(
|
||||||
|
roomId: string,
|
||||||
|
txn?: IDBTransaction,
|
||||||
|
): Promise<ParkedSharedHistory[]> {
|
||||||
|
if (!txn) {
|
||||||
|
txn = this.db.transaction(
|
||||||
|
"parked_shared_history", "readwrite",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const cursorReq = txn.objectStore("parked_shared_history").openCursor(roomId);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
cursorReq.onsuccess = () => {
|
||||||
|
const cursor = cursorReq.result;
|
||||||
|
if (!cursor) {
|
||||||
|
resolve([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = cursor.value;
|
||||||
|
cursor.delete();
|
||||||
|
resolve(data);
|
||||||
|
};
|
||||||
|
cursorReq.onerror = reject;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public doTxn<T>(
|
public doTxn<T>(
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
stores: string | string[],
|
stores: string | string[],
|
||||||
@@ -903,45 +949,34 @@ export class Backend implements CryptoStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
|
type DbMigration = (db: IDBDatabase) => void;
|
||||||
logger.log(
|
const DB_MIGRATIONS: DbMigration[] = [
|
||||||
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
|
(db) => { createDatabase(db); },
|
||||||
+ ` to ${VERSION}`,
|
(db) => { db.createObjectStore("account"); },
|
||||||
);
|
(db) => {
|
||||||
if (oldVersion < 1) { // The database did not previously exist.
|
|
||||||
createDatabase(db);
|
|
||||||
}
|
|
||||||
if (oldVersion < 2) {
|
|
||||||
db.createObjectStore("account");
|
|
||||||
}
|
|
||||||
if (oldVersion < 3) {
|
|
||||||
const sessionsStore = db.createObjectStore("sessions", {
|
const sessionsStore = db.createObjectStore("sessions", {
|
||||||
keyPath: ["deviceKey", "sessionId"],
|
keyPath: ["deviceKey", "sessionId"],
|
||||||
});
|
});
|
||||||
sessionsStore.createIndex("deviceKey", "deviceKey");
|
sessionsStore.createIndex("deviceKey", "deviceKey");
|
||||||
}
|
},
|
||||||
if (oldVersion < 4) {
|
(db) => {
|
||||||
db.createObjectStore("inbound_group_sessions", {
|
db.createObjectStore("inbound_group_sessions", {
|
||||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
if (oldVersion < 5) {
|
(db) => { db.createObjectStore("device_data"); },
|
||||||
db.createObjectStore("device_data");
|
(db) => { db.createObjectStore("rooms"); },
|
||||||
}
|
(db) => {
|
||||||
if (oldVersion < 6) {
|
|
||||||
db.createObjectStore("rooms");
|
|
||||||
}
|
|
||||||
if (oldVersion < 7) {
|
|
||||||
db.createObjectStore("sessions_needing_backup", {
|
db.createObjectStore("sessions_needing_backup", {
|
||||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
if (oldVersion < 8) {
|
(db) => {
|
||||||
db.createObjectStore("inbound_group_sessions_withheld", {
|
db.createObjectStore("inbound_group_sessions_withheld", {
|
||||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
if (oldVersion < 9) {
|
(db) => {
|
||||||
const problemsStore = db.createObjectStore("session_problems", {
|
const problemsStore = db.createObjectStore("session_problems", {
|
||||||
keyPath: ["deviceKey", "time"],
|
keyPath: ["deviceKey", "time"],
|
||||||
});
|
});
|
||||||
@@ -950,13 +985,29 @@ export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
|
|||||||
db.createObjectStore("notified_error_devices", {
|
db.createObjectStore("notified_error_devices", {
|
||||||
keyPath: ["userId", "deviceId"],
|
keyPath: ["userId", "deviceId"],
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
if (oldVersion < 10) {
|
(db) => {
|
||||||
db.createObjectStore("shared_history_inbound_group_sessions", {
|
db.createObjectStore("shared_history_inbound_group_sessions", {
|
||||||
keyPath: ["roomId"],
|
keyPath: ["roomId"],
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
(db) => {
|
||||||
|
db.createObjectStore("parked_shared_history", {
|
||||||
|
keyPath: ["roomId"],
|
||||||
|
});
|
||||||
|
},
|
||||||
// Expand as needed.
|
// Expand as needed.
|
||||||
|
];
|
||||||
|
export const VERSION = DB_MIGRATIONS.length;
|
||||||
|
|
||||||
|
export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
|
||||||
|
logger.log(
|
||||||
|
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
|
||||||
|
+ ` to ${VERSION}`,
|
||||||
|
);
|
||||||
|
DB_MIGRATIONS.forEach((migration, index) => {
|
||||||
|
if (oldVersion <= index) migration(db);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDatabase(db: IDBDatabase): void {
|
function createDatabase(db: IDBDatabase): void {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
IWithheld,
|
IWithheld,
|
||||||
Mode,
|
Mode,
|
||||||
OutgoingRoomKeyRequest,
|
OutgoingRoomKeyRequest,
|
||||||
|
ParkedSharedHistory,
|
||||||
} from "./base";
|
} from "./base";
|
||||||
import { IRoomKeyRequestBody } from "../index";
|
import { IRoomKeyRequestBody } from "../index";
|
||||||
import { ICrossSigningKey } from "../../client";
|
import { ICrossSigningKey } from "../../client";
|
||||||
@@ -55,6 +56,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
|||||||
public static STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
|
public static STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
|
||||||
public static STORE_INBOUND_GROUP_SESSIONS_WITHHELD = 'inbound_group_sessions_withheld';
|
public static STORE_INBOUND_GROUP_SESSIONS_WITHHELD = 'inbound_group_sessions_withheld';
|
||||||
public static STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS = 'shared_history_inbound_group_sessions';
|
public static STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS = 'shared_history_inbound_group_sessions';
|
||||||
|
public static STORE_PARKED_SHARED_HISTORY = 'parked_shared_history';
|
||||||
public static STORE_DEVICE_DATA = 'device_data';
|
public static STORE_DEVICE_DATA = 'device_data';
|
||||||
public static STORE_ROOMS = 'rooms';
|
public static STORE_ROOMS = 'rooms';
|
||||||
public static STORE_BACKUP = 'sessions_needing_backup';
|
public static STORE_BACKUP = 'sessions_needing_backup';
|
||||||
@@ -669,6 +671,27 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
|||||||
return this.backend.getSharedHistoryInboundGroupSessions(roomId, txn);
|
return this.backend.getSharedHistoryInboundGroupSessions(roomId, txn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Park a shared-history group session for a room we may be invited to later.
|
||||||
|
*/
|
||||||
|
public addParkedSharedHistory(
|
||||||
|
roomId: string,
|
||||||
|
parkedData: ParkedSharedHistory,
|
||||||
|
txn?: IDBTransaction,
|
||||||
|
): void {
|
||||||
|
this.backend.addParkedSharedHistory(roomId, parkedData, txn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pop out all shared-history group sessions for a room.
|
||||||
|
*/
|
||||||
|
public takeParkedSharedHistory(
|
||||||
|
roomId: string,
|
||||||
|
txn?: IDBTransaction,
|
||||||
|
): Promise<ParkedSharedHistory[]> {
|
||||||
|
return this.backend.takeParkedSharedHistory(roomId, txn);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform a transaction on the crypto store. Any store methods
|
* Perform a transaction on the crypto store. Any store methods
|
||||||
* that require a transaction (txn) object to be passed in may
|
* that require a transaction (txn) object to be passed in may
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
IWithheld,
|
IWithheld,
|
||||||
Mode,
|
Mode,
|
||||||
OutgoingRoomKeyRequest,
|
OutgoingRoomKeyRequest,
|
||||||
|
ParkedSharedHistory,
|
||||||
} from "./base";
|
} from "./base";
|
||||||
import { IRoomKeyRequestBody } from "../index";
|
import { IRoomKeyRequestBody } from "../index";
|
||||||
import { ICrossSigningKey } from "../../client";
|
import { ICrossSigningKey } from "../../client";
|
||||||
@@ -58,6 +59,7 @@ export class MemoryCryptoStore implements CryptoStore {
|
|||||||
private rooms: { [roomId: string]: IRoomEncryption } = {};
|
private rooms: { [roomId: string]: IRoomEncryption } = {};
|
||||||
private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {};
|
private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {};
|
||||||
private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {};
|
private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {};
|
||||||
|
private parkedSharedHistory = new Map<string, ParkedSharedHistory[]>(); // keyed by room ID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure the database exists and is up-to-date.
|
* Ensure the database exists and is up-to-date.
|
||||||
@@ -191,11 +193,13 @@ export class MemoryCryptoStore implements CryptoStore {
|
|||||||
deviceId: string,
|
deviceId: string,
|
||||||
wantedStates: number[],
|
wantedStates: number[],
|
||||||
): Promise<OutgoingRoomKeyRequest[]> {
|
): Promise<OutgoingRoomKeyRequest[]> {
|
||||||
const results = [];
|
const results: OutgoingRoomKeyRequest[] = [];
|
||||||
|
|
||||||
for (const req of this.outgoingRoomKeyRequests) {
|
for (const req of this.outgoingRoomKeyRequests) {
|
||||||
for (const state of wantedStates) {
|
for (const state of wantedStates) {
|
||||||
if (req.state === state && req.recipients.includes({ userId, deviceId })) {
|
if (req.state === state && req.recipients.some(
|
||||||
|
(recipient) => recipient.userId === userId && recipient.deviceId === deviceId,
|
||||||
|
)) {
|
||||||
results.push(req);
|
results.push(req);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -524,6 +528,18 @@ export class MemoryCryptoStore implements CryptoStore {
|
|||||||
return Promise.resolve(this.sharedHistoryInboundGroupSessions[roomId] || []);
|
return Promise.resolve(this.sharedHistoryInboundGroupSessions[roomId] || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addParkedSharedHistory(roomId: string, parkedData: ParkedSharedHistory): void {
|
||||||
|
const parked = this.parkedSharedHistory.get(roomId) ?? [];
|
||||||
|
parked.push(parkedData);
|
||||||
|
this.parkedSharedHistory.set(roomId, parked);
|
||||||
|
}
|
||||||
|
|
||||||
|
public takeParkedSharedHistory(roomId: string): Promise<ParkedSharedHistory[]> {
|
||||||
|
const parked = this.parkedSharedHistory.get(roomId) ?? [];
|
||||||
|
this.parkedSharedHistory.delete(roomId);
|
||||||
|
return Promise.resolve(parked);
|
||||||
|
}
|
||||||
|
|
||||||
// Session key backups
|
// Session key backups
|
||||||
|
|
||||||
public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn?: unknown) => T): Promise<T> {
|
public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn?: unknown) => T): Promise<T> {
|
||||||
|
|||||||
@@ -299,7 +299,13 @@ export class VerificationBase<
|
|||||||
if (this.doVerification && !this.started) {
|
if (this.doVerification && !this.started) {
|
||||||
this.started = true;
|
this.started = true;
|
||||||
this.resetTimer(); // restart the timeout
|
this.resetTimer(); // restart the timeout
|
||||||
Promise.resolve(this.doVerification()).then(this.done.bind(this), this.cancel.bind(this));
|
new Promise<void>((resolve, reject) => {
|
||||||
|
const crossSignId = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(this.userId)?.getId();
|
||||||
|
if (crossSignId === this.deviceId) {
|
||||||
|
reject(new Error("Device ID is the same as the cross-signing ID"));
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
}).then(() => this.doVerification()).then(this.done.bind(this), this.cancel.bind(this));
|
||||||
}
|
}
|
||||||
return this.promise;
|
return this.promise;
|
||||||
}
|
}
|
||||||
@@ -310,14 +316,14 @@ export class VerificationBase<
|
|||||||
// we try to verify all the keys that we're told about, but we might
|
// we try to verify all the keys that we're told about, but we might
|
||||||
// not know about all of them, so keep track of the keys that we know
|
// not know about all of them, so keep track of the keys that we know
|
||||||
// about, and ignore the rest
|
// about, and ignore the rest
|
||||||
const verifiedDevices = [];
|
const verifiedDevices: [string, string, string][] = [];
|
||||||
|
|
||||||
for (const [keyId, keyInfo] of Object.entries(keys)) {
|
for (const [keyId, keyInfo] of Object.entries(keys)) {
|
||||||
const deviceId = keyId.split(':', 2)[1];
|
const deviceId = keyId.split(':', 2)[1];
|
||||||
const device = this.baseApis.getStoredDevice(userId, deviceId);
|
const device = this.baseApis.getStoredDevice(userId, deviceId);
|
||||||
if (device) {
|
if (device) {
|
||||||
verifier(keyId, device, keyInfo);
|
verifier(keyId, device, keyInfo);
|
||||||
verifiedDevices.push(deviceId);
|
verifiedDevices.push([deviceId, keyId, device.keys[keyId]]);
|
||||||
} else {
|
} else {
|
||||||
const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId);
|
const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId);
|
||||||
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
|
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
|
||||||
@@ -326,7 +332,7 @@ export class VerificationBase<
|
|||||||
[keyId]: deviceId,
|
[keyId]: deviceId,
|
||||||
},
|
},
|
||||||
}, deviceId), keyInfo);
|
}, deviceId), keyInfo);
|
||||||
verifiedDevices.push(deviceId);
|
verifiedDevices.push([deviceId, keyId, deviceId]);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`verification: Could not find device ${deviceId} to verify`,
|
`verification: Could not find device ${deviceId} to verify`,
|
||||||
@@ -348,8 +354,15 @@ export class VerificationBase<
|
|||||||
// TODO: There should probably be a batch version of this, otherwise it's going
|
// TODO: There should probably be a batch version of this, otherwise it's going
|
||||||
// to upload each signature in a separate API call which is silly because the
|
// to upload each signature in a separate API call which is silly because the
|
||||||
// API supports as many signatures as you like.
|
// API supports as many signatures as you like.
|
||||||
for (const deviceId of verifiedDevices) {
|
for (const [deviceId, keyId, key] of verifiedDevices) {
|
||||||
await this.baseApis.setDeviceVerified(userId, deviceId);
|
await this.baseApis.crypto.setDeviceVerification(userId, deviceId, true, null, null, { [keyId]: key });
|
||||||
|
}
|
||||||
|
|
||||||
|
// if one of the user's own devices is being marked as verified / unverified,
|
||||||
|
// check the key backup status, since whether or not we use this depends on
|
||||||
|
// whether it has a signature from a verified device
|
||||||
|
if (userId == this.baseApis.credentials.userId) {
|
||||||
|
await this.baseApis.checkKeyBackup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
|
|||||||
export interface MapperOpts {
|
export interface MapperOpts {
|
||||||
preventReEmit?: boolean;
|
preventReEmit?: boolean;
|
||||||
decrypt?: boolean;
|
decrypt?: boolean;
|
||||||
|
toDevice?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper {
|
export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper {
|
||||||
@@ -29,6 +30,10 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
|||||||
const decrypt = options.decrypt !== false;
|
const decrypt = options.decrypt !== false;
|
||||||
|
|
||||||
function mapper(plainOldJsObject: Partial<IEvent>) {
|
function mapper(plainOldJsObject: Partial<IEvent>) {
|
||||||
|
if (options.toDevice) {
|
||||||
|
delete plainOldJsObject.room_id;
|
||||||
|
}
|
||||||
|
|
||||||
const room = client.getRoom(plainOldJsObject.room_id);
|
const room = client.getRoom(plainOldJsObject.room_id);
|
||||||
|
|
||||||
let event: MatrixEvent;
|
let event: MatrixEvent;
|
||||||
|
|||||||
62
src/feature.ts
Normal file
62
src/feature.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 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 { IServerVersions } from "./client";
|
||||||
|
|
||||||
|
export enum ServerSupport {
|
||||||
|
Stable,
|
||||||
|
Unstable,
|
||||||
|
Unsupported
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Feature {
|
||||||
|
Thread = "Thread",
|
||||||
|
ThreadUnreadNotifications = "ThreadUnreadNotifications",
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeatureSupportCondition = {
|
||||||
|
unstablePrefixes?: string[];
|
||||||
|
matrixVersion?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const featureSupportResolver: Record<string, FeatureSupportCondition> = {
|
||||||
|
[Feature.Thread]: {
|
||||||
|
unstablePrefixes: ["org.matrix.msc3440"],
|
||||||
|
matrixVersion: "v1.3",
|
||||||
|
},
|
||||||
|
[Feature.ThreadUnreadNotifications]: {
|
||||||
|
unstablePrefixes: ["org.matrix.msc3771", "org.matrix.msc3773"],
|
||||||
|
matrixVersion: "v1.4",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function buildFeatureSupportMap(versions: IServerVersions): Promise<Map<Feature, ServerSupport>> {
|
||||||
|
const supportMap = new Map<Feature, ServerSupport>();
|
||||||
|
for (const [feature, supportCondition] of Object.entries(featureSupportResolver)) {
|
||||||
|
const supportMatrixVersion = versions.versions?.includes(supportCondition.matrixVersion || "") ?? false;
|
||||||
|
const supportUnstablePrefixes = supportCondition.unstablePrefixes?.every(unstablePrefix => {
|
||||||
|
return versions.unstable_features?.[unstablePrefix] === true;
|
||||||
|
}) ?? false;
|
||||||
|
if (supportMatrixVersion) {
|
||||||
|
supportMap.set(feature as Feature, ServerSupport.Stable);
|
||||||
|
} else if (supportUnstablePrefixes) {
|
||||||
|
supportMap.set(feature as Feature, ServerSupport.Unstable);
|
||||||
|
} else {
|
||||||
|
supportMap.set(feature as Feature, ServerSupport.Unsupported);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return supportMap;
|
||||||
|
}
|
||||||
@@ -73,7 +73,7 @@ export interface IFilterComponent {
|
|||||||
* @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true }
|
* @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true }
|
||||||
*/
|
*/
|
||||||
export class FilterComponent {
|
export class FilterComponent {
|
||||||
constructor(private filterJson: IFilterComponent, public readonly userId?: string) {}
|
constructor(private filterJson: IFilterComponent, public readonly userId?: string | undefined | null) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks with the filter component matches the given event
|
* Checks with the filter component matches the given event
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
EventType,
|
EventType,
|
||||||
RelationType,
|
RelationType,
|
||||||
} from "./@types/event";
|
} from "./@types/event";
|
||||||
|
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync";
|
||||||
import { FilterComponent, IFilterComponent } from "./filter-component";
|
import { FilterComponent, IFilterComponent } from "./filter-component";
|
||||||
import { MatrixEvent } from "./models/event";
|
import { MatrixEvent } from "./models/event";
|
||||||
|
|
||||||
@@ -57,6 +58,8 @@ export interface IRoomEventFilter extends IFilterComponent {
|
|||||||
types?: Array<EventType | string>;
|
types?: Array<EventType | string>;
|
||||||
related_by_senders?: Array<RelationType | string>;
|
related_by_senders?: Array<RelationType | string>;
|
||||||
related_by_rel_types?: string[];
|
related_by_rel_types?: string[];
|
||||||
|
unread_thread_notifications?: boolean;
|
||||||
|
"org.matrix.msc3773.unread_thread_notifications"?: boolean;
|
||||||
|
|
||||||
// Unstable values
|
// Unstable values
|
||||||
"io.element.relation_senders"?: Array<RelationType | string>;
|
"io.element.relation_senders"?: Array<RelationType | string>;
|
||||||
@@ -97,7 +100,7 @@ export class Filter {
|
|||||||
* @param {Object} jsonObj
|
* @param {Object} jsonObj
|
||||||
* @return {Filter}
|
* @return {Filter}
|
||||||
*/
|
*/
|
||||||
public static fromJson(userId: string, filterId: string, jsonObj: IFilterDefinition): Filter {
|
public static fromJson(userId: string | undefined | null, filterId: string, jsonObj: IFilterDefinition): Filter {
|
||||||
const filter = new Filter(userId, filterId);
|
const filter = new Filter(userId, filterId);
|
||||||
filter.setDefinition(jsonObj);
|
filter.setDefinition(jsonObj);
|
||||||
return filter;
|
return filter;
|
||||||
@@ -107,7 +110,7 @@ export class Filter {
|
|||||||
private roomFilter: FilterComponent;
|
private roomFilter: FilterComponent;
|
||||||
private roomTimelineFilter: FilterComponent;
|
private roomTimelineFilter: FilterComponent;
|
||||||
|
|
||||||
constructor(public readonly userId: string, public filterId?: string) {}
|
constructor(public readonly userId: string | undefined | null, public filterId?: string) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the ID of this filter on your homeserver (if known)
|
* Get the ID of this filter on your homeserver (if known)
|
||||||
@@ -220,7 +223,24 @@ export class Filter {
|
|||||||
setProp(this.definition, "room.timeline.limit", limit);
|
setProp(this.definition, "room.timeline.limit", limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLazyLoadMembers(enabled: boolean) {
|
/**
|
||||||
|
* Enable threads unread notification
|
||||||
|
* @param {boolean} enabled
|
||||||
|
*/
|
||||||
|
public setUnreadThreadNotifications(enabled: boolean): void {
|
||||||
|
this.definition = {
|
||||||
|
...this.definition,
|
||||||
|
room: {
|
||||||
|
...this.definition?.room,
|
||||||
|
timeline: {
|
||||||
|
...this.definition?.room?.timeline,
|
||||||
|
[UNREAD_THREAD_NOTIFICATIONS.name]: !!enabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setLazyLoadMembers(enabled: boolean): void {
|
||||||
setProp(this.definition, "room.state.lazy_load_members", !!enabled);
|
setProp(this.definition, "room.state.lazy_load_members", !!enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -365,7 +365,7 @@ export class MatrixHttpApi {
|
|||||||
// we're setting opts.json=false so that it doesn't JSON-encode the
|
// we're setting opts.json=false so that it doesn't JSON-encode the
|
||||||
// request, which also means it doesn't JSON-decode the response. Either
|
// request, which also means it doesn't JSON-decode the response. Either
|
||||||
// way, we have to JSON-parse the response ourselves.
|
// way, we have to JSON-parse the response ourselves.
|
||||||
let bodyParser = null;
|
let bodyParser: ((body: string) => any) | undefined;
|
||||||
if (!rawResponse) {
|
if (!rawResponse) {
|
||||||
bodyParser = function(rawBody: string) {
|
bodyParser = function(rawBody: string) {
|
||||||
let body = JSON.parse(rawBody);
|
let body = JSON.parse(rawBody);
|
||||||
@@ -472,7 +472,7 @@ export class MatrixHttpApi {
|
|||||||
headers["Content-Length"] = "0";
|
headers["Content-Length"] = "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
promise = this.authedRequest(
|
promise = this.authedRequest<UploadContentResponseType<O>>(
|
||||||
opts.callback, Method.Post, "/upload", queryParams, body, {
|
opts.callback, Method.Post, "/upload", queryParams, body, {
|
||||||
prefix: "/_matrix/media/r0",
|
prefix: "/_matrix/media/r0",
|
||||||
headers,
|
headers,
|
||||||
@@ -590,10 +590,10 @@ export class MatrixHttpApi {
|
|||||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||||
*/
|
*/
|
||||||
public authedRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
public authedRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||||
callback: Callback<T>,
|
callback: Callback<T> | undefined,
|
||||||
method: Method,
|
method: Method,
|
||||||
path: string,
|
path: string,
|
||||||
queryParams?: Record<string, string | string[]>,
|
queryParams?: Record<string, string | string[] | undefined>,
|
||||||
data?: CoreOptions["body"],
|
data?: CoreOptions["body"],
|
||||||
opts?: O | number, // number is legacy
|
opts?: O | number, // number is legacy
|
||||||
): IAbortablePromise<ResponseType<T, O>> {
|
): IAbortablePromise<ResponseType<T, O>> {
|
||||||
@@ -667,7 +667,7 @@ export class MatrixHttpApi {
|
|||||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||||
*/
|
*/
|
||||||
public request<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
public request<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||||
callback: Callback<T>,
|
callback: Callback<T> | undefined,
|
||||||
method: Method,
|
method: Method,
|
||||||
path: string,
|
path: string,
|
||||||
queryParams?: CoreOptions["qs"],
|
queryParams?: CoreOptions["qs"],
|
||||||
@@ -711,7 +711,7 @@ export class MatrixHttpApi {
|
|||||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||||
*/
|
*/
|
||||||
public requestOtherUrl<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
public requestOtherUrl<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||||
callback: Callback<T>,
|
callback: Callback<T> | undefined,
|
||||||
method: Method,
|
method: Method,
|
||||||
uri: string,
|
uri: string,
|
||||||
queryParams?: CoreOptions["qs"],
|
queryParams?: CoreOptions["qs"],
|
||||||
@@ -778,7 +778,7 @@ export class MatrixHttpApi {
|
|||||||
* Generic O should be inferred
|
* Generic O should be inferred
|
||||||
*/
|
*/
|
||||||
private doRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
private doRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||||
callback: Callback<T>,
|
callback: Callback<T> | undefined,
|
||||||
method: Method,
|
method: Method,
|
||||||
uri: string,
|
uri: string,
|
||||||
queryParams?: Record<string, string>,
|
queryParams?: Record<string, string>,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { IContent, MatrixEvent } from "./event";
|
|||||||
import { MSC3089TreeSpace } from "./MSC3089TreeSpace";
|
import { MSC3089TreeSpace } from "./MSC3089TreeSpace";
|
||||||
import { EventTimeline } from "./event-timeline";
|
import { EventTimeline } from "./event-timeline";
|
||||||
import { FileType } from "../http-api";
|
import { FileType } from "../http-api";
|
||||||
import type { ISendEventResponse } from "..";
|
import type { ISendEventResponse } from "../@types/requests";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference
|
* Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { MBeaconEventContent } from "../@types/beacon";
|
import { MBeaconEventContent } from "../@types/beacon";
|
||||||
import { M_TIMESTAMP } from "../@types/location";
|
|
||||||
import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers";
|
import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers";
|
||||||
import { MatrixEvent } from "../matrix";
|
import { MatrixEvent } from "../matrix";
|
||||||
import { sortEventsByLatestContentTimestamp } from "../utils";
|
import { sortEventsByLatestContentTimestamp } from "../utils";
|
||||||
@@ -161,7 +160,9 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
|
|||||||
|
|
||||||
const validLocationEvents = beaconLocationEvents.filter(event => {
|
const validLocationEvents = beaconLocationEvents.filter(event => {
|
||||||
const content = event.getContent<MBeaconEventContent>();
|
const content = event.getContent<MBeaconEventContent>();
|
||||||
const timestamp = M_TIMESTAMP.findIn<number>(content);
|
const parsed = parseBeaconContent(content);
|
||||||
|
if (!parsed.uri || !parsed.timestamp) return false; // we won't be able to process these
|
||||||
|
const { timestamp } = parsed;
|
||||||
return (
|
return (
|
||||||
// only include positions that were taken inside the beacon's live period
|
// only include positions that were taken inside the beacon's live period
|
||||||
isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) &&
|
isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) &&
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
private readonly displayPendingEvents: boolean;
|
private readonly displayPendingEvents: boolean;
|
||||||
private liveTimeline: EventTimeline;
|
private liveTimeline: EventTimeline;
|
||||||
private timelines: EventTimeline[];
|
private timelines: EventTimeline[];
|
||||||
private _eventIdToTimeline: Record<string, EventTimeline>;
|
private _eventIdToTimeline = new Map<string, EventTimeline>();
|
||||||
private filter?: Filter;
|
private filter?: Filter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -123,12 +123,15 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
* @param {MatrixClient=} client the Matrix client which owns this EventTimelineSet,
|
* @param {MatrixClient=} client the Matrix client which owns this EventTimelineSet,
|
||||||
* can be omitted if room is specified.
|
* can be omitted if room is specified.
|
||||||
* @param {Thread=} thread the thread to which this timeline set relates.
|
* @param {Thread=} thread the thread to which this timeline set relates.
|
||||||
|
* @param {boolean} isThreadTimeline Whether this timeline set relates to a thread list timeline
|
||||||
|
* (e.g., All threads or My threads)
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
public readonly room: Room | undefined,
|
public readonly room: Room | undefined,
|
||||||
opts: IOpts = {},
|
opts: IOpts = {},
|
||||||
client?: MatrixClient,
|
client?: MatrixClient,
|
||||||
public readonly thread?: Thread,
|
public readonly thread?: Thread,
|
||||||
|
public readonly isThreadTimeline: boolean = false,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@@ -138,7 +141,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
|
|
||||||
// just a list - *not* ordered.
|
// just a list - *not* ordered.
|
||||||
this.timelines = [this.liveTimeline];
|
this.timelines = [this.liveTimeline];
|
||||||
this._eventIdToTimeline = {};
|
this._eventIdToTimeline = new Map<string, EventTimeline>();
|
||||||
|
|
||||||
this.filter = opts.filter;
|
this.filter = opts.filter;
|
||||||
|
|
||||||
@@ -210,7 +213,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
* @return {module:models/event-timeline~EventTimeline} timeline
|
* @return {module:models/event-timeline~EventTimeline} timeline
|
||||||
*/
|
*/
|
||||||
public eventIdToTimeline(eventId: string): EventTimeline {
|
public eventIdToTimeline(eventId: string): EventTimeline {
|
||||||
return this._eventIdToTimeline[eventId];
|
return this._eventIdToTimeline.get(eventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -220,10 +223,10 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
* @param {String} newEventId event ID of the replacement event
|
* @param {String} newEventId event ID of the replacement event
|
||||||
*/
|
*/
|
||||||
public replaceEventId(oldEventId: string, newEventId: string): void {
|
public replaceEventId(oldEventId: string, newEventId: string): void {
|
||||||
const existingTimeline = this._eventIdToTimeline[oldEventId];
|
const existingTimeline = this._eventIdToTimeline.get(oldEventId);
|
||||||
if (existingTimeline) {
|
if (existingTimeline) {
|
||||||
delete this._eventIdToTimeline[oldEventId];
|
this._eventIdToTimeline.delete(oldEventId);
|
||||||
this._eventIdToTimeline[newEventId] = existingTimeline;
|
this._eventIdToTimeline.set(newEventId, existingTimeline);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,7 +260,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
|
|
||||||
if (resetAllTimelines) {
|
if (resetAllTimelines) {
|
||||||
this.timelines = [newTimeline];
|
this.timelines = [newTimeline];
|
||||||
this._eventIdToTimeline = {};
|
this._eventIdToTimeline = new Map<string, EventTimeline>();
|
||||||
} else {
|
} else {
|
||||||
this.timelines.push(newTimeline);
|
this.timelines.push(newTimeline);
|
||||||
}
|
}
|
||||||
@@ -287,8 +290,9 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
* @return {?module:models/event-timeline~EventTimeline} timeline containing
|
* @return {?module:models/event-timeline~EventTimeline} timeline containing
|
||||||
* the given event, or null if unknown
|
* the given event, or null if unknown
|
||||||
*/
|
*/
|
||||||
public getTimelineForEvent(eventId: string): EventTimeline | null {
|
public getTimelineForEvent(eventId: string | null): EventTimeline | null {
|
||||||
const res = this._eventIdToTimeline[eventId];
|
if (eventId === null) { return null; }
|
||||||
|
const res = this._eventIdToTimeline.get(eventId);
|
||||||
return (res === undefined) ? null : res;
|
return (res === undefined) ? null : res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,7 +454,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
const event = events[i];
|
const event = events[i];
|
||||||
const eventId = event.getId();
|
const eventId = event.getId();
|
||||||
|
|
||||||
const existingTimeline = this._eventIdToTimeline[eventId];
|
const existingTimeline = this._eventIdToTimeline.get(eventId);
|
||||||
|
|
||||||
if (!existingTimeline) {
|
if (!existingTimeline) {
|
||||||
// we don't know about this event yet. Just add it to the timeline.
|
// we don't know about this event yet. Just add it to the timeline.
|
||||||
@@ -601,7 +605,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeline = this._eventIdToTimeline[event.getId()];
|
const timeline = this._eventIdToTimeline.get(event.getId());
|
||||||
if (timeline) {
|
if (timeline) {
|
||||||
if (duplicateStrategy === DuplicateStrategy.Replace) {
|
if (duplicateStrategy === DuplicateStrategy.Replace) {
|
||||||
debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " + event.getId());
|
debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " + event.getId());
|
||||||
@@ -697,7 +701,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
roomState,
|
roomState,
|
||||||
timelineWasEmpty,
|
timelineWasEmpty,
|
||||||
});
|
});
|
||||||
this._eventIdToTimeline[eventId] = timeline;
|
this._eventIdToTimeline.set(eventId, timeline);
|
||||||
|
|
||||||
this.relations.aggregateParentEvent(event);
|
this.relations.aggregateParentEvent(event);
|
||||||
this.relations.aggregateChildEvent(event, this);
|
this.relations.aggregateChildEvent(event, this);
|
||||||
@@ -725,23 +729,15 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
newEventId: string,
|
newEventId: string,
|
||||||
): void {
|
): void {
|
||||||
// XXX: why don't we infer newEventId from localEvent?
|
// XXX: why don't we infer newEventId from localEvent?
|
||||||
const existingTimeline = this._eventIdToTimeline[oldEventId];
|
const existingTimeline = this._eventIdToTimeline.get(oldEventId);
|
||||||
if (existingTimeline) {
|
if (existingTimeline) {
|
||||||
delete this._eventIdToTimeline[oldEventId];
|
this._eventIdToTimeline.delete(oldEventId);
|
||||||
this._eventIdToTimeline[newEventId] = existingTimeline;
|
this._eventIdToTimeline.set(newEventId, existingTimeline);
|
||||||
} else {
|
} else if (!this.filter || this.filter.filterRoomTimeline([localEvent]).length) {
|
||||||
if (this.filter) {
|
|
||||||
if (this.filter.filterRoomTimeline([localEvent]).length) {
|
|
||||||
this.addEventToTimeline(localEvent, this.liveTimeline, {
|
this.addEventToTimeline(localEvent, this.liveTimeline, {
|
||||||
toStartOfTimeline: false,
|
toStartOfTimeline: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.addEventToTimeline(localEvent, this.liveTimeline, {
|
|
||||||
toStartOfTimeline: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -753,14 +749,14 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
* in this room.
|
* in this room.
|
||||||
*/
|
*/
|
||||||
public removeEvent(eventId: string): MatrixEvent | null {
|
public removeEvent(eventId: string): MatrixEvent | null {
|
||||||
const timeline = this._eventIdToTimeline[eventId];
|
const timeline = this._eventIdToTimeline.get(eventId);
|
||||||
if (!timeline) {
|
if (!timeline) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const removed = timeline.removeEvent(eventId);
|
const removed = timeline.removeEvent(eventId);
|
||||||
if (removed) {
|
if (removed) {
|
||||||
delete this._eventIdToTimeline[eventId];
|
this._eventIdToTimeline.delete(eventId);
|
||||||
const data = {
|
const data = {
|
||||||
timeline: timeline,
|
timeline: timeline,
|
||||||
};
|
};
|
||||||
@@ -787,8 +783,8 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeline1 = this._eventIdToTimeline[eventId1];
|
const timeline1 = this._eventIdToTimeline.get(eventId1);
|
||||||
const timeline2 = this._eventIdToTimeline[eventId2];
|
const timeline2 = this._eventIdToTimeline.get(eventId2);
|
||||||
|
|
||||||
if (timeline1 === undefined) {
|
if (timeline1 === undefined) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user