1
0
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:
Kegan Dougal
2022-10-12 17:22:52 +01:00
120 changed files with 8816 additions and 3888 deletions

View File

@@ -1,14 +1,22 @@
module.exports = { module.exports = {
plugins: [ plugins: [
"matrix-org", "matrix-org",
"import",
], ],
extends: [ extends: [
"plugin:matrix-org/babel", "plugin:matrix-org/babel",
"plugin:import/typescript",
], ],
env: { env: {
browser: true, browser: true,
node: true, node: true,
}, },
settings: {
"import/resolver": {
typescript: true,
node: true,
},
},
// NOTE: These rules are frozen and new rules should not be added here. // NOTE: These rules are frozen and new rules should not be added here.
// New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/ // New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
rules: { rules: {
@@ -35,7 +43,19 @@ module.exports = {
"no-console": "error", "no-console": "error",
// restrict EventEmitters to force callers to use TypedEventEmitter // restrict EventEmitters to force callers to use TypedEventEmitter
"no-restricted-imports": ["error", "events"], "no-restricted-imports": ["error", {
name: "events",
message: "Please use TypedEventEmitter instead"
}],
"import/no-restricted-paths": ["error", {
"zones": [{
"target": "./src/",
"from": "./src/index.ts",
"message": "The package index is dynamic between src and lib depending on " +
"whether release or development, target the specific module or matrix.ts instead",
}],
}],
}, },
overrides: [{ overrides: [{
files: [ files: [

View File

@@ -25,6 +25,6 @@ jobs:
steps: steps:
- uses: tibdex/backport@v2 - uses: tibdex/backport@v2
with: with:
labels_template: "<%= JSON.stringify(labels) %>" labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
# We can't use GITHUB_TOKEN here or CI won't run on the new PR # We can't use GITHUB_TOKEN here or CI won't run on the new PR
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }} github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}

41
.github/workflows/release-npm.yml vendored Normal file
View 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 }}

View File

@@ -24,7 +24,6 @@ jobs:
- name: 📋 Copy to temp - name: 📋 Copy to temp
run: | run: |
ls -lah
tag="${{ github.ref_name }}" tag="${{ github.ref_name }}"
version="${tag#v}" version="${tag#v}"
echo "VERSION=$version" >> $GITHUB_ENV echo "VERSION=$version" >> $GITHUB_ENV
@@ -51,3 +50,9 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
keep_files: true keep_files: true
publish_dir: . publish_dir: .
npm:
name: Publish
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -10,7 +10,18 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success' if: github.event.workflow_run.conclusion == 'success'
steps: steps:
# We create the status here and then update it to success/failure in the `report` stage
# This provides an easy link to this workflow_run from the PR before Cypress is done.
- uses: Sibz/github-status-action@v1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🩻 SonarCloud Scan" - name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v2.2 uses: matrix-org/sonarcloud-workflow-action@v2.2
with: with:
repository: ${{ github.event.workflow_run.head_repository.full_name }} repository: ${{ github.event.workflow_run.head_repository.full_name }}
@@ -22,3 +33,13 @@ jobs:
coverage_run_id: ${{ github.event.workflow_run.id }} coverage_run_id: ${{ github.event.workflow_run.id }}
coverage_workflow_name: tests.yml coverage_workflow_name: tests.yml
coverage_extract_path: coverage coverage_extract_path: coverage
- uses: Sibz/github-status-action@v1
if: always()
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: ${{ steps.sonarcloud.outcome == 'success' && 'success' || 'failure' }}
context: ${{ github.workflow }} / SonarCloud (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}

View File

@@ -23,6 +23,16 @@ jobs:
- name: Typecheck - name: Typecheck
run: "yarn run lint:types" run: "yarn run lint:types"
- name: Switch js-sdk to release mode
run: |
scripts/switch_package_to_release.js
yarn install
yarn run build:compile
yarn run build:types
- name: Typecheck (release mode)
run: "yarn run lint:types"
js_lint: js_lint:
name: "ESLint" name: "ESLint"
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -73,7 +83,7 @@ jobs:
- name: Detecting files changed - name: Detecting files changed
id: files id: files
uses: futuratrepadeira/changed-files@v3.2.1 uses: futuratrepadeira/changed-files@v4.0.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
pattern: '^.*\.tsx?$' pattern: '^.*\.tsx?$'
@@ -81,7 +91,7 @@ jobs:
- uses: t3chguy/typescript-check-action@main - uses: t3chguy/typescript-check-action@main
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
use-check: true use-check: false
check-fail-mode: added check-fail-mode: added
output-behaviour: annotate output-behaviour: annotate
ts-extra-args: '--strict' ts-extra-args: '--strict'

View File

@@ -1,3 +1,89 @@
Changes in [20.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.1.0) (2022-10-11)
============================================================================================================
## ✨ Features
* Add local notification settings capability ([\#2700](https://github.com/matrix-org/matrix-js-sdk/pull/2700)).
* Implementation of MSC3882 login token request ([\#2687](https://github.com/matrix-org/matrix-js-sdk/pull/2687)). Contributed by @hughns.
* Typings for MSC2965 OIDC provider discovery ([\#2424](https://github.com/matrix-org/matrix-js-sdk/pull/2424)). Contributed by @hughns.
* Support to remotely toggle push notifications ([\#2686](https://github.com/matrix-org/matrix-js-sdk/pull/2686)).
* Read receipts for threads ([\#2635](https://github.com/matrix-org/matrix-js-sdk/pull/2635)).
## 🐛 Bug Fixes
* Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374.
* Unexpected ignored self key request when it's not shared history ([\#2724](https://github.com/matrix-org/matrix-js-sdk/pull/2724)). Contributed by @mcalinghee.
* Fix IDB initial migration handling causing spurious lazy loading upgrade loops ([\#2718](https://github.com/matrix-org/matrix-js-sdk/pull/2718)). Fixes vector-im/element-web#23377.
* Fix backpagination at end logic being spec non-conforming ([\#2680](https://github.com/matrix-org/matrix-js-sdk/pull/2680)). Fixes vector-im/element-web#22784.
Changes in [20.0.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.2) (2022-09-30)
==================================================================================================
## 🐛 Bug Fixes
* Fix issue in sync when crypto is not supported by client ([\#2715](https://github.com/matrix-org/matrix-js-sdk/pull/2715)). Contributed by @stas-demydiuk.
Changes in [20.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.1) (2022-09-28)
==================================================================================================
## 🐛 Bug Fixes
* Fix missing return when receiving an invitation without shared history ([\#2710](https://github.com/matrix-org/matrix-js-sdk/pull/2710)).
Changes in [20.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v20.0.0) (2022-09-28)
==================================================================================================
## 🚨 BREAKING CHANGES
* Bump IDB crypto store version ([\#2705](https://github.com/matrix-org/matrix-js-sdk/pull/2705)).
Changes in [19.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.7.0) (2022-09-28)
==================================================================================================
## 🔒 Security
* Fix for [CVE-2022-39249](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39249)
* Fix for [CVE-2022-39250](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39250)
* Fix for [CVE-2022-39251](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39251)
* Fix for [CVE-2022-39236](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39236)
Changes in [19.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.6.0) (2022-09-27)
==================================================================================================
## ✨ Features
* Add a property aggregating all names of a NamespacedValue ([\#2656](https://github.com/matrix-org/matrix-js-sdk/pull/2656)).
* Implementation of MSC3824 to add action= param on SSO login ([\#2398](https://github.com/matrix-org/matrix-js-sdk/pull/2398)). Contributed by @hughns.
* Add invited_count and joined_count to sliding sync room responses. ([\#2628](https://github.com/matrix-org/matrix-js-sdk/pull/2628)).
* Base support for MSC3847: Ignore invites with policy rooms ([\#2626](https://github.com/matrix-org/matrix-js-sdk/pull/2626)). Contributed by @Yoric.
## 🐛 Bug Fixes
* Fix handling of remote echoes doubling up ([\#2639](https://github.com/matrix-org/matrix-js-sdk/pull/2639)). Fixes #2618.
Changes in [19.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.5.0) (2022-09-13)
==================================================================================================
## 🐛 Bug Fixes
* Fix bug in deepCompare which would incorrectly return objects with disjoint keys as equal ([\#2586](https://github.com/matrix-org/matrix-js-sdk/pull/2586)). Contributed by @3nprob.
* Refactor Sync and fix `initialSyncLimit` ([\#2587](https://github.com/matrix-org/matrix-js-sdk/pull/2587)).
* Use deep equality comparisons when searching for outgoing key requests by target ([\#2623](https://github.com/matrix-org/matrix-js-sdk/pull/2623)). Contributed by @duxovni.
* Fix room membership race with PREPARED event ([\#2613](https://github.com/matrix-org/matrix-js-sdk/pull/2613)). Contributed by @jotto.
Changes in [19.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.4.0) (2022-08-31)
==================================================================================================
## 🔒 Security
* Fix for [CVE-2022-36059](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D36059)
Find more details at https://matrix.org/blog/2022/08/31/security-releases-matrix-js-sdk-19-4-0-and-matrix-react-sdk-3-53-0
## ✨ Features
* Re-emit room state events on rooms ([\#2607](https://github.com/matrix-org/matrix-js-sdk/pull/2607)).
* Add ability to override built in room name generator for an i18n'able one ([\#2609](https://github.com/matrix-org/matrix-js-sdk/pull/2609)).
* Add txn_id support to sliding sync ([\#2567](https://github.com/matrix-org/matrix-js-sdk/pull/2567)).
## 🐛 Bug Fixes
* Refactor Sync and fix `initialSyncLimit` ([\#2587](https://github.com/matrix-org/matrix-js-sdk/pull/2587)).
* Use deep equality comparisons when searching for outgoing key requests by target ([\#2623](https://github.com/matrix-org/matrix-js-sdk/pull/2623)). Contributed by @duxovni.
* Fix room membership race with PREPARED event ([\#2613](https://github.com/matrix-org/matrix-js-sdk/pull/2613)). Contributed by @jotto.
* fixed a sliding sync bug which could cause the `roomIndexToRoomId` map to be incorrect when a new room is added in the middle of the list or when an existing room is deleted from the middle of the list. ([\#2610](https://github.com/matrix-org/matrix-js-sdk/pull/2610)).
* Fix: Handle parsing of a beacon info event without asset ([\#2591](https://github.com/matrix-org/matrix-js-sdk/pull/2591)). Fixes vector-im/element-web#23078. Contributed by @kerryarchibald.
* Fix finding event read up to if stable private read receipts is missing ([\#2585](https://github.com/matrix-org/matrix-js-sdk/pull/2585)). Fixes vector-im/element-web#23027.
* fixed a sliding sync issue where history could be interpreted as live events. ([\#2583](https://github.com/matrix-org/matrix-js-sdk/pull/2583)).
Changes in [19.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.3.0) (2022-08-16) Changes in [19.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.3.0) (2022-08-16)
================================================================================================== ==================================================================================================

View File

@@ -1,284 +1,5 @@
Contributing code to matrix-js-sdk Contributing code to matrix-js-sdk
================================== ==================================
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are matrix-js-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md
willing to license their contributions under the same license as the project
itself. We follow a simple 'inbound=outbound' model for contributions: the act
of submitting an 'inbound' contribution means that the contributor agrees to
license the code under the same terms as the project's overall 'outbound'
license - in this case, Apache Software License v2 (see
[LICENSE](LICENSE)).
How to contribute
-----------------
The preferred and easiest way to contribute changes to the project is to fork
it on github, and then create a pull request to ask us to pull your changes
into our repo (https://help.github.com/articles/using-pull-requests/)
We use GitHub's pull request workflow to review the contribution, and either
ask you to make any refinements needed or merge it and make them ourselves.
Things that should go into your PR description:
* A changelog entry in the `Notes` section (see below)
* References to any bugs fixed by the change (in GitHub's `Fixes` notation)
* Describe the why and what is changing in the PR description so it's easy for
onlookers and reviewers to onboard and context switch. This information is
also helpful when we come back to look at this in 6 months and ask "why did
we do it like that?" we have a chance of finding out.
* Why didn't it work before? Why does it work now? What use cases does it
unlock?
* If you find yourself adding information on how the code works or why you
chose to do it the way you did, make sure this information is instead
written as comments in the code itself.
* Sometimes a PR can change considerably as it is developed. In this case,
the description should be updated to reflect the most recent state of
the PR. (It can be helpful to retain the old content under a suitable
heading, for additional context.)
* Include both **before** and **after** screenshots to easily compare and discuss
what's changing.
* Include a step-by-step testing strategy so that a reviewer can check out the
code locally and easily get to the point of testing your change.
* Add comments to the diff for the reviewer that might help them to understand
why the change is necessary or how they might better understand and review it.
We rely on information in pull request to populate the information that goes
into the changelogs our users see, both for the JS SDK itself and also for some
projects based on it. This is picked up from both labels on the pull request and
the `Notes:` annotation in the description. By default, the PR title will be
used for the changelog entry, but you can specify more options, as follows.
To add a longer, more detailed description of the change for the changelog:
*Fix llama herding bug*
```
Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase
```
For some PRs, it's not useful to have an entry in the user-facing changelog (this is
the default for PRs labelled with `T-Task`):
*Remove outdated comment from `Ungulates.ts`*
```
Notes: none
```
Sometimes, you're fixing a bug in a downstream project, in which case you want
an entry in that project's changelog. You can do that too:
*Fix another herding bug*
```
Notes: Fix a bug where the `herd()` function would only work on Tuesdays
element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays
```
This example is for Element Web. You can specify:
* matrix-react-sdk
* element-web
* element-desktop
If your PR introduces a breaking change, use the `Notes` section in the same
way, additionally adding the `X-Breaking-Change` label (see below). There's no need
to specify in the notes that it's a breaking change - this will be added
automatically based on the label - but remember to tell the developer how to
migrate:
*Remove legacy class*
```
Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead.
```
Other metadata can be added using labels.
* `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a *major* version bump.
* `T-Enhancement`: A new feature - adding this label will mean the change causes a *minor* version bump.
* `T-Defect`: A bug fix (in either code or docs).
* `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one.
If you don't have permission to add labels, your PR reviewer(s) can work with you
to add them: ask in the PR description or comments.
We use continuous integration, and all pull requests get automatically tested:
if your change breaks the build, then the PR will show that there are failed
checks, so please check back after a few minutes.
Tests
-----
Your PR should include tests.
For new user facing features in `matrix-react-sdk` or `element-web`, you
must include:
1. Comprehensive unit tests written in Jest. These are located in `/test`.
2. "happy path" end-to-end tests.
These are located in `/test/end-to-end-tests` in `matrix-react-sdk`, and
are run using `element-web`. Ideally, you would also include tests for edge
and error cases.
Unit tests are expected even when the feature is in labs. It's good practice
to write tests alongside the code as it ensures the code is testable from
the start, and gives you a fast feedback loop while you're developing the
functionality. End-to-end tests should be added prior to the feature
leaving labs, but don't have to be present from the start (although it might
be beneficial to have some running early, so you can test things faster).
For bugs in those repos, your change must include at least one unit test or
end-to-end test; which is best depends on what sort of test most concisely
exercises the area.
Changes to `matrix-js-sdk` must be accompanied by unit tests written in Jest.
These are located in `/spec/`.
When writing unit tests, please aim for a high level of test coverage
for new code - 80% or greater. If you cannot achieve that, please document
why it's not possible in your PR.
Some sections of code are not sensible to add coverage for, such as those
which explicitly inhibit noisy logging for tests. Which can be hidden using
an istanbul magic comment as [documented here][1]. See example:
```javascript
/* istanbul ignore if */
if (process.env.NODE_ENV !== "test") {
logger.error("Log line that is noisy enough in tests to want to skip");
}
```
Tests validate that your change works as intended and also document
concisely what is being changed. Ideally, your new tests fail
prior to your change, and succeed once it has been applied. You may
find this simpler to achieve if you write the tests first.
If you're spiking some code that's experimental and not being used to support
production features, exceptions can be made to requirements for tests.
Note that tests will still be required in order to ship the feature, and it's
strongly encouraged to think about tests early in the process, as adding
tests later will become progressively more difficult.
If you're not sure how to approach writing tests for your change, ask for help
in [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
Code style
----------
The js-sdk aims to target TypeScript/ES6. All new files should be written in
TypeScript and existing files should use ES6 principles where possible.
Members should not be exported as a default export in general - it causes problems
with the architecture of the SDK (index file becomes less clear) and could
introduce naming problems (as default exports get aliased upon import). In
general, avoid using `export default`.
The remaining code-style for matrix-js-sdk is not formally documented, but
contributors are encouraged to read the
[code style document for matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md)
and follow the principles set out there.
Please ensure your changes match the cosmetic style of the existing project,
and ***never*** mix cosmetic and functional changes in the same commit, as it
makes it horribly hard to review otherwise.
Attribution
-----------
Everyone who contributes anything to Matrix is welcome to be listed in the
AUTHORS.rst file for the project in question. Please feel free to include a
change to AUTHORS.rst in your pull request to list yourself and a short
description of the area(s) you've worked on. Also, we sometimes have swag to
give away to contributors - if you feel that Matrix-branded apparel is missing
from your life, please mail us your shipping address to matrix at matrix.org
and we'll try to fix it :)
Sign off
--------
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've
adopted the same lightweight approach that the Linux Kernel
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO (Developer Certificate of Origin:
http://developercertificate.org/). This is a simple declaration that you wrote
the contribution or otherwise have the right to contribute it to Matrix:
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
If you agree to this for your contribution, then all that's needed is to
include the line in your commit or pull request comment:
```
Signed-off-by: Your Name <your@email.example.org>
```
We accept contributions under a legally identifiable name, such as your name on
government documentation or common-law names (names claimed by legitimate usage
or repute). Unfortunately, we cannot accept anonymous contributions at this
time.
Git allows you to add this signoff automatically when using the `-s` flag to
`git commit`, which uses the name and email set in your `user.name` and
`user.email` git configs.
If you forgot to sign off your commits before making your pull request and are
on Git 2.17+ you can mass signoff using rebase:
```
git rebase --signoff origin/develop
```
Review expectations
===================
See https://github.com/vector-im/element-meta/wiki/Review-process
Merge Strategy
==============
The preferred method for merging pull requests is squash merging to keep the
commit history trim, but it is up to the discretion of the team member merging
the change. We do not support rebase merges due to `allchange` being unable to
handle them. When merging make sure to leave the default commit title, or
at least leave the PR number at the end in brackets like by default.
When stacking pull requests, you may wish to do the following:
1. Branch from develop to your branch (branch1), push commits onto it and open a pull request
2. Branch from your base branch (branch1) to your work branch (branch2), push commits and open a pull request configuring the base to be branch1, saying in the description that it is based on your other PR.
3. Merge the first PR using a merge commit otherwise your stacked PR will need a rebase. Github will automatically adjust the base branch of your other PR to be develop.
[1]: https://github.com/gotwarlost/istanbul/blob/master/ignoring-code-for-coverage.md

View File

@@ -1,6 +1,6 @@
{ {
"name": "matrix-js-sdk", "name": "matrix-js-sdk",
"version": "19.3.0", "version": "20.1.0",
"description": "Matrix Client-Server SDK for Javascript", "description": "Matrix Client-Server SDK for Javascript",
"engines": { "engines": {
"node": ">=12.9.0" "node": ">=12.9.0"
@@ -78,28 +78,30 @@
"@babel/preset-env": "^7.12.11", "@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7", "@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10", "@babel/register": "^7.12.10",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz", "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz",
"@types/bs58": "^4.0.1", "@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5", "@types/content-type": "^1.1.5",
"@types/jest": "^28.0.0", "@types/jest": "^29.0.0",
"@types/node": "16", "@types/node": "16",
"@types/request": "^2.48.5", "@types/request": "^2.48.5",
"@typescript-eslint/eslint-plugin": "^5.6.0", "@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0", "@typescript-eslint/parser": "^5.6.0",
"allchange": "^1.0.6", "allchange": "^1.0.6",
"babel-jest": "^28.0.0", "babel-jest": "^29.0.0",
"babelify": "^10.0.0", "babelify": "^10.0.0",
"better-docs": "^2.4.0-beta.9", "better-docs": "^2.4.0-beta.9",
"browserify": "^17.0.0", "browserify": "^17.0.0",
"docdash": "^1.2.0", "docdash": "^1.2.0",
"eslint": "8.22.0", "eslint": "8.24.0",
"eslint-config-google": "^0.14.0", "eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.25.4", "eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-matrix-org": "^0.6.0", "eslint-plugin-matrix-org": "^0.6.0",
"exorcist": "^2.0.0", "exorcist": "^2.0.0",
"fake-indexeddb": "^4.0.0", "fake-indexeddb": "^4.0.0",
"jest": "^28.0.0", "jest": "^29.0.0",
"jest-localstorage-mock": "^2.4.6", "jest-localstorage-mock": "^2.4.6",
"jest-mock": "^27.5.1",
"jest-sonar-reporter": "^2.0.0", "jest-sonar-reporter": "^2.0.0",
"jsdoc": "^3.6.6", "jsdoc": "^3.6.6",
"matrix-mock-request": "^2.1.2", "matrix-mock-request": "^2.1.2",

37
post-release.sh Executable file
View 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

View File

@@ -3,19 +3,16 @@
# Script to perform a release of matrix-js-sdk and downstream projects. # Script to perform a release of matrix-js-sdk and downstream projects.
# #
# Requires: # Requires:
# github-changelog-generator; install via:
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/) # jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9 # hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
# npm; typically installed by Node.js
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/) # yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
# #
# Note: this script is also used to release matrix-react-sdk and element-web. # Note: this script is also used to release matrix-react-sdk, element-web, and element-desktop.
set -e set -e
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$) jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
if [[ `command -v hub` ]] && [[ `hub --version` =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then if [[ $(command -v hub) ]] && [[ $(hub --version) =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
HUB_VERSION_MAJOR=${BASH_REMATCH[1]} HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
HUB_VERSION_MINOR=${BASH_REMATCH[2]} HUB_VERSION_MINOR=${BASH_REMATCH[2]}
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
@@ -26,7 +23,6 @@ else
echo "hub is required: please install it" echo "hub is required: please install it"
exit exit
fi fi
npm --version > /dev/null || (echo "npm is required: please install it"; kill $$)
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$) yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
USAGE="$0 [-x] [-c changelog_file] vX.Y.Z" USAGE="$0 [-x] [-c changelog_file] vX.Y.Z"
@@ -37,17 +33,9 @@ $USAGE
-c changelog_file: specify name of file containing changelog -c changelog_file: specify name of file containing changelog
-x: skip updating the changelog -x: skip updating the changelog
-n: skip publish to NPM
EOF EOF
} }
ret=0
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
if [ "$ret" -eq 0 ]; then
echo "package.json contains develop dependencies. Refusing to release."
exit
fi
if ! git diff-index --quiet --cached HEAD; then if ! git diff-index --quiet --cached HEAD; then
echo "this git checkout has staged (uncommitted) changes. Refusing to release." echo "this git checkout has staged (uncommitted) changes. Refusing to release."
exit exit
@@ -59,10 +47,8 @@ if ! git diff-files --quiet; then
fi fi
skip_changelog= skip_changelog=
skip_npm=
changelog_file="CHANGELOG.md" changelog_file="CHANGELOG.md"
expected_npm_user="matrixdotorg" while getopts hc:x f; do
while getopts hc:u:xzn f; do
case $f in case $f in
h) h)
help help
@@ -74,21 +60,70 @@ while getopts hc:u:xzn f; do
x) x)
skip_changelog=1 skip_changelog=1
;; ;;
n)
skip_npm=1
;;
u)
expected_npm_user="$OPTARG"
;;
esac esac
done done
shift `expr $OPTIND - 1` shift $(expr $OPTIND - 1)
if [ $# -ne 1 ]; then if [ $# -ne 1 ]; then
echo "Usage: $USAGE" >&2 echo "Usage: $USAGE" >&2
exit 1 exit 1
fi fi
function check_dependency {
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
if [ "$depver" == "null" ]; then return 0; fi
echo "Checking version of $1..."
local latestver=$(yarn info -s "$1" dist-tags.next)
if [ "$depver" != "$latestver" ]
then
echo "The latest version of $1 is $latestver but package.json depends on $depver."
echo -n "Type 'u' to auto-upgrade, 'c' to continue anyway, or 'a' to abort:"
read resp
if [ "$resp" != "u" ] && [ "$resp" != "c" ]
then
echo "Aborting."
exit 1
fi
if [ "$resp" == "u" ]
then
echo "Upgrading $1 to $latestver..."
yarn add -E "$1@$latestver"
git add -u
git commit -m "Upgrade $1 to $latestver"
fi
fi
}
function reset_dependency {
local depver=$(cat package.json | jq -r .dependencies[\"$1\"])
if [ "$depver" == "null" ]; then return 0; fi
echo "Resetting $1 to develop branch..."
yarn add "github:matrix-org/$1#develop"
git add -u
git commit -m "Reset $1 back to develop branch"
}
has_subprojects=0
if [ -f release_config.yaml ]; then
subprojects=$(cat release_config.yaml | python -c "import yaml; import sys; print(' '.join(list(yaml.load(sys.stdin)['subprojects'].keys())))" 2> /dev/null)
if [ "$?" -eq 0 ]; then
has_subprojects=1
echo "Checking subprojects for upgrades"
for proj in $subprojects; do
check_dependency "$proj"
done
fi
fi
ret=0
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
if [ "$ret" -eq 0 ]; then
echo "package.json contains develop dependencies. Refusing to release."
exit
fi
# We use Git branch / commit dependencies for some packages, and Yarn seems # We use Git branch / commit dependencies for some packages, and Yarn seems
# to have a hard time getting that right. See also # to have a hard time getting that right. See also
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the # https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
@@ -97,20 +132,9 @@ yarn cache clean
# Ensure all dependencies are updated # Ensure all dependencies are updated
yarn install --ignore-scripts --pure-lockfile yarn install --ignore-scripts --pure-lockfile
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
if [ -z "$skip_npm" ]; then
actual_npm_user=`npm whoami`;
if [ $expected_npm_user != $actual_npm_user ]; then
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
exit 1
fi
fi
# ignore leading v on release # ignore leading v on release
release="${1#v}" release="${1#v}"
tag="v${release}" tag="v${release}"
rel_branch="release-$tag"
prerelease=0 prerelease=0
# We check if this build is a prerelease by looking to # We check if this build is a prerelease by looking to
@@ -125,18 +149,7 @@ else
read -p "Making a FINAL RELEASE, press enter to continue " REPLY read -p "Making a FINAL RELEASE, press enter to continue " REPLY
fi fi
# We might already be on the release branch, in which case, yay rel_branch=$(git symbolic-ref --short HEAD)
# If we're on any branch starting with 'release', or the staging branch
# we don't create a separate release branch (this allows us to use the same
# release branch for releases and release candidates).
curbranch=$(git symbolic-ref --short HEAD)
if [[ "$curbranch" != release* && "$curbranch" != "staging" ]]; then
echo "Creating release branch"
git checkout -b "$rel_branch"
else
echo "Using current branch ($curbranch) for release"
rel_branch=$curbranch
fi
if [ -z "$skip_changelog" ]; then if [ -z "$skip_changelog" ]; then
echo "Generating changelog" echo "Generating changelog"
@@ -148,8 +161,8 @@ if [ -z "$skip_changelog" ]; then
git commit "$changelog_file" -m "Prepare changelog for $tag" git commit "$changelog_file" -m "Prepare changelog for $tag"
fi fi
fi fi
latest_changes=`mktemp` latest_changes=$(mktemp)
cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_changes}" cat "${changelog_file}" | "$(dirname "$0")/scripts/changelog_head.py" > "${latest_changes}"
set -x set -x
@@ -176,7 +189,7 @@ do
done done
# commit yarn.lock if it exists, is versioned, and is modified # commit yarn.lock if it exists, is versioned, and is modified
if [[ -f yarn.lock && `git status --porcelain yarn.lock | grep '^ M'` ]]; if [[ -f yarn.lock && $(git status --porcelain yarn.lock | grep '^ M') ]];
then then
pkglock='yarn.lock' pkglock='yarn.lock'
else else
@@ -188,7 +201,7 @@ git commit package.json $pkglock -m "$tag"
# figure out if we should be signing this release # figure out if we should be signing this release
signing_id= signing_id=
if [ -f release_config.yaml ]; then if [ -f release_config.yaml ]; then
result=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']" 2> /dev/null || true` result=$(cat release_config.yaml | python -c "import yaml; import sys; print(yaml.load(sys.stdin)['signing_id'])" 2> /dev/null || true)
if [ "$?" -eq 0 ]; then if [ "$?" -eq 0 ]; then
signing_id=$result signing_id=$result
fi fi
@@ -206,8 +219,8 @@ assets=''
dodist=0 dodist=0
jq -e .scripts.dist package.json 2> /dev/null || dodist=$? jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
if [ $dodist -eq 0 ]; then if [ $dodist -eq 0 ]; then
projdir=`pwd` projdir=$(pwd)
builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'` builddir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
echo "Building distribution copy in $builddir" echo "Building distribution copy in $builddir"
pushd "$builddir" pushd "$builddir"
git clone "$projdir" . git clone "$projdir" .
@@ -232,7 +245,7 @@ fi
if [ -n "$signing_id" ]; then if [ -n "$signing_id" ]; then
# make a signed tag # make a signed tag
# gnupg seems to fail to get the right tty device unless we set it here # gnupg seems to fail to get the right tty device unless we set it here
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag" GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=$(tty) git tag -u "$signing_id" -F "${latest_changes}" "$tag"
else else
git tag -a -F "${latest_changes}" "$tag" git tag -a -F "${latest_changes}" "$tag"
fi fi
@@ -298,7 +311,7 @@ if [ $prerelease -eq 1 ]; then
hubflags='-p' hubflags='-p'
fi fi
release_text=`mktemp` release_text=$(mktemp)
echo "$tag" > "${release_text}" echo "$tag" > "${release_text}"
echo >> "${release_text}" echo >> "${release_text}"
cat "${latest_changes}" >> "${release_text}" cat "${latest_changes}" >> "${release_text}"
@@ -310,19 +323,6 @@ fi
rm "${release_text}" rm "${release_text}"
rm "${latest_changes}" rm "${latest_changes}"
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
# Tag both releases and prereleases as `next` so the last stable release remains
# the default.
if [ -z "$skip_npm" ]; then
npm publish --tag next
if [ $prerelease -eq 0 ]; then
# For a release, also add the default `latest` tag.
package=$(cat package.json | jq -er .name)
npm dist-tag add "$package@$release" latest
fi
fi
# if it is a pre-release, leave it on the release branch for now. # if it is a pre-release, leave it on the release branch for now.
if [ $prerelease -eq 1 ]; then if [ $prerelease -eq 1 ]; then
git checkout "$rel_branch" git checkout "$rel_branch"
@@ -339,34 +339,19 @@ git merge "$rel_branch" --no-edit
git push origin master git push origin master
# finally, merge master back onto develop (if it exists) # finally, merge master back onto develop (if it exists)
if [ $(git branch -lr | grep origin/develop -c) -ge 1 ]; then if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
git checkout develop git checkout develop
git pull git pull
git merge master --no-edit git merge master --no-edit
git push origin develop
# When merging to develop, we need revert the `main` and `typings` fields if fi
# we adjusted them previously.
for i in main typings [ -x ./post-release.sh ] && ./post-release.sh
do
# If a `lib` prefixed value is present, it means we adjusted the field if [ $has_subprojects -eq 1 ] && [ $prerelease -eq 0 ]; then
# earlier at publish time, so we should revert it now. echo "Resetting subprojects to develop"
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then for proj in $subprojects; do
# If there's a `src` prefixed value, use that, otherwise delete. reset_dependency "$proj"
# This is used to delete the `typings` field and reset `main` back done
# to the TypeScript source.
src_value=$(jq -r ".matrix_src_$i" package.json)
if [ "$src_value" != "null" ]; then
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
else
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
fi
fi
done
if [ -n "$(git ls-files --modified package.json)" ]; then
echo "Committing develop package.json"
git commit package.json -m "Resetting package fields for development"
fi
git push origin develop git push origin develop
fi fi

View 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();

View File

@@ -1,6 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019, 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -17,31 +17,32 @@ limitations under the License.
/** /**
* A mock implementation of the webstorage api * A mock implementation of the webstorage api
* @constructor
*/ */
export function MockStorageApi() { export class MockStorageApi {
this.data = {}; public data: Record<string, string> = {};
this.keys = []; public keys: string[] = [];
this.length = 0; public length = 0;
}
MockStorageApi.prototype = { public setItem(k: string, v: string): void {
setItem: function(k, v) {
this.data[k] = v; this.data[k] = v;
this._recalc(); this.recalc();
}, }
getItem: function(k) {
public getItem(k: string): string | null {
return this.data[k] || null; return this.data[k] || null;
}, }
removeItem: function(k) {
public removeItem(k: string): void {
delete this.data[k]; delete this.data[k];
this._recalc(); this.recalc();
}, }
key: function(index) {
public key(index: number): string {
return this.keys[index]; return this.keys[index];
}, }
_recalc: function() {
const keys = []; private recalc(): void {
const keys: string[] = [];
for (const k in this.data) { for (const k in this.data) {
if (!this.data.hasOwnProperty(k)) { if (!this.data.hasOwnProperty(k)) {
continue; continue;
@@ -50,6 +51,5 @@ MockStorageApi.prototype = {
} }
this.keys = keys; this.keys = keys;
this.length = keys.length; this.length = keys.length;
}, }
}; }

View File

@@ -50,7 +50,7 @@ export class TestClient {
options?: Partial<ICreateClientOpts>, options?: Partial<ICreateClientOpts>,
) { ) {
if (sessionStoreBackend === undefined) { if (sessionStoreBackend === undefined) {
sessionStoreBackend = new MockStorageApi(); sessionStoreBackend = new MockStorageApi() as unknown as Storage;
} }
this.httpBackend = new MockHttpBackend(); this.httpBackend = new MockHttpBackend();

View File

@@ -15,9 +15,11 @@ limitations under the License.
*/ */
// stub for browser-matrix browserify tests // stub for browser-matrix browserify tests
// @ts-ignore
global.XMLHttpRequest = jest.fn(); global.XMLHttpRequest = jest.fn();
afterAll(() => { afterAll(() => {
// clean up XMLHttpRequest mock // clean up XMLHttpRequest mock
// @ts-ignore
global.XMLHttpRequest = undefined; global.XMLHttpRequest = undefined;
}); });

View File

@@ -290,8 +290,9 @@ describe("DeviceList management:", function() {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz']; const bobStat = data.trackingStatus['@bob:xyz'];
// Alice should be tracking bob's device list
expect(bobStat).toBeGreaterThan( expect(bobStat).toBeGreaterThan(
0, "Alice should be tracking bob's device list", 0,
); );
}); });
}); });
@@ -326,8 +327,9 @@ describe("DeviceList management:", function() {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz']; const bobStat = data.trackingStatus['@bob:xyz'];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual( expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked", 0,
); );
}); });
}); });
@@ -362,8 +364,9 @@ describe("DeviceList management:", function() {
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz']; const bobStat = data.trackingStatus['@bob:xyz'];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual( expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked", 0,
); );
}); });
}); });
@@ -378,13 +381,15 @@ describe("DeviceList management:", function() {
anotherTestClient.httpBackend.when('GET', '/sync').respond( anotherTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse([])); 200, getSyncResponse([]));
await anotherTestClient.flushSync(); await anotherTestClient.flushSync();
await anotherTestClient.client.crypto.deviceList.saveIfDirty(); await anotherTestClient.client?.crypto?.deviceList?.saveIfDirty();
// @ts-ignore accessing private property
anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz']; const bobStat = data!.trackingStatus['@bob:xyz'];
// Alice should have marked bob's device list as untracked
expect(bobStat).toEqual( expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked", 0,
); );
}); });
} finally { } finally {

View File

@@ -31,8 +31,9 @@ import '../olm-loader';
import { logger } from '../../src/logger'; import { logger } from '../../src/logger';
import * as testUtils from "../test-utils/test-utils"; import * as testUtils from "../test-utils/test-utils";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
import { CRYPTO_ENABLED } from "../../src/client"; import { CRYPTO_ENABLED, IUploadKeysRequest } from "../../src/client";
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix"; import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
import { DeviceInfo } from '../../src/crypto/deviceinfo';
let aliTestClient: TestClient; let aliTestClient: TestClient;
const roomId = "!room:localhost"; const roomId = "!room:localhost";
@@ -71,12 +72,12 @@ function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise<num
expect(uploader.deviceKeys).toBeTruthy(); expect(uploader.deviceKeys).toBeTruthy();
const uploaderKeys = {}; const uploaderKeys = {};
uploaderKeys[uploader.deviceId] = uploader.deviceKeys; uploaderKeys[uploader.deviceId!] = uploader.deviceKeys;
querier.httpBackend.when("POST", "/keys/query") querier.httpBackend.when("POST", "/keys/query")
.respond(200, function(_path, content) { .respond(200, function(_path, content: IUploadKeysRequest) {
expect(content.device_keys[uploader.userId]).toEqual([]); expect(content.device_keys![uploader.userId!]).toEqual([]);
const result = {}; const result = {};
result[uploader.userId] = uploaderKeys; result[uploader.userId!] = uploaderKeys;
return { device_keys: result }; return { device_keys: result };
}); });
return querier.httpBackend.flush("/keys/query", 1); return querier.httpBackend.flush("/keys/query", 1);
@@ -93,10 +94,10 @@ async function expectAliClaimKeys(): Promise<void> {
const keys = await bobTestClient.awaitOneTimeKeyUpload(); const keys = await bobTestClient.awaitOneTimeKeyUpload();
aliTestClient.httpBackend.when( aliTestClient.httpBackend.when(
"POST", "/keys/claim", "POST", "/keys/claim",
).respond(200, function(_path, content) { ).respond(200, function(_path, content: IUploadKeysRequest) {
const claimType = content.one_time_keys[bobUserId][bobDeviceId]; const claimType = content.one_time_keys![bobUserId][bobDeviceId];
expect(claimType).toEqual("signed_curve25519"); expect(claimType).toEqual("signed_curve25519");
let keyId = null; let keyId = '';
for (keyId in keys) { for (keyId in keys) {
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) { if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
if (keyId.indexOf(claimType + ":") === 0) { if (keyId.indexOf(claimType + ":") === 0) {
@@ -132,13 +133,13 @@ async function aliDownloadsKeys(): Promise<void> {
// check that the localStorage is updated as we expect (not sure this is // check that the localStorage is updated as we expect (not sure this is
// an integration test, but meh) // an integration test, but meh)
await Promise.all([p1(), p2()]); await Promise.all([p1(), p2()]);
await aliTestClient.client.crypto.deviceList.saveIfDirty(); await aliTestClient.client.crypto!.deviceList.saveIfDirty();
// @ts-ignore - protected // @ts-ignore - protected
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
const devices = data.devices[bobUserId]; const devices = data!.devices[bobUserId]!;
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys); expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
expect(devices[bobDeviceId].verified). expect(devices[bobDeviceId].verified).
toBe(0); // DeviceVerification.UNVERIFIED toBe(DeviceInfo.DeviceVerification.UNVERIFIED);
}); });
} }
@@ -237,7 +238,7 @@ function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> { async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
const path = "/send/m.room.encrypted/"; const path = "/send/m.room.encrypted/";
const prom = new Promise((resolve) => { const prom = new Promise<IContent>((resolve) => {
httpBackend.when("PUT", path).respond(200, function(_path, content) { httpBackend.when("PUT", path).respond(200, function(_path, content) {
resolve(content); resolve(content);
return { return {
@@ -252,14 +253,14 @@ async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]):
} }
function aliRecvMessage(): Promise<void> { function aliRecvMessage(): Promise<void> {
const message = bobMessages.shift(); const message = bobMessages.shift()!;
return recvMessage( return recvMessage(
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message, aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
); );
} }
function bobRecvMessage(): Promise<void> { function bobRecvMessage(): Promise<void> {
const message = aliMessages.shift(); const message = aliMessages.shift()!;
return recvMessage( return recvMessage(
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message, bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
); );
@@ -494,6 +495,7 @@ describe("MatrixClient crypto", () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start(); await aliTestClient.start();
await bobTestClient.start(); await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
await firstSync(aliTestClient); await firstSync(aliTestClient);
await aliEnablesEncryption(); await aliEnablesEncryption();
await aliSendsFirstMessage(); await aliSendsFirstMessage();
@@ -504,10 +506,11 @@ describe("MatrixClient crypto", () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start(); await aliTestClient.start();
await bobTestClient.start(); await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
await firstSync(aliTestClient); await firstSync(aliTestClient);
await aliEnablesEncryption(); await aliEnablesEncryption();
await aliSendsFirstMessage(); await aliSendsFirstMessage();
const message = aliMessages.shift(); const message = aliMessages.shift()!;
const syncData = { const syncData = {
next_batch: "x", next_batch: "x",
rooms: { rooms: {
@@ -567,6 +570,7 @@ describe("MatrixClient crypto", () => {
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} }); aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
await aliTestClient.start(); await aliTestClient.start();
await bobTestClient.start(); await bobTestClient.start();
bobTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
await firstSync(aliTestClient); await firstSync(aliTestClient);
await aliEnablesEncryption(); await aliEnablesEncryption();
await aliSendsFirstMessage(); await aliSendsFirstMessage();
@@ -584,6 +588,9 @@ describe("MatrixClient crypto", () => {
await firstSync(bobTestClient); await firstSync(bobTestClient);
await aliEnablesEncryption(); await aliEnablesEncryption();
await aliSendsFirstMessage(); await aliSendsFirstMessage();
bobTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {},
);
await bobRecvMessage(); await bobRecvMessage();
await bobEnablesEncryption(); await bobEnablesEncryption();
const ciphertext = await bobSendsReplyMessage(); const ciphertext = await bobSendsReplyMessage();
@@ -658,11 +665,10 @@ describe("MatrixClient crypto", () => {
]); ]);
logger.log(aliTestClient + ': started'); logger.log(aliTestClient + ': started');
httpBackend.when("POST", "/keys/upload") httpBackend.when("POST", "/keys/upload")
.respond(200, (_path, content) => { .respond(200, (_path, content: IUploadKeysRequest) => {
expect(content.one_time_keys).toBeTruthy(); expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).not.toEqual({}); expect(content.one_time_keys).not.toEqual({});
expect(Object.keys(content.one_time_keys).length).toBeGreaterThanOrEqual(1); expect(Object.keys(content.one_time_keys!).length).toBeGreaterThanOrEqual(1);
logger.log('received %i one-time keys', Object.keys(content.one_time_keys).length);
// cancel futher calls by telling the client // cancel futher calls by telling the client
// we have more than we need // we have more than we need
return { return {

View File

@@ -1,25 +1,59 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import HttpBackend from "matrix-mock-request";
import {
ClientEvent,
HttpApiEvent,
IEvent,
MatrixClient,
RoomEvent,
RoomMemberEvent,
RoomStateEvent,
UserEvent,
} from "../../src";
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
describe("MatrixClient events", function() { describe("MatrixClient events", function() {
let client;
let httpBackend;
const selfUserId = "@alice:localhost"; const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef"; const selfAccessToken = "aseukfgwef";
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
const setupTests = (): [MatrixClient, HttpBackend] => {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
const client = testClient.client;
const httpBackend = testClient.httpBackend;
httpBackend!.when("GET", "/versions").respond(200, {});
httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
return [client!, httpBackend];
};
beforeEach(function() { beforeEach(function() {
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken); [client!, httpBackend] = setupTests();
client = testClient.client;
httpBackend = testClient.httpBackend;
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
}); });
afterEach(function() { afterEach(function() {
httpBackend.verifyNoOutstandingExpectation(); httpBackend?.verifyNoOutstandingExpectation();
client.stopClient(); client?.stopClient();
return httpBackend.stop(); return httpBackend?.stop();
}); });
describe("emissions", function() { describe("emissions", function() {
@@ -93,10 +127,10 @@ describe("MatrixClient events", function() {
it("should emit events from both the first and subsequent /sync calls", it("should emit events from both the first and subsequent /sync calls",
function() { function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA); httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let expectedEvents = []; let expectedEvents: Partial<IEvent>[] = [];
expectedEvents = expectedEvents.concat( expectedEvents = expectedEvents.concat(
SYNC_DATA.presence.events, SYNC_DATA.presence.events,
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events, SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
@@ -105,7 +139,7 @@ describe("MatrixClient events", function() {
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events, NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
); );
client.on("event", function(event) { client!.on(ClientEvent.Event, function(event) {
let found = false; let found = false;
for (let i = 0; i < expectedEvents.length; i++) { for (let i = 0; i < expectedEvents.length; i++) {
if (expectedEvents[i].event_id === event.getId()) { if (expectedEvents[i].event_id === event.getId()) {
@@ -114,31 +148,27 @@ describe("MatrixClient events", function() {
break; break;
} }
} }
expect(found).toBe( expect(found).toBe(true);
true, "Unexpected 'event' emitted: " + event.getType(),
);
}); });
client.startClient(); client!.startClient();
return Promise.all([ return Promise.all([
// wait for two SYNCING events // wait for two SYNCING events
utils.syncPromise(client).then(() => { utils.syncPromise(client!).then(() => {
return utils.syncPromise(client); return utils.syncPromise(client!);
}), }),
httpBackend.flushAllExpected(), httpBackend!.flushAllExpected(),
]).then(() => { ]).then(() => {
expect(expectedEvents.length).toEqual( expect(expectedEvents.length).toEqual(0);
0, "Failed to see all events from /sync calls",
);
}); });
}); });
it("should emit User events", function(done) { it("should emit User events", function(done) {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA); httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let fired = false; let fired = false;
client.on("User.presence", function(event, user) { client!.on(UserEvent.Presence, function(event, user) {
fired = true; fired = true;
expect(user).toBeTruthy(); expect(user).toBeTruthy();
expect(event).toBeTruthy(); expect(event).toBeTruthy();
@@ -146,58 +176,52 @@ describe("MatrixClient events", function() {
return; return;
} }
expect(event.event).toMatch(SYNC_DATA.presence.events[0]); expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
expect(user.presence).toEqual( expect(user.presence).toEqual(
SYNC_DATA.presence.events[0].content.presence, SYNC_DATA.presence.events[0]?.content?.presence,
); );
}); });
client.startClient(); client!.startClient();
httpBackend.flushAllExpected().then(function() { httpBackend!.flushAllExpected().then(function() {
expect(fired).toBe(true, "User.presence didn't fire."); expect(fired).toBe(true);
done(); done();
}); });
}); });
it("should emit Room events", function() { it("should emit Room events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA); httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let roomInvokeCount = 0; let roomInvokeCount = 0;
let roomNameInvokeCount = 0; let roomNameInvokeCount = 0;
let timelineFireCount = 0; let timelineFireCount = 0;
client.on("Room", function(room) { client!.on(ClientEvent.Room, function(room) {
roomInvokeCount++; roomInvokeCount++;
expect(room.roomId).toEqual("!erufh:bar"); expect(room.roomId).toEqual("!erufh:bar");
}); });
client.on("Room.timeline", function(event, room) { client!.on(RoomEvent.Timeline, function(event, room) {
timelineFireCount++; timelineFireCount++;
expect(room.roomId).toEqual("!erufh:bar"); expect(room.roomId).toEqual("!erufh:bar");
}); });
client.on("Room.name", function(room) { client!.on(RoomEvent.Name, function(room) {
roomNameInvokeCount++; roomNameInvokeCount++;
}); });
client.startClient(); client!.startClient();
return Promise.all([ return Promise.all([
httpBackend.flushAllExpected(), httpBackend!.flushAllExpected(),
utils.syncPromise(client, 2), utils.syncPromise(client!, 2),
]).then(function() { ]).then(function() {
expect(roomInvokeCount).toEqual( expect(roomInvokeCount).toEqual(1);
1, "Room fired wrong number of times.", expect(roomNameInvokeCount).toEqual(1);
); expect(timelineFireCount).toEqual(3);
expect(roomNameInvokeCount).toEqual(
1, "Room.name fired wrong number of times.",
);
expect(timelineFireCount).toEqual(
3, "Room.timeline fired the wrong number of times",
);
}); });
}); });
it("should emit RoomState events", function() { it("should emit RoomState events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA); httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
const roomStateEventTypes = [ const roomStateEventTypes = [
"m.room.member", "m.room.create", "m.room.member", "m.room.create",
@@ -205,126 +229,106 @@ describe("MatrixClient events", function() {
let eventsInvokeCount = 0; let eventsInvokeCount = 0;
let membersInvokeCount = 0; let membersInvokeCount = 0;
let newMemberInvokeCount = 0; let newMemberInvokeCount = 0;
client.on("RoomState.events", function(event, state) { client!.on(RoomStateEvent.Events, function(event, state) {
eventsInvokeCount++; eventsInvokeCount++;
const index = roomStateEventTypes.indexOf(event.getType()); const index = roomStateEventTypes.indexOf(event.getType());
expect(index).not.toEqual( expect(index).not.toEqual(-1);
-1, "Unexpected room state event type: " + event.getType(),
);
if (index >= 0) { if (index >= 0) {
roomStateEventTypes.splice(index, 1); roomStateEventTypes.splice(index, 1);
} }
}); });
client.on("RoomState.members", function(event, state, member) { client!.on(RoomStateEvent.Members, function(event, state, member) {
membersInvokeCount++; membersInvokeCount++;
expect(member.roomId).toEqual("!erufh:bar"); expect(member.roomId).toEqual("!erufh:bar");
expect(member.userId).toEqual("@foo:bar"); expect(member.userId).toEqual("@foo:bar");
expect(member.membership).toEqual("join"); expect(member.membership).toEqual("join");
}); });
client.on("RoomState.newMember", function(event, state, member) { client!.on(RoomStateEvent.NewMember, function(event, state, member) {
newMemberInvokeCount++; newMemberInvokeCount++;
expect(member.roomId).toEqual("!erufh:bar"); expect(member.roomId).toEqual("!erufh:bar");
expect(member.userId).toEqual("@foo:bar"); expect(member.userId).toEqual("@foo:bar");
expect(member.membership).toBeFalsy(); expect(member.membership).toBeFalsy();
}); });
client.startClient(); client!.startClient();
return Promise.all([ return Promise.all([
httpBackend.flushAllExpected(), httpBackend!.flushAllExpected(),
utils.syncPromise(client, 2), utils.syncPromise(client!, 2),
]).then(function() { ]).then(function() {
expect(membersInvokeCount).toEqual( expect(membersInvokeCount).toEqual(1);
1, "RoomState.members fired wrong number of times", expect(newMemberInvokeCount).toEqual(1);
); expect(eventsInvokeCount).toEqual(2);
expect(newMemberInvokeCount).toEqual(
1, "RoomState.newMember fired wrong number of times",
);
expect(eventsInvokeCount).toEqual(
2, "RoomState.events fired wrong number of times",
);
}); });
}); });
it("should emit RoomMember events", function() { it("should emit RoomMember events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA); httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let typingInvokeCount = 0; let typingInvokeCount = 0;
let powerLevelInvokeCount = 0; let powerLevelInvokeCount = 0;
let nameInvokeCount = 0; let nameInvokeCount = 0;
let membershipInvokeCount = 0; let membershipInvokeCount = 0;
client.on("RoomMember.name", function(event, member) { client!.on(RoomMemberEvent.Name, function(event, member) {
nameInvokeCount++; nameInvokeCount++;
}); });
client.on("RoomMember.typing", function(event, member) { client!.on(RoomMemberEvent.Typing, function(event, member) {
typingInvokeCount++; typingInvokeCount++;
expect(member.typing).toBe(true); expect(member.typing).toBe(true);
}); });
client.on("RoomMember.powerLevel", function(event, member) { client!.on(RoomMemberEvent.PowerLevel, function(event, member) {
powerLevelInvokeCount++; powerLevelInvokeCount++;
}); });
client.on("RoomMember.membership", function(event, member) { client!.on(RoomMemberEvent.Membership, function(event, member) {
membershipInvokeCount++; membershipInvokeCount++;
expect(member.membership).toEqual("join"); expect(member.membership).toEqual("join");
}); });
client.startClient(); client!.startClient();
return Promise.all([ return Promise.all([
httpBackend.flushAllExpected(), httpBackend!.flushAllExpected(),
utils.syncPromise(client, 2), utils.syncPromise(client!, 2),
]).then(function() { ]).then(function() {
expect(typingInvokeCount).toEqual( expect(typingInvokeCount).toEqual(1);
1, "RoomMember.typing fired wrong number of times", expect(powerLevelInvokeCount).toEqual(0);
); expect(nameInvokeCount).toEqual(0);
expect(powerLevelInvokeCount).toEqual( expect(membershipInvokeCount).toEqual(1);
0, "RoomMember.powerLevel fired wrong number of times",
);
expect(nameInvokeCount).toEqual(
0, "RoomMember.name fired wrong number of times",
);
expect(membershipInvokeCount).toEqual(
1, "RoomMember.membership fired wrong number of times",
);
}); });
}); });
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() { it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
const error = { errcode: 'M_UNKNOWN_TOKEN' }; const error = { errcode: 'M_UNKNOWN_TOKEN' };
httpBackend.when("GET", "/sync").respond(401, error); httpBackend!.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0; let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(errObj) { client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
sessionLoggedOutCount++; sessionLoggedOutCount++;
expect(errObj.data).toEqual(error); expect(errObj.data).toEqual(error);
}); });
client.startClient(); client!.startClient();
return httpBackend.flushAllExpected().then(function() { return httpBackend!.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual( expect(sessionLoggedOutCount).toEqual(1);
1, "Session.logged_out fired wrong number of times",
);
}); });
}); });
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() { it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() {
const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true }; const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true };
httpBackend.when("GET", "/sync").respond(401, error); httpBackend!.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0; let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(errObj) { client!.on(HttpApiEvent.SessionLoggedOut, function(errObj) {
sessionLoggedOutCount++; sessionLoggedOutCount++;
expect(errObj.data).toEqual(error); expect(errObj.data).toEqual(error);
}); });
client.startClient(); client!.startClient();
return httpBackend.flushAllExpected().then(function() { return httpBackend!.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual( expect(sessionLoggedOutCount).toEqual(1);
1, "Session.logged_out fired wrong number of times",
);
}); });
}); });
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -13,68 +13,84 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
import { CRYPTO_ENABLED } from "../../src/client"; import { CRYPTO_ENABLED, MatrixClient, IStoredClientOpts } from "../../src/client";
import { MatrixEvent } from "../../src/models/event"; import { MatrixEvent } from "../../src/models/event";
import { Filter, MemoryStore, Room } from "../../src/matrix"; import { Filter, MemoryStore, Room } from "../../src/matrix";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
import { THREAD_RELATION_TYPE } from "../../src/models/thread"; import { THREAD_RELATION_TYPE } from "../../src/models/thread";
import { IFilterDefinition } from "../../src/filter";
import { FileType } from "../../src/http-api";
import { ISearchResults } from "../../src/@types/search";
import { IStore } from "../../src/store";
describe("MatrixClient", function() { describe("MatrixClient", function() {
let client = null;
let httpBackend = null;
let store = null;
const userId = "@alice:localhost"; const userId = "@alice:localhost";
const accessToken = "aseukfgwef"; const accessToken = "aseukfgwef";
const idServerDomain = "identity.localhost"; // not a real server const idServerDomain = "identity.localhost"; // not a real server
const identityAccessToken = "woop-i-am-a-secret"; const identityAccessToken = "woop-i-am-a-secret";
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
let store: MemoryStore | undefined;
beforeEach(function() { const defaultClientOpts: IStoredClientOpts = {
store = new MemoryStore(); canResetEntireTimeline: roomId => false,
experimentalThreadSupport: false,
crypto: {} as unknown as IStoredClientOpts['crypto'],
};
const setupTests = (): [MatrixClient, HttpBackend, MemoryStore] => {
const store = new MemoryStore();
const testClient = new TestClient(userId, "aliceDevice", accessToken, undefined, { const testClient = new TestClient(userId, "aliceDevice", accessToken, undefined, {
store, store: store as IStore,
identityServer: { identityServer: {
getAccessToken: () => Promise.resolve(identityAccessToken), getAccessToken: () => Promise.resolve(identityAccessToken),
}, },
idBaseUrl: `https://${idServerDomain}`, idBaseUrl: `https://${idServerDomain}`,
}); });
httpBackend = testClient.httpBackend;
client = testClient.client; return [testClient.client, testClient.httpBackend, store];
};
beforeEach(function() {
[client, httpBackend, store] = setupTests();
}); });
afterEach(function() { afterEach(function() {
httpBackend.verifyNoOutstandingExpectation(); httpBackend!.verifyNoOutstandingExpectation();
return httpBackend.stop(); return httpBackend!.stop();
}); });
describe("uploadContent", function() { describe("uploadContent", function() {
const buf = Buffer.from('hello world'); const buf = Buffer.from('hello world');
it("should upload the file", function() { it("should upload the file", function() {
httpBackend.when( httpBackend!.when(
"POST", "/_matrix/media/r0/upload", "POST", "/_matrix/media/r0/upload",
).check(function(req) { ).check(function(req) {
expect(req.rawData).toEqual(buf); expect(req.rawData).toEqual(buf);
expect(req.queryParams.filename).toEqual("hi.txt"); expect(req.queryParams?.filename).toEqual("hi.txt");
if (!(req.queryParams.access_token == accessToken || if (!(req.queryParams?.access_token == accessToken ||
req.headers["Authorization"] == "Bearer " + accessToken)) { req.headers["Authorization"] == "Bearer " + accessToken)) {
expect(true).toBe(false); expect(true).toBe(false);
} }
expect(req.headers["Content-Type"]).toEqual("text/plain"); expect(req.headers["Content-Type"]).toEqual("text/plain");
// @ts-ignore private property
expect(req.opts.json).toBeFalsy(); expect(req.opts.json).toBeFalsy();
// @ts-ignore private property
expect(req.opts.timeout).toBe(undefined); expect(req.opts.timeout).toBe(undefined);
}).respond(200, "content", true); }).respond(200, "content", true);
const prom = client.uploadContent({ const prom = client!.uploadContent({
stream: buf, stream: buf,
name: "hi.txt", name: "hi.txt",
type: "text/plain", type: "text/plain",
}); } as unknown as FileType);
expect(prom).toBeTruthy(); expect(prom).toBeTruthy();
const uploads = client.getCurrentUploads(); const uploads = client!.getCurrentUploads();
expect(uploads.length).toEqual(1); expect(uploads.length).toEqual(1);
expect(uploads[0].promise).toBe(prom); expect(uploads[0].promise).toBe(prom);
expect(uploads[0].loaded).toEqual(0); expect(uploads[0].loaded).toEqual(0);
@@ -83,51 +99,53 @@ describe("MatrixClient", function() {
// for backwards compatibility, we return the raw JSON // for backwards compatibility, we return the raw JSON
expect(response).toEqual("content"); expect(response).toEqual("content");
const uploads = client.getCurrentUploads(); const uploads = client!.getCurrentUploads();
expect(uploads.length).toEqual(0); expect(uploads.length).toEqual(0);
}); });
httpBackend.flush(); httpBackend!.flush('');
return prom2; return prom2;
}); });
it("should parse the response if rawResponse=false", function() { it("should parse the response if rawResponse=false", function() {
httpBackend.when( httpBackend!.when(
"POST", "/_matrix/media/r0/upload", "POST", "/_matrix/media/r0/upload",
).check(function(req) { ).check(function(req) {
// @ts-ignore private property
expect(req.opts.json).toBeFalsy(); expect(req.opts.json).toBeFalsy();
}).respond(200, { "content_uri": "uri" }); }).respond(200, { "content_uri": "uri" });
const prom = client.uploadContent({ const prom = client!.uploadContent({
stream: buf, stream: buf,
name: "hi.txt", name: "hi.txt",
type: "text/plain", type: "text/plain",
}, { } as unknown as FileType, {
rawResponse: false, rawResponse: false,
}).then(function(response) { }).then(function(response) {
expect(response.content_uri).toEqual("uri"); expect(response.content_uri).toEqual("uri");
}); });
httpBackend.flush(); httpBackend!.flush('');
return prom; return prom;
}); });
it("should parse errors into a MatrixError", function() { it("should parse errors into a MatrixError", function() {
httpBackend.when( httpBackend!.when(
"POST", "/_matrix/media/r0/upload", "POST", "/_matrix/media/r0/upload",
).check(function(req) { ).check(function(req) {
expect(req.rawData).toEqual(buf); expect(req.rawData).toEqual(buf);
// @ts-ignore private property
expect(req.opts.json).toBeFalsy(); expect(req.opts.json).toBeFalsy();
}).respond(400, { }).respond(400, {
"errcode": "M_SNAFU", "errcode": "M_SNAFU",
"error": "broken", "error": "broken",
}); });
const prom = client.uploadContent({ const prom = client!.uploadContent({
stream: buf, stream: buf,
name: "hi.txt", name: "hi.txt",
type: "text/plain", type: "text/plain",
}).then(function(response) { } as unknown as FileType).then(function(response) {
throw Error("request not failed"); throw Error("request not failed");
}, function(error) { }, function(error) {
expect(error.httpStatus).toEqual(400); expect(error.httpStatus).toEqual(400);
@@ -135,18 +153,18 @@ describe("MatrixClient", function() {
expect(error.message).toEqual("broken"); expect(error.message).toEqual("broken");
}); });
httpBackend.flush(); httpBackend!.flush('');
return prom; return prom;
}); });
it("should return a promise which can be cancelled", function() { it("should return a promise which can be cancelled", function() {
const prom = client.uploadContent({ const prom = client!.uploadContent({
stream: buf, stream: buf,
name: "hi.txt", name: "hi.txt",
type: "text/plain", type: "text/plain",
}); } as unknown as FileType);
const uploads = client.getCurrentUploads(); const uploads = client!.getCurrentUploads();
expect(uploads.length).toEqual(1); expect(uploads.length).toEqual(1);
expect(uploads[0].promise).toBe(prom); expect(uploads[0].promise).toBe(prom);
expect(uploads[0].loaded).toEqual(0); expect(uploads[0].loaded).toEqual(0);
@@ -156,11 +174,11 @@ describe("MatrixClient", function() {
}, function(error) { }, function(error) {
expect(error).toEqual("aborted"); expect(error).toEqual("aborted");
const uploads = client.getCurrentUploads(); const uploads = client!.getCurrentUploads();
expect(uploads.length).toEqual(0); expect(uploads.length).toEqual(0);
}); });
const r = client.cancelUpload(prom); const r = client!.cancelUpload(prom);
expect(r).toBe(true); expect(r).toBe(true);
return prom2; return prom2;
}); });
@@ -169,17 +187,20 @@ describe("MatrixClient", function() {
describe("joinRoom", function() { describe("joinRoom", function() {
it("should no-op if you've already joined a room", function() { it("should no-op if you've already joined a room", function() {
const roomId = "!foo:bar"; const roomId = "!foo:bar";
const room = new Room(roomId, client, userId); const room = new Room(roomId, client!, userId);
client.fetchRoomEvent = () => Promise.resolve({}); client!.fetchRoomEvent = () => Promise.resolve({
type: 'test',
content: {},
});
room.addLiveEvents([ room.addLiveEvents([
utils.mkMembership({ utils.mkMembership({
user: userId, room: roomId, mship: "join", event: true, user: userId, room: roomId, mship: "join", event: true,
}), }),
]); ]);
httpBackend.verifyNoOutstandingRequests(); httpBackend!.verifyNoOutstandingRequests();
store.storeRoom(room); store!.storeRoom(room);
client.joinRoom(roomId); client!.joinRoom(roomId);
httpBackend.verifyNoOutstandingRequests(); httpBackend!.verifyNoOutstandingRequests();
}); });
}); });
@@ -190,12 +211,12 @@ describe("MatrixClient", function() {
const filter = Filter.fromJson(userId, filterId, { const filter = Filter.fromJson(userId, filterId, {
event_format: "client", event_format: "client",
}); });
store.storeFilter(filter); store!.storeFilter(filter);
client.getFilter(userId, filterId, true).then(function(gotFilter) { client!.getFilter(userId, filterId, true).then(function(gotFilter) {
expect(gotFilter).toEqual(filter); expect(gotFilter).toEqual(filter);
done(); done();
}); });
httpBackend.verifyNoOutstandingRequests(); httpBackend!.verifyNoOutstandingRequests();
}); });
it("should do an HTTP request if !allowCached even if one exists", it("should do an HTTP request if !allowCached even if one exists",
@@ -204,20 +225,20 @@ describe("MatrixClient", function() {
event_format: "federation", event_format: "federation",
}; };
httpBackend.when( httpBackend!.when(
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId, "GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
).respond(200, httpFilterDefinition); ).respond(200, httpFilterDefinition);
const storeFilter = Filter.fromJson(userId, filterId, { const storeFilter = Filter.fromJson(userId, filterId, {
event_format: "client", event_format: "client",
}); });
store.storeFilter(storeFilter); store!.storeFilter(storeFilter);
client.getFilter(userId, filterId, false).then(function(gotFilter) { client!.getFilter(userId, filterId, false).then(function(gotFilter) {
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition); expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
done(); done();
}); });
httpBackend.flush(); httpBackend!.flush('');
}); });
it("should do an HTTP request if nothing is in the cache and then store it", it("should do an HTTP request if nothing is in the cache and then store it",
@@ -225,18 +246,18 @@ describe("MatrixClient", function() {
const httpFilterDefinition = { const httpFilterDefinition = {
event_format: "federation", event_format: "federation",
}; };
expect(store.getFilter(userId, filterId)).toBe(null); expect(store!.getFilter(userId, filterId)).toBe(null);
httpBackend.when( httpBackend!.when(
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId, "GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
).respond(200, httpFilterDefinition); ).respond(200, httpFilterDefinition);
client.getFilter(userId, filterId, true).then(function(gotFilter) { client!.getFilter(userId, filterId, true).then(function(gotFilter) {
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition); expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
expect(store.getFilter(userId, filterId)).toBeTruthy(); expect(store!.getFilter(userId, filterId)).toBeTruthy();
done(); done();
}); });
httpBackend.flush(); httpBackend!.flush('');
}); });
}); });
@@ -244,13 +265,13 @@ describe("MatrixClient", function() {
const filterId = "f1llllllerid"; const filterId = "f1llllllerid";
it("should do an HTTP request and then store the filter", function(done) { it("should do an HTTP request and then store the filter", function(done) {
expect(store.getFilter(userId, filterId)).toBe(null); expect(store!.getFilter(userId, filterId)).toBe(null);
const filterDefinition = { const filterDefinition = {
event_format: "client", event_format: "client" as IFilterDefinition['event_format'],
}; };
httpBackend.when( httpBackend!.when(
"POST", "/user/" + encodeURIComponent(userId) + "/filter", "POST", "/user/" + encodeURIComponent(userId) + "/filter",
).check(function(req) { ).check(function(req) {
expect(req.data).toEqual(filterDefinition); expect(req.data).toEqual(filterDefinition);
@@ -258,13 +279,13 @@ describe("MatrixClient", function() {
filter_id: filterId, filter_id: filterId,
}); });
client.createFilter(filterDefinition).then(function(gotFilter) { client!.createFilter(filterDefinition).then(function(gotFilter) {
expect(gotFilter.getDefinition()).toEqual(filterDefinition); expect(gotFilter.getDefinition()).toEqual(filterDefinition);
expect(store.getFilter(userId, filterId)).toEqual(gotFilter); expect(store!.getFilter(userId, filterId)).toEqual(gotFilter);
done(); done();
}); });
httpBackend.flush(); httpBackend!.flush('');
}); });
}); });
@@ -291,10 +312,10 @@ describe("MatrixClient", function() {
}, },
}; };
client.searchMessageText({ client!.searchMessageText({
query: "monkeys", query: "monkeys",
}); });
httpBackend.when("POST", "/search").check(function(req) { httpBackend!.when("POST", "/search").check(function(req) {
expect(req.data).toEqual({ expect(req.data).toEqual({
search_categories: { search_categories: {
room_events: { room_events: {
@@ -304,7 +325,7 @@ describe("MatrixClient", function() {
}); });
}).respond(200, response); }).respond(200, response);
return httpBackend.flush(); return httpBackend!.flush('');
}); });
describe("should filter out context from different timelines (threads)", () => { describe("should filter out context from different timelines (threads)", () => {
@@ -313,11 +334,14 @@ describe("MatrixClient", function() {
search_categories: { search_categories: {
room_events: { room_events: {
count: 24, count: 24,
highlights: [],
results: [{ results: [{
rank: 0.1, rank: 0.1,
result: { result: {
event_id: "$flibble:localhost", event_id: "$flibble:localhost",
type: "m.room.message", type: "m.room.message",
sender: '@test:locahost',
origin_server_ts: 123,
user_id: "@alice:localhost", user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost", room_id: "!feuiwhf:localhost",
content: { content: {
@@ -326,9 +350,12 @@ describe("MatrixClient", function() {
}, },
}, },
context: { context: {
profile_info: {},
events_after: [{ events_after: [{
event_id: "$ev-after:server", event_id: "$ev-after:server",
type: "m.room.message", type: "m.room.message",
sender: '@test:locahost',
origin_server_ts: 123,
user_id: "@alice:localhost", user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost", room_id: "!feuiwhf:localhost",
content: { content: {
@@ -343,6 +370,8 @@ describe("MatrixClient", function() {
events_before: [{ events_before: [{
event_id: "$ev-before:server", event_id: "$ev-before:server",
type: "m.room.message", type: "m.room.message",
sender: '@test:locahost',
origin_server_ts: 123,
user_id: "@alice:localhost", user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost", room_id: "!feuiwhf:localhost",
content: { content: {
@@ -356,15 +385,17 @@ describe("MatrixClient", function() {
}, },
}; };
const data = { const data: ISearchResults = {
results: [], results: [],
highlights: [], highlights: [],
}; };
client.processRoomEventsSearch(data, response); client!.processRoomEventsSearch(data, response);
expect(data.results).toHaveLength(1); expect(data.results).toHaveLength(1);
expect(data.results[0].context.timeline).toHaveLength(2); expect(data.results[0].context.getTimeline()).toHaveLength(2);
expect(data.results[0].context.timeline.find(e => e.getId() === "$ev-after:server")).toBeFalsy(); expect(
data.results[0].context.getTimeline().find(e => e.getId() === "$ev-after:server"),
).toBeFalsy();
}); });
it("filters out thread replies from threads other than the thread the result replied to", () => { it("filters out thread replies from threads other than the thread the result replied to", () => {
@@ -372,11 +403,14 @@ describe("MatrixClient", function() {
search_categories: { search_categories: {
room_events: { room_events: {
count: 24, count: 24,
highlights: [],
results: [{ results: [{
rank: 0.1, rank: 0.1,
result: { result: {
event_id: "$flibble:localhost", event_id: "$flibble:localhost",
type: "m.room.message", type: "m.room.message",
sender: '@test:locahost',
origin_server_ts: 123,
user_id: "@alice:localhost", user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost", room_id: "!feuiwhf:localhost",
content: { content: {
@@ -389,9 +423,12 @@ describe("MatrixClient", function() {
}, },
}, },
context: { context: {
profile_info: {},
events_after: [{ events_after: [{
event_id: "$ev-after:server", event_id: "$ev-after:server",
type: "m.room.message", type: "m.room.message",
sender: '@test:locahost',
origin_server_ts: 123,
user_id: "@alice:localhost", user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost", room_id: "!feuiwhf:localhost",
content: { content: {
@@ -410,15 +447,17 @@ describe("MatrixClient", function() {
}, },
}; };
const data = { const data: ISearchResults = {
results: [], results: [],
highlights: [], highlights: [],
}; };
client.processRoomEventsSearch(data, response); client!.processRoomEventsSearch(data, response);
expect(data.results).toHaveLength(1); expect(data.results).toHaveLength(1);
expect(data.results[0].context.timeline).toHaveLength(1); expect(data.results[0].context.getTimeline()).toHaveLength(1);
expect(data.results[0].context.timeline.find(e => e.getId() === "$flibble:localhost")).toBeTruthy(); expect(
data.results[0].context.getTimeline().find(e => e.getId() === "$flibble:localhost"),
).toBeTruthy();
}); });
it("filters out main timeline events when result is a thread reply", () => { it("filters out main timeline events when result is a thread reply", () => {
@@ -426,10 +465,13 @@ describe("MatrixClient", function() {
search_categories: { search_categories: {
room_events: { room_events: {
count: 24, count: 24,
highlights: [],
results: [{ results: [{
rank: 0.1, rank: 0.1,
result: { result: {
event_id: "$flibble:localhost", event_id: "$flibble:localhost",
sender: '@test:locahost',
origin_server_ts: 123,
type: "m.room.message", type: "m.room.message",
user_id: "@alice:localhost", user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost", room_id: "!feuiwhf:localhost",
@@ -445,6 +487,8 @@ describe("MatrixClient", function() {
context: { context: {
events_after: [{ events_after: [{
event_id: "$ev-after:server", event_id: "$ev-after:server",
sender: '@test:locahost',
origin_server_ts: 123,
type: "m.room.message", type: "m.room.message",
user_id: "@alice:localhost", user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost", room_id: "!feuiwhf:localhost",
@@ -454,21 +498,24 @@ describe("MatrixClient", function() {
}, },
}], }],
events_before: [], events_before: [],
profile_info: {},
}, },
}], }],
}, },
}, },
}; };
const data = { const data: ISearchResults = {
results: [], results: [],
highlights: [], highlights: [],
}; };
client.processRoomEventsSearch(data, response); client!.processRoomEventsSearch(data, response);
expect(data.results).toHaveLength(1); expect(data.results).toHaveLength(1);
expect(data.results[0].context.timeline).toHaveLength(1); expect(data.results[0].context.getTimeline()).toHaveLength(1);
expect(data.results[0].context.timeline.find(e => e.getId() === "$flibble:localhost")).toBeTruthy(); expect(
data.results[0].context.getTimeline().find(e => e.getId() === "$flibble:localhost"),
).toBeTruthy();
}); });
}); });
}); });
@@ -479,16 +526,16 @@ describe("MatrixClient", function() {
} }
beforeEach(function() { beforeEach(function() {
return client.initCrypto(); return client!.initCrypto();
}); });
afterEach(() => { afterEach(() => {
client.stopClient(); client!.stopClient();
}); });
it("should do an HTTP request and then store the keys", function() { it("should do an HTTP request and then store the keys", function() {
const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78"; const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
// ed25519key = client.getDeviceEd25519Key(); // ed25519key = client!.getDeviceEd25519Key();
const borisKeys = { const borisKeys = {
dev1: { dev1: {
algorithms: ["1"], algorithms: ["1"],
@@ -528,7 +575,7 @@ describe("MatrixClient", function() {
var b = JSON.parse(JSON.stringify(o)); var b = JSON.parse(JSON.stringify(o));
delete(b.signatures); delete(b.signatures);
delete(b.unsigned); delete(b.unsigned);
return client.crypto.olmDevice.sign(anotherjson.stringify(b)); return client!.crypto.olmDevice.sign(anotherjson.stringify(b));
}; };
logger.log("Ed25519: " + ed25519key); logger.log("Ed25519: " + ed25519key);
@@ -536,7 +583,7 @@ describe("MatrixClient", function() {
logger.log("chaz:", sign(chazKeys.dev2)); logger.log("chaz:", sign(chazKeys.dev2));
*/ */
httpBackend.when("POST", "/keys/query").check(function(req) { httpBackend!.when("POST", "/keys/query").check(function(req) {
expect(req.data).toEqual({ device_keys: { expect(req.data).toEqual({ device_keys: {
'boris': [], 'boris': [],
'chaz': [], 'chaz': [],
@@ -548,7 +595,7 @@ describe("MatrixClient", function() {
}, },
}); });
const prom = client.downloadKeys(["boris", "chaz"]).then(function(res) { const prom = client!.downloadKeys(["boris", "chaz"]).then(function(res) {
assertObjectContains(res.boris.dev1, { assertObjectContains(res.boris.dev1, {
verified: 0, // DeviceVerification.UNVERIFIED verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev1": ed25519key }, keys: { "ed25519:dev1": ed25519key },
@@ -564,23 +611,23 @@ describe("MatrixClient", function() {
}); });
}); });
httpBackend.flush(); httpBackend!.flush('');
return prom; return prom;
}); });
}); });
describe("deleteDevice", function() { describe("deleteDevice", function() {
const auth = { a: 1 }; const auth = { identifier: 1 };
it("should pass through an auth dict", function() { it("should pass through an auth dict", function() {
httpBackend.when( httpBackend!.when(
"DELETE", "/_matrix/client/r0/devices/my_device", "DELETE", "/_matrix/client/r0/devices/my_device",
).check(function(req) { ).check(function(req) {
expect(req.data).toEqual({ auth: auth }); expect(req.data).toEqual({ auth: auth });
}).respond(200); }).respond(200);
const prom = client.deleteDevice("my_device", auth); const prom = client!.deleteDevice("my_device", auth);
httpBackend.flush(); httpBackend!.flush('');
return prom; return prom;
}); });
}); });
@@ -588,7 +635,7 @@ describe("MatrixClient", function() {
describe("partitionThreadedEvents", function() { describe("partitionThreadedEvents", function() {
let room; let room;
beforeEach(() => { beforeEach(() => {
room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client, userId); room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client!, userId);
}); });
it("returns empty arrays when given an empty arrays", function() { it("returns empty arrays when given an empty arrays", function() {
@@ -599,7 +646,11 @@ describe("MatrixClient", function() {
}); });
it("copies pre-thread in-timeline vote events onto both timelines", function() { it("copies pre-thread in-timeline vote events onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true }; // @ts-ignore setting private property
client!.clientOpts = {
...defaultClientOpts,
experimentalThreadSupport: true,
};
const eventPollResponseReference = buildEventPollResponseReference(); const eventPollResponseReference = buildEventPollResponseReference();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot(); const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
@@ -611,6 +662,7 @@ describe("MatrixClient", function() {
eventPollResponseReference, eventPollResponseReference,
]; ];
// Vote has no threadId yet // Vote has no threadId yet
// @ts-ignore private property
expect(eventPollResponseReference.threadId).toBeFalsy(); expect(eventPollResponseReference.threadId).toBeFalsy();
const [timeline, threaded] = room.partitionThreadedEvents(events); const [timeline, threaded] = room.partitionThreadedEvents(events);
@@ -634,7 +686,11 @@ describe("MatrixClient", function() {
}); });
it("copies pre-thread in-timeline reactions onto both timelines", function() { it("copies pre-thread in-timeline reactions onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true }; // @ts-ignore setting private property
client!.clientOpts = {
...defaultClientOpts,
experimentalThreadSupport: true,
};
const eventPollStartThreadRoot = buildEventPollStartThreadRoot(); const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot); const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
@@ -661,7 +717,11 @@ describe("MatrixClient", function() {
}); });
it("copies post-thread in-timeline vote events onto both timelines", function() { it("copies post-thread in-timeline vote events onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true }; // @ts-ignore setting private property
client!.clientOpts = {
...defaultClientOpts,
experimentalThreadSupport: true,
};
const eventPollResponseReference = buildEventPollResponseReference(); const eventPollResponseReference = buildEventPollResponseReference();
const eventPollStartThreadRoot = buildEventPollStartThreadRoot(); const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
@@ -688,7 +748,11 @@ describe("MatrixClient", function() {
}); });
it("copies post-thread in-timeline reactions onto both timelines", function() { it("copies post-thread in-timeline reactions onto both timelines", function() {
client.clientOpts = { experimentalThreadSupport: true }; // @ts-ignore setting private property
client!.clientOpts = {
...defaultClientOpts,
experimentalThreadSupport: true,
};
const eventPollStartThreadRoot = buildEventPollStartThreadRoot(); const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot); const eventMessageInThread = buildEventMessageInThread(eventPollStartThreadRoot);
@@ -715,7 +779,11 @@ describe("MatrixClient", function() {
}); });
it("sends room state events to the main timeline only", function() { it("sends room state events to the main timeline only", function() {
client.clientOpts = { experimentalThreadSupport: true }; // @ts-ignore setting private property
client!.clientOpts = {
...defaultClientOpts,
experimentalThreadSupport: true,
};
// This is based on recording the events in a real room: // This is based on recording the events in a real room:
const eventPollStartThreadRoot = buildEventPollStartThreadRoot(); const eventPollStartThreadRoot = buildEventPollStartThreadRoot();
@@ -768,7 +836,11 @@ describe("MatrixClient", function() {
}); });
it("sends redactions of reactions to thread responses to thread timeline only", () => { it("sends redactions of reactions to thread responses to thread timeline only", () => {
client.clientOpts = { experimentalThreadSupport: true }; // @ts-ignore setting private property
client!.clientOpts = {
...defaultClientOpts,
experimentalThreadSupport: true,
};
const threadRootEvent = buildEventPollStartThreadRoot(); const threadRootEvent = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(threadRootEvent); const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
@@ -797,7 +869,11 @@ describe("MatrixClient", function() {
}); });
it("sends reply to reply to thread root outside of thread to main timeline only", () => { it("sends reply to reply to thread root outside of thread to main timeline only", () => {
client.clientOpts = { experimentalThreadSupport: true }; // @ts-ignore setting private property
client!.clientOpts = {
...defaultClientOpts,
experimentalThreadSupport: true,
};
const threadRootEvent = buildEventPollStartThreadRoot(); const threadRootEvent = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(threadRootEvent); const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
@@ -826,7 +902,11 @@ describe("MatrixClient", function() {
}); });
it("sends reply to thread responses to main timeline only", () => { it("sends reply to thread responses to main timeline only", () => {
client.clientOpts = { experimentalThreadSupport: true }; // @ts-ignore setting private property
client!.clientOpts = {
...defaultClientOpts,
experimentalThreadSupport: true,
};
const threadRootEvent = buildEventPollStartThreadRoot(); const threadRootEvent = buildEventPollStartThreadRoot();
const eventMessageInThread = buildEventMessageInThread(threadRootEvent); const eventMessageInThread = buildEventMessageInThread(threadRootEvent);
@@ -860,9 +940,9 @@ describe("MatrixClient", function() {
fields: {}, fields: {},
}]; }];
const prom = client.getThirdpartyUser("irc", {}); const prom = client!.getThirdpartyUser("irc", {});
httpBackend.when("GET", "/thirdparty/user/irc").respond(200, response); httpBackend!.when("GET", "/thirdparty/user/irc").respond(200, response);
await httpBackend.flush(); await httpBackend!.flush('');
expect(await prom).toStrictEqual(response); expect(await prom).toStrictEqual(response);
}); });
}); });
@@ -875,9 +955,9 @@ describe("MatrixClient", function() {
fields: {}, fields: {},
}]; }];
const prom = client.getThirdpartyLocation("irc", {}); const prom = client!.getThirdpartyLocation("irc", {});
httpBackend.when("GET", "/thirdparty/location/irc").respond(200, response); httpBackend!.when("GET", "/thirdparty/location/irc").respond(200, response);
await httpBackend.flush(); await httpBackend!.flush('');
expect(await prom).toStrictEqual(response); expect(await prom).toStrictEqual(response);
}); });
}); });
@@ -888,9 +968,10 @@ describe("MatrixClient", function() {
pushers: [], pushers: [],
}; };
const prom = client.getPushers(); const prom = client!.getPushers();
httpBackend.when("GET", "/pushers").respond(200, response); httpBackend!.when("GET", "/_matrix/client/versions").respond(200, {});
await httpBackend.flush(); httpBackend!.when("GET", "/pushers").respond(200, response);
await httpBackend!.flush('');
expect(await prom).toStrictEqual(response); expect(await prom).toStrictEqual(response);
}); });
}); });
@@ -902,12 +983,12 @@ describe("MatrixClient", function() {
left: [], left: [],
}; };
const prom = client.getKeyChanges("old", "new"); const prom = client!.getKeyChanges("old", "new");
httpBackend.when("GET", "/keys/changes").check((req) => { httpBackend!.when("GET", "/keys/changes").check((req) => {
expect(req.queryParams.from).toEqual("old"); expect(req.queryParams?.from).toEqual("old");
expect(req.queryParams.to).toEqual("new"); expect(req.queryParams?.to).toEqual("new");
}).respond(200, response); }).respond(200, response);
await httpBackend.flush(); await httpBackend!.flush('');
expect(await prom).toStrictEqual(response); expect(await prom).toStrictEqual(response);
}); });
}); });
@@ -918,9 +999,9 @@ describe("MatrixClient", function() {
devices: [], devices: [],
}; };
const prom = client.getDevices(); const prom = client!.getDevices();
httpBackend.when("GET", "/devices").respond(200, response); httpBackend!.when("GET", "/devices").respond(200, response);
await httpBackend.flush(); await httpBackend!.flush('');
expect(await prom).toStrictEqual(response); expect(await prom).toStrictEqual(response);
}); });
}); });
@@ -934,9 +1015,9 @@ describe("MatrixClient", function() {
last_seen_ts: 1, last_seen_ts: 1,
}; };
const prom = client.getDevice("DEADBEEF"); const prom = client!.getDevice("DEADBEEF");
httpBackend.when("GET", "/devices/DEADBEEF").respond(200, response); httpBackend!.when("GET", "/devices/DEADBEEF").respond(200, response);
await httpBackend.flush(); await httpBackend!.flush('');
expect(await prom).toStrictEqual(response); expect(await prom).toStrictEqual(response);
}); });
}); });
@@ -947,9 +1028,9 @@ describe("MatrixClient", function() {
threepids: [], threepids: [],
}; };
const prom = client.getThreePids(); const prom = client!.getThreePids();
httpBackend.when("GET", "/account/3pid").respond(200, response); httpBackend!.when("GET", "/account/3pid").respond(200, response);
await httpBackend.flush(); await httpBackend!.flush('');
expect(await prom).toStrictEqual(response); expect(await prom).toStrictEqual(response);
}); });
}); });
@@ -957,9 +1038,9 @@ describe("MatrixClient", function() {
describe("deleteAlias", () => { describe("deleteAlias", () => {
it("should hit the expected API endpoint", async () => { it("should hit the expected API endpoint", async () => {
const response = {}; const response = {};
const prom = client.deleteAlias("#foo:bar"); const prom = client!.deleteAlias("#foo:bar");
httpBackend.when("DELETE", "/directory/room/" + encodeURIComponent("#foo:bar")).respond(200, response); httpBackend!.when("DELETE", "/directory/room/" + encodeURIComponent("#foo:bar")).respond(200, response);
await httpBackend.flush(); await httpBackend!.flush('');
expect(await prom).toStrictEqual(response); expect(await prom).toStrictEqual(response);
}); });
}); });
@@ -967,10 +1048,10 @@ describe("MatrixClient", function() {
describe("deleteRoomTag", () => { describe("deleteRoomTag", () => {
it("should hit the expected API endpoint", async () => { it("should hit the expected API endpoint", async () => {
const response = {}; const response = {};
const prom = client.deleteRoomTag("!roomId:server", "u.tag"); const prom = client!.deleteRoomTag("!roomId:server", "u.tag");
const url = `/user/${encodeURIComponent(userId)}/rooms/${encodeURIComponent("!roomId:server")}/tags/u.tag`; const url = `/user/${encodeURIComponent(userId)}/rooms/${encodeURIComponent("!roomId:server")}/tags/u.tag`;
httpBackend.when("DELETE", url).respond(200, response); httpBackend!.when("DELETE", url).respond(200, response);
await httpBackend.flush(); await httpBackend!.flush('');
expect(await prom).toStrictEqual(response); expect(await prom).toStrictEqual(response);
}); });
}); });
@@ -985,10 +1066,10 @@ describe("MatrixClient", function() {
}, },
}; };
const prom = client.getRoomTags("!roomId:server"); const prom = client!.getRoomTags("!roomId:server");
const url = `/user/${encodeURIComponent(userId)}/rooms/${encodeURIComponent("!roomId:server")}/tags`; const url = `/user/${encodeURIComponent(userId)}/rooms/${encodeURIComponent("!roomId:server")}/tags`;
httpBackend.when("GET", url).respond(200, response); httpBackend!.when("GET", url).respond(200, response);
await httpBackend.flush(); await httpBackend!.flush('');
expect(await prom).toStrictEqual(response); expect(await prom).toStrictEqual(response);
}); });
}); });
@@ -1000,19 +1081,19 @@ describe("MatrixClient", function() {
submit_url: "https://foobar.matrix/_matrix/matrix", submit_url: "https://foobar.matrix/_matrix/matrix",
}; };
httpBackend.when("GET", "/_matrix/client/versions").respond(200, { httpBackend!.when("GET", "/_matrix/client/versions").respond(200, {
versions: ["r0.6.0"], versions: ["r0.6.0"],
}); });
const prom = client.requestRegisterEmailToken("bob@email", "secret", 1); const prom = client!.requestRegisterEmailToken("bob@email", "secret", 1);
httpBackend.when("POST", "/register/email/requestToken").check(req => { httpBackend!.when("POST", "/register/email/requestToken").check(req => {
expect(req.data).toStrictEqual({ expect(req.data).toStrictEqual({
email: "bob@email", email: "bob@email",
client_secret: "secret", client_secret: "secret",
send_attempt: 1, send_attempt: 1,
}); });
}).respond(200, response); }).respond(200, response);
await httpBackend.flush(); await httpBackend!.flush('');
expect(await prom).toStrictEqual(response); expect(await prom).toStrictEqual(response);
}); });
}); });
@@ -1021,11 +1102,11 @@ describe("MatrixClient", function() {
it("should supply an id_access_token", async () => { it("should supply an id_access_token", async () => {
const targetEmail = "gerald@example.org"; const targetEmail = "gerald@example.org";
httpBackend.when("GET", "/_matrix/client/versions").respond(200, { httpBackend!.when("GET", "/_matrix/client/versions").respond(200, {
versions: ["r0.6.0"], versions: ["r0.6.0"],
}); });
httpBackend.when("POST", "/invite").check(req => { httpBackend!.when("POST", "/invite").check(req => {
expect(req.data).toStrictEqual({ expect(req.data).toStrictEqual({
id_server: idServerDomain, id_server: idServerDomain,
id_access_token: identityAccessToken, id_access_token: identityAccessToken,
@@ -1034,8 +1115,8 @@ describe("MatrixClient", function() {
}); });
}).respond(200, {}); }).respond(200, {});
const prom = client.inviteByThreePid("!room:example.org", "email", targetEmail); const prom = client!.inviteByThreePid("!room:example.org", "email", targetEmail);
await httpBackend.flush(); await httpBackend!.flush('');
await prom; // returns empty object, so no validation needed await prom; // returns empty object, so no validation needed
}); });
}); });
@@ -1055,11 +1136,11 @@ describe("MatrixClient", function() {
}], }],
}; };
httpBackend.when("GET", "/_matrix/client/versions").respond(200, { httpBackend!.when("GET", "/_matrix/client/versions").respond(200, {
versions: ["r0.6.0"], versions: ["r0.6.0"],
}); });
httpBackend.when("POST", "/createRoom").check(req => { httpBackend!.when("POST", "/createRoom").check(req => {
expect(req.data).toMatchObject({ expect(req.data).toMatchObject({
invite_3pid: expect.arrayContaining([{ invite_3pid: expect.arrayContaining([{
...input.invite_3pid[0], ...input.invite_3pid[0],
@@ -1069,8 +1150,31 @@ describe("MatrixClient", function() {
expect(req.data.invite_3pid.length).toBe(1); expect(req.data.invite_3pid.length).toBe(1);
}).respond(200, response); }).respond(200, response);
const prom = client.createRoom(input); const prom = client!.createRoom(input);
await httpBackend.flush(); await httpBackend!.flush('');
expect(await prom).toStrictEqual(response);
});
});
describe("requestLoginToken", () => {
it("should hit the expected API endpoint with UIA", async () => {
const response = {};
const uiaData = {};
const prom = client!.requestLoginToken(uiaData);
httpBackend!
.when("POST", "/unstable/org.matrix.msc3882/login/token", { auth: uiaData })
.respond(200, response);
await httpBackend!.flush('');
expect(await prom).toStrictEqual(response);
});
it("should hit the expected API endpoint without UIA", async () => {
const response = {};
const prom = client!.requestLoginToken();
httpBackend!
.when("POST", "/unstable/org.matrix.msc3882/login/token", {})
.respond(200, response);
await httpBackend!.flush('');
expect(await prom).toStrictEqual(response); expect(await prom).toStrictEqual(response);
}); });
}); });

View File

@@ -5,10 +5,12 @@ import { MatrixClient } from "../../src/matrix";
import { MatrixScheduler } from "../../src/scheduler"; import { MatrixScheduler } from "../../src/scheduler";
import { MemoryStore } from "../../src/store/memory"; import { MemoryStore } from "../../src/store/memory";
import { MatrixError } from "../../src/http-api"; import { MatrixError } from "../../src/http-api";
import { ICreateClientOpts } from "../../src/client";
import { IStore } from "../../src/store";
describe("MatrixClient opts", function() { describe("MatrixClient opts", function() {
const baseUrl = "http://localhost.or.something"; const baseUrl = "http://localhost.or.something";
let httpBackend = null; let httpBackend = new HttpBackend();
const userId = "@alice:localhost"; const userId = "@alice:localhost";
const userB = "@bob:localhost"; const userB = "@bob:localhost";
const accessToken = "aseukfgwef"; const accessToken = "aseukfgwef";
@@ -67,7 +69,7 @@ describe("MatrixClient opts", function() {
let client; let client;
beforeEach(function() { beforeEach(function() {
client = new MatrixClient({ client = new MatrixClient({
request: httpBackend.requestFn, request: httpBackend.requestFn as unknown as ICreateClientOpts['request'],
store: undefined, store: undefined,
baseUrl: baseUrl, baseUrl: baseUrl,
userId: userId, userId: userId,
@@ -99,7 +101,7 @@ describe("MatrixClient opts", function() {
]; ];
client.on("event", function(event) { client.on("event", function(event) {
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual( expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
-1, "Recv unexpected event type: " + event.getType(), -1,
); );
expectedEventTypes.splice( expectedEventTypes.splice(
expectedEventTypes.indexOf(event.getType()), 1, expectedEventTypes.indexOf(event.getType()), 1,
@@ -118,7 +120,7 @@ describe("MatrixClient opts", function() {
utils.syncPromise(client), utils.syncPromise(client),
]); ]);
expect(expectedEventTypes.length).toEqual( expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes, 0,
); );
}); });
}); });
@@ -127,8 +129,8 @@ describe("MatrixClient opts", function() {
let client; let client;
beforeEach(function() { beforeEach(function() {
client = new MatrixClient({ client = new MatrixClient({
request: httpBackend.requestFn, request: httpBackend.requestFn as unknown as ICreateClientOpts['request'],
store: new MemoryStore(), store: new MemoryStore() as IStore,
baseUrl: baseUrl, baseUrl: baseUrl,
userId: userId, userId: userId,
accessToken: accessToken, accessToken: accessToken,
@@ -146,7 +148,7 @@ describe("MatrixClient opts", function() {
error: "Ruh roh", error: "Ruh roh",
})); }));
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) { client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
expect(false).toBe(true, "sendTextMessage resolved but shouldn't"); expect(false).toBe(true);
}, function(err) { }, function(err) {
expect(err.errcode).toEqual("M_SOMETHING"); expect(err.errcode).toEqual("M_SOMETHING");
done(); done();

View 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" });
});
});

View File

@@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { EventStatus, RoomEvent, MatrixClient } from "../../src/matrix"; import HttpBackend from "matrix-mock-request";
import { MatrixScheduler } from "../../src/scheduler";
import { EventStatus, RoomEvent, MatrixClient, MatrixScheduler } from "../../src/matrix";
import { Room } from "../../src/models/room"; import { Room } from "../../src/models/room";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
describe("MatrixClient retrying", function() { describe("MatrixClient retrying", function() {
let client: MatrixClient = null;
let httpBackend: TestClient["httpBackend"] = null;
let scheduler;
const userId = "@alice:localhost"; const userId = "@alice:localhost";
const accessToken = "aseukfgwef"; const accessToken = "aseukfgwef";
const roomId = "!room:here"; const roomId = "!room:here";
let room: Room; let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
let room: Room | undefined;
beforeEach(function() { const setupTests = (): [MatrixClient, HttpBackend, Room] => {
scheduler = new MatrixScheduler(); const scheduler = new MatrixScheduler();
const testClient = new TestClient( const testClient = new TestClient(
userId, userId,
"DEVICE", "DEVICE",
@@ -37,15 +37,21 @@ describe("MatrixClient retrying", function() {
undefined, undefined,
{ scheduler }, { scheduler },
); );
httpBackend = testClient.httpBackend; const httpBackend = testClient.httpBackend;
client = testClient.client; const client = testClient.client;
room = new Room(roomId, client, userId); const room = new Room(roomId, client, userId);
client.store.storeRoom(room); client!.store.storeRoom(room);
return [client, httpBackend, room];
};
beforeEach(function() {
[client, httpBackend, room] = setupTests();
}); });
afterEach(function() { afterEach(function() {
httpBackend.verifyNoOutstandingExpectation(); httpBackend!.verifyNoOutstandingExpectation();
return httpBackend.stop(); return httpBackend!.stop();
}); });
xit("should retry according to MatrixScheduler.retryFn", function() { xit("should retry according to MatrixScheduler.retryFn", function() {
@@ -66,7 +72,7 @@ describe("MatrixClient retrying", function() {
it("should mark events as EventStatus.CANCELLED when cancelled", function() { it("should mark events as EventStatus.CANCELLED when cancelled", function() {
// send a couple of events; the second will be queued // send a couple of events; the second will be queued
const p1 = client.sendMessage(roomId, { const p1 = client!.sendMessage(roomId, {
"msgtype": "m.text", "msgtype": "m.text",
"body": "m1", "body": "m1",
}).then(function() { }).then(function() {
@@ -79,13 +85,13 @@ describe("MatrixClient retrying", function() {
// XXX: it turns out that the promise returned by this message // XXX: it turns out that the promise returned by this message
// never gets resolved. // never gets resolved.
// https://github.com/matrix-org/matrix-js-sdk/issues/496 // https://github.com/matrix-org/matrix-js-sdk/issues/496
client.sendMessage(roomId, { client!.sendMessage(roomId, {
"msgtype": "m.text", "msgtype": "m.text",
"body": "m2", "body": "m2",
}); });
// both events should be in the timeline at this point // both events should be in the timeline at this point
const tl = room.getLiveTimeline().getEvents(); const tl = room!.getLiveTimeline().getEvents();
expect(tl.length).toEqual(2); expect(tl.length).toEqual(2);
const ev1 = tl[0]; const ev1 = tl[0];
const ev2 = tl[1]; const ev2 = tl[1];
@@ -94,24 +100,24 @@ describe("MatrixClient retrying", function() {
expect(ev2.status).toEqual(EventStatus.SENDING); expect(ev2.status).toEqual(EventStatus.SENDING);
// the first message should get sent, and the second should get queued // the first message should get sent, and the second should get queued
httpBackend.when("PUT", "/send/m.room.message/").check(function() { httpBackend!.when("PUT", "/send/m.room.message/").check(function() {
// ev2 should now have been queued // ev2 should now have been queued
expect(ev2.status).toEqual(EventStatus.QUEUED); expect(ev2.status).toEqual(EventStatus.QUEUED);
// now we can cancel the second and check everything looks sane // now we can cancel the second and check everything looks sane
client.cancelPendingEvent(ev2); client!.cancelPendingEvent(ev2);
expect(ev2.status).toEqual(EventStatus.CANCELLED); expect(ev2.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(1); expect(tl.length).toEqual(1);
// shouldn't be able to cancel the first message yet // shouldn't be able to cancel the first message yet
expect(function() { expect(function() {
client.cancelPendingEvent(ev1); client!.cancelPendingEvent(ev1);
}).toThrow(); }).toThrow();
}).respond(400); // fail the first message }).respond(400); // fail the first message
// wait for the localecho of ev1 to be updated // wait for the localecho of ev1 to be updated
const p3 = new Promise<void>((resolve, reject) => { const p3 = new Promise<void>((resolve, reject) => {
room.on(RoomEvent.LocalEchoUpdated, (ev0) => { room!.on(RoomEvent.LocalEchoUpdated, (ev0) => {
if (ev0 === ev1) { if (ev0 === ev1) {
resolve(); resolve();
} }
@@ -121,7 +127,7 @@ describe("MatrixClient retrying", function() {
expect(tl.length).toEqual(1); expect(tl.length).toEqual(1);
// cancel the first message // cancel the first message
client.cancelPendingEvent(ev1); client!.cancelPendingEvent(ev1);
expect(ev1.status).toEqual(EventStatus.CANCELLED); expect(ev1.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(0); expect(tl.length).toEqual(0);
}); });
@@ -129,7 +135,7 @@ describe("MatrixClient retrying", function() {
return Promise.all([ return Promise.all([
p1, p1,
p3, p3,
httpBackend.flushAllExpected(), httpBackend!.flushAllExpected(),
]); ]);
}); });

View File

@@ -1,16 +1,35 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import HttpBackend from "matrix-mock-request";
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
import { EventStatus } from "../../src/models/event"; import { EventStatus } from "../../src/models/event";
import { RoomEvent } from "../../src"; import { ClientEvent, IEvent, MatrixClient, RoomEvent } from "../../src";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
describe("MatrixClient room timelines", function() { describe("MatrixClient room timelines", function() {
let client = null;
let httpBackend = null;
const userId = "@alice:localhost"; const userId = "@alice:localhost";
const userName = "Alice"; const userName = "Alice";
const accessToken = "aseukfgwef"; const accessToken = "aseukfgwef";
const roomId = "!foo:bar"; const roomId = "!foo:bar";
const otherUserId = "@bob:localhost"; const otherUserId = "@bob:localhost";
let client: MatrixClient | undefined;
let httpBackend: HttpBackend | undefined;
const USER_MEMBERSHIP_EVENT = utils.mkMembership({ const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName, room: roomId, mship: "join", user: userId, name: userName,
}); });
@@ -55,8 +74,7 @@ describe("MatrixClient room timelines", function() {
}, },
}; };
function setNextSyncData(events) { function setNextSyncData(events: Partial<IEvent>[] = []) {
events = events || [];
NEXT_SYNC_DATA = { NEXT_SYNC_DATA = {
next_batch: "n", next_batch: "n",
presence: { events: [] }, presence: { events: [] },
@@ -77,19 +95,9 @@ describe("MatrixClient room timelines", function() {
throw new Error("setNextSyncData only works with one room id"); throw new Error("setNextSyncData only works with one room id");
} }
if (e.state_key) { if (e.state_key) {
if (e.__prev_event === undefined) {
throw new Error(
"setNextSyncData needs the prev state set to '__prev_event' " +
"for " + e.type,
);
}
if (e.__prev_event !== null) {
// push the previous state for this event type
NEXT_SYNC_DATA.rooms.join[roomId].state.events.push(e.__prev_event);
}
// push the current // push the current
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e); NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
} else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) { } else if (["m.typing", "m.receipt"].indexOf(e.type!) !== -1) {
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e); NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
} else { } else {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e); NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
@@ -97,7 +105,7 @@ describe("MatrixClient room timelines", function() {
}); });
} }
beforeEach(async function() { const setupTestClient = (): [MatrixClient, HttpBackend] => {
// these tests should work with or without timelineSupport // these tests should work with or without timelineSupport
const testClient = new TestClient( const testClient = new TestClient(
userId, userId,
@@ -106,41 +114,46 @@ describe("MatrixClient room timelines", function() {
undefined, undefined,
{ timelineSupport: true }, { timelineSupport: true },
); );
httpBackend = testClient.httpBackend; const httpBackend = testClient.httpBackend;
client = testClient.client; const client = testClient.client;
setNextSyncData(); setNextSyncData();
httpBackend.when("GET", "/versions").respond(200, {}); httpBackend!.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {}); httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" }); httpBackend!.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA); httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, function() { httpBackend!.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA; return NEXT_SYNC_DATA;
}); });
client.startClient(); client!.startClient();
return [client!, httpBackend];
};
beforeEach(async function() {
[client!, httpBackend] = setupTestClient();
await httpBackend.flush("/versions"); await httpBackend.flush("/versions");
await httpBackend.flush("/pushrules"); await httpBackend.flush("/pushrules");
await httpBackend.flush("/filter"); await httpBackend.flush("/filter");
}); });
afterEach(function() { afterEach(function() {
httpBackend.verifyNoOutstandingExpectation(); httpBackend!.verifyNoOutstandingExpectation();
client.stopClient(); client!.stopClient();
return httpBackend.stop(); return httpBackend!.stop();
}); });
describe("local echo events", function() { describe("local echo events", function() {
it("should be added immediately after calling MatrixClient.sendEvent " + it("should be added immediately after calling MatrixClient.sendEvent " +
"with EventStatus.SENDING and the right event.sender", function(done) { "with EventStatus.SENDING and the right event.sender", function(done) {
client.on("sync", function(state) { client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") { if (state !== "PREPARED") {
return; return;
} }
const room = client.getRoom(roomId); const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1); expect(room.timeline.length).toEqual(1);
client.sendTextMessage(roomId, "I am a fish", "txn1"); client!.sendTextMessage(roomId, "I am a fish", "txn1");
// check it was added // check it was added
expect(room.timeline.length).toEqual(2); expect(room.timeline.length).toEqual(2);
// check status // check status
@@ -150,68 +163,68 @@ describe("MatrixClient room timelines", function() {
expect(member.userId).toEqual(userId); expect(member.userId).toEqual(userId);
expect(member.name).toEqual(userName); expect(member.name).toEqual(userName);
httpBackend.flush("/sync", 1).then(function() { httpBackend!.flush("/sync", 1).then(function() {
done(); done();
}); });
}); });
httpBackend.flush("/sync", 1); httpBackend!.flush("/sync", 1);
}); });
it("should be updated correctly when the send request finishes " + it("should be updated correctly when the send request finishes " +
"BEFORE the event comes down the event stream", function(done) { "BEFORE the event comes down the event stream", function(done) {
const eventId = "$foo:bar"; const eventId = "$foo:bar";
httpBackend.when("PUT", "/txn1").respond(200, { httpBackend!.when("PUT", "/txn1").respond(200, {
event_id: eventId, event_id: eventId,
}); });
const ev = utils.mkMessage({ const ev = utils.mkMessage({
body: "I am a fish", user: userId, room: roomId, msg: "I am a fish", user: userId, room: roomId,
}); });
ev.event_id = eventId; ev.event_id = eventId;
ev.unsigned = { transaction_id: "txn1" }; ev.unsigned = { transaction_id: "txn1" };
setNextSyncData([ev]); setNextSyncData([ev]);
client.on("sync", function(state) { client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") { if (state !== "PREPARED") {
return; return;
} }
const room = client.getRoom(roomId); const room = client!.getRoom(roomId)!;
client.sendTextMessage(roomId, "I am a fish", "txn1").then( client!.sendTextMessage(roomId, "I am a fish", "txn1").then(
function() { function() {
expect(room.timeline[1].getId()).toEqual(eventId); expect(room.timeline[1].getId()).toEqual(eventId);
httpBackend.flush("/sync", 1).then(function() { httpBackend!.flush("/sync", 1).then(function() {
expect(room.timeline[1].getId()).toEqual(eventId); expect(room.timeline[1].getId()).toEqual(eventId);
done(); done();
}); });
}); });
httpBackend.flush("/txn1", 1); httpBackend!.flush("/txn1", 1);
}); });
httpBackend.flush("/sync", 1); httpBackend!.flush("/sync", 1);
}); });
it("should be updated correctly when the send request finishes " + it("should be updated correctly when the send request finishes " +
"AFTER the event comes down the event stream", function(done) { "AFTER the event comes down the event stream", function(done) {
const eventId = "$foo:bar"; const eventId = "$foo:bar";
httpBackend.when("PUT", "/txn1").respond(200, { httpBackend!.when("PUT", "/txn1").respond(200, {
event_id: eventId, event_id: eventId,
}); });
const ev = utils.mkMessage({ const ev = utils.mkMessage({
body: "I am a fish", user: userId, room: roomId, msg: "I am a fish", user: userId, room: roomId,
}); });
ev.event_id = eventId; ev.event_id = eventId;
ev.unsigned = { transaction_id: "txn1" }; ev.unsigned = { transaction_id: "txn1" };
setNextSyncData([ev]); setNextSyncData([ev]);
client.on("sync", function(state) { client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") { if (state !== "PREPARED") {
return; return;
} }
const room = client.getRoom(roomId); const room = client!.getRoom(roomId)!;
const promise = client.sendTextMessage(roomId, "I am a fish", "txn1"); const promise = client!.sendTextMessage(roomId, "I am a fish", "txn1");
httpBackend.flush("/sync", 1).then(function() { httpBackend!.flush("/sync", 1).then(function() {
expect(room.timeline.length).toEqual(2); expect(room.timeline.length).toEqual(2);
httpBackend.flush("/txn1", 1); httpBackend!.flush("/txn1", 1);
promise.then(function() { promise.then(function() {
expect(room.timeline.length).toEqual(2); expect(room.timeline.length).toEqual(2);
expect(room.timeline[1].getId()).toEqual(eventId); expect(room.timeline[1].getId()).toEqual(eventId);
@@ -219,7 +232,7 @@ describe("MatrixClient room timelines", function() {
}); });
}); });
}); });
httpBackend.flush("/sync", 1); httpBackend!.flush("/sync", 1);
}); });
}); });
@@ -229,7 +242,7 @@ describe("MatrixClient room timelines", function() {
beforeEach(function() { beforeEach(function() {
sbEvents = []; sbEvents = [];
httpBackend.when("GET", "/messages").respond(200, function() { httpBackend!.when("GET", "/messages").respond(200, function() {
return { return {
chunk: sbEvents, chunk: sbEvents,
start: "pagin_start", start: "pagin_start",
@@ -240,26 +253,26 @@ describe("MatrixClient room timelines", function() {
it("should set Room.oldState.paginationToken to null at the start" + it("should set Room.oldState.paginationToken to null at the start" +
" of the timeline.", function(done) { " of the timeline.", function(done) {
client.on("sync", function(state) { client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") { if (state !== "PREPARED") {
return; return;
} }
const room = client.getRoom(roomId); const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1); expect(room.timeline.length).toEqual(1);
client.scrollback(room).then(function() { client!.scrollback(room).then(function() {
expect(room.timeline.length).toEqual(1); expect(room.timeline.length).toEqual(1);
expect(room.oldState.paginationToken).toBe(null); expect(room.oldState.paginationToken).toBe(null);
// still have a sync to flush // still have a sync to flush
httpBackend.flush("/sync", 1).then(() => { httpBackend!.flush("/sync", 1).then(() => {
done(); done();
}); });
}); });
httpBackend.flush("/messages", 1); httpBackend!.flush("/messages", 1);
}); });
httpBackend.flush("/sync", 1); httpBackend!.flush("/sync", 1);
}); });
it("should set the right event.sender values", function(done) { it("should set the right event.sender values", function(done) {
@@ -275,7 +288,7 @@ describe("MatrixClient room timelines", function() {
// make an m.room.member event for alice's join // make an m.room.member event for alice's join
const joinMshipEvent = utils.mkMembership({ const joinMshipEvent = utils.mkMembership({
mship: "join", user: userId, room: roomId, name: "Old Alice", mship: "join", user: userId, room: roomId, name: "Old Alice",
url: null, url: undefined,
}); });
// make an m.room.member event with prev_content for alice's nick // make an m.room.member event with prev_content for alice's nick
@@ -286,7 +299,7 @@ describe("MatrixClient room timelines", function() {
}); });
oldMshipEvent.prev_content = { oldMshipEvent.prev_content = {
displayname: "Old Alice", displayname: "Old Alice",
avatar_url: null, avatar_url: undefined,
membership: "join", membership: "join",
}; };
@@ -303,15 +316,15 @@ describe("MatrixClient room timelines", function() {
joinMshipEvent, joinMshipEvent,
]; ];
client.on("sync", function(state) { client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") { if (state !== "PREPARED") {
return; return;
} }
const room = client.getRoom(roomId); const room = client!.getRoom(roomId)!;
// sync response // sync response
expect(room.timeline.length).toEqual(1); expect(room.timeline.length).toEqual(1);
client.scrollback(room).then(function() { client!.scrollback(room).then(function() {
expect(room.timeline.length).toEqual(5); expect(room.timeline.length).toEqual(5);
const joinMsg = room.timeline[0]; const joinMsg = room.timeline[0];
expect(joinMsg.sender.name).toEqual("Old Alice"); expect(joinMsg.sender.name).toEqual("Old Alice");
@@ -321,14 +334,14 @@ describe("MatrixClient room timelines", function() {
expect(newMsg.sender.name).toEqual(userName); expect(newMsg.sender.name).toEqual(userName);
// still have a sync to flush // still have a sync to flush
httpBackend.flush("/sync", 1).then(() => { httpBackend!.flush("/sync", 1).then(() => {
done(); done();
}); });
}); });
httpBackend.flush("/messages", 1); httpBackend!.flush("/messages", 1);
}); });
httpBackend.flush("/sync", 1); httpBackend!.flush("/sync", 1);
}); });
it("should add it them to the right place in the timeline", function(done) { it("should add it them to the right place in the timeline", function(done) {
@@ -342,27 +355,27 @@ describe("MatrixClient room timelines", function() {
}), }),
]; ];
client.on("sync", function(state) { client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") { if (state !== "PREPARED") {
return; return;
} }
const room = client.getRoom(roomId); const room = client!.getRoom(roomId)!;
expect(room.timeline.length).toEqual(1); expect(room.timeline.length).toEqual(1);
client.scrollback(room).then(function() { client!.scrollback(room).then(function() {
expect(room.timeline.length).toEqual(3); expect(room.timeline.length).toEqual(3);
expect(room.timeline[0].event).toEqual(sbEvents[1]); expect(room.timeline[0].event).toEqual(sbEvents[1]);
expect(room.timeline[1].event).toEqual(sbEvents[0]); expect(room.timeline[1].event).toEqual(sbEvents[0]);
// still have a sync to flush // still have a sync to flush
httpBackend.flush("/sync", 1).then(() => { httpBackend!.flush("/sync", 1).then(() => {
done(); done();
}); });
}); });
httpBackend.flush("/messages", 1); httpBackend!.flush("/messages", 1);
}); });
httpBackend.flush("/sync", 1); httpBackend!.flush("/sync", 1);
}); });
it("should use 'end' as the next pagination token", function(done) { it("should use 'end' as the next pagination token", function(done) {
@@ -373,25 +386,25 @@ describe("MatrixClient room timelines", function() {
}), }),
]; ];
client.on("sync", function(state) { client!.on(ClientEvent.Sync, function(state) {
if (state !== "PREPARED") { if (state !== "PREPARED") {
return; return;
} }
const room = client.getRoom(roomId); const room = client!.getRoom(roomId)!;
expect(room.oldState.paginationToken).toBeTruthy(); expect(room.oldState.paginationToken).toBeTruthy();
client.scrollback(room, 1).then(function() { client!.scrollback(room, 1).then(function() {
expect(room.oldState.paginationToken).toEqual(sbEndTok); expect(room.oldState.paginationToken).toEqual(sbEndTok);
}); });
httpBackend.flush("/messages", 1).then(function() { httpBackend!.flush("/messages", 1).then(function() {
// still have a sync to flush // still have a sync to flush
httpBackend.flush("/sync", 1).then(() => { httpBackend!.flush("/sync", 1).then(() => {
done(); done();
}); });
}); });
}); });
httpBackend.flush("/sync", 1); httpBackend!.flush("/sync", 1);
}); });
}); });
@@ -404,23 +417,23 @@ describe("MatrixClient room timelines", function() {
setNextSyncData(eventData); setNextSyncData(eventData);
return Promise.all([ return Promise.all([
httpBackend.flush("/sync", 1), httpBackend!.flush("/sync", 1),
utils.syncPromise(client), utils.syncPromise(client!),
]).then(() => { ]).then(() => {
const room = client.getRoom(roomId); const room = client!.getRoom(roomId)!;
let index = 0; let index = 0;
client.on("Room.timeline", function(event, rm, toStart) { client!.on(RoomEvent.Timeline, function(event, rm, toStart) {
expect(toStart).toBe(false); expect(toStart).toBe(false);
expect(rm).toEqual(room); expect(rm).toEqual(room);
expect(event.event).toEqual(eventData[index]); expect(event.event).toEqual(eventData[index]);
index += 1; index += 1;
}); });
httpBackend.flush("/messages", 1); httpBackend!.flush("/messages", 1);
return Promise.all([ return Promise.all([
httpBackend.flush("/sync", 1), httpBackend!.flush("/sync", 1),
utils.syncPromise(client), utils.syncPromise(client!),
]).then(function() { ]).then(function() {
expect(index).toEqual(2); expect(index).toEqual(2);
expect(room.timeline.length).toEqual(3); expect(room.timeline.length).toEqual(3);
@@ -442,17 +455,16 @@ describe("MatrixClient room timelines", function() {
}), }),
utils.mkMessage({ user: userId, room: roomId }), utils.mkMessage({ user: userId, room: roomId }),
]; ];
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
setNextSyncData(eventData); setNextSyncData(eventData);
return Promise.all([ return Promise.all([
httpBackend.flush("/sync", 1), httpBackend!.flush("/sync", 1),
utils.syncPromise(client), utils.syncPromise(client!),
]).then(() => { ]).then(() => {
const room = client.getRoom(roomId); const room = client!.getRoom(roomId)!;
return Promise.all([ return Promise.all([
httpBackend.flush("/sync", 1), httpBackend!.flush("/sync", 1),
utils.syncPromise(client), utils.syncPromise(client!),
]).then(function() { ]).then(function() {
const preNameEvent = room.timeline[room.timeline.length - 3]; const preNameEvent = room.timeline[room.timeline.length - 3];
const postNameEvent = room.timeline[room.timeline.length - 1]; const postNameEvent = room.timeline[room.timeline.length - 1];
@@ -468,22 +480,21 @@ describe("MatrixClient room timelines", function() {
name: "Room 2", name: "Room 2",
}, },
}); });
secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
setNextSyncData([secondRoomNameEvent]); setNextSyncData([secondRoomNameEvent]);
return Promise.all([ return Promise.all([
httpBackend.flush("/sync", 1), httpBackend!.flush("/sync", 1),
utils.syncPromise(client), utils.syncPromise(client!),
]).then(() => { ]).then(() => {
const room = client.getRoom(roomId); const room = client!.getRoom(roomId)!;
let nameEmitCount = 0; let nameEmitCount = 0;
client.on("Room.name", function(rm) { client!.on(RoomEvent.Name, function(rm) {
nameEmitCount += 1; nameEmitCount += 1;
}); });
return Promise.all([ return Promise.all([
httpBackend.flush("/sync", 1), httpBackend!.flush("/sync", 1),
utils.syncPromise(client), utils.syncPromise(client!),
]).then(function() { ]).then(function() {
expect(nameEmitCount).toEqual(1); expect(nameEmitCount).toEqual(1);
expect(room.name).toEqual("Room 2"); expect(room.name).toEqual("Room 2");
@@ -493,12 +504,11 @@ describe("MatrixClient room timelines", function() {
name: "Room 3", name: "Room 3",
}, },
}); });
thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
setNextSyncData([thirdRoomNameEvent]); setNextSyncData([thirdRoomNameEvent]);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA); httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
return Promise.all([ return Promise.all([
httpBackend.flush("/sync", 1), httpBackend!.flush("/sync", 1),
utils.syncPromise(client), utils.syncPromise(client!),
]); ]);
}).then(function() { }).then(function() {
expect(nameEmitCount).toEqual(2); expect(nameEmitCount).toEqual(2);
@@ -518,26 +528,24 @@ describe("MatrixClient room timelines", function() {
user: userC, room: roomId, mship: "invite", skey: userD, user: userC, room: roomId, mship: "invite", skey: userD,
}), }),
]; ];
eventData[0].__prev_event = null;
eventData[1].__prev_event = null;
setNextSyncData(eventData); setNextSyncData(eventData);
return Promise.all([ return Promise.all([
httpBackend.flush("/sync", 1), httpBackend!.flush("/sync", 1),
utils.syncPromise(client), utils.syncPromise(client!),
]).then(() => { ]).then(() => {
const room = client.getRoom(roomId); const room = client!.getRoom(roomId)!;
return Promise.all([ return Promise.all([
httpBackend.flush("/sync", 1), httpBackend!.flush("/sync", 1),
utils.syncPromise(client), utils.syncPromise(client!),
]).then(function() { ]).then(function() {
expect(room.currentState.getMembers().length).toEqual(4); expect(room.currentState.getMembers().length).toEqual(4);
expect(room.currentState.getMember(userC).name).toEqual("C"); expect(room.currentState.getMember(userC)!.name).toEqual("C");
expect(room.currentState.getMember(userC).membership).toEqual( expect(room.currentState.getMember(userC)!.membership).toEqual(
"join", "join",
); );
expect(room.currentState.getMember(userD).name).toEqual(userD); expect(room.currentState.getMember(userD)!.name).toEqual(userD);
expect(room.currentState.getMember(userD).membership).toEqual( expect(room.currentState.getMember(userD)!.membership).toEqual(
"invite", "invite",
); );
}); });
@@ -554,26 +562,26 @@ describe("MatrixClient room timelines", function() {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true; NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
return Promise.all([ return Promise.all([
httpBackend.flush("/versions", 1), httpBackend!.flush("/versions", 1),
httpBackend.flush("/sync", 1), httpBackend!.flush("/sync", 1),
utils.syncPromise(client), utils.syncPromise(client!),
]).then(() => { ]).then(() => {
const room = client.getRoom(roomId); const room = client!.getRoom(roomId)!;
httpBackend.flush("/messages", 1); httpBackend!.flush("/messages", 1);
return Promise.all([ return Promise.all([
httpBackend.flush("/sync", 1), httpBackend!.flush("/sync", 1),
utils.syncPromise(client), utils.syncPromise(client!),
]).then(function() { ]).then(function() {
expect(room.timeline.length).toEqual(1); expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(eventData[0]); expect(room.timeline[0].event).toEqual(eventData[0]);
expect(room.currentState.getMembers().length).toEqual(2); expect(room.currentState.getMembers().length).toEqual(2);
expect(room.currentState.getMember(userId).name).toEqual(userName); expect(room.currentState.getMember(userId)!.name).toEqual(userName);
expect(room.currentState.getMember(userId).membership).toEqual( expect(room.currentState.getMember(userId)!.membership).toEqual(
"join", "join",
); );
expect(room.currentState.getMember(otherUserId).name).toEqual("Bob"); expect(room.currentState.getMember(otherUserId)!.name).toEqual("Bob");
expect(room.currentState.getMember(otherUserId).membership).toEqual( expect(room.currentState.getMember(otherUserId)!.membership).toEqual(
"join", "join",
); );
}); });
@@ -588,21 +596,21 @@ describe("MatrixClient room timelines", function() {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true; NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
return Promise.all([ return Promise.all([
httpBackend.flush("/sync", 1), httpBackend!.flush("/sync", 1),
utils.syncPromise(client), utils.syncPromise(client!),
]).then(() => { ]).then(() => {
const room = client.getRoom(roomId); const room = client!.getRoom(roomId)!;
let emitCount = 0; let emitCount = 0;
client.on("Room.timelineReset", function(emitRoom) { client!.on(RoomEvent.TimelineReset, function(emitRoom) {
expect(emitRoom).toEqual(room); expect(emitRoom).toEqual(room);
emitCount++; emitCount++;
}); });
httpBackend.flush("/messages", 1); httpBackend!.flush("/messages", 1);
return Promise.all([ return Promise.all([
httpBackend.flush("/sync", 1), httpBackend!.flush("/sync", 1),
utils.syncPromise(client), utils.syncPromise(client!),
]).then(function() { ]).then(function() {
expect(emitCount).toEqual(1); expect(emitCount).toEqual(1);
}); });
@@ -618,7 +626,7 @@ describe("MatrixClient room timelines", function() {
]; ];
const contextUrl = `/rooms/${encodeURIComponent(roomId)}/context/` + const contextUrl = `/rooms/${encodeURIComponent(roomId)}/context/` +
`${encodeURIComponent(initialSyncEventData[2].event_id)}`; `${encodeURIComponent(initialSyncEventData[2].event_id!)}`;
const contextResponse = { const contextResponse = {
start: "start_token", start: "start_token",
events_before: [initialSyncEventData[1], initialSyncEventData[0]], events_before: [initialSyncEventData[1], initialSyncEventData[0]],
@@ -636,19 +644,19 @@ describe("MatrixClient room timelines", function() {
// Create a room from the sync // Create a room from the sync
await Promise.all([ await Promise.all([
httpBackend.flushAllExpected(), httpBackend!.flushAllExpected(),
utils.syncPromise(client, 1), utils.syncPromise(client!, 1),
]); ]);
// Get the room after the first sync so the room is created // Get the room after the first sync so the room is created
room = client.getRoom(roomId); room = client!.getRoom(roomId)!;
expect(room).toBeTruthy(); expect(room).toBeTruthy();
}); });
it('should clear and refresh messages in timeline', async () => { it('should clear and refresh messages in timeline', async () => {
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()` // `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
// to construct a new timeline from. // to construct a new timeline from.
httpBackend.when("GET", contextUrl) httpBackend!.when("GET", contextUrl)
.respond(200, function() { .respond(200, function() {
// The timeline should be cleared at this point in the refresh // The timeline should be cleared at this point in the refresh
expect(room.timeline.length).toEqual(0); expect(room.timeline.length).toEqual(0);
@@ -659,7 +667,7 @@ describe("MatrixClient room timelines", function() {
// Refresh the timeline. // Refresh the timeline.
await Promise.all([ await Promise.all([
room.refreshLiveTimeline(), room.refreshLiveTimeline(),
httpBackend.flushAllExpected(), httpBackend!.flushAllExpected(),
]); ]);
// Make sure the message are visible // Make sure the message are visible
@@ -681,7 +689,7 @@ describe("MatrixClient room timelines", function() {
// middle of all of this refresh timeline logic. We want to make // middle of all of this refresh timeline logic. We want to make
// sure the sync pagination still works as expected after messing // sure the sync pagination still works as expected after messing
// the refresh timline logic messes with the pagination tokens. // the refresh timline logic messes with the pagination tokens.
httpBackend.when("GET", contextUrl) httpBackend!.when("GET", contextUrl)
.respond(200, () => { .respond(200, () => {
// Now finally return and make the `/context` request respond // Now finally return and make the `/context` request respond
return contextResponse; return contextResponse;
@@ -700,7 +708,7 @@ describe("MatrixClient room timelines", function() {
const racingSyncEventData = [ const racingSyncEventData = [
utils.mkMessage({ user: userId, room: roomId }), utils.mkMessage({ user: userId, room: roomId }),
]; ];
const waitForRaceySyncAfterResetPromise = new Promise((resolve, reject) => { const waitForRaceySyncAfterResetPromise = new Promise<void>((resolve, reject) => {
let eventFired = false; let eventFired = false;
// Throw a more descriptive error if this part of the test times out. // Throw a more descriptive error if this part of the test times out.
const failTimeout = setTimeout(() => { const failTimeout = setTimeout(() => {
@@ -726,12 +734,12 @@ describe("MatrixClient room timelines", function() {
// Then make a `/sync` happen by sending a message and seeing that it // Then make a `/sync` happen by sending a message and seeing that it
// shows up (simulate a /sync naturally racing with us). // shows up (simulate a /sync naturally racing with us).
setNextSyncData(racingSyncEventData); setNextSyncData(racingSyncEventData);
httpBackend.when("GET", "/sync").respond(200, function() { httpBackend!.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA; return NEXT_SYNC_DATA;
}); });
await Promise.all([ await Promise.all([
httpBackend.flush("/sync", 1), httpBackend!.flush("/sync", 1),
utils.syncPromise(client, 1), utils.syncPromise(client!, 1),
]); ]);
// Make sure the timeline has the racey sync data // Make sure the timeline has the racey sync data
const afterRaceySyncTimelineEvents = room const afterRaceySyncTimelineEvents = room
@@ -761,7 +769,7 @@ describe("MatrixClient room timelines", function() {
await Promise.all([ await Promise.all([
refreshLiveTimelinePromise, refreshLiveTimelinePromise,
// Then flush the remaining `/context` to left the refresh logic complete // Then flush the remaining `/context` to left the refresh logic complete
httpBackend.flushAllExpected(), httpBackend!.flushAllExpected(),
]); ]);
// Make sure sync pagination still works by seeing a new message show up // Make sure sync pagination still works by seeing a new message show up
@@ -770,12 +778,12 @@ describe("MatrixClient room timelines", function() {
utils.mkMessage({ user: userId, room: roomId }), utils.mkMessage({ user: userId, room: roomId }),
]; ];
setNextSyncData(afterRefreshEventData); setNextSyncData(afterRefreshEventData);
httpBackend.when("GET", "/sync").respond(200, function() { httpBackend!.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA; return NEXT_SYNC_DATA;
}); });
await Promise.all([ await Promise.all([
httpBackend.flushAllExpected(), httpBackend!.flushAllExpected(),
utils.syncPromise(client, 1), utils.syncPromise(client!, 1),
]); ]);
// Make sure the timeline includes the the events from the `/sync` // Make sure the timeline includes the the events from the `/sync`
@@ -794,7 +802,7 @@ describe("MatrixClient room timelines", function() {
it('Timeline recovers after `/context` request to generate new timeline fails', async () => { it('Timeline recovers after `/context` request to generate new timeline fails', async () => {
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()` // `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
// to construct a new timeline from. // to construct a new timeline from.
httpBackend.when("GET", contextUrl) httpBackend!.when("GET", contextUrl)
.respond(500, function() { .respond(500, function() {
// The timeline should be cleared at this point in the refresh // The timeline should be cleared at this point in the refresh
expect(room.timeline.length).toEqual(0); expect(room.timeline.length).toEqual(0);
@@ -809,7 +817,7 @@ describe("MatrixClient room timelines", function() {
// Refresh the timeline and expect it to fail // Refresh the timeline and expect it to fail
const settledFailedRefreshPromises = await Promise.allSettled([ const settledFailedRefreshPromises = await Promise.allSettled([
room.refreshLiveTimeline(), room.refreshLiveTimeline(),
httpBackend.flushAllExpected(), httpBackend!.flushAllExpected(),
]); ]);
// We only expect `TEST_FAKE_ERROR` here. Anything else is // We only expect `TEST_FAKE_ERROR` here. Anything else is
// unexpected and should fail the test. // unexpected and should fail the test.
@@ -825,7 +833,7 @@ describe("MatrixClient room timelines", function() {
// `/messages` request for `refreshLiveTimeline()` -> // `/messages` request for `refreshLiveTimeline()` ->
// `getLatestTimeline()` to construct a new timeline from. // `getLatestTimeline()` to construct a new timeline from.
httpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`) httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`)
.respond(200, function() { .respond(200, function() {
return { return {
chunk: [{ chunk: [{
@@ -837,7 +845,7 @@ describe("MatrixClient room timelines", function() {
// `/context` request for `refreshLiveTimeline()` -> // `/context` request for `refreshLiveTimeline()` ->
// `getLatestTimeline()` -> `getEventTimeline()` to construct a new // `getLatestTimeline()` -> `getEventTimeline()` to construct a new
// timeline from. // timeline from.
httpBackend.when("GET", contextUrl) httpBackend!.when("GET", contextUrl)
.respond(200, function() { .respond(200, function() {
// The timeline should be cleared at this point in the refresh // The timeline should be cleared at this point in the refresh
expect(room.timeline.length).toEqual(0); expect(room.timeline.length).toEqual(0);
@@ -848,7 +856,7 @@ describe("MatrixClient room timelines", function() {
// Refresh the timeline again but this time it should pass // Refresh the timeline again but this time it should pass
await Promise.all([ await Promise.all([
room.refreshLiveTimeline(), room.refreshLiveTimeline(),
httpBackend.flushAllExpected(), httpBackend!.flushAllExpected(),
]); ]);
// Make sure sync pagination still works by seeing a new message show up // Make sure sync pagination still works by seeing a new message show up
@@ -857,12 +865,12 @@ describe("MatrixClient room timelines", function() {
utils.mkMessage({ user: userId, room: roomId }), utils.mkMessage({ user: userId, room: roomId }),
]; ];
setNextSyncData(afterRefreshEventData); setNextSyncData(afterRefreshEventData);
httpBackend.when("GET", "/sync").respond(200, function() { httpBackend!.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA; return NEXT_SYNC_DATA;
}); });
await Promise.all([ await Promise.all([
httpBackend.flushAllExpected(), httpBackend!.flushAllExpected(),
utils.syncPromise(client, 1), utils.syncPromise(client!, 1),
]); ]);
// Make sure the message are visible // Make sure the message are visible

View File

@@ -95,26 +95,31 @@ describe("megolm key backups", function() {
return; return;
} }
const Olm = global.Olm; const Olm = global.Olm;
let testOlmAccount: Olm.Account;
let testOlmAccount: Account;
let aliceTestClient: TestClient; let aliceTestClient: TestClient;
const setupTestClient = (): [Account, TestClient] => {
const aliceTestClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs",
);
const testOlmAccount = new Olm.Account();
testOlmAccount!.create();
return [testOlmAccount, aliceTestClient];
};
beforeAll(function() { beforeAll(function() {
return Olm.init(); return Olm.init();
}); });
beforeEach(async function() { beforeEach(async function() {
aliceTestClient = new TestClient( [testOlmAccount, aliceTestClient] = setupTestClient();
"@alice:localhost", "xzcvb", "akjgkrgjs", await aliceTestClient!.client.initCrypto();
); aliceTestClient!.client.crypto!.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
testOlmAccount = new Olm.Account();
testOlmAccount.create();
await aliceTestClient.client.initCrypto();
aliceTestClient.client.crypto.backupManager.backupInfo = CURVE25519_BACKUP_INFO;
}); });
afterEach(function() { afterEach(function() {
return aliceTestClient.stop(); return aliceTestClient!.stop();
}); });
it("Alice checks key backups when receiving a message she can't decrypt", function() { it("Alice checks key backups when receiving a message she can't decrypt", function() {
@@ -130,22 +135,22 @@ describe("megolm key backups", function() {
}, },
}; };
return aliceTestClient.start().then(() => { return aliceTestClient!.start().then(() => {
return createOlmSession(testOlmAccount, aliceTestClient); return createOlmSession(testOlmAccount, aliceTestClient);
}).then(() => { }).then(() => {
const privkey = decodeRecoveryKey(RECOVERY_KEY); const privkey = decodeRecoveryKey(RECOVERY_KEY);
return aliceTestClient.client.crypto.storeSessionBackupPrivateKey(privkey); return aliceTestClient!.client!.crypto!.storeSessionBackupPrivateKey(privkey);
}).then(() => { }).then(() => {
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse); aliceTestClient!.httpBackend.when("GET", "/sync").respond(200, syncResponse);
aliceTestClient.expectKeyBackupQuery( aliceTestClient!.expectKeyBackupQuery(
ROOM_ID, ROOM_ID,
SESSION_ID, SESSION_ID,
200, 200,
CURVE25519_KEY_BACKUP_DATA, CURVE25519_KEY_BACKUP_DATA,
); );
return aliceTestClient.httpBackend.flushAllExpected(); return aliceTestClient!.httpBackend.flushAllExpected();
}).then(function(): Promise<MatrixEvent> { }).then(function(): Promise<MatrixEvent> {
const room = aliceTestClient.client.getRoom(ROOM_ID); const room = aliceTestClient!.client.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0]; const event = room.getLiveTimeline().getEvents()[0];
if (event.getContent()) { if (event.getContent()) {

View File

@@ -29,8 +29,11 @@ import {
IDownloadKeyResult, IDownloadKeyResult,
MatrixEvent, MatrixEvent,
MatrixEventEvent, MatrixEventEvent,
IndexedDBCryptoStore,
Room,
} from "../../src/matrix"; } from "../../src/matrix";
import { IDeviceKeys } from "../../src/crypto/dehydration"; import { IDeviceKeys } from "../../src/crypto/dehydration";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
const ROOM_ID = "!room:id"; const ROOM_ID = "!room:id";
@@ -204,9 +207,11 @@ describe("megolm", () => {
} }
const Olm = global.Olm; const Olm = global.Olm;
let testOlmAccount: Olm.Account; let testOlmAccount = {} as unknown as Olm.Account;
let testSenderKey: string; let testSenderKey = '';
let aliceTestClient: TestClient; let aliceTestClient = new TestClient(
"@alice:localhost", "device2", "access_token2",
);
/** /**
* Get the device keys for testOlmAccount in a format suitable for a * Get the device keys for testOlmAccount in a format suitable for a
@@ -280,10 +285,13 @@ describe("megolm", () => {
it("Alice receives a megolm message", async () => { it("Alice receives a megolm message", async () => {
await aliceTestClient.start(); await aliceTestClient.start();
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient); const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
const groupSession = new Olm.OutboundGroupSession(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
// make the room_key event // make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({ const roomKeyEncrypted = encryptGroupSessionKey({
senderKey: testSenderKey, senderKey: testSenderKey,
@@ -316,7 +324,7 @@ describe("megolm", () => {
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse); aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
await aliceTestClient.flushSync(); await aliceTestClient.flushSync();
const room = aliceTestClient.client.getRoom(ROOM_ID); const room = aliceTestClient.client.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0]; const event = room.getLiveTimeline().getEvents()[0];
expect(event.isEncrypted()).toBe(true); expect(event.isEncrypted()).toBe(true);
const decryptedEvent = await testUtils.awaitDecryption(event); const decryptedEvent = await testUtils.awaitDecryption(event);
@@ -326,10 +334,13 @@ describe("megolm", () => {
it("Alice receives a megolm message before the session keys", async () => { it("Alice receives a megolm message before the session keys", async () => {
// https://github.com/vector-im/element-web/issues/2273 // https://github.com/vector-im/element-web/issues/2273
await aliceTestClient.start(); await aliceTestClient.start();
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient); const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
const groupSession = new Olm.OutboundGroupSession(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
// make the room_key event, but don't send it yet // make the room_key event, but don't send it yet
const roomKeyEncrypted = encryptGroupSessionKey({ const roomKeyEncrypted = encryptGroupSessionKey({
senderKey: testSenderKey, senderKey: testSenderKey,
@@ -353,7 +364,7 @@ describe("megolm", () => {
}); });
await aliceTestClient.flushSync(); await aliceTestClient.flushSync();
const room = aliceTestClient.client.getRoom(ROOM_ID); const room = aliceTestClient.client.getRoom(ROOM_ID)!;
expect(room.getLiveTimeline().getEvents()[0].getContent().msgtype).toEqual('m.bad.encrypted'); expect(room.getLiveTimeline().getEvents()[0].getContent().msgtype).toEqual('m.bad.encrypted');
// now she gets the room_key event // now she gets the room_key event
@@ -383,10 +394,13 @@ describe("megolm", () => {
it("Alice gets a second room_key message", async () => { it("Alice gets a second room_key message", async () => {
await aliceTestClient.start(); await aliceTestClient.start();
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient); const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
const groupSession = new Olm.OutboundGroupSession(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
// make the room_key event // make the room_key event
const roomKeyEncrypted1 = encryptGroupSessionKey({ const roomKeyEncrypted1 = encryptGroupSessionKey({
senderKey: testSenderKey, senderKey: testSenderKey,
@@ -439,7 +453,7 @@ describe("megolm", () => {
await aliceTestClient.flushSync(); await aliceTestClient.flushSync();
await aliceTestClient.flushSync(); await aliceTestClient.flushSync();
const room = aliceTestClient.client.getRoom(ROOM_ID); const room = aliceTestClient.client.getRoom(ROOM_ID)!;
await room.decryptCriticalEvents(); await room.decryptCriticalEvents();
const event = room.getLiveTimeline().getEvents()[0]; const event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42'); expect(event.getContent().body).toEqual('42');
@@ -468,6 +482,9 @@ describe("megolm", () => {
aliceTestClient.httpBackend.when('POST', '/keys/query').respond( aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'), 200, getTestKeysQueryResponse('@bob:xyz'),
); );
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'),
);
await Promise.all([ await Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => { aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
@@ -484,7 +501,7 @@ describe("megolm", () => {
let inboundGroupSession: Olm.InboundGroupSession; let inboundGroupSession: Olm.InboundGroupSession;
aliceTestClient.httpBackend.when( aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/', 'PUT', '/sendToDevice/m.room.encrypted/',
).respond(200, function(_path, content) { ).respond(200, function(_path, content: any) {
const m = content.messages['@bob:xyz'].DEVICE_ID; const m = content.messages['@bob:xyz'].DEVICE_ID;
const ct = m.ciphertext[testSenderKey]; const ct = m.ciphertext[testSenderKey];
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body)); const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
@@ -510,7 +527,7 @@ describe("megolm", () => {
return { event_id: '$event_id' }; return { event_id: '$event_id' };
}); });
const room = aliceTestClient.client.getRoom(ROOM_ID); const room = aliceTestClient.client.getRoom(ROOM_ID)!;
const pendingMsg = room.getPendingEvents()[0]; const pendingMsg = room.getPendingEvents()[0];
await Promise.all([ await Promise.all([
@@ -541,13 +558,16 @@ describe("megolm", () => {
logger.log('Forcing alice to download our device keys'); logger.log('Forcing alice to download our device keys');
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'),
);
aliceTestClient.httpBackend.when('POST', '/keys/query').respond( aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'), 200, getTestKeysQueryResponse('@bob:xyz'),
); );
await Promise.all([ await Promise.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']), aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1), aliceTestClient.httpBackend.flush('/keys/query', 2),
]); ]);
logger.log('Telling alice to block our device'); logger.log('Telling alice to block our device');
@@ -592,6 +612,9 @@ describe("megolm", () => {
logger.log("Fetching bob's devices and marking known"); logger.log("Fetching bob's devices and marking known");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'),
);
aliceTestClient.httpBackend.when('POST', '/keys/query').respond( aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'), 200, getTestKeysQueryResponse('@bob:xyz'),
); );
@@ -607,7 +630,7 @@ describe("megolm", () => {
let megolmSessionId: string; let megolmSessionId: string;
aliceTestClient.httpBackend.when( aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/', 'PUT', '/sendToDevice/m.room.encrypted/',
).respond(200, function(_path, content) { ).respond(200, function(_path, content: any) {
logger.log('sendToDevice: ', content); logger.log('sendToDevice: ', content);
const m = content.messages['@bob:xyz'].DEVICE_ID; const m = content.messages['@bob:xyz'].DEVICE_ID;
const ct = m.ciphertext[testSenderKey]; const ct = m.ciphertext[testSenderKey];
@@ -685,7 +708,7 @@ describe("megolm", () => {
// invalidate the device cache for all members in e2e rooms (ie, // invalidate the device cache for all members in e2e rooms (ie,
// herself), and do a key query. // herself), and do a key query.
aliceTestClient.expectKeyQuery( aliceTestClient.expectKeyQuery(
getTestKeysQueryResponse(aliceTestClient.userId), getTestKeysQueryResponse(aliceTestClient.userId!),
); );
await aliceTestClient.httpBackend.flushAllExpected(); await aliceTestClient.httpBackend.flushAllExpected();
@@ -695,28 +718,30 @@ describe("megolm", () => {
await aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'); await aliceTestClient.client.sendTextMessage(ROOM_ID, 'test');
throw new Error("sendTextMessage succeeded on an unknown device"); throw new Error("sendTextMessage succeeded on an unknown device");
} catch (e) { } catch (e) {
expect(e.name).toEqual("UnknownDeviceError"); expect((e as any).name).toEqual("UnknownDeviceError");
expect(Object.keys(e.devices)).toEqual([aliceTestClient.userId]); expect(Object.keys((e as any).devices)).toEqual([aliceTestClient.userId!]);
expect(Object.keys(e.devices[aliceTestClient.userId])). expect(Object.keys((e as any)?.devices[aliceTestClient.userId!])).
toEqual(['DEVICE_ID']); toEqual(['DEVICE_ID']);
} }
// mark the device as known, and resend. // mark the device as known, and resend.
aliceTestClient.client.setDeviceKnown(aliceTestClient.userId, 'DEVICE_ID'); aliceTestClient.client.setDeviceKnown(aliceTestClient.userId!, 'DEVICE_ID');
aliceTestClient.httpBackend.when('POST', '/keys/claim').respond( aliceTestClient.httpBackend.when('POST', '/keys/claim').respond(
200, function(_path, content) { 200, function(_path, content: IClaimOTKsResult) {
expect(content.one_time_keys[aliceTestClient.userId].DEVICE_ID) expect(content.one_time_keys[aliceTestClient.userId!].DEVICE_ID)
.toEqual("signed_curve25519"); .toEqual("signed_curve25519");
return getTestKeysClaimResponse(aliceTestClient.userId); return getTestKeysClaimResponse(aliceTestClient.userId!);
}); });
let p2pSession: Olm.Session; let p2pSession: Olm.Session;
let inboundGroupSession: Olm.InboundGroupSession; let inboundGroupSession: Olm.InboundGroupSession;
aliceTestClient.httpBackend.when( aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/', 'PUT', '/sendToDevice/m.room.encrypted/',
).respond(200, function(_path, content) { ).respond(200, function(_path, content: {
messages: { [userId: string]: { [deviceId: string]: Record<string, any> }};
}) {
logger.log("sendToDevice: ", content); logger.log("sendToDevice: ", content);
const m = content.messages[aliceTestClient.userId].DEVICE_ID; const m = content.messages[aliceTestClient.userId!].DEVICE_ID;
const ct = m.ciphertext[testSenderKey]; const ct = m.ciphertext[testSenderKey];
expect(ct.type).toEqual(0); // pre-key message expect(ct.type).toEqual(0); // pre-key message
@@ -730,7 +755,7 @@ describe("megolm", () => {
return {}; return {};
}); });
let decrypted: IEvent; let decrypted: Partial<IEvent> = {};
aliceTestClient.httpBackend.when( aliceTestClient.httpBackend.when(
'PUT', '/send/', 'PUT', '/send/',
).respond(200, function(_path, content: IContent) { ).respond(200, function(_path, content: IContent) {
@@ -745,7 +770,7 @@ describe("megolm", () => {
}); });
// Grab the event that we'll need to resend // Grab the event that we'll need to resend
const room = aliceTestClient.client.getRoom(ROOM_ID); const room = aliceTestClient.client.getRoom(ROOM_ID)!;
const pendingEvents = room.getPendingEvents(); const pendingEvents = room.getPendingEvents();
expect(pendingEvents.length).toEqual(1); expect(pendingEvents.length).toEqual(1);
const unsentEvent = pendingEvents[0]; const unsentEvent = pendingEvents[0];
@@ -760,7 +785,7 @@ describe("megolm", () => {
]); ]);
expect(decrypted.type).toEqual('m.room.message'); expect(decrypted.type).toEqual('m.room.message');
expect(decrypted.content.body).toEqual('test'); expect(decrypted.content?.body).toEqual('test');
}); });
it('Alice should wait for device list to complete when sending a megolm message', async () => { it('Alice should wait for device list to complete when sending a megolm message', async () => {
@@ -786,6 +811,10 @@ describe("megolm", () => {
logger.log('Forcing alice to download our device keys'); logger.log('Forcing alice to download our device keys');
const downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']); const downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'),
);
// so will this. // so will this.
const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test') const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test')
.then(() => { .then(() => {
@@ -805,9 +834,12 @@ describe("megolm", () => {
it("Alice exports megolm keys and imports them to a new device", async () => { it("Alice exports megolm keys and imports them to a new device", async () => {
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} }, failures: {} }); aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} }, failures: {} });
await aliceTestClient.start(); await aliceTestClient.start();
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
// establish an olm session with alice // establish an olm session with alice
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient); const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
const groupSession = new Olm.OutboundGroupSession(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
@@ -839,7 +871,7 @@ describe("megolm", () => {
}); });
await aliceTestClient.flushSync(); await aliceTestClient.flushSync();
const room = aliceTestClient.client.getRoom(ROOM_ID); const room = aliceTestClient.client.getRoom(ROOM_ID)!;
await room.decryptCriticalEvents(); await room.decryptCriticalEvents();
expect(room.getLiveTimeline().getEvents()[0].getContent().body).toEqual('42'); expect(room.getLiveTimeline().getEvents()[0].getContent().body).toEqual('42');
@@ -855,6 +887,8 @@ describe("megolm", () => {
await aliceTestClient.client.importRoomKeys(exported); await aliceTestClient.client.importRoomKeys(exported);
await aliceTestClient.start(); await aliceTestClient.start();
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
const syncResponse = { const syncResponse = {
next_batch: 1, next_batch: 1,
rooms: { rooms: {
@@ -897,7 +931,7 @@ describe("megolm", () => {
...rawEvent, ...rawEvent,
room: ROOM_ID, room: ROOM_ID,
}); });
await event1.attemptDecryption(testClient.client.crypto, { isRetry: true }); await event1.attemptDecryption(testClient.client.crypto!, { isRetry: true });
expect(event1.isKeySourceUntrusted()).toBeTruthy(); expect(event1.isKeySourceUntrusted()).toBeTruthy();
const event2 = testUtils.mkEvent({ const event2 = testUtils.mkEvent({
@@ -913,24 +947,27 @@ describe("megolm", () => {
// @ts-ignore - private // @ts-ignore - private
event2.senderCurve25519Key = testSenderKey; event2.senderCurve25519Key = testSenderKey;
// @ts-ignore - private // @ts-ignore - private
testClient.client.crypto.onRoomKeyEvent(event2); testClient.client.crypto!.onRoomKeyEvent(event2);
const event3 = testUtils.mkEvent({ const event3 = testUtils.mkEvent({
event: true, event: true,
...rawEvent, ...rawEvent,
room: ROOM_ID, room: ROOM_ID,
}); });
await event3.attemptDecryption(testClient.client.crypto, { isRetry: true }); await event3.attemptDecryption(testClient.client.crypto!, { isRetry: true });
expect(event3.isKeySourceUntrusted()).toBeFalsy(); expect(event3.isKeySourceUntrusted()).toBeFalsy();
testClient.stop(); testClient.stop();
}); });
it("Alice can decrypt a message with falsey content", async () => { it("Alice can decrypt a message with falsey content", async () => {
await aliceTestClient.start(); await aliceTestClient.start();
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient); const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
const groupSession = new Olm.OutboundGroupSession(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
// make the room_key event // make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({ const roomKeyEncrypted = encryptGroupSessionKey({
senderKey: testSenderKey, senderKey: testSenderKey,
@@ -972,7 +1009,7 @@ describe("megolm", () => {
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse); aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
await aliceTestClient.flushSync(); await aliceTestClient.flushSync();
const room = aliceTestClient.client.getRoom(ROOM_ID); const room = aliceTestClient.client.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0]; const event = room.getLiveTimeline().getEvents()[0];
expect(event.isEncrypted()).toBe(true); expect(event.isEncrypted()).toBe(true);
const decryptedEvent = await testUtils.awaitDecryption(event); const decryptedEvent = await testUtils.awaitDecryption(event);
@@ -985,10 +1022,13 @@ describe("megolm", () => {
"should successfully decrypt bundled redaction events that don't include a room_id in their /sync data", "should successfully decrypt bundled redaction events that don't include a room_id in their /sync data",
async () => { async () => {
await aliceTestClient.start(); await aliceTestClient.start();
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient); const p2pSession = await createOlmSession(testOlmAccount, aliceTestClient);
const groupSession = new Olm.OutboundGroupSession(); const groupSession = new Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => "@bob:xyz";
// make the room_key event // make the room_key event
const roomKeyEncrypted = encryptGroupSessionKey({ const roomKeyEncrypted = encryptGroupSessionKey({
senderKey: testSenderKey, senderKey: testSenderKey,
@@ -1036,13 +1076,292 @@ describe("megolm", () => {
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse); aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
await aliceTestClient.flushSync(); await aliceTestClient.flushSync();
const room = aliceTestClient.client.getRoom(ROOM_ID); const room = aliceTestClient.client.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0]; const event = room.getLiveTimeline().getEvents()[0];
expect(event.isEncrypted()).toBe(true); expect(event.isEncrypted()).toBe(true);
await event.attemptDecryption(aliceTestClient.client.crypto); await event.attemptDecryption(aliceTestClient.client.crypto!);
expect(event.getContent()).toEqual({}); expect(event.getContent()).toEqual({});
const redactionEvent: any = event.getRedactionEvent(); const redactionEvent: any = event.getRedactionEvent();
expect(redactionEvent.content.reason).toEqual("redaction test"); expect(redactionEvent.content.reason).toEqual("redaction test");
}, },
); );
it("Alice receives shared history before being invited to a room by the sharer", async () => {
const beccaTestClient = new TestClient(
"@becca:localhost", "foobar", "bazquux",
);
await beccaTestClient.client.initCrypto();
await aliceTestClient.start();
aliceTestClient.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({});
await beccaTestClient.start();
const beccaRoom = new Room(ROOM_ID, beccaTestClient.client, "@becca:localhost", {});
beccaTestClient.client.store.storeRoom(beccaRoom);
await beccaTestClient.client.setRoomEncryption(ROOM_ID, { "algorithm": "m.megolm.v1.aes-sha2" });
const event = new MatrixEvent({
type: "m.room.message",
sender: "@becca:localhost",
room_id: ROOM_ID,
event_id: "$1",
content: {
msgtype: "m.text",
body: "test message",
},
});
await beccaTestClient.client.crypto!.encryptEvent(event, beccaRoom);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
// @ts-ignore private properties
event.senderCurve25519Key = null;
// @ts-ignore private properties
event.claimedEd25519Key = null;
const device = new DeviceInfo(beccaTestClient.client.deviceId!);
aliceTestClient.client.crypto!.deviceList.getDeviceByIdentityKey = () => device;
aliceTestClient.client.crypto!.deviceList.getUserByIdentityKey = () => beccaTestClient.client.getUserId()!;
// Create an olm session for Becca and Alice's devices
const aliceOtks = await aliceTestClient.awaitOneTimeKeyUpload();
const aliceOtkId = Object.keys(aliceOtks)[0];
const aliceOtk = aliceOtks[aliceOtkId];
const p2pSession = new global.Olm.Session();
await beccaTestClient.client.crypto!.cryptoStore.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string) => {
const account = new global.Olm.Account();
try {
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount);
p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
} finally {
account.free();
}
});
},
);
const content = event.getWireContent();
const groupSessionKey = await beccaTestClient.client.crypto!.olmDevice.getInboundGroupSessionKey(
ROOM_ID,
content.sender_key,
content.session_id,
);
const encryptedForwardedKey = encryptOlmEvent({
sender: "@becca:localhost",
senderKey: beccaTestClient.getDeviceKey(),
recipient: aliceTestClient,
p2pSession: p2pSession,
plaincontent: {
"algorithm": 'm.megolm.v1.aes-sha2',
"room_id": ROOM_ID,
"sender_key": content.sender_key,
"sender_claimed_ed25519_key": groupSessionKey.sender_claimed_ed25519_key,
"session_id": content.session_id,
"session_key": groupSessionKey.key,
"chain_index": groupSessionKey.chain_index,
"forwarding_curve25519_key_chain": groupSessionKey.forwarding_curve25519_key_chain,
"org.matrix.msc3061.shared_history": true,
},
plaintype: 'm.forwarded_room_key',
});
// Alice receives shared history
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 1,
to_device: { events: [encryptedForwardedKey] },
});
await aliceTestClient.flushSync();
// Alice is invited to the room by Becca
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 2,
rooms: { invite: { [ROOM_ID]: { invite_state: { events: [
{
sender: '@becca:localhost',
type: 'm.room.encryption',
state_key: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
},
{
sender: '@becca:localhost',
type: 'm.room.member',
state_key: '@alice:localhost',
content: {
membership: 'invite',
},
},
] } } } },
});
await aliceTestClient.flushSync();
// Alice has joined the room
aliceTestClient.httpBackend.when("GET", "/sync").respond(
200, getSyncResponse(["@alice:localhost", "@becca:localhost"]),
);
await aliceTestClient.flushSync();
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 4,
rooms: {
join: {
[ROOM_ID]: { timeline: { events: [event.event] } },
},
},
});
await aliceTestClient.flushSync();
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
const roomEvent = room.getLiveTimeline().getEvents()[0];
expect(roomEvent.isEncrypted()).toBe(true);
const decryptedEvent = await testUtils.awaitDecryption(roomEvent);
expect(decryptedEvent.getContent().body).toEqual('test message');
await beccaTestClient.stop();
});
it("Alice receives shared history before being invited to a room by someone else", async () => {
const beccaTestClient = new TestClient(
"@becca:localhost", "foobar", "bazquux",
);
await beccaTestClient.client.initCrypto();
await aliceTestClient.start();
await beccaTestClient.start();
const beccaRoom = new Room(ROOM_ID, beccaTestClient.client, "@becca:localhost", {});
beccaTestClient.client.store.storeRoom(beccaRoom);
await beccaTestClient.client.setRoomEncryption(ROOM_ID, { "algorithm": "m.megolm.v1.aes-sha2" });
const event = new MatrixEvent({
type: "m.room.message",
sender: "@becca:localhost",
room_id: ROOM_ID,
event_id: "$1",
content: {
msgtype: "m.text",
body: "test message",
},
});
await beccaTestClient.client.crypto!.encryptEvent(event, beccaRoom);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
// @ts-ignore private properties
event.senderCurve25519Key = null;
// @ts-ignore private properties
event.claimedEd25519Key = null;
const device = new DeviceInfo(beccaTestClient.client.deviceId!);
aliceTestClient.client.crypto!.deviceList.getDeviceByIdentityKey = () => device;
// Create an olm session for Becca and Alice's devices
const aliceOtks = await aliceTestClient.awaitOneTimeKeyUpload();
const aliceOtkId = Object.keys(aliceOtks)[0];
const aliceOtk = aliceOtks[aliceOtkId];
const p2pSession = new global.Olm.Session();
await beccaTestClient.client.crypto!.cryptoStore.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string) => {
const account = new global.Olm.Account();
try {
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount);
p2pSession.create_outbound(account, aliceTestClient.getDeviceKey(), aliceOtk.key);
} finally {
account.free();
}
});
},
);
const content = event.getWireContent();
const groupSessionKey = await beccaTestClient.client.crypto!.olmDevice.getInboundGroupSessionKey(
ROOM_ID,
content.sender_key,
content.session_id,
);
const encryptedForwardedKey = encryptOlmEvent({
sender: "@becca:localhost",
senderKey: beccaTestClient.getDeviceKey(),
recipient: aliceTestClient,
p2pSession: p2pSession,
plaincontent: {
"algorithm": 'm.megolm.v1.aes-sha2',
"room_id": ROOM_ID,
"sender_key": content.sender_key,
"sender_claimed_ed25519_key": groupSessionKey.sender_claimed_ed25519_key,
"session_id": content.session_id,
"session_key": groupSessionKey.key,
"chain_index": groupSessionKey.chain_index,
"forwarding_curve25519_key_chain": groupSessionKey.forwarding_curve25519_key_chain,
"org.matrix.msc3061.shared_history": true,
},
plaintype: 'm.forwarded_room_key',
});
// Alice receives forwarded history from Becca
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 1,
to_device: { events: [encryptedForwardedKey] },
});
await aliceTestClient.flushSync();
// Alice is invited to the room by Charlie
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 2,
rooms: { invite: { [ROOM_ID]: { invite_state: { events: [
{
sender: '@becca:localhost',
type: 'm.room.encryption',
state_key: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
},
{
sender: '@charlie:localhost',
type: 'm.room.member',
state_key: '@alice:localhost',
content: {
membership: 'invite',
},
},
] } } } },
});
await aliceTestClient.flushSync();
// Alice has joined the room
aliceTestClient.httpBackend.when("GET", "/sync").respond(
200, getSyncResponse(["@alice:localhost", "@becca:localhost", "@charlie:localhost"]),
);
await aliceTestClient.flushSync();
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 4,
rooms: {
join: {
[ROOM_ID]: { timeline: { events: [event.event] } },
},
},
});
await aliceTestClient.flushSync();
// Decryption should fail, because Alice hasn't received any keys she can trust
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
const roomEvent = room.getLiveTimeline().getEvents()[0];
expect(roomEvent.isEncrypted()).toBe(true);
const decryptedEvent = await testUtils.awaitDecryption(roomEvent);
expect(decryptedEvent.isDecryptionFailure()).toBe(true);
await beccaTestClient.stop();
});
}); });

View File

@@ -31,10 +31,10 @@ import { IStoredClientOpts } from "../../src/client";
import { logger } from "../../src/logger"; import { logger } from "../../src/logger";
describe("SlidingSyncSdk", () => { describe("SlidingSyncSdk", () => {
let client: MatrixClient = null; let client: MatrixClient | undefined;
let httpBackend: MockHttpBackend = null; let httpBackend: MockHttpBackend | undefined;
let sdk: SlidingSyncSdk = null; let sdk: SlidingSyncSdk | undefined;
let mockSlidingSync: SlidingSync = null; let mockSlidingSync: SlidingSync | undefined;
const selfUserId = "@alice:localhost"; const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef"; const selfAccessToken = "aseukfgwef";
@@ -66,7 +66,7 @@ describe("SlidingSyncSdk", () => {
event_id: "$" + eventIdCounter, event_id: "$" + eventIdCounter,
}; };
}; };
const mkOwnStateEvent = (evType: string, content: object, stateKey?: string): IStateEvent => { const mkOwnStateEvent = (evType: string, content: object, stateKey = ''): IStateEvent => {
eventIdCounter++; eventIdCounter++;
return { return {
type: evType, type: evType,
@@ -103,24 +103,24 @@ describe("SlidingSyncSdk", () => {
client = testClient.client; client = testClient.client;
mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0)); mockSlidingSync = mockifySlidingSync(new SlidingSync("", [], {}, client, 0));
if (testOpts.withCrypto) { if (testOpts.withCrypto) {
httpBackend.when("GET", "/room_keys/version").respond(404, {}); httpBackend!.when("GET", "/room_keys/version").respond(404, {});
await client.initCrypto(); await client!.initCrypto();
testOpts.crypto = client.crypto; testOpts.crypto = client!.crypto;
} }
httpBackend.when("GET", "/_matrix/client/r0/pushrules").respond(200, {}); httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts); sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts);
}; };
// tear down client/httpBackend globals // tear down client/httpBackend globals
const teardownClient = () => { const teardownClient = () => {
client.stopClient(); client!.stopClient();
return httpBackend.stop(); return httpBackend!.stop();
}; };
// find an extension on a SlidingSyncSdk instance // find an extension on a SlidingSyncSdk instance
const findExtension = (name: string): Extension => { const findExtension = (name: string): Extension => {
expect(mockSlidingSync.registerExtension).toHaveBeenCalled(); expect(mockSlidingSync!.registerExtension).toHaveBeenCalled();
const mockFn = mockSlidingSync.registerExtension as jest.Mock; const mockFn = mockSlidingSync!.registerExtension as jest.Mock;
// find the extension // find the extension
for (let i = 0; i < mockFn.mock.calls.length; i++) { for (let i = 0; i < mockFn.mock.calls.length; i++) {
const calledExtension = mockFn.mock.calls[i][0] as Extension; const calledExtension = mockFn.mock.calls[i][0] as Extension;
@@ -137,14 +137,14 @@ describe("SlidingSyncSdk", () => {
}); });
afterAll(teardownClient); afterAll(teardownClient);
it("can sync()", async () => { it("can sync()", async () => {
const hasSynced = sdk.sync(); const hasSynced = sdk!.sync();
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await hasSynced; await hasSynced;
expect(mockSlidingSync.start).toBeCalled(); expect(mockSlidingSync!.start).toBeCalled();
}); });
it("can stop()", async () => { it("can stop()", async () => {
sdk.stop(); sdk!.stop();
expect(mockSlidingSync.stop).toBeCalled(); expect(mockSlidingSync!.stop).toBeCalled();
}); });
}); });
@@ -156,8 +156,8 @@ describe("SlidingSyncSdk", () => {
describe("initial", () => { describe("initial", () => {
beforeAll(async () => { beforeAll(async () => {
const hasSynced = sdk.sync(); const hasSynced = sdk!.sync();
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await hasSynced; await hasSynced;
}); });
// inject some rooms with different fields set. // inject some rooms with different fields set.
@@ -168,6 +168,7 @@ describe("SlidingSyncSdk", () => {
const roomD = "!d_with_notif_count:localhost"; const roomD = "!d_with_notif_count:localhost";
const roomE = "!e_with_invite:localhost"; const roomE = "!e_with_invite:localhost";
const roomF = "!f_calc_room_name:localhost"; const roomF = "!f_calc_room_name:localhost";
const roomG = "!g_join_invite_counts:localhost";
const data: Record<string, MSC3575RoomData> = { const data: Record<string, MSC3575RoomData> = {
[roomA]: { [roomA]: {
name: "A", name: "A",
@@ -261,56 +262,83 @@ describe("SlidingSyncSdk", () => {
], ],
initial: true, initial: true,
}, },
[roomG]: {
name: "G",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
],
joined_count: 5,
invited_count: 2,
initial: true,
},
}; };
it("can be created with required_state and timeline", () => { it("can be created with required_state and timeline", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
const gotRoom = client.getRoom(roomA); const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.name).toEqual(data[roomA].name); expect(gotRoom.name).toEqual(data[roomA].name);
expect(gotRoom.getMyMembership()).toEqual("join"); expect(gotRoom.getMyMembership()).toEqual("join");
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline); assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
}); });
it("can be created with timeline only", () => { it("can be created with timeline only", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
const gotRoom = client.getRoom(roomB); const gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.name).toEqual(data[roomB].name); expect(gotRoom.name).toEqual(data[roomB].name);
expect(gotRoom.getMyMembership()).toEqual("join"); expect(gotRoom.getMyMembership()).toEqual("join");
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline); assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
}); });
it("can be created with a highlight_count", () => { it("can be created with a highlight_count", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
const gotRoom = client.getRoom(roomC); const gotRoom = client!.getRoom(roomC);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect( expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight), gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
).toEqual(data[roomC].highlight_count); ).toEqual(data[roomC].highlight_count);
}); });
it("can be created with a notification_count", () => { it("can be created with a notification_count", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
const gotRoom = client.getRoom(roomD); const gotRoom = client!.getRoom(roomD);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect( expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Total), gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
).toEqual(data[roomD].notification_count); ).toEqual(data[roomD].notification_count);
}); });
it("can be created with invite_state", () => { it("can be created with an invited/joined_count", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
const gotRoom = client.getRoom(roomE); const gotRoom = client!.getRoom(roomG);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
});
it("can be created with invite_state", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
const gotRoom = client!.getRoom(roomE);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getMyMembership()).toEqual("invite"); expect(gotRoom.getMyMembership()).toEqual("invite");
expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite); expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite);
}); });
it("uses the 'name' field to caluclate the room name", () => { it("uses the 'name' field to caluclate the room name", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]); mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
const gotRoom = client.getRoom(roomF); const gotRoom = client!.getRoom(roomF);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect( expect(
gotRoom.name, gotRoom.name,
).toEqual(data[roomF].name); ).toEqual(data[roomF].name);
@@ -319,61 +347,80 @@ describe("SlidingSyncSdk", () => {
describe("updating", () => { describe("updating", () => {
it("can update with a new timeline event", async () => { it("can update with a new timeline event", async () => {
const newEvent = mkOwnEvent(EventType.RoomMessage, { body: "new event A" }); const newEvent = mkOwnEvent(EventType.RoomMessage, { body: "new event A" });
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, {
timeline: [newEvent], timeline: [newEvent],
required_state: [], required_state: [],
name: data[roomA].name, name: data[roomA].name,
}); });
const gotRoom = client.getRoom(roomA); const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
const newTimeline = data[roomA].timeline; const newTimeline = data[roomA].timeline;
newTimeline.push(newEvent); newTimeline.push(newEvent);
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline); assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline);
}); });
it("can update with a new required_state event", async () => { it("can update with a new required_state event", async () => {
let gotRoom = client.getRoom(roomB); let gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomB, {
required_state: [ required_state: [
mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, ""), mkOwnStateEvent("m.room.join_rules", { join_rule: "restricted" }, ""),
], ],
timeline: [], timeline: [],
name: data[roomB].name, name: data[roomB].name,
}); });
gotRoom = client.getRoom(roomB); gotRoom = client!.getRoom(roomB);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted); expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
}); });
it("can update with a new highlight_count", async () => { it("can update with a new highlight_count", async () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomC, {
name: data[roomC].name, name: data[roomC].name,
required_state: [], required_state: [],
timeline: [], timeline: [],
highlight_count: 1, highlight_count: 1,
}); });
const gotRoom = client.getRoom(roomC); const gotRoom = client!.getRoom(roomC);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect( expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight), gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
).toEqual(1); ).toEqual(1);
}); });
it("can update with a new notification_count", async () => { it("can update with a new notification_count", async () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomD, {
name: data[roomD].name, name: data[roomD].name,
required_state: [], required_state: [],
timeline: [], timeline: [],
notification_count: 1, notification_count: 1,
}); });
const gotRoom = client.getRoom(roomD); const gotRoom = client!.getRoom(roomD);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect( expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Total), gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
).toEqual(1); ).toEqual(1);
}); });
it("can update with a new joined_count", () => {
mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomG, {
name: data[roomD].name,
required_state: [],
timeline: [],
joined_count: 1,
});
const gotRoom = client!.getRoom(roomG);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinedMemberCount()).toEqual(1);
});
// Regression test for a bug which caused the timeline entries to be out-of-order // Regression test for a bug which caused the timeline entries to be out-of-order
// when the same room appears twice with different timeline limits. E.g appears in // when the same room appears twice with different timeline limits. E.g appears in
// the list with timeline_limit:1 then appears again as a room subscription with // the list with timeline_limit:1 then appears again as a room subscription with
@@ -386,14 +433,15 @@ describe("SlidingSyncSdk", () => {
mkOwnEvent(EventType.RoomMessage, { body: "old event C" }), mkOwnEvent(EventType.RoomMessage, { body: "old event C" }),
...timeline, ...timeline,
]; ];
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomA, {
timeline: oldTimeline, timeline: oldTimeline,
required_state: [], required_state: [],
name: data[roomA].name, name: data[roomA].name,
initial: true, // e.g requested via room subscription initial: true, // e.g requested via room subscription
}); });
const gotRoom = client.getRoom(roomA); const gotRoom = client!.getRoom(roomA);
expect(gotRoom).toBeDefined(); expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
logger.log("want:", oldTimeline.map((e) => (e.type + " : " + (e.content || {}).body))); logger.log("want:", oldTimeline.map((e) => (e.type + " : " + (e.content || {}).body)));
logger.log("got:", gotRoom.getLiveTimeline().getEvents().map( logger.log("got:", gotRoom.getLiveTimeline().getEvents().map(
@@ -410,50 +458,50 @@ describe("SlidingSyncSdk", () => {
describe("lifecycle", () => { describe("lifecycle", () => {
beforeAll(async () => { beforeAll(async () => {
await setupClient(); await setupClient();
const hasSynced = sdk.sync(); const hasSynced = sdk!.sync();
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await hasSynced; await hasSynced;
}); });
const FAILED_SYNC_ERROR_THRESHOLD = 3; // would be nice to export the const in the actual class... const FAILED_SYNC_ERROR_THRESHOLD = 3; // would be nice to export the const in the actual class...
it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => { it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => {
mockSlidingSync.emit( mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete,
{ pos: "h", lists: [], rooms: {}, extensions: {} }, null, { pos: "h", lists: [], rooms: {}, extensions: {} }, null,
); );
expect(sdk.getSyncState()).toEqual(SyncState.Syncing); expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
mockSlidingSync.emit( mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"), SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
); );
expect(sdk.getSyncState()).toEqual(SyncState.Reconnecting); expect(sdk!.getSyncState()).toEqual(SyncState.Reconnecting);
for (let i = 0; i < FAILED_SYNC_ERROR_THRESHOLD; i++) { for (let i = 0; i < FAILED_SYNC_ERROR_THRESHOLD; i++) {
mockSlidingSync.emit( mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"), SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new Error("generic"),
); );
} }
expect(sdk.getSyncState()).toEqual(SyncState.Error); expect(sdk!.getSyncState()).toEqual(SyncState.Error);
}); });
it("emits SyncState.Syncing after a previous SyncState.Error", async () => { it("emits SyncState.Syncing after a previous SyncState.Error", async () => {
mockSlidingSync.emit( mockSlidingSync!.emit(
SlidingSyncEvent.Lifecycle, SlidingSyncEvent.Lifecycle,
SlidingSyncState.Complete, SlidingSyncState.Complete,
{ pos: "i", lists: [], rooms: {}, extensions: {} }, { pos: "i", lists: [], rooms: {}, extensions: {} },
null, null,
); );
expect(sdk.getSyncState()).toEqual(SyncState.Syncing); expect(sdk!.getSyncState()).toEqual(SyncState.Syncing);
}); });
it("emits SyncState.Error immediately when receiving M_UNKNOWN_TOKEN and stops syncing", async () => { it("emits SyncState.Error immediately when receiving M_UNKNOWN_TOKEN and stops syncing", async () => {
expect(mockSlidingSync.stop).not.toBeCalled(); expect(mockSlidingSync!.stop).not.toBeCalled();
mockSlidingSync.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({ mockSlidingSync!.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.RequestFinished, null, new MatrixError({
errcode: "M_UNKNOWN_TOKEN", errcode: "M_UNKNOWN_TOKEN",
message: "Oh no your access token is no longer valid", message: "Oh no your access token is no longer valid",
})); }));
expect(sdk.getSyncState()).toEqual(SyncState.Error); expect(sdk!.getSyncState()).toEqual(SyncState.Error);
expect(mockSlidingSync.stop).toBeCalled(); expect(mockSlidingSync!.stop).toBeCalled();
}); });
}); });
@@ -469,8 +517,8 @@ describe("SlidingSyncSdk", () => {
avatar_url: "mxc://foobar", avatar_url: "mxc://foobar",
displayname: "The Invitee", displayname: "The Invitee",
}; };
httpBackend.when("GET", "/profile").respond(200, inviteeProfile); httpBackend!.when("GET", "/profile").respond(200, inviteeProfile);
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomId, { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
initial: true, initial: true,
name: "Room with Invite", name: "Room with Invite",
required_state: [], required_state: [],
@@ -481,10 +529,10 @@ describe("SlidingSyncSdk", () => {
mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee), mkOwnStateEvent(EventType.RoomMember, { membership: "invite" }, invitee),
], ],
}); });
await httpBackend.flush("/profile", 1, 1000); await httpBackend!.flush("/profile", 1, 1000);
const room = client.getRoom(roomId); const room = client!.getRoom(roomId)!;
expect(room).toBeDefined(); expect(room).toBeDefined();
const inviteeMember = room.getMember(invitee); const inviteeMember = room.getMember(invitee)!;
expect(inviteeMember).toBeDefined(); expect(inviteeMember).toBeDefined();
expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url); expect(inviteeMember.getMxcAvatarUrl()).toEqual(inviteeProfile.avatar_url);
expect(inviteeMember.name).toEqual(inviteeProfile.displayname); expect(inviteeMember.name).toEqual(inviteeProfile.displayname);
@@ -497,8 +545,8 @@ describe("SlidingSyncSdk", () => {
await setupClient({ await setupClient({
withCrypto: true, withCrypto: true,
}); });
const hasSynced = sdk.sync(); const hasSynced = sdk!.sync();
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await hasSynced; await hasSynced;
ext = findExtension("e2ee"); ext = findExtension("e2ee");
}); });
@@ -506,7 +554,7 @@ describe("SlidingSyncSdk", () => {
// needed else we do some async operations in the background which can cause Jest to whine: // needed else we do some async operations in the background which can cause Jest to whine:
// "Cannot log after tests are done. Did you forget to wait for something async in your test?" // "Cannot log after tests are done. Did you forget to wait for something async in your test?"
// Attempted to log "Saving device tracking data null"." // Attempted to log "Saving device tracking data null"."
client.crypto.stop(); client!.crypto!.stop();
}); });
it("gets enabled on the initial request only", () => { it("gets enabled on the initial request only", () => {
expect(ext.onRequest(true)).toEqual({ expect(ext.onRequest(true)).toEqual({
@@ -524,38 +572,38 @@ describe("SlidingSyncSdk", () => {
// TODO: more assertions? // TODO: more assertions?
}); });
it("can update OTK counts", () => { it("can update OTK counts", () => {
client.crypto.updateOneTimeKeyCount = jest.fn(); client!.crypto!.updateOneTimeKeyCount = jest.fn();
ext.onResponse({ ext.onResponse({
device_one_time_keys_count: { device_one_time_keys_count: {
signed_curve25519: 42, signed_curve25519: 42,
}, },
}); });
expect(client.crypto.updateOneTimeKeyCount).toHaveBeenCalledWith(42); expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(42);
ext.onResponse({ ext.onResponse({
device_one_time_keys_count: { device_one_time_keys_count: {
not_signed_curve25519: 42, not_signed_curve25519: 42,
// missing field -> default to 0 // missing field -> default to 0
}, },
}); });
expect(client.crypto.updateOneTimeKeyCount).toHaveBeenCalledWith(0); expect(client!.crypto!.updateOneTimeKeyCount).toHaveBeenCalledWith(0);
}); });
it("can update fallback keys", () => { it("can update fallback keys", () => {
ext.onResponse({ ext.onResponse({
device_unused_fallback_key_types: ["signed_curve25519"], device_unused_fallback_key_types: ["signed_curve25519"],
}); });
expect(client.crypto.getNeedsNewFallback()).toEqual(false); expect(client!.crypto!.getNeedsNewFallback()).toEqual(false);
ext.onResponse({ ext.onResponse({
device_unused_fallback_key_types: ["not_signed_curve25519"], device_unused_fallback_key_types: ["not_signed_curve25519"],
}); });
expect(client.crypto.getNeedsNewFallback()).toEqual(true); expect(client!.crypto!.getNeedsNewFallback()).toEqual(true);
}); });
}); });
describe("ExtensionAccountData", () => { describe("ExtensionAccountData", () => {
let ext: Extension; let ext: Extension;
beforeAll(async () => { beforeAll(async () => {
await setupClient(); await setupClient();
const hasSynced = sdk.sync(); const hasSynced = sdk!.sync();
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await hasSynced; await hasSynced;
ext = findExtension("account_data"); ext = findExtension("account_data");
}); });
@@ -570,7 +618,7 @@ describe("SlidingSyncSdk", () => {
const globalContent = { const globalContent = {
info: "here", info: "here",
}; };
let globalData = client.getAccountData(globalType); let globalData = client!.getAccountData(globalType);
expect(globalData).toBeUndefined(); expect(globalData).toBeUndefined();
ext.onResponse({ ext.onResponse({
global: [ global: [
@@ -580,13 +628,13 @@ describe("SlidingSyncSdk", () => {
}, },
], ],
}); });
globalData = client.getAccountData(globalType); globalData = client!.getAccountData(globalType)!;
expect(globalData).toBeDefined(); expect(globalData).toBeDefined();
expect(globalData.getContent()).toEqual(globalContent); expect(globalData.getContent()).toEqual(globalContent);
}); });
it("processes rooms account data", async () => { it("processes rooms account data", async () => {
const roomId = "!room:id"; const roomId = "!room:id";
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomId, { mockSlidingSync!.emit(SlidingSyncEvent.RoomData, roomId, {
name: "Room with account data", name: "Room with account data",
required_state: [], required_state: [],
timeline: [ timeline: [
@@ -612,9 +660,9 @@ describe("SlidingSyncSdk", () => {
], ],
}, },
}); });
const room = client.getRoom(roomId); const room = client!.getRoom(roomId)!;
expect(room).toBeDefined(); expect(room).toBeDefined();
const event = room.getAccountData(roomType); const event = room.getAccountData(roomType)!;
expect(event).toBeDefined(); expect(event).toBeDefined();
expect(event.getContent()).toEqual(roomContent); expect(event.getContent()).toEqual(roomContent);
}); });
@@ -633,9 +681,9 @@ describe("SlidingSyncSdk", () => {
], ],
}, },
}); });
const room = client.getRoom(unknownRoomId); const room = client!.getRoom(unknownRoomId);
expect(room).toBeNull(); expect(room).toBeNull();
expect(client.getAccountData(roomType)).toBeUndefined(); expect(client!.getAccountData(roomType)).toBeUndefined();
}); });
it("can update push rules via account data", async () => { it("can update push rules via account data", async () => {
const roomId = "!foo:bar"; const roomId = "!foo:bar";
@@ -655,7 +703,7 @@ describe("SlidingSyncSdk", () => {
}], }],
}, },
}; };
let pushRule = client.getRoomPushRule("global", roomId); let pushRule = client!.getRoomPushRule("global", roomId);
expect(pushRule).toBeUndefined(); expect(pushRule).toBeUndefined();
ext.onResponse({ ext.onResponse({
global: [ global: [
@@ -665,16 +713,16 @@ describe("SlidingSyncSdk", () => {
}, },
], ],
}); });
pushRule = client.getRoomPushRule("global", roomId); pushRule = client!.getRoomPushRule("global", roomId)!;
expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific][0]); expect(pushRule).toEqual(pushRulesContent.global[PushRuleKind.RoomSpecific]![0]);
}); });
}); });
describe("ExtensionToDevice", () => { describe("ExtensionToDevice", () => {
let ext: Extension; let ext: Extension;
beforeAll(async () => { beforeAll(async () => {
await setupClient(); await setupClient();
const hasSynced = sdk.sync(); const hasSynced = sdk!.sync();
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await hasSynced; await hasSynced;
ext = findExtension("to_device"); ext = findExtension("to_device");
}); });
@@ -705,7 +753,7 @@ describe("SlidingSyncSdk", () => {
foo: "bar", foo: "bar",
}; };
let called = false; let called = false;
client.once(ClientEvent.ToDeviceEvent, (ev) => { client!.once(ClientEvent.ToDeviceEvent, (ev) => {
expect(ev.getContent()).toEqual(toDeviceContent); expect(ev.getContent()).toEqual(toDeviceContent);
expect(ev.getType()).toEqual(toDeviceType); expect(ev.getType()).toEqual(toDeviceType);
called = true; called = true;
@@ -723,7 +771,7 @@ describe("SlidingSyncSdk", () => {
}); });
it("can cancel key verification requests", async () => { it("can cancel key verification requests", async () => {
const seen: Record<string, boolean> = {}; const seen: Record<string, boolean> = {};
client.on(ClientEvent.ToDeviceEvent, (ev) => { client!.on(ClientEvent.ToDeviceEvent, (ev) => {
const evType = ev.getType(); const evType = ev.getType();
expect(seen[evType]).toBeFalsy(); expect(seen[evType]).toBeFalsy();
seen[evType] = true; seen[evType] = true;

View File

@@ -30,8 +30,8 @@ import { sleep } from "../../src/utils";
* Each test will call different functions on SlidingSync which may depend on state from previous tests. * Each test will call different functions on SlidingSync which may depend on state from previous tests.
*/ */
describe("SlidingSync", () => { describe("SlidingSync", () => {
let client: MatrixClient = null; let client: MatrixClient | undefined;
let httpBackend: MockHttpBackend = null; let httpBackend: MockHttpBackend | undefined;
const selfUserId = "@alice:localhost"; const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef"; const selfAccessToken = "aseukfgwef";
const proxyBaseUrl = "http://localhost:8008"; const proxyBaseUrl = "http://localhost:8008";
@@ -46,9 +46,9 @@ describe("SlidingSync", () => {
// tear down client/httpBackend globals // tear down client/httpBackend globals
const teardownClient = () => { const teardownClient = () => {
httpBackend.verifyNoOutstandingExpectation(); httpBackend!.verifyNoOutstandingExpectation();
client.stopClient(); client!.stopClient();
return httpBackend.stop(); return httpBackend!.stop();
}; };
describe("start/stop", () => { describe("start/stop", () => {
@@ -57,14 +57,14 @@ describe("SlidingSync", () => {
let slidingSync: SlidingSync; let slidingSync: SlidingSync;
it("should start the sync loop upon calling start()", async () => { it("should start the sync loop upon calling start()", async () => {
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1); slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
const fakeResp = { const fakeResp = {
pos: "a", pos: "a",
lists: [], lists: [],
rooms: {}, rooms: {},
extensions: {}, extensions: {},
}; };
httpBackend.when("POST", syncUrl).respond(200, fakeResp); httpBackend!.when("POST", syncUrl).respond(200, fakeResp);
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => { const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => {
expect(state).toEqual(SlidingSyncState.RequestFinished); expect(state).toEqual(SlidingSyncState.RequestFinished);
expect(resp).toEqual(fakeResp); expect(resp).toEqual(fakeResp);
@@ -72,13 +72,13 @@ describe("SlidingSync", () => {
return true; return true;
}); });
slidingSync.start(); slidingSync.start();
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await p; await p;
}); });
it("should stop the sync loop upon calling stop()", () => { it("should stop the sync loop upon calling stop()", () => {
slidingSync.stop(); slidingSync.stop();
httpBackend.verifyNoOutstandingExpectation(); httpBackend!.verifyNoOutstandingExpectation();
}); });
it("should reset the connection on HTTP 400 and send everything again", async () => { it("should reset the connection on HTTP 400 and send everything again", async () => {
@@ -203,9 +203,9 @@ describe("SlidingSync", () => {
it("should be able to subscribe to a room", async () => { it("should be able to subscribe to a room", async () => {
// add the subscription // add the subscription
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client, 1); slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
slidingSync.modifyRoomSubscriptions(new Set([roomId])); slidingSync.modifyRoomSubscriptions(new Set([roomId]));
httpBackend.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.log("room sub", body); logger.log("room sub", body);
expect(body.room_subscriptions).toBeTruthy(); expect(body.room_subscriptions).toBeTruthy();
@@ -225,7 +225,7 @@ describe("SlidingSync", () => {
return true; return true;
}); });
slidingSync.start(); slidingSync.start();
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await p; await p;
}); });
@@ -237,7 +237,7 @@ describe("SlidingSync", () => {
["m.room.member", "*"], ["m.room.member", "*"],
], ],
}; };
httpBackend.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.log("adjusted sub", body); logger.log("adjusted sub", body);
expect(body.room_subscriptions).toBeTruthy(); expect(body.room_subscriptions).toBeTruthy();
@@ -258,7 +258,7 @@ describe("SlidingSync", () => {
}); });
slidingSync.modifyRoomSubscriptionInfo(newSubInfo); slidingSync.modifyRoomSubscriptionInfo(newSubInfo);
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await p; await p;
// need to set what the new subscription info is for subsequent tests // need to set what the new subscription info is for subsequent tests
roomSubInfo = newSubInfo; roomSubInfo = newSubInfo;
@@ -279,7 +279,7 @@ describe("SlidingSync", () => {
required_state: [], required_state: [],
timeline: [], timeline: [],
}; };
httpBackend.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.log("new subs", body); logger.log("new subs", body);
expect(body.room_subscriptions).toBeTruthy(); expect(body.room_subscriptions).toBeTruthy();
@@ -304,12 +304,12 @@ describe("SlidingSync", () => {
const subs = slidingSync.getRoomSubscriptions(); const subs = slidingSync.getRoomSubscriptions();
subs.add(anotherRoomID); subs.add(anotherRoomID);
slidingSync.modifyRoomSubscriptions(subs); slidingSync.modifyRoomSubscriptions(subs);
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await p; await p;
}); });
it("should be able to unsubscribe from a room", async () => { it("should be able to unsubscribe from a room", async () => {
httpBackend.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.log("unsub request", body); logger.log("unsub request", body);
expect(body.room_subscriptions).toBeFalsy(); expect(body.room_subscriptions).toBeFalsy();
@@ -326,7 +326,7 @@ describe("SlidingSync", () => {
// remove the subscription for the first room // remove the subscription for the first room
slidingSync.modifyRoomSubscriptions(new Set([anotherRoomID])); slidingSync.modifyRoomSubscriptions(new Set([anotherRoomID]));
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await p; await p;
slidingSync.stop(); slidingSync.stop();
@@ -373,8 +373,8 @@ describe("SlidingSync", () => {
is_dm: true, is_dm: true,
}, },
}; };
slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client, 1); slidingSync = new SlidingSync(proxyBaseUrl, [listReq], {}, client!, 1);
httpBackend.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.log("list", body); logger.log("list", body);
expect(body.lists).toBeTruthy(); expect(body.lists).toBeTruthy();
@@ -401,7 +401,7 @@ describe("SlidingSync", () => {
return state === SlidingSyncState.Complete; return state === SlidingSyncState.Complete;
}); });
slidingSync.start(); slidingSync.start();
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await responseProcessed; await responseProcessed;
expect(listenerData[roomA]).toEqual(rooms[roomA]); expect(listenerData[roomA]).toEqual(rooms[roomA]);
@@ -427,7 +427,7 @@ describe("SlidingSync", () => {
it("should be possible to adjust list ranges", async () => { it("should be possible to adjust list ranges", async () => {
// modify the list ranges // modify the list ranges
httpBackend.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.log("next ranges", body.lists[0].ranges); logger.log("next ranges", body.lists[0].ranges);
expect(body.lists).toBeTruthy(); expect(body.lists).toBeTruthy();
@@ -451,7 +451,7 @@ describe("SlidingSync", () => {
return state === SlidingSyncState.RequestFinished; return state === SlidingSyncState.RequestFinished;
}); });
slidingSync.setListRanges(0, newRanges); slidingSync.setListRanges(0, newRanges);
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await responseProcessed; await responseProcessed;
}); });
@@ -464,7 +464,7 @@ describe("SlidingSync", () => {
"is_dm": true, "is_dm": true,
}, },
}; };
httpBackend.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.log("extra list", body); logger.log("extra list", body);
expect(body.lists).toBeTruthy(); expect(body.lists).toBeTruthy();
@@ -503,13 +503,13 @@ describe("SlidingSync", () => {
return state === SlidingSyncState.Complete; return state === SlidingSyncState.Complete;
}); });
slidingSync.setList(1, extraListReq); slidingSync.setList(1, extraListReq);
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await responseProcessed; await responseProcessed;
}); });
it("should be possible to get list DELETE/INSERTs", async () => { it("should be possible to get list DELETE/INSERTs", async () => {
// move C (2) to A (0) // move C (2) to A (0)
httpBackend.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "e", pos: "e",
lists: [{ lists: [{
count: 500, count: 500,
@@ -540,12 +540,12 @@ describe("SlidingSync", () => {
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete; return state === SlidingSyncState.Complete;
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await responseProcessed; await responseProcessed;
await listPromise; await listPromise;
// move C (0) back to A (2) // move C (0) back to A (2)
httpBackend.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "f", pos: "f",
lists: [{ lists: [{
count: 500, count: 500,
@@ -576,13 +576,13 @@ describe("SlidingSync", () => {
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete; return state === SlidingSyncState.Complete;
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await responseProcessed; await responseProcessed;
await listPromise; await listPromise;
}); });
it("should ignore invalid list indexes", async () => { it("should ignore invalid list indexes", async () => {
httpBackend.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "e", pos: "e",
lists: [{ lists: [{
count: 500, count: 500,
@@ -609,13 +609,13 @@ describe("SlidingSync", () => {
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete; return state === SlidingSyncState.Complete;
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await responseProcessed; await responseProcessed;
await listPromise; await listPromise;
}); });
it("should be possible to update a list", async () => { it("should be possible to update a list", async () => {
httpBackend.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "g", pos: "g",
lists: [{ lists: [{
count: 42, count: 42,
@@ -655,7 +655,7 @@ describe("SlidingSync", () => {
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete; return state === SlidingSyncState.Complete;
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await responseProcessed; await responseProcessed;
await listPromise; await listPromise;
}); });
@@ -667,7 +667,7 @@ describe("SlidingSync", () => {
1: roomC, 1: roomC,
}; };
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual(indexToRoomId); expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual(indexToRoomId);
httpBackend.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "f", pos: "f",
// currently the list is [B,C] so we will insert D then immediately delete it // currently the list is [B,C] so we will insert D then immediately delete it
lists: [{ lists: [{
@@ -698,7 +698,7 @@ describe("SlidingSync", () => {
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete; return state === SlidingSyncState.Complete;
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await responseProcessed; await responseProcessed;
await listPromise; await listPromise;
}); });
@@ -708,7 +708,7 @@ describe("SlidingSync", () => {
0: roomB, 0: roomB,
1: roomC, 1: roomC,
}); });
httpBackend.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "g", pos: "g",
lists: [{ lists: [{
count: 499, count: 499,
@@ -734,7 +734,7 @@ describe("SlidingSync", () => {
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete; return state === SlidingSyncState.Complete;
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await responseProcessed; await responseProcessed;
await listPromise; await listPromise;
}); });
@@ -743,7 +743,7 @@ describe("SlidingSync", () => {
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({ expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
0: roomC, 0: roomC,
}); });
httpBackend.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "h", pos: "h",
lists: [{ lists: [{
count: 500, count: 500,
@@ -770,11 +770,11 @@ describe("SlidingSync", () => {
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete; return state === SlidingSyncState.Complete;
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await responseProcessed; await responseProcessed;
await listPromise; await listPromise;
httpBackend.when("POST", syncUrl).respond(200, { httpBackend!.when("POST", syncUrl).respond(200, {
pos: "h", pos: "h",
lists: [{ lists: [{
count: 501, count: 501,
@@ -802,7 +802,7 @@ describe("SlidingSync", () => {
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => { responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete; return state === SlidingSyncState.Complete;
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await responseProcessed; await responseProcessed;
await listPromise; await listPromise;
slidingSync.stop(); slidingSync.stop();
@@ -825,11 +825,11 @@ describe("SlidingSync", () => {
], ],
}; };
// add the subscription // add the subscription
slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client, 1); slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1);
// modification before SlidingSync.start() // modification before SlidingSync.start()
const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId])); const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId]));
let txnId; let txnId;
httpBackend.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.debug("got ", body); logger.debug("got ", body);
expect(body.room_subscriptions).toBeTruthy(); expect(body.room_subscriptions).toBeTruthy();
@@ -852,7 +852,7 @@ describe("SlidingSync", () => {
}; };
}); });
slidingSync.start(); slidingSync.start();
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await subscribePromise; await subscribePromise;
}); });
it("should resolve setList during a connection", async () => { it("should resolve setList during a connection", async () => {
@@ -861,7 +861,7 @@ describe("SlidingSync", () => {
}; };
const promise = slidingSync.setList(0, newList); const promise = slidingSync.setList(0, newList);
let txnId; let txnId;
httpBackend.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.debug("got ", body); logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy(); expect(body.room_subscriptions).toBeFalsy();
@@ -876,14 +876,14 @@ describe("SlidingSync", () => {
extensions: {}, extensions: {},
}; };
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await promise; await promise;
expect(txnId).toBeDefined(); expect(txnId).toBeDefined();
}); });
it("should resolve setListRanges during a connection", async () => { it("should resolve setListRanges during a connection", async () => {
const promise = slidingSync.setListRanges(0, [[20, 40]]); const promise = slidingSync.setListRanges(0, [[20, 40]]);
let txnId; let txnId;
httpBackend.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.debug("got ", body); logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy(); expect(body.room_subscriptions).toBeFalsy();
@@ -900,7 +900,7 @@ describe("SlidingSync", () => {
extensions: {}, extensions: {},
}; };
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await promise; await promise;
expect(txnId).toBeDefined(); expect(txnId).toBeDefined();
}); });
@@ -909,7 +909,7 @@ describe("SlidingSync", () => {
timeline_limit: 99, timeline_limit: 99,
}); });
let txnId; let txnId;
httpBackend.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.debug("got ", body); logger.debug("got ", body);
expect(body.room_subscriptions).toBeTruthy(); expect(body.room_subscriptions).toBeTruthy();
@@ -925,22 +925,22 @@ describe("SlidingSync", () => {
extensions: {}, extensions: {},
}; };
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await promise; await promise;
expect(txnId).toBeDefined(); expect(txnId).toBeDefined();
}); });
it("should reject earlier pending promises if a later transaction is acknowledged", async () => { it("should reject earlier pending promises if a later transaction is acknowledged", async () => {
// i.e if we have [A,B,C] and see txn_id=C then A,B should be rejected. // i.e if we have [A,B,C] and see txn_id=C then A,B should be rejected.
const gotTxnIds = []; const gotTxnIds: any[] = [];
const pushTxn = function(req) { const pushTxn = function(req) {
gotTxnIds.push(req.data.txn_id); gotTxnIds.push(req.data.txn_id);
}; };
const failPromise = slidingSync.setListRanges(0, [[20, 40]]); const failPromise = slidingSync.setListRanges(0, [[20, 40]]);
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "e" }); // missing txn_id
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
const failPromise2 = slidingSync.setListRanges(0, [[60, 70]]); const failPromise2 = slidingSync.setListRanges(0, [[60, 70]]);
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "f" }); // missing txn_id
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection // attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
// which is a fail. // which is a fail.
@@ -949,7 +949,7 @@ describe("SlidingSync", () => {
const okPromise = slidingSync.setListRanges(0, [[0, 20]]); const okPromise = slidingSync.setListRanges(0, [[0, 20]]);
let txnId; let txnId;
httpBackend.when("POST", syncUrl).check((req) => { httpBackend!.when("POST", syncUrl).check((req) => {
txnId = req.data.txn_id; txnId = req.data.txn_id;
}).respond(200, () => { }).respond(200, () => {
// include the txn_id, earlier requests should now be reject()ed. // include the txn_id, earlier requests should now be reject()ed.
@@ -958,23 +958,23 @@ describe("SlidingSync", () => {
txn_id: txnId, txn_id: txnId,
}; };
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await okPromise; await okPromise;
expect(txnId).toBeDefined(); expect(txnId).toBeDefined();
}); });
it("should not reject later pending promises if an earlier transaction is acknowledged", async () => { it("should not reject later pending promises if an earlier transaction is acknowledged", async () => {
// i.e if we have [A,B,C] and see txn_id=B then C should not be rejected but A should. // i.e if we have [A,B,C] and see txn_id=B then C should not be rejected but A should.
const gotTxnIds = []; const gotTxnIds: any[] = [];
const pushTxn = function(req) { const pushTxn = function(req) {
gotTxnIds.push(req.data.txn_id); gotTxnIds.push(req.data?.txn_id);
}; };
const A = slidingSync.setListRanges(0, [[20, 40]]); const A = slidingSync.setListRanges(0, [[20, 40]]);
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" }); httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "A" });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
const B = slidingSync.setListRanges(0, [[60, 70]]); const B = slidingSync.setListRanges(0, [[60, 70]]);
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, { pos: "B" }); // missing txn_id
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
// attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection // attach rejection handlers now else if we do it later Jest treats that as an unhandled rejection
// which is a fail. // which is a fail.
@@ -985,14 +985,14 @@ describe("SlidingSync", () => {
C.finally(() => { C.finally(() => {
pendingC = false; pendingC = false;
}); });
httpBackend.when("POST", syncUrl).check(pushTxn).respond(200, () => { httpBackend!.when("POST", syncUrl).check(pushTxn).respond(200, () => {
// include the txn_id for B, so C's promise is outstanding // include the txn_id for B, so C's promise is outstanding
return { return {
pos: "C", pos: "C",
txn_id: gotTxnIds[1], txn_id: gotTxnIds[1],
}; };
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
// A is rejected, see above // A is rejected, see above
expect(B).resolves.toEqual(gotTxnIds[1]); // B is resolved expect(B).resolves.toEqual(gotTxnIds[1]); // B is resolved
expect(pendingC).toBe(true); // C is pending still expect(pendingC).toBe(true); // C is pending still
@@ -1004,7 +1004,7 @@ describe("SlidingSync", () => {
pending = false; pending = false;
}); });
let txnId; let txnId;
httpBackend.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.debug("got ", body); logger.debug("got ", body);
expect(body.room_subscriptions).toBeFalsy(); expect(body.room_subscriptions).toBeFalsy();
@@ -1021,7 +1021,7 @@ describe("SlidingSync", () => {
extensions: {}, extensions: {},
}; };
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
expect(txnId).toBeDefined(); expect(txnId).toBeDefined();
expect(pending).toBe(true); expect(pending).toBe(true);
slidingSync.stop(); slidingSync.stop();
@@ -1063,10 +1063,10 @@ describe("SlidingSync", () => {
}; };
it("should be able to register an extension", async () => { it("should be able to register an extension", async () => {
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1); slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
slidingSync.registerExtension(extPre); slidingSync.registerExtension(extPre);
const callbackOrder = []; const callbackOrder: string[] = [];
let extensionOnResponseCalled = false; let extensionOnResponseCalled = false;
onPreExtensionRequest = () => { onPreExtensionRequest = () => {
return extReq; return extReq;
@@ -1077,7 +1077,7 @@ describe("SlidingSync", () => {
expect(resp).toEqual(extResp); expect(resp).toEqual(extResp);
}; };
httpBackend.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.log("ext req", body); logger.log("ext req", body);
expect(body.extensions).toBeTruthy(); expect(body.extensions).toBeTruthy();
@@ -1098,7 +1098,7 @@ describe("SlidingSync", () => {
} }
}); });
slidingSync.start(); slidingSync.start();
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await p; await p;
expect(extensionOnResponseCalled).toBe(true); expect(extensionOnResponseCalled).toBe(true);
expect(callbackOrder).toEqual(["onPreExtensionResponse", "Lifecycle"]); expect(callbackOrder).toEqual(["onPreExtensionResponse", "Lifecycle"]);
@@ -1112,7 +1112,7 @@ describe("SlidingSync", () => {
onPreExtensionResponse = (resp) => { onPreExtensionResponse = (resp) => {
responseCalled = true; responseCalled = true;
}; };
httpBackend.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.log("ext req nothing", body); logger.log("ext req nothing", body);
expect(body.extensions).toBeTruthy(); expect(body.extensions).toBeTruthy();
@@ -1130,7 +1130,7 @@ describe("SlidingSync", () => {
const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => { const p = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state, resp, err) => {
return state === SlidingSyncState.Complete; return state === SlidingSyncState.Complete;
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await p; await p;
expect(responseCalled).toBe(false); expect(responseCalled).toBe(false);
}); });
@@ -1141,13 +1141,13 @@ describe("SlidingSync", () => {
return extReq; return extReq;
}; };
let responseCalled = false; let responseCalled = false;
const callbackOrder = []; const callbackOrder: string[] = [];
onPostExtensionResponse = (resp) => { onPostExtensionResponse = (resp) => {
expect(resp).toEqual(extResp); expect(resp).toEqual(extResp);
responseCalled = true; responseCalled = true;
callbackOrder.push("onPostExtensionResponse"); callbackOrder.push("onPostExtensionResponse");
}; };
httpBackend.when("POST", syncUrl).check(function(req) { httpBackend!.when("POST", syncUrl).check(function(req) {
const body = req.data; const body = req.data;
logger.log("ext req after start", body); logger.log("ext req after start", body);
expect(body.extensions).toBeTruthy(); expect(body.extensions).toBeTruthy();
@@ -1171,7 +1171,7 @@ describe("SlidingSync", () => {
return true; return true;
} }
}); });
await httpBackend.flushAllExpected(); await httpBackend!.flushAllExpected();
await p; await p;
expect(responseCalled).toBe(true); expect(responseCalled).toBe(true);
expect(callbackOrder).toEqual(["Lifecycle", "onPostExtensionResponse"]); expect(callbackOrder).toEqual(["Lifecycle", "onPostExtensionResponse"]);
@@ -1179,7 +1179,7 @@ describe("SlidingSync", () => {
}); });
it("is not possible to register the same extension name twice", async () => { it("is not possible to register the same extension name twice", async () => {
slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1); slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1);
slidingSync.registerExtension(extPre); slidingSync.registerExtension(extPre);
expect(() => { slidingSync.registerExtension(extPre); }).toThrow(); expect(() => { slidingSync.registerExtension(extPre); }).toThrow();
}); });
@@ -1206,7 +1206,7 @@ function listenUntil<T>(
callback: (...args: any[]) => T, callback: (...args: any[]) => T,
timeoutMs = 500, timeoutMs = 500,
): Promise<T> { ): Promise<T> {
const trace = new Error().stack.split(`\n`)[2]; const trace = new Error().stack?.split(`\n`)[2];
return Promise.race([new Promise<T>((resolve, reject) => { return Promise.race([new Promise<T>((resolve, reject) => {
const wrapper = (...args) => { const wrapper = (...args) => {
try { try {

View File

@@ -20,6 +20,7 @@ import * as utils from "../src/utils";
// try to load the olm library. // try to load the olm library.
try { try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
global.Olm = require('@matrix-org/olm'); global.Olm = require('@matrix-org/olm');
logger.log('loaded libolm'); logger.log('loaded libolm');
} catch (e) { } catch (e) {
@@ -28,6 +29,7 @@ try {
// also try to set node crypto // also try to set node crypto
try { try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const crypto = require('crypto'); const crypto = require('crypto');
utils.setCrypto(crypto); utils.setCrypto(crypto);
} catch (err) { } catch (err) {

94
spec/test-utils/client.ts Normal file
View 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),
});

View File

@@ -24,5 +24,5 @@ limitations under the License.
* expect(beaconLivenessEmits.length).toBe(1); * expect(beaconLivenessEmits.length).toBe(1);
* ``` * ```
*/ */
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, unknown[]>) => export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, any[]>) =>
spy.mock.calls.filter((args) => args[0] === eventType); spy.mock.calls.filter((args) => args[0] === eventType);

View File

@@ -6,7 +6,7 @@ import '../olm-loader';
import { logger } from '../../src/logger'; import { logger } from '../../src/logger';
import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event"; import { IContent, IEvent, IUnsigned, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
import { ClientEvent, EventType, MatrixClient, MsgType } from "../../src"; import { ClientEvent, EventType, IPusher, MatrixClient, MsgType } from "../../src";
import { SyncState } from "../../src/sync"; import { SyncState } from "../../src/sync";
import { eventMapperFor } from "../../src/event-mapper"; import { eventMapperFor } from "../../src/event-mapper";
@@ -74,6 +74,7 @@ interface IEventOpts {
sender?: string; sender?: string;
skey?: string; skey?: string;
content: IContent; content: IContent;
prev_content?: IContent;
user?: string; user?: string;
unsigned?: IUnsigned; unsigned?: IUnsigned;
redacts?: string; redacts?: string;
@@ -103,6 +104,7 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
room_id: opts.room, room_id: opts.room,
sender: opts.sender || opts.user, // opts.user for backwards-compat sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content, content: opts.content,
prev_content: opts.prev_content,
unsigned: opts.unsigned || {}, unsigned: opts.unsigned || {},
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(), event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
txn_id: "~" + Math.random(), txn_id: "~" + Math.random(),
@@ -147,9 +149,9 @@ export function mkEventCustom<T>(base: T): T & GeneratedMetadata {
interface IPresenceOpts { interface IPresenceOpts {
user?: string; user?: string;
sender?: string; sender?: string;
url: string; url?: string;
name: string; name?: string;
ago: number; ago?: number;
presence?: string; presence?: string;
event?: boolean; event?: boolean;
} }
@@ -371,3 +373,14 @@ export async function awaitDecryption(event: MatrixEvent): Promise<MatrixEvent>
} }
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r)); export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise(r => e.once(k, r));
export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
app_display_name: "app",
app_id: "123",
data: {},
device_display_name: "name",
kind: "http",
lang: "en",
pushkey: "pushpush",
...extra,
});

View File

@@ -21,18 +21,21 @@ describe("NamespacedValue", () => {
const ns = new NamespacedValue("stable", "unstable"); const ns = new NamespacedValue("stable", "unstable");
expect(ns.name).toBe(ns.stable); expect(ns.name).toBe(ns.stable);
expect(ns.altName).toBe(ns.unstable); expect(ns.altName).toBe(ns.unstable);
expect(ns.names).toEqual([ns.stable, ns.unstable]);
}); });
it("should return unstable if there is no stable", () => { it("should return unstable if there is no stable", () => {
const ns = new NamespacedValue(null, "unstable"); const ns = new NamespacedValue(null, "unstable");
expect(ns.name).toBe(ns.unstable); expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBeFalsy(); expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.unstable]);
}); });
it("should have a falsey unstable if needed", () => { it("should have a falsey unstable if needed", () => {
const ns = new NamespacedValue("stable", null); const ns = new NamespacedValue("stable", null);
expect(ns.name).toBe(ns.stable); expect(ns.name).toBe(ns.stable);
expect(ns.altName).toBeFalsy(); expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.stable]);
}); });
it("should match against either stable or unstable", () => { it("should match against either stable or unstable", () => {
@@ -58,12 +61,14 @@ describe("UnstableValue", () => {
const ns = new UnstableValue("stable", "unstable"); const ns = new UnstableValue("stable", "unstable");
expect(ns.name).toBe(ns.unstable); expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBe(ns.stable); expect(ns.altName).toBe(ns.stable);
expect(ns.names).toEqual([ns.unstable, ns.stable]);
}); });
it("should return unstable if there is no stable", () => { it("should return unstable if there is no stable", () => {
const ns = new UnstableValue(null, "unstable"); const ns = new UnstableValue(null, "unstable");
expect(ns.name).toBe(ns.unstable); expect(ns.name).toBe(ns.unstable);
expect(ns.altName).toBeFalsy(); expect(ns.altName).toBeFalsy();
expect(ns.names).toEqual([ns.unstable]);
}); });
it("should not permit falsey unstable values", () => { it("should not permit falsey unstable values", () => {

View File

@@ -22,6 +22,7 @@ import {
makeBeaconContent, makeBeaconContent,
makeBeaconInfoContent, makeBeaconInfoContent,
makeTopicContent, makeTopicContent,
parseBeaconContent,
parseTopicContent, parseTopicContent,
} from "../../src/content-helpers"; } from "../../src/content-helpers";
@@ -127,6 +128,66 @@ describe('Beacon content helpers', () => {
}); });
}); });
}); });
describe("parseBeaconContent()", () => {
it("should not explode when parsing an invalid beacon", () => {
// deliberate cast to simulate wire content being invalid
const result = parseBeaconContent({} as any);
expect(result).toEqual({
description: undefined,
uri: undefined,
timestamp: undefined,
});
});
it("should parse unstable values", () => {
const uri = "urigoeshere";
const description = "descriptiongoeshere";
const timestamp = 1234;
const result = parseBeaconContent({
"org.matrix.msc3488.location": {
uri,
description,
},
"org.matrix.msc3488.ts": timestamp,
// relationship not used - just here to satisfy types
"m.relates_to": {
rel_type: "m.reference",
event_id: "$unused",
},
});
expect(result).toEqual({
description,
uri,
timestamp,
});
});
it("should parse stable values", () => {
const uri = "urigoeshere";
const description = "descriptiongoeshere";
const timestamp = 1234;
const result = parseBeaconContent({
"m.location": {
uri,
description,
},
"m.ts": timestamp,
// relationship not used - just here to satisfy types
"m.relates_to": {
rel_type: "m.reference",
event_id: "$unused",
},
});
expect(result).toEqual({
description,
uri,
timestamp,
});
});
});
}); });
describe('Topic content helpers', () => { describe('Topic content helpers', () => {

View File

@@ -15,6 +15,9 @@ import { CRYPTO_ENABLED } from "../../src/client";
import { DeviceInfo } from "../../src/crypto/deviceinfo"; import { DeviceInfo } from "../../src/crypto/deviceinfo";
import { logger } from '../../src/logger'; import { logger } from '../../src/logger';
import { MemoryStore } from "../../src"; import { MemoryStore } from "../../src";
import { RoomKeyRequestState } from '../../src/crypto/OutgoingRoomKeyRequestManager';
import { RoomMember } from '../../src/models/room-member';
import { IStore } from '../../src/store';
const Olm = global.Olm; const Olm = global.Olm;
@@ -39,20 +42,52 @@ async function keyshareEventForEvent(client, event, index): Promise<MatrixEvent>
type: "m.forwarded_room_key", type: "m.forwarded_room_key",
sender: client.getUserId(), sender: client.getUserId(),
content: { content: {
algorithm: olmlib.MEGOLM_ALGORITHM, "algorithm": olmlib.MEGOLM_ALGORITHM,
room_id: roomId, "room_id": roomId,
sender_key: eventContent.sender_key, "sender_key": eventContent.sender_key,
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key, "sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
session_id: eventContent.session_id, "session_id": eventContent.session_id,
session_key: key.key, "session_key": key.key,
chain_index: key.chain_index, "chain_index": key.chain_index,
forwarding_curve25519_key_chain: "forwarding_curve25519_key_chain": key.forwarding_curve_key_chain,
key.forwarding_curve_key_chain, "org.matrix.msc3061.shared_history": true,
}, },
}); });
// make onRoomKeyEvent think this was an encrypted event // make onRoomKeyEvent think this was an encrypted event
// @ts-ignore private property // @ts-ignore private property
ksEvent.senderCurve25519Key = "akey"; ksEvent.senderCurve25519Key = "akey";
ksEvent.getWireType = () => "m.room.encrypted";
ksEvent.getWireContent = () => {
return {
algorithm: "m.olm.v1.curve25519-aes-sha2",
};
};
return ksEvent;
}
function roomKeyEventForEvent(client: MatrixClient, event: MatrixEvent): MatrixEvent {
const roomId = event.getRoomId();
const eventContent = event.getWireContent();
const key = client.crypto.olmDevice.getOutboundGroupSessionKey(eventContent.session_id);
const ksEvent = new MatrixEvent({
type: "m.room_key",
sender: client.getUserId(),
content: {
"algorithm": olmlib.MEGOLM_ALGORITHM,
"room_id": roomId,
"session_id": eventContent.session_id,
"session_key": key.key,
},
});
// make onRoomKeyEvent think this was an encrypted event
// @ts-ignore private property
ksEvent.senderCurve25519Key = event.getSenderKey();
ksEvent.getWireType = () => "m.room.encrypted";
ksEvent.getWireContent = () => {
return {
algorithm: "m.olm.v1.curve25519-aes-sha2",
};
};
return ksEvent; return ksEvent;
} }
@@ -94,7 +129,7 @@ describe("Crypto", function() {
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI'; event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };}; event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };};
event.getForwardingCurve25519KeyChain = () => ["not empty"]; event.getForwardingCurve25519KeyChain = () => ["not empty"];
event.isKeySourceUntrusted = () => false; event.isKeySourceUntrusted = () => true;
event.getClaimedEd25519Key = event.getClaimedEd25519Key =
() => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; () => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
@@ -158,8 +193,8 @@ describe("Crypto", function() {
let fakeEmitter; let fakeEmitter;
beforeEach(async function() { beforeEach(async function() {
const mockStorage = new MockStorageApi(); const mockStorage = new MockStorageApi() as unknown as Storage;
const clientStore = new MemoryStore({ localStorage: mockStorage }); const clientStore = new MemoryStore({ localStorage: mockStorage }) as unknown as IStore;
const cryptoStore = new MemoryCryptoStore(); const cryptoStore = new MemoryCryptoStore();
cryptoStore.storeEndToEndDeviceData({ cryptoStore.storeEndToEndDeviceData({
@@ -232,6 +267,7 @@ describe("Crypto", function() {
describe('Key requests', function() { describe('Key requests', function() {
let aliceClient: MatrixClient; let aliceClient: MatrixClient;
let bobClient: MatrixClient; let bobClient: MatrixClient;
let claraClient: MatrixClient;
beforeEach(async function() { beforeEach(async function() {
aliceClient = (new TestClient( aliceClient = (new TestClient(
@@ -240,22 +276,35 @@ describe("Crypto", function() {
bobClient = (new TestClient( bobClient = (new TestClient(
"@bob:example.com", "bobdevice", "@bob:example.com", "bobdevice",
)).client; )).client;
claraClient = (new TestClient(
"@clara:example.com", "claradevice",
)).client;
await aliceClient.initCrypto(); await aliceClient.initCrypto();
await bobClient.initCrypto(); await bobClient.initCrypto();
await claraClient.initCrypto();
}); });
afterEach(async function() { afterEach(async function() {
aliceClient.stopClient(); aliceClient.stopClient();
bobClient.stopClient(); bobClient.stopClient();
claraClient.stopClient();
}); });
it("does not cancel keyshare requests if some messages are not decrypted", async function() { it("does not cancel keyshare requests until all messages are decrypted with trusted keys", async function() {
const encryptionCfg = { const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2", "algorithm": "m.megolm.v1.aes-sha2",
}; };
const roomId = "!someroom"; const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {}); const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {}); const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
// Make Bob invited by Alice so Bob will accept Alice's forwarded keys
bobRoom.currentState.setStateEvents([new MatrixEvent({
type: "m.room.member",
sender: "@alice:example.com",
room_id: roomId,
content: { membership: "invite" },
state_key: "@bob:example.com",
})]);
aliceClient.store.storeRoom(aliceRoom); aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom); bobClient.store.storeRoom(bobRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg); await aliceClient.setRoomEncryption(roomId, encryptionCfg);
@@ -301,6 +350,9 @@ describe("Crypto", function() {
} }
})); }));
const device = new DeviceInfo(aliceClient.deviceId);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
const bobDecryptor = bobClient.crypto.getRoomDecryptor( const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM, roomId, olmlib.MEGOLM_ALGORITHM,
); );
@@ -313,6 +365,8 @@ describe("Crypto", function() {
// the first message can't be decrypted yet, but the second one // the first message can't be decrypted yet, but the second one
// can // can
let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1); let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1);
bobClient.crypto.deviceList.downloadKeys = () => Promise.resolve({});
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
await bobDecryptor.onRoomKeyEvent(ksEvent); await bobDecryptor.onRoomKeyEvent(ksEvent);
await decryptEventsPromise; await decryptEventsPromise;
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted"); expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
@@ -339,8 +393,24 @@ describe("Crypto", function() {
await bobDecryptor.onRoomKeyEvent(ksEvent); await bobDecryptor.onRoomKeyEvent(ksEvent);
await decryptEventPromise; await decryptEventPromise;
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted"); expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
expect(events[0].isKeySourceUntrusted()).toBeTruthy();
await sleep(1); await sleep(1);
// the room key request should be gone since we've now decrypted everything // the room key request should still be there, since we've
// decrypted everything with an untrusted key
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeDefined();
// Now share a trusted room key event so Bob will re-decrypt the messages.
// Bob will backfill trust when they receive a trusted session with a higher
// index that connects to an untrusted session with a lower index.
const roomKeyEvent = roomKeyEventForEvent(aliceClient, events[1]);
const trustedDecryptEventPromise = awaitEvent(events[0], "Event.decrypted");
await bobDecryptor.onRoomKeyEvent(roomKeyEvent);
await trustedDecryptEventPromise;
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
expect(events[0].isKeySourceUntrusted()).toBeFalsy();
await sleep(1);
// now the room key request should be gone, since there's
// no better key to wait for
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy(); expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody)).toBeFalsy();
}); });
@@ -382,6 +452,9 @@ describe("Crypto", function() {
// decryption keys yet // decryption keys yet
} }
const device = new DeviceInfo(aliceClient.deviceId);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
const bobDecryptor = bobClient.crypto.getRoomDecryptor( const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM, roomId, olmlib.MEGOLM_ALGORITHM,
); );
@@ -461,6 +534,420 @@ describe("Crypto", function() {
expect(aliceSendToDevice).toBeCalledTimes(3); expect(aliceSendToDevice).toBeCalledTimes(3);
expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId); expect(aliceSendToDevice.mock.calls[2][2]).not.toBe(txnId);
}); });
it("should accept forwarded keys which it requested", async function() {
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
const events = [
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$1",
content: {
msgtype: "m.text",
body: "1",
},
}),
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$2",
content: {
msgtype: "m.text",
body: "2",
},
}),
];
await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
// @ts-ignore private properties
event.senderCurve25519Key = null;
// @ts-ignore private properties
event.claimedEd25519Key = null;
try {
await bobClient.crypto.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
}
}));
const device = new DeviceInfo(aliceClient.deviceId);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const cryptoStore = bobClient.crypto.cryptoStore;
const eventContent = events[0].getWireContent();
const senderKey = eventContent.sender_key;
const sessionId = eventContent.session_id;
const roomKeyRequestBody = {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: roomId,
sender_key: senderKey,
session_id: sessionId,
};
const outgoingReq = await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody);
expect(outgoingReq).toBeDefined();
await cryptoStore.updateOutgoingRoomKeyRequest(
outgoingReq.requestId, RoomKeyRequestState.Unsent,
{ state: RoomKeyRequestState.Sent },
);
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
const decryptEventsPromise = Promise.all(events.map((ev) => {
return awaitEvent(ev, "Event.decrypted");
}));
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
roomId,
events[0].getWireContent().sender_key,
events[0].getWireContent().session_id,
);
expect(key).not.toBeNull();
await decryptEventsPromise;
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
});
it("should accept forwarded keys from the user who invited it to the room", async function() {
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
const claraRoom = new Room(roomId, claraClient, "@clara:example.com", {});
// Make Bob invited by Clara
bobRoom.currentState.setStateEvents([new MatrixEvent({
type: "m.room.member",
sender: "@clara:example.com",
room_id: roomId,
content: { membership: "invite" },
state_key: "@bob:example.com",
})]);
aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom);
claraClient.store.storeRoom(claraRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
await claraClient.setRoomEncryption(roomId, encryptionCfg);
const events = [
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$1",
content: {
msgtype: "m.text",
body: "1",
},
}),
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$2",
content: {
msgtype: "m.text",
body: "2",
},
}),
];
await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
// @ts-ignore private properties
event.senderCurve25519Key = null;
// @ts-ignore private properties
event.claimedEd25519Key = null;
try {
await bobClient.crypto.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
}
}));
const device = new DeviceInfo(claraClient.deviceId);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@clara:example.com";
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
const decryptEventsPromise = Promise.all(events.map((ev) => {
return awaitEvent(ev, "Event.decrypted");
}));
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
ksEvent.event.sender = claraClient.getUserId(),
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId());
await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
roomId,
events[0].getWireContent().sender_key,
events[0].getWireContent().session_id,
);
expect(key).not.toBeNull();
await decryptEventsPromise;
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
});
it("should accept forwarded keys from one of its own user's other devices", async function() {
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
const events = [
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$1",
content: {
msgtype: "m.text",
body: "1",
},
}),
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$2",
content: {
msgtype: "m.text",
body: "2",
},
}),
];
await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
// @ts-ignore private properties
event.senderCurve25519Key = null;
// @ts-ignore private properties
event.claimedEd25519Key = null;
try {
await bobClient.crypto.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
}
}));
const device = new DeviceInfo(claraClient.deviceId);
device.verified = DeviceInfo.DeviceVerification.VERIFIED;
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:example.com";
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
const decryptEventsPromise = Promise.all(events.map((ev) => {
return awaitEvent(ev, "Event.decrypted");
}));
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
ksEvent.event.sender = bobClient.getUserId(),
ksEvent.sender = new RoomMember(roomId, bobClient.getUserId());
await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
roomId,
events[0].getWireContent().sender_key,
events[0].getWireContent().session_id,
);
expect(key).not.toBeNull();
await decryptEventsPromise;
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
});
it("should not accept unexpected forwarded keys for a room it's in", async function() {
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
const claraRoom = new Room(roomId, claraClient, "@clara:example.com", {});
aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom);
claraClient.store.storeRoom(claraRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
await claraClient.setRoomEncryption(roomId, encryptionCfg);
const events = [
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$1",
content: {
msgtype: "m.text",
body: "1",
},
}),
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$2",
content: {
msgtype: "m.text",
body: "2",
},
}),
];
await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
// @ts-ignore private properties
event.senderCurve25519Key = null;
// @ts-ignore private properties
event.claimedEd25519Key = null;
try {
await bobClient.crypto.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
}
}));
const device = new DeviceInfo(claraClient.deviceId);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
ksEvent.event.sender = claraClient.getUserId(),
ksEvent.sender = new RoomMember(roomId, claraClient.getUserId());
await bobDecryptor.onRoomKeyEvent(ksEvent);
const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
roomId,
events[0].getWireContent().sender_key,
events[0].getWireContent().session_id,
);
expect(key).toBeNull();
});
it("should park forwarded keys for a room it's not in", async function() {
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
aliceClient.store.storeRoom(aliceRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
const events = [
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$1",
content: {
msgtype: "m.text",
body: "1",
},
}),
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$2",
content: {
msgtype: "m.text",
body: "2",
},
}),
];
await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient.crypto.encryptEvent(event, aliceRoom);
// remove keys from the event
// @ts-ignore private properties
event.clearEvent = undefined;
// @ts-ignore private properties
event.senderCurve25519Key = null;
// @ts-ignore private properties
event.claimedEd25519Key = null;
}));
const device = new DeviceInfo(aliceClient.deviceId);
bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device;
bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
const content = events[0].getWireContent();
const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0);
await bobDecryptor.onRoomKeyEvent(ksEvent);
const bobKey = await bobClient.crypto.olmDevice.getInboundGroupSessionKey(
roomId,
content.sender_key,
content.session_id,
);
expect(bobKey).toBeNull();
const aliceKey = await aliceClient.crypto.olmDevice.getInboundGroupSessionKey(
roomId,
content.sender_key,
content.session_id,
);
const parked = await bobClient.crypto.cryptoStore.takeParkedSharedHistory(roomId);
expect(parked).toEqual([{
senderId: aliceClient.getUserId(),
senderKey: content.sender_key,
sessionId: content.session_id,
sessionKey: aliceKey.key,
keysClaimed: { ed25519: aliceKey.sender_claimed_ed25519_key },
forwardingCurve25519KeyChain: ["akey"],
}]);
});
}); });
describe('Secret storage', function() { describe('Secret storage', function() {
@@ -469,12 +956,12 @@ describe("Crypto", function() {
jest.setTimeout(10000); jest.setTimeout(10000);
const client = (new TestClient("@a:example.com", "dev")).client; const client = (new TestClient("@a:example.com", "dev")).client;
await client.initCrypto(); await client.initCrypto();
client.crypto.getSecretStorageKey = async () => null; client.crypto.getSecretStorageKey = jest.fn().mockResolvedValue(null);
client.crypto.isCrossSigningReady = async () => false; client.crypto.isCrossSigningReady = async () => false;
client.crypto.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null); client.crypto.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
client.crypto.baseApis.setAccountData = () => null; client.crypto.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
client.crypto.baseApis.uploadKeySignatures = () => null; client.crypto.baseApis.uploadKeySignatures = jest.fn();
client.crypto.baseApis.http.authedRequest = () => null; client.crypto.baseApis.http.authedRequest = jest.fn();
const createSecretStorageKey = async () => { const createSecretStorageKey = async () => {
return { return {
keyInfo: undefined, // Returning undefined here used to cause a crash keyInfo: undefined, // Returning undefined here used to cause a crash

View File

@@ -32,8 +32,8 @@ import { ClientEvent, MatrixClient, RoomMember } from '../../../../src';
import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo'; import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo';
import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning'; import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning';
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2']; const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
const ROOM_ID = '!ROOM:ID'; const ROOM_ID = '!ROOM:ID';
@@ -110,6 +110,12 @@ describe("MegolmDecryption", function() {
senderCurve25519Key: "SENDER_CURVE25519", senderCurve25519Key: "SENDER_CURVE25519",
claimedEd25519Key: "SENDER_ED25519", claimedEd25519Key: "SENDER_ED25519",
}; };
event.getWireType = () => "m.room.encrypted";
event.getWireContent = () => {
return {
algorithm: "m.olm.v1.curve25519-aes-sha2",
};
};
const mockCrypto = { const mockCrypto = {
decryptEvent: function() { decryptEvent: function() {

View File

@@ -34,7 +34,7 @@ import { IAbortablePromise, MatrixScheduler } from '../../../src';
const Olm = global.Olm; const Olm = global.Olm;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
const ROOM_ID = '!ROOM:ID'; const ROOM_ID = '!ROOM:ID';
@@ -214,6 +214,12 @@ describe("MegolmBackup", function() {
const event = new MatrixEvent({ const event = new MatrixEvent({
type: 'm.room.encrypted', type: 'm.room.encrypted',
}); });
event.getWireType = () => "m.room.encrypted";
event.getWireContent = () => {
return {
algorithm: "m.olm.v1.curve25519-aes-sha2",
};
};
const decryptedData = { const decryptedData = {
clearEvent: { clearEvent: {
type: 'm.room_key', type: 'm.room_key',

View File

@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { import { CryptoStore } from '../../../src/crypto/store/base';
IndexedDBCryptoStore, import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store';
} from '../../../src/crypto/store/indexeddb-crypto-store'; import { LocalStorageCryptoStore } from '../../../src/crypto/store/localStorage-crypto-store';
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store'; import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager'; import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
@@ -26,36 +26,39 @@ import 'jest-localstorage-mock';
const requests = [ const requests = [
{ {
requestId: "A", requestId: "A",
requestBody: { session_id: "A", room_id: "A" }, requestBody: { session_id: "A", room_id: "A", sender_key: "A", algorithm: "m.megolm.v1.aes-sha2" },
state: RoomKeyRequestState.Sent, state: RoomKeyRequestState.Sent,
recipients: [
{ userId: "@alice:example.com", deviceId: "*" },
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
],
}, },
{ {
requestId: "B", requestId: "B",
requestBody: { session_id: "B", room_id: "B" }, requestBody: { session_id: "B", room_id: "B", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" },
state: RoomKeyRequestState.Sent, state: RoomKeyRequestState.Sent,
recipients: [
{ userId: "@alice:example.com", deviceId: "*" },
{ userId: "@carrie:example.com", deviceId: "barbazquux" },
],
}, },
{ {
requestId: "C", requestId: "C",
requestBody: { session_id: "C", room_id: "C" }, requestBody: { session_id: "C", room_id: "C", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" },
state: RoomKeyRequestState.Unsent, state: RoomKeyRequestState.Unsent,
recipients: [
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
],
}, },
]; ];
describe.each([ describe.each([
["IndexedDBCryptoStore", ["IndexedDBCryptoStore",
() => new IndexedDBCryptoStore(global.indexedDB, "tests")], () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["LocalStorageCryptoStore", ["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
() => new IndexedDBCryptoStore(undefined, "tests")], ["MemoryCryptoStore", () => new MemoryCryptoStore()],
["MemoryCryptoStore", () => {
const store = new IndexedDBCryptoStore(undefined, "tests");
// @ts-ignore set private properties
store.backend = new MemoryCryptoStore();
// @ts-ignore
store.backendPromise = Promise.resolve(store.backend);
return store;
}],
])("Outgoing room key requests [%s]", function(name, dbFactory) { ])("Outgoing room key requests [%s]", function(name, dbFactory) {
let store; let store: CryptoStore;
beforeAll(async () => { beforeAll(async () => {
store = dbFactory(); store = dbFactory();
@@ -75,6 +78,15 @@ describe.each([
}); });
}); });
it("getOutgoingRoomKeyRequestsByTarget retrieves all entries with a given target",
async () => {
const r = await store.getOutgoingRoomKeyRequestsByTarget(
"@becca:example.com", "foobarbaz", [RoomKeyRequestState.Sent],
);
expect(r).toHaveLength(1);
expect(r[0]).toEqual(requests[0]);
});
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state", test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
async () => { async () => {
const r = const r =

View File

@@ -26,6 +26,7 @@ import { logger } from '../../../src/logger';
import * as utils from "../../../src/utils"; import * as utils from "../../../src/utils";
import { ICreateClientOpts } from '../../../src/client'; import { ICreateClientOpts } from '../../../src/client';
import { ISecretStorageKeyInfo } from '../../../src/crypto/api'; import { ISecretStorageKeyInfo } from '../../../src/crypto/api';
import { DeviceInfo } from '../../../src/crypto/deviceinfo';
try { try {
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -250,20 +251,20 @@ describe("Secrets", function() {
osborne2.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", { osborne2.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
"VAX": { "VAX": {
user_id: "@alice:example.com", known: false,
device_id: "VAX",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: { keys: {
"ed25519:VAX": vaxDevice.deviceEd25519Key, "ed25519:VAX": vaxDevice.deviceEd25519Key,
"curve25519:VAX": vaxDevice.deviceCurve25519Key, "curve25519:VAX": vaxDevice.deviceCurve25519Key,
}, },
verified: DeviceInfo.DeviceVerification.VERIFIED,
}, },
}); });
vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", { vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
"Osborne2": { "Osborne2": {
user_id: "@alice:example.com",
device_id: "Osborne2",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
verified: 0,
known: false,
keys: { keys: {
"ed25519:Osborne2": osborne2Device.deviceEd25519Key, "ed25519:Osborne2": osborne2Device.deviceEd25519Key,
"curve25519:Osborne2": osborne2Device.deviceCurve25519Key, "curve25519:Osborne2": osborne2Device.deviceCurve25519Key,
@@ -280,10 +281,12 @@ describe("Secrets", function() {
Object.values(otks)[0], Object.values(otks)[0],
); );
const request = await secretStorage.request("foo", ["VAX"]); osborne2.client.crypto.deviceList.downloadKeys = () => Promise.resolve({});
const secret = await request.promise; osborne2.client.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com";
const request = await secretStorage.request("foo", ["VAX"]);
await request.promise; // return value not used
expect(secret).toBe("bar");
osborne2.stop(); osborne2.stop();
vax.stop(); vax.stop();
clearTestClientTimeouts(); clearTestClientTimeouts();

View File

@@ -13,9 +13,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MatrixClient } from "../../../../src/client";
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel"; import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
import { MatrixEvent } from "../../../../src/models/event"; import { MatrixEvent } from "../../../../src/models/event";
"../../../../src/crypto/verification/request/ToDeviceChannel";
describe("InRoomChannel tests", function() { describe("InRoomChannel tests", function() {
const ALICE = "@alice:hs.tld"; const ALICE = "@alice:hs.tld";
@@ -23,7 +23,7 @@ describe("InRoomChannel tests", function() {
const MALORY = "@malory:hs.tld"; const MALORY = "@malory:hs.tld";
const client = { const client = {
getUserId() { return ALICE; }, getUserId() { return ALICE; },
}; } as unknown as MatrixClient;
it("getEventType only returns .request for a message with a msgtype", function() { it("getEventType only returns .request for a message with a msgtype", function() {
const invalidEvent = new MatrixEvent({ const invalidEvent = new MatrixEvent({

View File

@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import "../../../olm-loader"; import "../../../olm-loader";
import { verificationMethods } from "../../../../src/crypto"; import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
import { logger } from "../../../../src/logger"; import { logger } from "../../../../src/logger";
import { SAS } from "../../../../src/crypto/verification/SAS"; import { SAS } from "../../../../src/crypto/verification/SAS";
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util'; import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
@@ -52,23 +52,22 @@ describe("verification request integration tests with crypto layer", function()
alice.client.crypto.deviceList.getRawStoredDevicesForUser = function() { alice.client.crypto.deviceList.getRawStoredDevicesForUser = function() {
return { return {
Dynabook: { Dynabook: {
algorithms: [],
verified: 0,
known: false,
keys: { keys: {
"ed25519:Dynabook": "bob+base64+ed25519+key", "ed25519:Dynabook": "bob+base64+ed25519+key",
}, },
}, },
}; };
}; };
alice.client.downloadKeys = () => { alice.client.downloadKeys = jest.fn().mockResolvedValue({});
return Promise.resolve(); bob.client.downloadKeys = jest.fn().mockResolvedValue({});
}; bob.client.on(CryptoEvent.VerificationRequest, (request) => {
bob.client.downloadKeys = () => {
return Promise.resolve();
};
bob.client.on("crypto.verification.request", (request) => {
const bobVerifier = request.beginKeyVerification(verificationMethods.SAS); const bobVerifier = request.beginKeyVerification(verificationMethods.SAS);
bobVerifier.verify(); bobVerifier.verify();
// XXX: Private function access (but it's a test, so we're okay) // @ts-ignore Private function access (but it's a test, so we're okay)
bobVerifier.endTimer(); bobVerifier.endTimer();
}); });
const aliceRequest = await alice.client.requestVerification("@bob:example.com"); const aliceRequest = await alice.client.requestVerification("@bob:example.com");
@@ -76,7 +75,7 @@ describe("verification request integration tests with crypto layer", function()
const aliceVerifier = aliceRequest.verifier; const aliceVerifier = aliceRequest.verifier;
expect(aliceVerifier).toBeInstanceOf(SAS); expect(aliceVerifier).toBeInstanceOf(SAS);
// XXX: Private function access (but it's a test, so we're okay) // @ts-ignore Private function access (but it's a test, so we're okay)
aliceVerifier.endTimer(); aliceVerifier.endTimer();
alice.stop(); alice.stop();

View File

@@ -19,10 +19,14 @@ import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
import { MatrixEvent } from "../../../../src/models/event"; import { MatrixEvent } from "../../../../src/models/event";
import { SAS } from "../../../../src/crypto/verification/SAS"; import { SAS } from "../../../../src/crypto/verification/SAS";
import { DeviceInfo } from "../../../../src/crypto/deviceinfo"; import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
import { verificationMethods } from "../../../../src/crypto"; import { CryptoEvent, verificationMethods } from "../../../../src/crypto";
import * as olmlib from "../../../../src/crypto/olmlib"; import * as olmlib from "../../../../src/crypto/olmlib";
import { logger } from "../../../../src/logger"; import { logger } from "../../../../src/logger";
import { resetCrossSigningKeys } from "../crypto-utils"; import { resetCrossSigningKeys } from "../crypto-utils";
import { VerificationBase } from "../../../../src/crypto/verification/Base";
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
import { MatrixClient } from "../../../../src";
import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest";
const Olm = global.Olm; const Olm = global.Olm;
@@ -48,13 +52,15 @@ describe("SAS verification", function() {
//channel, baseApis, userId, deviceId, startEvent, request //channel, baseApis, userId, deviceId, startEvent, request
const request = { const request = {
onVerifierCancelled: function() {}, onVerifierCancelled: function() {},
}; } as VerificationRequest;
const channel = { const channel = {
send: function() { send: function() {
return Promise.resolve(); return Promise.resolve();
}, },
}; } as unknown as IVerificationChannel;
const sas = new SAS(channel, {}, "@alice:example.com", "ABCDEFG", null, request); const mockClient = {} as unknown as MatrixClient;
const event = new MatrixEvent({ type: 'test' });
const sas = new SAS(channel, mockClient, "@alice:example.com", "ABCDEFG", event, request);
sas.handleEvent(new MatrixEvent({ sas.handleEvent(new MatrixEvent({
sender: "@alice:example.com", sender: "@alice:example.com",
type: "es.inquisition", type: "es.inquisition",
@@ -65,7 +71,7 @@ describe("SAS verification", function() {
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
// Cancel the SAS for cleanup (we started a verification, so abort) // Cancel the SAS for cleanup (we started a verification, so abort)
sas.cancel(); sas.cancel(new Error('error'));
}); });
describe("verification", () => { describe("verification", () => {
@@ -403,16 +409,12 @@ describe("SAS verification", function() {
}, },
); );
alice.client.setDeviceVerified = jest.fn(); alice.client.setDeviceVerified = jest.fn();
alice.client.downloadKeys = () => { alice.client.downloadKeys = jest.fn().mockResolvedValue({});
return Promise.resolve();
};
bob.client.setDeviceVerified = jest.fn(); bob.client.setDeviceVerified = jest.fn();
bob.client.downloadKeys = () => { bob.client.downloadKeys = jest.fn().mockResolvedValue({});
return Promise.resolve();
};
const bobPromise = new Promise((resolve, reject) => { const bobPromise = new Promise<VerificationBase<any, any>>((resolve, reject) => {
bob.client.on("crypto.verification.request", request => { bob.client.on(CryptoEvent.VerificationRequest, request => {
request.verifier.on("show_sas", (e) => { request.verifier.on("show_sas", (e) => {
e.mismatch(); e.mismatch();
}); });
@@ -421,7 +423,7 @@ describe("SAS verification", function() {
}); });
const aliceVerifier = alice.client.beginKeyVerification( const aliceVerifier = alice.client.beginKeyVerification(
verificationMethods.SAS, bob.client.getUserId(), bob.client.deviceId, verificationMethods.SAS, bob.client.getUserId()!, bob.client.deviceId!,
); );
const aliceSpy = jest.fn(); const aliceSpy = jest.fn();
@@ -462,7 +464,7 @@ describe("SAS verification", function() {
}, },
); );
alice.client.setDeviceVerified = jest.fn(); alice.client.crypto.setDeviceVerification = jest.fn();
alice.client.getDeviceEd25519Key = () => { alice.client.getDeviceEd25519Key = () => {
return "alice+base64+ed25519+key"; return "alice+base64+ed25519+key";
}; };
@@ -480,7 +482,7 @@ describe("SAS verification", function() {
return Promise.resolve(); return Promise.resolve();
}; };
bob.client.setDeviceVerified = jest.fn(); bob.client.crypto.setDeviceVerification = jest.fn();
bob.client.getStoredDevice = () => { bob.client.getStoredDevice = () => {
return DeviceInfo.fromStorage( return DeviceInfo.fromStorage(
{ {
@@ -501,7 +503,7 @@ describe("SAS verification", function() {
aliceSasEvent = null; aliceSasEvent = null;
bobSasEvent = null; bobSasEvent = null;
bobPromise = new Promise((resolve, reject) => { bobPromise = new Promise<void>((resolve, reject) => {
bob.client.on("crypto.verification.request", async (request) => { bob.client.on("crypto.verification.request", async (request) => {
const verifier = request.beginKeyVerification(SAS.NAME); const verifier = request.beginKeyVerification(SAS.NAME);
verifier.on("show_sas", (e) => { verifier.on("show_sas", (e) => {
@@ -563,10 +565,24 @@ describe("SAS verification", function() {
]); ]);
// make sure Alice and Bob verified each other // make sure Alice and Bob verified each other
expect(alice.client.setDeviceVerified) expect(alice.client.crypto.setDeviceVerification)
.toHaveBeenCalledWith(bob.client.getUserId(), bob.client.deviceId); .toHaveBeenCalledWith(
expect(bob.client.setDeviceVerified) bob.client.getUserId(),
.toHaveBeenCalledWith(alice.client.getUserId(), alice.client.deviceId); bob.client.deviceId,
true,
null,
null,
{ "ed25519:Dynabook": "bob+base64+ed25519+key" },
);
expect(bob.client.crypto.setDeviceVerification)
.toHaveBeenCalledWith(
alice.client.getUserId(),
alice.client.deviceId,
true,
null,
null,
{ "ed25519:Osborne2": "alice+base64+ed25519+key" },
);
}); });
}); });
}); });

View File

@@ -18,6 +18,9 @@ import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
import { encodeBase64 } from "../../../../src/crypto/olmlib"; import { encodeBase64 } from "../../../../src/crypto/olmlib";
import { setupWebcrypto, teardownWebcrypto } from './util'; import { setupWebcrypto, teardownWebcrypto } from './util';
import { VerificationBase } from '../../../../src/crypto/verification/Base'; import { VerificationBase } from '../../../../src/crypto/verification/Base';
import { MatrixClient, MatrixEvent } from '../../../../src';
import { VerificationRequest } from '../../../../src/crypto/verification/request/VerificationRequest';
import { IVerificationChannel } from '../../../../src/crypto/verification/request/Channel';
jest.useFakeTimers(); jest.useFakeTimers();
@@ -54,9 +57,21 @@ describe("self-verifications", () => {
cacheCallbacks, cacheCallbacks,
); );
crossSigningInfo.keys = { crossSigningInfo.keys = {
master: { keys: { X: testKeyPub } }, master: {
self_signing: { keys: { X: testKeyPub } }, keys: { X: testKeyPub },
user_signing: { keys: { X: testKeyPub } }, usage: [],
user_id: 'user-id',
},
self_signing: {
keys: { X: testKeyPub },
usage: [],
user_id: 'user-id',
},
user_signing: {
keys: { X: testKeyPub },
usage: [],
user_id: 'user-id',
},
}; };
const secretStorage = { const secretStorage = {
@@ -79,20 +94,22 @@ describe("self-verifications", () => {
getUserId: () => userId, getUserId: () => userId,
getKeyBackupVersion: () => Promise.resolve({}), getKeyBackupVersion: () => Promise.resolve({}),
restoreKeyBackupWithCache, restoreKeyBackupWithCache,
}; } as unknown as MatrixClient;
const request = { const request = {
onVerifierFinished: () => undefined, onVerifierFinished: () => undefined,
}; } as unknown as VerificationRequest;
const verification = new VerificationBase( const verification = new VerificationBase(
undefined, // channel undefined as unknown as IVerificationChannel, // channel
client, // baseApis client, // baseApis
userId, userId,
"ABC", // deviceId "ABC", // deviceId
undefined, // startEvent undefined as unknown as MatrixEvent, // startEvent
request, request,
); );
// @ts-ignore set private property
verification.resolve = () => undefined; verification.resolve = () => undefined;
const result = await verification.done(); const result = await verification.done();

View File

@@ -19,20 +19,23 @@ import nodeCrypto from "crypto";
import { TestClient } from '../../../TestClient'; import { TestClient } from '../../../TestClient';
import { MatrixEvent } from "../../../../src/models/event"; import { MatrixEvent } from "../../../../src/models/event";
import { IRoomTimelineData } from "../../../../src/models/event-timeline-set";
import { Room, RoomEvent } from "../../../../src/models/room";
import { logger } from '../../../../src/logger'; import { logger } from '../../../../src/logger';
import { MatrixClient, ClientEvent } from '../../../../src/client';
export async function makeTestClients(userInfos, options) { export async function makeTestClients(userInfos, options): Promise<[TestClient[], () => void]> {
const clients = []; const clients: TestClient[] = [];
const timeouts = []; const timeouts: ReturnType<typeof setTimeout>[] = [];
const clientMap = {}; const clientMap: Record<string, Record<string, MatrixClient>> = {};
const sendToDevice = function(type, map) { const makeSendToDevice = (matrixClient: MatrixClient): MatrixClient['sendToDevice'] => async (type, map) => {
// logger.log(this.getUserId(), "sends", type, map); // logger.log(this.getUserId(), "sends", type, map);
for (const [userId, devMap] of Object.entries(map)) { for (const [userId, devMap] of Object.entries(map)) {
if (userId in clientMap) { if (userId in clientMap) {
for (const [deviceId, msg] of Object.entries(devMap)) { for (const [deviceId, msg] of Object.entries(devMap)) {
if (deviceId in clientMap[userId]) { if (deviceId in clientMap[userId]) {
const event = new MatrixEvent({ const event = new MatrixEvent({
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this sender: matrixClient.getUserId()!,
type: type, type: type,
content: msg, content: msg,
}); });
@@ -42,18 +45,19 @@ export async function makeTestClients(userInfos, options) {
Promise.resolve(); Promise.resolve();
decryptionPromise.then( decryptionPromise.then(
() => client.emit("toDeviceEvent", event), () => client.emit(ClientEvent.ToDeviceEvent, event),
); );
} }
} }
} }
} }
return {};
}; };
const sendEvent = function(room, type, content) { const makeSendEvent = (matrixClient: MatrixClient) => (room, type, content) => {
// make up a unique ID as the event ID // make up a unique ID as the event ID
const eventId = "$" + this.makeTxnId(); // eslint-disable-line @babel/no-invalid-this const eventId = "$" + matrixClient.makeTxnId();
const rawEvent = { const rawEvent = {
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this sender: matrixClient.getUserId()!,
type: type, type: type,
content: content, content: content,
room_id: room, room_id: room,
@@ -63,22 +67,24 @@ export async function makeTestClients(userInfos, options) {
const event = new MatrixEvent(rawEvent); const event = new MatrixEvent(rawEvent);
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, { const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
unsigned: { unsigned: {
transaction_id: this.makeTxnId(), // eslint-disable-line @babel/no-invalid-this transaction_id: matrixClient.makeTxnId(),
}, },
})); }));
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
for (const tc of clients) { for (const tc of clients) {
if (tc.client === this) { // eslint-disable-line @babel/no-invalid-this const room = new Room('test', tc.client, tc.client.getUserId()!);
const roomTimelineData = {} as unknown as IRoomTimelineData;
if (tc.client === matrixClient) {
logger.log("sending remote echo!!"); logger.log("sending remote echo!!");
tc.client.emit("Room.timeline", remoteEcho); tc.client.emit(RoomEvent.Timeline, remoteEcho, room, false, false, roomTimelineData);
} else { } else {
tc.client.emit("Room.timeline", event); tc.client.emit(RoomEvent.Timeline, event, room, false, false, roomTimelineData);
} }
} }
}); });
timeouts.push(timeout); timeouts.push(timeout as unknown as ReturnType<typeof setTimeout>);
return Promise.resolve({ event_id: eventId }); return Promise.resolve({ event_id: eventId });
}; };
@@ -99,8 +105,8 @@ export async function makeTestClients(userInfos, options) {
clientMap[userInfo.userId] = {}; clientMap[userInfo.userId] = {};
} }
clientMap[userInfo.userId][userInfo.deviceId] = testClient.client; clientMap[userInfo.userId][userInfo.deviceId] = testClient.client;
testClient.client.sendToDevice = sendToDevice; testClient.client.sendToDevice = makeSendToDevice(testClient.client);
testClient.client.sendEvent = sendEvent; testClient.client.sendEvent = makeSendEvent(testClient.client);
clients.push(testClient); clients.push(testClient);
} }
@@ -116,11 +122,12 @@ export async function makeTestClients(userInfos, options) {
export function setupWebcrypto() { export function setupWebcrypto() {
global.crypto = { global.crypto = {
getRandomValues: (buf) => { getRandomValues: (buf) => {
return nodeCrypto.randomFillSync(buf); return nodeCrypto.randomFillSync(buf as any);
}, },
}; } as unknown as Crypto;
} }
export function teardownWebcrypto() { export function teardownWebcrypto() {
// @ts-ignore undefined != Crypto
global.crypto = undefined; global.crypto = undefined;
} }

View File

@@ -19,11 +19,18 @@ import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoo
import { ToDeviceChannel } from import { ToDeviceChannel } from
"../../../../src/crypto/verification/request/ToDeviceChannel"; "../../../../src/crypto/verification/request/ToDeviceChannel";
import { MatrixEvent } from "../../../../src/models/event"; import { MatrixEvent } from "../../../../src/models/event";
import { MatrixClient } from "../../../../src/client";
import { setupWebcrypto, teardownWebcrypto } from "./util"; import { setupWebcrypto, teardownWebcrypto } from "./util";
import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel";
import { VerificationBase } from "../../../../src/crypto/verification/Base";
function makeMockClient(userId, deviceId) { type MockClient = MatrixClient & {
popEvents: () => MatrixEvent[];
popDeviceEvents: (userId: string, deviceId: string) => MatrixEvent[];
};
function makeMockClient(userId: string, deviceId: string): MockClient {
let counter = 1; let counter = 1;
let events = []; let events: MatrixEvent[] = [];
const deviceEvents = {}; const deviceEvents = {};
return { return {
getUserId() { return userId; }, getUserId() { return userId; },
@@ -54,16 +61,18 @@ function makeMockClient(userId, deviceId) {
deviceEvents[userId][deviceId].push(event); deviceEvents[userId][deviceId].push(event);
} }
} }
return Promise.resolve(); return Promise.resolve({});
}, },
popEvents() { // @ts-ignore special testing fn
popEvents(): MatrixEvent[] {
const e = events; const e = events;
events = []; events = [];
return e; return e;
}, },
popDeviceEvents(userId, deviceId) { // @ts-ignore special testing fn
popDeviceEvents(userId: string, deviceId: string): MatrixEvent[] {
const forDevice = deviceEvents[userId]; const forDevice = deviceEvents[userId];
const events = forDevice && forDevice[deviceId]; const events = forDevice && forDevice[deviceId];
const result = events || []; const result = events || [];
@@ -72,12 +81,21 @@ function makeMockClient(userId, deviceId) {
} }
return result; return result;
}, },
}; } as unknown as MockClient;
} }
const MOCK_METHOD = "mock-verify"; const MOCK_METHOD = "mock-verify";
class MockVerifier { class MockVerifier extends VerificationBase<'', any> {
constructor(channel, client, userId, deviceId, startEvent) { public _channel;
public _startEvent;
constructor(
channel: IVerificationChannel,
client: MatrixClient,
userId: string,
deviceId: string,
startEvent: MatrixEvent,
) {
super(channel, client, userId, deviceId, startEvent, {} as unknown as VerificationRequest);
this._channel = channel; this._channel = channel;
this._startEvent = startEvent; this._startEvent = startEvent;
} }
@@ -115,7 +133,10 @@ function makeRemoteEcho(event) {
async function distributeEvent(ownRequest, theirRequest, event) { async function distributeEvent(ownRequest, theirRequest, event) {
await ownRequest.channel.handleEvent( await ownRequest.channel.handleEvent(
makeRemoteEcho(event), ownRequest, true); makeRemoteEcho(event),
ownRequest,
true,
);
await theirRequest.channel.handleEvent(event, theirRequest, true); await theirRequest.channel.handleEvent(event, theirRequest, true);
} }
@@ -133,12 +154,19 @@ describe("verification request unit tests", function() {
it("transition from UNSENT to DONE through happy path", async function() { it("transition from UNSENT to DONE through happy path", async function() {
const alice = makeMockClient("@alice:matrix.tld", "device1"); const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1"); const bob = makeMockClient("@bob:matrix.tld", "device1");
const verificationMethods = new Map(
[[MOCK_METHOD, MockVerifier]],
) as unknown as Map<string, typeof VerificationBase>;
const aliceRequest = new VerificationRequest( const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()), new InRoomChannel(alice, "!room", bob.getUserId()!),
new Map([[MOCK_METHOD, MockVerifier]]), alice); verificationMethods,
alice,
);
const bobRequest = new VerificationRequest( const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"), new InRoomChannel(bob, "!room"),
new Map([[MOCK_METHOD, MockVerifier]]), bob); verificationMethods,
bob,
);
expect(aliceRequest.invalid).toBe(true); expect(aliceRequest.invalid).toBe(true);
expect(bobRequest.invalid).toBe(true); expect(bobRequest.invalid).toBe(true);
@@ -157,7 +185,7 @@ describe("verification request unit tests", function() {
expect(aliceRequest.ready).toBe(true); expect(aliceRequest.ready).toBe(true);
const verifier = aliceRequest.beginKeyVerification(MOCK_METHOD); const verifier = aliceRequest.beginKeyVerification(MOCK_METHOD);
await verifier.start(); await (verifier as MockVerifier).start();
const [startEvent] = alice.popEvents(); const [startEvent] = alice.popEvents();
expect(startEvent.getType()).toBe(START_TYPE); expect(startEvent.getType()).toBe(START_TYPE);
await distributeEvent(aliceRequest, bobRequest, startEvent); await distributeEvent(aliceRequest, bobRequest, startEvent);
@@ -165,8 +193,7 @@ describe("verification request unit tests", function() {
expect(aliceRequest.verifier).toBeInstanceOf(MockVerifier); expect(aliceRequest.verifier).toBeInstanceOf(MockVerifier);
expect(bobRequest.started).toBe(true); expect(bobRequest.started).toBe(true);
expect(bobRequest.verifier).toBeInstanceOf(MockVerifier); expect(bobRequest.verifier).toBeInstanceOf(MockVerifier);
await (bobRequest.verifier as MockVerifier).start();
await bobRequest.verifier.start();
const [bobDoneEvent] = bob.popEvents(); const [bobDoneEvent] = bob.popEvents();
expect(bobDoneEvent.getType()).toBe(DONE_TYPE); expect(bobDoneEvent.getType()).toBe(DONE_TYPE);
await distributeEvent(bobRequest, aliceRequest, bobDoneEvent); await distributeEvent(bobRequest, aliceRequest, bobDoneEvent);
@@ -180,12 +207,20 @@ describe("verification request unit tests", function() {
it("methods only contains common methods", async function() { it("methods only contains common methods", async function() {
const alice = makeMockClient("@alice:matrix.tld", "device1"); const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1"); const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceVerificationMethods = new Map(
[["c", function() {}], ["a", function() {}]],
) as unknown as Map<string, typeof VerificationBase>;
const bobVerificationMethods = new Map(
[["c", function() {}], ["b", function() {}]],
) as unknown as Map<string, typeof VerificationBase>;
const aliceRequest = new VerificationRequest( const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()), new InRoomChannel(alice, "!room", bob.getUserId()!),
new Map([["c", function() {}], ["a", function() {}]]), alice); aliceVerificationMethods, alice);
const bobRequest = new VerificationRequest( const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"), new InRoomChannel(bob, "!room"),
new Map([["c", function() {}], ["b", function() {}]]), bob); bobVerificationMethods,
bob,
);
await aliceRequest.sendRequest(); await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents(); const [requestEvent] = alice.popEvents();
await distributeEvent(aliceRequest, bobRequest, requestEvent); await distributeEvent(aliceRequest, bobRequest, requestEvent);
@@ -201,13 +236,22 @@ describe("verification request unit tests", function() {
const bob1 = makeMockClient("@bob:matrix.tld", "device1"); const bob1 = makeMockClient("@bob:matrix.tld", "device1");
const bob2 = makeMockClient("@bob:matrix.tld", "device2"); const bob2 = makeMockClient("@bob:matrix.tld", "device2");
const aliceRequest = new VerificationRequest( const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob1.getUserId()), new Map(), alice); new InRoomChannel(alice, "!room", bob1.getUserId()!),
new Map(),
alice,
);
await aliceRequest.sendRequest(); await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents(); const [requestEvent] = alice.popEvents();
const bob1Request = new VerificationRequest( const bob1Request = new VerificationRequest(
new InRoomChannel(bob1, "!room"), new Map(), bob1); new InRoomChannel(bob1, "!room"),
new Map(),
bob1,
);
const bob2Request = new VerificationRequest( const bob2Request = new VerificationRequest(
new InRoomChannel(bob2, "!room"), new Map(), bob2); new InRoomChannel(bob2, "!room"),
new Map(),
bob2,
);
await bob1Request.channel.handleEvent(requestEvent, bob1Request, true); await bob1Request.channel.handleEvent(requestEvent, bob1Request, true);
await bob2Request.channel.handleEvent(requestEvent, bob2Request, true); await bob2Request.channel.handleEvent(requestEvent, bob2Request, true);
@@ -222,22 +266,34 @@ describe("verification request unit tests", function() {
it("verify own device with to_device messages", async function() { it("verify own device with to_device messages", async function() {
const bob1 = makeMockClient("@bob:matrix.tld", "device1"); const bob1 = makeMockClient("@bob:matrix.tld", "device1");
const bob2 = makeMockClient("@bob:matrix.tld", "device2"); const bob2 = makeMockClient("@bob:matrix.tld", "device2");
const verificationMethods = new Map(
[[MOCK_METHOD, MockVerifier]],
) as unknown as Map<string, typeof VerificationBase>;
const bob1Request = new VerificationRequest( const bob1Request = new VerificationRequest(
new ToDeviceChannel(bob1, bob1.getUserId(), ["device1", "device2"], new ToDeviceChannel(
ToDeviceChannel.makeTransactionId(), "device2"), bob1,
new Map([[MOCK_METHOD, MockVerifier]]), bob1); bob1.getUserId()!,
["device1", "device2"],
ToDeviceChannel.makeTransactionId(),
"device2",
),
verificationMethods,
bob1,
);
const to = { userId: "@bob:matrix.tld", deviceId: "device2" }; const to = { userId: "@bob:matrix.tld", deviceId: "device2" };
const verifier = bob1Request.beginKeyVerification(MOCK_METHOD, to); const verifier = bob1Request.beginKeyVerification(MOCK_METHOD, to);
expect(verifier).toBeInstanceOf(MockVerifier); expect(verifier).toBeInstanceOf(MockVerifier);
await verifier.start(); await (verifier as MockVerifier).start();
const [startEvent] = bob1.popDeviceEvents(to.userId, to.deviceId); const [startEvent] = bob1.popDeviceEvents(to.userId, to.deviceId);
expect(startEvent.getType()).toBe(START_TYPE); expect(startEvent.getType()).toBe(START_TYPE);
const bob2Request = new VerificationRequest( const bob2Request = new VerificationRequest(
new ToDeviceChannel(bob2, bob2.getUserId(), ["device1"]), new ToDeviceChannel(bob2, bob2.getUserId()!, ["device1"]),
new Map([[MOCK_METHOD, MockVerifier]]), bob2); verificationMethods,
bob2,
);
await bob2Request.channel.handleEvent(startEvent, bob2Request, true); await bob2Request.channel.handleEvent(startEvent, bob2Request, true);
await bob2Request.verifier.start(); await (bob2Request.verifier as MockVerifier).start();
const [doneEvent1] = bob2.popDeviceEvents("@bob:matrix.tld", "device1"); const [doneEvent1] = bob2.popDeviceEvents("@bob:matrix.tld", "device1");
expect(doneEvent1.getType()).toBe(DONE_TYPE); expect(doneEvent1.getType()).toBe(DONE_TYPE);
await bob1Request.channel.handleEvent(doneEvent1, bob1Request, true); await bob1Request.channel.handleEvent(doneEvent1, bob1Request, true);
@@ -253,11 +309,13 @@ describe("verification request unit tests", function() {
const alice = makeMockClient("@alice:matrix.tld", "device1"); const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1"); const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceRequest = new VerificationRequest( const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()), new Map(), alice); new InRoomChannel(alice, "!room", bob.getUserId()!),
new Map(),
alice,
);
await aliceRequest.sendRequest(); await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents(); const [requestEvent] = alice.popEvents();
await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true, await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true);
true, true);
expect(aliceRequest.cancelled).toBe(false); expect(aliceRequest.cancelled).toBe(false);
expect(aliceRequest._cancellingUserId).toBe(undefined); expect(aliceRequest._cancellingUserId).toBe(undefined);
@@ -269,11 +327,17 @@ describe("verification request unit tests", function() {
const alice = makeMockClient("@alice:matrix.tld", "device1"); const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1"); const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceRequest = new VerificationRequest( const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()), new Map(), alice); new InRoomChannel(alice, "!room", bob.getUserId()!),
new Map(),
alice,
);
await aliceRequest.sendRequest(); await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents(); const [requestEvent] = alice.popEvents();
const bobRequest = new VerificationRequest( const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"), new Map(), bob); new InRoomChannel(bob, "!room"),
new Map(),
bob,
);
await bobRequest.channel.handleEvent(requestEvent, bobRequest, true); await bobRequest.channel.handleEvent(requestEvent, bobRequest, true);

View File

@@ -16,14 +16,15 @@ limitations under the License.
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
import { import {
DuplicateStrategy,
EventTimeline, EventTimeline,
EventTimelineSet, EventTimelineSet,
EventType, EventType,
Filter,
MatrixClient, MatrixClient,
MatrixEvent, MatrixEvent,
MatrixEventEvent, MatrixEventEvent,
Room, Room,
DuplicateStrategy,
} from '../../src'; } from '../../src';
import { Thread } from "../../src/models/thread"; import { Thread } from "../../src/models/thread";
import { ReEmitter } from "../../src/ReEmitter"; import { ReEmitter } from "../../src/ReEmitter";
@@ -291,4 +292,34 @@ describe('EventTimelineSet', () => {
expect(eventTimelineSet.canContain(event)).toBeTruthy(); expect(eventTimelineSet.canContain(event)).toBeTruthy();
}); });
}); });
describe("handleRemoteEcho", () => {
it("should add to liveTimeline only if the event matches the filter", () => {
const filter = new Filter(client.getUserId()!, "test_filter");
filter.setDefinition({
room: {
timeline: {
types: [EventType.RoomMessage],
},
},
});
const eventTimelineSet = new EventTimelineSet(room, { filter }, client);
const roomMessageEvent = new MatrixEvent({
type: EventType.RoomMessage,
content: { body: "test" },
event_id: "!test1:server",
});
eventTimelineSet.handleRemoteEcho(roomMessageEvent, "~!local-event-id:server", roomMessageEvent.getId());
expect(eventTimelineSet.getLiveTimeline().getEvents()).toContain(roomMessageEvent);
const roomFilteredEvent = new MatrixEvent({
type: "other_event_type",
content: { body: "test" },
event_id: "!test2:server",
});
eventTimelineSet.handleRemoteEcho(roomFilteredEvent, "~!local-event-id:server", roomFilteredEvent.getId());
expect(eventTimelineSet.getLiveTimeline().getEvents()).not.toContain(roomFilteredEvent);
});
});
}); });

62
spec/unit/feature.spec.ts Normal file
View 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);
});
});

View File

@@ -1,3 +1,4 @@
import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync";
import { Filter, IFilterDefinition } from "../../src/filter"; import { Filter, IFilterDefinition } from "../../src/filter";
describe("Filter", function() { describe("Filter", function() {
@@ -43,4 +44,17 @@ describe("Filter", function() {
expect(filter.getDefinition()).toEqual(definition); expect(filter.getDefinition()).toEqual(definition);
}); });
}); });
describe("setUnreadThreadNotifications", function() {
it("setUnreadThreadNotifications", function() {
filter.setUnreadThreadNotifications(true);
expect(filter.getDefinition()).toEqual({
room: {
timeline: {
[UNREAD_THREAD_NOTIFICATIONS.name]: true,
},
},
});
});
});
}); });

View 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,
);
});
});
});

View File

@@ -1,3 +1,4 @@
import { SSOAction } from '../../src/@types/auth';
import { TestClient } from '../TestClient'; import { TestClient } from '../TestClient';
describe('Login request', function() { describe('Login request', function() {
@@ -22,3 +23,37 @@ describe('Login request', function() {
expect(client.client.getUserId()).toBe(response.user_id); expect(client.client.getUserId()).toBe(response.user_id);
}); });
}); });
describe('SSO login URL', function() {
let client: TestClient;
beforeEach(function() {
client = new TestClient();
});
afterEach(function() {
client.stop();
});
describe('SSOAction', function() {
const redirectUri = "https://test.com/foo";
it('No action', function() {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, undefined);
const url = new URL(urlString);
expect(url.searchParams.has('org.matrix.msc3824.action')).toBe(false);
});
it('register', function() {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.REGISTER);
const url = new URL(urlString);
expect(url.searchParams.get('org.matrix.msc3824.action')).toEqual('register');
});
it('login', function() {
const urlString = client.client.getSsoLoginUrl(redirectUri, undefined, undefined, SSOAction.LOGIN);
const url = new URL(urlString);
expect(url.searchParams.get('org.matrix.msc3824.action')).toEqual('login');
});
});
});

View File

@@ -36,9 +36,14 @@ import { ReceiptType } from "../../src/@types/read_receipts";
import * as testUtils from "../test-utils/test-utils"; import * as testUtils from "../test-utils/test-utils";
import { makeBeaconInfoContent } from "../../src/content-helpers"; import { makeBeaconInfoContent } from "../../src/content-helpers";
import { M_BEACON_INFO } from "../../src/@types/beacon"; import { M_BEACON_INFO } from "../../src/@types/beacon";
import { ContentHelpers, Room } from "../../src"; import { ContentHelpers, EventTimeline, Room } from "../../src";
import { supportsMatrixCall } from "../../src/webrtc/call"; import { supportsMatrixCall } from "../../src/webrtc/call";
import { makeBeaconEvent } from "../test-utils/beacon"; import { makeBeaconEvent } from "../test-utils/beacon";
import {
IGNORE_INVITES_ACCOUNT_EVENT_KEY,
POLICIES_ACCOUNT_EVENT_TYPE,
PolicyScope,
} from "../../src/models/invites-ignorer";
jest.useFakeTimers(); jest.useFakeTimers();
@@ -427,7 +432,7 @@ describe("MatrixClient", function() {
} }
}); });
}); });
await client.startClient(); await client.startClient({ filter });
await syncPromise; await syncPromise;
}); });
@@ -1412,4 +1417,301 @@ describe("MatrixClient", function() {
expect(client.crypto.encryptAndSendToDevices).toHaveBeenLastCalledWith(deviceInfos, payload); expect(client.crypto.encryptAndSendToDevices).toHaveBeenLastCalledWith(deviceInfos, payload);
}); });
}); });
describe("support for ignoring invites", () => {
beforeEach(() => {
// Mockup `getAccountData`/`setAccountData`.
const dataStore = new Map();
client.setAccountData = function(eventType, content) {
dataStore.set(eventType, content);
return Promise.resolve();
};
client.getAccountData = function(eventType) {
const data = dataStore.get(eventType);
return new MatrixEvent({
content: data,
});
};
// Mockup `createRoom`/`getRoom`/`joinRoom`, including state.
const rooms = new Map();
client.createRoom = function(options = {}) {
const roomId = options["_roomId"] || `!room-${rooms.size}:example.org`;
const state = new Map();
const room = {
roomId,
_options: options,
_state: state,
getUnfilteredTimelineSet: function() {
return {
getLiveTimeline: function() {
return {
getState: function(direction) {
expect(direction).toBe(EventTimeline.FORWARDS);
return {
getStateEvents: function(type) {
const store = state.get(type) || {};
return Object.keys(store).map(key => store[key]);
},
};
},
};
},
};
},
};
rooms.set(roomId, room);
return Promise.resolve({ room_id: roomId });
};
client.getRoom = function(roomId) {
return rooms.get(roomId);
};
client.joinRoom = function(roomId) {
return this.getRoom(roomId) || this.createRoom({ _roomId: roomId });
};
// Mockup state events
client.sendStateEvent = function(roomId, type, content) {
const room = this.getRoom(roomId);
const state: Map<string, any> = room._state;
let store = state.get(type);
if (!store) {
store = {};
state.set(type, store);
}
const eventId = `$event-${Math.random()}:example.org`;
store[eventId] = {
getId: function() {
return eventId;
},
getRoomId: function() {
return roomId;
},
getContent: function() {
return content;
},
};
return { event_id: eventId };
};
client.redactEvent = function(roomId, eventId) {
const room = this.getRoom(roomId);
const state: Map<string, any> = room._state;
for (const store of state.values()) {
delete store[eventId];
}
};
});
it("should initialize and return the same `target` consistently", async () => {
const target1 = await client.ignoredInvites.getOrCreateTargetRoom();
const target2 = await client.ignoredInvites.getOrCreateTargetRoom();
expect(target1).toBeTruthy();
expect(target1).toBe(target2);
});
it("should initialize and return the same `sources` consistently", async () => {
const sources1 = await client.ignoredInvites.getOrCreateSourceRooms();
const sources2 = await client.ignoredInvites.getOrCreateSourceRooms();
expect(sources1).toBeTruthy();
expect(sources1).toHaveLength(1);
expect(sources1).toEqual(sources2);
});
it("should initially not reject any invite", async () => {
const rule = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(rule).toBeFalsy();
});
it("should reject invites once we have added a matching rule in the target room (scope: user)", async () => {
await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
// We should reject this invite.
const ruleMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch).toBeTruthy();
expect(ruleMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: "just a test",
});
// We should let these invites go through.
const ruleWrongServer = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleWrongServer).toBeFalsy();
const ruleWrongServerRoom = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:example.org",
});
expect(ruleWrongServerRoom).toBeFalsy();
});
it("should reject invites once we have added a matching rule in the target room (scope: server)", async () => {
const REASON = `Just a test ${Math.random()}`;
await client.ignoredInvites.addRule(PolicyScope.Server, "example.org", REASON);
// We should reject these invites.
const ruleSenderMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleSenderMatch).toBeTruthy();
expect(ruleSenderMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: REASON,
});
const ruleRoomMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:example.org",
});
expect(ruleRoomMatch).toBeTruthy();
expect(ruleRoomMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: REASON,
});
// We should let these invites go through.
const ruleWrongServer = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleWrongServer).toBeFalsy();
});
it("should reject invites once we have added a matching rule in the target room (scope: room)", async () => {
const REASON = `Just a test ${Math.random()}`;
const BAD_ROOM_ID = "!bad:example.org";
const GOOD_ROOM_ID = "!good:example.org";
await client.ignoredInvites.addRule(PolicyScope.Room, BAD_ROOM_ID, REASON);
// We should reject this invite.
const ruleSenderMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: BAD_ROOM_ID,
});
expect(ruleSenderMatch).toBeTruthy();
expect(ruleSenderMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: REASON,
});
// We should let these invites go through.
const ruleWrongRoom = await client.ignoredInvites.getRuleForInvite({
sender: BAD_ROOM_ID,
roomId: GOOD_ROOM_ID,
});
expect(ruleWrongRoom).toBeFalsy();
});
it("should reject invites once we have added a matching rule in a non-target source room", async () => {
const NEW_SOURCE_ROOM_ID = "!another-source:example.org";
// Make sure that everything is initialized.
await client.ignoredInvites.getOrCreateSourceRooms();
await client.joinRoom(NEW_SOURCE_ROOM_ID);
await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
// Add a rule in the new source room.
await client.sendStateEvent(NEW_SOURCE_ROOM_ID, PolicyScope.User, {
entity: "*:example.org",
reason: "just a test",
recommendation: "m.ban",
});
// We should reject this invite.
const ruleMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch).toBeTruthy();
expect(ruleMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: "just a test",
});
// We should let these invites go through.
const ruleWrongServer = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleWrongServer).toBeFalsy();
const ruleWrongServerRoom = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:example.org",
});
expect(ruleWrongServerRoom).toBeFalsy();
});
it("should not reject invites anymore once we have removed a rule", async () => {
await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
// We should reject this invite.
const ruleMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch).toBeTruthy();
expect(ruleMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: "just a test",
});
// After removing the invite, we shouldn't reject it anymore.
await client.ignoredInvites.removeRule(ruleMatch);
const ruleMatch2 = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch2).toBeFalsy();
});
it("should add new rules in the target room, rather than any other source room", async () => {
const NEW_SOURCE_ROOM_ID = "!another-source:example.org";
// Make sure that everything is initialized.
await client.ignoredInvites.getOrCreateSourceRooms();
await client.joinRoom(NEW_SOURCE_ROOM_ID);
const newSourceRoom = client.getRoom(NEW_SOURCE_ROOM_ID);
// Fetch the list of sources and check that we do not have the new room yet.
const policies = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent();
expect(policies).toBeTruthy();
const ignoreInvites = policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name];
expect(ignoreInvites).toBeTruthy();
expect(ignoreInvites.sources).toBeTruthy();
expect(ignoreInvites.sources).not.toContain(NEW_SOURCE_ROOM_ID);
// Add a source.
const added = await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
expect(added).toBe(true);
const added2 = await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
expect(added2).toBe(false);
// Fetch the list of sources and check that we have added the new room.
const policies2 = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent();
expect(policies2).toBeTruthy();
const ignoreInvites2 = policies2[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name];
expect(ignoreInvites2).toBeTruthy();
expect(ignoreInvites2.sources).toBeTruthy();
expect(ignoreInvites2.sources).toContain(NEW_SOURCE_ROOM_ID);
// Add a rule.
const eventId = await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
// Check where it shows up.
const targetRoomId = ignoreInvites2.target;
const targetRoom = client.getRoom(targetRoomId);
expect(targetRoom._state.get(PolicyScope.User)[eventId]).toBeTruthy();
expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy();
});
});
}); });

View File

@@ -565,7 +565,7 @@ describe("MSC3089TreeSpace", () => {
rooms = {}; rooms = {};
rooms[tree.roomId] = parentRoom; rooms[tree.roomId] = parentRoom;
(<any>tree).room = parentRoom; // override readonly (<any>tree).room = parentRoom; // override readonly
client.getRoom = (r) => rooms[r]; client.getRoom = (r) => rooms[r ?? ""];
clientSendStateFn = jest.fn() clientSendStateFn = jest.fn()
.mockImplementation((roomId: string, eventType: EventType, content: any, stateKey: string) => { .mockImplementation((roomId: string, eventType: EventType, content: any, stateKey: string) => {

View File

@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { REFERENCE_RELATION } from "matrix-events-sdk";
import { MatrixEvent } from "../../../src"; import { MatrixEvent } from "../../../src";
import { M_BEACON_INFO } from "../../../src/@types/beacon"; import { M_BEACON_INFO } from "../../../src/@types/beacon";
import { import {
@@ -431,6 +433,27 @@ describe('Beacon', () => {
expect(emitSpy).not.toHaveBeenCalled(); expect(emitSpy).not.toHaveBeenCalled();
}); });
it("should ignore invalid beacon events", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true, timeout: 60000 }));
const emitSpy = jest.spyOn(beacon, 'emit');
const ev = new MatrixEvent({
type: M_BEACON_INFO.name,
sender: userId,
room_id: roomId,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: beacon.beaconInfoId,
},
},
});
beacon.addLocations([ev]);
expect(beacon.latestLocationEvent).toBeFalsy();
expect(emitSpy).not.toHaveBeenCalled();
});
describe('when beacon is live with a start timestamp is in the future', () => { describe('when beacon is live with a start timestamp is in the future', () => {
it('ignores locations before the beacon start timestamp', () => { it('ignores locations before the beacon start timestamp', () => {
const startTimestamp = now + 60000; const startTimestamp = now + 60000;

View 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
View 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);
});
});
});

View File

@@ -1,6 +1,6 @@
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
import { PushProcessor } from "../../src/pushprocessor"; import { IActionsObject, PushProcessor } from "../../src/pushprocessor";
import { EventType, MatrixClient, MatrixEvent } from "../../src"; import { EventType, IContent, MatrixClient, MatrixEvent } from "../../src";
describe('NotificationService', function() { describe('NotificationService', function() {
const testUserId = "@ali:matrix.org"; const testUserId = "@ali:matrix.org";
@@ -336,4 +336,102 @@ describe('NotificationService', function() {
enabled: true, enabled: true,
}, testEvent)).toBe(true); }, testEvent)).toBe(true);
}); });
describe("performCustomEventHandling()", () => {
const getActionsForEvent = (prevContent: IContent, content: IContent): IActionsObject => {
testEvent = utils.mkEvent({
type: "org.matrix.msc3401.call",
room: testRoomId,
user: "@alice:foo",
skey: "state_key",
event: true,
content: content,
prev_content: prevContent,
});
return pushProcessor.actionsForEvent(testEvent);
};
const assertDoesNotify = (actions: IActionsObject): void => {
expect(actions.notify).toBeTruthy();
expect(actions.tweaks.sound).toBeTruthy();
expect(actions.tweaks.highlight).toBeFalsy();
};
const assertDoesNotNotify = (actions: IActionsObject): void => {
expect(actions.notify).toBeFalsy();
expect(actions.tweaks.sound).toBeFalsy();
expect(actions.tweaks.highlight).toBeFalsy();
};
it.each(
["m.ring", "m.prompt"],
)("should notify when new group call event appears with %s intent", (intent: string) => {
assertDoesNotify(getActionsForEvent({}, {
"m.intent": intent,
"m.type": "m.voice",
"m.name": "Call",
}));
});
it("should notify when a call is un-terminated", () => {
assertDoesNotify(getActionsForEvent({
"m.intent": "m.ring",
"m.type": "m.voice",
"m.name": "Call",
"m.terminated": "All users left",
}, {
"m.intent": "m.ring",
"m.type": "m.voice",
"m.name": "Call",
}));
});
it("should not notify when call is terminated", () => {
assertDoesNotNotify(getActionsForEvent({
"m.intent": "m.ring",
"m.type": "m.voice",
"m.name": "Call",
}, {
"m.intent": "m.ring",
"m.type": "m.voice",
"m.name": "Call",
"m.terminated": "All users left",
}));
});
it("should ignore with m.room intent", () => {
assertDoesNotNotify(getActionsForEvent({}, {
"m.intent": "m.room",
"m.type": "m.voice",
"m.name": "Call",
}));
});
describe("ignoring non-relevant state changes", () => {
it("should ignore intent changes", () => {
assertDoesNotNotify(getActionsForEvent({
"m.intent": "m.ring",
"m.type": "m.voice",
"m.name": "Call",
}, {
"m.intent": "m.ring",
"m.type": "m.video",
"m.name": "Call",
}));
});
it("should ignore name changes", () => {
assertDoesNotNotify(getActionsForEvent({
"m.intent": "m.ring",
"m.type": "m.voice",
"m.name": "Call",
}, {
"m.intent": "m.ring",
"m.type": "m.voice",
"m.name": "New call",
}));
});
});
});
}); });

View 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();
});
});
});

View File

@@ -1,12 +1,29 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
import { RoomMember } from "../../src/models/room-member"; import { RoomMember, RoomMemberEvent } from "../../src/models/room-member";
import { RoomState } from "../../src";
describe("RoomMember", function() { describe("RoomMember", function() {
const roomId = "!foo:bar"; const roomId = "!foo:bar";
const userA = "@alice:bar"; const userA = "@alice:bar";
const userB = "@bertha:bar"; const userB = "@bertha:bar";
const userC = "@clarissa:bar"; const userC = "@clarissa:bar";
let member; let member = new RoomMember(roomId, userA);
beforeEach(function() { beforeEach(function() {
member = new RoomMember(roomId, userA); member = new RoomMember(roomId, userA);
@@ -27,15 +44,15 @@ describe("RoomMember", function() {
avatar_url: "mxc://flibble/wibble", avatar_url: "mxc://flibble/wibble",
}, },
}); });
const url = member.getAvatarUrl(hsUrl); const url = member.getAvatarUrl(hsUrl, 1, 1, '', false, false);
// we don't care about how the mxc->http conversion is done, other // we don't care about how the mxc->http conversion is done, other
// than it contains the mxc body. // than it contains the mxc body.
expect(url.indexOf("flibble/wibble")).not.toEqual(-1); expect(url?.indexOf("flibble/wibble")).not.toEqual(-1);
}); });
it("should return nothing if there is no m.room.member and allowDefault=false", it("should return nothing if there is no m.room.member and allowDefault=false",
function() { function() {
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false); const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false, false);
expect(url).toEqual(null); expect(url).toEqual(null);
}); });
}); });
@@ -82,7 +99,7 @@ describe("RoomMember", function() {
}); });
let emitCount = 0; let emitCount = 0;
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) { member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
emitCount += 1; emitCount += 1;
expect(emitMember).toEqual(member); expect(emitMember).toEqual(member);
expect(emitEvent).toEqual(event); expect(emitEvent).toEqual(event);
@@ -113,7 +130,7 @@ describe("RoomMember", function() {
// set the power level to something other than zero or we // set the power level to something other than zero or we
// won't get an event // won't get an event
member.powerLevel = 1; member.powerLevel = 1;
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) { member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
emitCount += 1; emitCount += 1;
expect(emitMember.userId).toEqual('@alice:bar'); expect(emitMember.userId).toEqual('@alice:bar');
expect(emitMember.powerLevel).toEqual(0); expect(emitMember.powerLevel).toEqual(0);
@@ -141,7 +158,7 @@ describe("RoomMember", function() {
}); });
let emitCount = 0; let emitCount = 0;
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) { member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
emitCount += 1; emitCount += 1;
expect(emitMember.userId).toEqual('@alice:bar'); expect(emitMember.userId).toEqual('@alice:bar');
expect(emitMember.powerLevel).toEqual(20); expect(emitMember.powerLevel).toEqual(20);
@@ -195,7 +212,7 @@ describe("RoomMember", function() {
event: true, event: true,
}); });
let emitCount = 0; let emitCount = 0;
member.on("RoomMember.typing", function(ev, mem) { member.on(RoomMemberEvent.Typing, function(ev, mem) {
expect(mem).toEqual(member); expect(mem).toEqual(member);
expect(ev).toEqual(event); expect(ev).toEqual(event);
emitCount += 1; emitCount += 1;
@@ -210,7 +227,7 @@ describe("RoomMember", function() {
describe("isOutOfBand", function() { describe("isOutOfBand", function() {
it("should be set by markOutOfBand", function() { it("should be set by markOutOfBand", function() {
const member = new RoomMember(); const member = new RoomMember(roomId, userA);
expect(member.isOutOfBand()).toEqual(false); expect(member.isOutOfBand()).toEqual(false);
member.markOutOfBand(); member.markOutOfBand();
expect(member.isOutOfBand()).toEqual(true); expect(member.isOutOfBand()).toEqual(true);
@@ -266,7 +283,7 @@ describe("RoomMember", function() {
getUserIdsWithDisplayName: function(displayName) { getUserIdsWithDisplayName: function(displayName) {
return [userA, userC]; return [userA, userC];
}, },
}; } as unknown as RoomState;
expect(member.name).toEqual(userA); // default = user_id expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent); member.setMembershipEvent(joinEvent);
expect(member.name).toEqual("Alice"); // prefer displayname expect(member.name).toEqual("Alice"); // prefer displayname
@@ -278,7 +295,7 @@ describe("RoomMember", function() {
it("should emit 'RoomMember.membership' if the membership changes", function() { it("should emit 'RoomMember.membership' if the membership changes", function() {
let emitCount = 0; let emitCount = 0;
member.on("RoomMember.membership", function(ev, mem) { member.on(RoomMemberEvent.Membership, function(ev, mem) {
emitCount += 1; emitCount += 1;
expect(mem).toEqual(member); expect(mem).toEqual(member);
expect(ev).toEqual(inviteEvent); expect(ev).toEqual(inviteEvent);
@@ -291,7 +308,7 @@ describe("RoomMember", function() {
it("should emit 'RoomMember.name' if the name changes", function() { it("should emit 'RoomMember.name' if the name changes", function() {
let emitCount = 0; let emitCount = 0;
member.on("RoomMember.name", function(ev, mem) { member.on(RoomMemberEvent.Name, function(ev, mem) {
emitCount += 1; emitCount += 1;
expect(mem).toEqual(member); expect(mem).toEqual(member);
expect(ev).toEqual(joinEvent); expect(ev).toEqual(joinEvent);
@@ -341,7 +358,7 @@ describe("RoomMember", function() {
getUserIdsWithDisplayName: function(displayName) { getUserIdsWithDisplayName: function(displayName) {
return [userA, userC]; return [userA, userC];
}, },
}; } as unknown as RoomState;
expect(member.name).toEqual(userA); // default = user_id expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent, roomState); member.setMembershipEvent(joinEvent, roomState);
expect(member.name).not.toEqual("Alíce"); // it should disambig. expect(member.name).not.toEqual("Alíce"); // it should disambig.

View File

@@ -1,14 +1,37 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { MockedObject } from 'jest-mock';
import * as utils from "../test-utils/test-utils"; import * as utils from "../test-utils/test-utils";
import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon"; import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
import { filterEmitCallsByEventType } from "../test-utils/emitter"; import { filterEmitCallsByEventType } from "../test-utils/emitter";
import { RoomState, RoomStateEvent } from "../../src/models/room-state"; import { RoomState, RoomStateEvent } from "../../src/models/room-state";
import { BeaconEvent, getBeaconInfoIdentifier } from "../../src/models/beacon"; import {
Beacon,
BeaconEvent,
getBeaconInfoIdentifier,
} from "../../src/models/beacon";
import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@types/event"; import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@types/event";
import { import {
MatrixEvent, MatrixEvent,
MatrixEventEvent, MatrixEventEvent,
} from "../../src/models/event"; } from "../../src/models/event";
import { M_BEACON } from "../../src/@types/beacon"; import { M_BEACON } from "../../src/@types/beacon";
import { MatrixClient } from "../../src/client";
describe("RoomState", function() { describe("RoomState", function() {
const roomId = "!foo:bar"; const roomId = "!foo:bar";
@@ -17,7 +40,7 @@ describe("RoomState", function() {
const userC = "@cleo:bar"; const userC = "@cleo:bar";
const userLazy = "@lazy:bar"; const userLazy = "@lazy:bar";
let state; let state = new RoomState(roomId);
beforeEach(function() { beforeEach(function() {
state = new RoomState(roomId); state = new RoomState(roomId);
@@ -67,8 +90,8 @@ describe("RoomState", function() {
it("should return a member which changes as state changes", function() { it("should return a member which changes as state changes", function() {
const member = state.getMember(userB); const member = state.getMember(userB);
expect(member.membership).toEqual("join"); expect(member?.membership).toEqual("join");
expect(member.name).toEqual(userB); expect(member?.name).toEqual(userB);
state.setStateEvents([ state.setStateEvents([
utils.mkMembership({ utils.mkMembership({
@@ -77,14 +100,14 @@ describe("RoomState", function() {
}), }),
]); ]);
expect(member.membership).toEqual("leave"); expect(member?.membership).toEqual("leave");
expect(member.name).toEqual("BobGone"); expect(member?.name).toEqual("BobGone");
}); });
}); });
describe("getSentinelMember", function() { describe("getSentinelMember", function() {
it("should return a member with the user id as name", function() { it("should return a member with the user id as name", function() {
expect(state.getSentinelMember("@no-one:here").name).toEqual("@no-one:here"); expect(state.getSentinelMember("@no-one:here")?.name).toEqual("@no-one:here");
}); });
it("should return a member which doesn't change when the state is updated", it("should return a member which doesn't change when the state is updated",
@@ -98,11 +121,11 @@ describe("RoomState", function() {
]); ]);
const postLeaveUser = state.getSentinelMember(userA); const postLeaveUser = state.getSentinelMember(userA);
expect(preLeaveUser.membership).toEqual("join"); expect(preLeaveUser?.membership).toEqual("join");
expect(preLeaveUser.name).toEqual(userA); expect(preLeaveUser?.name).toEqual(userA);
expect(postLeaveUser.membership).toEqual("leave"); expect(postLeaveUser?.membership).toEqual("leave");
expect(postLeaveUser.name).toEqual("AliceIsGone"); expect(postLeaveUser?.name).toEqual("AliceIsGone");
}); });
}); });
@@ -122,8 +145,8 @@ describe("RoomState", function() {
const events = state.getStateEvents("m.room.member"); const events = state.getStateEvents("m.room.member");
expect(events.length).toEqual(2); expect(events.length).toEqual(2);
// ordering unimportant // ordering unimportant
expect([userA, userB].indexOf(events[0].getStateKey())).not.toEqual(-1); expect([userA, userB].indexOf(events[0].getStateKey() as string)).not.toEqual(-1);
expect([userA, userB].indexOf(events[1].getStateKey())).not.toEqual(-1); expect([userA, userB].indexOf(events[1].getStateKey() as string)).not.toEqual(-1);
}); });
it("should return a single MatrixEvent if a state_key was specified", it("should return a single MatrixEvent if a state_key was specified",
@@ -146,7 +169,7 @@ describe("RoomState", function() {
}), }),
]; ];
let emitCount = 0; let emitCount = 0;
state.on("RoomState.members", function(ev, st, mem) { state.on(RoomStateEvent.Members, function(ev, st, mem) {
expect(ev).toEqual(memberEvents[emitCount]); expect(ev).toEqual(memberEvents[emitCount]);
expect(st).toEqual(state); expect(st).toEqual(state);
expect(mem).toEqual(state.getMember(ev.getSender())); expect(mem).toEqual(state.getMember(ev.getSender()));
@@ -166,7 +189,7 @@ describe("RoomState", function() {
}), }),
]; ];
let emitCount = 0; let emitCount = 0;
state.on("RoomState.newMember", function(ev, st, mem) { state.on(RoomStateEvent.NewMember, function(ev, st, mem) {
expect(state.getMember(mem.userId)).toEqual(mem); expect(state.getMember(mem.userId)).toEqual(mem);
expect(mem.userId).toEqual(memberEvents[emitCount].getSender()); expect(mem.userId).toEqual(memberEvents[emitCount].getSender());
expect(mem.membership).toBeFalsy(); // not defined yet expect(mem.membership).toBeFalsy(); // not defined yet
@@ -192,7 +215,7 @@ describe("RoomState", function() {
}), }),
]; ];
let emitCount = 0; let emitCount = 0;
state.on("RoomState.events", function(ev, st) { state.on(RoomStateEvent.Events, function(ev, st) {
expect(ev).toEqual(events[emitCount]); expect(ev).toEqual(events[emitCount]);
expect(st).toEqual(state); expect(st).toEqual(state);
emitCount += 1; emitCount += 1;
@@ -272,7 +295,7 @@ describe("RoomState", function() {
}), }),
]; ];
let emitCount = 0; let emitCount = 0;
state.on("RoomState.Marker", function(markerEvent, markerFoundOptions) { state.on(RoomStateEvent.Marker, function(markerEvent, markerFoundOptions) {
expect(markerEvent).toEqual(events[emitCount]); expect(markerEvent).toEqual(events[emitCount]);
expect(markerFoundOptions).toEqual({ timelineWasEmpty: true }); expect(markerFoundOptions).toEqual({ timelineWasEmpty: true });
emitCount += 1; emitCount += 1;
@@ -296,7 +319,7 @@ describe("RoomState", function() {
it('does not add redacted beacon info events to state', () => { it('does not add redacted beacon info events to state', () => {
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId); const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
const redactionEvent = { event: { type: 'm.room.redaction' } }; const redactionEvent = new MatrixEvent({ type: 'm.room.redaction' });
redactedBeaconEvent.makeRedacted(redactionEvent); redactedBeaconEvent.makeRedacted(redactionEvent);
const emitSpy = jest.spyOn(state, 'emit'); const emitSpy = jest.spyOn(state, 'emit');
@@ -316,27 +339,27 @@ describe("RoomState", function() {
state.setStateEvents([beaconEvent]); state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent)); const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
expect(beaconInstance.isLive).toEqual(true); expect(beaconInstance?.isLive).toEqual(true);
state.setStateEvents([updatedBeaconEvent]); state.setStateEvents([updatedBeaconEvent]);
// same Beacon // same Beacon
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance); expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance);
// updated liveness // updated liveness
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent)).isLive).toEqual(false); expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))?.isLive).toEqual(false);
}); });
it('destroys and removes redacted beacon events', () => { it('destroys and removes redacted beacon events', () => {
const beaconId = '$beacon1'; const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId); const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId); const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactionEvent = { event: { type: 'm.room.redaction', redacts: beaconEvent.getId() } }; const redactionEvent = new MatrixEvent({ type: 'm.room.redaction', redacts: beaconEvent.getId() });
redactedBeaconEvent.makeRedacted(redactionEvent); redactedBeaconEvent.makeRedacted(redactionEvent);
state.setStateEvents([beaconEvent]); state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent)); const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
const destroySpy = jest.spyOn(beaconInstance, 'destroy'); const destroySpy = jest.spyOn(beaconInstance as Beacon, 'destroy');
expect(beaconInstance.isLive).toEqual(true); expect(beaconInstance?.isLive).toEqual(true);
state.setStateEvents([redactedBeaconEvent]); state.setStateEvents([redactedBeaconEvent]);
@@ -357,7 +380,7 @@ describe("RoomState", function() {
// live beacon is now not live // live beacon is now not live
const updatedLiveBeaconEvent = makeBeaconInfoEvent( const updatedLiveBeaconEvent = makeBeaconInfoEvent(
userA, roomId, { isLive: false }, liveBeaconEvent.getId(), '$beacon1', userA, roomId, { isLive: false }, liveBeaconEvent.getId(),
); );
state.setStateEvents([updatedLiveBeaconEvent]); state.setStateEvents([updatedLiveBeaconEvent]);
@@ -377,8 +400,8 @@ describe("RoomState", function() {
state.markOutOfBandMembersStarted(); state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]); state.setOutOfBandMembers([oobMemberEvent]);
const member = state.getMember(userLazy); const member = state.getMember(userLazy);
expect(member.userId).toEqual(userLazy); expect(member?.userId).toEqual(userLazy);
expect(member.isOutOfBand()).toEqual(true); expect(member?.isOutOfBand()).toEqual(true);
}); });
it("should have no effect when not in correct status", function() { it("should have no effect when not in correct status", function() {
@@ -394,7 +417,7 @@ describe("RoomState", function() {
user: userLazy, mship: "join", room: roomId, event: true, user: userLazy, mship: "join", room: roomId, event: true,
}); });
let eventReceived = false; let eventReceived = false;
state.once('RoomState.newMember', (_, __, member) => { state.once(RoomStateEvent.NewMember, (_event, _state, member) => {
expect(member.userId).toEqual(userLazy); expect(member.userId).toEqual(userLazy);
eventReceived = true; eventReceived = true;
}); });
@@ -410,8 +433,8 @@ describe("RoomState", function() {
state.markOutOfBandMembersStarted(); state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]); state.setOutOfBandMembers([oobMemberEvent]);
const memberA = state.getMember(userA); const memberA = state.getMember(userA);
expect(memberA.events.member.getId()).not.toEqual(oobMemberEvent.getId()); expect(memberA?.events?.member?.getId()).not.toEqual(oobMemberEvent.getId());
expect(memberA.isOutOfBand()).toEqual(false); expect(memberA?.isOutOfBand()).toEqual(false);
}); });
it("should emit members when updating a member", function() { it("should emit members when updating a member", function() {
@@ -420,7 +443,7 @@ describe("RoomState", function() {
user: doesntExistYetUserId, mship: "join", room: roomId, event: true, user: doesntExistYetUserId, mship: "join", room: roomId, event: true,
}); });
let eventReceived = false; let eventReceived = false;
state.once('RoomState.members', (_, __, member) => { state.once(RoomStateEvent.Members, (_event, _state, member) => {
expect(member.userId).toEqual(doesntExistYetUserId); expect(member.userId).toEqual(doesntExistYetUserId);
eventReceived = true; eventReceived = true;
}); });
@@ -443,8 +466,8 @@ describe("RoomState", function() {
[userA, userB, userLazy].forEach((userId) => { [userA, userB, userLazy].forEach((userId) => {
const member = state.getMember(userId); const member = state.getMember(userId);
const memberCopy = copy.getMember(userId); const memberCopy = copy.getMember(userId);
expect(member.name).toEqual(memberCopy.name); expect(member?.name).toEqual(memberCopy?.name);
expect(member.isOutOfBand()).toEqual(memberCopy.isOutOfBand()); expect(member?.isOutOfBand()).toEqual(memberCopy?.isOutOfBand());
}); });
// check member keys // check member keys
expect(Object.keys(state.members)).toEqual(Object.keys(copy.members)); expect(Object.keys(state.members)).toEqual(Object.keys(copy.members));
@@ -503,19 +526,20 @@ describe("RoomState", function() {
it("should say members with power >=50 may send state with power level event " + it("should say members with power >=50 may send state with power level event " +
"but no state default", "but no state default",
function() { function() {
const powerLevelEvent = { const powerLevelEvent = new MatrixEvent({
type: "m.room.power_levels", room: roomId, user: userA, event: true, type: "m.room.power_levels", room_id: roomId, sender: userA,
state_key: "",
content: { content: {
users_default: 10, users_default: 10,
// state_default: 50, "intentionally left blank" // state_default: 50, "intentionally left blank"
events_default: 25, events_default: 25,
users: { users: {
[userA]: 50,
}, },
}, },
}; });
powerLevelEvent.content.users[userA] = 50;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]); state.setStateEvents([powerLevelEvent]);
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true); expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false); expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
@@ -523,20 +547,21 @@ describe("RoomState", function() {
it("should obey state_default", it("should obey state_default",
function() { function() {
const powerLevelEvent = { const powerLevelEvent = new MatrixEvent({
type: "m.room.power_levels", room: roomId, user: userA, event: true, type: "m.room.power_levels", room_id: roomId, sender: userA,
state_key: "",
content: { content: {
users_default: 10, users_default: 10,
state_default: 30, state_default: 30,
events_default: 25, events_default: 25,
users: { users: {
[userA]: 30,
[userB]: 29,
}, },
}, },
}; });
powerLevelEvent.content.users[userA] = 30;
powerLevelEvent.content.users[userB] = 29;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]); state.setStateEvents([powerLevelEvent]);
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true); expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false); expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
@@ -544,9 +569,9 @@ describe("RoomState", function() {
it("should honour explicit event power levels in the power_levels event", it("should honour explicit event power levels in the power_levels event",
function() { function() {
const powerLevelEvent = { const powerLevelEvent = new MatrixEvent({
type: "m.room.power_levels", room: roomId, user: userA, event: true, type: "m.room.power_levels", room_id: roomId, sender: userA,
content: { state_key: "", content: {
events: { events: {
"m.room.other_thing": 76, "m.room.other_thing": 76,
}, },
@@ -554,13 +579,13 @@ describe("RoomState", function() {
state_default: 50, state_default: 50,
events_default: 25, events_default: 25,
users: { users: {
[userA]: 80,
[userB]: 50,
}, },
}, },
}; });
powerLevelEvent.content.users[userA] = 80;
powerLevelEvent.content.users[userB] = 50;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]); state.setStateEvents([powerLevelEvent]);
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true); expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(true); expect(state.maySendStateEvent('m.room.name', userB)).toEqual(true);
@@ -689,20 +714,21 @@ describe("RoomState", function() {
it("should obey events_default", it("should obey events_default",
function() { function() {
const powerLevelEvent = { const powerLevelEvent = new MatrixEvent({
type: "m.room.power_levels", room: roomId, user: userA, event: true, type: "m.room.power_levels", room_id: roomId, sender: userA,
state_key: "",
content: { content: {
users_default: 10, users_default: 10,
state_default: 30, state_default: 30,
events_default: 25, events_default: 25,
users: { users: {
[userA]: 26,
[userB]: 24,
}, },
}, },
}; });
powerLevelEvent.content.users[userA] = 26;
powerLevelEvent.content.users[userB] = 24;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]); state.setStateEvents([powerLevelEvent]);
expect(state.maySendEvent('m.room.message', userA)).toEqual(true); expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendEvent('m.room.message', userB)).toEqual(false); expect(state.maySendEvent('m.room.message', userB)).toEqual(false);
@@ -713,8 +739,9 @@ describe("RoomState", function() {
it("should honour explicit event power levels in the power_levels event", it("should honour explicit event power levels in the power_levels event",
function() { function() {
const powerLevelEvent = { const powerLevelEvent = new MatrixEvent({
type: "m.room.power_levels", room: roomId, user: userA, event: true, type: "m.room.power_levels", room_id: roomId, sender: userA,
state_key: "",
content: { content: {
events: { events: {
"m.room.other_thing": 33, "m.room.other_thing": 33,
@@ -723,13 +750,13 @@ describe("RoomState", function() {
state_default: 50, state_default: 50,
events_default: 25, events_default: 25,
users: { users: {
[userA]: 40,
[userB]: 30,
}, },
}, },
}; });
powerLevelEvent.content.users[userA] = 40;
powerLevelEvent.content.users[userB] = 30;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]); state.setStateEvents([powerLevelEvent]);
expect(state.maySendEvent('m.room.message', userA)).toEqual(true); expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendEvent('m.room.message', userB)).toEqual(true); expect(state.maySendEvent('m.room.message', userB)).toEqual(true);
@@ -743,10 +770,10 @@ describe("RoomState", function() {
}); });
describe('processBeaconEvents', () => { describe('processBeaconEvents', () => {
const beacon1 = makeBeaconInfoEvent(userA, roomId, {}, '$beacon1', '$beacon1'); const beacon1 = makeBeaconInfoEvent(userA, roomId, {}, '$beacon1');
const beacon2 = makeBeaconInfoEvent(userB, roomId, {}, '$beacon2', '$beacon2'); const beacon2 = makeBeaconInfoEvent(userB, roomId, {}, '$beacon2');
const mockClient = { decryptEventIfNeeded: jest.fn() }; const mockClient = { decryptEventIfNeeded: jest.fn() } as unknown as MockedObject<MatrixClient>;
beforeEach(() => { beforeEach(() => {
mockClient.decryptEventIfNeeded.mockClear(); mockClient.decryptEventIfNeeded.mockClear();
@@ -816,11 +843,11 @@ describe("RoomState", function() {
beaconInfoId: 'some-other-beacon', beaconInfoId: 'some-other-beacon',
}); });
state.setStateEvents([beacon1, beacon2], mockClient); state.setStateEvents([beacon1, beacon2]);
expect(state.beacons.size).toEqual(2); expect(state.beacons.size).toEqual(2);
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1)); const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
const addLocationsSpy = jest.spyOn(beaconInstance, 'addLocations'); const addLocationsSpy = jest.spyOn(beaconInstance, 'addLocations');
state.processBeaconEvents([location1, location2, location3], mockClient); state.processBeaconEvents([location1, location2, location3], mockClient);
@@ -885,7 +912,7 @@ describe("RoomState", function() {
}); });
state.setStateEvents([beacon1, beacon2]); state.setStateEvents([beacon1, beacon2]);
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)); const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear(); const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
state.processBeaconEvents([location, otherRelatedEvent], mockClient); state.processBeaconEvents([location, otherRelatedEvent], mockClient);
expect(addLocationsSpy).not.toHaveBeenCalled(); expect(addLocationsSpy).not.toHaveBeenCalled();
@@ -945,13 +972,13 @@ describe("RoomState", function() {
}); });
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true); jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
state.setStateEvents([beacon1, beacon2]); state.setStateEvents([beacon1, beacon2]);
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)); const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear(); const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
state.processBeaconEvents([decryptingRelatedEvent], mockClient); state.processBeaconEvents([decryptingRelatedEvent], mockClient);
// this event is a message after decryption // this event is a message after decryption
decryptingRelatedEvent.type = EventType.RoomMessage; decryptingRelatedEvent.event.type = EventType.RoomMessage;
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted); decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted, decryptingRelatedEvent);
expect(addLocationsSpy).not.toHaveBeenCalled(); expect(addLocationsSpy).not.toHaveBeenCalled();
}); });
@@ -967,14 +994,14 @@ describe("RoomState", function() {
}); });
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true); jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
state.setStateEvents([beacon1, beacon2]); state.setStateEvents([beacon1, beacon2]);
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)); const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear(); const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
state.processBeaconEvents([decryptingRelatedEvent], mockClient); state.processBeaconEvents([decryptingRelatedEvent], mockClient);
// update type after '''decryption''' // update type after '''decryption'''
decryptingRelatedEvent.event.type = M_BEACON.name; decryptingRelatedEvent.event.type = M_BEACON.name;
decryptingRelatedEvent.event.content = locationEvent.content; decryptingRelatedEvent.event.content = locationEvent.event.content;
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted); decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted, decryptingRelatedEvent);
expect(addLocationsSpy).toHaveBeenCalledWith([decryptingRelatedEvent]); expect(addLocationsSpy).toHaveBeenCalledWith([decryptingRelatedEvent]);
}); });

View File

@@ -32,13 +32,14 @@ import {
RoomEvent, RoomEvent,
} from "../../src"; } from "../../src";
import { EventTimeline } from "../../src/models/event-timeline"; import { EventTimeline } from "../../src/models/event-timeline";
import { IWrappedReceipt, Room } from "../../src/models/room"; import { NotificationCountType, Room } from "../../src/models/room";
import { RoomState } from "../../src/models/room-state"; import { RoomState } from "../../src/models/room-state";
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event"; import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
import { TestClient } from "../TestClient"; import { TestClient } from "../TestClient";
import { emitPromise } from "../test-utils/test-utils"; import { emitPromise } from "../test-utils/test-utils";
import { ReceiptType } from "../../src/@types/read_receipts"; import { ReceiptType } from "../../src/@types/read_receipts";
import { Thread, ThreadEvent } from "../../src/models/thread"; import { FeatureSupport, Thread, ThreadEvent } from "../../src/models/thread";
import { WrappedReceipt } from "../../src/models/read-receipt";
describe("Room", function() { describe("Room", function() {
const roomId = "!foo:bar"; const roomId = "!foo:bar";
@@ -288,11 +289,11 @@ describe("Room", function() {
room.addLiveEvents(events); room.addLiveEvents(events);
expect(room.currentState.setStateEvents).toHaveBeenCalledWith( expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
[events[0]], [events[0]],
{ timelineWasEmpty: undefined }, { timelineWasEmpty: false },
); );
expect(room.currentState.setStateEvents).toHaveBeenCalledWith( expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
[events[1]], [events[1]],
{ timelineWasEmpty: undefined }, { timelineWasEmpty: false },
); );
expect(events[0].forwardLooking).toBe(true); expect(events[0].forwardLooking).toBe(true);
expect(events[1].forwardLooking).toBe(true); expect(events[1].forwardLooking).toBe(true);
@@ -426,6 +427,17 @@ describe("Room", function() {
// but without the event ID matching we will still have the local event in pending events // but without the event ID matching we will still have the local event in pending events
expect(room.getEventForTxnId(txnId)).toBeUndefined(); expect(room.getEventForTxnId(txnId)).toBeUndefined();
}); });
it("should correctly handle remote echoes from other devices", () => {
const remoteEvent = utils.mkMessage({
room: roomId, user: userA, event: true,
});
remoteEvent.event.unsigned = { transaction_id: "TXN_ID" };
// add the remoteEvent
room.addLiveEvents([remoteEvent]);
expect(room.timeline.length).toEqual(1);
});
}); });
describe('addEphemeralEvents', () => { describe('addEphemeralEvents', () => {
@@ -1419,6 +1431,19 @@ describe("Room", function() {
expect(room.getUsersReadUpTo(eventToAck)).toEqual([userB]); expect(room.getUsersReadUpTo(eventToAck)).toEqual([userB]);
}); });
}); });
describe("hasUserReadUpTo", function() {
it("should acknowledge if an event has been read", function() {
const ts = 13787898424;
room.addReceipt(mkReceipt(roomId, [
mkRecord(eventToAck.getId(), "m.read", userB, ts),
]));
expect(room.hasUserReadEvent(userB, eventToAck.getId())).toEqual(true);
});
it("return false for an unknown event", function() {
expect(room.hasUserReadEvent(userB, "unknown_event")).toEqual(false);
});
});
}); });
describe("tags", function() { describe("tags", function() {
@@ -2383,7 +2408,7 @@ describe("Room", function() {
}); });
it("should aggregate relations in thread event timeline set", () => { it("should aggregate relations in thread event timeline set", () => {
Thread.setServerSideSupport(true, true); Thread.setServerSideSupport(FeatureSupport.Stable);
const threadRoot = mkMessage(); const threadRoot = mkMessage();
const rootReaction = mkReaction(threadRoot); const rootReaction = mkReaction(threadRoot);
const threadResponse = mkThreadResponse(threadRoot); const threadResponse = mkThreadResponse(threadRoot);
@@ -2428,8 +2453,8 @@ describe("Room", function() {
const room = new Room(roomId, client, userA); const room = new Room(roomId, client, userA);
it("handles missing receipt type", () => { it("handles missing receipt type", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType) => { room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
return receiptType === ReceiptType.ReadPrivate ? { eventId: "eventId" } as IWrappedReceipt : null; return receiptType === ReceiptType.ReadPrivate ? { eventId: "eventId" } as WrappedReceipt : null;
}; };
expect(room.getEventReadUpTo(userA)).toEqual("eventId"); expect(room.getEventReadUpTo(userA)).toEqual("eventId");
@@ -2437,19 +2462,17 @@ describe("Room", function() {
describe("prefers newer receipt", () => { describe("prefers newer receipt", () => {
it("should compare correctly using timelines", () => { it("should compare correctly using timelines", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType) => { room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.ReadPrivate) { if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1" } as IWrappedReceipt; return { eventId: "eventId1" } as WrappedReceipt;
}
if (receiptType === ReceiptType.UnstableReadPrivate) {
return { eventId: "eventId2" } as IWrappedReceipt;
} }
if (receiptType === ReceiptType.Read) { if (receiptType === ReceiptType.Read) {
return { eventId: "eventId3" } as IWrappedReceipt; return { eventId: "eventId2" } as WrappedReceipt;
} }
return null;
}; };
for (let i = 1; i <= 3; i++) { for (let i = 1; i <= 2; i++) {
room.getUnfilteredTimelineSet = () => ({ compareEventOrdering: (event1, event2) => { room.getUnfilteredTimelineSet = () => ({ compareEventOrdering: (event1, event2) => {
return (event1 === `eventId${i}`) ? 1 : -1; return (event1 === `eventId${i}`) ? 1 : -1;
} } as EventTimelineSet); } } as EventTimelineSet);
@@ -2460,20 +2483,18 @@ describe("Room", function() {
describe("correctly compares by timestamp", () => { describe("correctly compares by timestamp", () => {
it("should correctly compare, if we have all receipts", () => { it("should correctly compare, if we have all receipts", () => {
for (let i = 1; i <= 3; i++) { for (let i = 1; i <= 2; i++) {
room.getUnfilteredTimelineSet = () => ({ room.getUnfilteredTimelineSet = () => ({
compareEventOrdering: (_1, _2) => null, compareEventOrdering: (_1, _2) => null,
} as EventTimelineSet); } as EventTimelineSet);
room.getReadReceiptForUserId = (userId, ignore, receiptType) => { room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.ReadPrivate) { if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1", data: { ts: i === 1 ? 1 : 0 } } as IWrappedReceipt; return { eventId: "eventId1", data: { ts: i === 1 ? 2 : 1 } } as WrappedReceipt;
}
if (receiptType === ReceiptType.UnstableReadPrivate) {
return { eventId: "eventId2", data: { ts: i === 2 ? 1 : 0 } } as IWrappedReceipt;
} }
if (receiptType === ReceiptType.Read) { if (receiptType === ReceiptType.Read) {
return { eventId: "eventId3", data: { ts: i === 3 ? 1 : 0 } } as IWrappedReceipt; return { eventId: "eventId2", data: { ts: i === 2 ? 2 : 1 } } as WrappedReceipt;
} }
return null;
}; };
expect(room.getEventReadUpTo(userA)).toEqual(`eventId${i}`); expect(room.getEventReadUpTo(userA)).toEqual(`eventId${i}`);
@@ -2484,13 +2505,11 @@ describe("Room", function() {
room.getUnfilteredTimelineSet = () => ({ room.getUnfilteredTimelineSet = () => ({
compareEventOrdering: (_1, _2) => null, compareEventOrdering: (_1, _2) => null,
} as EventTimelineSet); } as EventTimelineSet);
room.getReadReceiptForUserId = (userId, ignore, receiptType) => { room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.UnstableReadPrivate) {
return { eventId: "eventId1", data: { ts: 0 } } as IWrappedReceipt;
}
if (receiptType === ReceiptType.Read) { if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2", data: { ts: 1 } } as IWrappedReceipt; return { eventId: "eventId2", data: { ts: 1 } } as WrappedReceipt;
} }
return null;
}; };
expect(room.getEventReadUpTo(userA)).toEqual(`eventId2`); expect(room.getEventReadUpTo(userA)).toEqual(`eventId2`);
@@ -2505,39 +2524,25 @@ describe("Room", function() {
}); });
it("should give precedence to m.read.private", () => { it("should give precedence to m.read.private", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType) => { room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.ReadPrivate) { if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1" } as IWrappedReceipt; return { eventId: "eventId1" } as WrappedReceipt;
}
if (receiptType === ReceiptType.UnstableReadPrivate) {
return { eventId: "eventId2" } as IWrappedReceipt;
} }
if (receiptType === ReceiptType.Read) { if (receiptType === ReceiptType.Read) {
return { eventId: "eventId3" } as IWrappedReceipt; return { eventId: "eventId2" } as WrappedReceipt;
} }
return null;
}; };
expect(room.getEventReadUpTo(userA)).toEqual(`eventId1`); expect(room.getEventReadUpTo(userA)).toEqual(`eventId1`);
}); });
it("should give precedence to org.matrix.msc2285.read.private", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType) => {
if (receiptType === ReceiptType.UnstableReadPrivate) {
return { eventId: "eventId2" } as IWrappedReceipt;
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2" } as IWrappedReceipt;
}
};
expect(room.getEventReadUpTo(userA)).toEqual(`eventId2`);
});
it("should give precedence to m.read", () => { it("should give precedence to m.read", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType) => { room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.Read) { if (receiptType === ReceiptType.Read) {
return { eventId: "eventId3" } as IWrappedReceipt; return { eventId: "eventId3" } as WrappedReceipt;
} }
return null;
}; };
expect(room.getEventReadUpTo(userA)).toEqual(`eventId3`); expect(room.getEventReadUpTo(userA)).toEqual(`eventId3`);
@@ -2557,4 +2562,40 @@ describe("Room", function() {
expect(client.roomNameGenerator).toHaveBeenCalled(); expect(client.roomNameGenerator).toHaveBeenCalled();
}); });
}); });
describe("thread notifications", () => {
let room;
beforeEach(() => {
const client = new TestClient(userA).client;
room = new Room(roomId, client, userA);
});
it("defaults to undefined", () => {
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBeUndefined();
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBeUndefined();
});
it("lets you set values", () => {
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 1);
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(1);
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBeUndefined();
room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 10);
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBe(1);
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBe(10);
});
it("lets you reset threads notifications", () => {
room.setThreadUnreadNotificationCount("123", NotificationCountType.Total, 666);
room.setThreadUnreadNotificationCount("123", NotificationCountType.Highlight, 123);
room.resetThreadUnreadNotificationCount();
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Total)).toBeUndefined();
expect(room.getThreadUnreadNotificationCount("123", NotificationCountType.Highlight)).toBeUndefined();
});
});
}); });

View File

@@ -20,6 +20,7 @@ import 'jest-localstorage-mock';
import { IndexedDBStore, IStateEventWithRoomId, MemoryStore } from "../../../src"; import { IndexedDBStore, IStateEventWithRoomId, MemoryStore } from "../../../src";
import { emitPromise } from "../../test-utils/test-utils"; import { emitPromise } from "../../test-utils/test-utils";
import { LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend"; import { LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend";
import { defer } from "../../../src/utils";
describe("IndexedDBStore", () => { describe("IndexedDBStore", () => {
afterEach(() => { afterEach(() => {
@@ -111,4 +112,57 @@ describe("IndexedDBStore", () => {
await store.setPendingEvents(roomId, []); await store.setPendingEvents(roomId, []);
expect(localStorage.getItem("mx_pending_events_" + roomId)).toBeNull(); expect(localStorage.getItem("mx_pending_events_" + roomId)).toBeNull();
}); });
it("should resolve isNewlyCreated to true if no database existed initially", async () => {
const store = new IndexedDBStore({
indexedDB,
dbName: "db1",
localStorage,
});
await store.startup();
await expect(store.isNewlyCreated()).resolves.toBeTruthy();
});
it("should resolve isNewlyCreated to false if database existed already", async () => {
let store = new IndexedDBStore({
indexedDB,
dbName: "db2",
localStorage,
});
await store.startup();
store = new IndexedDBStore({
indexedDB,
dbName: "db2",
localStorage,
});
await store.startup();
await expect(store.isNewlyCreated()).resolves.toBeFalsy();
});
it("should resolve isNewlyCreated to false if database existed already but needs upgrade", async () => {
const deferred = defer<Event>();
// seed db3 to Version 1 so it forces a migration
const req = indexedDB.open("matrix-js-sdk:db3", 1);
req.onupgradeneeded = () => {
const db = req.result;
db.createObjectStore("users", { keyPath: ["userId"] });
db.createObjectStore("accountData", { keyPath: ["type"] });
db.createObjectStore("sync", { keyPath: ["clobber"] });
};
req.onsuccess = deferred.resolve;
await deferred.promise;
req.result.close();
const store = new IndexedDBStore({
indexedDB,
dbName: "db3",
localStorage,
});
await store.startup();
await expect(store.isNewlyCreated()).resolves.toBeFalsy();
});
}); });

View File

@@ -30,6 +30,12 @@ const RES_WITH_AGE = {
account_data: { events: [] }, account_data: { events: [] },
ephemeral: { events: [] }, ephemeral: { events: [] },
unread_notifications: {}, unread_notifications: {},
unread_thread_notifications: {
"$143273582443PhrSn:example.org": {
highlight_count: 0,
notification_count: 1,
},
},
timeline: { timeline: {
events: [ events: [
Object.freeze({ Object.freeze({
@@ -302,9 +308,6 @@ describe("SyncAccumulator", function() {
[ReceiptType.ReadPrivate]: { [ReceiptType.ReadPrivate]: {
"@dan:localhost": { ts: 4 }, "@dan:localhost": { ts: 4 },
}, },
[ReceiptType.UnstableReadPrivate]: {
"@matthew:localhost": { ts: 5 },
},
"some.other.receipt.type": { "some.other.receipt.type": {
"@should_be_ignored:localhost": { key: "val" }, "@should_be_ignored:localhost": { key: "val" },
}, },
@@ -350,9 +353,6 @@ describe("SyncAccumulator", function() {
[ReceiptType.ReadPrivate]: { [ReceiptType.ReadPrivate]: {
"@dan:localhost": { ts: 4 }, "@dan:localhost": { ts: 4 },
}, },
[ReceiptType.UnstableReadPrivate]: {
"@matthew:localhost": { ts: 5 },
},
}, },
"$event2:localhost": { "$event2:localhost": {
[ReceiptType.Read]: { [ReceiptType.Read]: {
@@ -445,6 +445,13 @@ describe("SyncAccumulator", function() {
Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]), Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]),
); );
}); });
it("should retrieve unread thread notifications", () => {
sa.accumulate(RES_WITH_AGE);
const output = sa.getJSON();
expect(output.roomsData.join["!foo:bar"]
.unread_thread_notifications["$143273582443PhrSn:example.org"]).not.toBeUndefined();
});
}); });
}); });

View File

@@ -152,6 +152,9 @@ describe("utils", function() {
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 2 })); assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 2 }));
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { b: 2, a: 1 })); assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { b: 2, a: 1 }));
assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 3 })); assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 3 }));
assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1 }));
assert.isFalse(utils.deepCompare({ a: 1 }, { a: 1, b: 2 }));
assert.isFalse(utils.deepCompare({ a: 1 }, { b: 1 }));
assert.isTrue(utils.deepCompare({ assert.isTrue(utils.deepCompare({
1: { name: "mhc", age: 28 }, 1: { name: "mhc", age: 28 },
@@ -525,38 +528,6 @@ describe("utils", function() {
}); });
}); });
describe('getPrivateReadReceiptField', () => {
it('should return m.read.private if server supports stable', async () => {
expect(await utils.getPrivateReadReceiptField({
doesServerSupportUnstableFeature: jest.fn().mockImplementation((feature) => {
return feature === "org.matrix.msc2285.stable";
}),
} as any)).toBe(ReceiptType.ReadPrivate);
});
it('should return m.read.private if server supports stable and unstable', async () => {
expect(await utils.getPrivateReadReceiptField({
doesServerSupportUnstableFeature: jest.fn().mockImplementation((feature) => {
return ["org.matrix.msc2285.stable", "org.matrix.msc2285"].includes(feature);
}),
} as any)).toBe(ReceiptType.ReadPrivate);
});
it('should return org.matrix.msc2285.read.private if server supports unstable', async () => {
expect(await utils.getPrivateReadReceiptField({
doesServerSupportUnstableFeature: jest.fn().mockImplementation((feature) => {
return feature === "org.matrix.msc2285";
}),
} as any)).toBe(ReceiptType.UnstableReadPrivate);
});
it('should return none if server does not support either', async () => {
expect(await utils.getPrivateReadReceiptField({
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
} as any)).toBeFalsy();
});
});
describe('isSupportedReceiptType', () => { describe('isSupportedReceiptType', () => {
it('should support m.read', () => { it('should support m.read', () => {
expect(utils.isSupportedReceiptType(ReceiptType.Read)).toBeTruthy(); expect(utils.isSupportedReceiptType(ReceiptType.Read)).toBeTruthy();
@@ -566,10 +537,6 @@ describe("utils", function() {
expect(utils.isSupportedReceiptType(ReceiptType.ReadPrivate)).toBeTruthy(); expect(utils.isSupportedReceiptType(ReceiptType.ReadPrivate)).toBeTruthy();
}); });
it('should support org.matrix.msc2285.read.private', () => {
expect(utils.isSupportedReceiptType(ReceiptType.UnstableReadPrivate)).toBeTruthy();
});
it('should not support other receipt types', () => { it('should not support other receipt types', () => {
expect(utils.isSupportedReceiptType("this is a receipt type")).toBeFalsy(); expect(utils.isSupportedReceiptType("this is a receipt type")).toBeFalsy();
}); });

View File

@@ -156,9 +156,13 @@ export interface IPusher {
lang: string; lang: string;
profile_tag?: string; profile_tag?: string;
pushkey: string; pushkey: string;
enabled?: boolean | null | undefined;
"org.matrix.msc3881.enabled"?: boolean | null | undefined;
device_id?: string | null;
"org.matrix.msc3881.device_id"?: string | null;
} }
export interface IPusherRequest extends IPusher { export interface IPusherRequest extends Omit<IPusher, "device_id" | "org.matrix.msc3881.device_id"> {
append?: boolean; append?: boolean;
} }

View File

@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { UnstableValue } from "../NamespacedValue";
// disable lint because these are wire responses // disable lint because these are wire responses
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@@ -27,3 +29,89 @@ export interface IRefreshTokenResponse {
} }
/* eslint-enable camelcase */ /* eslint-enable camelcase */
/**
* Response to GET login flows as per https://spec.matrix.org/v1.3/client-server-api/#get_matrixclientv3login
*/
export interface ILoginFlowsResponse {
flows: LoginFlow[];
}
export type LoginFlow = ISSOFlow | IPasswordFlow | ILoginFlow;
export interface ILoginFlow {
type: string;
}
export interface IPasswordFlow extends ILoginFlow {
type: "m.login.password";
}
export const DELEGATED_OIDC_COMPATIBILITY = new UnstableValue(
"delegated_oidc_compatibility",
"org.matrix.msc3824.delegated_oidc_compatibility",
);
/**
* Representation of SSO flow as per https://spec.matrix.org/v1.3/client-server-api/#client-login-via-sso
*/
export interface ISSOFlow extends ILoginFlow {
type: "m.login.sso" | "m.login.cas";
// eslint-disable-next-line camelcase
identity_providers?: IIdentityProvider[];
[DELEGATED_OIDC_COMPATIBILITY.name]?: boolean;
[DELEGATED_OIDC_COMPATIBILITY.altName]?: boolean;
}
export enum IdentityProviderBrand {
Gitlab = "gitlab",
Github = "github",
Apple = "apple",
Google = "google",
Facebook = "facebook",
Twitter = "twitter",
}
export interface IIdentityProvider {
id: string;
name: string;
icon?: string;
brand?: IdentityProviderBrand | string;
}
/**
* Parameters to login request as per https://spec.matrix.org/v1.3/client-server-api/#login
*/
/* eslint-disable camelcase */
export interface ILoginParams {
identifier?: object;
password?: string;
token?: string;
device_id?: string;
initial_device_display_name?: string;
}
/* eslint-enable camelcase */
export enum SSOAction {
/** The user intends to login to an existing account */
LOGIN = "login",
/** The user intends to register for a new account */
REGISTER = "register",
}
/**
* The result of a successful [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882)
* `m.login.token` issuance request.
* Note that this is UNSTABLE and subject to breaking changes without notice.
*/
export interface LoginTokenPostResponse {
/**
* The token to use with `m.login.token` to authenticate.
*/
login_token: string;
/**
* Expiration in seconds.
*/
expires_in: number;
}

20
src/@types/crypto.ts Normal file
View 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;
};

View File

@@ -191,6 +191,33 @@ export const EVENT_VISIBILITY_CHANGE_TYPE = new UnstableValue(
"m.visibility", "m.visibility",
"org.matrix.msc3531.visibility"); "org.matrix.msc3531.visibility");
/**
* https://github.com/matrix-org/matrix-doc/pull/3881
*
* @experimental
*/
export const PUSHER_ENABLED = new UnstableValue(
"enabled",
"org.matrix.msc3881.enabled");
/**
* https://github.com/matrix-org/matrix-doc/pull/3881
*
* @experimental
*/
export const PUSHER_DEVICE_ID = new UnstableValue(
"device_id",
"org.matrix.msc3881.device_id");
/**
* https://github.com/matrix-org/matrix-doc/pull/3890
*
* @experimental
*/
export const LOCAL_NOTIFICATION_SETTINGS_PREFIX = new UnstableValue(
"m.local_notification_settings",
"org.matrix.msc3890.local_notification_settings");
export interface IEncryptedFile { export interface IEncryptedFile {
url: string; url: string;
mimetype?: string; mimetype?: string;

View 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;
}

View File

@@ -18,8 +18,4 @@ export enum ReceiptType {
Read = "m.read", Read = "m.read",
FullyRead = "m.fully_read", FullyRead = "m.fully_read",
ReadPrivate = "m.read.private", ReadPrivate = "m.read.private",
/**
* @deprecated Please use the ReadPrivate type when possible. This value may be removed at any time without notice.
*/
UnstableReadPrivate = "org.matrix.msc2285.read.private",
} }

View File

@@ -22,7 +22,7 @@ import { IRoomEventFilter } from "../filter";
import { Direction } from "../models/event-timeline"; import { Direction } from "../models/event-timeline";
import { PushRuleAction } from "./PushRules"; import { PushRuleAction } from "./PushRules";
import { IRoomEvent } from "../sync-accumulator"; import { IRoomEvent } from "../sync-accumulator";
import { RoomType } from "./event"; import { EventType, RoomType } from "./event";
// allow camelcase as these are things that go onto the wire // allow camelcase as these are things that go onto the wire
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@@ -98,7 +98,18 @@ export interface ICreateRoomOpts {
name?: string; name?: string;
topic?: string; topic?: string;
preset?: Preset; preset?: Preset;
power_level_content_override?: object; power_level_content_override?: {
ban?: number;
events?: Record<EventType | string, number>;
events_default?: number;
invite?: number;
kick?: number;
notifications?: Record<string, number>;
redact?: number;
state_default?: number;
users?: Record<string, number>;
users_default?: number;
};
creation_content?: object; creation_content?: object;
initial_state?: ICreateRoomStateEvent[]; initial_state?: ICreateRoomStateEvent[];
invite?: string[]; invite?: string[];
@@ -149,7 +160,7 @@ export interface IRelationsRequestOpts {
from?: string; from?: string;
to?: string; to?: string;
limit?: number; limit?: number;
direction?: Direction; dir?: Direction;
} }
export interface IRelationsResponse { export interface IRelationsResponse {

26
src/@types/sync.ts Normal file
View 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
View 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;

View File

@@ -1,5 +1,5 @@
/* /*
Copyright 2021 The Matrix.org Foundation C.I.C. Copyright 2021 - 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { Optional } from "matrix-events-sdk/lib/types";
/** /**
* Represents a simple Matrix namespaced value. This will assume that if a stable prefix * Represents a simple Matrix namespaced value. This will assume that if a stable prefix
* is provided that the stable prefix should be used when representing the identifier. * is provided that the stable prefix should be used when representing the identifier.
@@ -41,13 +43,20 @@ export class NamespacedValue<S extends string, U extends string> {
return this.unstable; return this.unstable;
} }
public get names(): (U | S)[] {
const names = [this.name];
const altName = this.altName;
if (altName) names.push(altName);
return names;
}
public matches(val: string): boolean { public matches(val: string): boolean {
return this.name === val || this.altName === val; return this.name === val || this.altName === val;
} }
// this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class // this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class
// so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace. // so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace.
public findIn<T>(obj: any): T { public findIn<T>(obj: any): Optional<T> {
let val: T; let val: T;
if (this.name) { if (this.name) {
val = obj?.[this.name]; val = obj?.[this.name];

View File

@@ -19,7 +19,7 @@ limitations under the License.
* @module client * @module client
*/ */
import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent } from "matrix-events-sdk"; import { EmoteEvent, IPartialEvent, MessageEvent, NoticeEvent, Optional } from "matrix-events-sdk";
import { ISyncStateData, SyncApi, SyncState } from "./sync"; import { ISyncStateData, SyncApi, SyncState } from "./sync";
import { import {
@@ -33,7 +33,7 @@ import {
} from "./models/event"; } from "./models/event";
import { StubStore } from "./store/stub"; import { StubStore } from "./store/stub";
import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, supportsMatrixCall } from "./webrtc/call"; import { CallEvent, CallEventHandlerMap, createNewMatrixCall, MatrixCall, supportsMatrixCall } from "./webrtc/call";
import { Filter, IFilterDefinition } from "./filter"; import { Filter, IFilterDefinition, IRoomEventFilter } from "./filter";
import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler'; import { CallEventHandlerEvent, CallEventHandler, CallEventHandlerEventHandlerMap } from './webrtc/callEventHandler';
import * as utils from './utils'; import * as utils from './utils';
import { sleep } from './utils'; import { sleep } from './utils';
@@ -158,7 +158,9 @@ import {
} from "./@types/requests"; } from "./@types/requests";
import { import {
EventType, EventType,
LOCAL_NOTIFICATION_SETTINGS_PREFIX,
MsgType, MsgType,
PUSHER_ENABLED,
RelationType, RelationType,
RoomCreateTypeField, RoomCreateTypeField,
RoomType, RoomType,
@@ -188,15 +190,22 @@ import { IPusher, IPusherRequest, IPushRules, PushRuleAction, PushRuleKind, Rule
import { IThreepid } from "./@types/threepids"; import { IThreepid } from "./@types/threepids";
import { CryptoStore } from "./crypto/store/base"; import { CryptoStore } from "./crypto/store/base";
import { MediaHandler } from "./webrtc/mediaHandler"; import { MediaHandler } from "./webrtc/mediaHandler";
import { IRefreshTokenResponse } from "./@types/auth"; import { LoginTokenPostResponse, ILoginFlowsResponse, IRefreshTokenResponse, SSOAction } from "./@types/auth";
import { TypedEventEmitter } from "./models/typed-event-emitter"; import { TypedEventEmitter } from "./models/typed-event-emitter";
import { ReceiptType } from "./@types/read_receipts"; import { ReceiptType } from "./@types/read_receipts";
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync"; import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
import { SlidingSyncSdk } from "./sliding-sync-sdk"; import { SlidingSyncSdk } from "./sliding-sync-sdk";
import { Thread, THREAD_RELATION_TYPE } from "./models/thread"; import { FeatureSupport, Thread, THREAD_RELATION_TYPE, determineFeatureSupport } from "./models/thread";
import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon"; import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
import { UnstableValue } from "./NamespacedValue";
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue"; import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
import { ToDeviceBatch } from "./models/ToDeviceMessage"; import { ToDeviceBatch } from "./models/ToDeviceMessage";
import { MAIN_ROOM_TIMELINE } from "./models/read-receipt";
import { IgnoredInvites } from "./models/invites-ignorer";
import { UIARequest, UIAResponse } from "./@types/uia";
import { LocalNotificationSettings } from "./@types/local_notifications";
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync";
import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature";
export type Store = IStore; export type Store = IStore;
@@ -208,6 +217,11 @@ export const CRYPTO_ENABLED: boolean = isCryptoAvailable();
const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes
export const UNSTABLE_MSC3852_LAST_SEEN_UA = new UnstableValue(
"last_seen_user_agent",
"org.matrix.msc3852.last_seen_user_agent",
);
interface IExportedDevice { interface IExportedDevice {
olmDevice: IExportedOlmDevice; olmDevice: IExportedOlmDevice;
userId: string; userId: string;
@@ -396,8 +410,7 @@ export interface IStartClientOpts {
pollTimeout?: number; pollTimeout?: number;
/** /**
* The filter to apply to /sync calls. This will override the opts.initialSyncLimit, which would * The filter to apply to /sync calls.
* normally result in a timeline limit filter.
*/ */
filter?: Filter; filter?: Filter;
@@ -517,15 +530,21 @@ export interface ITurnServer {
credential: string; credential: string;
} }
interface IServerVersions { export interface IServerVersions {
versions: string[]; versions: string[];
unstable_features: Record<string, boolean>; unstable_features: Record<string, boolean>;
} }
export const M_AUTHENTICATION = new UnstableValue(
"m.authentication",
"org.matrix.msc2965.authentication",
);
export interface IClientWellKnown { export interface IClientWellKnown {
[key: string]: any; [key: string]: any;
"m.homeserver"?: IWellKnownConfig; "m.homeserver"?: IWellKnownConfig;
"m.identity_server"?: IWellKnownConfig; "m.identity_server"?: IWellKnownConfig;
[M_AUTHENTICATION.name]?: IDelegatedAuthConfig; // MSC2965
} }
export interface IWellKnownConfig { export interface IWellKnownConfig {
@@ -537,6 +556,13 @@ export interface IWellKnownConfig {
base_url?: string | null; base_url?: string | null;
} }
export interface IDelegatedAuthConfig { // MSC2965
/** The OIDC Provider/issuer the client should use */
issuer: string;
/** The optional URL of the web UI where the user can manage their account */
account?: string;
}
interface IKeyBackupPath { interface IKeyBackupPath {
path: string; path: string;
queryData?: { queryData?: {
@@ -572,6 +598,13 @@ interface IMessagesResponse {
state: IStateEvent[]; state: IStateEvent[];
} }
interface IThreadedMessagesResponse {
prev_batch: string;
next_batch: string;
chunk: IRoomEvent[];
state: IStateEvent[];
}
export interface IRequestTokenResponse { export interface IRequestTokenResponse {
sid: string; sid: string;
submit_url?: string; submit_url?: string;
@@ -669,6 +702,8 @@ export interface IMyDevice {
display_name?: string; display_name?: string;
last_seen_ip?: string; last_seen_ip?: string;
last_seen_ts?: number; last_seen_ts?: number;
[UNSTABLE_MSC3852_LAST_SEEN_UA.stable]?: string;
[UNSTABLE_MSC3852_LAST_SEEN_UA.unstable]?: string;
} }
export interface IDownloadKeyResult { export interface IDownloadKeyResult {
@@ -846,7 +881,7 @@ type UserEvents = UserEvent.AvatarUrl
| UserEvent.CurrentlyActive | UserEvent.CurrentlyActive
| UserEvent.LastPresenceTs; | UserEvent.LastPresenceTs;
type EmittedEvents = ClientEvent export type EmittedEvents = ClientEvent
| RoomEvents | RoomEvents
| RoomStateEvents | RoomStateEvents
| CryptoEvents | CryptoEvents
@@ -881,6 +916,8 @@ export type ClientEventHandlerMap = {
& HttpApiEventHandlerMap & HttpApiEventHandlerMap
& BeaconEventHandlerMap; & BeaconEventHandlerMap;
const SSO_ACTION_PARAM = new UnstableValue("action", "org.matrix.msc3824.action");
/** /**
* Represents a Matrix Client. Only directly construct this if you want to use * Represents a Matrix Client. Only directly construct this if you want to use
* custom modules. Normally, {@link createClient} should be used * custom modules. Normally, {@link createClient} should be used
@@ -902,7 +939,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public urlPreviewCache: { [key: string]: Promise<IPreviewUrlResponse> } = {}; public urlPreviewCache: { [key: string]: Promise<IPreviewUrlResponse> } = {};
public identityServer: IIdentityServerProvider; public identityServer: IIdentityServerProvider;
public http: MatrixHttpApi; // XXX: Intended private, used in code. public http: MatrixHttpApi; // XXX: Intended private, used in code.
public crypto: Crypto; // XXX: Intended private, used in code. public crypto?: Crypto; // XXX: Intended private, used in code.
public cryptoCallbacks: ICryptoCallbacks; // XXX: Intended private, used in code. public cryptoCallbacks: ICryptoCallbacks; // XXX: Intended private, used in code.
public callEventHandler: CallEventHandler; // XXX: Intended private, used in code. public callEventHandler: CallEventHandler; // XXX: Intended private, used in code.
public supportsCallTransfer = false; // XXX: Intended private, used in code. public supportsCallTransfer = false; // XXX: Intended private, used in code.
@@ -932,6 +969,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
protected clientWellKnownIntervalID: ReturnType<typeof setInterval>; protected clientWellKnownIntervalID: ReturnType<typeof setInterval>;
protected canResetTimelineCallback: ResetTimelineCallback; protected canResetTimelineCallback: ResetTimelineCallback;
public canSupport = new Map<Feature, ServerSupport>();
// The pushprocessor caches useful things, so keep one and re-use it // The pushprocessor caches useful things, so keep one and re-use it
protected pushProcessor = new PushProcessor(this); protected pushProcessor = new PushProcessor(this);
@@ -955,6 +994,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
private toDeviceMessageQueue: ToDeviceMessageQueue; private toDeviceMessageQueue: ToDeviceMessageQueue;
// A manager for determining which invites should be ignored.
public readonly ignoredInvites: IgnoredInvites;
constructor(opts: IMatrixClientCreateOpts) { constructor(opts: IMatrixClientCreateOpts) {
super(); super();
@@ -1057,35 +1099,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// We do this so that push rules are correctly executed on events in their decrypted // We do this so that push rules are correctly executed on events in their decrypted
// state, such as highlights when the user's name is mentioned. // state, such as highlights when the user's name is mentioned.
this.on(MatrixEventEvent.Decrypted, (event) => { this.on(MatrixEventEvent.Decrypted, (event) => {
const oldActions = event.getPushActions(); fixNotificationCountOnDecryption(this, event);
const actions = this.getPushActionsForEvent(event, true);
const room = this.getRoom(event.getRoomId());
if (!room) return;
const currentCount = room.getUnreadNotificationCount(NotificationCountType.Highlight);
// Ensure the unread counts are kept up to date if the event is encrypted
// We also want to make sure that the notification count goes up if we already
// have encrypted events to avoid other code from resetting 'highlight' to zero.
const oldHighlight = !!oldActions?.tweaks?.highlight;
const newHighlight = !!actions?.tweaks?.highlight;
if (oldHighlight !== newHighlight || currentCount > 0) {
// TODO: Handle mentions received while the client is offline
// See also https://github.com/vector-im/element-web/issues/9069
if (!room.hasUserReadEvent(this.getUserId(), event.getId())) {
let newCount = currentCount;
if (newHighlight && !oldHighlight) newCount++;
if (!newHighlight && oldHighlight) newCount--;
room.setUnreadNotificationCount(NotificationCountType.Highlight, newCount);
// Fix 'Mentions Only' rooms from not having the right badge count
const totalCount = room.getUnreadNotificationCount(NotificationCountType.Total);
if (totalCount < newCount) {
room.setUnreadNotificationCount(NotificationCountType.Total, newCount);
}
}
}
}); });
// Like above, we have to listen for read receipts from ourselves in order to // Like above, we have to listen for read receipts from ourselves in order to
@@ -1136,6 +1150,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
room.setUnreadNotificationCount(NotificationCountType.Highlight, highlightCount); room.setUnreadNotificationCount(NotificationCountType.Highlight, highlightCount);
} }
}); });
this.ignoredInvites = new IgnoredInvites(this);
} }
/** /**
@@ -1185,14 +1201,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.syncApi.stop(); this.syncApi.stop();
} }
try { const serverVersions = await this.getVersions();
const { serverSupport, stable } = await this.doesServerSupportThread(); this.canSupport = await buildFeatureSupportMap(serverVersions);
Thread.setServerSideSupport(serverSupport, stable);
} catch (e) { const support = this.canSupport.get(Feature.ThreadUnreadNotifications);
// Most likely cause is that `doesServerSupportThread` returned `null` (as it UNREAD_THREAD_NOTIFICATIONS.setPreferUnstable(support === ServerSupport.Unstable);
// is allowed to do) and thus we enter "degraded mode" on threads.
Thread.setServerSideSupport(false, true); const { threads, list } = await this.doesServerSupportThread();
} Thread.setServerSideSupport(threads);
Thread.setServerSideListSupport(list);
// shallow-copy the opts dict before modifying and storing it // shallow-copy the opts dict before modifying and storing it
this.clientOpts = Object.assign({}, opts) as IStoredClientOpts; this.clientOpts = Object.assign({}, opts) as IStoredClientOpts;
@@ -2667,7 +2684,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
try { try {
res = await this.http.authedRequest<IKeyBackupInfo>( res = await this.http.authedRequest<IKeyBackupInfo>(
undefined, Method.Get, "/room_keys/version", undefined, undefined, undefined, Method.Get, "/room_keys/version", undefined, undefined,
{ prefix: PREFIX_UNSTABLE }, { prefix: PREFIX_V3 },
); );
} catch (e) { } catch (e) {
if (e.errcode === 'M_NOT_FOUND') { if (e.errcode === 'M_NOT_FOUND') {
@@ -2823,7 +2840,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const res = await this.http.authedRequest<IKeyBackupInfo>( const res = await this.http.authedRequest<IKeyBackupInfo>(
undefined, Method.Post, "/room_keys/version", undefined, data, undefined, Method.Post, "/room_keys/version", undefined, data,
{ prefix: PREFIX_UNSTABLE }, { prefix: PREFIX_V3 },
); );
// We could assume everything's okay and enable directly, but this ensures // We could assume everything's okay and enable directly, but this ensures
@@ -2855,7 +2872,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.http.authedRequest( return this.http.authedRequest(
undefined, Method.Delete, path, undefined, undefined, undefined, Method.Delete, path, undefined, undefined,
{ prefix: PREFIX_UNSTABLE }, { prefix: PREFIX_V3 },
); );
} }
@@ -3324,7 +3341,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {string} roomId The room ID * @param {string} roomId The room ID
* @return {Room|null} The Room or null if it doesn't exist or there is no data store. * @return {Room|null} The Room or null if it doesn't exist or there is no data store.
*/ */
public getRoom(roomId: string): Room | null { public getRoom(roomId: string | undefined): Room | null {
if (!roomId) {
return null;
}
return this.store.getRoom(roomId); return this.store.getRoom(roomId);
} }
@@ -3413,7 +3433,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {string} eventType The event type * @param {string} eventType The event type
* @return {?object} The contents of the given account data event * @return {?object} The contents of the given account data event
*/ */
public getAccountData(eventType: string): MatrixEvent { public getAccountData(eventType: string): MatrixEvent | undefined {
return this.store.getAccountData(eventType); return this.store.getAccountData(eventType);
} }
@@ -4582,7 +4602,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {Promise} Resolves: to an empty object {} * @return {Promise} Resolves: to an empty object {}
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
public sendReceipt(event: MatrixEvent, receiptType: ReceiptType, body: any, callback?: Callback): Promise<{}> { public async sendReceipt(
event: MatrixEvent,
receiptType: ReceiptType,
body: any,
callback?: Callback,
): Promise<{}> {
if (typeof (body) === 'function') { if (typeof (body) === 'function') {
callback = body as any as Callback; // legacy callback = body as any as Callback; // legacy
body = {}; body = {};
@@ -4597,10 +4622,19 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$receiptType: receiptType, $receiptType: receiptType,
$eventId: event.getId(), $eventId: event.getId(),
}); });
// TODO: Add a check for which spec version this will be released in
if (await this.doesServerSupportUnstableFeature("org.matrix.msc3771")) {
const isThread = !!event.threadRootId;
body.thread_id = isThread
? event.threadRootId
: MAIN_ROOM_TIMELINE;
}
const promise = this.http.authedRequest(callback, Method.Post, path, undefined, body || {}); const promise = this.http.authedRequest(callback, Method.Post, path, undefined, body || {});
const room = this.getRoom(event.getRoomId()); const room = this.getRoom(event.getRoomId());
if (room) { if (room && this.credentials.userId) {
room.addLocalEchoReceipt(this.credentials.userId, event, receiptType); room.addLocalEchoReceipt(this.credentials.userId, event, receiptType);
} }
return promise; return promise;
@@ -4614,7 +4648,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {Promise} Resolves: to an empty object {} * @return {Promise} Resolves: to an empty object {}
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
public async sendReadReceipt(event: MatrixEvent, receiptType = ReceiptType.Read, callback?: Callback): Promise<{}> { public async sendReadReceipt(
event: MatrixEvent | null,
receiptType = ReceiptType.Read,
callback?: Callback,
): Promise<{} | undefined> {
if (!event) return;
const eventId = event.getId(); const eventId = event.getId();
const room = this.getRoom(event.getRoomId()); const room = this.getRoom(event.getRoomId());
if (room && room.hasPendingEvent(eventId)) { if (room && room.hasPendingEvent(eventId)) {
@@ -5279,6 +5318,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {object} [options] * @param {object} [options]
* @param {boolean} options.preventReEmit don't re-emit events emitted on an event mapped by this mapper on the client * @param {boolean} options.preventReEmit don't re-emit events emitted on an event mapped by this mapper on the client
* @param {boolean} options.decrypt decrypt event proactively * @param {boolean} options.decrypt decrypt event proactively
* @param {boolean} options.toDevice the event is a to_device event
* @return {Function} * @return {Function}
*/ */
public getEventMapper(options?: MapperOpts): EventMapper { public getEventMapper(options?: MapperOpts): EventMapper {
@@ -5299,13 +5339,17 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {Promise} Resolves: * @return {Promise} Resolves:
* {@link module:models/event-timeline~EventTimeline} including the given event * {@link module:models/event-timeline~EventTimeline} including the given event
*/ */
public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<EventTimeline | undefined> { public async getEventTimeline(timelineSet: EventTimelineSet, eventId: string): Promise<Optional<EventTimeline>> {
// don't allow any timeline support unless it's been enabled. // don't allow any timeline support unless it's been enabled.
if (!this.timelineSupport) { if (!this.timelineSupport) {
throw new Error("timeline support is disabled. Set the 'timelineSupport'" + throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
" parameter to true when creating MatrixClient to enable it."); " parameter to true when creating MatrixClient to enable it.");
} }
if (!timelineSet?.room) {
throw new Error("getEventTimeline only supports room timelines");
}
if (timelineSet.getTimelineForEvent(eventId)) { if (timelineSet.getTimelineForEvent(eventId)) {
return timelineSet.getTimelineForEvent(eventId); return timelineSet.getTimelineForEvent(eventId);
} }
@@ -5317,7 +5361,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}, },
); );
let params: Record<string, string | string[]> = undefined; let params: Record<string, string | string[]> | undefined = undefined;
if (this.clientOpts.lazyLoadMembers) { if (this.clientOpts.lazyLoadMembers) {
params = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) }; params = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) };
} }
@@ -5355,12 +5399,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (Thread.hasServerSideSupport && timelineSet.thread) { if (Thread.hasServerSideSupport && timelineSet.thread) {
const thread = timelineSet.thread; const thread = timelineSet.thread;
const opts: IRelationsRequestOpts = { const opts: IRelationsRequestOpts = {
direction: Direction.Backward, dir: Direction.Backward,
limit: 50, limit: 50,
}; };
await thread.fetchInitialEvents(); await thread.fetchInitialEvents();
let nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward); let nextBatch: string | null | undefined = thread.liveTimeline.getPaginationToken(Direction.Backward);
// Fetch events until we find the one we were asked for, or we run out of pages // Fetch events until we find the one we were asked for, or we run out of pages
while (!thread.findEventById(eventId)) { while (!thread.findEventById(eventId)) {
@@ -5410,27 +5454,36 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {Promise} Resolves: * @return {Promise} Resolves:
* {@link module:models/event-timeline~EventTimeline} timeline with the latest events in the room * {@link module:models/event-timeline~EventTimeline} timeline with the latest events in the room
*/ */
public async getLatestTimeline(timelineSet: EventTimelineSet): Promise<EventTimeline> { public async getLatestTimeline(timelineSet: EventTimelineSet): Promise<Optional<EventTimeline>> {
// don't allow any timeline support unless it's been enabled. // don't allow any timeline support unless it's been enabled.
if (!this.timelineSupport) { if (!this.timelineSupport) {
throw new Error("timeline support is disabled. Set the 'timelineSupport'" + throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
" parameter to true when creating MatrixClient to enable it."); " parameter to true when creating MatrixClient to enable it.");
} }
const messagesPath = utils.encodeUri( if (!timelineSet.room) {
"/rooms/$roomId/messages", { throw new Error("getLatestTimeline only supports room timelines");
$roomId: timelineSet.room.roomId,
},
);
const params: Record<string, string | string[]> = {
dir: 'b',
};
if (this.clientOpts.lazyLoadMembers) {
params.filter = JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER);
} }
const res = await this.http.authedRequest<IMessagesResponse>(undefined, Method.Get, messagesPath, params); let res: IMessagesResponse;
const roomId = timelineSet.room.roomId;
if (timelineSet.isThreadTimeline) {
res = await this.createThreadListMessagesRequest(
roomId,
null,
1,
Direction.Backward,
timelineSet.getFilter(),
);
} else {
res = await this.createMessagesRequest(
roomId,
null,
1,
Direction.Backward,
timelineSet.getFilter(),
);
}
const event = res.chunk?.[0]; const event = res.chunk?.[0];
if (!event) { if (!event) {
throw new Error("No message returned from /messages when trying to construct getLatestTimeline"); throw new Error("No message returned from /messages when trying to construct getLatestTimeline");
@@ -5470,7 +5523,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
params.from = fromToken; params.from = fromToken;
} }
let filter = null; let filter: IRoomEventFilter | null = null;
if (this.clientOpts.lazyLoadMembers) { if (this.clientOpts.lazyLoadMembers) {
// create a shallow copy of LAZY_LOADING_MESSAGES_FILTER, // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
// so the timelineFilter doesn't get written into it below // so the timelineFilter doesn't get written into it below
@@ -5488,6 +5541,72 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.http.authedRequest(undefined, Method.Get, path, params); return this.http.authedRequest(undefined, Method.Get, path, params);
} }
/**
* Makes a request to /messages with the appropriate lazy loading filter set.
* XXX: if we do get rid of scrollback (as it's not used at the moment),
* we could inline this method again in paginateEventTimeline as that would
* then be the only call-site
* @param {string} roomId
* @param {string} fromToken
* @param {number} limit the maximum amount of events the retrieve
* @param {string} dir 'f' or 'b'
* @param {Filter} timelineFilter the timeline filter to pass
* @return {Promise}
*/
// XXX: Intended private, used by room.fetchRoomThreads
public createThreadListMessagesRequest(
roomId: string,
fromToken: string | null,
limit = 30,
dir = Direction.Backward,
timelineFilter?: Filter,
): Promise<IMessagesResponse> {
const path = utils.encodeUri("/rooms/$roomId/threads", { $roomId: roomId });
const params: Record<string, string> = {
limit: limit.toString(),
dir: dir,
include: 'all',
};
if (fromToken) {
params.from = fromToken;
}
let filter: IRoomEventFilter | null = null;
if (this.clientOpts.lazyLoadMembers) {
// create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
// so the timelineFilter doesn't get written into it below
filter = {
...filter,
...Filter.LAZY_LOADING_MESSAGES_FILTER,
};
}
if (timelineFilter) {
// XXX: it's horrific that /messages' filter parameter doesn't match
// /sync's one - see https://matrix.org/jira/browse/SPEC-451
filter = {
...filter,
...timelineFilter.getRoomTimelineFilterComponent()?.toJSON(),
};
}
if (filter) {
params.filter = JSON.stringify(filter);
}
const opts: { prefix?: string } = {};
if (Thread.hasServerSideListSupport === FeatureSupport.Experimental) {
opts.prefix = "/_matrix/client/unstable/org.matrix.msc3856";
}
return this.http.authedRequest<IThreadedMessagesResponse>(undefined, Method.Get, path, params, undefined, opts)
.then(res => ({
...res,
start: res.prev_batch,
end: res.next_batch,
}));
}
/** /**
* Take an EventTimeline, and back/forward-fill results. * Take an EventTimeline, and back/forward-fill results.
* *
@@ -5503,6 +5622,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/ */
public paginateEventTimeline(eventTimeline: EventTimeline, opts: IPaginateOpts): Promise<boolean> { public paginateEventTimeline(eventTimeline: EventTimeline, opts: IPaginateOpts): Promise<boolean> {
const isNotifTimeline = (eventTimeline.getTimelineSet() === this.notifTimelineSet); const isNotifTimeline = (eventTimeline.getTimelineSet() === this.notifTimelineSet);
const room = this.getRoom(eventTimeline.getRoomId());
const isThreadTimeline = eventTimeline.getTimelineSet().isThreadTimeline;
// TODO: we should implement a backoff (as per scrollback()) to deal more // TODO: we should implement a backoff (as per scrollback()) to deal more
// nicely with HTTP errors. // nicely with HTTP errors.
@@ -5536,7 +5657,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
only: 'highlight', only: 'highlight',
}; };
if (token !== "end") { if (token && token !== "end") {
params.from = token; params.from = token;
} }
@@ -5544,7 +5665,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
undefined, Method.Get, path, params, undefined, Method.Get, path, params,
).then(async (res) => { ).then(async (res) => {
const token = res.next_token; const token = res.next_token;
const matrixEvents = []; const matrixEvents: MatrixEvent[] = [];
for (let i = 0; i < res.notifications.length; i++) { for (let i = 0; i < res.notifications.length; i++) {
const notification = res.notifications[i]; const notification = res.notifications[i];
@@ -5568,13 +5689,48 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (backwards && !res.next_token) { if (backwards && !res.next_token) {
eventTimeline.setPaginationToken(null, dir); eventTimeline.setPaginationToken(null, dir);
} }
return res.next_token ? true : false; return Boolean(res.next_token);
}).finally(() => {
eventTimeline.paginationRequests[dir] = null;
});
eventTimeline.paginationRequests[dir] = promise;
} else if (isThreadTimeline) {
if (!room) {
throw new Error("Unknown room " + eventTimeline.getRoomId());
}
promise = this.createThreadListMessagesRequest(
eventTimeline.getRoomId(),
token,
opts.limit,
dir,
eventTimeline.getFilter(),
).then((res) => {
if (res.state) {
const roomState = eventTimeline.getState(dir);
const stateEvents = res.state.map(this.getEventMapper());
roomState.setUnknownStateEvents(stateEvents);
}
const token = res.end;
const matrixEvents = res.chunk.map(this.getEventMapper());
const timelineSet = eventTimeline.getTimelineSet();
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
this.processBeaconEvents(room, matrixEvents);
this.processThreadRoots(room, matrixEvents, backwards);
// if we've hit the end of the timeline, we need to stop trying to
// paginate. We need to keep the 'forwards' token though, to make sure
// we can recover from gappy syncs.
if (backwards && res.end == res.start) {
eventTimeline.setPaginationToken(null, dir);
}
return res.end !== res.start;
}).finally(() => { }).finally(() => {
eventTimeline.paginationRequests[dir] = null; eventTimeline.paginationRequests[dir] = null;
}); });
eventTimeline.paginationRequests[dir] = promise; eventTimeline.paginationRequests[dir] = promise;
} else { } else {
const room = this.getRoom(eventTimeline.getRoomId());
if (!room) { if (!room) {
throw new Error("Unknown room " + eventTimeline.getRoomId()); throw new Error("Unknown room " + eventTimeline.getRoomId());
} }
@@ -5595,18 +5751,20 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const matrixEvents = res.chunk.map(this.getEventMapper()); const matrixEvents = res.chunk.map(this.getEventMapper());
const timelineSet = eventTimeline.getTimelineSet(); const timelineSet = eventTimeline.getTimelineSet();
const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(matrixEvents); const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents);
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token); timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
this.processBeaconEvents(timelineSet.room, timelineEvents); this.processBeaconEvents(room, timelineEvents);
this.processThreadEvents(room, threadedEvents, backwards); this.processThreadEvents(room, threadedEvents, backwards);
const atEnd = res.end === undefined || res.end === res.start;
// if we've hit the end of the timeline, we need to stop trying to // if we've hit the end of the timeline, we need to stop trying to
// paginate. We need to keep the 'forwards' token though, to make sure // paginate. We need to keep the 'forwards' token though, to make sure
// we can recover from gappy syncs. // we can recover from gappy syncs.
if (backwards && res.end == res.start) { if (backwards && atEnd) {
eventTimeline.setPaginationToken(null, dir); eventTimeline.setPaginationToken(null, dir);
} }
return res.end != res.start; return !atEnd;
}).finally(() => { }).finally(() => {
eventTimeline.paginationRequests[dir] = null; eventTimeline.paginationRequests[dir] = null;
}); });
@@ -6684,23 +6842,28 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
} }
public async doesServerSupportThread(): Promise<{ public async doesServerSupportThread(): Promise<{
serverSupport: boolean; threads: FeatureSupport;
stable: boolean; list: FeatureSupport;
} | null> { }> {
try { try {
const hasUnstableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440"); const [threadUnstable, threadStable, listUnstable, listStable] = await Promise.all([
const hasStableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable"); this.doesServerSupportUnstableFeature("org.matrix.msc3440"),
this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable"),
this.doesServerSupportUnstableFeature("org.matrix.msc3856"),
this.doesServerSupportUnstableFeature("org.matrix.msc3856.stable"),
]);
// TODO: Use `this.isVersionSupported("v1.3")` for whatever spec version includes MSC3440 formally. // TODO: Use `this.isVersionSupported("v1.3")` for whatever spec version includes MSC3440 formally.
return { return {
serverSupport: hasUnstableSupport || hasStableSupport, threads: determineFeatureSupport(threadStable, threadUnstable),
stable: hasStableSupport, list: determineFeatureSupport(listStable, listUnstable),
}; };
} catch (e) { } catch (e) {
// Assume server support and stability aren't available: null/no data return. return {
// XXX: This should just return an object with `false` booleans instead. threads: FeatureSupport.None,
return null; list: FeatureSupport.None,
};
} }
} }
@@ -6757,7 +6920,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
eventId: string, eventId: string,
relationType?: RelationType | string | null, relationType?: RelationType | string | null,
eventType?: EventType | string | null, eventType?: EventType | string | null,
opts: IRelationsRequestOpts = { direction: Direction.Backward }, opts: IRelationsRequestOpts = { dir: Direction.Backward },
): Promise<{ ): Promise<{
originalEvent: MatrixEvent; originalEvent: MatrixEvent;
events: MatrixEvent[]; events: MatrixEvent[];
@@ -7072,10 +7235,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/** /**
* @param {module:client.callback} callback Optional. * @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: TODO * @return {Promise<ILoginFlowsResponse>} Resolves to the available login flows
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
public loginFlows(callback?: Callback): Promise<any> { // TODO: Types public loginFlows(callback?: Callback): Promise<ILoginFlowsResponse> {
return this.http.request(callback, Method.Get, "/login"); return this.http.request(callback, Method.Get, "/login");
} }
@@ -7151,15 +7314,26 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param {string} loginType The type of SSO login we are doing (sso or cas). * @param {string} loginType The type of SSO login we are doing (sso or cas).
* Defaults to 'sso'. * Defaults to 'sso'.
* @param {string} idpId The ID of the Identity Provider being targeted, optional. * @param {string} idpId The ID of the Identity Provider being targeted, optional.
* @param {SSOAction} action the SSO flow to indicate to the IdP, optional.
* @return {string} The HS URL to hit to begin the SSO login process. * @return {string} The HS URL to hit to begin the SSO login process.
*/ */
public getSsoLoginUrl(redirectUrl: string, loginType = "sso", idpId?: string): string { public getSsoLoginUrl(
redirectUrl: string,
loginType = "sso",
idpId?: string,
action?: SSOAction,
): string {
let url = "/login/" + loginType + "/redirect"; let url = "/login/" + loginType + "/redirect";
if (idpId) { if (idpId) {
url += "/" + idpId; url += "/" + idpId;
} }
return this.http.getUrl(url, { redirectUrl }, PREFIX_R0); const params = {
redirectUrl,
[SSO_ACTION_PARAM.unstable!]: action,
};
return this.http.getUrl(url, params, PREFIX_R0);
} }
/** /**
@@ -7233,6 +7407,27 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.http.authedRequest(undefined, Method.Post, '/account/deactivate', undefined, body); return this.http.authedRequest(undefined, Method.Post, '/account/deactivate', undefined, body);
} }
/**
* Make a request for an `m.login.token` to be issued as per
* [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882).
* The server may require User-Interactive auth.
* Note that this is UNSTABLE and subject to breaking changes without notice.
* @param {IAuthData} auth Optional. Auth data to supply for User-Interactive auth.
* @return {Promise<UIAResponse<LoginTokenPostResponse>>} Resolves: On success, the token response
* or UIA auth data.
*/
public requestLoginToken(auth?: IAuthData): Promise<UIAResponse<LoginTokenPostResponse>> {
const body: UIARequest<{}> = { auth };
return this.http.authedRequest(
undefined, // no callback support
Method.Post,
"/org.matrix.msc3882/login/token",
undefined, // no query params
body,
{ prefix: PREFIX_UNSTABLE },
);
}
/** /**
* Get the fallback URL to use for unknown interactive-auth stages. * Get the fallback URL to use for unknown interactive-auth stages.
* *
@@ -7303,7 +7498,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
eventId: string, eventId: string,
relationType?: RelationType | string | null, relationType?: RelationType | string | null,
eventType?: EventType | string | null, eventType?: EventType | string | null,
opts: IRelationsRequestOpts = { direction: Direction.Backward }, opts: IRelationsRequestOpts = { dir: Direction.Backward },
): Promise<IRelationsResponse> { ): Promise<IRelationsResponse> {
const queryString = utils.encodeParams(opts as Record<string, string | number>); const queryString = utils.encodeParams(opts as Record<string, string | number>);
@@ -7327,7 +7522,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$eventType: eventType, $eventType: eventType,
}); });
return this.http.authedRequest( return this.http.authedRequest(
undefined, Method.Get, path, null, null, { undefined, Method.Get, path, undefined, undefined, {
prefix: PREFIX_UNSTABLE, prefix: PREFIX_UNSTABLE,
}, },
); );
@@ -7524,9 +7719,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
[ReceiptType.Read]: rrEventId, [ReceiptType.Read]: rrEventId,
}; };
const privateField = await utils.getPrivateReadReceiptField(this); if (
if (privateField) { (await this.doesServerSupportUnstableFeature("org.matrix.msc2285.stable"))
content[privateField] = rpEventId; || (await this.isVersionSupported("v1.4"))
) {
content[ReceiptType.ReadPrivate] = rpEventId;
} }
return this.http.authedRequest(undefined, Method.Post, path, undefined, content); return this.http.authedRequest(undefined, Method.Post, path, undefined, content);
@@ -7633,7 +7830,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public getLocalAliases(roomId: string): Promise<{ aliases: string[] }> { public getLocalAliases(roomId: string): Promise<{ aliases: string[] }> {
const path = utils.encodeUri("/rooms/$roomId/aliases", { $roomId: roomId }); const path = utils.encodeUri("/rooms/$roomId/aliases", { $roomId: roomId });
const prefix = PREFIX_V3; const prefix = PREFIX_V3;
return this.http.authedRequest(undefined, Method.Get, path, null, null, { prefix }); return this.http.authedRequest(undefined, Method.Get, path, undefined, undefined, { prefix });
} }
/** /**
@@ -7870,7 +8067,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
'bind': bind, 'bind': bind,
}; };
return this.http.authedRequest( return this.http.authedRequest(
callback, Method.Post, path, null, data, callback, Method.Post, path, undefined, data,
); );
} }
@@ -7889,7 +8086,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public async addThreePidOnly(data: IAddThreePidOnlyBody): Promise<{}> { public async addThreePidOnly(data: IAddThreePidOnlyBody): Promise<{}> {
const path = "/account/3pid/add"; const path = "/account/3pid/add";
const prefix = await this.isVersionSupported("r0.6.0") ? PREFIX_R0 : PREFIX_UNSTABLE; const prefix = await this.isVersionSupported("r0.6.0") ? PREFIX_R0 : PREFIX_UNSTABLE;
return this.http.authedRequest(undefined, Method.Post, path, null, data, { prefix }); return this.http.authedRequest(undefined, Method.Post, path, undefined, data, { prefix });
} }
/** /**
@@ -7911,7 +8108,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const prefix = await this.isVersionSupported("r0.6.0") ? const prefix = await this.isVersionSupported("r0.6.0") ?
PREFIX_R0 : PREFIX_UNSTABLE; PREFIX_R0 : PREFIX_UNSTABLE;
return this.http.authedRequest( return this.http.authedRequest(
undefined, Method.Post, path, null, data, { prefix }, undefined, Method.Post, path, undefined, data, { prefix },
); );
} }
@@ -7938,7 +8135,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
id_server: this.getIdentityServerUrl(true), id_server: this.getIdentityServerUrl(true),
}; };
const prefix = await this.isVersionSupported("r0.6.0") ? PREFIX_R0 : PREFIX_UNSTABLE; const prefix = await this.isVersionSupported("r0.6.0") ? PREFIX_R0 : PREFIX_UNSTABLE;
return this.http.authedRequest(undefined, Method.Post, path, null, data, { prefix }); return this.http.authedRequest(undefined, Method.Post, path, undefined, data, { prefix });
} }
/** /**
@@ -7955,7 +8152,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
): Promise<{ id_server_unbind_result: IdServerUnbindResult }> { ): Promise<{ id_server_unbind_result: IdServerUnbindResult }> {
const path = "/account/3pid/delete"; const path = "/account/3pid/delete";
return this.http.authedRequest(undefined, Method.Post, path, null, { medium, address }); return this.http.authedRequest(undefined, Method.Post, path, undefined, { medium, address });
} }
/** /**
@@ -8001,7 +8198,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}; };
return this.http.authedRequest<{}>( return this.http.authedRequest<{}>(
callback, Method.Post, path, null, data, callback, Method.Post, path, undefined, data,
); );
} }
@@ -8092,8 +8289,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @return {Promise} Resolves: Array of objects representing pushers * @return {Promise} Resolves: Array of objects representing pushers
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
public getPushers(callback?: Callback): Promise<{ pushers: IPusher[] }> { public async getPushers(callback?: Callback): Promise<{ pushers: IPusher[] }> {
return this.http.authedRequest(callback, Method.Get, "/pushers"); const response = await this.http.authedRequest(callback, Method.Get, "/pushers");
// Migration path for clients that connect to a homeserver that does not support
// MSC3881 yet, see https://github.com/matrix-org/matrix-spec-proposals/blob/kerry/remote-push-toggle/proposals/3881-remote-push-notification-toggling.md#migration
if (!await this.doesServerSupportUnstableFeature("org.matrix.msc3881")) {
response.pushers = response.pushers.map(pusher => {
if (!pusher.hasOwnProperty(PUSHER_ENABLED.name)) {
pusher[PUSHER_ENABLED.name] = true;
}
return pusher;
});
}
return response;
} }
/** /**
@@ -8106,7 +8316,22 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/ */
public setPusher(pusher: IPusherRequest, callback?: Callback): Promise<{}> { public setPusher(pusher: IPusherRequest, callback?: Callback): Promise<{}> {
const path = "/pushers/set"; const path = "/pushers/set";
return this.http.authedRequest(callback, Method.Post, path, null, pusher); return this.http.authedRequest(callback, Method.Post, path, undefined, pusher);
}
/**
* Persists local notification settings
* @param {string} deviceId
* @param {LocalNotificationSettings} notificationSettings
* @return {Promise} Resolves: an empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
public setLocalNotificationSettings(
deviceId: string,
notificationSettings: LocalNotificationSettings,
): Promise<{}> {
const key = `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
return this.setAccountData(key, notificationSettings);
} }
/** /**
@@ -8893,7 +9118,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$eventId: eventId, $eventId: eventId,
}); });
return this.http.authedRequest(undefined, Method.Post, path, null, { score, reason }); return this.http.authedRequest(undefined, Method.Post, path, undefined, { score, reason });
} }
/** /**
@@ -9055,7 +9280,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/ */
public async getRoomSummary(roomIdOrAlias: string, via?: string[]): Promise<IRoomSummary> { public async getRoomSummary(roomIdOrAlias: string, via?: string[]): Promise<IRoomSummary> {
const path = utils.encodeUri("/rooms/$roomid/summary", { $roomid: roomIdOrAlias }); const path = utils.encodeUri("/rooms/$roomid/summary", { $roomid: roomIdOrAlias });
return this.http.authedRequest(undefined, Method.Get, path, { via }, null, { return this.http.authedRequest(undefined, Method.Get, path, { via }, undefined, {
qsStringifyOptions: { arrayFormat: 'repeat' }, qsStringifyOptions: { arrayFormat: 'repeat' },
prefix: "/_matrix/client/unstable/im.nheko.summary", prefix: "/_matrix/client/unstable/im.nheko.summary",
}); });
@@ -9068,6 +9293,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
room.processThreadedEvents(threadedEvents, toStartOfTimeline); room.processThreadedEvents(threadedEvents, toStartOfTimeline);
} }
/**
* @experimental
*/
public processThreadRoots(room: Room, threadedEvents: MatrixEvent[], toStartOfTimeline: boolean): void {
room.processThreadRoots(threadedEvents, toStartOfTimeline);
}
public processBeaconEvents( public processBeaconEvents(
room?: Room, room?: Room,
events?: MatrixEvent[], events?: MatrixEvent[],
@@ -9116,6 +9348,73 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
} }
} }
/**
* recalculates an accurate notifications count on event decryption.
* Servers do not have enough knowledge about encrypted events to calculate an
* accurate notification_count
*/
export function fixNotificationCountOnDecryption(cli: MatrixClient, event: MatrixEvent): void {
const oldActions = event.getPushActions();
const actions = cli.getPushActionsForEvent(event, true);
const room = cli.getRoom(event.getRoomId());
if (!room || !cli.getUserId()) return;
const isThreadEvent = !!event.threadRootId && !event.isThreadRoot;
const currentCount = (isThreadEvent
? room.getThreadUnreadNotificationCount(
event.threadRootId,
NotificationCountType.Highlight,
)
: room.getUnreadNotificationCount(NotificationCountType.Highlight)) ?? 0;
// Ensure the unread counts are kept up to date if the event is encrypted
// We also want to make sure that the notification count goes up if we already
// have encrypted events to avoid other code from resetting 'highlight' to zero.
const oldHighlight = !!oldActions?.tweaks?.highlight;
const newHighlight = !!actions?.tweaks?.highlight;
if (oldHighlight !== newHighlight || currentCount > 0) {
// TODO: Handle mentions received while the client is offline
// See also https://github.com/vector-im/element-web/issues/9069
const hasReadEvent = isThreadEvent
? room.getThread(event.threadRootId).hasUserReadEvent(cli.getUserId(), event.getId())
: room.hasUserReadEvent(cli.getUserId(), event.getId());
if (!hasReadEvent) {
let newCount = currentCount;
if (newHighlight && !oldHighlight) newCount++;
if (!newHighlight && oldHighlight) newCount--;
if (isThreadEvent) {
room.setThreadUnreadNotificationCount(
event.threadRootId,
NotificationCountType.Highlight,
newCount,
);
} else {
room.setUnreadNotificationCount(NotificationCountType.Highlight, newCount);
}
// Fix 'Mentions Only' rooms from not having the right badge count
const totalCount = (isThreadEvent
? room.getThreadUnreadNotificationCount(event.threadRootId, NotificationCountType.Total)
: room.getUnreadNotificationCount(NotificationCountType.Total)) ?? 0;
if (totalCount < newCount) {
if (isThreadEvent) {
room.setThreadUnreadNotificationCount(
event.threadRootId,
NotificationCountType.Total,
newCount,
);
} else {
room.setUnreadNotificationCount(NotificationCountType.Total, newCount);
}
}
}
}
}
/** /**
* Fires whenever the SDK receives a new event. * Fires whenever the SDK receives a new event.
* <p> * <p>

View File

@@ -292,16 +292,17 @@ export const makeBeaconContent: MakeBeaconContent = (
}); });
export type BeaconLocationState = MLocationContent & { export type BeaconLocationState = MLocationContent & {
timestamp: number; uri?: string; // override from MLocationContent to allow optionals
timestamp?: number;
}; };
export const parseBeaconContent = (content: MBeaconEventContent): BeaconLocationState => { export const parseBeaconContent = (content: MBeaconEventContent): BeaconLocationState => {
const { description, uri } = M_LOCATION.findIn<MLocationContent>(content); const location = M_LOCATION.findIn<MLocationContent>(content);
const timestamp = M_TIMESTAMP.findIn<number>(content); const timestamp = M_TIMESTAMP.findIn<number>(content);
return { return {
description, description: location?.description,
uri, uri: location?.uri,
timestamp, timestamp,
}; };
}; };

View File

@@ -18,7 +18,7 @@ import { logger } from "../logger";
import { MatrixEvent } from "../models/event"; import { MatrixEvent } from "../models/event";
import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning"; import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning";
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { Method, PREFIX_UNSTABLE } from "../http-api"; import { Method, PREFIX_V3 } from "../http-api";
import { Crypto, IBootstrapCrossSigningOpts } from "./index"; import { Crypto, IBootstrapCrossSigningOpts } from "./index";
import { import {
ClientEvent, ClientEvent,
@@ -246,14 +246,14 @@ export class EncryptionSetupOperation {
algorithm: this.keyBackupInfo.algorithm, algorithm: this.keyBackupInfo.algorithm,
auth_data: this.keyBackupInfo.auth_data, auth_data: this.keyBackupInfo.auth_data,
}, },
{ prefix: PREFIX_UNSTABLE }, { prefix: PREFIX_V3 },
); );
} else { } else {
// add new key backup // add new key backup
await baseApis.http.authedRequest( await baseApis.http.authedRequest(
undefined, Method.Post, "/room_keys/version", undefined, Method.Post, "/room_keys/version",
undefined, this.keyBackupInfo, undefined, this.keyBackupInfo,
{ prefix: PREFIX_UNSTABLE }, { prefix: PREFIX_V3 },
); );
} }
} }

View File

@@ -23,6 +23,7 @@ import * as algorithms from './algorithms';
import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base"; import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm"; import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm";
import { IMegolmSessionData } from "./index"; import { IMegolmSessionData } from "./index";
import { OlmGroupSessionExtraData } from "../@types/crypto";
// The maximum size of an event is 65K, and we base64 the content, so this is a // The maximum size of an event is 65K, and we base64 the content, so this is a
// reasonable approximation to the biggest plaintext we can encrypt. // reasonable approximation to the biggest plaintext we can encrypt.
@@ -122,6 +123,7 @@ interface IInboundGroupSessionKey {
forwarding_curve25519_key_chain: string[]; forwarding_curve25519_key_chain: string[];
sender_claimed_ed25519_key: string; sender_claimed_ed25519_key: string;
shared_history: boolean; shared_history: boolean;
untrusted: boolean;
} }
/* eslint-enable camelcase */ /* eslint-enable camelcase */
@@ -1101,7 +1103,7 @@ export class OlmDevice {
sessionKey: string, sessionKey: string,
keysClaimed: Record<string, string>, keysClaimed: Record<string, string>,
exportFormat: boolean, exportFormat: boolean,
extraSessionData: Record<string, any> = {}, extraSessionData: OlmGroupSessionExtraData = {},
): Promise<void> { ): Promise<void> {
await this.cryptoStore.doTxn( await this.cryptoStore.doTxn(
'readwrite', [ 'readwrite', [
@@ -1133,18 +1135,43 @@ export class OlmDevice {
"Update for megolm session " "Update for megolm session "
+ senderKey + "/" + sessionId, + senderKey + "/" + sessionId,
); );
if (existingSession.first_known_index() if (existingSession.first_known_index() <= session.first_known_index()) {
<= session.first_known_index() if (!existingSessionData.untrusted || extraSessionData.untrusted) {
&& !(existingSession.first_known_index() == session.first_known_index() // existing session has less-than-or-equal index
&& !extraSessionData.untrusted // (i.e. can decrypt at least as much), and the
&& existingSessionData.untrusted)) { // new session's trust does not win over the old
// existing session has lower index (i.e. can // session's trust, so keep it
// decrypt more), or they have the same index and
// the new sessions trust does not win over the old
// sessions trust, so keep it
logger.log(`Keeping existing megolm session ${sessionId}`); logger.log(`Keeping existing megolm session ${sessionId}`);
return; return;
} }
if (existingSession.first_known_index() < session.first_known_index()) {
// We want to upgrade the existing session's trust,
// but we can't just use the new session because we'll
// lose the lower index. Check that the sessions connect
// properly, and then manually set the existing session
// as trusted.
if (
existingSession.export_session(session.first_known_index())
=== session.export_session(session.first_known_index())
) {
logger.info(
"Upgrading trust of existing megolm session " +
sessionId + " based on newly-received trusted session",
);
existingSessionData.untrusted = false;
this.cryptoStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, existingSessionData, txn,
);
} else {
logger.warn(
"Newly-received megolm session " + sessionId +
" does not match existing session! Keeping existing session",
);
}
return;
}
// If the sessions have the same index, go ahead and store the new trusted one.
}
} }
logger.info( logger.info(
@@ -1427,13 +1454,23 @@ export class OlmDevice {
const claimedKeys = sessionData.keysClaimed || {}; const claimedKeys = sessionData.keysClaimed || {};
const senderEd25519Key = claimedKeys.ed25519 || null; const senderEd25519Key = claimedKeys.ed25519 || null;
const forwardingKeyChain = sessionData.forwardingCurve25519KeyChain || [];
// older forwarded keys didn't set the "untrusted"
// property, but can be identified by having a
// non-empty forwarding key chain. These keys should
// be marked as untrusted since we don't know that they
// can be trusted
const untrusted = "untrusted" in sessionData
? sessionData.untrusted
: forwardingKeyChain.length > 0;
result = { result = {
"chain_index": chainIndex, "chain_index": chainIndex,
"key": exportedSession, "key": exportedSession,
"forwarding_curve25519_key_chain": "forwarding_curve25519_key_chain": forwardingKeyChain,
sessionData.forwardingCurve25519KeyChain || [],
"sender_claimed_ed25519_key": senderEd25519Key, "sender_claimed_ed25519_key": senderEd25519Key,
"shared_history": sessionData.sharedHistory || false, "shared_history": sessionData.sharedHistory || false,
"untrusted": untrusted,
}; };
}, },
); );

View File

@@ -539,7 +539,23 @@ export class SecretStorage {
// because someone could be trying to send us bogus data // because someone could be trying to send us bogus data
return; return;
} }
if (!olmlib.isOlmEncrypted(event)) {
logger.error("secret event not properly encrypted");
return;
}
const content = event.getContent(); const content = event.getContent();
const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(
olmlib.OLM_ALGORITHM,
event.getSenderKey() || "",
);
if (senderKeyUser !== event.getSender()) {
logger.error("sending device does not belong to the user it claims to be from");
return;
}
logger.log("got secret share for request", content.request_id); logger.log("got secret share for request", content.request_id);
const requestControl = this.requests.get(content.request_id); const requestControl = this.requests.get(content.request_id);
if (requestControl) { if (requestControl) {
@@ -559,6 +575,14 @@ export class SecretStorage {
logger.log("unsolicited secret share from device", deviceInfo.deviceId); logger.log("unsolicited secret share from device", deviceInfo.deviceId);
return; return;
} }
// unsure that the sender is trusted. In theory, this check is
// unnecessary since we only accept secret shares from devices that
// we requested from, but it doesn't hurt.
const deviceTrust = this.baseApis.crypto.checkDeviceInfoTrust(event.getSender(), deviceInfo);
if (!deviceTrust.isVerified()) {
logger.log("secret share from unverified device");
return;
}
logger.log( logger.log(
`Successfully received secret ${requestControl.name} ` + `Successfully received secret ${requestControl.name} ` +

View File

@@ -23,7 +23,7 @@ limitations under the License.
import { MatrixClient } from "../../client"; import { MatrixClient } from "../../client";
import { Room } from "../../models/room"; import { Room } from "../../models/room";
import { OlmDevice } from "../OlmDevice"; import { OlmDevice } from "../OlmDevice";
import { MatrixEvent, RoomMember } from "../.."; import { MatrixEvent, RoomMember } from "../../matrix";
import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from ".."; import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "..";
import { DeviceInfo } from "../deviceinfo"; import { DeviceInfo } from "../deviceinfo";
import { IRoomEncryption } from "../RoomList"; import { IRoomEncryption } from "../RoomList";
@@ -34,7 +34,7 @@ import { IRoomEncryption } from "../RoomList";
* *
* @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>} * @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>}
*/ */
export const ENCRYPTION_CLASSES: Record<string, new (params: IParams) => EncryptionAlgorithm> = {}; export const ENCRYPTION_CLASSES = new Map<string, new (params: IParams) => EncryptionAlgorithm>();
type DecryptionClassParams = Omit<IParams, "deviceId" | "config">; type DecryptionClassParams = Omit<IParams, "deviceId" | "config">;
@@ -44,7 +44,7 @@ type DecryptionClassParams = Omit<IParams, "deviceId" | "config">;
* *
* @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>} * @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
*/ */
export const DECRYPTION_CLASSES: Record<string, new (params: DecryptionClassParams) => DecryptionAlgorithm> = {}; export const DECRYPTION_CLASSES = new Map<string, new (params: DecryptionClassParams) => DecryptionAlgorithm>();
export interface IParams { export interface IParams {
userId: string; userId: string;
@@ -297,6 +297,6 @@ export function registerAlgorithm(
encryptor: new (params: IParams) => EncryptionAlgorithm, encryptor: new (params: IParams) => EncryptionAlgorithm,
decryptor: new (params: Omit<IParams, "deviceId">) => DecryptionAlgorithm, decryptor: new (params: Omit<IParams, "deviceId">) => DecryptionAlgorithm,
): void { ): void {
ENCRYPTION_CLASSES[algorithm] = encryptor; ENCRYPTION_CLASSES.set(algorithm, encryptor);
DECRYPTION_CLASSES[algorithm] = decryptor; DECRYPTION_CLASSES.set(algorithm, decryptor);
} }

View File

@@ -35,8 +35,10 @@ import { Room } from '../../models/room';
import { DeviceInfo } from "../deviceinfo"; import { DeviceInfo } from "../deviceinfo";
import { IOlmSessionResult } from "../olmlib"; import { IOlmSessionResult } from "../olmlib";
import { DeviceInfoMap } from "../DeviceList"; import { DeviceInfoMap } from "../DeviceList";
import { MatrixEvent } from "../.."; import { MatrixEvent } from "../../models/event";
import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index"; import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index";
import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager';
import { OlmGroupSessionExtraData } from "../../@types/crypto";
// determine whether the key can be shared with invitees // determine whether the key can be shared with invitees
export function isRoomSharedHistory(room: Room): boolean { export function isRoomSharedHistory(room: Room): boolean {
@@ -1189,9 +1191,10 @@ class MegolmEncryption extends EncryptionAlgorithm {
* {@link module:crypto/algorithms/DecryptionAlgorithm} * {@link module:crypto/algorithms/DecryptionAlgorithm}
*/ */
class MegolmDecryption extends DecryptionAlgorithm { class MegolmDecryption extends DecryptionAlgorithm {
// events which we couldn't decrypt due to unknown sessions / indexes: map from // events which we couldn't decrypt due to unknown sessions /
// senderKey|sessionId to Set of MatrixEvents // indexes, or which we could only decrypt with untrusted keys:
private pendingEvents: Record<string, Map<string, Set<MatrixEvent>>> = {}; // map from senderKey|sessionId to Set of MatrixEvents
private pendingEvents = new Map<string, Map<string, Set<MatrixEvent>>>();
// this gets stubbed out by the unit tests. // this gets stubbed out by the unit tests.
private olmlib = olmlib; private olmlib = olmlib;
@@ -1294,9 +1297,13 @@ class MegolmDecryption extends DecryptionAlgorithm {
); );
} }
// success. We can remove the event from the pending list, if that hasn't // Success. We can remove the event from the pending list, if
// already happened. // that hasn't already happened. However, if the event was
// decrypted with an untrusted key, leave it on the pending
// list so it will be retried if we find a trusted key later.
if (!res.untrusted) {
this.removeEventFromPendingList(event); this.removeEventFromPendingList(event);
}
const payload = JSON.parse(res.result); const payload = JSON.parse(res.result);
@@ -1343,10 +1350,10 @@ class MegolmDecryption extends DecryptionAlgorithm {
const content = event.getWireContent(); const content = event.getWireContent();
const senderKey = content.sender_key; const senderKey = content.sender_key;
const sessionId = content.session_id; const sessionId = content.session_id;
if (!this.pendingEvents[senderKey]) { if (!this.pendingEvents.has(senderKey)) {
this.pendingEvents[senderKey] = new Map(); this.pendingEvents.set(senderKey, new Map<string, Set<MatrixEvent>>());
} }
const senderPendingEvents = this.pendingEvents[senderKey]; const senderPendingEvents = this.pendingEvents.get(senderKey);
if (!senderPendingEvents.has(sessionId)) { if (!senderPendingEvents.has(sessionId)) {
senderPendingEvents.set(sessionId, new Set()); senderPendingEvents.set(sessionId, new Set());
} }
@@ -1364,7 +1371,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
const content = event.getWireContent(); const content = event.getWireContent();
const senderKey = content.sender_key; const senderKey = content.sender_key;
const sessionId = content.session_id; const sessionId = content.session_id;
const senderPendingEvents = this.pendingEvents[senderKey]; const senderPendingEvents = this.pendingEvents.get(senderKey);
const pendingEvents = senderPendingEvents?.get(sessionId); const pendingEvents = senderPendingEvents?.get(sessionId);
if (!pendingEvents) { if (!pendingEvents) {
return; return;
@@ -1375,7 +1382,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
senderPendingEvents.delete(sessionId); senderPendingEvents.delete(sessionId);
} }
if (senderPendingEvents.size === 0) { if (senderPendingEvents.size === 0) {
delete this.pendingEvents[senderKey]; this.pendingEvents.delete(senderKey);
} }
} }
@@ -1391,6 +1398,8 @@ class MegolmDecryption extends DecryptionAlgorithm {
let exportFormat = false; let exportFormat = false;
let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>; let keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>;
const extraSessionData: OlmGroupSessionExtraData = {};
if (!content.room_id || if (!content.room_id ||
!content.session_key || !content.session_key ||
!content.session_id || !content.session_id ||
@@ -1400,12 +1409,59 @@ class MegolmDecryption extends DecryptionAlgorithm {
return; return;
} }
if (!senderKey) { if (!olmlib.isOlmEncrypted(event)) {
logger.error("key event has no sender key (not encrypted?)"); logger.error("key event not properly encrypted");
return; return;
} }
if (content["org.matrix.msc3061.shared_history"]) {
extraSessionData.sharedHistory = true;
}
if (event.getType() == "m.forwarded_room_key") { if (event.getType() == "m.forwarded_room_key") {
const deviceInfo = this.crypto.deviceList.getDeviceByIdentityKey(
olmlib.OLM_ALGORITHM,
senderKey,
);
const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(
olmlib.OLM_ALGORITHM,
senderKey,
);
if (senderKeyUser !== event.getSender()) {
logger.error("sending device does not belong to the user it claims to be from");
return;
}
const outgoingRequests = deviceInfo ? await this.crypto.cryptoStore.getOutgoingRoomKeyRequestsByTarget(
event.getSender(), deviceInfo.deviceId, [RoomKeyRequestState.Sent],
) : [];
const weRequested = outgoingRequests.some((req) => (
req.requestBody.room_id === content.room_id && req.requestBody.session_id === content.session_id
));
const room = this.baseApis.getRoom(content.room_id);
const memberEvent = room?.getMember(this.userId)?.events.member;
const fromInviter = memberEvent?.getSender() === event.getSender() ||
(memberEvent?.getUnsigned()?.prev_sender === event.getSender() &&
memberEvent?.getPrevContent()?.membership === "invite");
const fromUs = event.getSender() === this.baseApis.getUserId();
if (!weRequested && !fromUs) {
// If someone sends us an unsolicited key and they're
// not one of our other devices and it's not shared
// history, ignore it
if (!extraSessionData.sharedHistory) {
logger.log("forwarded key not shared history - ignoring");
return;
}
// If someone sends us an unsolicited key for a room
// we're already in, and they're not one of our other
// devices or the one who invited us, ignore it
if (room && !fromInviter) {
logger.log("forwarded key not from inviter or from us - ignoring");
return;
}
}
exportFormat = true; exportFormat = true;
forwardingKeyChain = Array.isArray(content.forwarding_curve25519_key_chain) ? forwardingKeyChain = Array.isArray(content.forwarding_curve25519_key_chain) ?
content.forwarding_curve25519_key_chain : []; content.forwarding_curve25519_key_chain : [];
@@ -1418,7 +1474,6 @@ class MegolmDecryption extends DecryptionAlgorithm {
logger.error("forwarded_room_key event is missing sender_key field"); logger.error("forwarded_room_key event is missing sender_key field");
return; return;
} }
senderKey = content.sender_key;
const ed25519Key = content.sender_claimed_ed25519_key; const ed25519Key = content.sender_claimed_ed25519_key;
if (!ed25519Key) { if (!ed25519Key) {
@@ -1431,11 +1486,45 @@ class MegolmDecryption extends DecryptionAlgorithm {
keysClaimed = { keysClaimed = {
ed25519: ed25519Key, ed25519: ed25519Key,
}; };
// If this is a key for a room we're not in, don't load it
// yet, just park it in case *this sender* invites us to
// that room later
if (!room) {
const parkedData = {
senderId: event.getSender(),
senderKey: content.sender_key,
sessionId: content.session_id,
sessionKey: content.session_key,
keysClaimed,
forwardingCurve25519KeyChain: forwardingKeyChain,
};
await this.crypto.cryptoStore.doTxn(
'readwrite',
['parked_shared_history'],
(txn) => this.crypto.cryptoStore.addParkedSharedHistory(content.room_id, parkedData, txn),
logger.withPrefix("[addParkedSharedHistory]"),
);
return;
}
const sendingDevice = this.crypto.deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, senderKey);
const deviceTrust = this.crypto.checkDeviceInfoTrust(event.getSender(), sendingDevice);
if (fromUs && !deviceTrust.isVerified()) {
return;
}
// forwarded keys are always untrusted
extraSessionData.untrusted = true;
// replace the sender key with the sender key of the session
// creator for storage
senderKey = content.sender_key;
} else { } else {
keysClaimed = event.getKeysClaimed(); keysClaimed = event.getKeysClaimed();
} }
const extraSessionData: any = {};
if (content["org.matrix.msc3061.shared_history"]) { if (content["org.matrix.msc3061.shared_history"]) {
extraSessionData.sharedHistory = true; extraSessionData.sharedHistory = true;
} }
@@ -1453,7 +1542,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
); );
// have another go at decrypting events sent with this session. // have another go at decrypting events sent with this session.
if (await this.retryDecryption(senderKey, content.session_id)) { if (await this.retryDecryption(senderKey, content.session_id, !extraSessionData.untrusted)) {
// cancel any outstanding room key requests for this session. // cancel any outstanding room key requests for this session.
// Only do this if we managed to decrypt every message in the // Only do this if we managed to decrypt every message in the
// session, because if we didn't, we leave the other key // session, because if we didn't, we leave the other key
@@ -1668,7 +1757,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
session: IMegolmSessionData, session: IMegolmSessionData,
opts: { untrusted?: boolean, source?: string } = {}, opts: { untrusted?: boolean, source?: string } = {},
): Promise<void> { ): Promise<void> {
const extraSessionData: any = {}; const extraSessionData: OlmGroupSessionExtraData = {};
if (opts.untrusted || session.untrusted) { if (opts.untrusted || session.untrusted) {
extraSessionData.untrusted = true; extraSessionData.untrusted = true;
} }
@@ -1696,7 +1785,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
}); });
} }
// have another go at decrypting events sent with this session. // have another go at decrypting events sent with this session.
this.retryDecryption(session.sender_key, session.session_id); this.retryDecryption(session.sender_key, session.session_id, !extraSessionData.untrusted);
}); });
} }
@@ -1707,11 +1796,18 @@ class MegolmDecryption extends DecryptionAlgorithm {
* @private * @private
* @param {String} senderKey * @param {String} senderKey
* @param {String} sessionId * @param {String} sessionId
* @param {Boolean} forceRedecryptIfUntrusted whether messages that were already
* successfully decrypted using untrusted keys should be re-decrypted
* *
* @return {Boolean} whether all messages were successfully decrypted * @return {Boolean} whether all messages were successfully
* decrypted with trusted keys
*/ */
private async retryDecryption(senderKey: string, sessionId: string): Promise<boolean> { private async retryDecryption(
const senderPendingEvents = this.pendingEvents[senderKey]; senderKey: string,
sessionId: string,
forceRedecryptIfUntrusted?: boolean,
): Promise<boolean> {
const senderPendingEvents = this.pendingEvents.get(senderKey);
if (!senderPendingEvents) { if (!senderPendingEvents) {
return true; return true;
} }
@@ -1725,23 +1821,24 @@ class MegolmDecryption extends DecryptionAlgorithm {
await Promise.all([...pending].map(async (ev) => { await Promise.all([...pending].map(async (ev) => {
try { try {
await ev.attemptDecryption(this.crypto, { isRetry: true }); await ev.attemptDecryption(this.crypto, { isRetry: true, forceRedecryptIfUntrusted });
} catch (e) { } catch (e) {
// don't die if something goes wrong // don't die if something goes wrong
} }
})); }));
// If decrypted successfully, they'll have been removed from pendingEvents // If decrypted successfully with trusted keys, they'll have
return !this.pendingEvents[senderKey]?.has(sessionId); // been removed from pendingEvents
return !this.pendingEvents.get(senderKey)?.has(sessionId);
} }
public async retryDecryptionFromSender(senderKey: string): Promise<boolean> { public async retryDecryptionFromSender(senderKey: string): Promise<boolean> {
const senderPendingEvents = this.pendingEvents[senderKey]; const senderPendingEvents = this.pendingEvents.get(senderKey);
if (!senderPendingEvents) { if (!senderPendingEvents) {
return true; return true;
} }
delete this.pendingEvents[senderKey]; this.pendingEvents.delete(senderKey);
await Promise.all([...senderPendingEvents].map(async ([_sessionId, pending]) => { await Promise.all([...senderPendingEvents].map(async ([_sessionId, pending]) => {
await Promise.all([...pending].map(async (ev) => { await Promise.all([...pending].map(async (ev) => {
@@ -1753,7 +1850,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
})); }));
})); }));
return !this.pendingEvents[senderKey]; return !this.pendingEvents.has(senderKey);
} }
public async sendSharedHistoryInboundSessions(devicesByUser: Record<string, DeviceInfo[]>): Promise<void> { public async sendSharedHistoryInboundSessions(devicesByUser: Record<string, DeviceInfo[]>): Promise<void> {

View File

@@ -30,7 +30,7 @@ import {
registerAlgorithm, registerAlgorithm,
} from "./base"; } from "./base";
import { Room } from '../../models/room'; import { Room } from '../../models/room';
import { MatrixEvent } from "../.."; import { MatrixEvent } from "../../models/event";
import { IEventDecryptionResult } from "../index"; import { IEventDecryptionResult } from "../index";
import { IInboundSession } from "../OlmDevice"; import { IInboundSession } from "../OlmDevice";
@@ -222,6 +222,26 @@ class OlmDecryption extends DecryptionAlgorithm {
); );
} }
// check that the device that encrypted the event belongs to the user
// that the event claims it's from. We need to make sure that our
// device list is up-to-date. If the device is unknown, we can only
// assume that the device logged out. Some event handlers, such as
// secret sharing, may be more strict and reject events that come from
// unknown devices.
await this.crypto.deviceList.downloadKeys([event.getSender()], false);
const senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(
olmlib.OLM_ALGORITHM,
deviceKey,
);
if (senderKeyUser !== event.getSender() && senderKeyUser !== undefined) {
throw new DecryptionError(
"OLM_BAD_SENDER",
"Message claimed to be from " + event.getSender(), {
real_sender: senderKeyUser,
},
);
}
// check that the original sender matches what the homeserver told us, to // check that the original sender matches what the homeserver told us, to
// avoid people masquerading as others. // avoid people masquerading as others.
// (this check is also provided via the sender's embedded ed25519 key, // (this check is also provided via the sender's embedded ed25519 key,

View File

@@ -431,7 +431,6 @@ export class BackupManager {
) )
); );
}); });
ret.usable = ret.usable || ret.trusted_locally;
return ret; return ret;
} }

View File

@@ -278,9 +278,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
private oneTimeKeyCheckInProgress = false; private oneTimeKeyCheckInProgress = false;
// EncryptionAlgorithm instance for each room // EncryptionAlgorithm instance for each room
private roomEncryptors: Record<string, EncryptionAlgorithm> = {}; private roomEncryptors = new Map<string, EncryptionAlgorithm>();
// map from algorithm to DecryptionAlgorithm instance, for each room // map from algorithm to DecryptionAlgorithm instance, for each room
private roomDecryptors: Record<string, Record<string, DecryptionAlgorithm>> = {}; private roomDecryptors = new Map<string, Map<string, DecryptionAlgorithm>>();
private deviceKeys: Record<string, string> = {}; // type: key private deviceKeys: Record<string, string> = {}; // type: key
@@ -422,7 +422,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
this.deviceList.on(CryptoEvent.UserCrossSigningUpdated, this.onDeviceListUserCrossSigningUpdated); this.deviceList.on(CryptoEvent.UserCrossSigningUpdated, this.onDeviceListUserCrossSigningUpdated);
this.reEmitter.reEmit(this.deviceList, [CryptoEvent.DevicesUpdated, CryptoEvent.WillUpdateDevices]); this.reEmitter.reEmit(this.deviceList, [CryptoEvent.DevicesUpdated, CryptoEvent.WillUpdateDevices]);
this.supportedAlgorithms = Object.keys(algorithms.DECRYPTION_CLASSES); this.supportedAlgorithms = Array.from(algorithms.DECRYPTION_CLASSES.keys());
this.outgoingRoomKeyRequestManager = new OutgoingRoomKeyRequestManager( this.outgoingRoomKeyRequestManager = new OutgoingRoomKeyRequestManager(
baseApis, this.deviceId, this.cryptoStore, baseApis, this.deviceId, this.cryptoStore,
@@ -2105,6 +2105,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @param {?boolean} known whether to mark that the user has been made aware of * @param {?boolean} known whether to mark that the user has been made aware of
* the existence of this device. Null to leave unchanged * the existence of this device. Null to leave unchanged
* *
* @param {?Record<string, any>} keys The list of keys that was present
* during the device verification. This will be double checked with the list
* of keys the given device has currently.
*
* @return {Promise<module:crypto/deviceinfo>} updated DeviceInfo * @return {Promise<module:crypto/deviceinfo>} updated DeviceInfo
*/ */
public async setDeviceVerification( public async setDeviceVerification(
@@ -2113,6 +2117,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
verified?: boolean, verified?: boolean,
blocked?: boolean, blocked?: boolean,
known?: boolean, known?: boolean,
keys?: Record<string, string>,
): Promise<DeviceInfo | CrossSigningInfo> { ): Promise<DeviceInfo | CrossSigningInfo> {
// get rid of any `undefined`s here so we can just check // get rid of any `undefined`s here so we can just check
// for null rather than null or undefined // for null rather than null or undefined
@@ -2131,6 +2136,10 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
if (!verified) { if (!verified) {
throw new Error("Cannot set a cross-signing key as unverified"); throw new Error("Cannot set a cross-signing key as unverified");
} }
const gotKeyId = keys ? Object.values(keys)[0] : null;
if (keys && (Object.values(keys).length !== 1 || gotKeyId !== xsk.getId())) {
throw new Error(`Key did not match expected value: expected ${xsk.getId()}, got ${gotKeyId}`);
}
if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) { if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) {
this.storeTrustedSelfKeys(xsk.keys); this.storeTrustedSelfKeys(xsk.keys);
@@ -2191,6 +2200,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
let verificationStatus = dev.verified; let verificationStatus = dev.verified;
if (verified) { if (verified) {
if (keys) {
for (const [keyId, key] of Object.entries(keys)) {
if (dev.keys[keyId] !== key) {
throw new Error(`Key did not match expected value: expected ${key}, got ${dev.keys[keyId]}`);
}
}
}
verificationStatus = DeviceVerification.VERIFIED; verificationStatus = DeviceVerification.VERIFIED;
} else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) { } else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) {
verificationStatus = DeviceVerification.UNVERIFIED; verificationStatus = DeviceVerification.UNVERIFIED;
@@ -2400,13 +2416,6 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return null; return null;
} }
const forwardingChain = event.getForwardingCurve25519KeyChain();
if (forwardingChain.length > 0) {
// we got the key this event from somewhere else
// TODO: check if we can trust the forwarders.
return null;
}
if (event.isKeySourceUntrusted()) { if (event.isKeySourceUntrusted()) {
// we got the key for this event from a source that we consider untrusted // we got the key for this event from a source that we consider untrusted
return null; return null;
@@ -2478,8 +2487,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
} }
ret.encrypted = true; ret.encrypted = true;
const forwardingChain = event.getForwardingCurve25519KeyChain(); if (event.isKeySourceUntrusted()) {
if (forwardingChain.length > 0 || event.isKeySourceUntrusted()) {
// we got the key this event from somewhere else // we got the key this event from somewhere else
// TODO: check if we can trust the forwarders. // TODO: check if we can trust the forwarders.
ret.authenticated = false; ret.authenticated = false;
@@ -2527,7 +2535,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* This should not normally be necessary. * This should not normally be necessary.
*/ */
public forceDiscardSession(roomId: string): void { public forceDiscardSession(roomId: string): void {
const alg = this.roomEncryptors[roomId]; const alg = this.roomEncryptors.get(roomId);
if (alg === undefined) throw new Error("Room not encrypted"); if (alg === undefined) throw new Error("Room not encrypted");
if (alg.forceDiscardSession === undefined) { if (alg.forceDiscardSession === undefined) {
throw new Error("Room encryption algorithm doesn't support session discarding"); throw new Error("Room encryption algorithm doesn't support session discarding");
@@ -2580,7 +2588,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// the encryption event would appear in both. // the encryption event would appear in both.
// If it's called more than twice though, // If it's called more than twice though,
// it signals a bug on client or server. // it signals a bug on client or server.
const existingAlg = this.roomEncryptors[roomId]; const existingAlg = this.roomEncryptors.get(roomId);
if (existingAlg) { if (existingAlg) {
return; return;
} }
@@ -2594,7 +2602,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
storeConfigPromise = this.roomList.setRoomEncryption(roomId, config); storeConfigPromise = this.roomList.setRoomEncryption(roomId, config);
} }
const AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm]; const AlgClass = algorithms.ENCRYPTION_CLASSES.get(config.algorithm);
if (!AlgClass) { if (!AlgClass) {
throw new Error("Unable to encrypt with " + config.algorithm); throw new Error("Unable to encrypt with " + config.algorithm);
} }
@@ -2608,7 +2616,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
roomId, roomId,
config, config,
}); });
this.roomEncryptors[roomId] = alg; this.roomEncryptors.set(roomId, alg);
if (storeConfigPromise) { if (storeConfigPromise) {
await storeConfigPromise; await storeConfigPromise;
@@ -2640,7 +2648,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
public trackRoomDevices(roomId: string): Promise<void> { public trackRoomDevices(roomId: string): Promise<void> {
const trackMembers = async () => { const trackMembers = async () => {
// not an encrypted room // not an encrypted room
if (!this.roomEncryptors[roomId]) { if (!this.roomEncryptors.has(roomId)) {
return; return;
} }
const room = this.clientStore.getRoom(roomId); const room = this.clientStore.getRoom(roomId);
@@ -2785,7 +2793,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @param {module:models/room} room the room the event is in * @param {module:models/room} room the room the event is in
*/ */
public prepareToEncrypt(room: Room): void { public prepareToEncrypt(room: Room): void {
const alg = this.roomEncryptors[room.roomId]; const alg = this.roomEncryptors.get(room.roomId);
if (alg) { if (alg) {
alg.prepareToEncrypt(room); alg.prepareToEncrypt(room);
} }
@@ -2808,7 +2816,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
const roomId = event.getRoomId(); const roomId = event.getRoomId();
const alg = this.roomEncryptors[roomId]; const alg = this.roomEncryptors.get(roomId);
if (!alg) { if (!alg) {
// MatrixClient has already checked that this room should be encrypted, // MatrixClient has already checked that this room should be encrypted,
// so this is an unexpected situation. // so this is an unexpected situation.
@@ -3097,7 +3105,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
private getTrackedE2eRooms(): Room[] { private getTrackedE2eRooms(): Room[] {
return this.clientStore.getRooms().filter((room) => { return this.clientStore.getRooms().filter((room) => {
// check for rooms with encryption enabled // check for rooms with encryption enabled
const alg = this.roomEncryptors[room.roomId]; const alg = this.roomEncryptors.get(room.roomId);
if (!alg) { if (!alg) {
return false; return false;
} }
@@ -3533,7 +3541,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
const roomId = member.roomId; const roomId = member.roomId;
const alg = this.roomEncryptors[roomId]; const alg = this.roomEncryptors.get(roomId);
if (!alg) { if (!alg) {
// not encrypting in this room // not encrypting in this room
return; return;
@@ -3634,11 +3642,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
` for ${roomId} / ${body.session_id} (id ${req.requestId})`); ` for ${roomId} / ${body.session_id} (id ${req.requestId})`);
if (userId !== this.userId) { if (userId !== this.userId) {
if (!this.roomEncryptors[roomId]) { if (!this.roomEncryptors.get(roomId)) {
logger.debug(`room key request for unencrypted room ${roomId}`); logger.debug(`room key request for unencrypted room ${roomId}`);
return; return;
} }
const encryptor = this.roomEncryptors[roomId]; const encryptor = this.roomEncryptors.get(roomId);
const device = this.deviceList.getStoredDevice(userId, deviceId); const device = this.deviceList.getStoredDevice(userId, deviceId);
if (!device) { if (!device) {
logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`); logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`);
@@ -3674,12 +3682,12 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// if we don't have a decryptor for this room/alg, we don't have // if we don't have a decryptor for this room/alg, we don't have
// the keys for the requested events, and can drop the requests. // the keys for the requested events, and can drop the requests.
if (!this.roomDecryptors[roomId]) { if (!this.roomDecryptors.has(roomId)) {
logger.log(`room key request for unencrypted room ${roomId}`); logger.log(`room key request for unencrypted room ${roomId}`);
return; return;
} }
const decryptor = this.roomDecryptors[roomId][alg]; const decryptor = this.roomDecryptors.get(roomId).get(alg);
if (!decryptor) { if (!decryptor) {
logger.log(`room key request for unknown alg ${alg} in room ${roomId}`); logger.log(`room key request for unknown alg ${alg} in room ${roomId}`);
return; return;
@@ -3745,23 +3753,24 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* unknown * unknown
*/ */
public getRoomDecryptor(roomId: string, algorithm: string): DecryptionAlgorithm { public getRoomDecryptor(roomId: string, algorithm: string): DecryptionAlgorithm {
let decryptors: Record<string, DecryptionAlgorithm>; let decryptors: Map<string, DecryptionAlgorithm>;
let alg: DecryptionAlgorithm; let alg: DecryptionAlgorithm;
roomId = roomId || null; roomId = roomId || null;
if (roomId) { if (roomId) {
decryptors = this.roomDecryptors[roomId]; decryptors = this.roomDecryptors.get(roomId);
if (!decryptors) { if (!decryptors) {
this.roomDecryptors[roomId] = decryptors = {}; decryptors = new Map<string, DecryptionAlgorithm>();
this.roomDecryptors.set(roomId, decryptors);
} }
alg = decryptors[algorithm]; alg = decryptors.get(algorithm);
if (alg) { if (alg) {
return alg; return alg;
} }
} }
const AlgClass = algorithms.DECRYPTION_CLASSES[algorithm]; const AlgClass = algorithms.DECRYPTION_CLASSES.get(algorithm);
if (!AlgClass) { if (!AlgClass) {
throw new algorithms.DecryptionError( throw new algorithms.DecryptionError(
'UNKNOWN_ENCRYPTION_ALGORITHM', 'UNKNOWN_ENCRYPTION_ALGORITHM',
@@ -3777,7 +3786,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}); });
if (decryptors) { if (decryptors) {
decryptors[algorithm] = alg; decryptors.set(algorithm, alg);
} }
return alg; return alg;
} }
@@ -3791,9 +3800,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
*/ */
private getRoomDecryptors(algorithm: string): DecryptionAlgorithm[] { private getRoomDecryptors(algorithm: string): DecryptionAlgorithm[] {
const decryptors = []; const decryptors = [];
for (const d of Object.values(this.roomDecryptors)) { for (const d of this.roomDecryptors.values()) {
if (algorithm in d) { if (d.has(algorithm)) {
decryptors.push(d[algorithm]); decryptors.push(d.get(algorithm));
} }
} }
return decryptors; return decryptors;

View File

@@ -30,6 +30,8 @@ import { logger } from '../logger';
import { IOneTimeKey } from "./dehydration"; import { IOneTimeKey } from "./dehydration";
import { IClaimOTKsResult, MatrixClient } from "../client"; import { IClaimOTKsResult, MatrixClient } from "../client";
import { ISignatures } from "../@types/signed"; import { ISignatures } from "../@types/signed";
import { MatrixEvent } from "../models/event";
import { EventType } from "../@types/event";
enum Algorithm { enum Algorithm {
Olm = "m.olm.v1.curve25519-aes-sha2", Olm = "m.olm.v1.curve25519-aes-sha2",
@@ -554,6 +556,22 @@ export function pkVerify(obj: IObject, pubKey: string, userId: string) {
} }
} }
/**
* Check that an event was encrypted using olm.
*/
export function isOlmEncrypted(event: MatrixEvent): boolean {
if (!event.getSenderKey()) {
logger.error("Event has no sender key (not encrypted?)");
return false;
}
if (event.getWireType() !== EventType.RoomMessageEncrypted ||
!(["m.olm.v1.curve25519-aes-sha2"].includes(event.getWireContent().algorithm))) {
logger.error("Event was not encrypted using an appropriate algorithm");
return false;
}
return true;
}
/** /**
* Encode a typed array of uint8 as base64. * Encode a typed array of uint8 as base64.
* @param {Uint8Array} uint8Array The data to encode. * @param {Uint8Array} uint8Array The data to encode.

View File

@@ -25,6 +25,7 @@ import { ICrossSigningInfo } from "../CrossSigning";
import { PrefixedLogger } from "../../logger"; import { PrefixedLogger } from "../../logger";
import { InboundGroupSessionData } from "../OlmDevice"; import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes"; import { IEncryptedPayload } from "../aes";
import { MatrixEvent } from "../../models/event";
/** /**
* Internal module. Definitions for storage for the crypto module * Internal module. Definitions for storage for the crypto module
@@ -127,6 +128,8 @@ export interface CryptoStore {
roomId: string, roomId: string,
txn?: unknown, txn?: unknown,
): Promise<[senderKey: string, sessionId: string][]>; ): Promise<[senderKey: string, sessionId: string][]>;
addParkedSharedHistory(roomId: string, data: ParkedSharedHistory, txn?: unknown): void;
takeParkedSharedHistory(roomId: string, txn?: unknown): Promise<ParkedSharedHistory[]>;
// Session key backups // Session key backups
doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>; doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn: unknown) => T, log?: PrefixedLogger): Promise<T>;
@@ -203,3 +206,12 @@ export interface OutgoingRoomKeyRequest {
requestBody: IRoomKeyRequestBody; requestBody: IRoomKeyRequestBody;
state: RoomKeyRequestState; state: RoomKeyRequestState;
} }
export interface ParkedSharedHistory {
senderId: string;
senderKey: string;
sessionId: string;
sessionKey: string;
keysClaimed: ReturnType<MatrixEvent["getKeysClaimed"]>; // XXX: Less type dependence on MatrixEvent
forwardingCurve25519KeyChain: string[];
}

View File

@@ -25,15 +25,15 @@ import {
IWithheld, IWithheld,
Mode, Mode,
OutgoingRoomKeyRequest, OutgoingRoomKeyRequest,
ParkedSharedHistory,
} from "./base"; } from "./base";
import { IRoomKeyRequestBody } from "../index"; import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
import { ICrossSigningKey } from "../../client"; import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm"; import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList"; import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../OlmDevice"; import { InboundGroupSessionData } from "../OlmDevice";
import { IEncryptedPayload } from "../aes"; import { IEncryptedPayload } from "../aes";
export const VERSION = 10;
const PROFILE_TRANSACTIONS = false; const PROFILE_TRANSACTIONS = false;
/** /**
@@ -261,7 +261,9 @@ export class Backend implements CryptoStore {
const cursor = this.result; const cursor = this.result;
if (cursor) { if (cursor) {
const keyReq = cursor.value; const keyReq = cursor.value;
if (keyReq.recipients.includes({ userId, deviceId })) { if (keyReq.recipients.some((recipient: IRoomKeyRequestRecipient) =>
recipient.userId === userId && recipient.deviceId === deviceId,
)) {
results.push(keyReq); results.push(keyReq);
} }
cursor.continue(); cursor.continue();
@@ -871,6 +873,50 @@ export class Backend implements CryptoStore {
}); });
} }
public addParkedSharedHistory(
roomId: string,
parkedData: ParkedSharedHistory,
txn?: IDBTransaction,
): void {
if (!txn) {
txn = this.db.transaction(
"parked_shared_history", "readwrite",
);
}
const objectStore = txn.objectStore("parked_shared_history");
const req = objectStore.get([roomId]);
req.onsuccess = () => {
const { parked } = req.result || { parked: [] };
parked.push(parkedData);
objectStore.put({ roomId, parked });
};
}
public takeParkedSharedHistory(
roomId: string,
txn?: IDBTransaction,
): Promise<ParkedSharedHistory[]> {
if (!txn) {
txn = this.db.transaction(
"parked_shared_history", "readwrite",
);
}
const cursorReq = txn.objectStore("parked_shared_history").openCursor(roomId);
return new Promise((resolve, reject) => {
cursorReq.onsuccess = () => {
const cursor = cursorReq.result;
if (!cursor) {
resolve([]);
return;
}
const data = cursor.value;
cursor.delete();
resolve(data);
};
cursorReq.onerror = reject;
});
}
public doTxn<T>( public doTxn<T>(
mode: Mode, mode: Mode,
stores: string | string[], stores: string | string[],
@@ -903,45 +949,34 @@ export class Backend implements CryptoStore {
} }
} }
export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void { type DbMigration = (db: IDBDatabase) => void;
logger.log( const DB_MIGRATIONS: DbMigration[] = [
`Upgrading IndexedDBCryptoStore from version ${oldVersion}` (db) => { createDatabase(db); },
+ ` to ${VERSION}`, (db) => { db.createObjectStore("account"); },
); (db) => {
if (oldVersion < 1) { // The database did not previously exist.
createDatabase(db);
}
if (oldVersion < 2) {
db.createObjectStore("account");
}
if (oldVersion < 3) {
const sessionsStore = db.createObjectStore("sessions", { const sessionsStore = db.createObjectStore("sessions", {
keyPath: ["deviceKey", "sessionId"], keyPath: ["deviceKey", "sessionId"],
}); });
sessionsStore.createIndex("deviceKey", "deviceKey"); sessionsStore.createIndex("deviceKey", "deviceKey");
} },
if (oldVersion < 4) { (db) => {
db.createObjectStore("inbound_group_sessions", { db.createObjectStore("inbound_group_sessions", {
keyPath: ["senderCurve25519Key", "sessionId"], keyPath: ["senderCurve25519Key", "sessionId"],
}); });
} },
if (oldVersion < 5) { (db) => { db.createObjectStore("device_data"); },
db.createObjectStore("device_data"); (db) => { db.createObjectStore("rooms"); },
} (db) => {
if (oldVersion < 6) {
db.createObjectStore("rooms");
}
if (oldVersion < 7) {
db.createObjectStore("sessions_needing_backup", { db.createObjectStore("sessions_needing_backup", {
keyPath: ["senderCurve25519Key", "sessionId"], keyPath: ["senderCurve25519Key", "sessionId"],
}); });
} },
if (oldVersion < 8) { (db) => {
db.createObjectStore("inbound_group_sessions_withheld", { db.createObjectStore("inbound_group_sessions_withheld", {
keyPath: ["senderCurve25519Key", "sessionId"], keyPath: ["senderCurve25519Key", "sessionId"],
}); });
} },
if (oldVersion < 9) { (db) => {
const problemsStore = db.createObjectStore("session_problems", { const problemsStore = db.createObjectStore("session_problems", {
keyPath: ["deviceKey", "time"], keyPath: ["deviceKey", "time"],
}); });
@@ -950,13 +985,29 @@ export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
db.createObjectStore("notified_error_devices", { db.createObjectStore("notified_error_devices", {
keyPath: ["userId", "deviceId"], keyPath: ["userId", "deviceId"],
}); });
} },
if (oldVersion < 10) { (db) => {
db.createObjectStore("shared_history_inbound_group_sessions", { db.createObjectStore("shared_history_inbound_group_sessions", {
keyPath: ["roomId"], keyPath: ["roomId"],
}); });
} },
(db) => {
db.createObjectStore("parked_shared_history", {
keyPath: ["roomId"],
});
},
// Expand as needed. // Expand as needed.
];
export const VERSION = DB_MIGRATIONS.length;
export function upgradeDatabase(db: IDBDatabase, oldVersion: number): void {
logger.log(
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
+ ` to ${VERSION}`,
);
DB_MIGRATIONS.forEach((migration, index) => {
if (oldVersion <= index) migration(db);
});
} }
function createDatabase(db: IDBDatabase): void { function createDatabase(db: IDBDatabase): void {

View File

@@ -29,6 +29,7 @@ import {
IWithheld, IWithheld,
Mode, Mode,
OutgoingRoomKeyRequest, OutgoingRoomKeyRequest,
ParkedSharedHistory,
} from "./base"; } from "./base";
import { IRoomKeyRequestBody } from "../index"; import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client"; import { ICrossSigningKey } from "../../client";
@@ -55,6 +56,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
public static STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions'; public static STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
public static STORE_INBOUND_GROUP_SESSIONS_WITHHELD = 'inbound_group_sessions_withheld'; public static STORE_INBOUND_GROUP_SESSIONS_WITHHELD = 'inbound_group_sessions_withheld';
public static STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS = 'shared_history_inbound_group_sessions'; public static STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS = 'shared_history_inbound_group_sessions';
public static STORE_PARKED_SHARED_HISTORY = 'parked_shared_history';
public static STORE_DEVICE_DATA = 'device_data'; public static STORE_DEVICE_DATA = 'device_data';
public static STORE_ROOMS = 'rooms'; public static STORE_ROOMS = 'rooms';
public static STORE_BACKUP = 'sessions_needing_backup'; public static STORE_BACKUP = 'sessions_needing_backup';
@@ -669,6 +671,27 @@ export class IndexedDBCryptoStore implements CryptoStore {
return this.backend.getSharedHistoryInboundGroupSessions(roomId, txn); return this.backend.getSharedHistoryInboundGroupSessions(roomId, txn);
} }
/**
* Park a shared-history group session for a room we may be invited to later.
*/
public addParkedSharedHistory(
roomId: string,
parkedData: ParkedSharedHistory,
txn?: IDBTransaction,
): void {
this.backend.addParkedSharedHistory(roomId, parkedData, txn);
}
/**
* Pop out all shared-history group sessions for a room.
*/
public takeParkedSharedHistory(
roomId: string,
txn?: IDBTransaction,
): Promise<ParkedSharedHistory[]> {
return this.backend.takeParkedSharedHistory(roomId, txn);
}
/** /**
* Perform a transaction on the crypto store. Any store methods * Perform a transaction on the crypto store. Any store methods
* that require a transaction (txn) object to be passed in may * that require a transaction (txn) object to be passed in may

View File

@@ -25,6 +25,7 @@ import {
IWithheld, IWithheld,
Mode, Mode,
OutgoingRoomKeyRequest, OutgoingRoomKeyRequest,
ParkedSharedHistory,
} from "./base"; } from "./base";
import { IRoomKeyRequestBody } from "../index"; import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client"; import { ICrossSigningKey } from "../../client";
@@ -58,6 +59,7 @@ export class MemoryCryptoStore implements CryptoStore {
private rooms: { [roomId: string]: IRoomEncryption } = {}; private rooms: { [roomId: string]: IRoomEncryption } = {};
private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {}; private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {};
private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {}; private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {};
private parkedSharedHistory = new Map<string, ParkedSharedHistory[]>(); // keyed by room ID
/** /**
* Ensure the database exists and is up-to-date. * Ensure the database exists and is up-to-date.
@@ -191,11 +193,13 @@ export class MemoryCryptoStore implements CryptoStore {
deviceId: string, deviceId: string,
wantedStates: number[], wantedStates: number[],
): Promise<OutgoingRoomKeyRequest[]> { ): Promise<OutgoingRoomKeyRequest[]> {
const results = []; const results: OutgoingRoomKeyRequest[] = [];
for (const req of this.outgoingRoomKeyRequests) { for (const req of this.outgoingRoomKeyRequests) {
for (const state of wantedStates) { for (const state of wantedStates) {
if (req.state === state && req.recipients.includes({ userId, deviceId })) { if (req.state === state && req.recipients.some(
(recipient) => recipient.userId === userId && recipient.deviceId === deviceId,
)) {
results.push(req); results.push(req);
} }
} }
@@ -524,6 +528,18 @@ export class MemoryCryptoStore implements CryptoStore {
return Promise.resolve(this.sharedHistoryInboundGroupSessions[roomId] || []); return Promise.resolve(this.sharedHistoryInboundGroupSessions[roomId] || []);
} }
public addParkedSharedHistory(roomId: string, parkedData: ParkedSharedHistory): void {
const parked = this.parkedSharedHistory.get(roomId) ?? [];
parked.push(parkedData);
this.parkedSharedHistory.set(roomId, parked);
}
public takeParkedSharedHistory(roomId: string): Promise<ParkedSharedHistory[]> {
const parked = this.parkedSharedHistory.get(roomId) ?? [];
this.parkedSharedHistory.delete(roomId);
return Promise.resolve(parked);
}
// Session key backups // Session key backups
public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn?: unknown) => T): Promise<T> { public doTxn<T>(mode: Mode, stores: Iterable<string>, func: (txn?: unknown) => T): Promise<T> {

View File

@@ -299,7 +299,13 @@ export class VerificationBase<
if (this.doVerification && !this.started) { if (this.doVerification && !this.started) {
this.started = true; this.started = true;
this.resetTimer(); // restart the timeout this.resetTimer(); // restart the timeout
Promise.resolve(this.doVerification()).then(this.done.bind(this), this.cancel.bind(this)); new Promise<void>((resolve, reject) => {
const crossSignId = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(this.userId)?.getId();
if (crossSignId === this.deviceId) {
reject(new Error("Device ID is the same as the cross-signing ID"));
}
resolve();
}).then(() => this.doVerification()).then(this.done.bind(this), this.cancel.bind(this));
} }
return this.promise; return this.promise;
} }
@@ -310,14 +316,14 @@ export class VerificationBase<
// we try to verify all the keys that we're told about, but we might // we try to verify all the keys that we're told about, but we might
// not know about all of them, so keep track of the keys that we know // not know about all of them, so keep track of the keys that we know
// about, and ignore the rest // about, and ignore the rest
const verifiedDevices = []; const verifiedDevices: [string, string, string][] = [];
for (const [keyId, keyInfo] of Object.entries(keys)) { for (const [keyId, keyInfo] of Object.entries(keys)) {
const deviceId = keyId.split(':', 2)[1]; const deviceId = keyId.split(':', 2)[1];
const device = this.baseApis.getStoredDevice(userId, deviceId); const device = this.baseApis.getStoredDevice(userId, deviceId);
if (device) { if (device) {
verifier(keyId, device, keyInfo); verifier(keyId, device, keyInfo);
verifiedDevices.push(deviceId); verifiedDevices.push([deviceId, keyId, device.keys[keyId]]);
} else { } else {
const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId); const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId);
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) { if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
@@ -326,7 +332,7 @@ export class VerificationBase<
[keyId]: deviceId, [keyId]: deviceId,
}, },
}, deviceId), keyInfo); }, deviceId), keyInfo);
verifiedDevices.push(deviceId); verifiedDevices.push([deviceId, keyId, deviceId]);
} else { } else {
logger.warn( logger.warn(
`verification: Could not find device ${deviceId} to verify`, `verification: Could not find device ${deviceId} to verify`,
@@ -348,8 +354,15 @@ export class VerificationBase<
// TODO: There should probably be a batch version of this, otherwise it's going // TODO: There should probably be a batch version of this, otherwise it's going
// to upload each signature in a separate API call which is silly because the // to upload each signature in a separate API call which is silly because the
// API supports as many signatures as you like. // API supports as many signatures as you like.
for (const deviceId of verifiedDevices) { for (const [deviceId, keyId, key] of verifiedDevices) {
await this.baseApis.setDeviceVerified(userId, deviceId); await this.baseApis.crypto.setDeviceVerification(userId, deviceId, true, null, null, { [keyId]: key });
}
// if one of the user's own devices is being marked as verified / unverified,
// check the key backup status, since whether or not we use this depends on
// whether it has a signature from a verified device
if (userId == this.baseApis.credentials.userId) {
await this.baseApis.checkKeyBackup();
} }
} }

View File

@@ -22,6 +22,7 @@ export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
export interface MapperOpts { export interface MapperOpts {
preventReEmit?: boolean; preventReEmit?: boolean;
decrypt?: boolean; decrypt?: boolean;
toDevice?: boolean;
} }
export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper { export function eventMapperFor(client: MatrixClient, options: MapperOpts): EventMapper {
@@ -29,6 +30,10 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
const decrypt = options.decrypt !== false; const decrypt = options.decrypt !== false;
function mapper(plainOldJsObject: Partial<IEvent>) { function mapper(plainOldJsObject: Partial<IEvent>) {
if (options.toDevice) {
delete plainOldJsObject.room_id;
}
const room = client.getRoom(plainOldJsObject.room_id); const room = client.getRoom(plainOldJsObject.room_id);
let event: MatrixEvent; let event: MatrixEvent;

62
src/feature.ts Normal file
View 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;
}

View File

@@ -73,7 +73,7 @@ export interface IFilterComponent {
* @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true } * @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true }
*/ */
export class FilterComponent { export class FilterComponent {
constructor(private filterJson: IFilterComponent, public readonly userId?: string) {} constructor(private filterJson: IFilterComponent, public readonly userId?: string | undefined | null) {}
/** /**
* Checks with the filter component matches the given event * Checks with the filter component matches the given event

View File

@@ -22,6 +22,7 @@ import {
EventType, EventType,
RelationType, RelationType,
} from "./@types/event"; } from "./@types/event";
import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync";
import { FilterComponent, IFilterComponent } from "./filter-component"; import { FilterComponent, IFilterComponent } from "./filter-component";
import { MatrixEvent } from "./models/event"; import { MatrixEvent } from "./models/event";
@@ -57,6 +58,8 @@ export interface IRoomEventFilter extends IFilterComponent {
types?: Array<EventType | string>; types?: Array<EventType | string>;
related_by_senders?: Array<RelationType | string>; related_by_senders?: Array<RelationType | string>;
related_by_rel_types?: string[]; related_by_rel_types?: string[];
unread_thread_notifications?: boolean;
"org.matrix.msc3773.unread_thread_notifications"?: boolean;
// Unstable values // Unstable values
"io.element.relation_senders"?: Array<RelationType | string>; "io.element.relation_senders"?: Array<RelationType | string>;
@@ -97,7 +100,7 @@ export class Filter {
* @param {Object} jsonObj * @param {Object} jsonObj
* @return {Filter} * @return {Filter}
*/ */
public static fromJson(userId: string, filterId: string, jsonObj: IFilterDefinition): Filter { public static fromJson(userId: string | undefined | null, filterId: string, jsonObj: IFilterDefinition): Filter {
const filter = new Filter(userId, filterId); const filter = new Filter(userId, filterId);
filter.setDefinition(jsonObj); filter.setDefinition(jsonObj);
return filter; return filter;
@@ -107,7 +110,7 @@ export class Filter {
private roomFilter: FilterComponent; private roomFilter: FilterComponent;
private roomTimelineFilter: FilterComponent; private roomTimelineFilter: FilterComponent;
constructor(public readonly userId: string, public filterId?: string) {} constructor(public readonly userId: string | undefined | null, public filterId?: string) {}
/** /**
* Get the ID of this filter on your homeserver (if known) * Get the ID of this filter on your homeserver (if known)
@@ -220,7 +223,24 @@ export class Filter {
setProp(this.definition, "room.timeline.limit", limit); setProp(this.definition, "room.timeline.limit", limit);
} }
setLazyLoadMembers(enabled: boolean) { /**
* Enable threads unread notification
* @param {boolean} enabled
*/
public setUnreadThreadNotifications(enabled: boolean): void {
this.definition = {
...this.definition,
room: {
...this.definition?.room,
timeline: {
...this.definition?.room?.timeline,
[UNREAD_THREAD_NOTIFICATIONS.name]: !!enabled,
},
},
};
}
setLazyLoadMembers(enabled: boolean): void {
setProp(this.definition, "room.state.lazy_load_members", !!enabled); setProp(this.definition, "room.state.lazy_load_members", !!enabled);
} }

View File

@@ -365,7 +365,7 @@ export class MatrixHttpApi {
// we're setting opts.json=false so that it doesn't JSON-encode the // we're setting opts.json=false so that it doesn't JSON-encode the
// request, which also means it doesn't JSON-decode the response. Either // request, which also means it doesn't JSON-decode the response. Either
// way, we have to JSON-parse the response ourselves. // way, we have to JSON-parse the response ourselves.
let bodyParser = null; let bodyParser: ((body: string) => any) | undefined;
if (!rawResponse) { if (!rawResponse) {
bodyParser = function(rawBody: string) { bodyParser = function(rawBody: string) {
let body = JSON.parse(rawBody); let body = JSON.parse(rawBody);
@@ -472,7 +472,7 @@ export class MatrixHttpApi {
headers["Content-Length"] = "0"; headers["Content-Length"] = "0";
} }
promise = this.authedRequest( promise = this.authedRequest<UploadContentResponseType<O>>(
opts.callback, Method.Post, "/upload", queryParams, body, { opts.callback, Method.Post, "/upload", queryParams, body, {
prefix: "/_matrix/media/r0", prefix: "/_matrix/media/r0",
headers, headers,
@@ -590,10 +590,10 @@ export class MatrixHttpApi {
* occurred. This includes network problems and Matrix-specific error JSON. * occurred. This includes network problems and Matrix-specific error JSON.
*/ */
public authedRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>( public authedRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
callback: Callback<T>, callback: Callback<T> | undefined,
method: Method, method: Method,
path: string, path: string,
queryParams?: Record<string, string | string[]>, queryParams?: Record<string, string | string[] | undefined>,
data?: CoreOptions["body"], data?: CoreOptions["body"],
opts?: O | number, // number is legacy opts?: O | number, // number is legacy
): IAbortablePromise<ResponseType<T, O>> { ): IAbortablePromise<ResponseType<T, O>> {
@@ -667,7 +667,7 @@ export class MatrixHttpApi {
* occurred. This includes network problems and Matrix-specific error JSON. * occurred. This includes network problems and Matrix-specific error JSON.
*/ */
public request<T, O extends IRequestOpts<T> = IRequestOpts<T>>( public request<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
callback: Callback<T>, callback: Callback<T> | undefined,
method: Method, method: Method,
path: string, path: string,
queryParams?: CoreOptions["qs"], queryParams?: CoreOptions["qs"],
@@ -711,7 +711,7 @@ export class MatrixHttpApi {
* occurred. This includes network problems and Matrix-specific error JSON. * occurred. This includes network problems and Matrix-specific error JSON.
*/ */
public requestOtherUrl<T, O extends IRequestOpts<T> = IRequestOpts<T>>( public requestOtherUrl<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
callback: Callback<T>, callback: Callback<T> | undefined,
method: Method, method: Method,
uri: string, uri: string,
queryParams?: CoreOptions["qs"], queryParams?: CoreOptions["qs"],
@@ -778,7 +778,7 @@ export class MatrixHttpApi {
* Generic O should be inferred * Generic O should be inferred
*/ */
private doRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>( private doRequest<T, O extends IRequestOpts<T> = IRequestOpts<T>>(
callback: Callback<T>, callback: Callback<T> | undefined,
method: Method, method: Method,
uri: string, uri: string,
queryParams?: Record<string, string>, queryParams?: Record<string, string>,

View File

@@ -20,7 +20,7 @@ import { IContent, MatrixEvent } from "./event";
import { MSC3089TreeSpace } from "./MSC3089TreeSpace"; import { MSC3089TreeSpace } from "./MSC3089TreeSpace";
import { EventTimeline } from "./event-timeline"; import { EventTimeline } from "./event-timeline";
import { FileType } from "../http-api"; import { FileType } from "../http-api";
import type { ISendEventResponse } from ".."; import type { ISendEventResponse } from "../@types/requests";
/** /**
* Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference * Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference

View File

@@ -15,7 +15,6 @@ limitations under the License.
*/ */
import { MBeaconEventContent } from "../@types/beacon"; import { MBeaconEventContent } from "../@types/beacon";
import { M_TIMESTAMP } from "../@types/location";
import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers"; import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers";
import { MatrixEvent } from "../matrix"; import { MatrixEvent } from "../matrix";
import { sortEventsByLatestContentTimestamp } from "../utils"; import { sortEventsByLatestContentTimestamp } from "../utils";
@@ -161,7 +160,9 @@ export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.N
const validLocationEvents = beaconLocationEvents.filter(event => { const validLocationEvents = beaconLocationEvents.filter(event => {
const content = event.getContent<MBeaconEventContent>(); const content = event.getContent<MBeaconEventContent>();
const timestamp = M_TIMESTAMP.findIn<number>(content); const parsed = parseBeaconContent(content);
if (!parsed.uri || !parsed.timestamp) return false; // we won't be able to process these
const { timestamp } = parsed;
return ( return (
// only include positions that were taken inside the beacon's live period // only include positions that were taken inside the beacon's live period
isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) && isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) &&

View File

@@ -86,7 +86,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
private readonly displayPendingEvents: boolean; private readonly displayPendingEvents: boolean;
private liveTimeline: EventTimeline; private liveTimeline: EventTimeline;
private timelines: EventTimeline[]; private timelines: EventTimeline[];
private _eventIdToTimeline: Record<string, EventTimeline>; private _eventIdToTimeline = new Map<string, EventTimeline>();
private filter?: Filter; private filter?: Filter;
/** /**
@@ -123,12 +123,15 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
* @param {MatrixClient=} client the Matrix client which owns this EventTimelineSet, * @param {MatrixClient=} client the Matrix client which owns this EventTimelineSet,
* can be omitted if room is specified. * can be omitted if room is specified.
* @param {Thread=} thread the thread to which this timeline set relates. * @param {Thread=} thread the thread to which this timeline set relates.
* @param {boolean} isThreadTimeline Whether this timeline set relates to a thread list timeline
* (e.g., All threads or My threads)
*/ */
constructor( constructor(
public readonly room: Room | undefined, public readonly room: Room | undefined,
opts: IOpts = {}, opts: IOpts = {},
client?: MatrixClient, client?: MatrixClient,
public readonly thread?: Thread, public readonly thread?: Thread,
public readonly isThreadTimeline: boolean = false,
) { ) {
super(); super();
@@ -138,7 +141,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
// just a list - *not* ordered. // just a list - *not* ordered.
this.timelines = [this.liveTimeline]; this.timelines = [this.liveTimeline];
this._eventIdToTimeline = {}; this._eventIdToTimeline = new Map<string, EventTimeline>();
this.filter = opts.filter; this.filter = opts.filter;
@@ -210,7 +213,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
* @return {module:models/event-timeline~EventTimeline} timeline * @return {module:models/event-timeline~EventTimeline} timeline
*/ */
public eventIdToTimeline(eventId: string): EventTimeline { public eventIdToTimeline(eventId: string): EventTimeline {
return this._eventIdToTimeline[eventId]; return this._eventIdToTimeline.get(eventId);
} }
/** /**
@@ -220,10 +223,10 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
* @param {String} newEventId event ID of the replacement event * @param {String} newEventId event ID of the replacement event
*/ */
public replaceEventId(oldEventId: string, newEventId: string): void { public replaceEventId(oldEventId: string, newEventId: string): void {
const existingTimeline = this._eventIdToTimeline[oldEventId]; const existingTimeline = this._eventIdToTimeline.get(oldEventId);
if (existingTimeline) { if (existingTimeline) {
delete this._eventIdToTimeline[oldEventId]; this._eventIdToTimeline.delete(oldEventId);
this._eventIdToTimeline[newEventId] = existingTimeline; this._eventIdToTimeline.set(newEventId, existingTimeline);
} }
} }
@@ -257,7 +260,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
if (resetAllTimelines) { if (resetAllTimelines) {
this.timelines = [newTimeline]; this.timelines = [newTimeline];
this._eventIdToTimeline = {}; this._eventIdToTimeline = new Map<string, EventTimeline>();
} else { } else {
this.timelines.push(newTimeline); this.timelines.push(newTimeline);
} }
@@ -287,8 +290,9 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
* @return {?module:models/event-timeline~EventTimeline} timeline containing * @return {?module:models/event-timeline~EventTimeline} timeline containing
* the given event, or null if unknown * the given event, or null if unknown
*/ */
public getTimelineForEvent(eventId: string): EventTimeline | null { public getTimelineForEvent(eventId: string | null): EventTimeline | null {
const res = this._eventIdToTimeline[eventId]; if (eventId === null) { return null; }
const res = this._eventIdToTimeline.get(eventId);
return (res === undefined) ? null : res; return (res === undefined) ? null : res;
} }
@@ -450,7 +454,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
const event = events[i]; const event = events[i];
const eventId = event.getId(); const eventId = event.getId();
const existingTimeline = this._eventIdToTimeline[eventId]; const existingTimeline = this._eventIdToTimeline.get(eventId);
if (!existingTimeline) { if (!existingTimeline) {
// we don't know about this event yet. Just add it to the timeline. // we don't know about this event yet. Just add it to the timeline.
@@ -601,7 +605,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
} }
} }
const timeline = this._eventIdToTimeline[event.getId()]; const timeline = this._eventIdToTimeline.get(event.getId());
if (timeline) { if (timeline) {
if (duplicateStrategy === DuplicateStrategy.Replace) { if (duplicateStrategy === DuplicateStrategy.Replace) {
debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " + event.getId()); debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " + event.getId());
@@ -697,7 +701,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
roomState, roomState,
timelineWasEmpty, timelineWasEmpty,
}); });
this._eventIdToTimeline[eventId] = timeline; this._eventIdToTimeline.set(eventId, timeline);
this.relations.aggregateParentEvent(event); this.relations.aggregateParentEvent(event);
this.relations.aggregateChildEvent(event, this); this.relations.aggregateChildEvent(event, this);
@@ -725,23 +729,15 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
newEventId: string, newEventId: string,
): void { ): void {
// XXX: why don't we infer newEventId from localEvent? // XXX: why don't we infer newEventId from localEvent?
const existingTimeline = this._eventIdToTimeline[oldEventId]; const existingTimeline = this._eventIdToTimeline.get(oldEventId);
if (existingTimeline) { if (existingTimeline) {
delete this._eventIdToTimeline[oldEventId]; this._eventIdToTimeline.delete(oldEventId);
this._eventIdToTimeline[newEventId] = existingTimeline; this._eventIdToTimeline.set(newEventId, existingTimeline);
} else { } else if (!this.filter || this.filter.filterRoomTimeline([localEvent]).length) {
if (this.filter) {
if (this.filter.filterRoomTimeline([localEvent]).length) {
this.addEventToTimeline(localEvent, this.liveTimeline, { this.addEventToTimeline(localEvent, this.liveTimeline, {
toStartOfTimeline: false, toStartOfTimeline: false,
}); });
} }
} else {
this.addEventToTimeline(localEvent, this.liveTimeline, {
toStartOfTimeline: false,
});
}
}
} }
/** /**
@@ -753,14 +749,14 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
* in this room. * in this room.
*/ */
public removeEvent(eventId: string): MatrixEvent | null { public removeEvent(eventId: string): MatrixEvent | null {
const timeline = this._eventIdToTimeline[eventId]; const timeline = this._eventIdToTimeline.get(eventId);
if (!timeline) { if (!timeline) {
return null; return null;
} }
const removed = timeline.removeEvent(eventId); const removed = timeline.removeEvent(eventId);
if (removed) { if (removed) {
delete this._eventIdToTimeline[eventId]; this._eventIdToTimeline.delete(eventId);
const data = { const data = {
timeline: timeline, timeline: timeline,
}; };
@@ -787,8 +783,8 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
return 0; return 0;
} }
const timeline1 = this._eventIdToTimeline[eventId1]; const timeline1 = this._eventIdToTimeline.get(eventId1);
const timeline2 = this._eventIdToTimeline[eventId2]; const timeline2 = this._eventIdToTimeline.get(eventId2);
if (timeline1 === undefined) { if (timeline1 === undefined) {
return null; return null;

Some files were not shown because too many files have changed in this diff Show More