You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-05 00:42:10 +03:00
Merge branch 'develop' into robertlong/group-call
This commit is contained in:
2
.github/workflows/backport.yml
vendored
2
.github/workflows/backport.yml
vendored
@@ -25,6 +25,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: tibdex/backport@v2
|
- uses: tibdex/backport@v2
|
||||||
with:
|
with:
|
||||||
labels_template: "<%= JSON.stringify(labels) %>"
|
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
|
||||||
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
|
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
|
||||||
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
39
.github/workflows/release-npm.yml
vendored
Normal file
39
.github/workflows/release-npm.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# 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"
|
||||||
|
|
||||||
|
- 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:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
release: ${{ steps.npm-publish.outputs.version }}
|
@@ -24,7 +24,6 @@ jobs:
|
|||||||
|
|
||||||
- name: 📋 Copy to temp
|
- name: 📋 Copy to temp
|
||||||
run: |
|
run: |
|
||||||
ls -lah
|
|
||||||
tag="${{ github.ref_name }}"
|
tag="${{ github.ref_name }}"
|
||||||
version="${tag#v}"
|
version="${tag#v}"
|
||||||
echo "VERSION=$version" >> $GITHUB_ENV
|
echo "VERSION=$version" >> $GITHUB_ENV
|
||||||
@@ -51,3 +50,9 @@ jobs:
|
|||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
keep_files: true
|
keep_files: true
|
||||||
publish_dir: .
|
publish_dir: .
|
||||||
|
|
||||||
|
npm:
|
||||||
|
name: Publish
|
||||||
|
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
|
||||||
|
secrets:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
35
.github/workflows/static_analysis.yml
vendored
35
.github/workflows/static_analysis.yml
vendored
@@ -54,3 +54,38 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate Docs
|
- name: Generate Docs
|
||||||
run: "yarn run gendoc"
|
run: "yarn run gendoc"
|
||||||
|
|
||||||
|
tsc-strict:
|
||||||
|
name: Typescript Strict Error Checker
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
checks: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Get diff lines
|
||||||
|
id: diff
|
||||||
|
uses: Equip-Collaboration/diff-line-numbers@v1.0.0
|
||||||
|
with:
|
||||||
|
include: '["\\.tsx?$"]'
|
||||||
|
|
||||||
|
- name: Detecting files changed
|
||||||
|
id: files
|
||||||
|
uses: futuratrepadeira/changed-files@v4.0.0
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
pattern: '^.*\.tsx?$'
|
||||||
|
|
||||||
|
- uses: t3chguy/typescript-check-action@main
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
use-check: false
|
||||||
|
check-fail-mode: added
|
||||||
|
output-behaviour: annotate
|
||||||
|
ts-extra-args: '--strict'
|
||||||
|
files-changed: ${{ steps.files.outputs.files_updated }}
|
||||||
|
files-added: ${{ steps.files.outputs.files_created }}
|
||||||
|
files-deleted: ${{ steps.files.outputs.files_deleted }}
|
||||||
|
line-numbers: ${{ steps.diff.outputs.lineNumbers }}
|
||||||
|
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,3 +1,25 @@
|
|||||||
|
Changes in [19.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.4.0) (2022-08-31)
|
||||||
|
==================================================================================================
|
||||||
|
|
||||||
|
## 🔒 Security
|
||||||
|
* Fix for [CVE-2022-36059](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D36059)
|
||||||
|
|
||||||
|
Find more details at https://matrix.org/blog/2022/08/31/security-releases-matrix-js-sdk-19-4-0-and-matrix-react-sdk-3-53-0
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
* Re-emit room state events on rooms ([\#2607](https://github.com/matrix-org/matrix-js-sdk/pull/2607)).
|
||||||
|
* Add ability to override built in room name generator for an i18n'able one ([\#2609](https://github.com/matrix-org/matrix-js-sdk/pull/2609)).
|
||||||
|
* Add txn_id support to sliding sync ([\#2567](https://github.com/matrix-org/matrix-js-sdk/pull/2567)).
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
* Refactor Sync and fix `initialSyncLimit` ([\#2587](https://github.com/matrix-org/matrix-js-sdk/pull/2587)).
|
||||||
|
* Use deep equality comparisons when searching for outgoing key requests by target ([\#2623](https://github.com/matrix-org/matrix-js-sdk/pull/2623)). Contributed by @duxovni.
|
||||||
|
* Fix room membership race with PREPARED event ([\#2613](https://github.com/matrix-org/matrix-js-sdk/pull/2613)). Contributed by @jotto.
|
||||||
|
* fixed a sliding sync bug which could cause the `roomIndexToRoomId` map to be incorrect when a new room is added in the middle of the list or when an existing room is deleted from the middle of the list. ([\#2610](https://github.com/matrix-org/matrix-js-sdk/pull/2610)).
|
||||||
|
* Fix: Handle parsing of a beacon info event without asset ([\#2591](https://github.com/matrix-org/matrix-js-sdk/pull/2591)). Fixes vector-im/element-web#23078. Contributed by @kerryarchibald.
|
||||||
|
* Fix finding event read up to if stable private read receipts is missing ([\#2585](https://github.com/matrix-org/matrix-js-sdk/pull/2585)). Fixes vector-im/element-web#23027.
|
||||||
|
* fixed a sliding sync issue where history could be interpreted as live events. ([\#2583](https://github.com/matrix-org/matrix-js-sdk/pull/2583)).
|
||||||
|
|
||||||
Changes in [19.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.3.0) (2022-08-16)
|
Changes in [19.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v19.3.0) (2022-08-16)
|
||||||
==================================================================================================
|
==================================================================================================
|
||||||
|
|
||||||
|
281
CONTRIBUTING.md
281
CONTRIBUTING.md
@@ -1,284 +1,5 @@
|
|||||||
Contributing code to matrix-js-sdk
|
Contributing code to matrix-js-sdk
|
||||||
==================================
|
==================================
|
||||||
|
|
||||||
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
|
matrix-js-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md
|
||||||
willing to license their contributions under the same license as the project
|
|
||||||
itself. We follow a simple 'inbound=outbound' model for contributions: the act
|
|
||||||
of submitting an 'inbound' contribution means that the contributor agrees to
|
|
||||||
license the code under the same terms as the project's overall 'outbound'
|
|
||||||
license - in this case, Apache Software License v2 (see
|
|
||||||
[LICENSE](LICENSE)).
|
|
||||||
|
|
||||||
How to contribute
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
The preferred and easiest way to contribute changes to the project is to fork
|
|
||||||
it on github, and then create a pull request to ask us to pull your changes
|
|
||||||
into our repo (https://help.github.com/articles/using-pull-requests/)
|
|
||||||
|
|
||||||
We use GitHub's pull request workflow to review the contribution, and either
|
|
||||||
ask you to make any refinements needed or merge it and make them ourselves.
|
|
||||||
|
|
||||||
Things that should go into your PR description:
|
|
||||||
* A changelog entry in the `Notes` section (see below)
|
|
||||||
* References to any bugs fixed by the change (in GitHub's `Fixes` notation)
|
|
||||||
* Describe the why and what is changing in the PR description so it's easy for
|
|
||||||
onlookers and reviewers to onboard and context switch. This information is
|
|
||||||
also helpful when we come back to look at this in 6 months and ask "why did
|
|
||||||
we do it like that?" we have a chance of finding out.
|
|
||||||
* Why didn't it work before? Why does it work now? What use cases does it
|
|
||||||
unlock?
|
|
||||||
* If you find yourself adding information on how the code works or why you
|
|
||||||
chose to do it the way you did, make sure this information is instead
|
|
||||||
written as comments in the code itself.
|
|
||||||
* Sometimes a PR can change considerably as it is developed. In this case,
|
|
||||||
the description should be updated to reflect the most recent state of
|
|
||||||
the PR. (It can be helpful to retain the old content under a suitable
|
|
||||||
heading, for additional context.)
|
|
||||||
* Include both **before** and **after** screenshots to easily compare and discuss
|
|
||||||
what's changing.
|
|
||||||
* Include a step-by-step testing strategy so that a reviewer can check out the
|
|
||||||
code locally and easily get to the point of testing your change.
|
|
||||||
* Add comments to the diff for the reviewer that might help them to understand
|
|
||||||
why the change is necessary or how they might better understand and review it.
|
|
||||||
|
|
||||||
We rely on information in pull request to populate the information that goes
|
|
||||||
into the changelogs our users see, both for the JS SDK itself and also for some
|
|
||||||
projects based on it. This is picked up from both labels on the pull request and
|
|
||||||
the `Notes:` annotation in the description. By default, the PR title will be
|
|
||||||
used for the changelog entry, but you can specify more options, as follows.
|
|
||||||
|
|
||||||
To add a longer, more detailed description of the change for the changelog:
|
|
||||||
|
|
||||||
|
|
||||||
*Fix llama herding bug*
|
|
||||||
|
|
||||||
```
|
|
||||||
Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase
|
|
||||||
```
|
|
||||||
|
|
||||||
For some PRs, it's not useful to have an entry in the user-facing changelog (this is
|
|
||||||
the default for PRs labelled with `T-Task`):
|
|
||||||
|
|
||||||
*Remove outdated comment from `Ungulates.ts`*
|
|
||||||
```
|
|
||||||
Notes: none
|
|
||||||
```
|
|
||||||
|
|
||||||
Sometimes, you're fixing a bug in a downstream project, in which case you want
|
|
||||||
an entry in that project's changelog. You can do that too:
|
|
||||||
|
|
||||||
*Fix another herding bug*
|
|
||||||
```
|
|
||||||
Notes: Fix a bug where the `herd()` function would only work on Tuesdays
|
|
||||||
element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays
|
|
||||||
```
|
|
||||||
|
|
||||||
This example is for Element Web. You can specify:
|
|
||||||
* matrix-react-sdk
|
|
||||||
* element-web
|
|
||||||
* element-desktop
|
|
||||||
|
|
||||||
If your PR introduces a breaking change, use the `Notes` section in the same
|
|
||||||
way, additionally adding the `X-Breaking-Change` label (see below). There's no need
|
|
||||||
to specify in the notes that it's a breaking change - this will be added
|
|
||||||
automatically based on the label - but remember to tell the developer how to
|
|
||||||
migrate:
|
|
||||||
|
|
||||||
*Remove legacy class*
|
|
||||||
|
|
||||||
```
|
|
||||||
Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead.
|
|
||||||
```
|
|
||||||
|
|
||||||
Other metadata can be added using labels.
|
|
||||||
* `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a *major* version bump.
|
|
||||||
* `T-Enhancement`: A new feature - adding this label will mean the change causes a *minor* version bump.
|
|
||||||
* `T-Defect`: A bug fix (in either code or docs).
|
|
||||||
* `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one.
|
|
||||||
|
|
||||||
If you don't have permission to add labels, your PR reviewer(s) can work with you
|
|
||||||
to add them: ask in the PR description or comments.
|
|
||||||
|
|
||||||
We use continuous integration, and all pull requests get automatically tested:
|
|
||||||
if your change breaks the build, then the PR will show that there are failed
|
|
||||||
checks, so please check back after a few minutes.
|
|
||||||
|
|
||||||
Tests
|
|
||||||
-----
|
|
||||||
Your PR should include tests.
|
|
||||||
|
|
||||||
For new user facing features in `matrix-react-sdk` or `element-web`, you
|
|
||||||
must include:
|
|
||||||
|
|
||||||
1. Comprehensive unit tests written in Jest. These are located in `/test`.
|
|
||||||
2. "happy path" end-to-end tests.
|
|
||||||
These are located in `/test/end-to-end-tests` in `matrix-react-sdk`, and
|
|
||||||
are run using `element-web`. Ideally, you would also include tests for edge
|
|
||||||
and error cases.
|
|
||||||
|
|
||||||
Unit tests are expected even when the feature is in labs. It's good practice
|
|
||||||
to write tests alongside the code as it ensures the code is testable from
|
|
||||||
the start, and gives you a fast feedback loop while you're developing the
|
|
||||||
functionality. End-to-end tests should be added prior to the feature
|
|
||||||
leaving labs, but don't have to be present from the start (although it might
|
|
||||||
be beneficial to have some running early, so you can test things faster).
|
|
||||||
|
|
||||||
For bugs in those repos, your change must include at least one unit test or
|
|
||||||
end-to-end test; which is best depends on what sort of test most concisely
|
|
||||||
exercises the area.
|
|
||||||
|
|
||||||
Changes to `matrix-js-sdk` must be accompanied by unit tests written in Jest.
|
|
||||||
These are located in `/spec/`.
|
|
||||||
|
|
||||||
When writing unit tests, please aim for a high level of test coverage
|
|
||||||
for new code - 80% or greater. If you cannot achieve that, please document
|
|
||||||
why it's not possible in your PR.
|
|
||||||
|
|
||||||
Some sections of code are not sensible to add coverage for, such as those
|
|
||||||
which explicitly inhibit noisy logging for tests. Which can be hidden using
|
|
||||||
an istanbul magic comment as [documented here][1]. See example:
|
|
||||||
```javascript
|
|
||||||
/* istanbul ignore if */
|
|
||||||
if (process.env.NODE_ENV !== "test") {
|
|
||||||
logger.error("Log line that is noisy enough in tests to want to skip");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Tests validate that your change works as intended and also document
|
|
||||||
concisely what is being changed. Ideally, your new tests fail
|
|
||||||
prior to your change, and succeed once it has been applied. You may
|
|
||||||
find this simpler to achieve if you write the tests first.
|
|
||||||
|
|
||||||
If you're spiking some code that's experimental and not being used to support
|
|
||||||
production features, exceptions can be made to requirements for tests.
|
|
||||||
Note that tests will still be required in order to ship the feature, and it's
|
|
||||||
strongly encouraged to think about tests early in the process, as adding
|
|
||||||
tests later will become progressively more difficult.
|
|
||||||
|
|
||||||
If you're not sure how to approach writing tests for your change, ask for help
|
|
||||||
in [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
|
|
||||||
|
|
||||||
Code style
|
|
||||||
----------
|
|
||||||
The js-sdk aims to target TypeScript/ES6. All new files should be written in
|
|
||||||
TypeScript and existing files should use ES6 principles where possible.
|
|
||||||
|
|
||||||
Members should not be exported as a default export in general - it causes problems
|
|
||||||
with the architecture of the SDK (index file becomes less clear) and could
|
|
||||||
introduce naming problems (as default exports get aliased upon import). In
|
|
||||||
general, avoid using `export default`.
|
|
||||||
|
|
||||||
The remaining code-style for matrix-js-sdk is not formally documented, but
|
|
||||||
contributors are encouraged to read the
|
|
||||||
[code style document for matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md)
|
|
||||||
and follow the principles set out there.
|
|
||||||
|
|
||||||
Please ensure your changes match the cosmetic style of the existing project,
|
|
||||||
and ***never*** mix cosmetic and functional changes in the same commit, as it
|
|
||||||
makes it horribly hard to review otherwise.
|
|
||||||
|
|
||||||
Attribution
|
|
||||||
-----------
|
|
||||||
Everyone who contributes anything to Matrix is welcome to be listed in the
|
|
||||||
AUTHORS.rst file for the project in question. Please feel free to include a
|
|
||||||
change to AUTHORS.rst in your pull request to list yourself and a short
|
|
||||||
description of the area(s) you've worked on. Also, we sometimes have swag to
|
|
||||||
give away to contributors - if you feel that Matrix-branded apparel is missing
|
|
||||||
from your life, please mail us your shipping address to matrix at matrix.org
|
|
||||||
and we'll try to fix it :)
|
|
||||||
|
|
||||||
Sign off
|
|
||||||
--------
|
|
||||||
In order to have a concrete record that your contribution is intentional
|
|
||||||
and you agree to license it under the same terms as the project's license, we've
|
|
||||||
adopted the same lightweight approach that the Linux Kernel
|
|
||||||
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
|
|
||||||
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
|
||||||
projects use: the DCO (Developer Certificate of Origin:
|
|
||||||
http://developercertificate.org/). This is a simple declaration that you wrote
|
|
||||||
the contribution or otherwise have the right to contribute it to Matrix:
|
|
||||||
|
|
||||||
```
|
|
||||||
Developer Certificate of Origin
|
|
||||||
Version 1.1
|
|
||||||
|
|
||||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
|
||||||
660 York Street, Suite 102,
|
|
||||||
San Francisco, CA 94110 USA
|
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies of this
|
|
||||||
license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Developer's Certificate of Origin 1.1
|
|
||||||
|
|
||||||
By making a contribution to this project, I certify that:
|
|
||||||
|
|
||||||
(a) The contribution was created in whole or in part by me and I
|
|
||||||
have the right to submit it under the open source license
|
|
||||||
indicated in the file; or
|
|
||||||
|
|
||||||
(b) The contribution is based upon previous work that, to the best
|
|
||||||
of my knowledge, is covered under an appropriate open source
|
|
||||||
license and I have the right under that license to submit that
|
|
||||||
work with modifications, whether created in whole or in part
|
|
||||||
by me, under the same open source license (unless I am
|
|
||||||
permitted to submit under a different license), as indicated
|
|
||||||
in the file; or
|
|
||||||
|
|
||||||
(c) The contribution was provided directly to me by some other
|
|
||||||
person who certified (a), (b) or (c) and I have not modified
|
|
||||||
it.
|
|
||||||
|
|
||||||
(d) I understand and agree that this project and the contribution
|
|
||||||
are public and that a record of the contribution (including all
|
|
||||||
personal information I submit with it, including my sign-off) is
|
|
||||||
maintained indefinitely and may be redistributed consistent with
|
|
||||||
this project or the open source license(s) involved.
|
|
||||||
```
|
|
||||||
|
|
||||||
If you agree to this for your contribution, then all that's needed is to
|
|
||||||
include the line in your commit or pull request comment:
|
|
||||||
|
|
||||||
```
|
|
||||||
Signed-off-by: Your Name <your@email.example.org>
|
|
||||||
```
|
|
||||||
|
|
||||||
We accept contributions under a legally identifiable name, such as your name on
|
|
||||||
government documentation or common-law names (names claimed by legitimate usage
|
|
||||||
or repute). Unfortunately, we cannot accept anonymous contributions at this
|
|
||||||
time.
|
|
||||||
|
|
||||||
Git allows you to add this signoff automatically when using the `-s` flag to
|
|
||||||
`git commit`, which uses the name and email set in your `user.name` and
|
|
||||||
`user.email` git configs.
|
|
||||||
|
|
||||||
If you forgot to sign off your commits before making your pull request and are
|
|
||||||
on Git 2.17+ you can mass signoff using rebase:
|
|
||||||
|
|
||||||
```
|
|
||||||
git rebase --signoff origin/develop
|
|
||||||
```
|
|
||||||
|
|
||||||
Review expectations
|
|
||||||
===================
|
|
||||||
|
|
||||||
See https://github.com/vector-im/element-meta/wiki/Review-process
|
|
||||||
|
|
||||||
|
|
||||||
Merge Strategy
|
|
||||||
==============
|
|
||||||
|
|
||||||
The preferred method for merging pull requests is squash merging to keep the
|
|
||||||
commit history trim, but it is up to the discretion of the team member merging
|
|
||||||
the change. We do not support rebase merges due to `allchange` being unable to
|
|
||||||
handle them. When merging make sure to leave the default commit title, or
|
|
||||||
at least leave the PR number at the end in brackets like by default.
|
|
||||||
When stacking pull requests, you may wish to do the following:
|
|
||||||
|
|
||||||
1. Branch from develop to your branch (branch1), push commits onto it and open a pull request
|
|
||||||
2. Branch from your base branch (branch1) to your work branch (branch2), push commits and open a pull request configuring the base to be branch1, saying in the description that it is based on your other PR.
|
|
||||||
3. Merge the first PR using a merge commit otherwise your stacked PR will need a rebase. Github will automatically adjust the base branch of your other PR to be develop.
|
|
||||||
|
|
||||||
|
|
||||||
[1]: https://github.com/gotwarlost/istanbul/blob/master/ignoring-code-for-coverage.md
|
|
||||||
|
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "matrix-js-sdk",
|
"name": "matrix-js-sdk",
|
||||||
"version": "19.3.0",
|
"version": "19.4.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"
|
||||||
@@ -84,25 +84,25 @@
|
|||||||
"@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.12.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.20.0",
|
"eslint": "8.23.0",
|
||||||
"eslint-config-google": "^0.14.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
"eslint-plugin-import": "^2.25.4",
|
"eslint-plugin-import": "^2.25.4",
|
||||||
"eslint-plugin-matrix-org": "^0.5.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-environment-jsdom": "^28.1.3",
|
"jest-environment-jsdom": "^28.1.3",
|
||||||
|
"jest": "^29.0.0",
|
||||||
"jest-localstorage-mock": "^2.4.6",
|
"jest-localstorage-mock": "^2.4.6",
|
||||||
"jest-sonar-reporter": "^2.0.0",
|
"jest-sonar-reporter": "^2.0.0",
|
||||||
"jsdoc": "^3.6.6",
|
"jsdoc": "^3.6.6",
|
||||||
|
37
post-release.sh
Executable file
37
post-release.sh
Executable file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Script to perform a post-release steps of matrix-js-sdk.
|
||||||
|
#
|
||||||
|
# Requires:
|
||||||
|
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
||||||
|
|
||||||
|
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
|
||||||
|
# When merging to develop, we need revert the `main` and `typings` fields if we adjusted them previously.
|
||||||
|
for i in main typings
|
||||||
|
do
|
||||||
|
# If a `lib` prefixed value is present, it means we adjusted the field
|
||||||
|
# earlier at publish time, so we should revert it now.
|
||||||
|
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
|
||||||
|
# If there's a `src` prefixed value, use that, otherwise delete.
|
||||||
|
# This is used to delete the `typings` field and reset `main` back
|
||||||
|
# to the TypeScript source.
|
||||||
|
src_value=$(jq -r ".matrix_src_$i" package.json)
|
||||||
|
if [ "$src_value" != "null" ]; then
|
||||||
|
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
|
||||||
|
else
|
||||||
|
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$(git ls-files --modified package.json)" ]; then
|
||||||
|
echo "Committing develop package.json"
|
||||||
|
git commit package.json -m "Resetting package fields for development"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git push origin develop
|
||||||
|
fi
|
150
release.sh
150
release.sh
@@ -3,19 +3,16 @@
|
|||||||
# Script to perform a release of matrix-js-sdk and downstream projects.
|
# Script to perform a release of matrix-js-sdk and downstream projects.
|
||||||
#
|
#
|
||||||
# Requires:
|
# Requires:
|
||||||
# github-changelog-generator; install via:
|
|
||||||
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
|
|
||||||
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
|
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
|
||||||
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
|
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
|
||||||
# npm; typically installed by Node.js
|
|
||||||
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
|
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
|
||||||
#
|
#
|
||||||
# Note: this script is also used to release matrix-react-sdk and element-web.
|
# Note: this script is also used to release matrix-react-sdk, element-web, and element-desktop.
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
||||||
if [[ `command -v hub` ]] && [[ `hub --version` =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
|
if [[ $(command -v hub) ]] && [[ $(hub --version) =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
|
||||||
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
|
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
|
||||||
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
|
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
|
||||||
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
|
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
|
||||||
@@ -26,7 +23,6 @@ else
|
|||||||
echo "hub is required: please install it"
|
echo "hub is required: please install it"
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
npm --version > /dev/null || (echo "npm is required: please install it"; kill $$)
|
|
||||||
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
|
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
|
||||||
|
|
||||||
USAGE="$0 [-x] [-c changelog_file] vX.Y.Z"
|
USAGE="$0 [-x] [-c changelog_file] vX.Y.Z"
|
||||||
@@ -37,7 +33,6 @@ $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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,10 +54,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 +67,58 @@ 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 {
|
||||||
|
echo "Checking version of $1..."
|
||||||
|
local depver=$(cat package.json | jq -r .dependencies[\"$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 {
|
||||||
|
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
|
||||||
|
|
||||||
# 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,16 +127,6 @@ 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}"
|
||||||
@@ -117,7 +137,7 @@ prerelease=0
|
|||||||
# see if the version has a hyphen in it. Crude,
|
# see if the version has a hyphen in it. Crude,
|
||||||
# but semver doesn't support postreleases so anything
|
# but semver doesn't support postreleases so anything
|
||||||
# with a hyphen is a prerelease.
|
# with a hyphen is a prerelease.
|
||||||
echo $release | grep -q '-' && prerelease=1
|
echo "$release" | grep -q '-' && prerelease=1
|
||||||
|
|
||||||
if [ $prerelease -eq 1 ]; then
|
if [ $prerelease -eq 1 ]; then
|
||||||
echo Making a PRE-RELEASE
|
echo Making a PRE-RELEASE
|
||||||
@@ -143,13 +163,13 @@ if [ -z "$skip_changelog" ]; then
|
|||||||
yarn run allchange "$release"
|
yarn run allchange "$release"
|
||||||
read -p "Edit $changelog_file manually, or press enter to continue " REPLY
|
read -p "Edit $changelog_file manually, or press enter to continue " REPLY
|
||||||
|
|
||||||
if [ -n "$(git ls-files --modified $changelog_file)" ]; then
|
if [ -n "$(git ls-files --modified "$changelog_file")" ]; then
|
||||||
echo "Committing updated changelog"
|
echo "Committing updated changelog"
|
||||||
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,19 +196,19 @@ 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
|
||||||
pkglock=''
|
pkglock=''
|
||||||
fi
|
fi
|
||||||
git commit package.json $pkglock -m "$tag"
|
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 +226,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 +252,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
|
||||||
@@ -270,7 +290,7 @@ if [ -n "$signing_id" ]; then
|
|||||||
curl -L "${gh_project_url}/archive/${tarfile}" -o "${tarfile}"
|
curl -L "${gh_project_url}/archive/${tarfile}" -o "${tarfile}"
|
||||||
|
|
||||||
# unzip it and compare it with the tar we would generate
|
# unzip it and compare it with the tar we would generate
|
||||||
if ! cmp --silent <(gunzip -c $tarfile) \
|
if ! cmp --silent <(gunzip -c "$tarfile") \
|
||||||
<(git archive --format tar --prefix="${project_name}-${release}/" "$tag"); then
|
<(git archive --format tar --prefix="${project_name}-${release}/" "$tag"); then
|
||||||
|
|
||||||
# we don't bail out here, because really it's more likely that our comparison
|
# we don't bail out here, because really it's more likely that our comparison
|
||||||
@@ -298,11 +318,11 @@ 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}"
|
||||||
hub release create $hubflags $assets -F "${release_text}" "$tag"
|
hub release create $hubflags "$assets" -F "${release_text}" "$tag"
|
||||||
|
|
||||||
if [ $dodist -eq 0 ]; then
|
if [ $dodist -eq 0 ]; then
|
||||||
rm -rf "$builddir"
|
rm -rf "$builddir"
|
||||||
@@ -310,19 +330,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 +346,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
|
||||||
|
@@ -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;
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|
@@ -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();
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -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,12 +262,25 @@ 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);
|
||||||
@@ -276,6 +290,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
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);
|
||||||
@@ -285,6 +300,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
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);
|
||||||
@@ -294,15 +310,26 @@ describe("SlidingSyncSdk", () => {
|
|||||||
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 an invited/joined_count", () => {
|
||||||
|
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
|
||||||
|
const gotRoom = client.getRoom(roomG);
|
||||||
|
expect(gotRoom).toBeDefined();
|
||||||
|
if (gotRoom == null) { return; }
|
||||||
|
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
|
||||||
|
expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
|
||||||
|
});
|
||||||
|
|
||||||
it("can be created with invite_state", () => {
|
it("can be created with invite_state", () => {
|
||||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
|
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
|
||||||
const gotRoom = client.getRoom(roomE);
|
const gotRoom = client.getRoom(roomE);
|
||||||
expect(gotRoom).toBeDefined();
|
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);
|
||||||
});
|
});
|
||||||
@@ -311,6 +338,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
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);
|
||||||
@@ -326,6 +354,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
});
|
});
|
||||||
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);
|
||||||
@@ -333,6 +362,8 @@ describe("SlidingSyncSdk", () => {
|
|||||||
|
|
||||||
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: [
|
||||||
@@ -343,6 +374,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
});
|
});
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -355,6 +387,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
});
|
});
|
||||||
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);
|
||||||
@@ -369,11 +402,25 @@ describe("SlidingSyncSdk", () => {
|
|||||||
});
|
});
|
||||||
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
|
||||||
@@ -394,6 +441,7 @@ describe("SlidingSyncSdk", () => {
|
|||||||
});
|
});
|
||||||
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(
|
||||||
|
@@ -558,6 +558,153 @@ describe("SlidingSync", () => {
|
|||||||
await httpBackend.flushAllExpected();
|
await httpBackend.flushAllExpected();
|
||||||
await responseProcessed;
|
await responseProcessed;
|
||||||
await listPromise;
|
await listPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
// this refers to a set of operations where the end result is no change.
|
||||||
|
it("should handle net zero operations correctly", async () => {
|
||||||
|
const indexToRoomId = {
|
||||||
|
0: roomB,
|
||||||
|
1: roomC,
|
||||||
|
};
|
||||||
|
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual(indexToRoomId);
|
||||||
|
httpBackend.when("POST", syncUrl).respond(200, {
|
||||||
|
pos: "f",
|
||||||
|
// currently the list is [B,C] so we will insert D then immediately delete it
|
||||||
|
lists: [{
|
||||||
|
count: 500,
|
||||||
|
ops: [
|
||||||
|
{
|
||||||
|
op: "DELETE", index: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
op: "INSERT", index: 0, room_id: roomA,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
op: "DELETE", index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 50,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
const listPromise = listenUntil(slidingSync, "SlidingSync.List",
|
||||||
|
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||||
|
expect(listIndex).toEqual(0);
|
||||||
|
expect(joinedCount).toEqual(500);
|
||||||
|
expect(roomIndexToRoomId).toEqual(indexToRoomId);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||||
|
return state === SlidingSyncState.Complete;
|
||||||
|
});
|
||||||
|
await httpBackend.flushAllExpected();
|
||||||
|
await responseProcessed;
|
||||||
|
await listPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle deletions correctly", async () => {
|
||||||
|
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
|
||||||
|
0: roomB,
|
||||||
|
1: roomC,
|
||||||
|
});
|
||||||
|
httpBackend.when("POST", syncUrl).respond(200, {
|
||||||
|
pos: "g",
|
||||||
|
lists: [{
|
||||||
|
count: 499,
|
||||||
|
ops: [
|
||||||
|
{
|
||||||
|
op: "DELETE", index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 50,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
const listPromise = listenUntil(slidingSync, "SlidingSync.List",
|
||||||
|
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||||
|
expect(listIndex).toEqual(0);
|
||||||
|
expect(joinedCount).toEqual(499);
|
||||||
|
expect(roomIndexToRoomId).toEqual({
|
||||||
|
0: roomC,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||||
|
return state === SlidingSyncState.Complete;
|
||||||
|
});
|
||||||
|
await httpBackend.flushAllExpected();
|
||||||
|
await responseProcessed;
|
||||||
|
await listPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle insertions correctly", async () => {
|
||||||
|
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
|
||||||
|
0: roomC,
|
||||||
|
});
|
||||||
|
httpBackend.when("POST", syncUrl).respond(200, {
|
||||||
|
pos: "h",
|
||||||
|
lists: [{
|
||||||
|
count: 500,
|
||||||
|
ops: [
|
||||||
|
{
|
||||||
|
op: "INSERT", index: 1, room_id: roomA,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 50,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
let listPromise = listenUntil(slidingSync, "SlidingSync.List",
|
||||||
|
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||||
|
expect(listIndex).toEqual(0);
|
||||||
|
expect(joinedCount).toEqual(500);
|
||||||
|
expect(roomIndexToRoomId).toEqual({
|
||||||
|
0: roomC,
|
||||||
|
1: roomA,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||||
|
return state === SlidingSyncState.Complete;
|
||||||
|
});
|
||||||
|
await httpBackend.flushAllExpected();
|
||||||
|
await responseProcessed;
|
||||||
|
await listPromise;
|
||||||
|
|
||||||
|
httpBackend.when("POST", syncUrl).respond(200, {
|
||||||
|
pos: "h",
|
||||||
|
lists: [{
|
||||||
|
count: 501,
|
||||||
|
ops: [
|
||||||
|
{
|
||||||
|
op: "INSERT", index: 1, room_id: roomB,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 50,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
listPromise = listenUntil(slidingSync, "SlidingSync.List",
|
||||||
|
(listIndex, joinedCount, roomIndexToRoomId) => {
|
||||||
|
expect(listIndex).toEqual(0);
|
||||||
|
expect(joinedCount).toEqual(501);
|
||||||
|
expect(roomIndexToRoomId).toEqual({
|
||||||
|
0: roomC,
|
||||||
|
1: roomB,
|
||||||
|
2: roomA,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
|
||||||
|
return state === SlidingSyncState.Complete;
|
||||||
|
});
|
||||||
|
await httpBackend.flushAllExpected();
|
||||||
|
await responseProcessed;
|
||||||
|
await listPromise;
|
||||||
slidingSync.stop();
|
slidingSync.stop();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -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) {
|
@@ -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);
|
||||||
|
@@ -147,9 +147,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;
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,7 @@ 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 { IStore } from '../../src/store';
|
||||||
|
|
||||||
const Olm = global.Olm;
|
const Olm = global.Olm;
|
||||||
|
|
||||||
@@ -158,8 +159,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({
|
||||||
@@ -469,12 +470,12 @@ describe("Crypto", function() {
|
|||||||
jest.setTimeout(10000);
|
jest.setTimeout(10000);
|
||||||
const client = (new TestClient("@a:example.com", "dev")).client;
|
const client = (new TestClient("@a:example.com", "dev")).client;
|
||||||
await client.initCrypto();
|
await client.initCrypto();
|
||||||
client.crypto.getSecretStorageKey = async () => null;
|
client.crypto.getSecretStorageKey = jest.fn().mockResolvedValue(null);
|
||||||
client.crypto.isCrossSigningReady = async () => false;
|
client.crypto.isCrossSigningReady = async () => false;
|
||||||
client.crypto.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
|
client.crypto.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
|
||||||
client.crypto.baseApis.setAccountData = () => null;
|
client.crypto.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
|
||||||
client.crypto.baseApis.uploadKeySignatures = () => null;
|
client.crypto.baseApis.uploadKeySignatures = jest.fn();
|
||||||
client.crypto.baseApis.http.authedRequest = () => null;
|
client.crypto.baseApis.http.authedRequest = jest.fn();
|
||||||
const createSecretStorageKey = async () => {
|
const createSecretStorageKey = async () => {
|
||||||
return {
|
return {
|
||||||
keyInfo: undefined, // Returning undefined here used to cause a crash
|
keyInfo: undefined, // Returning undefined here used to cause a crash
|
||||||
|
@@ -32,8 +32,8 @@ import { ClientEvent, MatrixClient, RoomMember } from '../../../../src';
|
|||||||
import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo';
|
import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo';
|
||||||
import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning';
|
import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning';
|
||||||
|
|
||||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||||
|
|
||||||
const ROOM_ID = '!ROOM:ID';
|
const ROOM_ID = '!ROOM:ID';
|
||||||
|
|
||||||
|
@@ -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';
|
||||||
|
|
||||||
|
@@ -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 =
|
||||||
|
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -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,17 +44,17 @@ 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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("setPowerLevelEvent", function() {
|
describe("setPowerLevelEvent", function() {
|
||||||
@@ -66,92 +83,92 @@ describe("RoomMember", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should emit 'RoomMember.powerLevel' if the power level changes.",
|
it("should emit 'RoomMember.powerLevel' if the power level changes.",
|
||||||
function() {
|
function() {
|
||||||
const event = utils.mkEvent({
|
const event = utils.mkEvent({
|
||||||
type: "m.room.power_levels",
|
type: "m.room.power_levels",
|
||||||
room: roomId,
|
room: roomId,
|
||||||
user: userA,
|
user: userA,
|
||||||
content: {
|
content: {
|
||||||
users_default: 20,
|
users_default: 20,
|
||||||
users: {
|
users: {
|
||||||
"@bertha:bar": 200,
|
"@bertha:bar": 200,
|
||||||
"@invalid:user": 10, // shouldn't barf on this.
|
"@invalid:user": 10, // shouldn't barf on this.
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
event: true,
|
||||||
event: true,
|
});
|
||||||
});
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
member.setPowerLevelEvent(event);
|
member.setPowerLevelEvent(event);
|
||||||
expect(emitCount).toEqual(1);
|
expect(emitCount).toEqual(1);
|
||||||
member.setPowerLevelEvent(event); // no-op
|
member.setPowerLevelEvent(event); // no-op
|
||||||
expect(emitCount).toEqual(1);
|
expect(emitCount).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should honour power levels of zero.",
|
it("should honour power levels of zero.",
|
||||||
function() {
|
function() {
|
||||||
const event = utils.mkEvent({
|
const event = utils.mkEvent({
|
||||||
type: "m.room.power_levels",
|
type: "m.room.power_levels",
|
||||||
room: roomId,
|
room: roomId,
|
||||||
user: userA,
|
user: userA,
|
||||||
content: {
|
content: {
|
||||||
users_default: 20,
|
users_default: 20,
|
||||||
users: {
|
users: {
|
||||||
"@alice:bar": 0,
|
"@alice:bar": 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
event: true,
|
||||||
event: true,
|
});
|
||||||
});
|
let emitCount = 0;
|
||||||
let emitCount = 0;
|
|
||||||
|
|
||||||
// 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);
|
||||||
expect(emitEvent).toEqual(event);
|
expect(emitEvent).toEqual(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
member.setPowerLevelEvent(event);
|
member.setPowerLevelEvent(event);
|
||||||
expect(member.powerLevel).toEqual(0);
|
expect(member.powerLevel).toEqual(0);
|
||||||
expect(emitCount).toEqual(1);
|
expect(emitCount).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not honor string power levels.",
|
it("should not honor string power levels.",
|
||||||
function() {
|
function() {
|
||||||
const event = utils.mkEvent({
|
const event = utils.mkEvent({
|
||||||
type: "m.room.power_levels",
|
type: "m.room.power_levels",
|
||||||
room: roomId,
|
room: roomId,
|
||||||
user: userA,
|
user: userA,
|
||||||
content: {
|
content: {
|
||||||
users_default: 20,
|
users_default: 20,
|
||||||
users: {
|
users: {
|
||||||
"@alice:bar": "5",
|
"@alice:bar": "5",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
event: true,
|
||||||
event: true,
|
});
|
||||||
});
|
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);
|
||||||
expect(emitEvent).toEqual(event);
|
expect(emitEvent).toEqual(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
member.setPowerLevelEvent(event);
|
member.setPowerLevelEvent(event);
|
||||||
expect(member.powerLevel).toEqual(20);
|
expect(member.powerLevel).toEqual(20);
|
||||||
expect(emitCount).toEqual(1);
|
expect(emitCount).toEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("setTypingEvent", function() {
|
describe("setTypingEvent", function() {
|
||||||
@@ -183,34 +200,34 @@ describe("RoomMember", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should emit 'RoomMember.typing' if the typing state changes",
|
it("should emit 'RoomMember.typing' if the typing state changes",
|
||||||
function() {
|
function() {
|
||||||
const event = utils.mkEvent({
|
const event = utils.mkEvent({
|
||||||
type: "m.typing",
|
type: "m.typing",
|
||||||
room: roomId,
|
room: roomId,
|
||||||
content: {
|
content: {
|
||||||
user_ids: [
|
user_ids: [
|
||||||
userA, userC,
|
userA, userC,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
event: true,
|
event: true,
|
||||||
|
});
|
||||||
|
let emitCount = 0;
|
||||||
|
member.on(RoomMemberEvent.Typing, function(ev, mem) {
|
||||||
|
expect(mem).toEqual(member);
|
||||||
|
expect(ev).toEqual(event);
|
||||||
|
emitCount += 1;
|
||||||
|
});
|
||||||
|
member.typing = false;
|
||||||
|
member.setTypingEvent(event);
|
||||||
|
expect(emitCount).toEqual(1);
|
||||||
|
member.setTypingEvent(event); // no-op
|
||||||
|
expect(emitCount).toEqual(1);
|
||||||
});
|
});
|
||||||
let emitCount = 0;
|
|
||||||
member.on("RoomMember.typing", function(ev, mem) {
|
|
||||||
expect(mem).toEqual(member);
|
|
||||||
expect(ev).toEqual(event);
|
|
||||||
emitCount += 1;
|
|
||||||
});
|
|
||||||
member.typing = false;
|
|
||||||
member.setTypingEvent(event);
|
|
||||||
expect(emitCount).toEqual(1);
|
|
||||||
member.setTypingEvent(event); // no-op
|
|
||||||
expect(emitCount).toEqual(1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("isOutOfBand", function() {
|
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);
|
||||||
@@ -235,50 +252,50 @@ describe("RoomMember", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should set 'membership' and assign the event to 'events.member'.",
|
it("should set 'membership' and assign the event to 'events.member'.",
|
||||||
function() {
|
function() {
|
||||||
member.setMembershipEvent(inviteEvent);
|
member.setMembershipEvent(inviteEvent);
|
||||||
expect(member.membership).toEqual("invite");
|
expect(member.membership).toEqual("invite");
|
||||||
expect(member.events.member).toEqual(inviteEvent);
|
expect(member.events.member).toEqual(inviteEvent);
|
||||||
member.setMembershipEvent(joinEvent);
|
member.setMembershipEvent(joinEvent);
|
||||||
expect(member.membership).toEqual("join");
|
expect(member.membership).toEqual("join");
|
||||||
expect(member.events.member).toEqual(joinEvent);
|
expect(member.events.member).toEqual(joinEvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set 'name' based on user_id, displayname and room state",
|
it("should set 'name' based on user_id, displayname and room state",
|
||||||
function() {
|
function() {
|
||||||
const roomState = {
|
const roomState = {
|
||||||
getStateEvents: function(type) {
|
getStateEvents: function(type) {
|
||||||
if (type !== "m.room.member") {
|
if (type !== "m.room.member") {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
event: true, mship: "join", room: roomId,
|
event: true, mship: "join", room: roomId,
|
||||||
user: userB,
|
user: userB,
|
||||||
}),
|
}),
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
event: true, mship: "join", room: roomId,
|
event: true, mship: "join", room: roomId,
|
||||||
user: userC, name: "Alice",
|
user: userC, name: "Alice",
|
||||||
}),
|
}),
|
||||||
joinEvent,
|
joinEvent,
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
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
|
||||||
member.setMembershipEvent(joinEvent, roomState);
|
member.setMembershipEvent(joinEvent, roomState);
|
||||||
expect(member.name).not.toEqual("Alice"); // it should disambig.
|
expect(member.name).not.toEqual("Alice"); // it should disambig.
|
||||||
// user_id should be there somewhere
|
// user_id should be there somewhere
|
||||||
expect(member.name.indexOf(userA)).not.toEqual(-1);
|
expect(member.name.indexOf(userA)).not.toEqual(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit 'RoomMember.membership' if the membership changes", function() {
|
it("should emit 'RoomMember.membership' if the membership changes", function() {
|
||||||
let emitCount = 0;
|
let emitCount = 0;
|
||||||
member.on("RoomMember.membership", function(ev, mem) {
|
member.on(RoomMemberEvent.Membership, function(ev, mem) {
|
||||||
emitCount += 1;
|
emitCount += 1;
|
||||||
expect(mem).toEqual(member);
|
expect(mem).toEqual(member);
|
||||||
expect(ev).toEqual(inviteEvent);
|
expect(ev).toEqual(inviteEvent);
|
||||||
@@ -291,7 +308,7 @@ describe("RoomMember", function() {
|
|||||||
|
|
||||||
it("should emit 'RoomMember.name' if the name changes", function() {
|
it("should emit 'RoomMember.name' if the name changes", function() {
|
||||||
let emitCount = 0;
|
let emitCount = 0;
|
||||||
member.on("RoomMember.name", function(ev, mem) {
|
member.on(RoomMemberEvent.Name, function(ev, mem) {
|
||||||
emitCount += 1;
|
emitCount += 1;
|
||||||
expect(mem).toEqual(member);
|
expect(mem).toEqual(member);
|
||||||
expect(ev).toEqual(joinEvent);
|
expect(ev).toEqual(joinEvent);
|
||||||
@@ -341,7 +358,7 @@ describe("RoomMember", function() {
|
|||||||
getUserIdsWithDisplayName: function(displayName) {
|
getUserIdsWithDisplayName: function(displayName) {
|
||||||
return [userA, userC];
|
return [userA, userC];
|
||||||
},
|
},
|
||||||
};
|
} as unknown as RoomState;
|
||||||
expect(member.name).toEqual(userA); // default = user_id
|
expect(member.name).toEqual(userA); // default = user_id
|
||||||
member.setMembershipEvent(joinEvent, roomState);
|
member.setMembershipEvent(joinEvent, roomState);
|
||||||
expect(member.name).not.toEqual("Alíce"); // it should disambig.
|
expect(member.name).not.toEqual("Alíce"); // it should disambig.
|
@@ -1,14 +1,37 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MockedObject } from 'jest-mock';
|
||||||
|
|
||||||
import * as utils from "../test-utils/test-utils";
|
import * as utils from "../test-utils/test-utils";
|
||||||
import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
|
import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
|
||||||
import { filterEmitCallsByEventType } from "../test-utils/emitter";
|
import { filterEmitCallsByEventType } from "../test-utils/emitter";
|
||||||
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
|
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
|
||||||
import { BeaconEvent, getBeaconInfoIdentifier } from "../../src/models/beacon";
|
import {
|
||||||
|
Beacon,
|
||||||
|
BeaconEvent,
|
||||||
|
getBeaconInfoIdentifier,
|
||||||
|
} from "../../src/models/beacon";
|
||||||
import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@types/event";
|
import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@types/event";
|
||||||
import {
|
import {
|
||||||
MatrixEvent,
|
MatrixEvent,
|
||||||
MatrixEventEvent,
|
MatrixEventEvent,
|
||||||
} from "../../src/models/event";
|
} from "../../src/models/event";
|
||||||
import { M_BEACON } from "../../src/@types/beacon";
|
import { M_BEACON } from "../../src/@types/beacon";
|
||||||
|
import { MatrixClient } from "../../src/client";
|
||||||
|
|
||||||
describe("RoomState", function() {
|
describe("RoomState", function() {
|
||||||
const roomId = "!foo:bar";
|
const roomId = "!foo:bar";
|
||||||
@@ -17,7 +40,7 @@ describe("RoomState", function() {
|
|||||||
const userC = "@cleo:bar";
|
const userC = "@cleo:bar";
|
||||||
const userLazy = "@lazy:bar";
|
const userLazy = "@lazy:bar";
|
||||||
|
|
||||||
let state;
|
let state = new RoomState(roomId);
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
state = new RoomState(roomId);
|
state = new RoomState(roomId);
|
||||||
@@ -67,8 +90,8 @@ describe("RoomState", function() {
|
|||||||
|
|
||||||
it("should return a member which changes as state changes", function() {
|
it("should return a member which changes as state changes", function() {
|
||||||
const member = state.getMember(userB);
|
const member = state.getMember(userB);
|
||||||
expect(member.membership).toEqual("join");
|
expect(member?.membership).toEqual("join");
|
||||||
expect(member.name).toEqual(userB);
|
expect(member?.name).toEqual(userB);
|
||||||
|
|
||||||
state.setStateEvents([
|
state.setStateEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
@@ -77,40 +100,40 @@ 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",
|
||||||
function() {
|
function() {
|
||||||
const preLeaveUser = state.getSentinelMember(userA);
|
const preLeaveUser = state.getSentinelMember(userA);
|
||||||
state.setStateEvents([
|
state.setStateEvents([
|
||||||
utils.mkMembership({
|
utils.mkMembership({
|
||||||
room: roomId, user: userA, mship: "leave", event: true,
|
room: roomId, user: userA, mship: "leave", event: true,
|
||||||
name: "AliceIsGone",
|
name: "AliceIsGone",
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getStateEvents", function() {
|
describe("getStateEvents", function() {
|
||||||
it("should return null if a state_key was specified and there was no match",
|
it("should return null if a state_key was specified and there was no match",
|
||||||
function() {
|
function() {
|
||||||
expect(state.getStateEvents("foo.bar.baz", "keyname")).toEqual(null);
|
expect(state.getStateEvents("foo.bar.baz", "keyname")).toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return an empty list if a state_key was not specified and there" +
|
it("should return an empty list if a state_key was not specified and there" +
|
||||||
" was no match", function() {
|
" was no match", function() {
|
||||||
@@ -118,21 +141,21 @@ describe("RoomState", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return a list of matching events if no state_key was specified",
|
it("should return a list of matching events if no state_key was specified",
|
||||||
function() {
|
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",
|
||||||
function() {
|
function() {
|
||||||
const event = state.getStateEvents("m.room.member", userA);
|
const event = state.getStateEvents("m.room.member", userA);
|
||||||
expect(event.getContent()).toMatchObject({
|
expect(event.getContent()).toMatchObject({
|
||||||
membership: "join",
|
membership: "join",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("setStateEvents", function() {
|
describe("setStateEvents", function() {
|
||||||
@@ -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));
|
||||||
@@ -496,78 +519,80 @@ describe("RoomState", function() {
|
|||||||
|
|
||||||
describe("maySendStateEvent", function() {
|
describe("maySendStateEvent", function() {
|
||||||
it("should say any member may send state with no power level event",
|
it("should say any member may send state with no power level event",
|
||||||
function() {
|
function() {
|
||||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
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,
|
||||||
content: {
|
state_key: "",
|
||||||
users_default: 10,
|
content: {
|
||||||
state_default: 30,
|
users_default: 10,
|
||||||
events_default: 25,
|
state_default: 30,
|
||||||
users: {
|
events_default: 25,
|
||||||
|
users: {
|
||||||
|
[userA]: 30,
|
||||||
|
[userB]: 29,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
};
|
|
||||||
powerLevelEvent.content.users[userA] = 30;
|
|
||||||
powerLevelEvent.content.users[userB] = 29;
|
|
||||||
|
|
||||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
state.setStateEvents([powerLevelEvent]);
|
||||||
|
|
||||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||||
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
|
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
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,
|
||||||
|
},
|
||||||
|
users_default: 10,
|
||||||
|
state_default: 50,
|
||||||
|
events_default: 25,
|
||||||
|
users: {
|
||||||
|
[userA]: 80,
|
||||||
|
[userB]: 50,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
users_default: 10,
|
});
|
||||||
state_default: 50,
|
|
||||||
events_default: 25,
|
|
||||||
users: {
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
powerLevelEvent.content.users[userA] = 80;
|
|
||||||
powerLevelEvent.content.users[userB] = 50;
|
|
||||||
|
|
||||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
state.setStateEvents([powerLevelEvent]);
|
||||||
|
|
||||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||||
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(true);
|
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(true);
|
||||||
|
|
||||||
expect(state.maySendStateEvent('m.room.other_thing', userA)).toEqual(true);
|
expect(state.maySendStateEvent('m.room.other_thing', userA)).toEqual(true);
|
||||||
expect(state.maySendStateEvent('m.room.other_thing', userB)).toEqual(false);
|
expect(state.maySendStateEvent('m.room.other_thing', userB)).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getJoinedMemberCount", function() {
|
describe("getJoinedMemberCount", function() {
|
||||||
@@ -682,71 +707,73 @@ describe("RoomState", function() {
|
|||||||
|
|
||||||
describe("maySendEvent", function() {
|
describe("maySendEvent", function() {
|
||||||
it("should say any member may send events with no power level event",
|
it("should say any member may send events with no power level event",
|
||||||
function() {
|
function() {
|
||||||
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
||||||
expect(state.maySendMessage(userA)).toEqual(true);
|
expect(state.maySendMessage(userA)).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
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,
|
||||||
content: {
|
state_key: "",
|
||||||
users_default: 10,
|
content: {
|
||||||
state_default: 30,
|
users_default: 10,
|
||||||
events_default: 25,
|
state_default: 30,
|
||||||
users: {
|
events_default: 25,
|
||||||
|
users: {
|
||||||
|
[userA]: 26,
|
||||||
|
[userB]: 24,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
};
|
|
||||||
powerLevelEvent.content.users[userA] = 26;
|
|
||||||
powerLevelEvent.content.users[userB] = 24;
|
|
||||||
|
|
||||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
state.setStateEvents([powerLevelEvent]);
|
||||||
|
|
||||||
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
||||||
expect(state.maySendEvent('m.room.message', userB)).toEqual(false);
|
expect(state.maySendEvent('m.room.message', userB)).toEqual(false);
|
||||||
|
|
||||||
expect(state.maySendMessage(userA)).toEqual(true);
|
expect(state.maySendMessage(userA)).toEqual(true);
|
||||||
expect(state.maySendMessage(userB)).toEqual(false);
|
expect(state.maySendMessage(userB)).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
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: "",
|
||||||
events: {
|
content: {
|
||||||
"m.room.other_thing": 33,
|
events: {
|
||||||
|
"m.room.other_thing": 33,
|
||||||
|
},
|
||||||
|
users_default: 10,
|
||||||
|
state_default: 50,
|
||||||
|
events_default: 25,
|
||||||
|
users: {
|
||||||
|
[userA]: 40,
|
||||||
|
[userB]: 30,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
users_default: 10,
|
});
|
||||||
state_default: 50,
|
|
||||||
events_default: 25,
|
|
||||||
users: {
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
powerLevelEvent.content.users[userA] = 40;
|
|
||||||
powerLevelEvent.content.users[userB] = 30;
|
|
||||||
|
|
||||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
state.setStateEvents([powerLevelEvent]);
|
||||||
|
|
||||||
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
||||||
expect(state.maySendEvent('m.room.message', userB)).toEqual(true);
|
expect(state.maySendEvent('m.room.message', userB)).toEqual(true);
|
||||||
|
|
||||||
expect(state.maySendMessage(userA)).toEqual(true);
|
expect(state.maySendMessage(userA)).toEqual(true);
|
||||||
expect(state.maySendMessage(userB)).toEqual(true);
|
expect(state.maySendMessage(userB)).toEqual(true);
|
||||||
|
|
||||||
expect(state.maySendEvent('m.room.other_thing', userA)).toEqual(true);
|
expect(state.maySendEvent('m.room.other_thing', userA)).toEqual(true);
|
||||||
expect(state.maySendEvent('m.room.other_thing', userB)).toEqual(false);
|
expect(state.maySendEvent('m.room.other_thing', userB)).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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]);
|
||||||
});
|
});
|
@@ -288,11 +288,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 +426,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', () => {
|
||||||
|
@@ -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 },
|
||||||
|
@@ -201,6 +201,7 @@ import { Thread, THREAD_RELATION_TYPE } from "./models/thread";
|
|||||||
import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
|
import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
|
||||||
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
|
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
|
||||||
import { ToDeviceBatch } from "./models/ToDeviceMessage";
|
import { ToDeviceBatch } from "./models/ToDeviceMessage";
|
||||||
|
import { IgnoredInvites } from "./models/invites-ignorer";
|
||||||
|
|
||||||
export type Store = IStore;
|
export type Store = IStore;
|
||||||
|
|
||||||
@@ -406,8 +407,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;
|
||||||
|
|
||||||
@@ -974,6 +974,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
private useE2eForGroupCall = true;
|
private useE2eForGroupCall = true;
|
||||||
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();
|
||||||
|
|
||||||
@@ -1159,6 +1162,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, highlightCount);
|
room.setUnreadNotificationCount(NotificationCountType.Highlight, highlightCount);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.ignoredInvites = new IgnoredInvites(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
@@ -1185,7 +1185,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
|||||||
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 / indexes: map from
|
||||||
// senderKey|sessionId to Set of MatrixEvents
|
// senderKey|sessionId to Set of MatrixEvents
|
||||||
private pendingEvents: Record<string, Map<string, Set<MatrixEvent>>> = {};
|
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;
|
||||||
@@ -1337,10 +1337,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());
|
||||||
}
|
}
|
||||||
@@ -1358,7 +1358,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;
|
||||||
@@ -1369,7 +1369,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1710,7 +1710,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
* @return {Boolean} whether all messages were successfully decrypted
|
* @return {Boolean} whether all messages were successfully decrypted
|
||||||
*/
|
*/
|
||||||
private async retryDecryption(senderKey: string, sessionId: string): Promise<boolean> {
|
private async retryDecryption(senderKey: string, sessionId: string): Promise<boolean> {
|
||||||
const senderPendingEvents = this.pendingEvents[senderKey];
|
const senderPendingEvents = this.pendingEvents.get(senderKey);
|
||||||
if (!senderPendingEvents) {
|
if (!senderPendingEvents) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1731,16 +1731,16 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// If decrypted successfully, they'll have been removed from pendingEvents
|
// If decrypted successfully, they'll have been removed from pendingEvents
|
||||||
return !this.pendingEvents[senderKey]?.has(sessionId);
|
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) => {
|
||||||
@@ -1752,7 +1752,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> {
|
||||||
|
@@ -301,9 +301,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
|
||||||
|
|
||||||
@@ -445,7 +445,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,
|
||||||
@@ -2550,7 +2550,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");
|
||||||
@@ -2603,7 +2603,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;
|
||||||
}
|
}
|
||||||
@@ -2617,7 +2617,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);
|
||||||
}
|
}
|
||||||
@@ -2631,7 +2631,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;
|
||||||
@@ -2663,7 +2663,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);
|
||||||
@@ -2808,7 +2808,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);
|
||||||
}
|
}
|
||||||
@@ -2831,7 +2831,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.
|
||||||
@@ -3120,7 +3120,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;
|
||||||
}
|
}
|
||||||
@@ -3556,7 +3556,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;
|
||||||
@@ -3657,11 +3657,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}`);
|
||||||
@@ -3697,12 +3697,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;
|
||||||
@@ -3768,23 +3768,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',
|
||||||
@@ -3800,7 +3801,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (decryptors) {
|
if (decryptors) {
|
||||||
decryptors[algorithm] = alg;
|
decryptors.set(algorithm, alg);
|
||||||
}
|
}
|
||||||
return alg;
|
return alg;
|
||||||
}
|
}
|
||||||
@@ -3814,9 +3815,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;
|
||||||
|
@@ -26,7 +26,7 @@ import {
|
|||||||
Mode,
|
Mode,
|
||||||
OutgoingRoomKeyRequest,
|
OutgoingRoomKeyRequest,
|
||||||
} 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";
|
||||||
@@ -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();
|
||||||
|
@@ -191,11 +191,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,7 +138,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 +210,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 +220,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 +257,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);
|
||||||
}
|
}
|
||||||
@@ -288,7 +288,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
|||||||
* the given event, or null if unknown
|
* the given event, or null if unknown
|
||||||
*/
|
*/
|
||||||
public getTimelineForEvent(eventId: string): EventTimeline | null {
|
public getTimelineForEvent(eventId: string): EventTimeline | null {
|
||||||
const res = this._eventIdToTimeline[eventId];
|
const res = this._eventIdToTimeline.get(eventId);
|
||||||
return (res === undefined) ? null : res;
|
return (res === undefined) ? null : res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,7 +450,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 +601,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 +697,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,22 +725,14 @@ 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) {
|
this.addEventToTimeline(localEvent, this.liveTimeline, {
|
||||||
if (this.filter.filterRoomTimeline([localEvent]).length) {
|
toStartOfTimeline: false,
|
||||||
this.addEventToTimeline(localEvent, this.liveTimeline, {
|
});
|
||||||
toStartOfTimeline: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.addEventToTimeline(localEvent, this.liveTimeline, {
|
|
||||||
toStartOfTimeline: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,14 +745,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 +779,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;
|
||||||
|
@@ -26,7 +26,7 @@ import { logger } from '../logger';
|
|||||||
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
|
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
|
||||||
import { EVENT_VISIBILITY_CHANGE_TYPE, EventType, MsgType, RelationType } from "../@types/event";
|
import { EVENT_VISIBILITY_CHANGE_TYPE, EventType, MsgType, RelationType } from "../@types/event";
|
||||||
import { Crypto, IEventDecryptionResult } from "../crypto";
|
import { Crypto, IEventDecryptionResult } from "../crypto";
|
||||||
import { deepSortedObjectEntries } from "../utils";
|
import { deepSortedObjectEntries, internaliseString } from "../utils";
|
||||||
import { RoomMember } from "./room-member";
|
import { RoomMember } from "./room-member";
|
||||||
import { Thread, ThreadEvent, EventHandlerMap as ThreadEventHandlerMap, THREAD_RELATION_TYPE } from "./thread";
|
import { Thread, ThreadEvent, EventHandlerMap as ThreadEventHandlerMap, THREAD_RELATION_TYPE } from "./thread";
|
||||||
import { IActionsObject } from '../pushprocessor';
|
import { IActionsObject } from '../pushprocessor';
|
||||||
@@ -37,14 +37,6 @@ import { EventStatus } from "./event-status";
|
|||||||
|
|
||||||
export { EventStatus } from "./event-status";
|
export { EventStatus } from "./event-status";
|
||||||
|
|
||||||
const interns: Record<string, string> = {};
|
|
||||||
function intern(str: string): string {
|
|
||||||
if (!interns[str]) {
|
|
||||||
interns[str] = str;
|
|
||||||
}
|
|
||||||
return interns[str];
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
export interface IContent {
|
export interface IContent {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
@@ -326,17 +318,17 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
|||||||
// of space if we don't intern it.
|
// of space if we don't intern it.
|
||||||
["state_key", "type", "sender", "room_id", "membership"].forEach((prop) => {
|
["state_key", "type", "sender", "room_id", "membership"].forEach((prop) => {
|
||||||
if (typeof event[prop] !== "string") return;
|
if (typeof event[prop] !== "string") return;
|
||||||
event[prop] = intern(event[prop]);
|
event[prop] = internaliseString(event[prop]);
|
||||||
});
|
});
|
||||||
|
|
||||||
["membership", "avatar_url", "displayname"].forEach((prop) => {
|
["membership", "avatar_url", "displayname"].forEach((prop) => {
|
||||||
if (typeof event.content?.[prop] !== "string") return;
|
if (typeof event.content?.[prop] !== "string") return;
|
||||||
event.content[prop] = intern(event.content[prop]);
|
event.content[prop] = internaliseString(event.content[prop]);
|
||||||
});
|
});
|
||||||
|
|
||||||
["rel_type"].forEach((prop) => {
|
["rel_type"].forEach((prop) => {
|
||||||
if (typeof event.content?.["m.relates_to"]?.[prop] !== "string") return;
|
if (typeof event.content?.["m.relates_to"]?.[prop] !== "string") return;
|
||||||
event.content["m.relates_to"][prop] = intern(event.content["m.relates_to"][prop]);
|
event.content["m.relates_to"][prop] = internaliseString(event.content["m.relates_to"][prop]);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.txnId = event.txn_id || null;
|
this.txnId = event.txn_id || null;
|
||||||
@@ -796,6 +788,8 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
|||||||
// not a decryption error: log the whole exception as an error
|
// not a decryption error: log the whole exception as an error
|
||||||
// (and don't bother with a retry)
|
// (and don't bother with a retry)
|
||||||
const re = options.isRetry ? 're' : '';
|
const re = options.isRetry ? 're' : '';
|
||||||
|
// For find results: this can produce "Error decrypting event (id=$ev)" and
|
||||||
|
// "Error redecrypting event (id=$ev)".
|
||||||
logger.error(
|
logger.error(
|
||||||
`Error ${re}decrypting event ` +
|
`Error ${re}decrypting event ` +
|
||||||
`(id=${this.getId()}): ${e.stack || e}`,
|
`(id=${this.getId()}): ${e.stack || e}`,
|
||||||
|
359
src/models/invites-ignorer.ts
Normal file
359
src/models/invites-ignorer.ts
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
/*
|
||||||
|
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 { UnstableValue } from "matrix-events-sdk";
|
||||||
|
|
||||||
|
import { MatrixClient } from "../client";
|
||||||
|
import { EventTimeline, MatrixEvent, Preset } from "../matrix";
|
||||||
|
import { globToRegexp } from "../utils";
|
||||||
|
import { Room } from "./room";
|
||||||
|
|
||||||
|
/// The event type storing the user's individual policies.
|
||||||
|
///
|
||||||
|
/// Exported for testing purposes.
|
||||||
|
export const POLICIES_ACCOUNT_EVENT_TYPE = new UnstableValue("m.policies", "org.matrix.msc3847.policies");
|
||||||
|
|
||||||
|
/// The key within the user's individual policies storing the user's ignored invites.
|
||||||
|
///
|
||||||
|
/// Exported for testing purposes.
|
||||||
|
export const IGNORE_INVITES_ACCOUNT_EVENT_KEY = new UnstableValue("m.ignore.invites",
|
||||||
|
"org.matrix.msc3847.ignore.invites");
|
||||||
|
|
||||||
|
/// The types of recommendations understood.
|
||||||
|
enum PolicyRecommendation {
|
||||||
|
Ban = "m.ban",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The various scopes for policies.
|
||||||
|
*/
|
||||||
|
export enum PolicyScope {
|
||||||
|
/**
|
||||||
|
* The policy deals with an individual user, e.g. reject invites
|
||||||
|
* from this user.
|
||||||
|
*/
|
||||||
|
User = "m.policy.user",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The policy deals with a room, e.g. reject invites towards
|
||||||
|
* a specific room.
|
||||||
|
*/
|
||||||
|
Room = "m.policy.room",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The policy deals with a server, e.g. reject invites from
|
||||||
|
* this server.
|
||||||
|
*/
|
||||||
|
Server = "m.policy.server",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container for ignored invites.
|
||||||
|
*
|
||||||
|
* # Performance
|
||||||
|
*
|
||||||
|
* This implementation is extremely naive. It expects that we are dealing
|
||||||
|
* with a very short list of sources (e.g. only one). If real-world
|
||||||
|
* applications turn out to require longer lists, we may need to rework
|
||||||
|
* our data structures.
|
||||||
|
*/
|
||||||
|
export class IgnoredInvites {
|
||||||
|
constructor(
|
||||||
|
private readonly client: MatrixClient,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new rule.
|
||||||
|
*
|
||||||
|
* @param scope The scope for this rule.
|
||||||
|
* @param entity The entity covered by this rule. Globs are supported.
|
||||||
|
* @param reason A human-readable reason for introducing this new rule.
|
||||||
|
* @return The event id for the new rule.
|
||||||
|
*/
|
||||||
|
public async addRule(scope: PolicyScope, entity: string, reason: string): Promise<string> {
|
||||||
|
const target = await this.getOrCreateTargetRoom();
|
||||||
|
const response = await this.client.sendStateEvent(target.roomId, scope, {
|
||||||
|
entity,
|
||||||
|
reason,
|
||||||
|
recommendation: PolicyRecommendation.Ban,
|
||||||
|
});
|
||||||
|
return response.event_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a rule.
|
||||||
|
*/
|
||||||
|
public async removeRule(event: MatrixEvent) {
|
||||||
|
await this.client.redactEvent(event.getRoomId()!, event.getId()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new room to the list of sources. If the user isn't a member of the
|
||||||
|
* room, attempt to join it.
|
||||||
|
*
|
||||||
|
* @param roomId A valid room id. If this room is already in the list
|
||||||
|
* of sources, it will not be duplicated.
|
||||||
|
* @return `true` if the source was added, `false` if it was already present.
|
||||||
|
* @throws If `roomId` isn't the id of a room that the current user is already
|
||||||
|
* member of or can join.
|
||||||
|
*
|
||||||
|
* # Safety
|
||||||
|
*
|
||||||
|
* This method will rewrite the `Policies` object in the user's account data.
|
||||||
|
* This rewrite is inherently racy and could overwrite or be overwritten by
|
||||||
|
* other concurrent rewrites of the same object.
|
||||||
|
*/
|
||||||
|
public async addSource(roomId: string): Promise<boolean> {
|
||||||
|
// We attempt to join the room *before* calling
|
||||||
|
// `await this.getOrCreateSourceRooms()` to decrease the duration
|
||||||
|
// of the racy section.
|
||||||
|
await this.client.joinRoom(roomId);
|
||||||
|
// Race starts.
|
||||||
|
const sources = (await this.getOrCreateSourceRooms())
|
||||||
|
.map(room => room.roomId);
|
||||||
|
if (sources.includes(roomId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
sources.push(roomId);
|
||||||
|
await this.withIgnoreInvitesPolicies(ignoreInvitesPolicies => {
|
||||||
|
ignoreInvitesPolicies.sources = sources;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Race ends.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find out whether an invite should be ignored.
|
||||||
|
*
|
||||||
|
* @param sender The user id for the user who issued the invite.
|
||||||
|
* @param roomId The room to which the user is invited.
|
||||||
|
* @returns A rule matching the entity, if any was found, `null` otherwise.
|
||||||
|
*/
|
||||||
|
public async getRuleForInvite({ sender, roomId }: {
|
||||||
|
sender: string;
|
||||||
|
roomId: string;
|
||||||
|
}): Promise<Readonly<MatrixEvent | null>> {
|
||||||
|
// In this implementation, we perform a very naive lookup:
|
||||||
|
// - search in each policy room;
|
||||||
|
// - turn each (potentially glob) rule entity into a regexp.
|
||||||
|
//
|
||||||
|
// Real-world testing will tell us whether this is performant enough.
|
||||||
|
// In the (unfortunately likely) case it isn't, there are several manners
|
||||||
|
// in which we could optimize this:
|
||||||
|
// - match several entities per go;
|
||||||
|
// - pre-compile each rule entity into a regexp;
|
||||||
|
// - pre-compile entire rooms into a single regexp.
|
||||||
|
const policyRooms = await this.getOrCreateSourceRooms();
|
||||||
|
const senderServer = sender.split(":")[1];
|
||||||
|
const roomServer = roomId.split(":")[1];
|
||||||
|
for (const room of policyRooms) {
|
||||||
|
const state = room.getUnfilteredTimelineSet().getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||||
|
|
||||||
|
for (const { scope, entities } of [
|
||||||
|
{ scope: PolicyScope.Room, entities: [roomId] },
|
||||||
|
{ scope: PolicyScope.User, entities: [sender] },
|
||||||
|
{ scope: PolicyScope.Server, entities: [senderServer, roomServer] },
|
||||||
|
]) {
|
||||||
|
const events = state.getStateEvents(scope);
|
||||||
|
for (const event of events) {
|
||||||
|
const content = event.getContent();
|
||||||
|
if (content?.recommendation != PolicyRecommendation.Ban) {
|
||||||
|
// Ignoring invites only looks at `m.ban` recommendations.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const glob = content?.entity;
|
||||||
|
if (!glob) {
|
||||||
|
// Invalid event.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let regexp: RegExp;
|
||||||
|
try {
|
||||||
|
regexp = new RegExp(globToRegexp(glob, false));
|
||||||
|
} catch (ex) {
|
||||||
|
// Assume invalid event.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const entity of entities) {
|
||||||
|
if (entity && regexp.test(entity)) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No match.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the target room, i.e. the room in which any new rule should be written.
|
||||||
|
*
|
||||||
|
* If there is no target room setup, a target room is created.
|
||||||
|
*
|
||||||
|
* Note: This method is public for testing reasons. Most clients should not need
|
||||||
|
* to call it directly.
|
||||||
|
*
|
||||||
|
* # Safety
|
||||||
|
*
|
||||||
|
* This method will rewrite the `Policies` object in the user's account data.
|
||||||
|
* This rewrite is inherently racy and could overwrite or be overwritten by
|
||||||
|
* other concurrent rewrites of the same object.
|
||||||
|
*/
|
||||||
|
public async getOrCreateTargetRoom(): Promise<Room> {
|
||||||
|
const ignoreInvitesPolicies = this.getIgnoreInvitesPolicies();
|
||||||
|
let target = ignoreInvitesPolicies.target;
|
||||||
|
// Validate `target`. If it is invalid, trash out the current `target`
|
||||||
|
// and create a new room.
|
||||||
|
if (typeof target !== "string") {
|
||||||
|
target = null;
|
||||||
|
}
|
||||||
|
if (target) {
|
||||||
|
// Check that the room exists and is valid.
|
||||||
|
const room = this.client.getRoom(target);
|
||||||
|
if (room) {
|
||||||
|
return room;
|
||||||
|
} else {
|
||||||
|
target = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We need to create our own policy room for ignoring invites.
|
||||||
|
target = (await this.client.createRoom({
|
||||||
|
name: "Individual Policy Room",
|
||||||
|
preset: Preset.PrivateChat,
|
||||||
|
})).room_id;
|
||||||
|
await this.withIgnoreInvitesPolicies(ignoreInvitesPolicies => {
|
||||||
|
ignoreInvitesPolicies.target = target;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Since we have just called `createRoom`, `getRoom` should not be `null`.
|
||||||
|
return this.client.getRoom(target)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of source rooms, i.e. the rooms from which rules need to be read.
|
||||||
|
*
|
||||||
|
* If no source rooms are setup, the target room is used as sole source room.
|
||||||
|
*
|
||||||
|
* Note: This method is public for testing reasons. Most clients should not need
|
||||||
|
* to call it directly.
|
||||||
|
*
|
||||||
|
* # Safety
|
||||||
|
*
|
||||||
|
* This method will rewrite the `Policies` object in the user's account data.
|
||||||
|
* This rewrite is inherently racy and could overwrite or be overwritten by
|
||||||
|
* other concurrent rewrites of the same object.
|
||||||
|
*/
|
||||||
|
public async getOrCreateSourceRooms(): Promise<Room[]> {
|
||||||
|
const ignoreInvitesPolicies = this.getIgnoreInvitesPolicies();
|
||||||
|
let sources = ignoreInvitesPolicies.sources;
|
||||||
|
|
||||||
|
// Validate `sources`. If it is invalid, trash out the current `sources`
|
||||||
|
// and create a new list of sources from `target`.
|
||||||
|
let hasChanges = false;
|
||||||
|
if (!Array.isArray(sources)) {
|
||||||
|
// `sources` could not be an array.
|
||||||
|
hasChanges = true;
|
||||||
|
sources = [];
|
||||||
|
}
|
||||||
|
let sourceRooms: Room[] = sources
|
||||||
|
// `sources` could contain non-string / invalid room ids
|
||||||
|
.filter(roomId => typeof roomId === "string")
|
||||||
|
.map(roomId => this.client.getRoom(roomId))
|
||||||
|
.filter(room => !!room);
|
||||||
|
if (sourceRooms.length != sources.length) {
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
if (sourceRooms.length == 0) {
|
||||||
|
// `sources` could be empty (possibly because we've removed
|
||||||
|
// invalid content)
|
||||||
|
const target = await this.getOrCreateTargetRoom();
|
||||||
|
hasChanges = true;
|
||||||
|
sourceRooms = [target];
|
||||||
|
}
|
||||||
|
if (hasChanges) {
|
||||||
|
// Reload `policies`/`ignoreInvitesPolicies` in case it has been changed
|
||||||
|
// during or by our call to `this.getTargetRoom()`.
|
||||||
|
await this.withIgnoreInvitesPolicies(ignoreInvitesPolicies => {
|
||||||
|
ignoreInvitesPolicies.sources = sources;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return sourceRooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the `IGNORE_INVITES_POLICIES` object from account data.
|
||||||
|
*
|
||||||
|
* If both an unstable prefix version and a stable prefix version are available,
|
||||||
|
* it will return the stable prefix version preferentially.
|
||||||
|
*
|
||||||
|
* The result is *not* validated but is guaranteed to be a non-null object.
|
||||||
|
*
|
||||||
|
* @returns A non-null object.
|
||||||
|
*/
|
||||||
|
private getIgnoreInvitesPolicies(): {[key: string]: any} {
|
||||||
|
return this.getPoliciesAndIgnoreInvitesPolicies().ignoreInvitesPolicies;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modify in place the `IGNORE_INVITES_POLICIES` object from account data.
|
||||||
|
*/
|
||||||
|
private async withIgnoreInvitesPolicies(cb: (ignoreInvitesPolicies: {[key: string]: any}) => void) {
|
||||||
|
const { policies, ignoreInvitesPolicies } = this.getPoliciesAndIgnoreInvitesPolicies();
|
||||||
|
cb(ignoreInvitesPolicies);
|
||||||
|
policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name] = ignoreInvitesPolicies;
|
||||||
|
await this.client.setAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name, policies);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* As `getIgnoreInvitesPolicies` but also return the `POLICIES_ACCOUNT_EVENT_TYPE`
|
||||||
|
* object.
|
||||||
|
*/
|
||||||
|
private getPoliciesAndIgnoreInvitesPolicies():
|
||||||
|
{policies: {[key: string]: any}, ignoreInvitesPolicies: {[key: string]: any}} {
|
||||||
|
let policies = {};
|
||||||
|
for (const key of [POLICIES_ACCOUNT_EVENT_TYPE.name, POLICIES_ACCOUNT_EVENT_TYPE.altName]) {
|
||||||
|
if (!key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const value = this.client.getAccountData(key)?.getContent();
|
||||||
|
if (value) {
|
||||||
|
policies = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ignoreInvitesPolicies = {};
|
||||||
|
let hasIgnoreInvitesPolicies = false;
|
||||||
|
for (const key of [IGNORE_INVITES_ACCOUNT_EVENT_KEY.name, IGNORE_INVITES_ACCOUNT_EVENT_KEY.altName]) {
|
||||||
|
if (!key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const value = policies[key];
|
||||||
|
if (value && typeof value == "object") {
|
||||||
|
ignoreInvitesPolicies = value;
|
||||||
|
hasIgnoreInvitesPolicies = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasIgnoreInvitesPolicies) {
|
||||||
|
policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name] = ignoreInvitesPolicies;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { policies, ignoreInvitesPolicies };
|
||||||
|
}
|
||||||
|
}
|
@@ -23,14 +23,8 @@ import { Room } from "./room";
|
|||||||
|
|
||||||
export class RelationsContainer {
|
export class RelationsContainer {
|
||||||
// A tree of objects to access a set of related children for an event, as in:
|
// A tree of objects to access a set of related children for an event, as in:
|
||||||
// this.relations[parentEventId][relationType][relationEventType]
|
// this.relations.get(parentEventId).get(relationType).get(relationEventType)
|
||||||
private relations: {
|
private relations = new Map<string, Map<RelationType | string, Map<EventType | string, Relations>>>();
|
||||||
[parentEventId: string]: {
|
|
||||||
[relationType: RelationType | string]: {
|
|
||||||
[eventType: EventType | string]: Relations;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
constructor(private readonly client: MatrixClient, private readonly room?: Room) {
|
constructor(private readonly client: MatrixClient, private readonly room?: Room) {
|
||||||
}
|
}
|
||||||
@@ -57,14 +51,15 @@ export class RelationsContainer {
|
|||||||
relationType: RelationType | string,
|
relationType: RelationType | string,
|
||||||
eventType: EventType | string,
|
eventType: EventType | string,
|
||||||
): Relations | undefined {
|
): Relations | undefined {
|
||||||
return this.relations[eventId]?.[relationType]?.[eventType];
|
return this.relations.get(eventId)?.get(relationType)?.get(eventType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllChildEventsForEvent(parentEventId: string): MatrixEvent[] {
|
public getAllChildEventsForEvent(parentEventId: string): MatrixEvent[] {
|
||||||
const relationsForEvent = this.relations[parentEventId] ?? {};
|
const relationsForEvent = this.relations.get(parentEventId)
|
||||||
|
?? new Map<RelationType | string, Map<EventType | string, Relations>>();
|
||||||
const events: MatrixEvent[] = [];
|
const events: MatrixEvent[] = [];
|
||||||
for (const relationsRecord of Object.values(relationsForEvent)) {
|
for (const relationsRecord of relationsForEvent.values()) {
|
||||||
for (const relations of Object.values(relationsRecord)) {
|
for (const relations of relationsRecord.values()) {
|
||||||
events.push(...relations.getRelations());
|
events.push(...relations.getRelations());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,11 +74,11 @@ export class RelationsContainer {
|
|||||||
* @param {MatrixEvent} event The event to check as relation target.
|
* @param {MatrixEvent} event The event to check as relation target.
|
||||||
*/
|
*/
|
||||||
public aggregateParentEvent(event: MatrixEvent): void {
|
public aggregateParentEvent(event: MatrixEvent): void {
|
||||||
const relationsForEvent = this.relations[event.getId()];
|
const relationsForEvent = this.relations.get(event.getId());
|
||||||
if (!relationsForEvent) return;
|
if (!relationsForEvent) return;
|
||||||
|
|
||||||
for (const relationsWithRelType of Object.values(relationsForEvent)) {
|
for (const relationsWithRelType of relationsForEvent.values()) {
|
||||||
for (const relationsWithEventType of Object.values(relationsWithRelType)) {
|
for (const relationsWithEventType of relationsWithRelType.values()) {
|
||||||
relationsWithEventType.setTargetEvent(event);
|
relationsWithEventType.setTargetEvent(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,23 +118,26 @@ export class RelationsContainer {
|
|||||||
const { event_id: relatesToEventId, rel_type: relationType } = relation;
|
const { event_id: relatesToEventId, rel_type: relationType } = relation;
|
||||||
const eventType = event.getType();
|
const eventType = event.getType();
|
||||||
|
|
||||||
let relationsForEvent = this.relations[relatesToEventId];
|
let relationsForEvent = this.relations.get(relatesToEventId);
|
||||||
if (!relationsForEvent) {
|
if (!relationsForEvent) {
|
||||||
relationsForEvent = this.relations[relatesToEventId] = {};
|
relationsForEvent = new Map<RelationType | string, Map<EventType | string, Relations>>();
|
||||||
|
this.relations.set(relatesToEventId, relationsForEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
let relationsWithRelType = relationsForEvent[relationType];
|
let relationsWithRelType = relationsForEvent.get(relationType);
|
||||||
if (!relationsWithRelType) {
|
if (!relationsWithRelType) {
|
||||||
relationsWithRelType = relationsForEvent[relationType] = {};
|
relationsWithRelType = new Map<EventType | string, Relations>();
|
||||||
|
relationsForEvent.set(relationType, relationsWithRelType);
|
||||||
}
|
}
|
||||||
|
|
||||||
let relationsWithEventType = relationsWithRelType[eventType];
|
let relationsWithEventType = relationsWithRelType.get(eventType);
|
||||||
if (!relationsWithEventType) {
|
if (!relationsWithEventType) {
|
||||||
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
|
relationsWithEventType = new Relations(
|
||||||
relationType,
|
relationType,
|
||||||
eventType,
|
eventType,
|
||||||
this.client,
|
this.client,
|
||||||
);
|
);
|
||||||
|
relationsWithRelType.set(eventType, relationsWithEventType);
|
||||||
|
|
||||||
const room = this.room ?? timelineSet?.room;
|
const room = this.room ?? timelineSet?.room;
|
||||||
const relatesToEvent = timelineSet?.findEventById(relatesToEventId)
|
const relatesToEvent = timelineSet?.findEventById(relatesToEventId)
|
||||||
|
@@ -79,7 +79,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
|||||||
public readonly reEmitter = new TypedReEmitter<EmittedEvents, EventHandlerMap>(this);
|
public readonly reEmitter = new TypedReEmitter<EmittedEvents, EventHandlerMap>(this);
|
||||||
private sentinels: Record<string, RoomMember> = {}; // userId: RoomMember
|
private sentinels: Record<string, RoomMember> = {}; // userId: RoomMember
|
||||||
// stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys)
|
// stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys)
|
||||||
private displayNameToUserIds: Record<string, string[]> = {};
|
private displayNameToUserIds = new Map<string, string[]>();
|
||||||
private userIdsToDisplayNames: Record<string, string> = {};
|
private userIdsToDisplayNames: Record<string, string> = {};
|
||||||
private tokenToInvite: Record<string, MatrixEvent> = {}; // 3pid invite state_key to m.room.member invite
|
private tokenToInvite: Record<string, MatrixEvent> = {}; // 3pid invite state_key to m.room.member invite
|
||||||
private joinedMemberCount: number = null; // cache of the number of joined members
|
private joinedMemberCount: number = null; // cache of the number of joined members
|
||||||
@@ -709,7 +709,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
|||||||
* @return {string[]} An array of user IDs or an empty array.
|
* @return {string[]} An array of user IDs or an empty array.
|
||||||
*/
|
*/
|
||||||
public getUserIdsWithDisplayName(displayName: string): string[] {
|
public getUserIdsWithDisplayName(displayName: string): string[] {
|
||||||
return this.displayNameToUserIds[utils.removeHiddenChars(displayName)] || [];
|
return this.displayNameToUserIds.get(utils.removeHiddenChars(displayName)) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -941,11 +941,11 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
|||||||
// the lot.
|
// the lot.
|
||||||
const strippedOldName = utils.removeHiddenChars(oldName);
|
const strippedOldName = utils.removeHiddenChars(oldName);
|
||||||
|
|
||||||
const existingUserIds = this.displayNameToUserIds[strippedOldName];
|
const existingUserIds = this.displayNameToUserIds.get(strippedOldName);
|
||||||
if (existingUserIds) {
|
if (existingUserIds) {
|
||||||
// remove this user ID from this array
|
// remove this user ID from this array
|
||||||
const filteredUserIDs = existingUserIds.filter((id) => id !== userId);
|
const filteredUserIDs = existingUserIds.filter((id) => id !== userId);
|
||||||
this.displayNameToUserIds[strippedOldName] = filteredUserIDs;
|
this.displayNameToUserIds.set(strippedOldName, filteredUserIDs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -954,10 +954,9 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
|||||||
const strippedDisplayname = displayName && utils.removeHiddenChars(displayName);
|
const strippedDisplayname = displayName && utils.removeHiddenChars(displayName);
|
||||||
// an empty stripped displayname (undefined/'') will be set to MXID in room-member.js
|
// an empty stripped displayname (undefined/'') will be set to MXID in room-member.js
|
||||||
if (strippedDisplayname) {
|
if (strippedDisplayname) {
|
||||||
if (!this.displayNameToUserIds[strippedDisplayname]) {
|
const arr = this.displayNameToUserIds.get(strippedDisplayname) ?? [];
|
||||||
this.displayNameToUserIds[strippedDisplayname] = [];
|
arr.push(userId);
|
||||||
}
|
this.displayNameToUserIds.set(strippedDisplayname, arr);
|
||||||
this.displayNameToUserIds[strippedDisplayname].push(userId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1981,14 +1981,6 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.getUnsigned().transaction_id) {
|
|
||||||
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id];
|
|
||||||
if (existingEvent) {
|
|
||||||
// remote echo of an event we sent earlier
|
|
||||||
this.handleRemoteEcho(event, existingEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1996,7 +1988,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
* "Room.timeline".
|
* "Room.timeline".
|
||||||
*
|
*
|
||||||
* @param {MatrixEvent} event Event to be added
|
* @param {MatrixEvent} event Event to be added
|
||||||
* @param {IAddLiveEventOptions} options addLiveEvent options
|
* @param {IAddLiveEventOptions} addLiveEventOptions addLiveEvent options
|
||||||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
@@ -2344,7 +2336,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
fromCache = false,
|
fromCache = false,
|
||||||
): void {
|
): void {
|
||||||
let duplicateStrategy = duplicateStrategyOrOpts as DuplicateStrategy;
|
let duplicateStrategy = duplicateStrategyOrOpts as DuplicateStrategy;
|
||||||
let timelineWasEmpty: boolean;
|
let timelineWasEmpty = false;
|
||||||
if (typeof (duplicateStrategyOrOpts) === 'object') {
|
if (typeof (duplicateStrategyOrOpts) === 'object') {
|
||||||
({
|
({
|
||||||
duplicateStrategy,
|
duplicateStrategy,
|
||||||
@@ -2383,10 +2375,25 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
const threadRoots = this.findThreadRoots(events);
|
const threadRoots = this.findThreadRoots(events);
|
||||||
const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
|
const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
|
||||||
|
|
||||||
|
const options: IAddLiveEventOptions = {
|
||||||
|
duplicateStrategy,
|
||||||
|
fromCache,
|
||||||
|
timelineWasEmpty,
|
||||||
|
};
|
||||||
|
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
// TODO: We should have a filter to say "only add state event types X Y Z to the timeline".
|
// TODO: We should have a filter to say "only add state event types X Y Z to the timeline".
|
||||||
this.processLiveEvent(event);
|
this.processLiveEvent(event);
|
||||||
|
|
||||||
|
if (event.getUnsigned().transaction_id) {
|
||||||
|
const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id!];
|
||||||
|
if (existingEvent) {
|
||||||
|
// remote echo of an event we sent earlier
|
||||||
|
this.handleRemoteEcho(event, existingEvent);
|
||||||
|
continue; // we can skip adding the event to the timeline sets, it is already there
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
shouldLiveInRoom,
|
shouldLiveInRoom,
|
||||||
shouldLiveInThread,
|
shouldLiveInThread,
|
||||||
@@ -2399,11 +2406,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
|||||||
eventsByThread[threadId]?.push(event);
|
eventsByThread[threadId]?.push(event);
|
||||||
|
|
||||||
if (shouldLiveInRoom) {
|
if (shouldLiveInRoom) {
|
||||||
this.addLiveEvent(event, {
|
this.addLiveEvent(event, options);
|
||||||
duplicateStrategy,
|
|
||||||
fromCache,
|
|
||||||
timelineWasEmpty,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -296,13 +296,16 @@ export class SlidingSyncSdk {
|
|||||||
this.processRoomData(this.client, room, roomData);
|
this.processRoomData(this.client, room, roomData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onLifecycle(state: SlidingSyncState, resp: MSC3575SlidingSyncResponse, err?: Error): void {
|
private onLifecycle(state: SlidingSyncState, resp: MSC3575SlidingSyncResponse | null, err: Error | null): void {
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.debug("onLifecycle", state, err);
|
logger.debug("onLifecycle", state, err);
|
||||||
}
|
}
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case SlidingSyncState.Complete:
|
case SlidingSyncState.Complete:
|
||||||
this.purgeNotifications();
|
this.purgeNotifications();
|
||||||
|
if (!resp) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
// Element won't stop showing the initial loading spinner unless we fire SyncState.Prepared
|
// Element won't stop showing the initial loading spinner unless we fire SyncState.Prepared
|
||||||
if (!this.lastPos) {
|
if (!this.lastPos) {
|
||||||
this.updateSyncState(SyncState.Prepared, {
|
this.updateSyncState(SyncState.Prepared, {
|
||||||
@@ -529,6 +532,13 @@ export class SlidingSyncSdk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Number.isInteger(roomData.invited_count)) {
|
||||||
|
room.currentState.setInvitedMemberCount(roomData.invited_count!);
|
||||||
|
}
|
||||||
|
if (Number.isInteger(roomData.joined_count)) {
|
||||||
|
room.currentState.setJoinedMemberCount(roomData.joined_count!);
|
||||||
|
}
|
||||||
|
|
||||||
if (roomData.invite_state) {
|
if (roomData.invite_state) {
|
||||||
const inviteStateEvents = mapEvents(this.client, room.roomId, roomData.invite_state);
|
const inviteStateEvents = mapEvents(this.client, room.roomId, roomData.invite_state);
|
||||||
this.injectRoomEvents(room, inviteStateEvents);
|
this.injectRoomEvents(room, inviteStateEvents);
|
||||||
@@ -609,6 +619,10 @@ export class SlidingSyncSdk {
|
|||||||
// we deliberately don't add ephemeral events to the timeline
|
// we deliberately don't add ephemeral events to the timeline
|
||||||
room.addEphemeralEvents(ephemeralEvents);
|
room.addEphemeralEvents(ephemeralEvents);
|
||||||
|
|
||||||
|
// local fields must be set before any async calls because call site assumes
|
||||||
|
// synchronous execution prior to emitting SlidingSyncState.Complete
|
||||||
|
room.updateMyMembership("join");
|
||||||
|
|
||||||
room.recalculate();
|
room.recalculate();
|
||||||
if (roomData.initial) {
|
if (roomData.initial) {
|
||||||
client.store.storeRoom(room);
|
client.store.storeRoom(room);
|
||||||
@@ -632,8 +646,6 @@ export class SlidingSyncSdk {
|
|||||||
client.emit(ClientEvent.Event, e);
|
client.emit(ClientEvent.Event, e);
|
||||||
});
|
});
|
||||||
|
|
||||||
room.updateMyMembership("join");
|
|
||||||
|
|
||||||
// Decrypt only the last message in all rooms to make sure we can generate a preview
|
// Decrypt only the last message in all rooms to make sure we can generate a preview
|
||||||
// And decrypt all events after the recorded read receipt to ensure an accurate
|
// And decrypt all events after the recorded read receipt to ensure an accurate
|
||||||
// notification count
|
// notification count
|
||||||
|
@@ -47,6 +47,8 @@ export interface MSC3575Filter {
|
|||||||
room_types?: string[];
|
room_types?: string[];
|
||||||
not_room_types?: string[];
|
not_room_types?: string[];
|
||||||
spaces?: string[];
|
spaces?: string[];
|
||||||
|
tags?: string[];
|
||||||
|
not_tags?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,6 +84,8 @@ export interface MSC3575RoomData {
|
|||||||
timeline: (IRoomEvent | IStateEvent)[];
|
timeline: (IRoomEvent | IStateEvent)[];
|
||||||
notification_count?: number;
|
notification_count?: number;
|
||||||
highlight_count?: number;
|
highlight_count?: number;
|
||||||
|
joined_count?: number;
|
||||||
|
invited_count?: number;
|
||||||
invite_state?: IStateEvent[];
|
invite_state?: IStateEvent[];
|
||||||
initial?: boolean;
|
initial?: boolean;
|
||||||
limited?: boolean;
|
limited?: boolean;
|
||||||
@@ -318,7 +322,9 @@ export enum SlidingSyncEvent {
|
|||||||
|
|
||||||
export type SlidingSyncEventHandlerMap = {
|
export type SlidingSyncEventHandlerMap = {
|
||||||
[SlidingSyncEvent.RoomData]: (roomId: string, roomData: MSC3575RoomData) => void;
|
[SlidingSyncEvent.RoomData]: (roomId: string, roomData: MSC3575RoomData) => void;
|
||||||
[SlidingSyncEvent.Lifecycle]: (state: SlidingSyncState, resp: MSC3575SlidingSyncResponse, err: Error) => void;
|
[SlidingSyncEvent.Lifecycle]: (
|
||||||
|
state: SlidingSyncState, resp: MSC3575SlidingSyncResponse | null, err: Error | null,
|
||||||
|
) => void;
|
||||||
[SlidingSyncEvent.List]: (
|
[SlidingSyncEvent.List]: (
|
||||||
listIndex: number, joinedCount: number, roomIndexToRoomId: Record<number, string>,
|
listIndex: number, joinedCount: number, roomIndexToRoomId: Record<number, string>,
|
||||||
) => void;
|
) => void;
|
||||||
@@ -530,6 +536,65 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
|||||||
this.emit(SlidingSyncEvent.Lifecycle, state, resp, err);
|
this.emit(SlidingSyncEvent.Lifecycle, state, resp, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private shiftRight(listIndex: number, hi: number, low: number) {
|
||||||
|
// l h
|
||||||
|
// 0,1,2,3,4 <- before
|
||||||
|
// 0,1,2,2,3 <- after, hi is deleted and low is duplicated
|
||||||
|
for (let i = hi; i > low; i--) {
|
||||||
|
if (this.lists[listIndex].isIndexInRange(i)) {
|
||||||
|
this.lists[listIndex].roomIndexToRoomId[i] =
|
||||||
|
this.lists[listIndex].roomIndexToRoomId[
|
||||||
|
i - 1
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private shiftLeft(listIndex: number, hi: number, low: number) {
|
||||||
|
// l h
|
||||||
|
// 0,1,2,3,4 <- before
|
||||||
|
// 0,1,3,4,4 <- after, low is deleted and hi is duplicated
|
||||||
|
for (let i = low; i < hi; i++) {
|
||||||
|
if (this.lists[listIndex].isIndexInRange(i)) {
|
||||||
|
this.lists[listIndex].roomIndexToRoomId[i] =
|
||||||
|
this.lists[listIndex].roomIndexToRoomId[
|
||||||
|
i + 1
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeEntry(listIndex: number, index: number) {
|
||||||
|
// work out the max index
|
||||||
|
let max = -1;
|
||||||
|
for (const n in this.lists[listIndex].roomIndexToRoomId) {
|
||||||
|
if (Number(n) > max) {
|
||||||
|
max = Number(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (max < 0 || index > max) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Everything higher than the gap needs to be shifted left.
|
||||||
|
this.shiftLeft(listIndex, max, index);
|
||||||
|
delete this.lists[listIndex].roomIndexToRoomId[max];
|
||||||
|
}
|
||||||
|
|
||||||
|
private addEntry(listIndex: number, index: number) {
|
||||||
|
// work out the max index
|
||||||
|
let max = -1;
|
||||||
|
for (const n in this.lists[listIndex].roomIndexToRoomId) {
|
||||||
|
if (Number(n) > max) {
|
||||||
|
max = Number(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (max < 0 || index > max) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Everything higher than the gap needs to be shifted right, +1 so we don't delete the highest element
|
||||||
|
this.shiftRight(listIndex, max+1, index);
|
||||||
|
}
|
||||||
|
|
||||||
private processListOps(list: ListResponse, listIndex: number): void {
|
private processListOps(list: ListResponse, listIndex: number): void {
|
||||||
let gapIndex = -1;
|
let gapIndex = -1;
|
||||||
list.ops.forEach((op: Operation) => {
|
list.ops.forEach((op: Operation) => {
|
||||||
@@ -537,6 +602,10 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
|||||||
case "DELETE": {
|
case "DELETE": {
|
||||||
logger.debug("DELETE", listIndex, op.index, ";");
|
logger.debug("DELETE", listIndex, op.index, ";");
|
||||||
delete this.lists[listIndex].roomIndexToRoomId[op.index];
|
delete this.lists[listIndex].roomIndexToRoomId[op.index];
|
||||||
|
if (gapIndex !== -1) {
|
||||||
|
// we already have a DELETE operation to process, so process it.
|
||||||
|
this.removeEntry(listIndex, gapIndex);
|
||||||
|
}
|
||||||
gapIndex = op.index;
|
gapIndex = op.index;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -551,20 +620,9 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
|||||||
if (this.lists[listIndex].roomIndexToRoomId[op.index]) {
|
if (this.lists[listIndex].roomIndexToRoomId[op.index]) {
|
||||||
// something is in this space, shift items out of the way
|
// something is in this space, shift items out of the way
|
||||||
if (gapIndex < 0) {
|
if (gapIndex < 0) {
|
||||||
logger.debug(
|
// we haven't been told where to shift from, so make way for a new room entry.
|
||||||
"cannot work out where gap is, INSERT without previous DELETE! List: ",
|
this.addEntry(listIndex, op.index);
|
||||||
listIndex,
|
} else if (gapIndex > op.index) {
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 0,1,2,3 index
|
|
||||||
// [A,B,C,D]
|
|
||||||
// DEL 3
|
|
||||||
// [A,B,C,_]
|
|
||||||
// INSERT E 0
|
|
||||||
// [E,A,B,C]
|
|
||||||
// gapIndex=3, op.index=0
|
|
||||||
if (gapIndex > op.index) {
|
|
||||||
// the gap is further down the list, shift every element to the right
|
// the gap is further down the list, shift every element to the right
|
||||||
// starting at the gap so we can just shift each element in turn:
|
// starting at the gap so we can just shift each element in turn:
|
||||||
// [A,B,C,_] gapIndex=3, op.index=0
|
// [A,B,C,_] gapIndex=3, op.index=0
|
||||||
@@ -572,26 +630,13 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
|||||||
// [A,B,B,C] i=2
|
// [A,B,B,C] i=2
|
||||||
// [A,A,B,C] i=1
|
// [A,A,B,C] i=1
|
||||||
// Terminate. We'll assign into op.index next.
|
// Terminate. We'll assign into op.index next.
|
||||||
for (let i = gapIndex; i > op.index; i--) {
|
this.shiftRight(listIndex, gapIndex, op.index);
|
||||||
if (this.lists[listIndex].isIndexInRange(i)) {
|
|
||||||
this.lists[listIndex].roomIndexToRoomId[i] =
|
|
||||||
this.lists[listIndex].roomIndexToRoomId[
|
|
||||||
i - 1
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (gapIndex < op.index) {
|
} else if (gapIndex < op.index) {
|
||||||
// the gap is further up the list, shift every element to the left
|
// the gap is further up the list, shift every element to the left
|
||||||
// starting at the gap so we can just shift each element in turn
|
// starting at the gap so we can just shift each element in turn
|
||||||
for (let i = gapIndex; i < op.index; i++) {
|
this.shiftLeft(listIndex, op.index, gapIndex);
|
||||||
if (this.lists[listIndex].isIndexInRange(i)) {
|
|
||||||
this.lists[listIndex].roomIndexToRoomId[i] =
|
|
||||||
this.lists[listIndex].roomIndexToRoomId[
|
|
||||||
i + 1
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
gapIndex = -1; // forget the gap, we don't need it anymore.
|
||||||
}
|
}
|
||||||
this.lists[listIndex].roomIndexToRoomId[op.index] = op.room_id;
|
this.lists[listIndex].roomIndexToRoomId[op.index] = op.room_id;
|
||||||
break;
|
break;
|
||||||
@@ -631,6 +676,11 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (gapIndex !== -1) {
|
||||||
|
// we already have a DELETE operation to process, so process it
|
||||||
|
// Everything higher than the gap needs to be shifted left.
|
||||||
|
this.removeEntry(listIndex, gapIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
560
src/sync.ts
560
src/sync.ts
@@ -23,6 +23,8 @@ limitations under the License.
|
|||||||
* for HTTP and WS at some point.
|
* for HTTP and WS at some point.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Optional } from "matrix-events-sdk";
|
||||||
|
|
||||||
import { User, UserEvent } from "./models/user";
|
import { User, UserEvent } from "./models/user";
|
||||||
import { NotificationCountType, Room, RoomEvent } from "./models/room";
|
import { NotificationCountType, Room, RoomEvent } from "./models/room";
|
||||||
import * as utils from "./utils";
|
import * as utils from "./utils";
|
||||||
@@ -100,18 +102,16 @@ const MSC2716_ROOM_VERSIONS = [
|
|||||||
function getFilterName(userId: string, suffix?: string): string {
|
function getFilterName(userId: string, suffix?: string): string {
|
||||||
// scope this on the user ID because people may login on many accounts
|
// scope this on the user ID because people may login on many accounts
|
||||||
// and they all need to be stored!
|
// and they all need to be stored!
|
||||||
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
|
return `FILTER_SYNC_${userId}` + (suffix ? "_" + suffix : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function debuglog(...params) {
|
function debuglog(...params) {
|
||||||
if (!DEBUG) {
|
if (!DEBUG) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.log(...params);
|
logger.log(...params);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISyncOptions {
|
interface ISyncOptions {
|
||||||
filterId?: string;
|
filter?: string;
|
||||||
hasSyncedBefore?: boolean;
|
hasSyncedBefore?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,14 +161,14 @@ type WrappedRoom<T> = T & {
|
|||||||
* updating presence.
|
* updating presence.
|
||||||
*/
|
*/
|
||||||
export class SyncApi {
|
export class SyncApi {
|
||||||
private _peekRoom: Room = null;
|
private _peekRoom: Optional<Room> = null;
|
||||||
private currentSyncRequest: IAbortablePromise<ISyncResponse> = null;
|
private currentSyncRequest: Optional<IAbortablePromise<ISyncResponse>> = null;
|
||||||
private syncState: SyncState = null;
|
private syncState: Optional<SyncState> = null;
|
||||||
private syncStateData: ISyncStateData = null; // additional data (eg. error object for failed sync)
|
private syncStateData: Optional<ISyncStateData> = null; // additional data (eg. error object for failed sync)
|
||||||
private catchingUp = false;
|
private catchingUp = false;
|
||||||
private running = false;
|
private running = false;
|
||||||
private keepAliveTimer: ReturnType<typeof setTimeout> = null;
|
private keepAliveTimer: Optional<ReturnType<typeof setTimeout>> = null;
|
||||||
private connectionReturnedDefer: IDeferred<boolean> = null;
|
private connectionReturnedDefer: Optional<IDeferred<boolean>> = null;
|
||||||
private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response
|
private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response
|
||||||
private failedSyncCount = 0; // Number of consecutive failed /sync requests
|
private failedSyncCount = 0; // Number of consecutive failed /sync requests
|
||||||
private storeIsInvalid = false; // flag set if the store needs to be cleared before we can start
|
private storeIsInvalid = false; // flag set if the store needs to be cleared before we can start
|
||||||
@@ -214,7 +214,7 @@ export class SyncApi {
|
|||||||
* historical messages are shown when we paginate `/messages` again.
|
* historical messages are shown when we paginate `/messages` again.
|
||||||
* @param {Room} room The room where the marker event was sent
|
* @param {Room} room The room where the marker event was sent
|
||||||
* @param {MatrixEvent} markerEvent The new marker event
|
* @param {MatrixEvent} markerEvent The new marker event
|
||||||
* @param {ISetStateOptions} setStateOptions When `timelineWasEmpty` is set
|
* @param {IMarkerFoundOptions} setStateOptions When `timelineWasEmpty` is set
|
||||||
* as `true`, the given marker event will be ignored
|
* as `true`, the given marker event will be ignored
|
||||||
*/
|
*/
|
||||||
private onMarkerStateEvent(
|
private onMarkerStateEvent(
|
||||||
@@ -367,7 +367,7 @@ export class SyncApi {
|
|||||||
|
|
||||||
// XXX: copypasted from /sync until we kill off this minging v1 API stuff)
|
// XXX: copypasted from /sync until we kill off this minging v1 API stuff)
|
||||||
// handle presence events (User objects)
|
// handle presence events (User objects)
|
||||||
if (response.presence && Array.isArray(response.presence)) {
|
if (Array.isArray(response.presence)) {
|
||||||
response.presence.map(client.getEventMapper()).forEach(
|
response.presence.map(client.getEventMapper()).forEach(
|
||||||
function(presenceEvent) {
|
function(presenceEvent) {
|
||||||
let user = client.store.getUser(presenceEvent.getContent().user_id);
|
let user = client.store.getUser(presenceEvent.getContent().user_id);
|
||||||
@@ -542,20 +542,135 @@ export class SyncApi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getPushRules = async () => {
|
||||||
|
try {
|
||||||
|
debuglog("Getting push rules...");
|
||||||
|
const result = await this.client.getPushRules();
|
||||||
|
debuglog("Got push rules");
|
||||||
|
|
||||||
|
this.client.pushRules = result;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Getting push rules failed", err);
|
||||||
|
if (this.shouldAbortSync(err)) return;
|
||||||
|
// wait for saved sync to complete before doing anything else,
|
||||||
|
// otherwise the sync state will end up being incorrect
|
||||||
|
debuglog("Waiting for saved sync before retrying push rules...");
|
||||||
|
await this.recoverFromSyncStartupError(this.savedSyncPromise, err);
|
||||||
|
return this.getPushRules(); // try again
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private buildDefaultFilter = () => {
|
||||||
|
return new Filter(this.client.credentials.userId);
|
||||||
|
};
|
||||||
|
|
||||||
|
private checkLazyLoadStatus = async () => {
|
||||||
|
debuglog("Checking lazy load status...");
|
||||||
|
if (this.opts.lazyLoadMembers && this.client.isGuest()) {
|
||||||
|
this.opts.lazyLoadMembers = false;
|
||||||
|
}
|
||||||
|
if (this.opts.lazyLoadMembers) {
|
||||||
|
debuglog("Checking server lazy load support...");
|
||||||
|
const supported = await this.client.doesServerSupportLazyLoading();
|
||||||
|
if (supported) {
|
||||||
|
debuglog("Enabling lazy load on sync filter...");
|
||||||
|
if (!this.opts.filter) {
|
||||||
|
this.opts.filter = this.buildDefaultFilter();
|
||||||
|
}
|
||||||
|
this.opts.filter.setLazyLoadMembers(true);
|
||||||
|
} else {
|
||||||
|
debuglog("LL: lazy loading requested but not supported " +
|
||||||
|
"by server, so disabling");
|
||||||
|
this.opts.lazyLoadMembers = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// need to vape the store when enabling LL and wasn't enabled before
|
||||||
|
debuglog("Checking whether lazy loading has changed in store...");
|
||||||
|
const shouldClear = await this.wasLazyLoadingToggled(this.opts.lazyLoadMembers);
|
||||||
|
if (shouldClear) {
|
||||||
|
this.storeIsInvalid = true;
|
||||||
|
const reason = InvalidStoreError.TOGGLED_LAZY_LOADING;
|
||||||
|
const error = new InvalidStoreError(reason, !!this.opts.lazyLoadMembers);
|
||||||
|
this.updateSyncState(SyncState.Error, { error });
|
||||||
|
// bail out of the sync loop now: the app needs to respond to this error.
|
||||||
|
// we leave the state as 'ERROR' which isn't great since this normally means
|
||||||
|
// we're retrying. The client must be stopped before clearing the stores anyway
|
||||||
|
// so the app should stop the client, clear the store and start it again.
|
||||||
|
logger.warn("InvalidStoreError: store is not usable: stopping sync.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.opts.lazyLoadMembers) {
|
||||||
|
this.opts.crypto?.enableLazyLoading();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
debuglog("Storing client options...");
|
||||||
|
await this.client.storeClientOptions();
|
||||||
|
debuglog("Stored client options");
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Storing client options failed", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private getFilter = async (): Promise<{
|
||||||
|
filterId?: string;
|
||||||
|
filter?: Filter;
|
||||||
|
}> => {
|
||||||
|
debuglog("Getting filter...");
|
||||||
|
let filter: Filter;
|
||||||
|
if (this.opts.filter) {
|
||||||
|
filter = this.opts.filter;
|
||||||
|
} else {
|
||||||
|
filter = this.buildDefaultFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
let filterId: string;
|
||||||
|
try {
|
||||||
|
filterId = await this.client.getOrCreateFilter(getFilterName(this.client.credentials.userId), filter);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Getting filter failed", err);
|
||||||
|
if (this.shouldAbortSync(err)) return {};
|
||||||
|
// wait for saved sync to complete before doing anything else,
|
||||||
|
// otherwise the sync state will end up being incorrect
|
||||||
|
debuglog("Waiting for saved sync before retrying filter...");
|
||||||
|
await this.recoverFromSyncStartupError(this.savedSyncPromise, err);
|
||||||
|
return this.getFilter(); // try again
|
||||||
|
}
|
||||||
|
return { filter, filterId };
|
||||||
|
};
|
||||||
|
|
||||||
|
private savedSyncPromise: Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main entry point
|
* Main entry point
|
||||||
*/
|
*/
|
||||||
public sync(): void {
|
public async sync(): Promise<void> {
|
||||||
const client = this.client;
|
|
||||||
|
|
||||||
this.running = true;
|
this.running = true;
|
||||||
|
|
||||||
if (global.window && global.window.addEventListener) {
|
global.window?.addEventListener?.("online", this.onOnline, false);
|
||||||
global.window.addEventListener("online", this.onOnline, false);
|
|
||||||
|
if (this.client.isGuest()) {
|
||||||
|
// no push rules for guests, no access to POST filter for guests.
|
||||||
|
return this.doSync({});
|
||||||
}
|
}
|
||||||
|
|
||||||
let savedSyncPromise = Promise.resolve();
|
// Pull the saved sync token out first, before the worker starts sending
|
||||||
let savedSyncToken = null;
|
// all the sync data which could take a while. This will let us send our
|
||||||
|
// first incremental sync request before we've processed our saved data.
|
||||||
|
debuglog("Getting saved sync token...");
|
||||||
|
const savedSyncTokenPromise = this.client.store.getSavedSyncToken().then(tok => {
|
||||||
|
debuglog("Got saved sync token");
|
||||||
|
return tok;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.savedSyncPromise = this.client.store.getSavedSync().then((savedSync) => {
|
||||||
|
debuglog(`Got reply from saved sync, exists? ${!!savedSync}`);
|
||||||
|
if (savedSync) {
|
||||||
|
return this.syncFromCache(savedSync);
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
logger.error("Getting saved sync failed", err);
|
||||||
|
});
|
||||||
|
|
||||||
// We need to do one-off checks before we can begin the /sync loop.
|
// We need to do one-off checks before we can begin the /sync loop.
|
||||||
// These are:
|
// These are:
|
||||||
@@ -565,149 +680,45 @@ export class SyncApi {
|
|||||||
// 3) We need to check the lazy loading option matches what was used in the
|
// 3) We need to check the lazy loading option matches what was used in the
|
||||||
// stored sync. If it doesn't, we can't use the stored sync.
|
// stored sync. If it doesn't, we can't use the stored sync.
|
||||||
|
|
||||||
const getPushRules = async () => {
|
// Now start the first incremental sync request: this can also
|
||||||
try {
|
// take a while so if we set it going now, we can wait for it
|
||||||
debuglog("Getting push rules...");
|
// to finish while we process our saved sync data.
|
||||||
const result = await client.getPushRules();
|
await this.getPushRules();
|
||||||
debuglog("Got push rules");
|
await this.checkLazyLoadStatus();
|
||||||
|
const { filterId, filter } = await this.getFilter();
|
||||||
|
if (!filter) return; // bail, getFilter failed
|
||||||
|
|
||||||
client.pushRules = result;
|
// reset the notifications timeline to prepare it to paginate from
|
||||||
} catch (err) {
|
// the current point in time.
|
||||||
logger.error("Getting push rules failed", err);
|
// The right solution would be to tie /sync pagination tokens into
|
||||||
if (this.shouldAbortSync(err)) return;
|
// /notifications API somehow.
|
||||||
// wait for saved sync to complete before doing anything else,
|
this.client.resetNotifTimelineSet();
|
||||||
// otherwise the sync state will end up being incorrect
|
|
||||||
debuglog("Waiting for saved sync before retrying push rules...");
|
|
||||||
await this.recoverFromSyncStartupError(savedSyncPromise, err);
|
|
||||||
getPushRules();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
checkLazyLoadStatus(); // advance to the next stage
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildDefaultFilter = () => {
|
if (this.currentSyncRequest === null) {
|
||||||
const filter = new Filter(client.credentials.userId);
|
let firstSyncFilter = filterId;
|
||||||
filter.setTimelineLimit(this.opts.initialSyncLimit);
|
const savedSyncToken = await savedSyncTokenPromise;
|
||||||
return filter;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkLazyLoadStatus = async () => {
|
if (savedSyncToken) {
|
||||||
debuglog("Checking lazy load status...");
|
|
||||||
if (this.opts.lazyLoadMembers && client.isGuest()) {
|
|
||||||
this.opts.lazyLoadMembers = false;
|
|
||||||
}
|
|
||||||
if (this.opts.lazyLoadMembers) {
|
|
||||||
debuglog("Checking server lazy load support...");
|
|
||||||
const supported = await client.doesServerSupportLazyLoading();
|
|
||||||
if (supported) {
|
|
||||||
debuglog("Enabling lazy load on sync filter...");
|
|
||||||
if (!this.opts.filter) {
|
|
||||||
this.opts.filter = buildDefaultFilter();
|
|
||||||
}
|
|
||||||
this.opts.filter.setLazyLoadMembers(true);
|
|
||||||
} else {
|
|
||||||
debuglog("LL: lazy loading requested but not supported " +
|
|
||||||
"by server, so disabling");
|
|
||||||
this.opts.lazyLoadMembers = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// need to vape the store when enabling LL and wasn't enabled before
|
|
||||||
debuglog("Checking whether lazy loading has changed in store...");
|
|
||||||
const shouldClear = await this.wasLazyLoadingToggled(this.opts.lazyLoadMembers);
|
|
||||||
if (shouldClear) {
|
|
||||||
this.storeIsInvalid = true;
|
|
||||||
const reason = InvalidStoreError.TOGGLED_LAZY_LOADING;
|
|
||||||
const error = new InvalidStoreError(reason, !!this.opts.lazyLoadMembers);
|
|
||||||
this.updateSyncState(SyncState.Error, { error });
|
|
||||||
// bail out of the sync loop now: the app needs to respond to this error.
|
|
||||||
// we leave the state as 'ERROR' which isn't great since this normally means
|
|
||||||
// we're retrying. The client must be stopped before clearing the stores anyway
|
|
||||||
// so the app should stop the client, clear the store and start it again.
|
|
||||||
logger.warn("InvalidStoreError: store is not usable: stopping sync.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.opts.lazyLoadMembers && this.opts.crypto) {
|
|
||||||
this.opts.crypto.enableLazyLoading();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
debuglog("Storing client options...");
|
|
||||||
await this.client.storeClientOptions();
|
|
||||||
debuglog("Stored client options");
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("Storing client options failed", err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFilter(); // Now get the filter and start syncing
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFilter = async () => {
|
|
||||||
debuglog("Getting filter...");
|
|
||||||
let filter;
|
|
||||||
if (this.opts.filter) {
|
|
||||||
filter = this.opts.filter;
|
|
||||||
} else {
|
|
||||||
filter = buildDefaultFilter();
|
|
||||||
}
|
|
||||||
|
|
||||||
let filterId;
|
|
||||||
try {
|
|
||||||
filterId = await client.getOrCreateFilter(getFilterName(client.credentials.userId), filter);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("Getting filter failed", err);
|
|
||||||
if (this.shouldAbortSync(err)) return;
|
|
||||||
// wait for saved sync to complete before doing anything else,
|
|
||||||
// otherwise the sync state will end up being incorrect
|
|
||||||
debuglog("Waiting for saved sync before retrying filter...");
|
|
||||||
await this.recoverFromSyncStartupError(savedSyncPromise, err);
|
|
||||||
getFilter();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// reset the notifications timeline to prepare it to paginate from
|
|
||||||
// the current point in time.
|
|
||||||
// The right solution would be to tie /sync pagination tokens into
|
|
||||||
// /notifications API somehow.
|
|
||||||
client.resetNotifTimelineSet();
|
|
||||||
|
|
||||||
if (this.currentSyncRequest === null) {
|
|
||||||
// Send this first sync request here so we can then wait for the saved
|
|
||||||
// sync data to finish processing before we process the results of this one.
|
|
||||||
debuglog("Sending first sync request...");
|
debuglog("Sending first sync request...");
|
||||||
this.currentSyncRequest = this.doSyncRequest({ filterId }, savedSyncToken);
|
} else {
|
||||||
|
debuglog("Sending initial sync request...");
|
||||||
|
const initialFilter = this.buildDefaultFilter();
|
||||||
|
initialFilter.setDefinition(filter.getDefinition());
|
||||||
|
initialFilter.setTimelineLimit(this.opts.initialSyncLimit);
|
||||||
|
// Use an inline filter, no point uploading it for a single usage
|
||||||
|
firstSyncFilter = JSON.stringify(initialFilter.getDefinition());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now wait for the saved sync to finish...
|
// Send this first sync request here so we can then wait for the saved
|
||||||
debuglog("Waiting for saved sync before starting sync processing...");
|
// sync data to finish processing before we process the results of this one.
|
||||||
await savedSyncPromise;
|
this.currentSyncRequest = this.doSyncRequest({ filter: firstSyncFilter }, savedSyncToken);
|
||||||
this.doSync({ filterId });
|
|
||||||
};
|
|
||||||
|
|
||||||
if (client.isGuest()) {
|
|
||||||
// no push rules for guests, no access to POST filter for guests.
|
|
||||||
this.doSync({});
|
|
||||||
} else {
|
|
||||||
// Pull the saved sync token out first, before the worker starts sending
|
|
||||||
// all the sync data which could take a while. This will let us send our
|
|
||||||
// first incremental sync request before we've processed our saved data.
|
|
||||||
debuglog("Getting saved sync token...");
|
|
||||||
savedSyncPromise = client.store.getSavedSyncToken().then((tok) => {
|
|
||||||
debuglog("Got saved sync token");
|
|
||||||
savedSyncToken = tok;
|
|
||||||
debuglog("Getting saved sync...");
|
|
||||||
return client.store.getSavedSync();
|
|
||||||
}).then((savedSync) => {
|
|
||||||
debuglog(`Got reply from saved sync, exists? ${!!savedSync}`);
|
|
||||||
if (savedSync) {
|
|
||||||
return this.syncFromCache(savedSync);
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
logger.error("Getting saved sync failed", err);
|
|
||||||
});
|
|
||||||
// Now start the first incremental sync request: this can also
|
|
||||||
// take a while so if we set it going now, we can wait for it
|
|
||||||
// to finish while we process our saved sync data.
|
|
||||||
getPushRules();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now wait for the saved sync to finish...
|
||||||
|
debuglog("Waiting for saved sync before starting sync processing...");
|
||||||
|
await this.savedSyncPromise;
|
||||||
|
// process the first sync request and continue syncing with the normal filterId
|
||||||
|
return this.doSync({ filter: filterId });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -719,9 +730,7 @@ export class SyncApi {
|
|||||||
// global.window AND global.window.removeEventListener.
|
// global.window AND global.window.removeEventListener.
|
||||||
// Some platforms (e.g. React Native) register global.window,
|
// Some platforms (e.g. React Native) register global.window,
|
||||||
// but do not have global.window.removeEventListener.
|
// but do not have global.window.removeEventListener.
|
||||||
if (global.window && global.window.removeEventListener) {
|
global.window?.removeEventListener?.("online", this.onOnline, false);
|
||||||
global.window.removeEventListener("online", this.onOnline, false);
|
|
||||||
}
|
|
||||||
this.running = false;
|
this.running = false;
|
||||||
this.currentSyncRequest?.abort();
|
this.currentSyncRequest?.abort();
|
||||||
if (this.keepAliveTimer) {
|
if (this.keepAliveTimer) {
|
||||||
@@ -756,8 +765,7 @@ export class SyncApi {
|
|||||||
this.client.store.setSyncToken(nextSyncToken);
|
this.client.store.setSyncToken(nextSyncToken);
|
||||||
|
|
||||||
// No previous sync, set old token to null
|
// No previous sync, set old token to null
|
||||||
const syncEventData = {
|
const syncEventData: ISyncStateData = {
|
||||||
oldSyncToken: null,
|
|
||||||
nextSyncToken,
|
nextSyncToken,
|
||||||
catchingUp: false,
|
catchingUp: false,
|
||||||
fromCache: true,
|
fromCache: true,
|
||||||
@@ -792,7 +800,91 @@ export class SyncApi {
|
|||||||
* @param {boolean} syncOptions.hasSyncedBefore
|
* @param {boolean} syncOptions.hasSyncedBefore
|
||||||
*/
|
*/
|
||||||
private async doSync(syncOptions: ISyncOptions): Promise<void> {
|
private async doSync(syncOptions: ISyncOptions): Promise<void> {
|
||||||
const client = this.client;
|
while (this.running) {
|
||||||
|
const syncToken = this.client.store.getSyncToken();
|
||||||
|
|
||||||
|
let data: ISyncResponse;
|
||||||
|
try {
|
||||||
|
//debuglog('Starting sync since=' + syncToken);
|
||||||
|
if (this.currentSyncRequest === null) {
|
||||||
|
this.currentSyncRequest = this.doSyncRequest(syncOptions, syncToken);
|
||||||
|
}
|
||||||
|
data = await this.currentSyncRequest;
|
||||||
|
} catch (e) {
|
||||||
|
const abort = await this.onSyncError(e);
|
||||||
|
if (abort) return;
|
||||||
|
continue;
|
||||||
|
} finally {
|
||||||
|
this.currentSyncRequest = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//debuglog('Completed sync, next_batch=' + data.next_batch);
|
||||||
|
|
||||||
|
// set the sync token NOW *before* processing the events. We do this so
|
||||||
|
// if something barfs on an event we can skip it rather than constantly
|
||||||
|
// polling with the same token.
|
||||||
|
this.client.store.setSyncToken(data.next_batch);
|
||||||
|
|
||||||
|
// Reset after a successful sync
|
||||||
|
this.failedSyncCount = 0;
|
||||||
|
|
||||||
|
await this.client.store.setSyncData(data);
|
||||||
|
|
||||||
|
const syncEventData = {
|
||||||
|
oldSyncToken: syncToken,
|
||||||
|
nextSyncToken: data.next_batch,
|
||||||
|
catchingUp: this.catchingUp,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.opts.crypto) {
|
||||||
|
// tell the crypto module we're about to process a sync
|
||||||
|
// response
|
||||||
|
await this.opts.crypto.onSyncWillProcess(syncEventData);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.processSyncResponse(syncEventData, data);
|
||||||
|
} catch (e) {
|
||||||
|
// log the exception with stack if we have it, else fall back
|
||||||
|
// to the plain description
|
||||||
|
logger.error("Caught /sync error", e);
|
||||||
|
|
||||||
|
// Emit the exception for client handling
|
||||||
|
this.client.emit(ClientEvent.SyncUnexpectedError, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update this as it may have changed
|
||||||
|
syncEventData.catchingUp = this.catchingUp;
|
||||||
|
|
||||||
|
// emit synced events
|
||||||
|
if (!syncOptions.hasSyncedBefore) {
|
||||||
|
this.updateSyncState(SyncState.Prepared, syncEventData);
|
||||||
|
syncOptions.hasSyncedBefore = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// tell the crypto module to do its processing. It may block (to do a
|
||||||
|
// /keys/changes request).
|
||||||
|
if (this.opts.crypto) {
|
||||||
|
await this.opts.crypto.onSyncCompleted(syncEventData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep emitting SYNCING -> SYNCING for clients who want to do bulk updates
|
||||||
|
this.updateSyncState(SyncState.Syncing, syncEventData);
|
||||||
|
|
||||||
|
if (this.client.store.wantsSave()) {
|
||||||
|
// We always save the device list (if it's dirty) before saving the sync data:
|
||||||
|
// this means we know the saved device list data is at least as fresh as the
|
||||||
|
// stored sync data which means we don't have to worry that we may have missed
|
||||||
|
// device changes. We can also skip the delay since we're not calling this very
|
||||||
|
// frequently (and we don't really want to delay the sync for it).
|
||||||
|
if (this.opts.crypto) {
|
||||||
|
await this.opts.crypto.saveDeviceList(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tell databases that everything is now in a consistent state and can be saved.
|
||||||
|
this.client.store.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.running) {
|
if (!this.running) {
|
||||||
debuglog("Sync no longer running: exiting.");
|
debuglog("Sync no longer running: exiting.");
|
||||||
@@ -801,94 +893,7 @@ export class SyncApi {
|
|||||||
this.connectionReturnedDefer = null;
|
this.connectionReturnedDefer = null;
|
||||||
}
|
}
|
||||||
this.updateSyncState(SyncState.Stopped);
|
this.updateSyncState(SyncState.Stopped);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncToken = client.store.getSyncToken();
|
|
||||||
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
//debuglog('Starting sync since=' + syncToken);
|
|
||||||
if (this.currentSyncRequest === null) {
|
|
||||||
this.currentSyncRequest = this.doSyncRequest(syncOptions, syncToken);
|
|
||||||
}
|
|
||||||
data = await this.currentSyncRequest;
|
|
||||||
} catch (e) {
|
|
||||||
this.onSyncError(e, syncOptions);
|
|
||||||
return;
|
|
||||||
} finally {
|
|
||||||
this.currentSyncRequest = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
//debuglog('Completed sync, next_batch=' + data.next_batch);
|
|
||||||
|
|
||||||
// set the sync token NOW *before* processing the events. We do this so
|
|
||||||
// if something barfs on an event we can skip it rather than constantly
|
|
||||||
// polling with the same token.
|
|
||||||
client.store.setSyncToken(data.next_batch);
|
|
||||||
|
|
||||||
// Reset after a successful sync
|
|
||||||
this.failedSyncCount = 0;
|
|
||||||
|
|
||||||
await client.store.setSyncData(data);
|
|
||||||
|
|
||||||
const syncEventData = {
|
|
||||||
oldSyncToken: syncToken,
|
|
||||||
nextSyncToken: data.next_batch,
|
|
||||||
catchingUp: this.catchingUp,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.opts.crypto) {
|
|
||||||
// tell the crypto module we're about to process a sync
|
|
||||||
// response
|
|
||||||
await this.opts.crypto.onSyncWillProcess(syncEventData);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.processSyncResponse(syncEventData, data);
|
|
||||||
} catch (e) {
|
|
||||||
// log the exception with stack if we have it, else fall back
|
|
||||||
// to the plain description
|
|
||||||
logger.error("Caught /sync error", e);
|
|
||||||
|
|
||||||
// Emit the exception for client handling
|
|
||||||
this.client.emit(ClientEvent.SyncUnexpectedError, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update this as it may have changed
|
|
||||||
syncEventData.catchingUp = this.catchingUp;
|
|
||||||
|
|
||||||
// emit synced events
|
|
||||||
if (!syncOptions.hasSyncedBefore) {
|
|
||||||
this.updateSyncState(SyncState.Prepared, syncEventData);
|
|
||||||
syncOptions.hasSyncedBefore = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// tell the crypto module to do its processing. It may block (to do a
|
|
||||||
// /keys/changes request).
|
|
||||||
if (this.opts.crypto) {
|
|
||||||
await this.opts.crypto.onSyncCompleted(syncEventData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// keep emitting SYNCING -> SYNCING for clients who want to do bulk updates
|
|
||||||
this.updateSyncState(SyncState.Syncing, syncEventData);
|
|
||||||
|
|
||||||
if (client.store.wantsSave()) {
|
|
||||||
// We always save the device list (if it's dirty) before saving the sync data:
|
|
||||||
// this means we know the saved device list data is at least as fresh as the
|
|
||||||
// stored sync data which means we don't have to worry that we may have missed
|
|
||||||
// device changes. We can also skip the delay since we're not calling this very
|
|
||||||
// frequently (and we don't really want to delay the sync for it).
|
|
||||||
if (this.opts.crypto) {
|
|
||||||
await this.opts.crypto.saveDeviceList(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// tell databases that everything is now in a consistent state and can be saved.
|
|
||||||
client.store.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Begin next sync
|
|
||||||
this.doSync(syncOptions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IAbortablePromise<ISyncResponse> {
|
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IAbortablePromise<ISyncResponse> {
|
||||||
@@ -902,7 +907,7 @@ export class SyncApi {
|
|||||||
private getSyncParams(syncOptions: ISyncOptions, syncToken: string): ISyncParams {
|
private getSyncParams(syncOptions: ISyncOptions, syncToken: string): ISyncParams {
|
||||||
let pollTimeout = this.opts.pollTimeout;
|
let pollTimeout = this.opts.pollTimeout;
|
||||||
|
|
||||||
if (this.getSyncState() !== 'SYNCING' || this.catchingUp) {
|
if (this.getSyncState() !== SyncState.Syncing || this.catchingUp) {
|
||||||
// unless we are happily syncing already, we want the server to return
|
// unless we are happily syncing already, we want the server to return
|
||||||
// as quickly as possible, even if there are no events queued. This
|
// as quickly as possible, even if there are no events queued. This
|
||||||
// serves two purposes:
|
// serves two purposes:
|
||||||
@@ -918,13 +923,13 @@ export class SyncApi {
|
|||||||
pollTimeout = 0;
|
pollTimeout = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let filterId = syncOptions.filterId;
|
let filter = syncOptions.filter;
|
||||||
if (this.client.isGuest() && !filterId) {
|
if (this.client.isGuest() && !filter) {
|
||||||
filterId = this.getGuestFilter();
|
filter = this.getGuestFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
const qps: ISyncParams = {
|
const qps: ISyncParams = {
|
||||||
filter: filterId,
|
filter,
|
||||||
timeout: pollTimeout,
|
timeout: pollTimeout,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -941,7 +946,7 @@ export class SyncApi {
|
|||||||
qps._cacheBuster = Date.now();
|
qps._cacheBuster = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.getSyncState() == 'ERROR' || this.getSyncState() == 'RECONNECTING') {
|
if ([SyncState.Reconnecting, SyncState.Error].includes(this.getSyncState())) {
|
||||||
// we think the connection is dead. If it comes back up, we won't know
|
// we think the connection is dead. If it comes back up, we won't know
|
||||||
// about it till /sync returns. If the timeout= is high, this could
|
// about it till /sync returns. If the timeout= is high, this could
|
||||||
// be a long time. Set it to 0 when doing retries so we don't have to wait
|
// be a long time. Set it to 0 when doing retries so we don't have to wait
|
||||||
@@ -952,7 +957,7 @@ export class SyncApi {
|
|||||||
return qps;
|
return qps;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSyncError(err: MatrixError, syncOptions: ISyncOptions): void {
|
private async onSyncError(err: MatrixError): Promise<boolean> {
|
||||||
if (!this.running) {
|
if (!this.running) {
|
||||||
debuglog("Sync no longer running: exiting");
|
debuglog("Sync no longer running: exiting");
|
||||||
if (this.connectionReturnedDefer) {
|
if (this.connectionReturnedDefer) {
|
||||||
@@ -960,14 +965,13 @@ export class SyncApi {
|
|||||||
this.connectionReturnedDefer = null;
|
this.connectionReturnedDefer = null;
|
||||||
}
|
}
|
||||||
this.updateSyncState(SyncState.Stopped);
|
this.updateSyncState(SyncState.Stopped);
|
||||||
return;
|
return true; // abort
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error("/sync error %s", err);
|
logger.error("/sync error %s", err);
|
||||||
logger.error(err);
|
|
||||||
|
|
||||||
if (this.shouldAbortSync(err)) {
|
if (this.shouldAbortSync(err)) {
|
||||||
return;
|
return true; // abort
|
||||||
}
|
}
|
||||||
|
|
||||||
this.failedSyncCount++;
|
this.failedSyncCount++;
|
||||||
@@ -981,20 +985,7 @@ export class SyncApi {
|
|||||||
// erroneous. We set the state to 'reconnecting'
|
// erroneous. We set the state to 'reconnecting'
|
||||||
// instead, so that clients can observe this state
|
// instead, so that clients can observe this state
|
||||||
// if they wish.
|
// if they wish.
|
||||||
this.startKeepAlives().then((connDidFail) => {
|
const keepAlivePromise = this.startKeepAlives();
|
||||||
// Only emit CATCHUP if we detected a connectivity error: if we didn't,
|
|
||||||
// it's quite likely the sync will fail again for the same reason and we
|
|
||||||
// want to stay in ERROR rather than keep flip-flopping between ERROR
|
|
||||||
// and CATCHUP.
|
|
||||||
if (connDidFail && this.getSyncState() === SyncState.Error) {
|
|
||||||
this.updateSyncState(SyncState.Catchup, {
|
|
||||||
oldSyncToken: null,
|
|
||||||
nextSyncToken: null,
|
|
||||||
catchingUp: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.doSync(syncOptions);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.currentSyncRequest = null;
|
this.currentSyncRequest = null;
|
||||||
// Transition from RECONNECTING to ERROR after a given number of failed syncs
|
// Transition from RECONNECTING to ERROR after a given number of failed syncs
|
||||||
@@ -1003,6 +994,19 @@ export class SyncApi {
|
|||||||
SyncState.Error : SyncState.Reconnecting,
|
SyncState.Error : SyncState.Reconnecting,
|
||||||
{ error: err },
|
{ error: err },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const connDidFail = await keepAlivePromise;
|
||||||
|
|
||||||
|
// Only emit CATCHUP if we detected a connectivity error: if we didn't,
|
||||||
|
// it's quite likely the sync will fail again for the same reason and we
|
||||||
|
// want to stay in ERROR rather than keep flip-flopping between ERROR
|
||||||
|
// and CATCHUP.
|
||||||
|
if (connDidFail && this.getSyncState() === SyncState.Error) {
|
||||||
|
this.updateSyncState(SyncState.Catchup, {
|
||||||
|
catchingUp: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1061,7 +1065,7 @@ export class SyncApi {
|
|||||||
// - The isBrandNewRoom boilerplate is boilerplatey.
|
// - The isBrandNewRoom boilerplate is boilerplatey.
|
||||||
|
|
||||||
// handle presence events (User objects)
|
// handle presence events (User objects)
|
||||||
if (data.presence && Array.isArray(data.presence.events)) {
|
if (Array.isArray(data.presence?.events)) {
|
||||||
data.presence.events.map(client.getEventMapper()).forEach(
|
data.presence.events.map(client.getEventMapper()).forEach(
|
||||||
function(presenceEvent) {
|
function(presenceEvent) {
|
||||||
let user = client.store.getUser(presenceEvent.getSender());
|
let user = client.store.getUser(presenceEvent.getSender());
|
||||||
@@ -1077,7 +1081,7 @@ export class SyncApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handle non-room account_data
|
// handle non-room account_data
|
||||||
if (data.account_data && Array.isArray(data.account_data.events)) {
|
if (Array.isArray(data.account_data?.events)) {
|
||||||
const events = data.account_data.events.map(client.getEventMapper());
|
const events = data.account_data.events.map(client.getEventMapper());
|
||||||
const prevEventsMap = events.reduce((m, c) => {
|
const prevEventsMap = events.reduce((m, c) => {
|
||||||
m[c.getId()] = client.store.getAccountData(c.getType());
|
m[c.getId()] = client.store.getAccountData(c.getType());
|
||||||
@@ -1218,8 +1222,7 @@ export class SyncApi {
|
|||||||
// bother setting it here. We trust our calculations better than the
|
// bother setting it here. We trust our calculations better than the
|
||||||
// server's for this case, and therefore will assume that our non-zero
|
// server's for this case, and therefore will assume that our non-zero
|
||||||
// count is accurate.
|
// count is accurate.
|
||||||
if (!encrypted
|
if (!encrypted || room.getUnreadNotificationCount(NotificationCountType.Highlight) <= 0) {
|
||||||
|| (encrypted && room.getUnreadNotificationCount(NotificationCountType.Highlight) <= 0)) {
|
|
||||||
room.setUnreadNotificationCount(
|
room.setUnreadNotificationCount(
|
||||||
NotificationCountType.Highlight,
|
NotificationCountType.Highlight,
|
||||||
joinObj.unread_notifications.highlight_count,
|
joinObj.unread_notifications.highlight_count,
|
||||||
@@ -1232,8 +1235,7 @@ export class SyncApi {
|
|||||||
if (joinObj.isBrandNewRoom) {
|
if (joinObj.isBrandNewRoom) {
|
||||||
// set the back-pagination token. Do this *before* adding any
|
// set the back-pagination token. Do this *before* adding any
|
||||||
// events so that clients can start back-paginating.
|
// events so that clients can start back-paginating.
|
||||||
room.getLiveTimeline().setPaginationToken(
|
room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
|
||||||
joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
|
|
||||||
} else if (joinObj.timeline.limited) {
|
} else if (joinObj.timeline.limited) {
|
||||||
let limited = true;
|
let limited = true;
|
||||||
|
|
||||||
|
48
src/utils.ts
48
src/utils.ts
@@ -29,6 +29,30 @@ import { MatrixClient } from ".";
|
|||||||
import { M_TIMESTAMP } from "./@types/location";
|
import { M_TIMESTAMP } from "./@types/location";
|
||||||
import { ReceiptType } from "./@types/read_receipts";
|
import { ReceiptType } from "./@types/read_receipts";
|
||||||
|
|
||||||
|
const interns = new Map<string, string>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internalises a string, reusing a known pointer or storing the pointer
|
||||||
|
* if needed for future strings.
|
||||||
|
* @param str The string to internalise.
|
||||||
|
* @returns The internalised string.
|
||||||
|
*/
|
||||||
|
export function internaliseString(str: string): string {
|
||||||
|
// Unwrap strings before entering the map, if we somehow got a wrapped
|
||||||
|
// string as our input. This should only happen from tests.
|
||||||
|
if ((str as unknown) instanceof String) {
|
||||||
|
str = str.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the map to see if we can store the value
|
||||||
|
if (!interns.has(str)) {
|
||||||
|
interns.set(str, str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return any cached string reference
|
||||||
|
return interns.get(str);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode a dictionary of query parameters.
|
* Encode a dictionary of query parameters.
|
||||||
* Omits any undefined/null values.
|
* Omits any undefined/null values.
|
||||||
@@ -75,8 +99,7 @@ export function decodeParams(query: string): QueryDict {
|
|||||||
* variables with. E.g. { "$bar": "baz" }.
|
* variables with. E.g. { "$bar": "baz" }.
|
||||||
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||||
*/
|
*/
|
||||||
export function encodeUri(pathTemplate: string,
|
export function encodeUri(pathTemplate: string, variables: Record<string, string>): string {
|
||||||
variables: Record<string, string>): string {
|
|
||||||
for (const key in variables) {
|
for (const key in variables) {
|
||||||
if (!variables.hasOwnProperty(key)) {
|
if (!variables.hasOwnProperty(key)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -216,33 +239,24 @@ export function deepCompare(x: any, y: any): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// disable jshint "The body of a for in should be wrapped in an if
|
|
||||||
// statement"
|
|
||||||
/* jshint -W089 */
|
|
||||||
|
|
||||||
// check that all of y's direct keys are in x
|
// check that all of y's direct keys are in x
|
||||||
let p;
|
for (const p in y) {
|
||||||
for (p in y) {
|
|
||||||
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
|
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// finally, compare each of x's keys with y
|
// finally, compare each of x's keys with y
|
||||||
for (p in y) { // eslint-disable-line guard-for-in
|
for (const p in x) {
|
||||||
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
|
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p) || !deepCompare(x[p], y[p])) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!deepCompare(x[p], y[p])) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* jshint +W089 */
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dev note: This returns a tuple, but jsdoc doesn't like that. https://github.com/jsdoc/jsdoc/issues/1703
|
// Dev note: This returns an array of tuples, but jsdoc doesn't like that. https://github.com/jsdoc/jsdoc/issues/1703
|
||||||
/**
|
/**
|
||||||
* Creates an array of object properties/values (entries) then
|
* Creates an array of object properties/values (entries) then
|
||||||
* sorts the result by key, recursively. The input object must
|
* sorts the result by key, recursively. The input object must
|
||||||
@@ -328,7 +342,7 @@ export function escapeRegExp(string: string): string {
|
|||||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function globToRegexp(glob: string, extended?: any): string {
|
export function globToRegexp(glob: string, extended = false): string {
|
||||||
// From
|
// From
|
||||||
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
|
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
|
||||||
// Because micromatch is about 130KB with dependencies,
|
// Because micromatch is about 130KB with dependencies,
|
||||||
@@ -336,7 +350,7 @@ export function globToRegexp(glob: string, extended?: any): string {
|
|||||||
const replacements: ([RegExp, string | ((substring: string, ...args: any[]) => string) ])[] = [
|
const replacements: ([RegExp, string | ((substring: string, ...args: any[]) => string) ])[] = [
|
||||||
[/\\\*/g, '.*'],
|
[/\\\*/g, '.*'],
|
||||||
[/\?/g, '.'],
|
[/\?/g, '.'],
|
||||||
extended !== false && [
|
!extended && [
|
||||||
/\\\[(!|)(.*)\\]/g,
|
/\\\[(!|)(.*)\\]/g,
|
||||||
(_match: string, neg: string, pat: string) => [
|
(_match: string, neg: string, pat: string) => [
|
||||||
'[',
|
'[',
|
||||||
|
@@ -12,6 +12,6 @@
|
|||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./src/**/*.ts",
|
"./src/**/*.ts",
|
||||||
"./spec/**/*.ts",
|
"./spec/**/*.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user