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 = {
|
||||
plugins: [
|
||||
"matrix-org",
|
||||
"import",
|
||||
],
|
||||
extends: [
|
||||
"plugin:matrix-org/babel",
|
||||
"plugin:import/typescript",
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: true,
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
// 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/
|
||||
rules: {
|
||||
@@ -35,7 +43,19 @@ module.exports = {
|
||||
"no-console": "error",
|
||||
|
||||
// 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: [{
|
||||
files: [
|
||||
|
||||
2
.github/workflows/backport.yml
vendored
2
.github/workflows/backport.yml
vendored
@@ -25,6 +25,6 @@ jobs:
|
||||
steps:
|
||||
- uses: tibdex/backport@v2
|
||||
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
|
||||
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
|
||||
run: |
|
||||
ls -lah
|
||||
tag="${{ github.ref_name }}"
|
||||
version="${tag#v}"
|
||||
echo "VERSION=$version" >> $GITHUB_ENV
|
||||
@@ -51,3 +50,9 @@ jobs:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
keep_files: true
|
||||
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
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
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"
|
||||
id: sonarcloud
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.2
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
@@ -22,3 +33,13 @@ jobs:
|
||||
coverage_run_id: ${{ github.event.workflow_run.id }}
|
||||
coverage_workflow_name: tests.yml
|
||||
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
|
||||
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:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
@@ -73,7 +83,7 @@ jobs:
|
||||
|
||||
- name: Detecting files changed
|
||||
id: files
|
||||
uses: futuratrepadeira/changed-files@v3.2.1
|
||||
uses: futuratrepadeira/changed-files@v4.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pattern: '^.*\.tsx?$'
|
||||
@@ -81,7 +91,7 @@ jobs:
|
||||
- uses: t3chguy/typescript-check-action@main
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
use-check: true
|
||||
use-check: false
|
||||
check-fail-mode: added
|
||||
output-behaviour: annotate
|
||||
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)
|
||||
==================================================================================================
|
||||
|
||||
|
||||
281
CONTRIBUTING.md
281
CONTRIBUTING.md
@@ -1,284 +1,5 @@
|
||||
Contributing code to matrix-js-sdk
|
||||
==================================
|
||||
|
||||
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
|
||||
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)).
|
||||
matrix-js-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md
|
||||
|
||||
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",
|
||||
"version": "19.3.0",
|
||||
"version": "20.1.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=12.9.0"
|
||||
@@ -78,28 +78,30 @@
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@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/content-type": "^1.1.5",
|
||||
"@types/jest": "^28.0.0",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "16",
|
||||
"@types/request": "^2.48.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"allchange": "^1.0.6",
|
||||
"babel-jest": "^28.0.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "8.22.0",
|
||||
"eslint": "8.24.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",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"jest": "^28.0.0",
|
||||
"jest": "^29.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-mock": "^27.5.1",
|
||||
"jest-sonar-reporter": "^2.0.0",
|
||||
"jsdoc": "^3.6.6",
|
||||
"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.
|
||||
#
|
||||
# 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/)
|
||||
# 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/)
|
||||
#
|
||||
# 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
|
||||
|
||||
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_MINOR=${BASH_REMATCH[2]}
|
||||
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"
|
||||
exit
|
||||
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 $$)
|
||||
|
||||
USAGE="$0 [-x] [-c changelog_file] vX.Y.Z"
|
||||
@@ -37,17 +33,9 @@ $USAGE
|
||||
|
||||
-c changelog_file: specify name of file containing changelog
|
||||
-x: skip updating the changelog
|
||||
-n: skip publish to NPM
|
||||
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
|
||||
echo "this git checkout has staged (uncommitted) changes. Refusing to release."
|
||||
exit
|
||||
@@ -59,10 +47,8 @@ if ! git diff-files --quiet; then
|
||||
fi
|
||||
|
||||
skip_changelog=
|
||||
skip_npm=
|
||||
changelog_file="CHANGELOG.md"
|
||||
expected_npm_user="matrixdotorg"
|
||||
while getopts hc:u:xzn f; do
|
||||
while getopts hc:x f; do
|
||||
case $f in
|
||||
h)
|
||||
help
|
||||
@@ -74,21 +60,70 @@ while getopts hc:u:xzn f; do
|
||||
x)
|
||||
skip_changelog=1
|
||||
;;
|
||||
n)
|
||||
skip_npm=1
|
||||
;;
|
||||
u)
|
||||
expected_npm_user="$OPTARG"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift `expr $OPTIND - 1`
|
||||
shift $(expr $OPTIND - 1)
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $USAGE" >&2
|
||||
exit 1
|
||||
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
|
||||
# to have a hard time getting that right. See also
|
||||
# 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
|
||||
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
|
||||
release="${1#v}"
|
||||
tag="v${release}"
|
||||
rel_branch="release-$tag"
|
||||
|
||||
prerelease=0
|
||||
# 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
|
||||
fi
|
||||
|
||||
# We might already be on the release branch, in which case, yay
|
||||
# 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
|
||||
rel_branch=$(git symbolic-ref --short HEAD)
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
echo "Generating changelog"
|
||||
@@ -148,8 +161,8 @@ if [ -z "$skip_changelog" ]; then
|
||||
git commit "$changelog_file" -m "Prepare changelog for $tag"
|
||||
fi
|
||||
fi
|
||||
latest_changes=`mktemp`
|
||||
cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_changes}"
|
||||
latest_changes=$(mktemp)
|
||||
cat "${changelog_file}" | "$(dirname "$0")/scripts/changelog_head.py" > "${latest_changes}"
|
||||
|
||||
set -x
|
||||
|
||||
@@ -176,7 +189,7 @@ do
|
||||
done
|
||||
|
||||
# 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
|
||||
pkglock='yarn.lock'
|
||||
else
|
||||
@@ -188,7 +201,7 @@ git commit package.json $pkglock -m "$tag"
|
||||
# figure out if we should be signing this release
|
||||
signing_id=
|
||||
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
|
||||
signing_id=$result
|
||||
fi
|
||||
@@ -206,8 +219,8 @@ assets=''
|
||||
dodist=0
|
||||
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
|
||||
if [ $dodist -eq 0 ]; then
|
||||
projdir=`pwd`
|
||||
builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'`
|
||||
projdir=$(pwd)
|
||||
builddir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
|
||||
echo "Building distribution copy in $builddir"
|
||||
pushd "$builddir"
|
||||
git clone "$projdir" .
|
||||
@@ -232,7 +245,7 @@ fi
|
||||
if [ -n "$signing_id" ]; then
|
||||
# make a signed tag
|
||||
# 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
|
||||
git tag -a -F "${latest_changes}" "$tag"
|
||||
fi
|
||||
@@ -298,7 +311,7 @@ if [ $prerelease -eq 1 ]; then
|
||||
hubflags='-p'
|
||||
fi
|
||||
|
||||
release_text=`mktemp`
|
||||
release_text=$(mktemp)
|
||||
echo "$tag" > "${release_text}"
|
||||
echo >> "${release_text}"
|
||||
cat "${latest_changes}" >> "${release_text}"
|
||||
@@ -310,19 +323,6 @@ fi
|
||||
rm "${release_text}"
|
||||
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 [ $prerelease -eq 1 ]; then
|
||||
git checkout "$rel_branch"
|
||||
@@ -339,34 +339,19 @@ git merge "$rel_branch" --no-edit
|
||||
git push origin master
|
||||
|
||||
# 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 pull
|
||||
git merge master --no-edit
|
||||
|
||||
# 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
|
||||
|
||||
[ -x ./post-release.sh ] && ./post-release.sh
|
||||
|
||||
if [ $has_subprojects -eq 1 ] && [ $prerelease -eq 0 ]; then
|
||||
echo "Resetting subprojects to develop"
|
||||
for proj in $subprojects; do
|
||||
reset_dependency "$proj"
|
||||
done
|
||||
git push origin develop
|
||||
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 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");
|
||||
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
|
||||
* @constructor
|
||||
*/
|
||||
export function MockStorageApi() {
|
||||
this.data = {};
|
||||
this.keys = [];
|
||||
this.length = 0;
|
||||
}
|
||||
export class MockStorageApi {
|
||||
public data: Record<string, string> = {};
|
||||
public keys: string[] = [];
|
||||
public length = 0;
|
||||
|
||||
MockStorageApi.prototype = {
|
||||
setItem: function(k, v) {
|
||||
public setItem(k: string, v: string): void {
|
||||
this.data[k] = v;
|
||||
this._recalc();
|
||||
},
|
||||
getItem: function(k) {
|
||||
this.recalc();
|
||||
}
|
||||
|
||||
public getItem(k: string): string | null {
|
||||
return this.data[k] || null;
|
||||
},
|
||||
removeItem: function(k) {
|
||||
}
|
||||
|
||||
public removeItem(k: string): void {
|
||||
delete this.data[k];
|
||||
this._recalc();
|
||||
},
|
||||
key: function(index) {
|
||||
this.recalc();
|
||||
}
|
||||
|
||||
public key(index: number): string {
|
||||
return this.keys[index];
|
||||
},
|
||||
_recalc: function() {
|
||||
const keys = [];
|
||||
}
|
||||
|
||||
private recalc(): void {
|
||||
const keys: string[] = [];
|
||||
for (const k in this.data) {
|
||||
if (!this.data.hasOwnProperty(k)) {
|
||||
continue;
|
||||
@@ -50,6 +51,5 @@ MockStorageApi.prototype = {
|
||||
}
|
||||
this.keys = keys;
|
||||
this.length = keys.length;
|
||||
},
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ export class TestClient {
|
||||
options?: Partial<ICreateClientOpts>,
|
||||
) {
|
||||
if (sessionStoreBackend === undefined) {
|
||||
sessionStoreBackend = new MockStorageApi();
|
||||
sessionStoreBackend = new MockStorageApi() as unknown as Storage;
|
||||
}
|
||||
|
||||
this.httpBackend = new MockHttpBackend();
|
||||
|
||||
@@ -15,9 +15,11 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
// stub for browser-matrix browserify tests
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest = jest.fn();
|
||||
|
||||
afterAll(() => {
|
||||
// clean up XMLHttpRequest mock
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest = undefined;
|
||||
});
|
||||
@@ -122,7 +122,7 @@ describe("DeviceList management:", function() {
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, {
|
||||
event_id: '$event_id',
|
||||
event_id: '$event_id',
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
@@ -290,8 +290,9 @@ describe("DeviceList management:", function() {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
// Alice should be tracking bob's device list
|
||||
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) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
// Alice should have marked bob's device list as untracked
|
||||
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) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
// Alice should have marked bob's device list as untracked
|
||||
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(
|
||||
200, getSyncResponse([]));
|
||||
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) => {
|
||||
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(
|
||||
0, "Alice should have marked bob's device list as untracked",
|
||||
0,
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
@@ -31,8 +31,9 @@ import '../olm-loader';
|
||||
import { logger } from '../../src/logger';
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
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 { DeviceInfo } from '../../src/crypto/deviceinfo';
|
||||
|
||||
let aliTestClient: TestClient;
|
||||
const roomId = "!room:localhost";
|
||||
@@ -71,12 +72,12 @@ function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise<num
|
||||
expect(uploader.deviceKeys).toBeTruthy();
|
||||
|
||||
const uploaderKeys = {};
|
||||
uploaderKeys[uploader.deviceId] = uploader.deviceKeys;
|
||||
uploaderKeys[uploader.deviceId!] = uploader.deviceKeys;
|
||||
querier.httpBackend.when("POST", "/keys/query")
|
||||
.respond(200, function(_path, content) {
|
||||
expect(content.device_keys[uploader.userId]).toEqual([]);
|
||||
.respond(200, function(_path, content: IUploadKeysRequest) {
|
||||
expect(content.device_keys![uploader.userId!]).toEqual([]);
|
||||
const result = {};
|
||||
result[uploader.userId] = uploaderKeys;
|
||||
result[uploader.userId!] = uploaderKeys;
|
||||
return { device_keys: result };
|
||||
});
|
||||
return querier.httpBackend.flush("/keys/query", 1);
|
||||
@@ -93,10 +94,10 @@ async function expectAliClaimKeys(): Promise<void> {
|
||||
const keys = await bobTestClient.awaitOneTimeKeyUpload();
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/claim",
|
||||
).respond(200, function(_path, content) {
|
||||
const claimType = content.one_time_keys[bobUserId][bobDeviceId];
|
||||
).respond(200, function(_path, content: IUploadKeysRequest) {
|
||||
const claimType = content.one_time_keys![bobUserId][bobDeviceId];
|
||||
expect(claimType).toEqual("signed_curve25519");
|
||||
let keyId = null;
|
||||
let keyId = '';
|
||||
for (keyId in keys) {
|
||||
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
|
||||
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
|
||||
// an integration test, but meh)
|
||||
await Promise.all([p1(), p2()]);
|
||||
await aliTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
await aliTestClient.client.crypto!.deviceList.saveIfDirty();
|
||||
// @ts-ignore - protected
|
||||
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].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> {
|
||||
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) {
|
||||
resolve(content);
|
||||
return {
|
||||
@@ -252,14 +253,14 @@ async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]):
|
||||
}
|
||||
|
||||
function aliRecvMessage(): Promise<void> {
|
||||
const message = bobMessages.shift();
|
||||
const message = bobMessages.shift()!;
|
||||
return recvMessage(
|
||||
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
|
||||
);
|
||||
}
|
||||
|
||||
function bobRecvMessage(): Promise<void> {
|
||||
const message = aliMessages.shift();
|
||||
const message = aliMessages.shift()!;
|
||||
return recvMessage(
|
||||
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
|
||||
);
|
||||
@@ -494,6 +495,7 @@ describe("MatrixClient crypto", () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
@@ -504,10 +506,11 @@ describe("MatrixClient crypto", () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
const message = aliMessages.shift();
|
||||
const message = aliMessages.shift()!;
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
@@ -567,6 +570,7 @@ describe("MatrixClient crypto", () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
@@ -584,6 +588,9 @@ describe("MatrixClient crypto", () => {
|
||||
await firstSync(bobTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
bobTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {},
|
||||
);
|
||||
await bobRecvMessage();
|
||||
await bobEnablesEncryption();
|
||||
const ciphertext = await bobSendsReplyMessage();
|
||||
@@ -658,11 +665,10 @@ describe("MatrixClient crypto", () => {
|
||||
]);
|
||||
logger.log(aliTestClient + ': started');
|
||||
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).not.toEqual({});
|
||||
expect(Object.keys(content.one_time_keys).length).toBeGreaterThanOrEqual(1);
|
||||
logger.log('received %i one-time keys', Object.keys(content.one_time_keys).length);
|
||||
expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1);
|
||||
// cancel futher calls by telling the client
|
||||
// we have more than we need
|
||||
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 { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient events", function() {
|
||||
let client;
|
||||
let httpBackend;
|
||||
const selfUserId = "@alice:localhost";
|
||||
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() {
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
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" });
|
||||
[client!, httpBackend] = setupTests();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
httpBackend?.verifyNoOutstandingExpectation();
|
||||
client?.stopClient();
|
||||
return httpBackend?.stop();
|
||||
});
|
||||
|
||||
describe("emissions", function() {
|
||||
@@ -92,53 +126,49 @@ describe("MatrixClient events", function() {
|
||||
};
|
||||
|
||||
it("should emit events from both the first and subsequent /sync calls",
|
||||
function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
function() {
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
let expectedEvents = [];
|
||||
expectedEvents = expectedEvents.concat(
|
||||
SYNC_DATA.presence.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
|
||||
);
|
||||
let expectedEvents: Partial<IEvent>[] = [];
|
||||
expectedEvents = expectedEvents.concat(
|
||||
SYNC_DATA.presence.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
|
||||
);
|
||||
|
||||
client.on("event", function(event) {
|
||||
let found = false;
|
||||
for (let i = 0; i < expectedEvents.length; i++) {
|
||||
if (expectedEvents[i].event_id === event.getId()) {
|
||||
expectedEvents.splice(i, 1);
|
||||
found = true;
|
||||
break;
|
||||
client!.on(ClientEvent.Event, function(event) {
|
||||
let found = false;
|
||||
for (let i = 0; i < expectedEvents.length; i++) {
|
||||
if (expectedEvents[i].event_id === event.getId()) {
|
||||
expectedEvents.splice(i, 1);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(found).toBe(
|
||||
true, "Unexpected 'event' emitted: " + event.getType(),
|
||||
);
|
||||
});
|
||||
expect(found).toBe(true);
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
return Promise.all([
|
||||
// wait for two SYNCING events
|
||||
utils.syncPromise(client).then(() => {
|
||||
return utils.syncPromise(client);
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]).then(() => {
|
||||
expect(expectedEvents.length).toEqual(
|
||||
0, "Failed to see all events from /sync calls",
|
||||
);
|
||||
utils.syncPromise(client!).then(() => {
|
||||
return utils.syncPromise(client!);
|
||||
}),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]).then(() => {
|
||||
expect(expectedEvents.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit User events", function(done) {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
let fired = false;
|
||||
client.on("User.presence", function(event, user) {
|
||||
client!.on(UserEvent.Presence, function(event, user) {
|
||||
fired = true;
|
||||
expect(user).toBeTruthy();
|
||||
expect(event).toBeTruthy();
|
||||
@@ -146,58 +176,52 @@ describe("MatrixClient events", function() {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(event.event).toMatch(SYNC_DATA.presence.events[0]);
|
||||
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
|
||||
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() {
|
||||
expect(fired).toBe(true, "User.presence didn't fire.");
|
||||
httpBackend!.flushAllExpected().then(function() {
|
||||
expect(fired).toBe(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Room events", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
let roomInvokeCount = 0;
|
||||
let roomNameInvokeCount = 0;
|
||||
let timelineFireCount = 0;
|
||||
client.on("Room", function(room) {
|
||||
client!.on(ClientEvent.Room, function(room) {
|
||||
roomInvokeCount++;
|
||||
expect(room.roomId).toEqual("!erufh:bar");
|
||||
});
|
||||
client.on("Room.timeline", function(event, room) {
|
||||
client!.on(RoomEvent.Timeline, function(event, room) {
|
||||
timelineFireCount++;
|
||||
expect(room.roomId).toEqual("!erufh:bar");
|
||||
});
|
||||
client.on("Room.name", function(room) {
|
||||
client!.on(RoomEvent.Name, function(room) {
|
||||
roomNameInvokeCount++;
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 2),
|
||||
]).then(function() {
|
||||
expect(roomInvokeCount).toEqual(
|
||||
1, "Room fired wrong number of times.",
|
||||
);
|
||||
expect(roomNameInvokeCount).toEqual(
|
||||
1, "Room.name fired wrong number of times.",
|
||||
);
|
||||
expect(timelineFireCount).toEqual(
|
||||
3, "Room.timeline fired the wrong number of times",
|
||||
);
|
||||
expect(roomInvokeCount).toEqual(1);
|
||||
expect(roomNameInvokeCount).toEqual(1);
|
||||
expect(timelineFireCount).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomState events", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
const roomStateEventTypes = [
|
||||
"m.room.member", "m.room.create",
|
||||
@@ -205,126 +229,106 @@ describe("MatrixClient events", function() {
|
||||
let eventsInvokeCount = 0;
|
||||
let membersInvokeCount = 0;
|
||||
let newMemberInvokeCount = 0;
|
||||
client.on("RoomState.events", function(event, state) {
|
||||
client!.on(RoomStateEvent.Events, function(event, state) {
|
||||
eventsInvokeCount++;
|
||||
const index = roomStateEventTypes.indexOf(event.getType());
|
||||
expect(index).not.toEqual(
|
||||
-1, "Unexpected room state event type: " + event.getType(),
|
||||
);
|
||||
expect(index).not.toEqual(-1);
|
||||
if (index >= 0) {
|
||||
roomStateEventTypes.splice(index, 1);
|
||||
}
|
||||
});
|
||||
client.on("RoomState.members", function(event, state, member) {
|
||||
client!.on(RoomStateEvent.Members, function(event, state, member) {
|
||||
membersInvokeCount++;
|
||||
expect(member.roomId).toEqual("!erufh:bar");
|
||||
expect(member.userId).toEqual("@foo:bar");
|
||||
expect(member.membership).toEqual("join");
|
||||
});
|
||||
client.on("RoomState.newMember", function(event, state, member) {
|
||||
client!.on(RoomStateEvent.NewMember, function(event, state, member) {
|
||||
newMemberInvokeCount++;
|
||||
expect(member.roomId).toEqual("!erufh:bar");
|
||||
expect(member.userId).toEqual("@foo:bar");
|
||||
expect(member.membership).toBeFalsy();
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 2),
|
||||
]).then(function() {
|
||||
expect(membersInvokeCount).toEqual(
|
||||
1, "RoomState.members fired wrong number of times",
|
||||
);
|
||||
expect(newMemberInvokeCount).toEqual(
|
||||
1, "RoomState.newMember fired wrong number of times",
|
||||
);
|
||||
expect(eventsInvokeCount).toEqual(
|
||||
2, "RoomState.events fired wrong number of times",
|
||||
);
|
||||
expect(membersInvokeCount).toEqual(1);
|
||||
expect(newMemberInvokeCount).toEqual(1);
|
||||
expect(eventsInvokeCount).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomMember events", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
let typingInvokeCount = 0;
|
||||
let powerLevelInvokeCount = 0;
|
||||
let nameInvokeCount = 0;
|
||||
let membershipInvokeCount = 0;
|
||||
client.on("RoomMember.name", function(event, member) {
|
||||
client!.on(RoomMemberEvent.Name, function(event, member) {
|
||||
nameInvokeCount++;
|
||||
});
|
||||
client.on("RoomMember.typing", function(event, member) {
|
||||
client!.on(RoomMemberEvent.Typing, function(event, member) {
|
||||
typingInvokeCount++;
|
||||
expect(member.typing).toBe(true);
|
||||
});
|
||||
client.on("RoomMember.powerLevel", function(event, member) {
|
||||
client!.on(RoomMemberEvent.PowerLevel, function(event, member) {
|
||||
powerLevelInvokeCount++;
|
||||
});
|
||||
client.on("RoomMember.membership", function(event, member) {
|
||||
client!.on(RoomMemberEvent.Membership, function(event, member) {
|
||||
membershipInvokeCount++;
|
||||
expect(member.membership).toEqual("join");
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 2),
|
||||
]).then(function() {
|
||||
expect(typingInvokeCount).toEqual(
|
||||
1, "RoomMember.typing fired wrong number of times",
|
||||
);
|
||||
expect(powerLevelInvokeCount).toEqual(
|
||||
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",
|
||||
);
|
||||
expect(typingInvokeCount).toEqual(1);
|
||||
expect(powerLevelInvokeCount).toEqual(0);
|
||||
expect(nameInvokeCount).toEqual(0);
|
||||
expect(membershipInvokeCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
|
||||
const error = { errcode: 'M_UNKNOWN_TOKEN' };
|
||||
httpBackend.when("GET", "/sync").respond(401, error);
|
||||
httpBackend!.when("GET", "/sync").respond(401, error);
|
||||
|
||||
let sessionLoggedOutCount = 0;
|
||||
client.on("Session.logged_out", function(errObj) {
|
||||
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
|
||||
sessionLoggedOutCount++;
|
||||
expect(errObj.data).toEqual(error);
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return httpBackend.flushAllExpected().then(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(
|
||||
1, "Session.logged_out fired wrong number of times",
|
||||
);
|
||||
return httpBackend!.flushAllExpected().then(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() {
|
||||
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;
|
||||
client.on("Session.logged_out", function(errObj) {
|
||||
client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
|
||||
sessionLoggedOutCount++;
|
||||
expect(errObj.data).toEqual(error);
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return httpBackend.flushAllExpected().then(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(
|
||||
1, "Session.logged_out fired wrong number of times",
|
||||
);
|
||||
return httpBackend!.flushAllExpected().then(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
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
|
||||
limitations under the License.
|
||||
*/
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
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 { Filter, MemoryStore, Room } from "../../src/matrix";
|
||||
import { TestClient } from "../TestClient";
|
||||
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() {
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
let store = null;
|
||||
const userId = "@alice:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
const idServerDomain = "identity.localhost"; // not a real server
|
||||
const identityAccessToken = "woop-i-am-a-secret";
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
let store: MemoryStore | undefined;
|
||||
|
||||
beforeEach(function() {
|
||||
store = new MemoryStore();
|
||||
const defaultClientOpts: IStoredClientOpts = {
|
||||
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, {
|
||||
store,
|
||||
store: store as IStore,
|
||||
identityServer: {
|
||||
getAccessToken: () => Promise.resolve(identityAccessToken),
|
||||
},
|
||||
idBaseUrl: `https://${idServerDomain}`,
|
||||
});
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
|
||||
return [testClient.client, testClient.httpBackend, store];
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
[client, httpBackend, store] = setupTests();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
return httpBackend.stop();
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
return httpBackend!.stop();
|
||||
});
|
||||
|
||||
describe("uploadContent", function() {
|
||||
const buf = Buffer.from('hello world');
|
||||
it("should upload the file", function() {
|
||||
httpBackend.when(
|
||||
httpBackend!.when(
|
||||
"POST", "/_matrix/media/r0/upload",
|
||||
).check(function(req) {
|
||||
expect(req.rawData).toEqual(buf);
|
||||
expect(req.queryParams.filename).toEqual("hi.txt");
|
||||
if (!(req.queryParams.access_token == accessToken ||
|
||||
expect(req.queryParams?.filename).toEqual("hi.txt");
|
||||
if (!(req.queryParams?.access_token == accessToken ||
|
||||
req.headers["Authorization"] == "Bearer " + accessToken)) {
|
||||
expect(true).toBe(false);
|
||||
}
|
||||
expect(req.headers["Content-Type"]).toEqual("text/plain");
|
||||
// @ts-ignore private property
|
||||
expect(req.opts.json).toBeFalsy();
|
||||
// @ts-ignore private property
|
||||
expect(req.opts.timeout).toBe(undefined);
|
||||
}).respond(200, "content", true);
|
||||
|
||||
const prom = client.uploadContent({
|
||||
const prom = client!.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
});
|
||||
} as unknown as FileType);
|
||||
|
||||
expect(prom).toBeTruthy();
|
||||
|
||||
const uploads = client.getCurrentUploads();
|
||||
const uploads = client!.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(1);
|
||||
expect(uploads[0].promise).toBe(prom);
|
||||
expect(uploads[0].loaded).toEqual(0);
|
||||
@@ -83,51 +99,53 @@ describe("MatrixClient", function() {
|
||||
// for backwards compatibility, we return the raw JSON
|
||||
expect(response).toEqual("content");
|
||||
|
||||
const uploads = client.getCurrentUploads();
|
||||
const uploads = client!.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(0);
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
httpBackend!.flush('');
|
||||
return prom2;
|
||||
});
|
||||
|
||||
it("should parse the response if rawResponse=false", function() {
|
||||
httpBackend.when(
|
||||
httpBackend!.when(
|
||||
"POST", "/_matrix/media/r0/upload",
|
||||
).check(function(req) {
|
||||
// @ts-ignore private property
|
||||
expect(req.opts.json).toBeFalsy();
|
||||
}).respond(200, { "content_uri": "uri" });
|
||||
|
||||
const prom = client.uploadContent({
|
||||
const prom = client!.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
}, {
|
||||
} as unknown as FileType, {
|
||||
rawResponse: false,
|
||||
}).then(function(response) {
|
||||
expect(response.content_uri).toEqual("uri");
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
httpBackend!.flush('');
|
||||
return prom;
|
||||
});
|
||||
|
||||
it("should parse errors into a MatrixError", function() {
|
||||
httpBackend.when(
|
||||
httpBackend!.when(
|
||||
"POST", "/_matrix/media/r0/upload",
|
||||
).check(function(req) {
|
||||
expect(req.rawData).toEqual(buf);
|
||||
// @ts-ignore private property
|
||||
expect(req.opts.json).toBeFalsy();
|
||||
}).respond(400, {
|
||||
"errcode": "M_SNAFU",
|
||||
"error": "broken",
|
||||
});
|
||||
|
||||
const prom = client.uploadContent({
|
||||
const prom = client!.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
}).then(function(response) {
|
||||
} as unknown as FileType).then(function(response) {
|
||||
throw Error("request not failed");
|
||||
}, function(error) {
|
||||
expect(error.httpStatus).toEqual(400);
|
||||
@@ -135,18 +153,18 @@ describe("MatrixClient", function() {
|
||||
expect(error.message).toEqual("broken");
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
httpBackend!.flush('');
|
||||
return prom;
|
||||
});
|
||||
|
||||
it("should return a promise which can be cancelled", function() {
|
||||
const prom = client.uploadContent({
|
||||
const prom = client!.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
});
|
||||
} as unknown as FileType);
|
||||
|
||||
const uploads = client.getCurrentUploads();
|
||||
const uploads = client!.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(1);
|
||||
expect(uploads[0].promise).toBe(prom);
|
||||
expect(uploads[0].loaded).toEqual(0);
|
||||
@@ -156,11 +174,11 @@ describe("MatrixClient", function() {
|
||||
}, function(error) {
|
||||
expect(error).toEqual("aborted");
|
||||
|
||||
const uploads = client.getCurrentUploads();
|
||||
const uploads = client!.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(0);
|
||||
});
|
||||
|
||||
const r = client.cancelUpload(prom);
|
||||
const r = client!.cancelUpload(prom);
|
||||
expect(r).toBe(true);
|
||||
return prom2;
|
||||
});
|
||||
@@ -169,17 +187,20 @@ describe("MatrixClient", function() {
|
||||
describe("joinRoom", function() {
|
||||
it("should no-op if you've already joined a room", function() {
|
||||
const roomId = "!foo:bar";
|
||||
const room = new Room(roomId, client, userId);
|
||||
client.fetchRoomEvent = () => Promise.resolve({});
|
||||
const room = new Room(roomId, client!, userId);
|
||||
client!.fetchRoomEvent = () => Promise.resolve({
|
||||
type: 'test',
|
||||
content: {},
|
||||
});
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userId, room: roomId, mship: "join", event: true,
|
||||
}),
|
||||
]);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
store.storeRoom(room);
|
||||
client.joinRoom(roomId);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
httpBackend!.verifyNoOutstandingRequests();
|
||||
store!.storeRoom(room);
|
||||
client!.joinRoom(roomId);
|
||||
httpBackend!.verifyNoOutstandingRequests();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -190,67 +211,67 @@ describe("MatrixClient", function() {
|
||||
const filter = Filter.fromJson(userId, filterId, {
|
||||
event_format: "client",
|
||||
});
|
||||
store.storeFilter(filter);
|
||||
client.getFilter(userId, filterId, true).then(function(gotFilter) {
|
||||
store!.storeFilter(filter);
|
||||
client!.getFilter(userId, filterId, true).then(function(gotFilter) {
|
||||
expect(gotFilter).toEqual(filter);
|
||||
done();
|
||||
});
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
httpBackend!.verifyNoOutstandingRequests();
|
||||
});
|
||||
|
||||
it("should do an HTTP request if !allowCached even if one exists",
|
||||
function(done) {
|
||||
const httpFilterDefinition = {
|
||||
event_format: "federation",
|
||||
};
|
||||
function(done) {
|
||||
const httpFilterDefinition = {
|
||||
event_format: "federation",
|
||||
};
|
||||
|
||||
httpBackend.when(
|
||||
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
|
||||
).respond(200, httpFilterDefinition);
|
||||
httpBackend!.when(
|
||||
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
|
||||
).respond(200, httpFilterDefinition);
|
||||
|
||||
const storeFilter = Filter.fromJson(userId, filterId, {
|
||||
event_format: "client",
|
||||
const storeFilter = Filter.fromJson(userId, filterId, {
|
||||
event_format: "client",
|
||||
});
|
||||
store!.storeFilter(storeFilter);
|
||||
client!.getFilter(userId, filterId, false).then(function(gotFilter) {
|
||||
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
||||
done();
|
||||
});
|
||||
|
||||
httpBackend!.flush('');
|
||||
});
|
||||
store.storeFilter(storeFilter);
|
||||
client.getFilter(userId, filterId, false).then(function(gotFilter) {
|
||||
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
||||
done();
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
it("should do an HTTP request if nothing is in the cache and then store it",
|
||||
function(done) {
|
||||
const httpFilterDefinition = {
|
||||
event_format: "federation",
|
||||
};
|
||||
expect(store.getFilter(userId, filterId)).toBe(null);
|
||||
function(done) {
|
||||
const httpFilterDefinition = {
|
||||
event_format: "federation",
|
||||
};
|
||||
expect(store!.getFilter(userId, filterId)).toBe(null);
|
||||
|
||||
httpBackend.when(
|
||||
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
|
||||
).respond(200, httpFilterDefinition);
|
||||
client.getFilter(userId, filterId, true).then(function(gotFilter) {
|
||||
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
||||
expect(store.getFilter(userId, filterId)).toBeTruthy();
|
||||
done();
|
||||
httpBackend!.when(
|
||||
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
|
||||
).respond(200, httpFilterDefinition);
|
||||
client!.getFilter(userId, filterId, true).then(function(gotFilter) {
|
||||
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
|
||||
expect(store!.getFilter(userId, filterId)).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
|
||||
httpBackend!.flush('');
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFilter", function() {
|
||||
const filterId = "f1llllllerid";
|
||||
|
||||
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 = {
|
||||
event_format: "client",
|
||||
event_format: "client" as IFilterDefinition['event_format'],
|
||||
};
|
||||
|
||||
httpBackend.when(
|
||||
httpBackend!.when(
|
||||
"POST", "/user/" + encodeURIComponent(userId) + "/filter",
|
||||
).check(function(req) {
|
||||
expect(req.data).toEqual(filterDefinition);
|
||||
@@ -258,13 +279,13 @@ describe("MatrixClient", function() {
|
||||
filter_id: filterId,
|
||||
});
|
||||
|
||||
client.createFilter(filterDefinition).then(function(gotFilter) {
|
||||
client!.createFilter(filterDefinition).then(function(gotFilter) {
|
||||
expect(gotFilter.getDefinition()).toEqual(filterDefinition);
|
||||
expect(store.getFilter(userId, filterId)).toEqual(gotFilter);
|
||||
expect(store!.getFilter(userId, filterId)).toEqual(gotFilter);
|
||||
done();
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
httpBackend!.flush('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -291,10 +312,10 @@ describe("MatrixClient", function() {
|
||||
},
|
||||
};
|
||||
|
||||
client.searchMessageText({
|
||||
client!.searchMessageText({
|
||||
query: "monkeys",
|
||||
});
|
||||
httpBackend.when("POST", "/search").check(function(req) {
|
||||
httpBackend!.when("POST", "/search").check(function(req) {
|
||||
expect(req.data).toEqual({
|
||||
search_categories: {
|
||||
room_events: {
|
||||
@@ -304,7 +325,7 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
}).respond(200, response);
|
||||
|
||||
return httpBackend.flush();
|
||||
return httpBackend!.flush('');
|
||||
});
|
||||
|
||||
describe("should filter out context from different timelines (threads)", () => {
|
||||
@@ -313,11 +334,14 @@ describe("MatrixClient", function() {
|
||||
search_categories: {
|
||||
room_events: {
|
||||
count: 24,
|
||||
highlights: [],
|
||||
results: [{
|
||||
rank: 0.1,
|
||||
result: {
|
||||
event_id: "$flibble:localhost",
|
||||
type: "m.room.message",
|
||||
sender: '@test:locahost',
|
||||
origin_server_ts: 123,
|
||||
user_id: "@alice:localhost",
|
||||
room_id: "!feuiwhf:localhost",
|
||||
content: {
|
||||
@@ -326,9 +350,12 @@ describe("MatrixClient", function() {
|
||||
},
|
||||
},
|
||||
context: {
|
||||
profile_info: {},
|
||||
events_after: [{
|
||||
event_id: "$ev-after:server",
|
||||
type: "m.room.message",
|
||||
sender: '@test:locahost',
|
||||
origin_server_ts: 123,
|
||||
user_id: "@alice:localhost",
|
||||
room_id: "!feuiwhf:localhost",
|
||||
content: {
|
||||
@@ -343,6 +370,8 @@ describe("MatrixClient", function() {
|
||||
events_before: [{
|
||||
event_id: "$ev-before:server",
|
||||
type: "m.room.message",
|
||||
sender: '@test:locahost',
|
||||
origin_server_ts: 123,
|
||||
user_id: "@alice:localhost",
|
||||
room_id: "!feuiwhf:localhost",
|
||||
content: {
|
||||
@@ -356,15 +385,17 @@ describe("MatrixClient", function() {
|
||||
},
|
||||
};
|
||||
|
||||
const data = {
|
||||
const data: ISearchResults = {
|
||||
results: [],
|
||||
highlights: [],
|
||||
};
|
||||
client.processRoomEventsSearch(data, response);
|
||||
client!.processRoomEventsSearch(data, response);
|
||||
|
||||
expect(data.results).toHaveLength(1);
|
||||
expect(data.results[0].context.timeline).toHaveLength(2);
|
||||
expect(data.results[0].context.timeline.find(e => e.getId() === "$ev-after:server")).toBeFalsy();
|
||||
expect(data.results[0].context.getTimeline()).toHaveLength(2);
|
||||
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", () => {
|
||||
@@ -372,11 +403,14 @@ describe("MatrixClient", function() {
|
||||
search_categories: {
|
||||
room_events: {
|
||||
count: 24,
|
||||
highlights: [],
|
||||
results: [{
|
||||
rank: 0.1,
|
||||
result: {
|
||||
event_id: "$flibble:localhost",
|
||||
type: "m.room.message",
|
||||
sender: '@test:locahost',
|
||||
origin_server_ts: 123,
|
||||
user_id: "@alice:localhost",
|
||||
room_id: "!feuiwhf:localhost",
|
||||
content: {
|
||||
@@ -389,9 +423,12 @@ describe("MatrixClient", function() {
|
||||
},
|
||||
},
|
||||
context: {
|
||||
profile_info: {},
|
||||
events_after: [{
|
||||
event_id: "$ev-after:server",
|
||||
type: "m.room.message",
|
||||
sender: '@test:locahost',
|
||||
origin_server_ts: 123,
|
||||
user_id: "@alice:localhost",
|
||||
room_id: "!feuiwhf:localhost",
|
||||
content: {
|
||||
@@ -410,15 +447,17 @@ describe("MatrixClient", function() {
|
||||
},
|
||||
};
|
||||
|
||||
const data = {
|
||||
const data: ISearchResults = {
|
||||
results: [],
|
||||
highlights: [],
|
||||
};
|
||||
client.processRoomEventsSearch(data, response);
|
||||
client!.processRoomEventsSearch(data, response);
|
||||
|
||||
expect(data.results).toHaveLength(1);
|
||||
expect(data.results[0].context.timeline).toHaveLength(1);
|
||||
expect(data.results[0].context.timeline.find(e => e.getId() === "$flibble:localhost")).toBeTruthy();
|
||||
expect(data.results[0].context.getTimeline()).toHaveLength(1);
|
||||
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", () => {
|
||||
@@ -426,10 +465,13 @@ describe("MatrixClient", function() {
|
||||
search_categories: {
|
||||
room_events: {
|
||||
count: 24,
|
||||
highlights: [],
|
||||
results: [{
|
||||
rank: 0.1,
|
||||
result: {
|
||||
event_id: "$flibble:localhost",
|
||||
sender: '@test:locahost',
|
||||
origin_server_ts: 123,
|
||||
type: "m.room.message",
|
||||
user_id: "@alice:localhost",
|
||||
room_id: "!feuiwhf:localhost",
|
||||
@@ -445,6 +487,8 @@ describe("MatrixClient", function() {
|
||||
context: {
|
||||
events_after: [{
|
||||
event_id: "$ev-after:server",
|
||||
sender: '@test:locahost',
|
||||
origin_server_ts: 123,
|
||||
type: "m.room.message",
|
||||
user_id: "@alice:localhost",
|
||||
room_id: "!feuiwhf:localhost",
|
||||
@@ -454,21 +498,24 @@ describe("MatrixClient", function() {
|
||||
},
|
||||
}],
|
||||
events_before: [],
|
||||
profile_info: {},
|
||||
},
|
||||
}],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const data = {
|
||||
const data: ISearchResults = {
|
||||
results: [],
|
||||
highlights: [],
|
||||
};
|
||||
client.processRoomEventsSearch(data, response);
|
||||
client!.processRoomEventsSearch(data, response);
|
||||
|
||||
expect(data.results).toHaveLength(1);
|
||||
expect(data.results[0].context.timeline).toHaveLength(1);
|
||||
expect(data.results[0].context.timeline.find(e => e.getId() === "$flibble:localhost")).toBeTruthy();
|
||||
expect(data.results[0].context.getTimeline()).toHaveLength(1);
|
||||
expect(
|
||||
data.results[0].context.getTimeline().find(e => e.getId() === "$flibble:localhost"),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -479,16 +526,16 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
return client.initCrypto();
|
||||
return client!.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
client.stopClient();
|
||||
client!.stopClient();
|
||||
});
|
||||
|
||||
it("should do an HTTP request and then store the keys", function() {
|
||||
const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
|
||||
// ed25519key = client.getDeviceEd25519Key();
|
||||
// ed25519key = client!.getDeviceEd25519Key();
|
||||
const borisKeys = {
|
||||
dev1: {
|
||||
algorithms: ["1"],
|
||||
@@ -512,7 +559,7 @@ describe("MatrixClient", function() {
|
||||
keys: { "ed25519:dev2": ed25519key },
|
||||
signatures: {
|
||||
chaz: {
|
||||
"ed25519:dev2":
|
||||
"ed25519:dev2":
|
||||
"FwslH/Q7EYSb7swDJbNB5PSzcbEO1xRRBF1riuijqvL" +
|
||||
"EkrK9/XVN8jl4h7thGuRITQ01siBQnNmMK9t45QfcCQ",
|
||||
},
|
||||
@@ -528,7 +575,7 @@ describe("MatrixClient", function() {
|
||||
var b = JSON.parse(JSON.stringify(o));
|
||||
delete(b.signatures);
|
||||
delete(b.unsigned);
|
||||
return client.crypto.olmDevice.sign(anotherjson.stringify(b));
|
||||
return client!.crypto.olmDevice.sign(anotherjson.stringify(b));
|
||||
};
|
||||
|
||||
logger.log("Ed25519: " + ed25519key);
|
||||
@@ -536,7 +583,7 @@ describe("MatrixClient", function() {
|
||||
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: {
|
||||
'boris': [],
|
||||
'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, {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev1": ed25519key },
|
||||
@@ -564,23 +611,23 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
httpBackend!.flush('');
|
||||
return prom;
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteDevice", function() {
|
||||
const auth = { a: 1 };
|
||||
const auth = { identifier: 1 };
|
||||
it("should pass through an auth dict", function() {
|
||||
httpBackend.when(
|
||||
httpBackend!.when(
|
||||
"DELETE", "/_matrix/client/r0/devices/my_device",
|
||||
).check(function(req) {
|
||||
expect(req.data).toEqual({ auth: auth });
|
||||
}).respond(200);
|
||||
|
||||
const prom = client.deleteDevice("my_device", auth);
|
||||
const prom = client!.deleteDevice("my_device", auth);
|
||||
|
||||
httpBackend.flush();
|
||||
httpBackend!.flush('');
|
||||
return prom;
|
||||
});
|
||||
});
|
||||
@@ -588,7 +635,7 @@ describe("MatrixClient", function() {
|
||||
describe("partitionThreadedEvents", function() {
|
||||
let room;
|
||||
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() {
|
||||
@@ -599,7 +646,11 @@ describe("MatrixClient", 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 eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
@@ -611,6 +662,7 @@ describe("MatrixClient", function() {
|
||||
eventPollResponseReference,
|
||||
];
|
||||
// Vote has no threadId yet
|
||||
// @ts-ignore private property
|
||||
expect(eventPollResponseReference.threadId).toBeFalsy();
|
||||
|
||||
const [timeline, threaded] = room.partitionThreadedEvents(events);
|
||||
@@ -634,7 +686,11 @@ describe("MatrixClient", 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 eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
|
||||
@@ -661,7 +717,11 @@ describe("MatrixClient", 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 eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
@@ -688,7 +748,11 @@ describe("MatrixClient", 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 eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
|
||||
@@ -715,7 +779,11 @@ describe("MatrixClient", 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:
|
||||
|
||||
const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
|
||||
@@ -768,7 +836,11 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
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 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", () => {
|
||||
client.clientOpts = { experimentalThreadSupport: true };
|
||||
// @ts-ignore setting private property
|
||||
client!.clientOpts = {
|
||||
...defaultClientOpts,
|
||||
experimentalThreadSupport: true,
|
||||
};
|
||||
|
||||
const threadRootEvent = buildEventPollStartThreadRoot();
|
||||
const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
|
||||
@@ -826,7 +902,11 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
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 eventMessageInThread = buildEventMessageInThread(threadRootEvent);
|
||||
@@ -860,9 +940,9 @@ describe("MatrixClient", function() {
|
||||
fields: {},
|
||||
}];
|
||||
|
||||
const prom = client.getThirdpartyUser("irc", {});
|
||||
httpBackend.when("GET", "/thirdparty/user/irc").respond(200, response);
|
||||
await httpBackend.flush();
|
||||
const prom = client!.getThirdpartyUser("irc", {});
|
||||
httpBackend!.when("GET", "/thirdparty/user/irc").respond(200, response);
|
||||
await httpBackend!.flush('');
|
||||
expect(await prom).toStrictEqual(response);
|
||||
});
|
||||
});
|
||||
@@ -875,9 +955,9 @@ describe("MatrixClient", function() {
|
||||
fields: {},
|
||||
}];
|
||||
|
||||
const prom = client.getThirdpartyLocation("irc", {});
|
||||
httpBackend.when("GET", "/thirdparty/location/irc").respond(200, response);
|
||||
await httpBackend.flush();
|
||||
const prom = client!.getThirdpartyLocation("irc", {});
|
||||
httpBackend!.when("GET", "/thirdparty/location/irc").respond(200, response);
|
||||
await httpBackend!.flush('');
|
||||
expect(await prom).toStrictEqual(response);
|
||||
});
|
||||
});
|
||||
@@ -888,9 +968,10 @@ describe("MatrixClient", function() {
|
||||
pushers: [],
|
||||
};
|
||||
|
||||
const prom = client.getPushers();
|
||||
httpBackend.when("GET", "/pushers").respond(200, response);
|
||||
await httpBackend.flush();
|
||||
const prom = client!.getPushers();
|
||||
httpBackend!.when("GET", "/_matrix/client/versions").respond(200, {});
|
||||
httpBackend!.when("GET", "/pushers").respond(200, response);
|
||||
await httpBackend!.flush('');
|
||||
expect(await prom).toStrictEqual(response);
|
||||
});
|
||||
});
|
||||
@@ -902,12 +983,12 @@ describe("MatrixClient", function() {
|
||||
left: [],
|
||||
};
|
||||
|
||||
const prom = client.getKeyChanges("old", "new");
|
||||
httpBackend.when("GET", "/keys/changes").check((req) => {
|
||||
expect(req.queryParams.from).toEqual("old");
|
||||
expect(req.queryParams.to).toEqual("new");
|
||||
const prom = client!.getKeyChanges("old", "new");
|
||||
httpBackend!.when("GET", "/keys/changes").check((req) => {
|
||||
expect(req.queryParams?.from).toEqual("old");
|
||||
expect(req.queryParams?.to).toEqual("new");
|
||||
}).respond(200, response);
|
||||
await httpBackend.flush();
|
||||
await httpBackend!.flush('');
|
||||
expect(await prom).toStrictEqual(response);
|
||||
});
|
||||
});
|
||||
@@ -918,9 +999,9 @@ describe("MatrixClient", function() {
|
||||
devices: [],
|
||||
};
|
||||
|
||||
const prom = client.getDevices();
|
||||
httpBackend.when("GET", "/devices").respond(200, response);
|
||||
await httpBackend.flush();
|
||||
const prom = client!.getDevices();
|
||||
httpBackend!.when("GET", "/devices").respond(200, response);
|
||||
await httpBackend!.flush('');
|
||||
expect(await prom).toStrictEqual(response);
|
||||
});
|
||||
});
|
||||
@@ -934,9 +1015,9 @@ describe("MatrixClient", function() {
|
||||
last_seen_ts: 1,
|
||||
};
|
||||
|
||||
const prom = client.getDevice("DEADBEEF");
|
||||
httpBackend.when("GET", "/devices/DEADBEEF").respond(200, response);
|
||||
await httpBackend.flush();
|
||||
const prom = client!.getDevice("DEADBEEF");
|
||||
httpBackend!.when("GET", "/devices/DEADBEEF").respond(200, response);
|
||||
await httpBackend!.flush('');
|
||||
expect(await prom).toStrictEqual(response);
|
||||
});
|
||||
});
|
||||
@@ -947,9 +1028,9 @@ describe("MatrixClient", function() {
|
||||
threepids: [],
|
||||
};
|
||||
|
||||
const prom = client.getThreePids();
|
||||
httpBackend.when("GET", "/account/3pid").respond(200, response);
|
||||
await httpBackend.flush();
|
||||
const prom = client!.getThreePids();
|
||||
httpBackend!.when("GET", "/account/3pid").respond(200, response);
|
||||
await httpBackend!.flush('');
|
||||
expect(await prom).toStrictEqual(response);
|
||||
});
|
||||
});
|
||||
@@ -957,9 +1038,9 @@ describe("MatrixClient", function() {
|
||||
describe("deleteAlias", () => {
|
||||
it("should hit the expected API endpoint", async () => {
|
||||
const response = {};
|
||||
const prom = client.deleteAlias("#foo:bar");
|
||||
httpBackend.when("DELETE", "/directory/room/" + encodeURIComponent("#foo:bar")).respond(200, response);
|
||||
await httpBackend.flush();
|
||||
const prom = client!.deleteAlias("#foo:bar");
|
||||
httpBackend!.when("DELETE", "/directory/room/" + encodeURIComponent("#foo:bar")).respond(200, response);
|
||||
await httpBackend!.flush('');
|
||||
expect(await prom).toStrictEqual(response);
|
||||
});
|
||||
});
|
||||
@@ -967,10 +1048,10 @@ describe("MatrixClient", function() {
|
||||
describe("deleteRoomTag", () => {
|
||||
it("should hit the expected API endpoint", async () => {
|
||||
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`;
|
||||
httpBackend.when("DELETE", url).respond(200, response);
|
||||
await httpBackend.flush();
|
||||
httpBackend!.when("DELETE", url).respond(200, response);
|
||||
await httpBackend!.flush('');
|
||||
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`;
|
||||
httpBackend.when("GET", url).respond(200, response);
|
||||
await httpBackend.flush();
|
||||
httpBackend!.when("GET", url).respond(200, response);
|
||||
await httpBackend!.flush('');
|
||||
expect(await prom).toStrictEqual(response);
|
||||
});
|
||||
});
|
||||
@@ -1000,19 +1081,19 @@ describe("MatrixClient", function() {
|
||||
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"],
|
||||
});
|
||||
|
||||
const prom = client.requestRegisterEmailToken("bob@email", "secret", 1);
|
||||
httpBackend.when("POST", "/register/email/requestToken").check(req => {
|
||||
const prom = client!.requestRegisterEmailToken("bob@email", "secret", 1);
|
||||
httpBackend!.when("POST", "/register/email/requestToken").check(req => {
|
||||
expect(req.data).toStrictEqual({
|
||||
email: "bob@email",
|
||||
client_secret: "secret",
|
||||
send_attempt: 1,
|
||||
});
|
||||
}).respond(200, response);
|
||||
await httpBackend.flush();
|
||||
await httpBackend!.flush('');
|
||||
expect(await prom).toStrictEqual(response);
|
||||
});
|
||||
});
|
||||
@@ -1021,11 +1102,11 @@ describe("MatrixClient", function() {
|
||||
it("should supply an id_access_token", async () => {
|
||||
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"],
|
||||
});
|
||||
|
||||
httpBackend.when("POST", "/invite").check(req => {
|
||||
httpBackend!.when("POST", "/invite").check(req => {
|
||||
expect(req.data).toStrictEqual({
|
||||
id_server: idServerDomain,
|
||||
id_access_token: identityAccessToken,
|
||||
@@ -1034,8 +1115,8 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
}).respond(200, {});
|
||||
|
||||
const prom = client.inviteByThreePid("!room:example.org", "email", targetEmail);
|
||||
await httpBackend.flush();
|
||||
const prom = client!.inviteByThreePid("!room:example.org", "email", targetEmail);
|
||||
await httpBackend!.flush('');
|
||||
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"],
|
||||
});
|
||||
|
||||
httpBackend.when("POST", "/createRoom").check(req => {
|
||||
httpBackend!.when("POST", "/createRoom").check(req => {
|
||||
expect(req.data).toMatchObject({
|
||||
invite_3pid: expect.arrayContaining([{
|
||||
...input.invite_3pid[0],
|
||||
@@ -1069,8 +1150,31 @@ describe("MatrixClient", function() {
|
||||
expect(req.data.invite_3pid.length).toBe(1);
|
||||
}).respond(200, response);
|
||||
|
||||
const prom = client.createRoom(input);
|
||||
await httpBackend.flush();
|
||||
const prom = client!.createRoom(input);
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -5,10 +5,12 @@ import { MatrixClient } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { MemoryStore } from "../../src/store/memory";
|
||||
import { MatrixError } from "../../src/http-api";
|
||||
import { ICreateClientOpts } from "../../src/client";
|
||||
import { IStore } from "../../src/store";
|
||||
|
||||
describe("MatrixClient opts", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let httpBackend = null;
|
||||
let httpBackend = new HttpBackend();
|
||||
const userId = "@alice:localhost";
|
||||
const userB = "@bob:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
@@ -67,7 +69,7 @@ describe("MatrixClient opts", function() {
|
||||
let client;
|
||||
beforeEach(function() {
|
||||
client = new MatrixClient({
|
||||
request: httpBackend.requestFn,
|
||||
request: httpBackend.requestFn as unknown as ICreateClientOpts['request'],
|
||||
store: undefined,
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
@@ -99,7 +101,7 @@ describe("MatrixClient opts", function() {
|
||||
];
|
||||
client.on("event", function(event) {
|
||||
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
|
||||
-1, "Recv unexpected event type: " + event.getType(),
|
||||
-1,
|
||||
);
|
||||
expectedEventTypes.splice(
|
||||
expectedEventTypes.indexOf(event.getType()), 1,
|
||||
@@ -118,7 +120,7 @@ describe("MatrixClient opts", function() {
|
||||
utils.syncPromise(client),
|
||||
]);
|
||||
expect(expectedEventTypes.length).toEqual(
|
||||
0, "Expected to see event types: " + expectedEventTypes,
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -127,8 +129,8 @@ describe("MatrixClient opts", function() {
|
||||
let client;
|
||||
beforeEach(function() {
|
||||
client = new MatrixClient({
|
||||
request: httpBackend.requestFn,
|
||||
store: new MemoryStore(),
|
||||
request: httpBackend.requestFn as unknown as ICreateClientOpts['request'],
|
||||
store: new MemoryStore() as IStore,
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
@@ -146,7 +148,7 @@ describe("MatrixClient opts", function() {
|
||||
error: "Ruh roh",
|
||||
}));
|
||||
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) {
|
||||
expect(err.errcode).toEqual("M_SOMETHING");
|
||||
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.
|
||||
*/
|
||||
|
||||
import { EventStatus, RoomEvent, MatrixClient } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
|
||||
import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src/matrix";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient retrying", function() {
|
||||
let client: MatrixClient = null;
|
||||
let httpBackend: TestClient["httpBackend"] = null;
|
||||
let scheduler;
|
||||
const userId = "@alice:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!room:here";
|
||||
let room: Room;
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
let room: Room | undefined;
|
||||
|
||||
beforeEach(function() {
|
||||
scheduler = new MatrixScheduler();
|
||||
const setupTests = (): [MatrixClient, HttpBackend, Room] => {
|
||||
const scheduler = new MatrixScheduler();
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
"DEVICE",
|
||||
@@ -37,15 +37,21 @@ describe("MatrixClient retrying", function() {
|
||||
undefined,
|
||||
{ scheduler },
|
||||
);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
room = new Room(roomId, client, userId);
|
||||
client.store.storeRoom(room);
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const client = testClient.client;
|
||||
const room = new Room(roomId, client, userId);
|
||||
client!.store.storeRoom(room);
|
||||
|
||||
return [client, httpBackend, room];
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
[client, httpBackend, room] = setupTests();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
return httpBackend.stop();
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
return httpBackend!.stop();
|
||||
});
|
||||
|
||||
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() {
|
||||
// send a couple of events; the second will be queued
|
||||
const p1 = client.sendMessage(roomId, {
|
||||
const p1 = client!.sendMessage(roomId, {
|
||||
"msgtype": "m.text",
|
||||
"body": "m1",
|
||||
}).then(function() {
|
||||
@@ -79,13 +85,13 @@ describe("MatrixClient retrying", function() {
|
||||
// XXX: it turns out that the promise returned by this message
|
||||
// never gets resolved.
|
||||
// https://github.com/matrix-org/matrix-js-sdk/issues/496
|
||||
client.sendMessage(roomId, {
|
||||
client!.sendMessage(roomId, {
|
||||
"msgtype": "m.text",
|
||||
"body": "m2",
|
||||
});
|
||||
|
||||
// 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);
|
||||
const ev1 = tl[0];
|
||||
const ev2 = tl[1];
|
||||
@@ -94,24 +100,24 @@ describe("MatrixClient retrying", function() {
|
||||
expect(ev2.status).toEqual(EventStatus.SENDING);
|
||||
|
||||
// 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
|
||||
expect(ev2.status).toEqual(EventStatus.QUEUED);
|
||||
|
||||
// now we can cancel the second and check everything looks sane
|
||||
client.cancelPendingEvent(ev2);
|
||||
client!.cancelPendingEvent(ev2);
|
||||
expect(ev2.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(tl.length).toEqual(1);
|
||||
|
||||
// shouldn't be able to cancel the first message yet
|
||||
expect(function() {
|
||||
client.cancelPendingEvent(ev1);
|
||||
client!.cancelPendingEvent(ev1);
|
||||
}).toThrow();
|
||||
}).respond(400); // fail the first message
|
||||
|
||||
// wait for the localecho of ev1 to be updated
|
||||
const p3 = new Promise<void>((resolve, reject) => {
|
||||
room.on(RoomEvent.LocalEchoUpdated, (ev0) => {
|
||||
room!.on(RoomEvent.LocalEchoUpdated, (ev0) => {
|
||||
if (ev0 === ev1) {
|
||||
resolve();
|
||||
}
|
||||
@@ -121,7 +127,7 @@ describe("MatrixClient retrying", function() {
|
||||
expect(tl.length).toEqual(1);
|
||||
|
||||
// cancel the first message
|
||||
client.cancelPendingEvent(ev1);
|
||||
client!.cancelPendingEvent(ev1);
|
||||
expect(ev1.status).toEqual(EventStatus.CANCELLED);
|
||||
expect(tl.length).toEqual(0);
|
||||
});
|
||||
@@ -129,7 +135,7 @@ describe("MatrixClient retrying", function() {
|
||||
return Promise.all([
|
||||
p1,
|
||||
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 { EventStatus } from "../../src/models/event";
|
||||
import { RoomEvent } from "../../src";
|
||||
import { ClientEvent, IEvent, MatrixClient, RoomEvent } from "../../src";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient room timelines", function() {
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
const userId = "@alice:localhost";
|
||||
const userName = "Alice";
|
||||
const accessToken = "aseukfgwef";
|
||||
const roomId = "!foo:bar";
|
||||
const otherUserId = "@bob:localhost";
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: HttpBackend | undefined;
|
||||
|
||||
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
|
||||
room: roomId, mship: "join", user: userId, name: userName,
|
||||
});
|
||||
@@ -55,8 +74,7 @@ describe("MatrixClient room timelines", function() {
|
||||
},
|
||||
};
|
||||
|
||||
function setNextSyncData(events) {
|
||||
events = events || [];
|
||||
function setNextSyncData(events: Partial<IEvent>[] = []) {
|
||||
NEXT_SYNC_DATA = {
|
||||
next_batch: "n",
|
||||
presence: { events: [] },
|
||||
@@ -77,19 +95,9 @@ describe("MatrixClient room timelines", function() {
|
||||
throw new Error("setNextSyncData only works with one room id");
|
||||
}
|
||||
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
|
||||
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);
|
||||
} else {
|
||||
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
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
@@ -106,41 +114,46 @@ describe("MatrixClient room timelines", function() {
|
||||
undefined,
|
||||
{ timelineSupport: true },
|
||||
);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
const httpBackend = testClient.httpBackend;
|
||||
const client = testClient.client;
|
||||
|
||||
setNextSyncData();
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
||||
httpBackend!.when("GET", "/versions").respond(200, {});
|
||||
httpBackend!.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
client.startClient();
|
||||
client!.startClient();
|
||||
|
||||
return [client!, httpBackend];
|
||||
};
|
||||
|
||||
beforeEach(async function() {
|
||||
[client!, httpBackend] = setupTestClient();
|
||||
await httpBackend.flush("/versions");
|
||||
await httpBackend.flush("/pushrules");
|
||||
await httpBackend.flush("/filter");
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
client!.stopClient();
|
||||
return httpBackend!.stop();
|
||||
});
|
||||
|
||||
describe("local echo events", function() {
|
||||
it("should be added immediately after calling MatrixClient.sendEvent " +
|
||||
"with EventStatus.SENDING and the right event.sender", function(done) {
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
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
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
// check status
|
||||
@@ -150,68 +163,68 @@ describe("MatrixClient room timelines", function() {
|
||||
expect(member.userId).toEqual(userId);
|
||||
expect(member.name).toEqual(userName);
|
||||
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
httpBackend!.flush("/sync", 1).then(function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
|
||||
it("should be updated correctly when the send request finishes " +
|
||||
"BEFORE the event comes down the event stream", function(done) {
|
||||
const eventId = "$foo:bar";
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
httpBackend!.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
});
|
||||
|
||||
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.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
client.sendTextMessage(roomId, "I am a fish", "txn1").then(
|
||||
function() {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
const room = client!.getRoom(roomId)!;
|
||||
client!.sendTextMessage(roomId, "I am a fish", "txn1").then(
|
||||
function() {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
done();
|
||||
httpBackend!.flush("/sync", 1).then(function() {
|
||||
expect(room.timeline[1].getId()).toEqual(eventId);
|
||||
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 " +
|
||||
"AFTER the event comes down the event stream", function(done) {
|
||||
const eventId = "$foo:bar";
|
||||
httpBackend.when("PUT", "/txn1").respond(200, {
|
||||
httpBackend!.when("PUT", "/txn1").respond(200, {
|
||||
event_id: eventId,
|
||||
});
|
||||
|
||||
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.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
httpBackend.flush("/sync", 1).then(function() {
|
||||
const room = client!.getRoom(roomId)!;
|
||||
const promise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
|
||||
httpBackend!.flush("/sync", 1).then(function() {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
httpBackend.flush("/txn1", 1);
|
||||
httpBackend!.flush("/txn1", 1);
|
||||
promise.then(function() {
|
||||
expect(room.timeline.length).toEqual(2);
|
||||
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() {
|
||||
sbEvents = [];
|
||||
httpBackend.when("GET", "/messages").respond(200, function() {
|
||||
httpBackend!.when("GET", "/messages").respond(200, function() {
|
||||
return {
|
||||
chunk: sbEvents,
|
||||
start: "pagin_start",
|
||||
@@ -240,26 +253,26 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
it("should set Room.oldState.paginationToken to null at the start" +
|
||||
" of the timeline.", function(done) {
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.scrollback(room).then(function() {
|
||||
client!.scrollback(room).then(function() {
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
expect(room.oldState.paginationToken).toBe(null);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend.flush("/sync", 1).then(() => {
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
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) {
|
||||
@@ -275,7 +288,7 @@ describe("MatrixClient room timelines", function() {
|
||||
// make an m.room.member event for alice's join
|
||||
const joinMshipEvent = utils.mkMembership({
|
||||
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
|
||||
@@ -286,7 +299,7 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
oldMshipEvent.prev_content = {
|
||||
displayname: "Old Alice",
|
||||
avatar_url: null,
|
||||
avatar_url: undefined,
|
||||
membership: "join",
|
||||
};
|
||||
|
||||
@@ -303,15 +316,15 @@ describe("MatrixClient room timelines", function() {
|
||||
joinMshipEvent,
|
||||
];
|
||||
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
// sync response
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
|
||||
client.scrollback(room).then(function() {
|
||||
client!.scrollback(room).then(function() {
|
||||
expect(room.timeline.length).toEqual(5);
|
||||
const joinMsg = room.timeline[0];
|
||||
expect(joinMsg.sender.name).toEqual("Old Alice");
|
||||
@@ -321,14 +334,14 @@ describe("MatrixClient room timelines", function() {
|
||||
expect(newMsg.sender.name).toEqual(userName);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend.flush("/sync", 1).then(() => {
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
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) {
|
||||
@@ -342,27 +355,27 @@ describe("MatrixClient room timelines", function() {
|
||||
}),
|
||||
];
|
||||
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
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[0].event).toEqual(sbEvents[1]);
|
||||
expect(room.timeline[1].event).toEqual(sbEvents[0]);
|
||||
|
||||
// still have a sync to flush
|
||||
httpBackend.flush("/sync", 1).then(() => {
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
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) {
|
||||
@@ -373,25 +386,25 @@ describe("MatrixClient room timelines", function() {
|
||||
}),
|
||||
];
|
||||
|
||||
client.on("sync", function(state) {
|
||||
client!.on(ClientEvent.Sync, function(state) {
|
||||
if (state !== "PREPARED") {
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room.oldState.paginationToken).toBeTruthy();
|
||||
|
||||
client.scrollback(room, 1).then(function() {
|
||||
client!.scrollback(room, 1).then(function() {
|
||||
expect(room.oldState.paginationToken).toEqual(sbEndTok);
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1).then(function() {
|
||||
httpBackend!.flush("/messages", 1).then(function() {
|
||||
// still have a sync to flush
|
||||
httpBackend.flush("/sync", 1).then(() => {
|
||||
httpBackend!.flush("/sync", 1).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
httpBackend.flush("/sync", 1);
|
||||
httpBackend!.flush("/sync", 1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -404,23 +417,23 @@ describe("MatrixClient room timelines", function() {
|
||||
setNextSyncData(eventData);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
|
||||
let index = 0;
|
||||
client.on("Room.timeline", function(event, rm, toStart) {
|
||||
client!.on(RoomEvent.Timeline, function(event, rm, toStart) {
|
||||
expect(toStart).toBe(false);
|
||||
expect(rm).toEqual(room);
|
||||
expect(event.event).toEqual(eventData[index]);
|
||||
index += 1;
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend!.flush("/messages", 1);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
expect(index).toEqual(2);
|
||||
expect(room.timeline.length).toEqual(3);
|
||||
@@ -442,17 +455,16 @@ describe("MatrixClient room timelines", function() {
|
||||
}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
|
||||
setNextSyncData(eventData);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
const preNameEvent = room.timeline[room.timeline.length - 3];
|
||||
const postNameEvent = room.timeline[room.timeline.length - 1];
|
||||
@@ -468,22 +480,21 @@ describe("MatrixClient room timelines", function() {
|
||||
name: "Room 2",
|
||||
},
|
||||
});
|
||||
secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
|
||||
setNextSyncData([secondRoomNameEvent]);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
let nameEmitCount = 0;
|
||||
client.on("Room.name", function(rm) {
|
||||
client!.on(RoomEvent.Name, function(rm) {
|
||||
nameEmitCount += 1;
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
expect(nameEmitCount).toEqual(1);
|
||||
expect(room.name).toEqual("Room 2");
|
||||
@@ -493,12 +504,11 @@ describe("MatrixClient room timelines", function() {
|
||||
name: "Room 3",
|
||||
},
|
||||
});
|
||||
thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
|
||||
setNextSyncData([thirdRoomNameEvent]);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]);
|
||||
}).then(function() {
|
||||
expect(nameEmitCount).toEqual(2);
|
||||
@@ -518,26 +528,24 @@ describe("MatrixClient room timelines", function() {
|
||||
user: userC, room: roomId, mship: "invite", skey: userD,
|
||||
}),
|
||||
];
|
||||
eventData[0].__prev_event = null;
|
||||
eventData[1].__prev_event = null;
|
||||
setNextSyncData(eventData);
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
expect(room.currentState.getMembers().length).toEqual(4);
|
||||
expect(room.currentState.getMember(userC).name).toEqual("C");
|
||||
expect(room.currentState.getMember(userC).membership).toEqual(
|
||||
expect(room.currentState.getMember(userC)!.name).toEqual("C");
|
||||
expect(room.currentState.getMember(userC)!.membership).toEqual(
|
||||
"join",
|
||||
);
|
||||
expect(room.currentState.getMember(userD).name).toEqual(userD);
|
||||
expect(room.currentState.getMember(userD).membership).toEqual(
|
||||
expect(room.currentState.getMember(userD)!.name).toEqual(userD);
|
||||
expect(room.currentState.getMember(userD)!.membership).toEqual(
|
||||
"invite",
|
||||
);
|
||||
});
|
||||
@@ -554,26 +562,26 @@ describe("MatrixClient room timelines", function() {
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/versions", 1),
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/versions", 1),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend!.flush("/messages", 1);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
expect(room.timeline[0].event).toEqual(eventData[0]);
|
||||
expect(room.currentState.getMembers().length).toEqual(2);
|
||||
expect(room.currentState.getMember(userId).name).toEqual(userName);
|
||||
expect(room.currentState.getMember(userId).membership).toEqual(
|
||||
expect(room.currentState.getMember(userId)!.name).toEqual(userName);
|
||||
expect(room.currentState.getMember(userId)!.membership).toEqual(
|
||||
"join",
|
||||
);
|
||||
expect(room.currentState.getMember(otherUserId).name).toEqual("Bob");
|
||||
expect(room.currentState.getMember(otherUserId).membership).toEqual(
|
||||
expect(room.currentState.getMember(otherUserId)!.name).toEqual("Bob");
|
||||
expect(room.currentState.getMember(otherUserId)!.membership).toEqual(
|
||||
"join",
|
||||
);
|
||||
});
|
||||
@@ -588,21 +596,21 @@ describe("MatrixClient room timelines", function() {
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
|
||||
let emitCount = 0;
|
||||
client.on("Room.timelineReset", function(emitRoom) {
|
||||
client!.on(RoomEvent.TimelineReset, function(emitRoom) {
|
||||
expect(emitRoom).toEqual(room);
|
||||
emitCount++;
|
||||
});
|
||||
|
||||
httpBackend.flush("/messages", 1);
|
||||
httpBackend!.flush("/messages", 1);
|
||||
return Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!),
|
||||
]).then(function() {
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
@@ -618,7 +626,7 @@ describe("MatrixClient room timelines", function() {
|
||||
];
|
||||
|
||||
const contextUrl = `/rooms/${encodeURIComponent(roomId)}/context/` +
|
||||
`${encodeURIComponent(initialSyncEventData[2].event_id)}`;
|
||||
`${encodeURIComponent(initialSyncEventData[2].event_id!)}`;
|
||||
const contextResponse = {
|
||||
start: "start_token",
|
||||
events_before: [initialSyncEventData[1], initialSyncEventData[0]],
|
||||
@@ -636,19 +644,19 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
// Create a room from the sync
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 1),
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 1),
|
||||
]);
|
||||
|
||||
// Get the room after the first sync so the room is created
|
||||
room = client.getRoom(roomId);
|
||||
room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should clear and refresh messages in timeline', async () => {
|
||||
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
|
||||
// to construct a new timeline from.
|
||||
httpBackend.when("GET", contextUrl)
|
||||
httpBackend!.when("GET", contextUrl)
|
||||
.respond(200, function() {
|
||||
// The timeline should be cleared at this point in the refresh
|
||||
expect(room.timeline.length).toEqual(0);
|
||||
@@ -659,7 +667,7 @@ describe("MatrixClient room timelines", function() {
|
||||
// Refresh the timeline.
|
||||
await Promise.all([
|
||||
room.refreshLiveTimeline(),
|
||||
httpBackend.flushAllExpected(),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
|
||||
// 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
|
||||
// sure the sync pagination still works as expected after messing
|
||||
// the refresh timline logic messes with the pagination tokens.
|
||||
httpBackend.when("GET", contextUrl)
|
||||
httpBackend!.when("GET", contextUrl)
|
||||
.respond(200, () => {
|
||||
// Now finally return and make the `/context` request respond
|
||||
return contextResponse;
|
||||
@@ -700,7 +708,7 @@ describe("MatrixClient room timelines", function() {
|
||||
const racingSyncEventData = [
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
const waitForRaceySyncAfterResetPromise = new Promise((resolve, reject) => {
|
||||
const waitForRaceySyncAfterResetPromise = new Promise<void>((resolve, reject) => {
|
||||
let eventFired = false;
|
||||
// Throw a more descriptive error if this part of the test times out.
|
||||
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
|
||||
// shows up (simulate a /sync naturally racing with us).
|
||||
setNextSyncData(racingSyncEventData);
|
||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
||||
httpBackend!.when("GET", "/sync").respond(200, function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
await Promise.all([
|
||||
httpBackend.flush("/sync", 1),
|
||||
utils.syncPromise(client, 1),
|
||||
httpBackend!.flush("/sync", 1),
|
||||
utils.syncPromise(client!, 1),
|
||||
]);
|
||||
// Make sure the timeline has the racey sync data
|
||||
const afterRaceySyncTimelineEvents = room
|
||||
@@ -761,7 +769,7 @@ describe("MatrixClient room timelines", function() {
|
||||
await Promise.all([
|
||||
refreshLiveTimelinePromise,
|
||||
// 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
|
||||
@@ -770,12 +778,12 @@ describe("MatrixClient room timelines", function() {
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(afterRefreshEventData);
|
||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
||||
httpBackend!.when("GET", "/sync").respond(200, function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 1),
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 1),
|
||||
]);
|
||||
|
||||
// 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 () => {
|
||||
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
|
||||
// to construct a new timeline from.
|
||||
httpBackend.when("GET", contextUrl)
|
||||
httpBackend!.when("GET", contextUrl)
|
||||
.respond(500, function() {
|
||||
// The timeline should be cleared at this point in the refresh
|
||||
expect(room.timeline.length).toEqual(0);
|
||||
@@ -809,7 +817,7 @@ describe("MatrixClient room timelines", function() {
|
||||
// Refresh the timeline and expect it to fail
|
||||
const settledFailedRefreshPromises = await Promise.allSettled([
|
||||
room.refreshLiveTimeline(),
|
||||
httpBackend.flushAllExpected(),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
// We only expect `TEST_FAKE_ERROR` here. Anything else is
|
||||
// unexpected and should fail the test.
|
||||
@@ -825,7 +833,7 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
// `/messages` request for `refreshLiveTimeline()` ->
|
||||
// `getLatestTimeline()` to construct a new timeline from.
|
||||
httpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`)
|
||||
httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`)
|
||||
.respond(200, function() {
|
||||
return {
|
||||
chunk: [{
|
||||
@@ -837,7 +845,7 @@ describe("MatrixClient room timelines", function() {
|
||||
// `/context` request for `refreshLiveTimeline()` ->
|
||||
// `getLatestTimeline()` -> `getEventTimeline()` to construct a new
|
||||
// timeline from.
|
||||
httpBackend.when("GET", contextUrl)
|
||||
httpBackend!.when("GET", contextUrl)
|
||||
.respond(200, function() {
|
||||
// The timeline should be cleared at this point in the refresh
|
||||
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
|
||||
await Promise.all([
|
||||
room.refreshLiveTimeline(),
|
||||
httpBackend.flushAllExpected(),
|
||||
httpBackend!.flushAllExpected(),
|
||||
]);
|
||||
|
||||
// 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 }),
|
||||
];
|
||||
setNextSyncData(afterRefreshEventData);
|
||||
httpBackend.when("GET", "/sync").respond(200, function() {
|
||||
httpBackend!.when("GET", "/sync").respond(200, function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
await Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 1),
|
||||
httpBackend!.flushAllExpected(),
|
||||
utils.syncPromise(client!, 1),
|
||||
]);
|
||||
|
||||
// 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;
|
||||
}
|
||||
const Olm = global.Olm;
|
||||
|
||||
let testOlmAccount: Account;
|
||||
let testOlmAccount: Olm.Account;
|
||||
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() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
beforeEach(async function() {
|
||||
aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs",
|
||||
);
|
||||
testOlmAccount = new Olm.Account();
|
||||
testOlmAccount.create();
|
||||
await aliceTestClient.client.initCrypto();
|
||||
aliceTestClient.client.crypto.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
|
||||
[testOlmAccount, aliceTestClient] = setupTestClient();
|
||||
await aliceTestClient!.client.initCrypto();
|
||||
aliceTestClient!.client.crypto!.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
return aliceTestClient.stop();
|
||||
return aliceTestClient!.stop();
|
||||
});
|
||||
|
||||
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);
|
||||
}).then(() => {
|
||||
const privkey = decodeRecoveryKey(RECOVERY_KEY);
|
||||
return aliceTestClient.client.crypto.storeSessionBackupPrivateKey(privkey);
|
||||
return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey);
|
||||
}).then(() => {
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
aliceTestClient.expectKeyBackupQuery(
|
||||
aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
aliceTestClient!.expectKeyBackupQuery(
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
200,
|
||||
CURVE25519_KEY_BACKUP_DATA,
|
||||
);
|
||||
return aliceTestClient.httpBackend.flushAllExpected();
|
||||
return aliceTestClient!.httpBackend.flushAllExpected();
|
||||
}).then(function(): Promise<MatrixEvent> {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient!.client.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
|
||||
if (event.getContent()) {
|
||||
|
||||
@@ -29,8 +29,11 @@ import {
|
||||
IDownloadKeyResult,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
IndexedDBCryptoStore,
|
||||
Room,
|
||||
} from "../../src/matrix";
|
||||
import { IDeviceKeys } from "../../src/crypto/dehydration";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -204,9 +207,11 @@ describe("megolm", () => {
|
||||
}
|
||||
const Olm = global.Olm;
|
||||
|
||||
let testOlmAccount: Olm.Account;
|
||||
let testSenderKey: string;
|
||||
let aliceTestClient: TestClient;
|
||||
let testOlmAccount = {} as unknown as Olm.Account;
|
||||
let testSenderKey = '';
|
||||
let aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "device2", "access_token2",
|
||||
);
|
||||
|
||||
/**
|
||||
* 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 () => {
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
@@ -316,7 +324,7 @@ describe("megolm", () => {
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.isEncrypted()).toBe(true);
|
||||
const decryptedEvent = await testUtils.awaitDecryption(event);
|
||||
@@ -326,10 +334,13 @@ describe("megolm", () => {
|
||||
it("Alice receives a megolm message before the session keys", async () => {
|
||||
// https://github.com/vector-im/element-web/issues/2273
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event, but don't send it yet
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
@@ -353,7 +364,7 @@ describe("megolm", () => {
|
||||
});
|
||||
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');
|
||||
|
||||
// now she gets the room_key event
|
||||
@@ -383,10 +394,13 @@ describe("megolm", () => {
|
||||
|
||||
it("Alice gets a second room_key message", async () => {
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted1 = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
@@ -439,7 +453,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();
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
@@ -468,6 +482,9 @@ describe("megolm", () => {
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
|
||||
@@ -484,7 +501,7 @@ describe("megolm", () => {
|
||||
let inboundGroupSession: Olm.InboundGroupSession;
|
||||
aliceTestClient.httpBackend.when(
|
||||
'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 ct = m.ciphertext[testSenderKey];
|
||||
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
|
||||
@@ -510,7 +527,7 @@ describe("megolm", () => {
|
||||
return { event_id: '$event_id' };
|
||||
});
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
const pendingMsg = room.getPendingEvents()[0];
|
||||
|
||||
await Promise.all([
|
||||
@@ -541,13 +558,16 @@ describe("megolm", () => {
|
||||
|
||||
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(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
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');
|
||||
@@ -592,6 +612,9 @@ describe("megolm", () => {
|
||||
|
||||
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(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
@@ -607,7 +630,7 @@ describe("megolm", () => {
|
||||
let megolmSessionId: string;
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/',
|
||||
).respond(200, function(_path, content) {
|
||||
).respond(200, function(_path, content: any) {
|
||||
logger.log('sendToDevice: ', content);
|
||||
const m = content.messages['@bob:xyz'].DEVICE_ID;
|
||||
const ct = m.ciphertext[testSenderKey];
|
||||
@@ -685,7 +708,7 @@ describe("megolm", () => {
|
||||
// invalidate the device cache for all members in e2e rooms (ie,
|
||||
// herself), and do a key query.
|
||||
aliceTestClient.expectKeyQuery(
|
||||
getTestKeysQueryResponse(aliceTestClient.userId),
|
||||
getTestKeysQueryResponse(aliceTestClient.userId!),
|
||||
);
|
||||
|
||||
await aliceTestClient.httpBackend.flushAllExpected();
|
||||
@@ -695,28 +718,30 @@ describe("megolm", () => {
|
||||
await aliceTestClient.client.sendTextMessage(ROOM_ID, 'test');
|
||||
throw new Error("sendTextMessage succeeded on an unknown device");
|
||||
} catch (e) {
|
||||
expect(e.name).toEqual("UnknownDeviceError");
|
||||
expect(Object.keys(e.devices)).toEqual([aliceTestClient.userId]);
|
||||
expect(Object.keys(e.devices[aliceTestClient.userId])).
|
||||
expect((e as any).name).toEqual("UnknownDeviceError");
|
||||
expect(Object.keys((e as any).devices)).toEqual([aliceTestClient.userId!]);
|
||||
expect(Object.keys((e as any)?.devices[aliceTestClient.userId!])).
|
||||
toEqual(['DEVICE_ID']);
|
||||
}
|
||||
|
||||
// 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(
|
||||
200, function(_path, content) {
|
||||
expect(content.one_time_keys[aliceTestClient.userId].DEVICE_ID)
|
||||
200, function(_path, content: IClaimOTKsResult) {
|
||||
expect(content.one_time_keys[aliceTestClient.userId!].DEVICE_ID)
|
||||
.toEqual("signed_curve25519");
|
||||
return getTestKeysClaimResponse(aliceTestClient.userId);
|
||||
return getTestKeysClaimResponse(aliceTestClient.userId!);
|
||||
});
|
||||
|
||||
let p2pSession: Olm.Session;
|
||||
let inboundGroupSession: Olm.InboundGroupSession;
|
||||
aliceTestClient.httpBackend.when(
|
||||
'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);
|
||||
const m = content.messages[aliceTestClient.userId].DEVICE_ID;
|
||||
const m = content.messages[aliceTestClient.userId!].DEVICE_ID;
|
||||
const ct = m.ciphertext[testSenderKey];
|
||||
expect(ct.type).toEqual(0); // pre-key message
|
||||
|
||||
@@ -730,7 +755,7 @@ describe("megolm", () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
let decrypted: IEvent;
|
||||
let decrypted: Partial<IEvent> = {};
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, function(_path, content: IContent) {
|
||||
@@ -745,7 +770,7 @@ describe("megolm", () => {
|
||||
});
|
||||
|
||||
// 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();
|
||||
expect(pendingEvents.length).toEqual(1);
|
||||
const unsentEvent = pendingEvents[0];
|
||||
@@ -760,7 +785,7 @@ describe("megolm", () => {
|
||||
]);
|
||||
|
||||
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 () => {
|
||||
@@ -786,6 +811,10 @@ describe("megolm", () => {
|
||||
logger.log('Forcing alice to download our device keys');
|
||||
const downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz'),
|
||||
);
|
||||
|
||||
// so will this.
|
||||
const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test')
|
||||
.then(() => {
|
||||
@@ -805,9 +834,12 @@ describe("megolm", () => {
|
||||
it("Alice exports megolm keys and imports them to a new device", async () => {
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} }, failures: {} });
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
// establish an olm session with alice
|
||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
@@ -839,7 +871,7 @@ describe("megolm", () => {
|
||||
});
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
await room.decryptCriticalEvents();
|
||||
expect(room.getLiveTimeline().getEvents()[0].getContent().body).toEqual('42');
|
||||
|
||||
@@ -855,6 +887,8 @@ describe("megolm", () => {
|
||||
await aliceTestClient.client.importRoomKeys(exported);
|
||||
await aliceTestClient.start();
|
||||
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
@@ -897,7 +931,7 @@ describe("megolm", () => {
|
||||
...rawEvent,
|
||||
room: ROOM_ID,
|
||||
});
|
||||
await event1.attemptDecryption(testClient.client.crypto, { isRetry: true });
|
||||
await event1.attemptDecryption(testClient.client.crypto!, { isRetry: true });
|
||||
expect(event1.isKeySourceUntrusted()).toBeTruthy();
|
||||
|
||||
const event2 = testUtils.mkEvent({
|
||||
@@ -913,24 +947,27 @@ describe("megolm", () => {
|
||||
// @ts-ignore - private
|
||||
event2.senderCurve25519Key = testSenderKey;
|
||||
// @ts-ignore - private
|
||||
testClient.client.crypto.onRoomKeyEvent(event2);
|
||||
testClient.client.crypto!.onRoomKeyEvent(event2);
|
||||
|
||||
const event3 = testUtils.mkEvent({
|
||||
event: true,
|
||||
...rawEvent,
|
||||
room: ROOM_ID,
|
||||
});
|
||||
await event3.attemptDecryption(testClient.client.crypto, { isRetry: true });
|
||||
await event3.attemptDecryption(testClient.client.crypto!, { isRetry: true });
|
||||
expect(event3.isKeySourceUntrusted()).toBeFalsy();
|
||||
testClient.stop();
|
||||
});
|
||||
|
||||
it("Alice can decrypt a message with falsey content", async () => {
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
@@ -972,7 +1009,7 @@ describe("megolm", () => {
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.isEncrypted()).toBe(true);
|
||||
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",
|
||||
async () => {
|
||||
await aliceTestClient.start();
|
||||
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
|
||||
|
||||
// make the room_key event
|
||||
const roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
@@ -1036,13 +1076,292 @@ describe("megolm", () => {
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.isEncrypted()).toBe(true);
|
||||
await event.attemptDecryption(aliceTestClient.client.crypto);
|
||||
await event.attemptDecryption(aliceTestClient.client.crypto!);
|
||||
expect(event.getContent()).toEqual({});
|
||||
const redactionEvent: any = event.getRedactionEvent();
|
||||
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";
|
||||
|
||||
describe("SlidingSyncSdk", () => {
|
||||
let client: MatrixClient = null;
|
||||
let httpBackend: MockHttpBackend = null;
|
||||
let sdk: SlidingSyncSdk = null;
|
||||
let mockSlidingSync: SlidingSync = null;
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: MockHttpBackend | undefined;
|
||||
let sdk: SlidingSyncSdk | undefined;
|
||||
let mockSlidingSync: SlidingSync | undefined;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
@@ -66,7 +66,7 @@ describe("SlidingSyncSdk", () => {
|
||||
event_id: "$" + eventIdCounter,
|
||||
};
|
||||
};
|
||||
const mkOwnStateEvent = (evType: string, content: object, stateKey?: string): IStateEvent => {
|
||||
const mkOwnStateEvent = (evType: string, content: object, stateKey = ''): IStateEvent => {
|
||||
eventIdCounter++;
|
||||
return {
|
||||
type: evType,
|
||||
@@ -103,24 +103,24 @@ describe("SlidingSyncSdk", () => {
|
||||
client = testClient.client;
|
||||
mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0));
|
||||
if (testOpts.withCrypto) {
|
||||
httpBackend.when("GET", "/room_keys/version").respond(404, {});
|
||||
await client.initCrypto();
|
||||
testOpts.crypto = client.crypto;
|
||||
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
|
||||
await client!.initCrypto();
|
||||
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);
|
||||
};
|
||||
|
||||
// tear down client/httpBackend globals
|
||||
const teardownClient = () => {
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
client!.stopClient();
|
||||
return httpBackend!.stop();
|
||||
};
|
||||
|
||||
// find an extension on a SlidingSyncSdk instance
|
||||
const findExtension = (name: string): Extension => {
|
||||
expect(mockSlidingSync.registerExtension).toHaveBeenCalled();
|
||||
const mockFn = mockSlidingSync.registerExtension as jest.Mock;
|
||||
expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
|
||||
const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
|
||||
// find the extension
|
||||
for (let i = 0; i < mockFn.mock.calls.length; i++) {
|
||||
const calledExtension = mockFn.mock.calls[i][0] as Extension;
|
||||
@@ -137,14 +137,14 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
afterAll(teardownClient);
|
||||
it("can sync()", async () => {
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
expect(mockSlidingSync.start).toBeCalled();
|
||||
expect(mockSlidingSync!.start).toBeCalled();
|
||||
});
|
||||
it("can stop()", async () => {
|
||||
sdk.stop();
|
||||
expect(mockSlidingSync.stop).toBeCalled();
|
||||
sdk!.stop();
|
||||
expect(mockSlidingSync!.stop).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -156,8 +156,8 @@ describe("SlidingSyncSdk", () => {
|
||||
|
||||
describe("initial", () => {
|
||||
beforeAll(async () => {
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
});
|
||||
// inject some rooms with different fields set.
|
||||
@@ -168,6 +168,7 @@ describe("SlidingSyncSdk", () => {
|
||||
const roomD = "!d_with_notif_count:localhost";
|
||||
const roomE = "!e_with_invite:localhost";
|
||||
const roomF = "!f_calc_room_name:localhost";
|
||||
const roomG = "!g_join_invite_counts:localhost";
|
||||
const data: Record<string, MSC3575RoomData> = {
|
||||
[roomA]: {
|
||||
name: "A",
|
||||
@@ -261,56 +262,83 @@ describe("SlidingSyncSdk", () => {
|
||||
],
|
||||
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", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
|
||||
const gotRoom = client.getRoom(roomA);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.name).toEqual(data[roomA].name);
|
||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
|
||||
});
|
||||
|
||||
it("can be created with timeline only", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
|
||||
const gotRoom = client.getRoom(roomB);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
|
||||
const gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.name).toEqual(data[roomB].name);
|
||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
|
||||
});
|
||||
|
||||
it("can be created with a highlight_count", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
|
||||
const gotRoom = client.getRoom(roomC);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
|
||||
const gotRoom = client!.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
|
||||
).toEqual(data[roomC].highlight_count);
|
||||
});
|
||||
|
||||
it("can be created with a notification_count", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
|
||||
const gotRoom = client.getRoom(roomD);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
|
||||
const gotRoom = client!.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
|
||||
).toEqual(data[roomD].notification_count);
|
||||
});
|
||||
|
||||
it("can be created with invite_state", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
|
||||
const gotRoom = client.getRoom(roomE);
|
||||
it("can be created with an invited/joined_count", () => {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
|
||||
const gotRoom = client!.getRoom(roomG);
|
||||
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.currentState.getJoinRule()).toEqual(JoinRule.Invite);
|
||||
});
|
||||
|
||||
it("uses the 'name' field to caluclate the room name", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
|
||||
const gotRoom = client.getRoom(roomF);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
|
||||
const gotRoom = client!.getRoom(roomF);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.name,
|
||||
).toEqual(data[roomF].name);
|
||||
@@ -319,61 +347,80 @@ describe("SlidingSyncSdk", () => {
|
||||
describe("updating", () => {
|
||||
it("can update with a new timeline event", async () => {
|
||||
const newEvent = mkOwnEvent(EventType.RoomMessage, { body: "new event A" });
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, {
|
||||
timeline: [newEvent],
|
||||
required_state: [],
|
||||
name: data[roomA].name,
|
||||
});
|
||||
const gotRoom = client.getRoom(roomA);
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
const newTimeline = data[roomA].timeline;
|
||||
newTimeline.push(newEvent);
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline);
|
||||
});
|
||||
|
||||
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
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, {
|
||||
required_state: [
|
||||
mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, ""),
|
||||
],
|
||||
timeline: [],
|
||||
name: data[roomB].name,
|
||||
});
|
||||
gotRoom = client.getRoom(roomB);
|
||||
gotRoom = client!.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
|
||||
});
|
||||
|
||||
it("can update with a new highlight_count", async () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, {
|
||||
name: data[roomC].name,
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
highlight_count: 1,
|
||||
});
|
||||
const gotRoom = client.getRoom(roomC);
|
||||
const gotRoom = client!.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it("can update with a new notification_count", async () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, {
|
||||
name: data[roomD].name,
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
notification_count: 1,
|
||||
});
|
||||
const gotRoom = client.getRoom(roomD);
|
||||
const gotRoom = client!.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
|
||||
).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
|
||||
// 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
|
||||
@@ -386,14 +433,15 @@ describe("SlidingSyncSdk", () => {
|
||||
mkOwnEvent(EventType.RoomMessage, { body: "old event C" }),
|
||||
...timeline,
|
||||
];
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, {
|
||||
timeline: oldTimeline,
|
||||
required_state: [],
|
||||
name: data[roomA].name,
|
||||
initial: true, // e.g requested via room subscription
|
||||
});
|
||||
const gotRoom = client.getRoom(roomA);
|
||||
const gotRoom = client!.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
|
||||
logger.log("want:", oldTimeline.map((e) => (e.type + " : " + (e.content || {}).body)));
|
||||
logger.log("got:", gotRoom.getLiveTimeline().getEvents().map(
|
||||
@@ -410,50 +458,50 @@ describe("SlidingSyncSdk", () => {
|
||||
describe("lifecycle", () => {
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
});
|
||||
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 () => {
|
||||
mockSlidingSync.emit(
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete,
|
||||
{ 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"),
|
||||
);
|
||||
expect(sdk.getSyncState()).toEqual(SyncState.Reconnecting);
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Reconnecting);
|
||||
|
||||
for (let i = 0; i < FAILED_SYNC_ERROR_THRESHOLD; i++) {
|
||||
mockSlidingSync.emit(
|
||||
mockSlidingSync!.emit(
|
||||
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 () => {
|
||||
mockSlidingSync.emit(
|
||||
mockSlidingSync!.emit(
|
||||
SlidingSyncEvent.Lifecycle,
|
||||
SlidingSyncState.Complete,
|
||||
{ pos: "i", lists: [], rooms: {}, extensions: {} },
|
||||
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 () => {
|
||||
expect(mockSlidingSync.stop).not.toBeCalled();
|
||||
mockSlidingSync.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({
|
||||
expect(mockSlidingSync!.stop).not.toBeCalled();
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({
|
||||
errcode: "M_UNKNOWN_TOKEN",
|
||||
message: "Oh no your access token is no longer valid",
|
||||
}));
|
||||
expect(sdk.getSyncState()).toEqual(SyncState.Error);
|
||||
expect(mockSlidingSync.stop).toBeCalled();
|
||||
expect(sdk!.getSyncState()).toEqual(SyncState.Error);
|
||||
expect(mockSlidingSync!.stop).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -469,8 +517,8 @@ describe("SlidingSyncSdk", () => {
|
||||
avatar_url: "mxc://foobar",
|
||||
displayname: "The Invitee",
|
||||
};
|
||||
httpBackend.when("GET", "/profile").respond(200, inviteeProfile);
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
httpBackend!.when("GET", "/profile").respond(200, inviteeProfile);
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
initial: true,
|
||||
name: "Room with Invite",
|
||||
required_state: [],
|
||||
@@ -481,10 +529,10 @@ describe("SlidingSyncSdk", () => {
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee),
|
||||
],
|
||||
});
|
||||
await httpBackend.flush("/profile", 1, 1000);
|
||||
const room = client.getRoom(roomId);
|
||||
await httpBackend!.flush("/profile", 1, 1000);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
const inviteeMember = room.getMember(invitee);
|
||||
const inviteeMember = room.getMember(invitee)!;
|
||||
expect(inviteeMember).toBeDefined();
|
||||
expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url);
|
||||
expect(inviteeMember.name).toEqual(inviteeProfile.displayname);
|
||||
@@ -497,8 +545,8 @@ describe("SlidingSyncSdk", () => {
|
||||
await setupClient({
|
||||
withCrypto: true,
|
||||
});
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
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:
|
||||
// "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"."
|
||||
client.crypto.stop();
|
||||
client!.crypto!.stop();
|
||||
});
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
@@ -524,38 +572,38 @@ describe("SlidingSyncSdk", () => {
|
||||
// TODO: more assertions?
|
||||
});
|
||||
it("can update OTK counts", () => {
|
||||
client.crypto.updateOneTimeKeyCount = jest.fn();
|
||||
client!.crypto!.updateOneTimeKeyCount = jest.fn();
|
||||
ext.onResponse({
|
||||
device_one_time_keys_count: {
|
||||
signed_curve25519: 42,
|
||||
},
|
||||
});
|
||||
expect(client.crypto.updateOneTimeKeyCount).toHaveBeenCalledWith(42);
|
||||
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(42);
|
||||
ext.onResponse({
|
||||
device_one_time_keys_count: {
|
||||
not_signed_curve25519: 42,
|
||||
// missing field -> default to 0
|
||||
},
|
||||
});
|
||||
expect(client.crypto.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
|
||||
expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
|
||||
});
|
||||
it("can update fallback keys", () => {
|
||||
ext.onResponse({
|
||||
device_unused_fallback_key_types: ["signed_curve25519"],
|
||||
});
|
||||
expect(client.crypto.getNeedsNewFallback()).toEqual(false);
|
||||
expect(client!.crypto!.getNeedsNewFallback()).toEqual(false);
|
||||
ext.onResponse({
|
||||
device_unused_fallback_key_types: ["not_signed_curve25519"],
|
||||
});
|
||||
expect(client.crypto.getNeedsNewFallback()).toEqual(true);
|
||||
expect(client!.crypto!.getNeedsNewFallback()).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe("ExtensionAccountData", () => {
|
||||
let ext: Extension;
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
ext = findExtension("account_data");
|
||||
});
|
||||
@@ -570,7 +618,7 @@ describe("SlidingSyncSdk", () => {
|
||||
const globalContent = {
|
||||
info: "here",
|
||||
};
|
||||
let globalData = client.getAccountData(globalType);
|
||||
let globalData = client!.getAccountData(globalType);
|
||||
expect(globalData).toBeUndefined();
|
||||
ext.onResponse({
|
||||
global: [
|
||||
@@ -580,13 +628,13 @@ describe("SlidingSyncSdk", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
globalData = client.getAccountData(globalType);
|
||||
globalData = client!.getAccountData(globalType)!;
|
||||
expect(globalData).toBeDefined();
|
||||
expect(globalData.getContent()).toEqual(globalContent);
|
||||
});
|
||||
it("processes rooms account data", async () => {
|
||||
const roomId = "!room:id";
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
|
||||
name: "Room with account data",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
@@ -612,9 +660,9 @@ describe("SlidingSyncSdk", () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
const room = client.getRoom(roomId);
|
||||
const room = client!.getRoom(roomId)!;
|
||||
expect(room).toBeDefined();
|
||||
const event = room.getAccountData(roomType);
|
||||
const event = room.getAccountData(roomType)!;
|
||||
expect(event).toBeDefined();
|
||||
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(client.getAccountData(roomType)).toBeUndefined();
|
||||
expect(client!.getAccountData(roomType)).toBeUndefined();
|
||||
});
|
||||
it("can update push rules via account data", async () => {
|
||||
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();
|
||||
ext.onResponse({
|
||||
global: [
|
||||
@@ -665,16 +713,16 @@ describe("SlidingSyncSdk", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
pushRule = client.getRoomPushRule("global", roomId);
|
||||
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific][0]);
|
||||
pushRule = client!.getRoomPushRule("global", roomId)!;
|
||||
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific]![0]);
|
||||
});
|
||||
});
|
||||
describe("ExtensionToDevice", () => {
|
||||
let ext: Extension;
|
||||
beforeAll(async () => {
|
||||
await setupClient();
|
||||
const hasSynced = sdk.sync();
|
||||
await httpBackend.flushAllExpected();
|
||||
const hasSynced = sdk!.sync();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await hasSynced;
|
||||
ext = findExtension("to_device");
|
||||
});
|
||||
@@ -705,7 +753,7 @@ describe("SlidingSyncSdk", () => {
|
||||
foo: "bar",
|
||||
};
|
||||
let called = false;
|
||||
client.once(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
client!.once(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
expect(ev.getContent()).toEqual(toDeviceContent);
|
||||
expect(ev.getType()).toEqual(toDeviceType);
|
||||
called = true;
|
||||
@@ -723,7 +771,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
it("can cancel key verification requests", async () => {
|
||||
const seen: Record<string, boolean> = {};
|
||||
client.on(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
client!.on(ClientEvent.ToDeviceEvent, (ev) => {
|
||||
const evType = ev.getType();
|
||||
expect(seen[evType]).toBeFalsy();
|
||||
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.
|
||||
*/
|
||||
describe("SlidingSync", () => {
|
||||
let client: MatrixClient = null;
|
||||
let httpBackend: MockHttpBackend = null;
|
||||
let client: MatrixClient | undefined;
|
||||
let httpBackend: MockHttpBackend | undefined;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
const proxyBaseUrl = "http://localhost:8008";
|
||||
@@ -46,9 +46,9 @@ describe("SlidingSync", () => {
|
||||
|
||||
// tear down client/httpBackend globals
|
||||
const teardownClient = () => {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
client!.stopClient();
|
||||
return httpBackend!.stop();
|
||||
};
|
||||
|
||||
describe("start/stop", () => {
|
||||
@@ -57,14 +57,14 @@ describe("SlidingSync", () => {
|
||||
let slidingSync: SlidingSync;
|
||||
|
||||
it("should start the sync loop upon calling start()", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
const fakeResp = {
|
||||
pos: "a",
|
||||
lists: [],
|
||||
rooms: {},
|
||||
extensions: {},
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).respond(200, fakeResp);
|
||||
httpBackend!.when("POST", syncUrl).respond(200, fakeResp);
|
||||
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => {
|
||||
expect(state).toEqual(SlidingSyncState.RequestFinished);
|
||||
expect(resp).toEqual(fakeResp);
|
||||
@@ -72,13 +72,13 @@ describe("SlidingSync", () => {
|
||||
return true;
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
});
|
||||
|
||||
it("should stop the sync loop upon calling stop()", () => {
|
||||
slidingSync.stop();
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
httpBackend!.verifyNoOutstandingExpectation();
|
||||
});
|
||||
|
||||
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 () => {
|
||||
// add the subscription
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
|
||||
slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("room sub", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
@@ -225,7 +225,7 @@ describe("SlidingSync", () => {
|
||||
return true;
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
});
|
||||
|
||||
@@ -237,7 +237,7 @@ describe("SlidingSync", () => {
|
||||
["m.room.member", "*"],
|
||||
],
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("adjusted sub", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
@@ -258,7 +258,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
slidingSync.modifyRoomSubscriptionInfo(newSubInfo);
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
// need to set what the new subscription info is for subsequent tests
|
||||
roomSubInfo = newSubInfo;
|
||||
@@ -279,7 +279,7 @@ describe("SlidingSync", () => {
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("new subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
@@ -304,12 +304,12 @@ describe("SlidingSync", () => {
|
||||
const subs = slidingSync.getRoomSubscriptions();
|
||||
subs.add(anotherRoomID);
|
||||
slidingSync.modifyRoomSubscriptions(subs);
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
});
|
||||
|
||||
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;
|
||||
logger.log("unsub request", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
@@ -326,7 +326,7 @@ describe("SlidingSync", () => {
|
||||
// remove the subscription for the first room
|
||||
slidingSync.modifyRoomSubscriptions(new Set([anotherRoomID]));
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
|
||||
slidingSync.stop();
|
||||
@@ -373,8 +373,8 @@ describe("SlidingSync", () => {
|
||||
is_dm: true,
|
||||
},
|
||||
};
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client, 1);
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client!, 1);
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("list", body);
|
||||
expect(body.lists).toBeTruthy();
|
||||
@@ -401,7 +401,7 @@ describe("SlidingSync", () => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
|
||||
expect(listenerData[roomA]).toEqual(rooms[roomA]);
|
||||
@@ -427,7 +427,7 @@ describe("SlidingSync", () => {
|
||||
|
||||
it("should be possible to adjust list ranges", async () => {
|
||||
// modify the list ranges
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("next ranges", body.lists[0].ranges);
|
||||
expect(body.lists).toBeTruthy();
|
||||
@@ -451,7 +451,7 @@ describe("SlidingSync", () => {
|
||||
return state === SlidingSyncState.RequestFinished;
|
||||
});
|
||||
slidingSync.setListRanges(0, newRanges);
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
});
|
||||
|
||||
@@ -464,7 +464,7 @@ describe("SlidingSync", () => {
|
||||
"is_dm": true,
|
||||
},
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("extra list", body);
|
||||
expect(body.lists).toBeTruthy();
|
||||
@@ -503,13 +503,13 @@ describe("SlidingSync", () => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
slidingSync.setList(1, extraListReq);
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
});
|
||||
|
||||
it("should be possible to get list DELETE/INSERTs", async () => {
|
||||
// move C (2) to A (0)
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "e",
|
||||
lists: [{
|
||||
count: 500,
|
||||
@@ -540,12 +540,12 @@ describe("SlidingSync", () => {
|
||||
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
|
||||
// move C (0) back to A (2)
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "f",
|
||||
lists: [{
|
||||
count: 500,
|
||||
@@ -576,13 +576,13 @@ describe("SlidingSync", () => {
|
||||
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
|
||||
it("should ignore invalid list indexes", async () => {
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "e",
|
||||
lists: [{
|
||||
count: 500,
|
||||
@@ -609,13 +609,13 @@ describe("SlidingSync", () => {
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
|
||||
it("should be possible to update a list", async () => {
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "g",
|
||||
lists: [{
|
||||
count: 42,
|
||||
@@ -655,7 +655,7 @@ describe("SlidingSync", () => {
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
@@ -667,7 +667,7 @@ describe("SlidingSync", () => {
|
||||
1: roomC,
|
||||
};
|
||||
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual(indexToRoomId);
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "f",
|
||||
// currently the list is [B,C] so we will insert D then immediately delete it
|
||||
lists: [{
|
||||
@@ -698,7 +698,7 @@ describe("SlidingSync", () => {
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
@@ -708,7 +708,7 @@ describe("SlidingSync", () => {
|
||||
0: roomB,
|
||||
1: roomC,
|
||||
});
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "g",
|
||||
lists: [{
|
||||
count: 499,
|
||||
@@ -734,7 +734,7 @@ describe("SlidingSync", () => {
|
||||
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
});
|
||||
@@ -743,7 +743,7 @@ describe("SlidingSync", () => {
|
||||
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
|
||||
0: roomC,
|
||||
});
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "h",
|
||||
lists: [{
|
||||
count: 500,
|
||||
@@ -770,11 +770,11 @@ describe("SlidingSync", () => {
|
||||
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
|
||||
httpBackend.when("POST", syncUrl).respond(200, {
|
||||
httpBackend!.when("POST", syncUrl).respond(200, {
|
||||
pos: "h",
|
||||
lists: [{
|
||||
count: 501,
|
||||
@@ -802,7 +802,7 @@ describe("SlidingSync", () => {
|
||||
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await responseProcessed;
|
||||
await listPromise;
|
||||
slidingSync.stop();
|
||||
@@ -825,11 +825,11 @@ describe("SlidingSync", () => {
|
||||
],
|
||||
};
|
||||
// add the subscription
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
|
||||
// modification before SlidingSync.start()
|
||||
const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId]));
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
@@ -852,7 +852,7 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await subscribePromise;
|
||||
});
|
||||
it("should resolve setList during a connection", async () => {
|
||||
@@ -861,7 +861,7 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
const promise = slidingSync.setList(0, newList);
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
@@ -876,14 +876,14 @@ describe("SlidingSync", () => {
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await promise;
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
it("should resolve setListRanges during a connection", async () => {
|
||||
const promise = slidingSync.setListRanges(0, [[20, 40]]);
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
@@ -900,7 +900,7 @@ describe("SlidingSync", () => {
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await promise;
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
@@ -909,7 +909,7 @@ describe("SlidingSync", () => {
|
||||
timeline_limit: 99,
|
||||
});
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
@@ -925,22 +925,22 @@ describe("SlidingSync", () => {
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await promise;
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
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.
|
||||
const gotTxnIds = [];
|
||||
const gotTxnIds: any[] = [];
|
||||
const pushTxn = function(req) {
|
||||
gotTxnIds.push(req.data.txn_id);
|
||||
};
|
||||
const failPromise = slidingSync.setListRanges(0, [[20, 40]]);
|
||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id
|
||||
await httpBackend.flushAllExpected();
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id
|
||||
await httpBackend!.flushAllExpected();
|
||||
const failPromise2 = slidingSync.setListRanges(0, [[60, 70]]);
|
||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
|
||||
await httpBackend.flushAllExpected();
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
|
||||
// which is a fail.
|
||||
@@ -949,7 +949,7 @@ describe("SlidingSync", () => {
|
||||
|
||||
const okPromise = slidingSync.setListRanges(0, [[0, 20]]);
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check((req) => {
|
||||
httpBackend!.when("POST", syncUrl).check((req) => {
|
||||
txnId = req.data.txn_id;
|
||||
}).respond(200, () => {
|
||||
// include the txn_id, earlier requests should now be reject()ed.
|
||||
@@ -958,23 +958,23 @@ describe("SlidingSync", () => {
|
||||
txn_id: txnId,
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await okPromise;
|
||||
|
||||
expect(txnId).toBeDefined();
|
||||
});
|
||||
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.
|
||||
const gotTxnIds = [];
|
||||
const gotTxnIds: any[] = [];
|
||||
const pushTxn = function(req) {
|
||||
gotTxnIds.push(req.data.txn_id);
|
||||
gotTxnIds.push(req.data?.txn_id);
|
||||
};
|
||||
const A = slidingSync.setListRanges(0, [[20, 40]]);
|
||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" });
|
||||
await httpBackend.flushAllExpected();
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" });
|
||||
await httpBackend!.flushAllExpected();
|
||||
const B = slidingSync.setListRanges(0, [[60, 70]]);
|
||||
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id
|
||||
await httpBackend.flushAllExpected();
|
||||
httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
|
||||
// which is a fail.
|
||||
@@ -985,14 +985,14 @@ describe("SlidingSync", () => {
|
||||
C.finally(() => {
|
||||
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
|
||||
return {
|
||||
pos: "C",
|
||||
txn_id: gotTxnIds[1],
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
// A is rejected, see above
|
||||
expect(B).resolves.toEqual(gotTxnIds[1]); // B is resolved
|
||||
expect(pendingC).toBe(true); // C is pending still
|
||||
@@ -1004,7 +1004,7 @@ describe("SlidingSync", () => {
|
||||
pending = false;
|
||||
});
|
||||
let txnId;
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.debug("got ", body);
|
||||
expect(body.room_subscriptions).toBeFalsy();
|
||||
@@ -1021,7 +1021,7 @@ describe("SlidingSync", () => {
|
||||
extensions: {},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
expect(txnId).toBeDefined();
|
||||
expect(pending).toBe(true);
|
||||
slidingSync.stop();
|
||||
@@ -1063,10 +1063,10 @@ describe("SlidingSync", () => {
|
||||
};
|
||||
|
||||
it("should be able to register an extension", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
slidingSync.registerExtension(extPre);
|
||||
|
||||
const callbackOrder = [];
|
||||
const callbackOrder: string[] = [];
|
||||
let extensionOnResponseCalled = false;
|
||||
onPreExtensionRequest = () => {
|
||||
return extReq;
|
||||
@@ -1077,7 +1077,7 @@ describe("SlidingSync", () => {
|
||||
expect(resp).toEqual(extResp);
|
||||
};
|
||||
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("ext req", body);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
@@ -1098,7 +1098,7 @@ describe("SlidingSync", () => {
|
||||
}
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
expect(extensionOnResponseCalled).toBe(true);
|
||||
expect(callbackOrder).toEqual(["onPreExtensionResponse", "Lifecycle"]);
|
||||
@@ -1112,7 +1112,7 @@ describe("SlidingSync", () => {
|
||||
onPreExtensionResponse = (resp) => {
|
||||
responseCalled = true;
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("ext req nothing", body);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
@@ -1130,7 +1130,7 @@ describe("SlidingSync", () => {
|
||||
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => {
|
||||
return state === SlidingSyncState.Complete;
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
expect(responseCalled).toBe(false);
|
||||
});
|
||||
@@ -1141,13 +1141,13 @@ describe("SlidingSync", () => {
|
||||
return extReq;
|
||||
};
|
||||
let responseCalled = false;
|
||||
const callbackOrder = [];
|
||||
const callbackOrder: string[] = [];
|
||||
onPostExtensionResponse = (resp) => {
|
||||
expect(resp).toEqual(extResp);
|
||||
responseCalled = true;
|
||||
callbackOrder.push("onPostExtensionResponse");
|
||||
};
|
||||
httpBackend.when("POST", syncUrl).check(function(req) {
|
||||
httpBackend!.when("POST", syncUrl).check(function(req) {
|
||||
const body = req.data;
|
||||
logger.log("ext req after start", body);
|
||||
expect(body.extensions).toBeTruthy();
|
||||
@@ -1171,7 +1171,7 @@ describe("SlidingSync", () => {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
await httpBackend!.flushAllExpected();
|
||||
await p;
|
||||
expect(responseCalled).toBe(true);
|
||||
expect(callbackOrder).toEqual(["Lifecycle", "onPostExtensionResponse"]);
|
||||
@@ -1179,7 +1179,7 @@ describe("SlidingSync", () => {
|
||||
});
|
||||
|
||||
it("is not possible to register the same extension name twice", async () => {
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1);
|
||||
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
|
||||
slidingSync.registerExtension(extPre);
|
||||
expect(() => { slidingSync.registerExtension(extPre); }).toThrow();
|
||||
});
|
||||
@@ -1206,7 +1206,7 @@ function listenUntil<T>(
|
||||
callback: (...args: any[]) => T,
|
||||
timeoutMs = 500,
|
||||
): 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) => {
|
||||
const wrapper = (...args) => {
|
||||
try {
|
||||
|
||||
@@ -20,6 +20,7 @@ import * as utils from "../src/utils";
|
||||
|
||||
// try to load the olm library.
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
global.Olm = require('@matrix-org/olm');
|
||||
logger.log('loaded libolm');
|
||||
} catch (e) {
|
||||
@@ -28,6 +29,7 @@ try {
|
||||
|
||||
// also try to set node crypto
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const crypto = require('crypto');
|
||||
utils.setCrypto(crypto);
|
||||
} 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);
|
||||
* ```
|
||||
*/
|
||||
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);
|
||||
|
||||
@@ -6,7 +6,7 @@ import '../olm-loader';
|
||||
|
||||
import { logger } from '../../src/logger';
|
||||
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 { eventMapperFor } from "../../src/event-mapper";
|
||||
|
||||
@@ -74,6 +74,7 @@ interface IEventOpts {
|
||||
sender?: string;
|
||||
skey?: string;
|
||||
content: IContent;
|
||||
prev_content?: IContent;
|
||||
user?: string;
|
||||
unsigned?: IUnsigned;
|
||||
redacts?: string;
|
||||
@@ -103,6 +104,7 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
|
||||
room_id: opts.room,
|
||||
sender: opts.sender || opts.user, // opts.user for backwards-compat
|
||||
content: opts.content,
|
||||
prev_content: opts.prev_content,
|
||||
unsigned: opts.unsigned || {},
|
||||
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
|
||||
txn_id: "~" + Math.random(),
|
||||
@@ -147,9 +149,9 @@ export function mkEventCustom<T>(base: T): T & GeneratedMetadata {
|
||||
interface IPresenceOpts {
|
||||
user?: string;
|
||||
sender?: string;
|
||||
url: string;
|
||||
name: string;
|
||||
ago: number;
|
||||
url?: string;
|
||||
name?: string;
|
||||
ago?: number;
|
||||
presence?: string;
|
||||
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 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");
|
||||
expect(ns.name).toBe(ns.stable);
|
||||
expect(ns.altName).toBe(ns.unstable);
|
||||
expect(ns.names).toEqual([ns.stable, ns.unstable]);
|
||||
});
|
||||
|
||||
it("should return unstable if there is no stable", () => {
|
||||
const ns = new NamespacedValue(null, "unstable");
|
||||
expect(ns.name).toBe(ns.unstable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
expect(ns.names).toEqual([ns.unstable]);
|
||||
});
|
||||
|
||||
it("should have a falsey unstable if needed", () => {
|
||||
const ns = new NamespacedValue("stable", null);
|
||||
expect(ns.name).toBe(ns.stable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
expect(ns.names).toEqual([ns.stable]);
|
||||
});
|
||||
|
||||
it("should match against either stable or unstable", () => {
|
||||
@@ -58,12 +61,14 @@ describe("UnstableValue", () => {
|
||||
const ns = new UnstableValue("stable", "unstable");
|
||||
expect(ns.name).toBe(ns.unstable);
|
||||
expect(ns.altName).toBe(ns.stable);
|
||||
expect(ns.names).toEqual([ns.unstable, ns.stable]);
|
||||
});
|
||||
|
||||
it("should return unstable if there is no stable", () => {
|
||||
const ns = new UnstableValue(null, "unstable");
|
||||
expect(ns.name).toBe(ns.unstable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
expect(ns.names).toEqual([ns.unstable]);
|
||||
});
|
||||
|
||||
it("should not permit falsey unstable values", () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
makeBeaconContent,
|
||||
makeBeaconInfoContent,
|
||||
makeTopicContent,
|
||||
parseBeaconContent,
|
||||
parseTopicContent,
|
||||
} 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', () => {
|
||||
|
||||
@@ -15,6 +15,9 @@ import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
import { logger } from '../../src/logger';
|
||||
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;
|
||||
|
||||
@@ -39,20 +42,52 @@ async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent>
|
||||
type: "m.forwarded_room_key",
|
||||
sender: client.getUserId(),
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: roomId,
|
||||
sender_key: eventContent.sender_key,
|
||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
||||
session_id: eventContent.session_id,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
forwarding_curve25519_key_chain:
|
||||
key.forwarding_curve_key_chain,
|
||||
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||
"room_id": roomId,
|
||||
"sender_key": eventContent.sender_key,
|
||||
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
|
||||
"session_id": eventContent.session_id,
|
||||
"session_key": key.key,
|
||||
"chain_index": key.chain_index,
|
||||
"forwarding_curve25519_key_chain": key.forwarding_curve_key_chain,
|
||||
"org.matrix.msc3061.shared_history": true,
|
||||
},
|
||||
});
|
||||
// make onRoomKeyEvent think this was an encrypted event
|
||||
// @ts-ignore private property
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -94,7 +129,7 @@ describe("Crypto", function() {
|
||||
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||
event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };};
|
||||
event.getForwardingCurve25519KeyChain = () => ["not empty"];
|
||||
event.isKeySourceUntrusted = () => false;
|
||||
event.isKeySourceUntrusted = () => true;
|
||||
event.getClaimedEd25519Key =
|
||||
() => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||
|
||||
@@ -158,8 +193,8 @@ describe("Crypto", function() {
|
||||
let fakeEmitter;
|
||||
|
||||
beforeEach(async function() {
|
||||
const mockStorage = new MockStorageApi();
|
||||
const clientStore = new MemoryStore({ localStorage: mockStorage });
|
||||
const mockStorage = new MockStorageApi() as unknown as Storage;
|
||||
const clientStore = new MemoryStore({ localStorage: mockStorage }) as unknown as IStore;
|
||||
const cryptoStore = new MemoryCryptoStore();
|
||||
|
||||
cryptoStore.storeEndToEndDeviceData({
|
||||
@@ -232,6 +267,7 @@ describe("Crypto", function() {
|
||||
describe('Key requests', function() {
|
||||
let aliceClient: MatrixClient;
|
||||
let bobClient: MatrixClient;
|
||||
let claraClient: MatrixClient;
|
||||
|
||||
beforeEach(async function() {
|
||||
aliceClient = (new TestClient(
|
||||
@@ -240,22 +276,35 @@ describe("Crypto", function() {
|
||||
bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
claraClient = (new TestClient(
|
||||
"@clara:example.com", "claradevice",
|
||||
)).client;
|
||||
await aliceClient.initCrypto();
|
||||
await bobClient.initCrypto();
|
||||
await claraClient.initCrypto();
|
||||
});
|
||||
|
||||
afterEach(async function() {
|
||||
aliceClient.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 = {
|
||||
"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", {});
|
||||
// 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);
|
||||
bobClient.store.storeRoom(bobRoom);
|
||||
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(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
@@ -313,6 +365,8 @@ describe("Crypto", function() {
|
||||
// the first message can't be decrypted yet, but the second one
|
||||
// can
|
||||
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
|
||||
bobClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await decryptEventsPromise;
|
||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||
@@ -339,8 +393,24 @@ describe("Crypto", function() {
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await decryptEventPromise;
|
||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
expect(events[0].isKeySourceUntrusted()).toBeTruthy();
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -382,6 +452,9 @@ describe("Crypto", function() {
|
||||
// decryption keys yet
|
||||
}
|
||||
|
||||
const device = new DeviceInfo(aliceClient.deviceId);
|
||||
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
@@ -461,6 +534,420 @@ describe("Crypto", function() {
|
||||
expect(aliceSendToDevice).toBeCalledTimes(3);
|
||||
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() {
|
||||
@@ -469,12 +956,12 @@ describe("Crypto", function() {
|
||||
jest.setTimeout(10000);
|
||||
const client = (new TestClient("@a:example.com", "dev")).client;
|
||||
await client.initCrypto();
|
||||
client.crypto.getSecretStorageKey = async () => null;
|
||||
client.crypto.getSecretStorageKey = jest.fn().mockResolvedValue(null);
|
||||
client.crypto.isCrossSigningReady = async () => false;
|
||||
client.crypto.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
|
||||
client.crypto.baseApis.setAccountData = () => null;
|
||||
client.crypto.baseApis.uploadKeySignatures = () => null;
|
||||
client.crypto.baseApis.http.authedRequest = () => null;
|
||||
client.crypto.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
|
||||
client.crypto.baseApis.uploadKeySignatures = jest.fn();
|
||||
client.crypto.baseApis.http.authedRequest = jest.fn();
|
||||
const createSecretStorageKey = async () => {
|
||||
return {
|
||||
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 { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning';
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||
|
||||
const ROOM_ID = '!ROOM:ID';
|
||||
|
||||
@@ -110,6 +110,12 @@ describe("MegolmDecryption", function() {
|
||||
senderCurve25519Key: "SENDER_CURVE25519",
|
||||
claimedEd25519Key: "SENDER_ED25519",
|
||||
};
|
||||
event.getWireType = () => "m.room.encrypted";
|
||||
event.getWireContent = () => {
|
||||
return {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
};
|
||||
};
|
||||
|
||||
const mockCrypto = {
|
||||
decryptEvent: function() {
|
||||
|
||||
@@ -34,7 +34,7 @@ import { IAbortablePromise, MatrixScheduler } from '../../../src';
|
||||
|
||||
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';
|
||||
|
||||
@@ -214,6 +214,12 @@ describe("MegolmBackup", function() {
|
||||
const event = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
});
|
||||
event.getWireType = () => "m.room.encrypted";
|
||||
event.getWireContent = () => {
|
||||
return {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
};
|
||||
};
|
||||
const decryptedData = {
|
||||
clearEvent: {
|
||||
type: 'm.room_key',
|
||||
|
||||
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
IndexedDBCryptoStore,
|
||||
} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import { CryptoStore } from '../../../src/crypto/store/base';
|
||||
import { IndexedDBCryptoStore } 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 { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||
|
||||
@@ -26,36 +26,39 @@ import 'jest-localstorage-mock';
|
||||
const requests = [
|
||||
{
|
||||
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,
|
||||
recipients: [
|
||||
{ userId: "@alice:example.com", deviceId: "*" },
|
||||
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
|
||||
],
|
||||
},
|
||||
{
|
||||
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,
|
||||
recipients: [
|
||||
{ userId: "@alice:example.com", deviceId: "*" },
|
||||
{ userId: "@carrie:example.com", deviceId: "barbazquux" },
|
||||
],
|
||||
},
|
||||
{
|
||||
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,
|
||||
recipients: [
|
||||
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore",
|
||||
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore",
|
||||
() => new IndexedDBCryptoStore(undefined, "tests")],
|
||||
["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;
|
||||
}],
|
||||
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
|
||||
["MemoryCryptoStore", () => new MemoryCryptoStore()],
|
||||
])("Outgoing room key requests [%s]", function(name, dbFactory) {
|
||||
let store;
|
||||
let store: CryptoStore;
|
||||
|
||||
beforeAll(async () => {
|
||||
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",
|
||||
async () => {
|
||||
const r =
|
||||
|
||||
@@ -26,6 +26,7 @@ import { logger } from '../../../src/logger';
|
||||
import * as utils from "../../../src/utils";
|
||||
import { ICreateClientOpts } from '../../../src/client';
|
||||
import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
|
||||
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
@@ -250,20 +251,20 @@ describe("Secrets", function() {
|
||||
|
||||
osborne2.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
"VAX": {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "VAX",
|
||||
known: false,
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:VAX": vaxDevice.deviceEd25519Key,
|
||||
"curve25519:VAX": vaxDevice.deviceCurve25519Key,
|
||||
},
|
||||
verified: DeviceInfo.DeviceVerification.VERIFIED,
|
||||
},
|
||||
});
|
||||
vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
"Osborne2": {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "Osborne2",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
verified: 0,
|
||||
known: false,
|
||||
keys: {
|
||||
"ed25519:Osborne2": osborne2Device.deviceEd25519Key,
|
||||
"curve25519:Osborne2": osborne2Device.deviceCurve25519Key,
|
||||
@@ -280,10 +281,12 @@ describe("Secrets", function() {
|
||||
Object.values(otks)[0],
|
||||
);
|
||||
|
||||
const request = await secretStorage.request("foo", ["VAX"]);
|
||||
const secret = await request.promise;
|
||||
osborne2.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
|
||||
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();
|
||||
vax.stop();
|
||||
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
|
||||
limitations under the License.
|
||||
*/
|
||||
import { MatrixClient } from "../../../../src/client";
|
||||
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
|
||||
describe("InRoomChannel tests", function() {
|
||||
const ALICE = "@alice:hs.tld";
|
||||
@@ -23,7 +23,7 @@ describe("InRoomChannel tests", function() {
|
||||
const MALORY = "@malory:hs.tld";
|
||||
const client = {
|
||||
getUserId() { return ALICE; },
|
||||
};
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
it("getEventType only returns .request for a message with a msgtype", function() {
|
||||
const invalidEvent = new MatrixEvent({
|
||||
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import "../../../olm-loader";
|
||||
import { verificationMethods } from "../../../../src/crypto";
|
||||
import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { SAS } from "../../../../src/crypto/verification/SAS";
|
||||
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() {
|
||||
return {
|
||||
Dynabook: {
|
||||
algorithms: [],
|
||||
verified: 0,
|
||||
known: false,
|
||||
keys: {
|
||||
"ed25519:Dynabook": "bob+base64+ed25519+key",
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.client.on("crypto.verification.request", (request) => {
|
||||
alice.client.downloadKeys = jest.fn().mockResolvedValue({});
|
||||
bob.client.downloadKeys = jest.fn().mockResolvedValue({});
|
||||
bob.client.on(CryptoEvent.VerificationRequest, (request) => {
|
||||
const bobVerifier = request.beginKeyVerification(verificationMethods.SAS);
|
||||
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();
|
||||
});
|
||||
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;
|
||||
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();
|
||||
|
||||
alice.stop();
|
||||
@@ -19,10 +19,14 @@ import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { SAS } from "../../../../src/crypto/verification/SAS";
|
||||
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 { logger } from "../../../../src/logger";
|
||||
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;
|
||||
|
||||
@@ -48,13 +52,15 @@ describe("SAS verification", function() {
|
||||
//channel, baseApis, userId, deviceId, startEvent, request
|
||||
const request = {
|
||||
onVerifierCancelled: function() {},
|
||||
};
|
||||
} as VerificationRequest;
|
||||
const channel = {
|
||||
send: function() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
const sas = new SAS(channel, {}, "@alice:example.com", "ABCDEFG", null, request);
|
||||
} as unknown as IVerificationChannel;
|
||||
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({
|
||||
sender: "@alice:example.com",
|
||||
type: "es.inquisition",
|
||||
@@ -65,7 +71,7 @@ describe("SAS verification", function() {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
// Cancel the SAS for cleanup (we started a verification, so abort)
|
||||
sas.cancel();
|
||||
sas.cancel(new Error('error'));
|
||||
});
|
||||
|
||||
describe("verification", () => {
|
||||
@@ -403,16 +409,12 @@ describe("SAS verification", function() {
|
||||
},
|
||||
);
|
||||
alice.client.setDeviceVerified = jest.fn();
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
alice.client.downloadKeys = jest.fn().mockResolvedValue({});
|
||||
bob.client.setDeviceVerified = jest.fn();
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.client.downloadKeys = jest.fn().mockResolvedValue({});
|
||||
|
||||
const bobPromise = new Promise((resolve, reject) => {
|
||||
bob.client.on("crypto.verification.request", request => {
|
||||
const bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
|
||||
bob.client.on(CryptoEvent.VerificationRequest, request => {
|
||||
request.verifier.on("show_sas", (e) => {
|
||||
e.mismatch();
|
||||
});
|
||||
@@ -421,7 +423,7 @@ describe("SAS verification", function() {
|
||||
});
|
||||
|
||||
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();
|
||||
@@ -462,7 +464,7 @@ describe("SAS verification", function() {
|
||||
},
|
||||
);
|
||||
|
||||
alice.client.setDeviceVerified = jest.fn();
|
||||
alice.client.crypto.setDeviceVerification = jest.fn();
|
||||
alice.client.getDeviceEd25519Key = () => {
|
||||
return "alice+base64+ed25519+key";
|
||||
};
|
||||
@@ -480,7 +482,7 @@ describe("SAS verification", function() {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
bob.client.setDeviceVerified = jest.fn();
|
||||
bob.client.crypto.setDeviceVerification = jest.fn();
|
||||
bob.client.getStoredDevice = () => {
|
||||
return DeviceInfo.fromStorage(
|
||||
{
|
||||
@@ -501,7 +503,7 @@ describe("SAS verification", function() {
|
||||
aliceSasEvent = null;
|
||||
bobSasEvent = null;
|
||||
|
||||
bobPromise = new Promise((resolve, reject) => {
|
||||
bobPromise = new Promise<void>((resolve, reject) => {
|
||||
bob.client.on("crypto.verification.request", async (request) => {
|
||||
const verifier = request.beginKeyVerification(SAS.NAME);
|
||||
verifier.on("show_sas", (e) => {
|
||||
@@ -563,10 +565,24 @@ describe("SAS verification", function() {
|
||||
]);
|
||||
|
||||
// make sure Alice and Bob verified each other
|
||||
expect(alice.client.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(bob.client.getUserId(), bob.client.deviceId);
|
||||
expect(bob.client.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(alice.client.getUserId(), alice.client.deviceId);
|
||||
expect(alice.client.crypto.setDeviceVerification)
|
||||
.toHaveBeenCalledWith(
|
||||
bob.client.getUserId(),
|
||||
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 { setupWebcrypto, teardownWebcrypto } from './util';
|
||||
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();
|
||||
|
||||
@@ -54,9 +57,21 @@ describe("self-verifications", () => {
|
||||
cacheCallbacks,
|
||||
);
|
||||
crossSigningInfo.keys = {
|
||||
master: { keys: { X: testKeyPub } },
|
||||
self_signing: { keys: { X: testKeyPub } },
|
||||
user_signing: { keys: { X: testKeyPub } },
|
||||
master: {
|
||||
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 = {
|
||||
@@ -79,20 +94,22 @@ describe("self-verifications", () => {
|
||||
getUserId: () => userId,
|
||||
getKeyBackupVersion: () => Promise.resolve({}),
|
||||
restoreKeyBackupWithCache,
|
||||
};
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
const request = {
|
||||
onVerifierFinished: () => undefined,
|
||||
};
|
||||
} as unknown as VerificationRequest;
|
||||
|
||||
const verification = new VerificationBase(
|
||||
undefined, // channel
|
||||
undefined as unknown as IVerificationChannel, // channel
|
||||
client, // baseApis
|
||||
userId,
|
||||
"ABC", // deviceId
|
||||
undefined, // startEvent
|
||||
undefined as unknown as MatrixEvent, // startEvent
|
||||
request,
|
||||
);
|
||||
|
||||
// @ts-ignore set private property
|
||||
verification.resolve = () => undefined;
|
||||
|
||||
const result = await verification.done();
|
||||
@@ -102,12 +119,12 @@ describe("self-verifications", () => {
|
||||
expect(secretStorage.request.mock.calls.length).toBe(4);
|
||||
|
||||
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1])
|
||||
.toEqual(testKey);
|
||||
.toEqual(testKey);
|
||||
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[1][1])
|
||||
.toEqual(testKey);
|
||||
.toEqual(testKey);
|
||||
|
||||
expect(storeSessionBackupPrivateKey.mock.calls[0][0])
|
||||
.toEqual(testKey);
|
||||
.toEqual(testKey);
|
||||
|
||||
expect(restoreKeyBackupWithCache).toHaveBeenCalled();
|
||||
|
||||
@@ -19,20 +19,23 @@ import nodeCrypto from "crypto";
|
||||
|
||||
import { TestClient } from '../../../TestClient';
|
||||
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 { MatrixClient, ClientEvent } from '../../../../src/client';
|
||||
|
||||
export async function makeTestClients(userInfos, options) {
|
||||
const clients = [];
|
||||
const timeouts = [];
|
||||
const clientMap = {};
|
||||
const sendToDevice = function(type, map) {
|
||||
export async function makeTestClients(userInfos, options): Promise<[TestClient[], () => void]> {
|
||||
const clients: TestClient[] = [];
|
||||
const timeouts: ReturnType<typeof setTimeout>[] = [];
|
||||
const clientMap: Record<string, Record<string, MatrixClient>> = {};
|
||||
const makeSendToDevice = (matrixClient: MatrixClient): MatrixClient['sendToDevice'] => async (type, map) => {
|
||||
// logger.log(this.getUserId(), "sends", type, map);
|
||||
for (const [userId, devMap] of Object.entries(map)) {
|
||||
if (userId in clientMap) {
|
||||
for (const [deviceId, msg] of Object.entries(devMap)) {
|
||||
if (deviceId in clientMap[userId]) {
|
||||
const event = new MatrixEvent({
|
||||
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
|
||||
sender: matrixClient.getUserId()!,
|
||||
type: type,
|
||||
content: msg,
|
||||
});
|
||||
@@ -42,18 +45,19 @@ export async function makeTestClients(userInfos, options) {
|
||||
Promise.resolve();
|
||||
|
||||
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
|
||||
const eventId = "$" + this.makeTxnId(); // eslint-disable-line @babel/no-invalid-this
|
||||
const eventId = "$" + matrixClient.makeTxnId();
|
||||
const rawEvent = {
|
||||
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
|
||||
sender: matrixClient.getUserId()!,
|
||||
type: type,
|
||||
content: content,
|
||||
room_id: room,
|
||||
@@ -63,22 +67,24 @@ export async function makeTestClients(userInfos, options) {
|
||||
const event = new MatrixEvent(rawEvent);
|
||||
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
|
||||
unsigned: {
|
||||
transaction_id: this.makeTxnId(), // eslint-disable-line @babel/no-invalid-this
|
||||
transaction_id: matrixClient.makeTxnId(),
|
||||
},
|
||||
}));
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
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!!");
|
||||
tc.client.emit("Room.timeline", remoteEcho);
|
||||
tc.client.emit(RoomEvent.Timeline, remoteEcho, room, false, false, roomTimelineData);
|
||||
} 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 });
|
||||
};
|
||||
@@ -99,8 +105,8 @@ export async function makeTestClients(userInfos, options) {
|
||||
clientMap[userInfo.userId] = {};
|
||||
}
|
||||
clientMap[userInfo.userId][userInfo.deviceId] = testClient.client;
|
||||
testClient.client.sendToDevice = sendToDevice;
|
||||
testClient.client.sendEvent = sendEvent;
|
||||
testClient.client.sendToDevice = makeSendToDevice(testClient.client);
|
||||
testClient.client.sendEvent = makeSendEvent(testClient.client);
|
||||
clients.push(testClient);
|
||||
}
|
||||
|
||||
@@ -116,11 +122,12 @@ export async function makeTestClients(userInfos, options) {
|
||||
export function setupWebcrypto() {
|
||||
global.crypto = {
|
||||
getRandomValues: (buf) => {
|
||||
return nodeCrypto.randomFillSync(buf);
|
||||
return nodeCrypto.randomFillSync(buf as any);
|
||||
},
|
||||
};
|
||||
} as unknown as Crypto;
|
||||
}
|
||||
|
||||
export function teardownWebcrypto() {
|
||||
// @ts-ignore undefined != Crypto
|
||||
global.crypto = undefined;
|
||||
}
|
||||
@@ -19,11 +19,18 @@ import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoo
|
||||
import { ToDeviceChannel } from
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { MatrixClient } from "../../../../src/client";
|
||||
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 events = [];
|
||||
let events: MatrixEvent[] = [];
|
||||
const deviceEvents = {};
|
||||
return {
|
||||
getUserId() { return userId; },
|
||||
@@ -54,16 +61,18 @@ function makeMockClient(userId, deviceId) {
|
||||
deviceEvents[userId][deviceId].push(event);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
return Promise.resolve({});
|
||||
},
|
||||
|
||||
popEvents() {
|
||||
// @ts-ignore special testing fn
|
||||
popEvents(): MatrixEvent[] {
|
||||
const e = events;
|
||||
events = [];
|
||||
return e;
|
||||
},
|
||||
|
||||
popDeviceEvents(userId, deviceId) {
|
||||
// @ts-ignore special testing fn
|
||||
popDeviceEvents(userId: string, deviceId: string): MatrixEvent[] {
|
||||
const forDevice = deviceEvents[userId];
|
||||
const events = forDevice && forDevice[deviceId];
|
||||
const result = events || [];
|
||||
@@ -72,12 +81,21 @@ function makeMockClient(userId, deviceId) {
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
} as unknown as MockClient;
|
||||
}
|
||||
|
||||
const MOCK_METHOD = "mock-verify";
|
||||
class MockVerifier {
|
||||
constructor(channel, client, userId, deviceId, startEvent) {
|
||||
class MockVerifier extends VerificationBase<'', any> {
|
||||
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._startEvent = startEvent;
|
||||
}
|
||||
@@ -115,7 +133,10 @@ function makeRemoteEcho(event) {
|
||||
|
||||
async function distributeEvent(ownRequest, theirRequest, event) {
|
||||
await ownRequest.channel.handleEvent(
|
||||
makeRemoteEcho(event), ownRequest, true);
|
||||
makeRemoteEcho(event),
|
||||
ownRequest,
|
||||
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() {
|
||||
const alice = makeMockClient("@alice: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(
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()),
|
||||
new Map([[MOCK_METHOD, MockVerifier]]), alice);
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()!),
|
||||
verificationMethods,
|
||||
alice,
|
||||
);
|
||||
const bobRequest = new VerificationRequest(
|
||||
new InRoomChannel(bob, "!room"),
|
||||
new Map([[MOCK_METHOD, MockVerifier]]), bob);
|
||||
verificationMethods,
|
||||
bob,
|
||||
);
|
||||
expect(aliceRequest.invalid).toBe(true);
|
||||
expect(bobRequest.invalid).toBe(true);
|
||||
|
||||
@@ -157,7 +185,7 @@ describe("verification request unit tests", function() {
|
||||
expect(aliceRequest.ready).toBe(true);
|
||||
|
||||
const verifier = aliceRequest.beginKeyVerification(MOCK_METHOD);
|
||||
await verifier.start();
|
||||
await (verifier as MockVerifier).start();
|
||||
const [startEvent] = alice.popEvents();
|
||||
expect(startEvent.getType()).toBe(START_TYPE);
|
||||
await distributeEvent(aliceRequest, bobRequest, startEvent);
|
||||
@@ -165,8 +193,7 @@ describe("verification request unit tests", function() {
|
||||
expect(aliceRequest.verifier).toBeInstanceOf(MockVerifier);
|
||||
expect(bobRequest.started).toBe(true);
|
||||
expect(bobRequest.verifier).toBeInstanceOf(MockVerifier);
|
||||
|
||||
await bobRequest.verifier.start();
|
||||
await (bobRequest.verifier as MockVerifier).start();
|
||||
const [bobDoneEvent] = bob.popEvents();
|
||||
expect(bobDoneEvent.getType()).toBe(DONE_TYPE);
|
||||
await distributeEvent(bobRequest, aliceRequest, bobDoneEvent);
|
||||
@@ -180,12 +207,20 @@ describe("verification request unit tests", function() {
|
||||
it("methods only contains common methods", async function() {
|
||||
const alice = makeMockClient("@alice: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(
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()),
|
||||
new Map([["c", function() {}], ["a", function() {}]]), alice);
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()!),
|
||||
aliceVerificationMethods, alice);
|
||||
const bobRequest = new VerificationRequest(
|
||||
new InRoomChannel(bob, "!room"),
|
||||
new Map([["c", function() {}], ["b", function() {}]]), bob);
|
||||
bobVerificationMethods,
|
||||
bob,
|
||||
);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
await distributeEvent(aliceRequest, bobRequest, requestEvent);
|
||||
@@ -201,13 +236,22 @@ describe("verification request unit tests", function() {
|
||||
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
|
||||
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();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
const bob1Request = new VerificationRequest(
|
||||
new InRoomChannel(bob1, "!room"), new Map(), bob1);
|
||||
new InRoomChannel(bob1, "!room"),
|
||||
new Map(),
|
||||
bob1,
|
||||
);
|
||||
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 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() {
|
||||
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
|
||||
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(
|
||||
new ToDeviceChannel(bob1, bob1.getUserId(), ["device1", "device2"],
|
||||
ToDeviceChannel.makeTransactionId(), "device2"),
|
||||
new Map([[MOCK_METHOD, MockVerifier]]), bob1);
|
||||
new ToDeviceChannel(
|
||||
bob1,
|
||||
bob1.getUserId()!,
|
||||
["device1", "device2"],
|
||||
ToDeviceChannel.makeTransactionId(),
|
||||
"device2",
|
||||
),
|
||||
verificationMethods,
|
||||
bob1,
|
||||
);
|
||||
const to = { userId: "@bob:matrix.tld", deviceId: "device2" };
|
||||
const verifier = bob1Request.beginKeyVerification(MOCK_METHOD, to);
|
||||
expect(verifier).toBeInstanceOf(MockVerifier);
|
||||
await verifier.start();
|
||||
await (verifier as MockVerifier).start();
|
||||
const [startEvent] = bob1.popDeviceEvents(to.userId, to.deviceId);
|
||||
expect(startEvent.getType()).toBe(START_TYPE);
|
||||
const bob2Request = new VerificationRequest(
|
||||
new ToDeviceChannel(bob2, bob2.getUserId(), ["device1"]),
|
||||
new Map([[MOCK_METHOD, MockVerifier]]), bob2);
|
||||
new ToDeviceChannel(bob2, bob2.getUserId()!, ["device1"]),
|
||||
verificationMethods,
|
||||
bob2,
|
||||
);
|
||||
|
||||
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");
|
||||
expect(doneEvent1.getType()).toBe(DONE_TYPE);
|
||||
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 bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
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();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true,
|
||||
true, true);
|
||||
await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true);
|
||||
|
||||
expect(aliceRequest.cancelled).toBe(false);
|
||||
expect(aliceRequest._cancellingUserId).toBe(undefined);
|
||||
@@ -269,11 +327,17 @@ describe("verification request unit tests", function() {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
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();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
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);
|
||||
|
||||
@@ -16,14 +16,15 @@ limitations under the License.
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import {
|
||||
DuplicateStrategy,
|
||||
EventTimeline,
|
||||
EventTimelineSet,
|
||||
EventType,
|
||||
Filter,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
Room,
|
||||
DuplicateStrategy,
|
||||
} from '../../src';
|
||||
import { Thread } from "../../src/models/thread";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
@@ -291,4 +292,34 @@ describe('EventTimelineSet', () => {
|
||||
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";
|
||||
|
||||
describe("Filter", function() {
|
||||
@@ -43,4 +44,17 @@ describe("Filter", function() {
|
||||
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';
|
||||
|
||||
describe('Login request', function() {
|
||||
@@ -22,3 +23,37 @@ describe('Login request', function() {
|
||||
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 { makeBeaconInfoContent } from "../../src/content-helpers";
|
||||
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 { makeBeaconEvent } from "../test-utils/beacon";
|
||||
import {
|
||||
IGNORE_INVITES_ACCOUNT_EVENT_KEY,
|
||||
POLICIES_ACCOUNT_EVENT_TYPE,
|
||||
PolicyScope,
|
||||
} from "../../src/models/invites-ignorer";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -427,7 +432,7 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
});
|
||||
});
|
||||
await client.startClient();
|
||||
await client.startClient({ filter });
|
||||
await syncPromise;
|
||||
});
|
||||
|
||||
@@ -1412,4 +1417,301 @@ describe("MatrixClient", function() {
|
||||
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[tree.roomId] = parentRoom;
|
||||
(<any>tree).room = parentRoom; // override readonly
|
||||
client.getRoom = (r) => rooms[r];
|
||||
client.getRoom = (r) => rooms[r ?? ""];
|
||||
|
||||
clientSendStateFn = jest.fn()
|
||||
.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.
|
||||
*/
|
||||
|
||||
import { REFERENCE_RELATION } from "matrix-events-sdk";
|
||||
|
||||
import { MatrixEvent } from "../../../src";
|
||||
import { M_BEACON_INFO } from "../../../src/@types/beacon";
|
||||
import {
|
||||
@@ -431,6 +433,27 @@ describe('Beacon', () => {
|
||||
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', () => {
|
||||
it('ignores locations before the beacon start timestamp', () => {
|
||||
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 { PushProcessor } from "../../src/pushprocessor";
|
||||
import { EventType, MatrixClient, MatrixEvent } from "../../src";
|
||||
import { IActionsObject, PushProcessor } from "../../src/pushprocessor";
|
||||
import { EventType, IContent, MatrixClient, MatrixEvent } from "../../src";
|
||||
|
||||
describe('NotificationService', function() {
|
||||
const testUserId = "@ali:matrix.org";
|
||||
@@ -336,4 +336,102 @@ describe('NotificationService', function() {
|
||||
enabled: 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 { RoomMember } from "../../src/models/room-member";
|
||||
import { RoomMember, RoomMemberEvent } from "../../src/models/room-member";
|
||||
import { RoomState } from "../../src";
|
||||
|
||||
describe("RoomMember", function() {
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
const userB = "@bertha:bar";
|
||||
const userC = "@clarissa:bar";
|
||||
let member;
|
||||
let member = new RoomMember(roomId, userA);
|
||||
|
||||
beforeEach(function() {
|
||||
member = new RoomMember(roomId, userA);
|
||||
@@ -27,17 +44,17 @@ describe("RoomMember", function() {
|
||||
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
|
||||
// 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",
|
||||
function() {
|
||||
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false);
|
||||
expect(url).toEqual(null);
|
||||
});
|
||||
function() {
|
||||
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false, false);
|
||||
expect(url).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setPowerLevelEvent", function() {
|
||||
@@ -66,92 +83,92 @@ describe("RoomMember", function() {
|
||||
});
|
||||
|
||||
it("should emit 'RoomMember.powerLevel' if the power level changes.",
|
||||
function() {
|
||||
const event = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@bertha:bar": 200,
|
||||
"@invalid:user": 10, // shouldn't barf on this.
|
||||
function() {
|
||||
const event = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@bertha:bar": 200,
|
||||
"@invalid:user": 10, // shouldn't barf on this.
|
||||
},
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
let emitCount = 0;
|
||||
event: true,
|
||||
});
|
||||
let emitCount = 0;
|
||||
|
||||
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
expect(emitMember).toEqual(member);
|
||||
expect(emitEvent).toEqual(event);
|
||||
});
|
||||
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
expect(emitMember).toEqual(member);
|
||||
expect(emitEvent).toEqual(event);
|
||||
});
|
||||
|
||||
member.setPowerLevelEvent(event);
|
||||
expect(emitCount).toEqual(1);
|
||||
member.setPowerLevelEvent(event); // no-op
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
member.setPowerLevelEvent(event);
|
||||
expect(emitCount).toEqual(1);
|
||||
member.setPowerLevelEvent(event); // no-op
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
|
||||
it("should honour power levels of zero.",
|
||||
function() {
|
||||
const event = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": 0,
|
||||
function() {
|
||||
const event = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
let emitCount = 0;
|
||||
event: true,
|
||||
});
|
||||
let emitCount = 0;
|
||||
|
||||
// set the power level to something other than zero or we
|
||||
// won't get an event
|
||||
member.powerLevel = 1;
|
||||
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
expect(emitMember.userId).toEqual('@alice:bar');
|
||||
expect(emitMember.powerLevel).toEqual(0);
|
||||
expect(emitEvent).toEqual(event);
|
||||
});
|
||||
// set the power level to something other than zero or we
|
||||
// won't get an event
|
||||
member.powerLevel = 1;
|
||||
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
expect(emitMember.userId).toEqual('@alice:bar');
|
||||
expect(emitMember.powerLevel).toEqual(0);
|
||||
expect(emitEvent).toEqual(event);
|
||||
});
|
||||
|
||||
member.setPowerLevelEvent(event);
|
||||
expect(member.powerLevel).toEqual(0);
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
member.setPowerLevelEvent(event);
|
||||
expect(member.powerLevel).toEqual(0);
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
|
||||
it("should not honor string power levels.",
|
||||
function() {
|
||||
const event = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": "5",
|
||||
function() {
|
||||
const event = utils.mkEvent({
|
||||
type: "m.room.power_levels",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
users_default: 20,
|
||||
users: {
|
||||
"@alice:bar": "5",
|
||||
},
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
let emitCount = 0;
|
||||
event: true,
|
||||
});
|
||||
let emitCount = 0;
|
||||
|
||||
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
expect(emitMember.userId).toEqual('@alice:bar');
|
||||
expect(emitMember.powerLevel).toEqual(20);
|
||||
expect(emitEvent).toEqual(event);
|
||||
});
|
||||
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
expect(emitMember.userId).toEqual('@alice:bar');
|
||||
expect(emitMember.powerLevel).toEqual(20);
|
||||
expect(emitEvent).toEqual(event);
|
||||
});
|
||||
|
||||
member.setPowerLevelEvent(event);
|
||||
expect(member.powerLevel).toEqual(20);
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
member.setPowerLevelEvent(event);
|
||||
expect(member.powerLevel).toEqual(20);
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setTypingEvent", function() {
|
||||
@@ -183,34 +200,34 @@ describe("RoomMember", function() {
|
||||
});
|
||||
|
||||
it("should emit 'RoomMember.typing' if the typing state changes",
|
||||
function() {
|
||||
const event = utils.mkEvent({
|
||||
type: "m.typing",
|
||||
room: roomId,
|
||||
content: {
|
||||
user_ids: [
|
||||
userA, userC,
|
||||
],
|
||||
},
|
||||
event: true,
|
||||
function() {
|
||||
const event = utils.mkEvent({
|
||||
type: "m.typing",
|
||||
room: roomId,
|
||||
content: {
|
||||
user_ids: [
|
||||
userA, userC,
|
||||
],
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
let emitCount = 0;
|
||||
member.on(RoomMemberEvent.Typing, function(ev, mem) {
|
||||
expect(mem).toEqual(member);
|
||||
expect(ev).toEqual(event);
|
||||
emitCount += 1;
|
||||
});
|
||||
member.typing = false;
|
||||
member.setTypingEvent(event);
|
||||
expect(emitCount).toEqual(1);
|
||||
member.setTypingEvent(event); // no-op
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
let emitCount = 0;
|
||||
member.on("RoomMember.typing", function(ev, mem) {
|
||||
expect(mem).toEqual(member);
|
||||
expect(ev).toEqual(event);
|
||||
emitCount += 1;
|
||||
});
|
||||
member.typing = false;
|
||||
member.setTypingEvent(event);
|
||||
expect(emitCount).toEqual(1);
|
||||
member.setTypingEvent(event); // no-op
|
||||
expect(emitCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isOutOfBand", function() {
|
||||
it("should be set by markOutOfBand", function() {
|
||||
const member = new RoomMember();
|
||||
const member = new RoomMember(roomId, userA);
|
||||
expect(member.isOutOfBand()).toEqual(false);
|
||||
member.markOutOfBand();
|
||||
expect(member.isOutOfBand()).toEqual(true);
|
||||
@@ -235,50 +252,50 @@ describe("RoomMember", function() {
|
||||
});
|
||||
|
||||
it("should set 'membership' and assign the event to 'events.member'.",
|
||||
function() {
|
||||
member.setMembershipEvent(inviteEvent);
|
||||
expect(member.membership).toEqual("invite");
|
||||
expect(member.events.member).toEqual(inviteEvent);
|
||||
member.setMembershipEvent(joinEvent);
|
||||
expect(member.membership).toEqual("join");
|
||||
expect(member.events.member).toEqual(joinEvent);
|
||||
});
|
||||
function() {
|
||||
member.setMembershipEvent(inviteEvent);
|
||||
expect(member.membership).toEqual("invite");
|
||||
expect(member.events.member).toEqual(inviteEvent);
|
||||
member.setMembershipEvent(joinEvent);
|
||||
expect(member.membership).toEqual("join");
|
||||
expect(member.events.member).toEqual(joinEvent);
|
||||
});
|
||||
|
||||
it("should set 'name' based on user_id, displayname and room state",
|
||||
function() {
|
||||
const roomState = {
|
||||
getStateEvents: function(type) {
|
||||
if (type !== "m.room.member") {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
utils.mkMembership({
|
||||
event: true, mship: "join", room: roomId,
|
||||
user: userB,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
event: true, mship: "join", room: roomId,
|
||||
user: userC, name: "Alice",
|
||||
}),
|
||||
joinEvent,
|
||||
];
|
||||
},
|
||||
getUserIdsWithDisplayName: function(displayName) {
|
||||
return [userA, userC];
|
||||
},
|
||||
};
|
||||
expect(member.name).toEqual(userA); // default = user_id
|
||||
member.setMembershipEvent(joinEvent);
|
||||
expect(member.name).toEqual("Alice"); // prefer displayname
|
||||
member.setMembershipEvent(joinEvent, roomState);
|
||||
expect(member.name).not.toEqual("Alice"); // it should disambig.
|
||||
// user_id should be there somewhere
|
||||
expect(member.name.indexOf(userA)).not.toEqual(-1);
|
||||
});
|
||||
function() {
|
||||
const roomState = {
|
||||
getStateEvents: function(type) {
|
||||
if (type !== "m.room.member") {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
utils.mkMembership({
|
||||
event: true, mship: "join", room: roomId,
|
||||
user: userB,
|
||||
}),
|
||||
utils.mkMembership({
|
||||
event: true, mship: "join", room: roomId,
|
||||
user: userC, name: "Alice",
|
||||
}),
|
||||
joinEvent,
|
||||
];
|
||||
},
|
||||
getUserIdsWithDisplayName: function(displayName) {
|
||||
return [userA, userC];
|
||||
},
|
||||
} as unknown as RoomState;
|
||||
expect(member.name).toEqual(userA); // default = user_id
|
||||
member.setMembershipEvent(joinEvent);
|
||||
expect(member.name).toEqual("Alice"); // prefer displayname
|
||||
member.setMembershipEvent(joinEvent, roomState);
|
||||
expect(member.name).not.toEqual("Alice"); // it should disambig.
|
||||
// user_id should be there somewhere
|
||||
expect(member.name.indexOf(userA)).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should emit 'RoomMember.membership' if the membership changes", function() {
|
||||
let emitCount = 0;
|
||||
member.on("RoomMember.membership", function(ev, mem) {
|
||||
member.on(RoomMemberEvent.Membership, function(ev, mem) {
|
||||
emitCount += 1;
|
||||
expect(mem).toEqual(member);
|
||||
expect(ev).toEqual(inviteEvent);
|
||||
@@ -291,7 +308,7 @@ describe("RoomMember", function() {
|
||||
|
||||
it("should emit 'RoomMember.name' if the name changes", function() {
|
||||
let emitCount = 0;
|
||||
member.on("RoomMember.name", function(ev, mem) {
|
||||
member.on(RoomMemberEvent.Name, function(ev, mem) {
|
||||
emitCount += 1;
|
||||
expect(mem).toEqual(member);
|
||||
expect(ev).toEqual(joinEvent);
|
||||
@@ -341,7 +358,7 @@ describe("RoomMember", function() {
|
||||
getUserIdsWithDisplayName: function(displayName) {
|
||||
return [userA, userC];
|
||||
},
|
||||
};
|
||||
} as unknown as RoomState;
|
||||
expect(member.name).toEqual(userA); // default = user_id
|
||||
member.setMembershipEvent(joinEvent, roomState);
|
||||
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 { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
|
||||
import { filterEmitCallsByEventType } from "../test-utils/emitter";
|
||||
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 {
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
} from "../../src/models/event";
|
||||
import { M_BEACON } from "../../src/@types/beacon";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
|
||||
describe("RoomState", function() {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -17,7 +40,7 @@ describe("RoomState", function() {
|
||||
const userC = "@cleo:bar";
|
||||
const userLazy = "@lazy:bar";
|
||||
|
||||
let state;
|
||||
let state = new RoomState(roomId);
|
||||
|
||||
beforeEach(function() {
|
||||
state = new RoomState(roomId);
|
||||
@@ -67,8 +90,8 @@ describe("RoomState", function() {
|
||||
|
||||
it("should return a member which changes as state changes", function() {
|
||||
const member = state.getMember(userB);
|
||||
expect(member.membership).toEqual("join");
|
||||
expect(member.name).toEqual(userB);
|
||||
expect(member?.membership).toEqual("join");
|
||||
expect(member?.name).toEqual(userB);
|
||||
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({
|
||||
@@ -77,40 +100,40 @@ describe("RoomState", function() {
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(member.membership).toEqual("leave");
|
||||
expect(member.name).toEqual("BobGone");
|
||||
expect(member?.membership).toEqual("leave");
|
||||
expect(member?.name).toEqual("BobGone");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSentinelMember", 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",
|
||||
function() {
|
||||
const preLeaveUser = state.getSentinelMember(userA);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({
|
||||
room: roomId, user: userA, mship: "leave", event: true,
|
||||
name: "AliceIsGone",
|
||||
}),
|
||||
]);
|
||||
const postLeaveUser = state.getSentinelMember(userA);
|
||||
function() {
|
||||
const preLeaveUser = state.getSentinelMember(userA);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({
|
||||
room: roomId, user: userA, mship: "leave", event: true,
|
||||
name: "AliceIsGone",
|
||||
}),
|
||||
]);
|
||||
const postLeaveUser = state.getSentinelMember(userA);
|
||||
|
||||
expect(preLeaveUser.membership).toEqual("join");
|
||||
expect(preLeaveUser.name).toEqual(userA);
|
||||
expect(preLeaveUser?.membership).toEqual("join");
|
||||
expect(preLeaveUser?.name).toEqual(userA);
|
||||
|
||||
expect(postLeaveUser.membership).toEqual("leave");
|
||||
expect(postLeaveUser.name).toEqual("AliceIsGone");
|
||||
});
|
||||
expect(postLeaveUser?.membership).toEqual("leave");
|
||||
expect(postLeaveUser?.name).toEqual("AliceIsGone");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStateEvents", function() {
|
||||
it("should return null if a state_key was specified and there was no match",
|
||||
function() {
|
||||
expect(state.getStateEvents("foo.bar.baz", "keyname")).toEqual(null);
|
||||
});
|
||||
function() {
|
||||
expect(state.getStateEvents("foo.bar.baz", "keyname")).toEqual(null);
|
||||
});
|
||||
|
||||
it("should return an empty list if a state_key was not specified and there" +
|
||||
" was no match", function() {
|
||||
@@ -118,21 +141,21 @@ describe("RoomState", function() {
|
||||
});
|
||||
|
||||
it("should return a list of matching events if no state_key was specified",
|
||||
function() {
|
||||
const events = state.getStateEvents("m.room.member");
|
||||
expect(events.length).toEqual(2);
|
||||
// ordering unimportant
|
||||
expect([userA, userB].indexOf(events[0].getStateKey())).not.toEqual(-1);
|
||||
expect([userA, userB].indexOf(events[1].getStateKey())).not.toEqual(-1);
|
||||
});
|
||||
function() {
|
||||
const events = state.getStateEvents("m.room.member");
|
||||
expect(events.length).toEqual(2);
|
||||
// ordering unimportant
|
||||
expect([userA, userB].indexOf(events[0].getStateKey() as string)).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",
|
||||
function() {
|
||||
const event = state.getStateEvents("m.room.member", userA);
|
||||
expect(event.getContent()).toMatchObject({
|
||||
membership: "join",
|
||||
function() {
|
||||
const event = state.getStateEvents("m.room.member", userA);
|
||||
expect(event.getContent()).toMatchObject({
|
||||
membership: "join",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("setStateEvents", function() {
|
||||
@@ -146,7 +169,7 @@ describe("RoomState", function() {
|
||||
}),
|
||||
];
|
||||
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(st).toEqual(state);
|
||||
expect(mem).toEqual(state.getMember(ev.getSender()));
|
||||
@@ -166,7 +189,7 @@ describe("RoomState", function() {
|
||||
}),
|
||||
];
|
||||
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(mem.userId).toEqual(memberEvents[emitCount].getSender());
|
||||
expect(mem.membership).toBeFalsy(); // not defined yet
|
||||
@@ -192,7 +215,7 @@ describe("RoomState", function() {
|
||||
}),
|
||||
];
|
||||
let emitCount = 0;
|
||||
state.on("RoomState.events", function(ev, st) {
|
||||
state.on(RoomStateEvent.Events, function(ev, st) {
|
||||
expect(ev).toEqual(events[emitCount]);
|
||||
expect(st).toEqual(state);
|
||||
emitCount += 1;
|
||||
@@ -272,7 +295,7 @@ describe("RoomState", function() {
|
||||
}),
|
||||
];
|
||||
let emitCount = 0;
|
||||
state.on("RoomState.Marker", function(markerEvent, markerFoundOptions) {
|
||||
state.on(RoomStateEvent.Marker, function(markerEvent, markerFoundOptions) {
|
||||
expect(markerEvent).toEqual(events[emitCount]);
|
||||
expect(markerFoundOptions).toEqual({ timelineWasEmpty: true });
|
||||
emitCount += 1;
|
||||
@@ -296,7 +319,7 @@ describe("RoomState", function() {
|
||||
|
||||
it('does not add redacted beacon info events to state', () => {
|
||||
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
|
||||
const redactionEvent = { event: { type: 'm.room.redaction' } };
|
||||
const redactionEvent = new MatrixEvent({ type: 'm.room.redaction' });
|
||||
redactedBeaconEvent.makeRedacted(redactionEvent);
|
||||
const emitSpy = jest.spyOn(state, 'emit');
|
||||
|
||||
@@ -316,27 +339,27 @@ describe("RoomState", function() {
|
||||
|
||||
state.setStateEvents([beaconEvent]);
|
||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
||||
expect(beaconInstance.isLive).toEqual(true);
|
||||
expect(beaconInstance?.isLive).toEqual(true);
|
||||
|
||||
state.setStateEvents([updatedBeaconEvent]);
|
||||
|
||||
// same Beacon
|
||||
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance);
|
||||
// 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', () => {
|
||||
const beaconId = '$beacon1';
|
||||
const beaconEvent = 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);
|
||||
|
||||
state.setStateEvents([beaconEvent]);
|
||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
||||
const destroySpy = jest.spyOn(beaconInstance, 'destroy');
|
||||
expect(beaconInstance.isLive).toEqual(true);
|
||||
const destroySpy = jest.spyOn(beaconInstance as Beacon, 'destroy');
|
||||
expect(beaconInstance?.isLive).toEqual(true);
|
||||
|
||||
state.setStateEvents([redactedBeaconEvent]);
|
||||
|
||||
@@ -357,7 +380,7 @@ describe("RoomState", function() {
|
||||
|
||||
// live beacon is now not live
|
||||
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
|
||||
userA, roomId, { isLive: false }, liveBeaconEvent.getId(), '$beacon1',
|
||||
userA, roomId, { isLive: false }, liveBeaconEvent.getId(),
|
||||
);
|
||||
|
||||
state.setStateEvents([updatedLiveBeaconEvent]);
|
||||
@@ -377,8 +400,8 @@ describe("RoomState", function() {
|
||||
state.markOutOfBandMembersStarted();
|
||||
state.setOutOfBandMembers([oobMemberEvent]);
|
||||
const member = state.getMember(userLazy);
|
||||
expect(member.userId).toEqual(userLazy);
|
||||
expect(member.isOutOfBand()).toEqual(true);
|
||||
expect(member?.userId).toEqual(userLazy);
|
||||
expect(member?.isOutOfBand()).toEqual(true);
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
let eventReceived = false;
|
||||
state.once('RoomState.newMember', (_, __, member) => {
|
||||
state.once(RoomStateEvent.NewMember, (_event, _state, member) => {
|
||||
expect(member.userId).toEqual(userLazy);
|
||||
eventReceived = true;
|
||||
});
|
||||
@@ -410,8 +433,8 @@ describe("RoomState", function() {
|
||||
state.markOutOfBandMembersStarted();
|
||||
state.setOutOfBandMembers([oobMemberEvent]);
|
||||
const memberA = state.getMember(userA);
|
||||
expect(memberA.events.member.getId()).not.toEqual(oobMemberEvent.getId());
|
||||
expect(memberA.isOutOfBand()).toEqual(false);
|
||||
expect(memberA?.events?.member?.getId()).not.toEqual(oobMemberEvent.getId());
|
||||
expect(memberA?.isOutOfBand()).toEqual(false);
|
||||
});
|
||||
|
||||
it("should emit members when updating a member", function() {
|
||||
@@ -420,7 +443,7 @@ describe("RoomState", function() {
|
||||
user: doesntExistYetUserId, mship: "join", room: roomId, event: true,
|
||||
});
|
||||
let eventReceived = false;
|
||||
state.once('RoomState.members', (_, __, member) => {
|
||||
state.once(RoomStateEvent.Members, (_event, _state, member) => {
|
||||
expect(member.userId).toEqual(doesntExistYetUserId);
|
||||
eventReceived = true;
|
||||
});
|
||||
@@ -443,8 +466,8 @@ describe("RoomState", function() {
|
||||
[userA, userB, userLazy].forEach((userId) => {
|
||||
const member = state.getMember(userId);
|
||||
const memberCopy = copy.getMember(userId);
|
||||
expect(member.name).toEqual(memberCopy.name);
|
||||
expect(member.isOutOfBand()).toEqual(memberCopy.isOutOfBand());
|
||||
expect(member?.name).toEqual(memberCopy?.name);
|
||||
expect(member?.isOutOfBand()).toEqual(memberCopy?.isOutOfBand());
|
||||
});
|
||||
// check member keys
|
||||
expect(Object.keys(state.members)).toEqual(Object.keys(copy.members));
|
||||
@@ -496,78 +519,80 @@ describe("RoomState", function() {
|
||||
|
||||
describe("maySendStateEvent", function() {
|
||||
it("should say any member may send state with no power level event",
|
||||
function() {
|
||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||
});
|
||||
function() {
|
||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||
});
|
||||
|
||||
it("should say members with power >=50 may send state with power level event " +
|
||||
"but no state default",
|
||||
function() {
|
||||
const powerLevelEvent = {
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
const powerLevelEvent = new MatrixEvent({
|
||||
type: "m.room.power_levels", room_id: roomId, sender: userA,
|
||||
state_key: "",
|
||||
content: {
|
||||
users_default: 10,
|
||||
// state_default: 50, "intentionally left blank"
|
||||
events_default: 25,
|
||||
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', userB)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should obey state_default",
|
||||
function() {
|
||||
const powerLevelEvent = {
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
content: {
|
||||
users_default: 10,
|
||||
state_default: 30,
|
||||
events_default: 25,
|
||||
users: {
|
||||
function() {
|
||||
const powerLevelEvent = new MatrixEvent({
|
||||
type: "m.room.power_levels", room_id: roomId, sender: userA,
|
||||
state_key: "",
|
||||
content: {
|
||||
users_default: 10,
|
||||
state_default: 30,
|
||||
events_default: 25,
|
||||
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', userB)).toEqual(false);
|
||||
});
|
||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should honour explicit event power levels in the power_levels event",
|
||||
function() {
|
||||
const powerLevelEvent = {
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
content: {
|
||||
events: {
|
||||
"m.room.other_thing": 76,
|
||||
function() {
|
||||
const powerLevelEvent = new MatrixEvent({
|
||||
type: "m.room.power_levels", room_id: roomId, sender: userA,
|
||||
state_key: "", content: {
|
||||
events: {
|
||||
"m.room.other_thing": 76,
|
||||
},
|
||||
users_default: 10,
|
||||
state_default: 50,
|
||||
events_default: 25,
|
||||
users: {
|
||||
[userA]: 80,
|
||||
[userB]: 50,
|
||||
},
|
||||
},
|
||||
users_default: 10,
|
||||
state_default: 50,
|
||||
events_default: 25,
|
||||
users: {
|
||||
},
|
||||
},
|
||||
};
|
||||
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', userB)).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.other_thing', userA)).toEqual(true);
|
||||
expect(state.maySendStateEvent('m.room.other_thing', userB)).toEqual(false);
|
||||
});
|
||||
expect(state.maySendStateEvent('m.room.other_thing', userA)).toEqual(true);
|
||||
expect(state.maySendStateEvent('m.room.other_thing', userB)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getJoinedMemberCount", function() {
|
||||
@@ -682,71 +707,73 @@ describe("RoomState", function() {
|
||||
|
||||
describe("maySendEvent", function() {
|
||||
it("should say any member may send events with no power level event",
|
||||
function() {
|
||||
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
||||
expect(state.maySendMessage(userA)).toEqual(true);
|
||||
});
|
||||
function() {
|
||||
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
||||
expect(state.maySendMessage(userA)).toEqual(true);
|
||||
});
|
||||
|
||||
it("should obey events_default",
|
||||
function() {
|
||||
const powerLevelEvent = {
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
content: {
|
||||
users_default: 10,
|
||||
state_default: 30,
|
||||
events_default: 25,
|
||||
users: {
|
||||
function() {
|
||||
const powerLevelEvent = new MatrixEvent({
|
||||
type: "m.room.power_levels", room_id: roomId, sender: userA,
|
||||
state_key: "",
|
||||
content: {
|
||||
users_default: 10,
|
||||
state_default: 30,
|
||||
events_default: 25,
|
||||
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', userB)).toEqual(false);
|
||||
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
||||
expect(state.maySendEvent('m.room.message', userB)).toEqual(false);
|
||||
|
||||
expect(state.maySendMessage(userA)).toEqual(true);
|
||||
expect(state.maySendMessage(userB)).toEqual(false);
|
||||
});
|
||||
expect(state.maySendMessage(userA)).toEqual(true);
|
||||
expect(state.maySendMessage(userB)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should honour explicit event power levels in the power_levels event",
|
||||
function() {
|
||||
const powerLevelEvent = {
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
content: {
|
||||
events: {
|
||||
"m.room.other_thing": 33,
|
||||
function() {
|
||||
const powerLevelEvent = new MatrixEvent({
|
||||
type: "m.room.power_levels", room_id: roomId, sender: userA,
|
||||
state_key: "",
|
||||
content: {
|
||||
events: {
|
||||
"m.room.other_thing": 33,
|
||||
},
|
||||
users_default: 10,
|
||||
state_default: 50,
|
||||
events_default: 25,
|
||||
users: {
|
||||
[userA]: 40,
|
||||
[userB]: 30,
|
||||
},
|
||||
},
|
||||
users_default: 10,
|
||||
state_default: 50,
|
||||
events_default: 25,
|
||||
users: {
|
||||
},
|
||||
},
|
||||
};
|
||||
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', userB)).toEqual(true);
|
||||
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
||||
expect(state.maySendEvent('m.room.message', userB)).toEqual(true);
|
||||
|
||||
expect(state.maySendMessage(userA)).toEqual(true);
|
||||
expect(state.maySendMessage(userB)).toEqual(true);
|
||||
expect(state.maySendMessage(userA)).toEqual(true);
|
||||
expect(state.maySendMessage(userB)).toEqual(true);
|
||||
|
||||
expect(state.maySendEvent('m.room.other_thing', userA)).toEqual(true);
|
||||
expect(state.maySendEvent('m.room.other_thing', userB)).toEqual(false);
|
||||
});
|
||||
expect(state.maySendEvent('m.room.other_thing', userA)).toEqual(true);
|
||||
expect(state.maySendEvent('m.room.other_thing', userB)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processBeaconEvents', () => {
|
||||
const beacon1 = makeBeaconInfoEvent(userA, roomId, {}, '$beacon1', '$beacon1');
|
||||
const beacon2 = makeBeaconInfoEvent(userB, roomId, {}, '$beacon2', '$beacon2');
|
||||
const beacon1 = makeBeaconInfoEvent(userA, roomId, {}, '$beacon1');
|
||||
const beacon2 = makeBeaconInfoEvent(userB, roomId, {}, '$beacon2');
|
||||
|
||||
const mockClient = { decryptEventIfNeeded: jest.fn() };
|
||||
const mockClient = { decryptEventIfNeeded: jest.fn() } as unknown as MockedObject<MatrixClient>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.decryptEventIfNeeded.mockClear();
|
||||
@@ -816,11 +843,11 @@ describe("RoomState", function() {
|
||||
beaconInfoId: 'some-other-beacon',
|
||||
});
|
||||
|
||||
state.setStateEvents([beacon1, beacon2], mockClient);
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
|
||||
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');
|
||||
|
||||
state.processBeaconEvents([location1, location2, location3], mockClient);
|
||||
@@ -885,7 +912,7 @@ describe("RoomState", function() {
|
||||
});
|
||||
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();
|
||||
state.processBeaconEvents([location, otherRelatedEvent], mockClient);
|
||||
expect(addLocationsSpy).not.toHaveBeenCalled();
|
||||
@@ -945,13 +972,13 @@ describe("RoomState", function() {
|
||||
});
|
||||
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
||||
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();
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
|
||||
// this event is a message after decryption
|
||||
decryptingRelatedEvent.type = EventType.RoomMessage;
|
||||
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted);
|
||||
decryptingRelatedEvent.event.type = EventType.RoomMessage;
|
||||
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted, decryptingRelatedEvent);
|
||||
|
||||
expect(addLocationsSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -967,14 +994,14 @@ describe("RoomState", function() {
|
||||
});
|
||||
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
||||
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();
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
|
||||
// update type after '''decryption'''
|
||||
decryptingRelatedEvent.event.type = M_BEACON.name;
|
||||
decryptingRelatedEvent.event.content = locationEvent.content;
|
||||
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted);
|
||||
decryptingRelatedEvent.event.content = locationEvent.event.content;
|
||||
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted, decryptingRelatedEvent);
|
||||
|
||||
expect(addLocationsSpy).toHaveBeenCalledWith([decryptingRelatedEvent]);
|
||||
});
|
||||
@@ -32,13 +32,14 @@ import {
|
||||
RoomEvent,
|
||||
} from "../../src";
|
||||
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 { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
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() {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -288,11 +289,11 @@ describe("Room", function() {
|
||||
room.addLiveEvents(events);
|
||||
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
|
||||
[events[0]],
|
||||
{ timelineWasEmpty: undefined },
|
||||
{ timelineWasEmpty: false },
|
||||
);
|
||||
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
|
||||
[events[1]],
|
||||
{ timelineWasEmpty: undefined },
|
||||
{ timelineWasEmpty: false },
|
||||
);
|
||||
expect(events[0].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
|
||||
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', () => {
|
||||
@@ -1419,6 +1431,19 @@ describe("Room", function() {
|
||||
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() {
|
||||
@@ -2383,7 +2408,7 @@ describe("Room", function() {
|
||||
});
|
||||
|
||||
it("should aggregate relations in thread event timeline set", () => {
|
||||
Thread.setServerSideSupport(true, true);
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
const threadRoot = mkMessage();
|
||||
const rootReaction = mkReaction(threadRoot);
|
||||
const threadResponse = mkThreadResponse(threadRoot);
|
||||
@@ -2428,8 +2453,8 @@ describe("Room", function() {
|
||||
const room = new Room(roomId, client, userA);
|
||||
|
||||
it("handles missing receipt type", () => {
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
||||
return receiptType === ReceiptType.ReadPrivate ? { eventId: "eventId" } as IWrappedReceipt : null;
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
||||
return receiptType === ReceiptType.ReadPrivate ? { eventId: "eventId" } as WrappedReceipt : null;
|
||||
};
|
||||
|
||||
expect(room.getEventReadUpTo(userA)).toEqual("eventId");
|
||||
@@ -2437,19 +2462,17 @@ describe("Room", function() {
|
||||
|
||||
describe("prefers newer receipt", () => {
|
||||
it("should compare correctly using timelines", () => {
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
||||
if (receiptType === ReceiptType.ReadPrivate) {
|
||||
return { eventId: "eventId1" } as IWrappedReceipt;
|
||||
}
|
||||
if (receiptType === ReceiptType.UnstableReadPrivate) {
|
||||
return { eventId: "eventId2" } as IWrappedReceipt;
|
||||
return { eventId: "eventId1" } as WrappedReceipt;
|
||||
}
|
||||
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) => {
|
||||
return (event1 === `eventId${i}`) ? 1 : -1;
|
||||
} } as EventTimelineSet);
|
||||
@@ -2460,20 +2483,18 @@ describe("Room", function() {
|
||||
|
||||
describe("correctly compares by timestamp", () => {
|
||||
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 = () => ({
|
||||
compareEventOrdering: (_1, _2) => null,
|
||||
} as EventTimelineSet);
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
||||
if (receiptType === ReceiptType.ReadPrivate) {
|
||||
return { eventId: "eventId1", data: { ts: i === 1 ? 1 : 0 } } as IWrappedReceipt;
|
||||
}
|
||||
if (receiptType === ReceiptType.UnstableReadPrivate) {
|
||||
return { eventId: "eventId2", data: { ts: i === 2 ? 1 : 0 } } as IWrappedReceipt;
|
||||
return { eventId: "eventId1", data: { ts: i === 1 ? 2 : 1 } } as WrappedReceipt;
|
||||
}
|
||||
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}`);
|
||||
@@ -2484,13 +2505,11 @@ describe("Room", function() {
|
||||
room.getUnfilteredTimelineSet = () => ({
|
||||
compareEventOrdering: (_1, _2) => null,
|
||||
} as EventTimelineSet);
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
||||
if (receiptType === ReceiptType.UnstableReadPrivate) {
|
||||
return { eventId: "eventId1", data: { ts: 0 } } as IWrappedReceipt;
|
||||
}
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
||||
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`);
|
||||
@@ -2505,39 +2524,25 @@ describe("Room", function() {
|
||||
});
|
||||
|
||||
it("should give precedence to m.read.private", () => {
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
||||
if (receiptType === ReceiptType.ReadPrivate) {
|
||||
return { eventId: "eventId1" } as IWrappedReceipt;
|
||||
}
|
||||
if (receiptType === ReceiptType.UnstableReadPrivate) {
|
||||
return { eventId: "eventId2" } as IWrappedReceipt;
|
||||
return { eventId: "eventId1" } as WrappedReceipt;
|
||||
}
|
||||
if (receiptType === ReceiptType.Read) {
|
||||
return { eventId: "eventId3" } as IWrappedReceipt;
|
||||
return { eventId: "eventId2" } as WrappedReceipt;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
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", () => {
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
|
||||
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
|
||||
if (receiptType === ReceiptType.Read) {
|
||||
return { eventId: "eventId3" } as IWrappedReceipt;
|
||||
return { eventId: "eventId3" } as WrappedReceipt;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
expect(room.getEventReadUpTo(userA)).toEqual(`eventId3`);
|
||||
@@ -2557,4 +2562,40 @@ describe("Room", function() {
|
||||
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 { emitPromise } from "../../test-utils/test-utils";
|
||||
import { LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend";
|
||||
import { defer } from "../../../src/utils";
|
||||
|
||||
describe("IndexedDBStore", () => {
|
||||
afterEach(() => {
|
||||
@@ -111,4 +112,57 @@ describe("IndexedDBStore", () => {
|
||||
await store.setPendingEvents(roomId, []);
|
||||
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: [] },
|
||||
ephemeral: { events: [] },
|
||||
unread_notifications: {},
|
||||
unread_thread_notifications: {
|
||||
"$143273582443PhrSn:example.org": {
|
||||
highlight_count: 0,
|
||||
notification_count: 1,
|
||||
},
|
||||
},
|
||||
timeline: {
|
||||
events: [
|
||||
Object.freeze({
|
||||
@@ -302,9 +308,6 @@ describe("SyncAccumulator", function() {
|
||||
[ReceiptType.ReadPrivate]: {
|
||||
"@dan:localhost": { ts: 4 },
|
||||
},
|
||||
[ReceiptType.UnstableReadPrivate]: {
|
||||
"@matthew:localhost": { ts: 5 },
|
||||
},
|
||||
"some.other.receipt.type": {
|
||||
"@should_be_ignored:localhost": { key: "val" },
|
||||
},
|
||||
@@ -350,9 +353,6 @@ describe("SyncAccumulator", function() {
|
||||
[ReceiptType.ReadPrivate]: {
|
||||
"@dan:localhost": { ts: 4 },
|
||||
},
|
||||
[ReceiptType.UnstableReadPrivate]: {
|
||||
"@matthew:localhost": { ts: 5 },
|
||||
},
|
||||
},
|
||||
"$event2:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
@@ -445,6 +445,13 @@ describe("SyncAccumulator", function() {
|
||||
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 }, { 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 }));
|
||||
assert.isFalse(utils.deepCompare({ a: 1 }, { a: 1, b: 2 }));
|
||||
assert.isFalse(utils.deepCompare({ a: 1 }, { b: 1 }));
|
||||
|
||||
assert.isTrue(utils.deepCompare({
|
||||
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', () => {
|
||||
it('should support m.read', () => {
|
||||
expect(utils.isSupportedReceiptType(ReceiptType.Read)).toBeTruthy();
|
||||
@@ -566,10 +537,6 @@ describe("utils", function() {
|
||||
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', () => {
|
||||
expect(utils.isSupportedReceiptType("this is a receipt type")).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -156,9 +156,13 @@ export interface IPusher {
|
||||
lang: string;
|
||||
profile_tag?: 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
|
||||
// disable lint because these are wire responses
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
@@ -27,3 +29,89 @@ export interface IRefreshTokenResponse {
|
||||
}
|
||||
|
||||
/* 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",
|
||||
"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 {
|
||||
url: 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",
|
||||
FullyRead = "m.fully_read",
|
||||
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 { PushRuleAction } from "./PushRules";
|
||||
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
|
||||
/* eslint-disable camelcase */
|
||||
@@ -98,7 +98,18 @@ export interface ICreateRoomOpts {
|
||||
name?: string;
|
||||
topic?: string;
|
||||
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;
|
||||
initial_state?: ICreateRoomStateEvent[];
|
||||
invite?: string[];
|
||||
@@ -149,7 +160,7 @@ export interface IRelationsRequestOpts {
|
||||
from?: string;
|
||||
to?: string;
|
||||
limit?: number;
|
||||
direction?: Direction;
|
||||
dir?: Direction;
|
||||
}
|
||||
|
||||
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");
|
||||
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.
|
||||
*/
|
||||
|
||||
import { Optional } from "matrix-events-sdk/lib/types";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -41,13 +43,20 @@ export class NamespacedValue<S extends string, U extends string> {
|
||||
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 {
|
||||
return this.name === val || this.altName === val;
|
||||
}
|
||||
|
||||
// this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class
|
||||
// so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace.
|
||||
public findIn<T>(obj: any): T {
|
||||
public findIn<T>(obj: any): Optional<T> {
|
||||
let val: T;
|
||||
if (this.name) {
|
||||
val = obj?.[this.name];
|
||||
|
||||
523
src/client.ts
523
src/client.ts
@@ -19,7 +19,7 @@ limitations under the License.
|
||||
* @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 {
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
} from "./models/event";
|
||||
import { StubStore } from "./store/stub";
|
||||
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 * as utils from './utils';
|
||||
import { sleep } from './utils';
|
||||
@@ -158,7 +158,9 @@ import {
|
||||
} from "./@types/requests";
|
||||
import {
|
||||
EventType,
|
||||
LOCAL_NOTIFICATION_SETTINGS_PREFIX,
|
||||
MsgType,
|
||||
PUSHER_ENABLED,
|
||||
RelationType,
|
||||
RoomCreateTypeField,
|
||||
RoomType,
|
||||
@@ -188,15 +190,22 @@ import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, Rule
|
||||
import { IThreepid } from "./@types/threepids";
|
||||
import { CryptoStore } from "./crypto/store/base";
|
||||
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 { ReceiptType } from "./@types/read_receipts";
|
||||
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
|
||||
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 { UnstableValue } from "./NamespacedValue";
|
||||
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
|
||||
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;
|
||||
|
||||
@@ -208,6 +217,11 @@ export const CRYPTO_ENABLED: boolean = isCryptoAvailable();
|
||||
const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
|
||||
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 {
|
||||
olmDevice: IExportedOlmDevice;
|
||||
userId: string;
|
||||
@@ -396,8 +410,7 @@ export interface IStartClientOpts {
|
||||
pollTimeout?: number;
|
||||
|
||||
/**
|
||||
* The filter to apply to /sync calls. This will override the opts.initialSyncLimit, which would
|
||||
* normally result in a timeline limit filter.
|
||||
* The filter to apply to /sync calls.
|
||||
*/
|
||||
filter?: Filter;
|
||||
|
||||
@@ -517,15 +530,21 @@ export interface ITurnServer {
|
||||
credential: string;
|
||||
}
|
||||
|
||||
interface IServerVersions {
|
||||
export interface IServerVersions {
|
||||
versions: string[];
|
||||
unstable_features: Record<string, boolean>;
|
||||
}
|
||||
|
||||
export const M_AUTHENTICATION = new UnstableValue(
|
||||
"m.authentication",
|
||||
"org.matrix.msc2965.authentication",
|
||||
);
|
||||
|
||||
export interface IClientWellKnown {
|
||||
[key: string]: any;
|
||||
"m.homeserver"?: IWellKnownConfig;
|
||||
"m.identity_server"?: IWellKnownConfig;
|
||||
[M_AUTHENTICATION.name]?: IDelegatedAuthConfig; // MSC2965
|
||||
}
|
||||
|
||||
export interface IWellKnownConfig {
|
||||
@@ -537,6 +556,13 @@ export interface IWellKnownConfig {
|
||||
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 {
|
||||
path: string;
|
||||
queryData?: {
|
||||
@@ -572,6 +598,13 @@ interface IMessagesResponse {
|
||||
state: IStateEvent[];
|
||||
}
|
||||
|
||||
interface IThreadedMessagesResponse {
|
||||
prev_batch: string;
|
||||
next_batch: string;
|
||||
chunk: IRoomEvent[];
|
||||
state: IStateEvent[];
|
||||
}
|
||||
|
||||
export interface IRequestTokenResponse {
|
||||
sid: string;
|
||||
submit_url?: string;
|
||||
@@ -669,6 +702,8 @@ export interface IMyDevice {
|
||||
display_name?: string;
|
||||
last_seen_ip?: string;
|
||||
last_seen_ts?: number;
|
||||
[UNSTABLE_MSC3852_LAST_SEEN_UA.stable]?: string;
|
||||
[UNSTABLE_MSC3852_LAST_SEEN_UA.unstable]?: string;
|
||||
}
|
||||
|
||||
export interface IDownloadKeyResult {
|
||||
@@ -846,7 +881,7 @@ type UserEvents = UserEvent.AvatarUrl
|
||||
| UserEvent.CurrentlyActive
|
||||
| UserEvent.LastPresenceTs;
|
||||
|
||||
type EmittedEvents = ClientEvent
|
||||
export type EmittedEvents = ClientEvent
|
||||
| RoomEvents
|
||||
| RoomStateEvents
|
||||
| CryptoEvents
|
||||
@@ -881,6 +916,8 @@ export type ClientEventHandlerMap = {
|
||||
& HttpApiEventHandlerMap
|
||||
& 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
|
||||
* 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 identityServer: IIdentityServerProvider;
|
||||
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 callEventHandler: CallEventHandler; // 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 canResetTimelineCallback: ResetTimelineCallback;
|
||||
|
||||
public canSupport = new Map<Feature, ServerSupport>();
|
||||
|
||||
// The pushprocessor caches useful things, so keep one and re-use it
|
||||
protected pushProcessor = new PushProcessor(this);
|
||||
|
||||
@@ -955,6 +994,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
private toDeviceMessageQueue: ToDeviceMessageQueue;
|
||||
|
||||
// A manager for determining which invites should be ignored.
|
||||
public readonly ignoredInvites: IgnoredInvites;
|
||||
|
||||
constructor(opts: IMatrixClientCreateOpts) {
|
||||
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
|
||||
// state, such as highlights when the user's name is mentioned.
|
||||
this.on(MatrixEventEvent.Decrypted, (event) => {
|
||||
const oldActions = event.getPushActions();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
fixNotificationCountOnDecryption(this, event);
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
|
||||
this.ignoredInvites = new IgnoredInvites(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1185,14 +1201,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.syncApi.stop();
|
||||
}
|
||||
|
||||
try {
|
||||
const { serverSupport, stable } = await this.doesServerSupportThread();
|
||||
Thread.setServerSideSupport(serverSupport, stable);
|
||||
} catch (e) {
|
||||
// Most likely cause is that `doesServerSupportThread` returned `null` (as it
|
||||
// is allowed to do) and thus we enter "degraded mode" on threads.
|
||||
Thread.setServerSideSupport(false, true);
|
||||
}
|
||||
const serverVersions = await this.getVersions();
|
||||
this.canSupport = await buildFeatureSupportMap(serverVersions);
|
||||
|
||||
const support = this.canSupport.get(Feature.ThreadUnreadNotifications);
|
||||
UNREAD_THREAD_NOTIFICATIONS.setPreferUnstable(support === ServerSupport.Unstable);
|
||||
|
||||
const { threads, list } = await this.doesServerSupportThread();
|
||||
Thread.setServerSideSupport(threads);
|
||||
Thread.setServerSideListSupport(list);
|
||||
|
||||
// shallow-copy the opts dict before modifying and storing it
|
||||
this.clientOpts = Object.assign({}, opts) as IStoredClientOpts;
|
||||
@@ -2667,7 +2684,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
try {
|
||||
res = await this.http.authedRequest<IKeyBackupInfo>(
|
||||
undefined, Method.Get, "/room_keys/version", undefined, undefined,
|
||||
{ prefix: PREFIX_UNSTABLE },
|
||||
{ prefix: PREFIX_V3 },
|
||||
);
|
||||
} catch (e) {
|
||||
if (e.errcode === 'M_NOT_FOUND') {
|
||||
@@ -2823,7 +2840,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
const res = await this.http.authedRequest<IKeyBackupInfo>(
|
||||
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
|
||||
@@ -2855,7 +2872,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
return this.http.authedRequest(
|
||||
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
|
||||
* @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);
|
||||
}
|
||||
|
||||
@@ -3413,7 +3433,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param {string} eventType The event type
|
||||
* @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);
|
||||
}
|
||||
|
||||
@@ -4582,7 +4602,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @return {Promise} Resolves: to an empty object {}
|
||||
* @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') {
|
||||
callback = body as any as Callback; // legacy
|
||||
body = {};
|
||||
@@ -4597,10 +4622,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
$receiptType: receiptType,
|
||||
$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 room = this.getRoom(event.getRoomId());
|
||||
if (room) {
|
||||
if (room && this.credentials.userId) {
|
||||
room.addLocalEchoReceipt(this.credentials.userId, event, receiptType);
|
||||
}
|
||||
return promise;
|
||||
@@ -4614,7 +4648,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @return {Promise} Resolves: to an empty object {}
|
||||
* @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 room = this.getRoom(event.getRoomId());
|
||||
if (room && room.hasPendingEvent(eventId)) {
|
||||
@@ -5279,6 +5318,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @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.decrypt decrypt event proactively
|
||||
* @param {boolean} options.toDevice the event is a to_device event
|
||||
* @return {Function}
|
||||
*/
|
||||
public getEventMapper(options?: MapperOpts): EventMapper {
|
||||
@@ -5299,13 +5339,17 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @return {Promise} Resolves:
|
||||
* {@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.
|
||||
if (!this.timelineSupport) {
|
||||
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
|
||||
" parameter to true when creating MatrixClient to enable it.");
|
||||
}
|
||||
|
||||
if (!timelineSet?.room) {
|
||||
throw new Error("getEventTimeline only supports room timelines");
|
||||
}
|
||||
|
||||
if (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) {
|
||||
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) {
|
||||
const thread = timelineSet.thread;
|
||||
const opts: IRelationsRequestOpts = {
|
||||
direction: Direction.Backward,
|
||||
dir: Direction.Backward,
|
||||
limit: 50,
|
||||
};
|
||||
|
||||
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
|
||||
while (!thread.findEventById(eventId)) {
|
||||
@@ -5410,27 +5454,36 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @return {Promise} Resolves:
|
||||
* {@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.
|
||||
if (!this.timelineSupport) {
|
||||
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
|
||||
" parameter to true when creating MatrixClient to enable it.");
|
||||
}
|
||||
|
||||
const messagesPath = utils.encodeUri(
|
||||
"/rooms/$roomId/messages", {
|
||||
$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);
|
||||
if (!timelineSet.room) {
|
||||
throw new Error("getLatestTimeline only supports room timelines");
|
||||
}
|
||||
|
||||
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];
|
||||
if (!event) {
|
||||
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;
|
||||
}
|
||||
|
||||
let filter = null;
|
||||
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
|
||||
@@ -5488,6 +5541,72 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
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.
|
||||
*
|
||||
@@ -5503,6 +5622,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
*/
|
||||
public paginateEventTimeline(eventTimeline: EventTimeline, opts: IPaginateOpts): Promise<boolean> {
|
||||
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
|
||||
// nicely with HTTP errors.
|
||||
@@ -5536,7 +5657,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
only: 'highlight',
|
||||
};
|
||||
|
||||
if (token !== "end") {
|
||||
if (token && token !== "end") {
|
||||
params.from = token;
|
||||
}
|
||||
|
||||
@@ -5544,7 +5665,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
undefined, Method.Get, path, params,
|
||||
).then(async (res) => {
|
||||
const token = res.next_token;
|
||||
const matrixEvents = [];
|
||||
const matrixEvents: MatrixEvent[] = [];
|
||||
|
||||
for (let i = 0; i < res.notifications.length; i++) {
|
||||
const notification = res.notifications[i];
|
||||
@@ -5568,13 +5689,48 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
if (backwards && !res.next_token) {
|
||||
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(() => {
|
||||
eventTimeline.paginationRequests[dir] = null;
|
||||
});
|
||||
eventTimeline.paginationRequests[dir] = promise;
|
||||
} else {
|
||||
const room = this.getRoom(eventTimeline.getRoomId());
|
||||
if (!room) {
|
||||
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 timelineSet = eventTimeline.getTimelineSet();
|
||||
const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(matrixEvents);
|
||||
const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents);
|
||||
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
|
||||
this.processBeaconEvents(timelineSet.room, timelineEvents);
|
||||
this.processBeaconEvents(room, timelineEvents);
|
||||
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
|
||||
// paginate. We need to keep the 'forwards' token though, to make sure
|
||||
// we can recover from gappy syncs.
|
||||
if (backwards && res.end == res.start) {
|
||||
if (backwards && atEnd) {
|
||||
eventTimeline.setPaginationToken(null, dir);
|
||||
}
|
||||
return res.end != res.start;
|
||||
return !atEnd;
|
||||
}).finally(() => {
|
||||
eventTimeline.paginationRequests[dir] = null;
|
||||
});
|
||||
@@ -6684,23 +6842,28 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
public async doesServerSupportThread(): Promise<{
|
||||
serverSupport: boolean;
|
||||
stable: boolean;
|
||||
} | null> {
|
||||
threads: FeatureSupport;
|
||||
list: FeatureSupport;
|
||||
}> {
|
||||
try {
|
||||
const hasUnstableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440");
|
||||
const hasStableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable");
|
||||
const [threadUnstable, threadStable, listUnstable, listStable] = await Promise.all([
|
||||
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.
|
||||
|
||||
return {
|
||||
serverSupport: hasUnstableSupport || hasStableSupport,
|
||||
stable: hasStableSupport,
|
||||
threads: determineFeatureSupport(threadStable, threadUnstable),
|
||||
list: determineFeatureSupport(listStable, listUnstable),
|
||||
};
|
||||
} catch (e) {
|
||||
// Assume server support and stability aren't available: null/no data return.
|
||||
// XXX: This should just return an object with `false` booleans instead.
|
||||
return null;
|
||||
return {
|
||||
threads: FeatureSupport.None,
|
||||
list: FeatureSupport.None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6757,7 +6920,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
eventId: string,
|
||||
relationType?: RelationType | string | null,
|
||||
eventType?: EventType | string | null,
|
||||
opts: IRelationsRequestOpts = { direction: Direction.Backward },
|
||||
opts: IRelationsRequestOpts = { dir: Direction.Backward },
|
||||
): Promise<{
|
||||
originalEvent: MatrixEvent;
|
||||
events: MatrixEvent[];
|
||||
@@ -7072,10 +7235,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
public loginFlows(callback?: Callback): Promise<any> { // TODO: Types
|
||||
public loginFlows(callback?: Callback): Promise<ILoginFlowsResponse> {
|
||||
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).
|
||||
* Defaults to 'sso'.
|
||||
* @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.
|
||||
*/
|
||||
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";
|
||||
if (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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@@ -7303,7 +7498,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
eventId: string,
|
||||
relationType?: RelationType | string | null,
|
||||
eventType?: EventType | string | null,
|
||||
opts: IRelationsRequestOpts = { direction: Direction.Backward },
|
||||
opts: IRelationsRequestOpts = { dir: Direction.Backward },
|
||||
): Promise<IRelationsResponse> {
|
||||
const queryString = utils.encodeParams(opts as Record<string, string | number>);
|
||||
|
||||
@@ -7327,7 +7522,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
$eventType: eventType,
|
||||
});
|
||||
return this.http.authedRequest(
|
||||
undefined, Method.Get, path, null, null, {
|
||||
undefined, Method.Get, path, undefined, undefined, {
|
||||
prefix: PREFIX_UNSTABLE,
|
||||
},
|
||||
);
|
||||
@@ -7524,9 +7719,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
[ReceiptType.Read]: rrEventId,
|
||||
};
|
||||
|
||||
const privateField = await utils.getPrivateReadReceiptField(this);
|
||||
if (privateField) {
|
||||
content[privateField] = rpEventId;
|
||||
if (
|
||||
(await this.doesServerSupportUnstableFeature("org.matrix.msc2285.stable"))
|
||||
|| (await this.isVersionSupported("v1.4"))
|
||||
) {
|
||||
content[ReceiptType.ReadPrivate] = rpEventId;
|
||||
}
|
||||
|
||||
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[] }> {
|
||||
const path = utils.encodeUri("/rooms/$roomId/aliases", { $roomId: roomId });
|
||||
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,
|
||||
};
|
||||
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<{}> {
|
||||
const path = "/account/3pid/add";
|
||||
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") ?
|
||||
PREFIX_R0 : PREFIX_UNSTABLE;
|
||||
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),
|
||||
};
|
||||
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
|
||||
): Promise<{ id_server_unbind_result: IdServerUnbindResult }> {
|
||||
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<{}>(
|
||||
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 {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
public getPushers(callback?: Callback): Promise<{ pushers: IPusher[] }> {
|
||||
return this.http.authedRequest(callback, Method.Get, "/pushers");
|
||||
public async getPushers(callback?: Callback): Promise<{ pushers: IPusher[] }> {
|
||||
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<{}> {
|
||||
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,
|
||||
});
|
||||
|
||||
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> {
|
||||
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' },
|
||||
prefix: "/_matrix/client/unstable/im.nheko.summary",
|
||||
});
|
||||
@@ -9068,6 +9293,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
room.processThreadedEvents(threadedEvents, toStartOfTimeline);
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
public processThreadRoots(room: Room, threadedEvents: MatrixEvent[], toStartOfTimeline: boolean): void {
|
||||
room.processThreadRoots(threadedEvents, toStartOfTimeline);
|
||||
}
|
||||
|
||||
public processBeaconEvents(
|
||||
room?: Room,
|
||||
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.
|
||||
* <p>
|
||||
|
||||
@@ -292,16 +292,17 @@ export const makeBeaconContent: MakeBeaconContent = (
|
||||
});
|
||||
|
||||
export type BeaconLocationState = MLocationContent & {
|
||||
timestamp: number;
|
||||
uri?: string; // override from MLocationContent to allow optionals
|
||||
timestamp?: number;
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
return {
|
||||
description,
|
||||
uri,
|
||||
description: location?.description,
|
||||
uri: location?.uri,
|
||||
timestamp,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ import { logger } from "../logger";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
|
||||
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 {
|
||||
ClientEvent,
|
||||
@@ -246,14 +246,14 @@ export class EncryptionSetupOperation {
|
||||
algorithm: this.keyBackupInfo.algorithm,
|
||||
auth_data: this.keyBackupInfo.auth_data,
|
||||
},
|
||||
{ prefix: PREFIX_UNSTABLE },
|
||||
{ prefix: PREFIX_V3 },
|
||||
);
|
||||
} else {
|
||||
// add new key backup
|
||||
await baseApis.http.authedRequest(
|
||||
undefined, Method.Post, "/room_keys/version",
|
||||
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 { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
|
||||
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
|
||||
// reasonable approximation to the biggest plaintext we can encrypt.
|
||||
@@ -122,6 +123,7 @@ interface IInboundGroupSessionKey {
|
||||
forwarding_curve25519_key_chain: string[];
|
||||
sender_claimed_ed25519_key: string;
|
||||
shared_history: boolean;
|
||||
untrusted: boolean;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@@ -1101,7 +1103,7 @@ export class OlmDevice {
|
||||
sessionKey: string,
|
||||
keysClaimed: Record<string, string>,
|
||||
exportFormat: boolean,
|
||||
extraSessionData: Record<string, any> = {},
|
||||
extraSessionData: OlmGroupSessionExtraData = {},
|
||||
): Promise<void> {
|
||||
await this.cryptoStore.doTxn(
|
||||
'readwrite', [
|
||||
@@ -1133,17 +1135,42 @@ export class OlmDevice {
|
||||
"Update for megolm session "
|
||||
+ senderKey + "/" + sessionId,
|
||||
);
|
||||
if (existingSession.first_known_index()
|
||||
<= session.first_known_index()
|
||||
&& !(existingSession.first_known_index() == session.first_known_index()
|
||||
&& !extraSessionData.untrusted
|
||||
&& existingSessionData.untrusted)) {
|
||||
// existing session has lower index (i.e. can
|
||||
// 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}`);
|
||||
return;
|
||||
if (existingSession.first_known_index() <= session.first_known_index()) {
|
||||
if (!existingSessionData.untrusted || extraSessionData.untrusted) {
|
||||
// existing session has less-than-or-equal index
|
||||
// (i.e. can decrypt at least as much), and the
|
||||
// new session's trust does not win over the old
|
||||
// session's trust, so keep it
|
||||
logger.log(`Keeping existing megolm session ${sessionId}`);
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1427,13 +1454,23 @@ export class OlmDevice {
|
||||
const claimedKeys = sessionData.keysClaimed || {};
|
||||
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 = {
|
||||
"chain_index": chainIndex,
|
||||
"key": exportedSession,
|
||||
"forwarding_curve25519_key_chain":
|
||||
sessionData.forwardingCurve25519KeyChain || [],
|
||||
"forwarding_curve25519_key_chain": forwardingKeyChain,
|
||||
"sender_claimed_ed25519_key": senderEd25519Key,
|
||||
"shared_history": sessionData.sharedHistory || false,
|
||||
"untrusted": untrusted,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
@@ -539,7 +539,23 @@ export class SecretStorage {
|
||||
// because someone could be trying to send us bogus data
|
||||
return;
|
||||
}
|
||||
|
||||
if (!olmlib.isOlmEncrypted(event)) {
|
||||
logger.error("secret event not properly encrypted");
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
const requestControl = this.requests.get(content.request_id);
|
||||
if (requestControl) {
|
||||
@@ -559,6 +575,14 @@ export class SecretStorage {
|
||||
logger.log("unsolicited secret share from device", deviceInfo.deviceId);
|
||||
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(
|
||||
`Successfully received secret ${requestControl.name} ` +
|
||||
|
||||
@@ -23,7 +23,7 @@ limitations under the License.
|
||||
import { MatrixClient } from "../../client";
|
||||
import { Room } from "../../models/room";
|
||||
import { OlmDevice } from "../OlmDevice";
|
||||
import { MatrixEvent, RoomMember } from "../..";
|
||||
import { MatrixEvent, RoomMember } from "../../matrix";
|
||||
import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "..";
|
||||
import { DeviceInfo } from "../deviceinfo";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
@@ -34,7 +34,7 @@ import { IRoomEncryption } from "../RoomList";
|
||||
*
|
||||
* @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">;
|
||||
|
||||
@@ -44,7 +44,7 @@ type DecryptionClassParams = Omit<IParams, "deviceId" | "config">;
|
||||
*
|
||||
* @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 {
|
||||
userId: string;
|
||||
@@ -297,6 +297,6 @@ export function registerAlgorithm(
|
||||
encryptor: new (params: IParams) => EncryptionAlgorithm,
|
||||
decryptor: new (params: Omit<IParams, "deviceId">) => DecryptionAlgorithm,
|
||||
): void {
|
||||
ENCRYPTION_CLASSES[algorithm] = encryptor;
|
||||
DECRYPTION_CLASSES[algorithm] = decryptor;
|
||||
ENCRYPTION_CLASSES.set(algorithm, encryptor);
|
||||
DECRYPTION_CLASSES.set(algorithm, decryptor);
|
||||
}
|
||||
|
||||
@@ -35,8 +35,10 @@ import { Room } from '../../models/room';
|
||||
import { DeviceInfo } from "../deviceinfo";
|
||||
import { IOlmSessionResult } from "../olmlib";
|
||||
import { DeviceInfoMap } from "../DeviceList";
|
||||
import { MatrixEvent } from "../..";
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
|
||||
import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager';
|
||||
import { OlmGroupSessionExtraData } from "../../@types/crypto";
|
||||
|
||||
// determine whether the key can be shared with invitees
|
||||
export function isRoomSharedHistory(room: Room): boolean {
|
||||
@@ -1189,9 +1191,10 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
* {@link module:crypto/algorithms/DecryptionAlgorithm}
|
||||
*/
|
||||
class MegolmDecryption extends DecryptionAlgorithm {
|
||||
// events which we couldn't decrypt due to unknown sessions / indexes: map from
|
||||
// senderKey|sessionId to Set of MatrixEvents
|
||||
private pendingEvents: Record<string, Map<string, Set<MatrixEvent>>> = {};
|
||||
// events which we couldn't decrypt due to unknown sessions /
|
||||
// indexes, or which we could only decrypt with untrusted keys:
|
||||
// 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.
|
||||
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
|
||||
// already happened.
|
||||
this.removeEventFromPendingList(event);
|
||||
// Success. We can remove the event from the pending list, if
|
||||
// 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);
|
||||
}
|
||||
|
||||
const payload = JSON.parse(res.result);
|
||||
|
||||
@@ -1343,10 +1350,10 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
const content = event.getWireContent();
|
||||
const senderKey = content.sender_key;
|
||||
const sessionId = content.session_id;
|
||||
if (!this.pendingEvents[senderKey]) {
|
||||
this.pendingEvents[senderKey] = new Map();
|
||||
if (!this.pendingEvents.has(senderKey)) {
|
||||
this.pendingEvents.set(senderKey, new Map<string, Set<MatrixEvent>>());
|
||||
}
|
||||
const senderPendingEvents = this.pendingEvents[senderKey];
|
||||
const senderPendingEvents = this.pendingEvents.get(senderKey);
|
||||
if (!senderPendingEvents.has(sessionId)) {
|
||||
senderPendingEvents.set(sessionId, new Set());
|
||||
}
|
||||
@@ -1364,7 +1371,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
const content = event.getWireContent();
|
||||
const senderKey = content.sender_key;
|
||||
const sessionId = content.session_id;
|
||||
const senderPendingEvents = this.pendingEvents[senderKey];
|
||||
const senderPendingEvents = this.pendingEvents.get(senderKey);
|
||||
const pendingEvents = senderPendingEvents?.get(sessionId);
|
||||
if (!pendingEvents) {
|
||||
return;
|
||||
@@ -1375,7 +1382,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
senderPendingEvents.delete(sessionId);
|
||||
}
|
||||
if (senderPendingEvents.size === 0) {
|
||||
delete this.pendingEvents[senderKey];
|
||||
this.pendingEvents.delete(senderKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1391,6 +1398,8 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
let exportFormat = false;
|
||||
let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>;
|
||||
|
||||
const extraSessionData: OlmGroupSessionExtraData = {};
|
||||
|
||||
if (!content.room_id ||
|
||||
!content.session_key ||
|
||||
!content.session_id ||
|
||||
@@ -1400,12 +1409,59 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!senderKey) {
|
||||
logger.error("key event has no sender key (not encrypted?)");
|
||||
if (!olmlib.isOlmEncrypted(event)) {
|
||||
logger.error("key event not properly encrypted");
|
||||
return;
|
||||
}
|
||||
|
||||
if (content["org.matrix.msc3061.shared_history"]) {
|
||||
extraSessionData.sharedHistory = true;
|
||||
}
|
||||
|
||||
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;
|
||||
forwardingKeyChain = Array.isArray(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");
|
||||
return;
|
||||
}
|
||||
senderKey = content.sender_key;
|
||||
|
||||
const ed25519Key = content.sender_claimed_ed25519_key;
|
||||
if (!ed25519Key) {
|
||||
@@ -1431,11 +1486,45 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
keysClaimed = {
|
||||
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 {
|
||||
keysClaimed = event.getKeysClaimed();
|
||||
}
|
||||
|
||||
const extraSessionData: any = {};
|
||||
if (content["org.matrix.msc3061.shared_history"]) {
|
||||
extraSessionData.sharedHistory = true;
|
||||
}
|
||||
@@ -1453,7 +1542,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
);
|
||||
|
||||
// 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.
|
||||
// Only do this if we managed to decrypt every message in the
|
||||
// session, because if we didn't, we leave the other key
|
||||
@@ -1668,7 +1757,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
session: IMegolmSessionData,
|
||||
opts: { untrusted?: boolean, source?: string } = {},
|
||||
): Promise<void> {
|
||||
const extraSessionData: any = {};
|
||||
const extraSessionData: OlmGroupSessionExtraData = {};
|
||||
if (opts.untrusted || session.untrusted) {
|
||||
extraSessionData.untrusted = true;
|
||||
}
|
||||
@@ -1696,7 +1785,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
});
|
||||
}
|
||||
// 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
|
||||
* @param {String} senderKey
|
||||
* @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> {
|
||||
const senderPendingEvents = this.pendingEvents[senderKey];
|
||||
private async retryDecryption(
|
||||
senderKey: string,
|
||||
sessionId: string,
|
||||
forceRedecryptIfUntrusted?: boolean,
|
||||
): Promise<boolean> {
|
||||
const senderPendingEvents = this.pendingEvents.get(senderKey);
|
||||
if (!senderPendingEvents) {
|
||||
return true;
|
||||
}
|
||||
@@ -1725,23 +1821,24 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
|
||||
await Promise.all([...pending].map(async (ev) => {
|
||||
try {
|
||||
await ev.attemptDecryption(this.crypto, { isRetry: true });
|
||||
await ev.attemptDecryption(this.crypto, { isRetry: true, forceRedecryptIfUntrusted });
|
||||
} catch (e) {
|
||||
// don't die if something goes wrong
|
||||
}
|
||||
}));
|
||||
|
||||
// If decrypted successfully, they'll have been removed from pendingEvents
|
||||
return !this.pendingEvents[senderKey]?.has(sessionId);
|
||||
// If decrypted successfully with trusted keys, they'll have
|
||||
// been removed from pendingEvents
|
||||
return !this.pendingEvents.get(senderKey)?.has(sessionId);
|
||||
}
|
||||
|
||||
public async retryDecryptionFromSender(senderKey: string): Promise<boolean> {
|
||||
const senderPendingEvents = this.pendingEvents[senderKey];
|
||||
const senderPendingEvents = this.pendingEvents.get(senderKey);
|
||||
if (!senderPendingEvents) {
|
||||
return true;
|
||||
}
|
||||
|
||||
delete this.pendingEvents[senderKey];
|
||||
this.pendingEvents.delete(senderKey);
|
||||
|
||||
await Promise.all([...senderPendingEvents].map(async ([_sessionId, pending]) => {
|
||||
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> {
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
registerAlgorithm,
|
||||
} from "./base";
|
||||
import { Room } from '../../models/room';
|
||||
import { MatrixEvent } from "../..";
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
import { IEventDecryptionResult } from "../index";
|
||||
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
|
||||
// avoid people masquerading as others.
|
||||
// (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;
|
||||
}
|
||||
|
||||
|
||||
@@ -278,9 +278,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
private oneTimeKeyCheckInProgress = false;
|
||||
|
||||
// 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
|
||||
private roomDecryptors: Record<string, Record<string, DecryptionAlgorithm>> = {};
|
||||
private roomDecryptors = new Map<string, Map<string, DecryptionAlgorithm>>();
|
||||
|
||||
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.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(
|
||||
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
|
||||
* 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
|
||||
*/
|
||||
public async setDeviceVerification(
|
||||
@@ -2113,6 +2117,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
verified?: boolean,
|
||||
blocked?: boolean,
|
||||
known?: boolean,
|
||||
keys?: Record<string, string>,
|
||||
): Promise<DeviceInfo | CrossSigningInfo> {
|
||||
// get rid of any `undefined`s here so we can just check
|
||||
// for null rather than null or undefined
|
||||
@@ -2131,6 +2136,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
if (!verified) {
|
||||
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) {
|
||||
this.storeTrustedSelfKeys(xsk.keys);
|
||||
@@ -2191,6 +2200,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
let verificationStatus = dev.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;
|
||||
} else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) {
|
||||
verificationStatus = DeviceVerification.UNVERIFIED;
|
||||
@@ -2400,13 +2416,6 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
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()) {
|
||||
// we got the key for this event from a source that we consider untrusted
|
||||
return null;
|
||||
@@ -2478,8 +2487,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
}
|
||||
ret.encrypted = true;
|
||||
|
||||
const forwardingChain = event.getForwardingCurve25519KeyChain();
|
||||
if (forwardingChain.length > 0 || event.isKeySourceUntrusted()) {
|
||||
if (event.isKeySourceUntrusted()) {
|
||||
// we got the key this event from somewhere else
|
||||
// TODO: check if we can trust the forwarders.
|
||||
ret.authenticated = false;
|
||||
@@ -2527,7 +2535,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
* This should not normally be necessary.
|
||||
*/
|
||||
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.forceDiscardSession === undefined) {
|
||||
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.
|
||||
// If it's called more than twice though,
|
||||
// it signals a bug on client or server.
|
||||
const existingAlg = this.roomEncryptors[roomId];
|
||||
const existingAlg = this.roomEncryptors.get(roomId);
|
||||
if (existingAlg) {
|
||||
return;
|
||||
}
|
||||
@@ -2594,7 +2602,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
storeConfigPromise = this.roomList.setRoomEncryption(roomId, config);
|
||||
}
|
||||
|
||||
const AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm];
|
||||
const AlgClass = algorithms.ENCRYPTION_CLASSES.get(config.algorithm);
|
||||
if (!AlgClass) {
|
||||
throw new Error("Unable to encrypt with " + config.algorithm);
|
||||
}
|
||||
@@ -2608,7 +2616,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
roomId,
|
||||
config,
|
||||
});
|
||||
this.roomEncryptors[roomId] = alg;
|
||||
this.roomEncryptors.set(roomId, alg);
|
||||
|
||||
if (storeConfigPromise) {
|
||||
await storeConfigPromise;
|
||||
@@ -2640,7 +2648,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
public trackRoomDevices(roomId: string): Promise<void> {
|
||||
const trackMembers = async () => {
|
||||
// not an encrypted room
|
||||
if (!this.roomEncryptors[roomId]) {
|
||||
if (!this.roomEncryptors.has(roomId)) {
|
||||
return;
|
||||
}
|
||||
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
|
||||
*/
|
||||
public prepareToEncrypt(room: Room): void {
|
||||
const alg = this.roomEncryptors[room.roomId];
|
||||
const alg = this.roomEncryptors.get(room.roomId);
|
||||
if (alg) {
|
||||
alg.prepareToEncrypt(room);
|
||||
}
|
||||
@@ -2808,7 +2816,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
|
||||
const roomId = event.getRoomId();
|
||||
|
||||
const alg = this.roomEncryptors[roomId];
|
||||
const alg = this.roomEncryptors.get(roomId);
|
||||
if (!alg) {
|
||||
// MatrixClient has already checked that this room should be encrypted,
|
||||
// so this is an unexpected situation.
|
||||
@@ -3097,7 +3105,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
private getTrackedE2eRooms(): Room[] {
|
||||
return this.clientStore.getRooms().filter((room) => {
|
||||
// check for rooms with encryption enabled
|
||||
const alg = this.roomEncryptors[room.roomId];
|
||||
const alg = this.roomEncryptors.get(room.roomId);
|
||||
if (!alg) {
|
||||
return false;
|
||||
}
|
||||
@@ -3533,7 +3541,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
|
||||
const roomId = member.roomId;
|
||||
|
||||
const alg = this.roomEncryptors[roomId];
|
||||
const alg = this.roomEncryptors.get(roomId);
|
||||
if (!alg) {
|
||||
// not encrypting in this room
|
||||
return;
|
||||
@@ -3634,11 +3642,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
` for ${roomId} / ${body.session_id} (id ${req.requestId})`);
|
||||
|
||||
if (userId !== this.userId) {
|
||||
if (!this.roomEncryptors[roomId]) {
|
||||
if (!this.roomEncryptors.get(roomId)) {
|
||||
logger.debug(`room key request for unencrypted room ${roomId}`);
|
||||
return;
|
||||
}
|
||||
const encryptor = this.roomEncryptors[roomId];
|
||||
const encryptor = this.roomEncryptors.get(roomId);
|
||||
const device = this.deviceList.getStoredDevice(userId, deviceId);
|
||||
if (!device) {
|
||||
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
|
||||
// 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}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const decryptor = this.roomDecryptors[roomId][alg];
|
||||
const decryptor = this.roomDecryptors.get(roomId).get(alg);
|
||||
if (!decryptor) {
|
||||
logger.log(`room key request for unknown alg ${alg} in room ${roomId}`);
|
||||
return;
|
||||
@@ -3745,23 +3753,24 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
* unknown
|
||||
*/
|
||||
public getRoomDecryptor(roomId: string, algorithm: string): DecryptionAlgorithm {
|
||||
let decryptors: Record<string, DecryptionAlgorithm>;
|
||||
let decryptors: Map<string, DecryptionAlgorithm>;
|
||||
let alg: DecryptionAlgorithm;
|
||||
|
||||
roomId = roomId || null;
|
||||
if (roomId) {
|
||||
decryptors = this.roomDecryptors[roomId];
|
||||
decryptors = this.roomDecryptors.get(roomId);
|
||||
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) {
|
||||
return alg;
|
||||
}
|
||||
}
|
||||
|
||||
const AlgClass = algorithms.DECRYPTION_CLASSES[algorithm];
|
||||
const AlgClass = algorithms.DECRYPTION_CLASSES.get(algorithm);
|
||||
if (!AlgClass) {
|
||||
throw new algorithms.DecryptionError(
|
||||
'UNKNOWN_ENCRYPTION_ALGORITHM',
|
||||
@@ -3777,7 +3786,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
});
|
||||
|
||||
if (decryptors) {
|
||||
decryptors[algorithm] = alg;
|
||||
decryptors.set(algorithm, alg);
|
||||
}
|
||||
return alg;
|
||||
}
|
||||
@@ -3791,9 +3800,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
*/
|
||||
private getRoomDecryptors(algorithm: string): DecryptionAlgorithm[] {
|
||||
const decryptors = [];
|
||||
for (const d of Object.values(this.roomDecryptors)) {
|
||||
if (algorithm in d) {
|
||||
decryptors.push(d[algorithm]);
|
||||
for (const d of this.roomDecryptors.values()) {
|
||||
if (d.has(algorithm)) {
|
||||
decryptors.push(d.get(algorithm));
|
||||
}
|
||||
}
|
||||
return decryptors;
|
||||
|
||||
@@ -30,6 +30,8 @@ import { logger } from '../logger';
|
||||
import { IOneTimeKey } from "./dehydration";
|
||||
import { IClaimOTKsResult, MatrixClient } from "../client";
|
||||
import { ISignatures } from "../@types/signed";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { EventType } from "../@types/event";
|
||||
|
||||
enum Algorithm {
|
||||
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.
|
||||
* @param {Uint8Array} uint8Array The data to encode.
|
||||
|
||||
@@ -25,6 +25,7 @@ import { ICrossSigningInfo } from "../CrossSigning";
|
||||
import { PrefixedLogger } from "../../logger";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { IEncryptedPayload } from "../aes";
|
||||
import { MatrixEvent } from "../../models/event";
|
||||
|
||||
/**
|
||||
* Internal module. Definitions for storage for the crypto module
|
||||
@@ -127,6 +128,8 @@ export interface CryptoStore {
|
||||
roomId: string,
|
||||
txn?: unknown,
|
||||
): Promise<[senderKey: string, sessionId: string][]>;
|
||||
addParkedSharedHistory(roomId: string, data: ParkedSharedHistory, txn?: unknown): void;
|
||||
takeParkedSharedHistory(roomId: string, txn?: unknown): Promise<ParkedSharedHistory[]>;
|
||||
|
||||
// Session key backups
|
||||
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>;
|
||||
@@ -203,3 +206,12 @@ export interface OutgoingRoomKeyRequest {
|
||||
requestBody: IRoomKeyRequestBody;
|
||||
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,
|
||||
Mode,
|
||||
OutgoingRoomKeyRequest,
|
||||
ParkedSharedHistory,
|
||||
} from "./base";
|
||||
import { IRoomKeyRequestBody } from "../index";
|
||||
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
import { InboundGroupSessionData } from "../OlmDevice";
|
||||
import { IEncryptedPayload } from "../aes";
|
||||
|
||||
export const VERSION = 10;
|
||||
const PROFILE_TRANSACTIONS = false;
|
||||
|
||||
/**
|
||||
@@ -261,7 +261,9 @@ export class Backend implements CryptoStore {
|
||||
const cursor = this.result;
|
||||
if (cursor) {
|
||||
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);
|
||||
}
|
||||
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>(
|
||||
mode: Mode,
|
||||
stores: string | string[],
|
||||
@@ -903,45 +949,34 @@ export class Backend implements CryptoStore {
|
||||
}
|
||||
}
|
||||
|
||||
export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
|
||||
logger.log(
|
||||
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
|
||||
+ ` to ${VERSION}`,
|
||||
);
|
||||
if (oldVersion < 1) { // The database did not previously exist.
|
||||
createDatabase(db);
|
||||
}
|
||||
if (oldVersion < 2) {
|
||||
db.createObjectStore("account");
|
||||
}
|
||||
if (oldVersion < 3) {
|
||||
type DbMigration = (db: IDBDatabase) => void;
|
||||
const DB_MIGRATIONS: DbMigration[] = [
|
||||
(db) => { createDatabase(db); },
|
||||
(db) => { db.createObjectStore("account"); },
|
||||
(db) => {
|
||||
const sessionsStore = db.createObjectStore("sessions", {
|
||||
keyPath: ["deviceKey", "sessionId"],
|
||||
});
|
||||
sessionsStore.createIndex("deviceKey", "deviceKey");
|
||||
}
|
||||
if (oldVersion < 4) {
|
||||
},
|
||||
(db) => {
|
||||
db.createObjectStore("inbound_group_sessions", {
|
||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||
});
|
||||
}
|
||||
if (oldVersion < 5) {
|
||||
db.createObjectStore("device_data");
|
||||
}
|
||||
if (oldVersion < 6) {
|
||||
db.createObjectStore("rooms");
|
||||
}
|
||||
if (oldVersion < 7) {
|
||||
},
|
||||
(db) => { db.createObjectStore("device_data"); },
|
||||
(db) => { db.createObjectStore("rooms"); },
|
||||
(db) => {
|
||||
db.createObjectStore("sessions_needing_backup", {
|
||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||
});
|
||||
}
|
||||
if (oldVersion < 8) {
|
||||
},
|
||||
(db) => {
|
||||
db.createObjectStore("inbound_group_sessions_withheld", {
|
||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||
});
|
||||
}
|
||||
if (oldVersion < 9) {
|
||||
},
|
||||
(db) => {
|
||||
const problemsStore = db.createObjectStore("session_problems", {
|
||||
keyPath: ["deviceKey", "time"],
|
||||
});
|
||||
@@ -950,13 +985,29 @@ export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
|
||||
db.createObjectStore("notified_error_devices", {
|
||||
keyPath: ["userId", "deviceId"],
|
||||
});
|
||||
}
|
||||
if (oldVersion < 10) {
|
||||
},
|
||||
(db) => {
|
||||
db.createObjectStore("shared_history_inbound_group_sessions", {
|
||||
keyPath: ["roomId"],
|
||||
});
|
||||
}
|
||||
},
|
||||
(db) => {
|
||||
db.createObjectStore("parked_shared_history", {
|
||||
keyPath: ["roomId"],
|
||||
});
|
||||
},
|
||||
// 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 {
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
IWithheld,
|
||||
Mode,
|
||||
OutgoingRoomKeyRequest,
|
||||
ParkedSharedHistory,
|
||||
} from "./base";
|
||||
import { IRoomKeyRequestBody } from "../index";
|
||||
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_WITHHELD = 'inbound_group_sessions_withheld';
|
||||
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_ROOMS = 'rooms';
|
||||
public static STORE_BACKUP = 'sessions_needing_backup';
|
||||
@@ -669,6 +671,27 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
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
|
||||
* that require a transaction (txn) object to be passed in may
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
IWithheld,
|
||||
Mode,
|
||||
OutgoingRoomKeyRequest,
|
||||
ParkedSharedHistory,
|
||||
} from "./base";
|
||||
import { IRoomKeyRequestBody } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
@@ -58,6 +59,7 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
private rooms: { [roomId: string]: IRoomEncryption } = {};
|
||||
private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {};
|
||||
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.
|
||||
@@ -191,11 +193,13 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
deviceId: string,
|
||||
wantedStates: number[],
|
||||
): Promise<OutgoingRoomKeyRequest[]> {
|
||||
const results = [];
|
||||
const results: OutgoingRoomKeyRequest[] = [];
|
||||
|
||||
for (const req of this.outgoingRoomKeyRequests) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -524,6 +528,18 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
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
|
||||
|
||||
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) {
|
||||
this.started = true;
|
||||
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;
|
||||
}
|
||||
@@ -310,14 +316,14 @@ export class VerificationBase<
|
||||
// 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
|
||||
// about, and ignore the rest
|
||||
const verifiedDevices = [];
|
||||
const verifiedDevices: [string, string, string][] = [];
|
||||
|
||||
for (const [keyId, keyInfo] of Object.entries(keys)) {
|
||||
const deviceId = keyId.split(':', 2)[1];
|
||||
const device = this.baseApis.getStoredDevice(userId, deviceId);
|
||||
if (device) {
|
||||
verifier(keyId, device, keyInfo);
|
||||
verifiedDevices.push(deviceId);
|
||||
verifiedDevices.push([deviceId, keyId, device.keys[keyId]]);
|
||||
} else {
|
||||
const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId);
|
||||
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
|
||||
@@ -326,7 +332,7 @@ export class VerificationBase<
|
||||
[keyId]: deviceId,
|
||||
},
|
||||
}, deviceId), keyInfo);
|
||||
verifiedDevices.push(deviceId);
|
||||
verifiedDevices.push([deviceId, keyId, deviceId]);
|
||||
} else {
|
||||
logger.warn(
|
||||
`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
|
||||
// to upload each signature in a separate API call which is silly because the
|
||||
// API supports as many signatures as you like.
|
||||
for (const deviceId of verifiedDevices) {
|
||||
await this.baseApis.setDeviceVerified(userId, deviceId);
|
||||
for (const [deviceId, keyId, key] of verifiedDevices) {
|
||||
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 {
|
||||
preventReEmit?: boolean;
|
||||
decrypt?: boolean;
|
||||
toDevice?: boolean;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
function mapper(plainOldJsObject: Partial<IEvent>) {
|
||||
if (options.toDevice) {
|
||||
delete plainOldJsObject.room_id;
|
||||
}
|
||||
|
||||
const room = client.getRoom(plainOldJsObject.room_id);
|
||||
|
||||
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 }
|
||||
*/
|
||||
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
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
EventType,
|
||||
RelationType,
|
||||
} from "./@types/event";
|
||||
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync";
|
||||
import { FilterComponent, IFilterComponent } from "./filter-component";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
|
||||
@@ -57,6 +58,8 @@ export interface IRoomEventFilter extends IFilterComponent {
|
||||
types?: Array<EventType | string>;
|
||||
related_by_senders?: Array<RelationType | string>;
|
||||
related_by_rel_types?: string[];
|
||||
unread_thread_notifications?: boolean;
|
||||
"org.matrix.msc3773.unread_thread_notifications"?: boolean;
|
||||
|
||||
// Unstable values
|
||||
"io.element.relation_senders"?: Array<RelationType | string>;
|
||||
@@ -97,7 +100,7 @@ export class Filter {
|
||||
* @param {Object} jsonObj
|
||||
* @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);
|
||||
filter.setDefinition(jsonObj);
|
||||
return filter;
|
||||
@@ -107,7 +110,7 @@ export class Filter {
|
||||
private roomFilter: 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)
|
||||
@@ -220,7 +223,24 @@ export class Filter {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -365,7 +365,7 @@ export class MatrixHttpApi {
|
||||
// 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
|
||||
// way, we have to JSON-parse the response ourselves.
|
||||
let bodyParser = null;
|
||||
let bodyParser: ((body: string) => any) | undefined;
|
||||
if (!rawResponse) {
|
||||
bodyParser = function(rawBody: string) {
|
||||
let body = JSON.parse(rawBody);
|
||||
@@ -472,7 +472,7 @@ export class MatrixHttpApi {
|
||||
headers["Content-Length"] = "0";
|
||||
}
|
||||
|
||||
promise = this.authedRequest(
|
||||
promise = this.authedRequest<UploadContentResponseType<O>>(
|
||||
opts.callback, Method.Post, "/upload", queryParams, body, {
|
||||
prefix: "/_matrix/media/r0",
|
||||
headers,
|
||||
@@ -590,10 +590,10 @@ export class MatrixHttpApi {
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
public authedRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||
callback: Callback<T>,
|
||||
callback: Callback<T> | undefined,
|
||||
method: Method,
|
||||
path: string,
|
||||
queryParams?: Record<string, string | string[]>,
|
||||
queryParams?: Record<string, string | string[] | undefined>,
|
||||
data?: CoreOptions["body"],
|
||||
opts?: O | number, // number is legacy
|
||||
): IAbortablePromise<ResponseType<T, O>> {
|
||||
@@ -667,7 +667,7 @@ export class MatrixHttpApi {
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
public request<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||
callback: Callback<T>,
|
||||
callback: Callback<T> | undefined,
|
||||
method: Method,
|
||||
path: string,
|
||||
queryParams?: CoreOptions["qs"],
|
||||
@@ -711,7 +711,7 @@ export class MatrixHttpApi {
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
public requestOtherUrl<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||
callback: Callback<T>,
|
||||
callback: Callback<T> | undefined,
|
||||
method: Method,
|
||||
uri: string,
|
||||
queryParams?: CoreOptions["qs"],
|
||||
@@ -778,7 +778,7 @@ export class MatrixHttpApi {
|
||||
* Generic O should be inferred
|
||||
*/
|
||||
private doRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
|
||||
callback: Callback<T>,
|
||||
callback: Callback<T> | undefined,
|
||||
method: Method,
|
||||
uri: string,
|
||||
queryParams?: Record<string, string>,
|
||||
|
||||
@@ -20,7 +20,7 @@ import { IContent, MatrixEvent } from "./event";
|
||||
import { MSC3089TreeSpace } from "./MSC3089TreeSpace";
|
||||
import { EventTimeline } from "./event-timeline";
|
||||
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
|
||||
|
||||
@@ -15,7 +15,6 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { MBeaconEventContent } from "../@types/beacon";
|
||||
import { M_TIMESTAMP } from "../@types/location";
|
||||
import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers";
|
||||
import { MatrixEvent } from "../matrix";
|
||||
import { sortEventsByLatestContentTimestamp } from "../utils";
|
||||
@@ -161,7 +160,9 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
|
||||
|
||||
const validLocationEvents = beaconLocationEvents.filter(event => {
|
||||
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 (
|
||||
// only include positions that were taken inside the beacon's live period
|
||||
isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) &&
|
||||
|
||||
@@ -86,7 +86,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
private readonly displayPendingEvents: boolean;
|
||||
private liveTimeline: EventTimeline;
|
||||
private timelines: EventTimeline[];
|
||||
private _eventIdToTimeline: Record<string, EventTimeline>;
|
||||
private _eventIdToTimeline = new Map<string, EventTimeline>();
|
||||
private filter?: Filter;
|
||||
|
||||
/**
|
||||
@@ -123,12 +123,15 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
* @param {MatrixClient=} client the Matrix client which owns this EventTimelineSet,
|
||||
* can be omitted if room is specified.
|
||||
* @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(
|
||||
public readonly room: Room | undefined,
|
||||
opts: IOpts = {},
|
||||
client?: MatrixClient,
|
||||
public readonly thread?: Thread,
|
||||
public readonly isThreadTimeline: boolean = false,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -138,7 +141,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
|
||||
// just a list - *not* ordered.
|
||||
this.timelines = [this.liveTimeline];
|
||||
this._eventIdToTimeline = {};
|
||||
this._eventIdToTimeline = new Map<string, EventTimeline>();
|
||||
|
||||
this.filter = opts.filter;
|
||||
|
||||
@@ -210,7 +213,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
* @return {module:models/event-timeline~EventTimeline} timeline
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public replaceEventId(oldEventId: string, newEventId: string): void {
|
||||
const existingTimeline = this._eventIdToTimeline[oldEventId];
|
||||
const existingTimeline = this._eventIdToTimeline.get(oldEventId);
|
||||
if (existingTimeline) {
|
||||
delete this._eventIdToTimeline[oldEventId];
|
||||
this._eventIdToTimeline[newEventId] = existingTimeline;
|
||||
this._eventIdToTimeline.delete(oldEventId);
|
||||
this._eventIdToTimeline.set(newEventId, existingTimeline);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +260,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
|
||||
if (resetAllTimelines) {
|
||||
this.timelines = [newTimeline];
|
||||
this._eventIdToTimeline = {};
|
||||
this._eventIdToTimeline = new Map<string, EventTimeline>();
|
||||
} else {
|
||||
this.timelines.push(newTimeline);
|
||||
}
|
||||
@@ -287,8 +290,9 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
* @return {?module:models/event-timeline~EventTimeline} timeline containing
|
||||
* the given event, or null if unknown
|
||||
*/
|
||||
public getTimelineForEvent(eventId: string): EventTimeline | null {
|
||||
const res = this._eventIdToTimeline[eventId];
|
||||
public getTimelineForEvent(eventId: string | null): EventTimeline | null {
|
||||
if (eventId === null) { return null; }
|
||||
const res = this._eventIdToTimeline.get(eventId);
|
||||
return (res === undefined) ? null : res;
|
||||
}
|
||||
|
||||
@@ -450,7 +454,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
const event = events[i];
|
||||
const eventId = event.getId();
|
||||
|
||||
const existingTimeline = this._eventIdToTimeline[eventId];
|
||||
const existingTimeline = this._eventIdToTimeline.get(eventId);
|
||||
|
||||
if (!existingTimeline) {
|
||||
// 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 (duplicateStrategy === DuplicateStrategy.Replace) {
|
||||
debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " + event.getId());
|
||||
@@ -697,7 +701,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
roomState,
|
||||
timelineWasEmpty,
|
||||
});
|
||||
this._eventIdToTimeline[eventId] = timeline;
|
||||
this._eventIdToTimeline.set(eventId, timeline);
|
||||
|
||||
this.relations.aggregateParentEvent(event);
|
||||
this.relations.aggregateChildEvent(event, this);
|
||||
@@ -725,22 +729,14 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
newEventId: string,
|
||||
): void {
|
||||
// XXX: why don't we infer newEventId from localEvent?
|
||||
const existingTimeline = this._eventIdToTimeline[oldEventId];
|
||||
const existingTimeline = this._eventIdToTimeline.get(oldEventId);
|
||||
if (existingTimeline) {
|
||||
delete this._eventIdToTimeline[oldEventId];
|
||||
this._eventIdToTimeline[newEventId] = existingTimeline;
|
||||
} else {
|
||||
if (this.filter) {
|
||||
if (this.filter.filterRoomTimeline([localEvent]).length) {
|
||||
this.addEventToTimeline(localEvent, this.liveTimeline, {
|
||||
toStartOfTimeline: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.addEventToTimeline(localEvent, this.liveTimeline, {
|
||||
toStartOfTimeline: false,
|
||||
});
|
||||
}
|
||||
this._eventIdToTimeline.delete(oldEventId);
|
||||
this._eventIdToTimeline.set(newEventId, existingTimeline);
|
||||
} else if (!this.filter || this.filter.filterRoomTimeline([localEvent]).length) {
|
||||
this.addEventToTimeline(localEvent, this.liveTimeline, {
|
||||
toStartOfTimeline: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -753,14 +749,14 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
* in this room.
|
||||
*/
|
||||
public removeEvent(eventId: string): MatrixEvent | null {
|
||||
const timeline = this._eventIdToTimeline[eventId];
|
||||
const timeline = this._eventIdToTimeline.get(eventId);
|
||||
if (!timeline) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const removed = timeline.removeEvent(eventId);
|
||||
if (removed) {
|
||||
delete this._eventIdToTimeline[eventId];
|
||||
this._eventIdToTimeline.delete(eventId);
|
||||
const data = {
|
||||
timeline: timeline,
|
||||
};
|
||||
@@ -787,8 +783,8 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
return 0;
|
||||
}
|
||||
|
||||
const timeline1 = this._eventIdToTimeline[eventId1];
|
||||
const timeline2 = this._eventIdToTimeline[eventId2];
|
||||
const timeline1 = this._eventIdToTimeline.get(eventId1);
|
||||
const timeline2 = this._eventIdToTimeline.get(eventId2);
|
||||
|
||||
if (timeline1 === undefined) {
|
||||
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