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:
|
||||
- uses: tibdex/backport@v2
|
||||
with:
|
||||
labels_template: "<%= JSON.stringify(labels) %>"
|
||||
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
|
||||
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
|
||||
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
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
|
||||
run: |
|
||||
ls -lah
|
||||
tag="${{ github.ref_name }}"
|
||||
version="${tag#v}"
|
||||
echo "VERSION=$version" >> $GITHUB_ENV
|
||||
@@ -51,3 +50,9 @@ jobs:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
keep_files: true
|
||||
publish_dir: .
|
||||
|
||||
npm:
|
||||
name: Publish
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
35
.github/workflows/static_analysis.yml
vendored
35
.github/workflows/static_analysis.yml
vendored
@@ -54,3 +54,38 @@ jobs:
|
||||
|
||||
- name: Generate Docs
|
||||
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)
|
||||
==================================================================================================
|
||||
|
||||
|
281
CONTRIBUTING.md
281
CONTRIBUTING.md
@@ -1,284 +1,5 @@
|
||||
Contributing code to matrix-js-sdk
|
||||
==================================
|
||||
|
||||
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
|
||||
willing to license their contributions under the same license as the project
|
||||
itself. We follow a simple 'inbound=outbound' model for contributions: the act
|
||||
of submitting an 'inbound' contribution means that the contributor agrees to
|
||||
license the code under the same terms as the project's overall 'outbound'
|
||||
license - in this case, Apache Software License v2 (see
|
||||
[LICENSE](LICENSE)).
|
||||
matrix-js-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md
|
||||
|
||||
How to contribute
|
||||
-----------------
|
||||
|
||||
The preferred and easiest way to contribute changes to the project is to fork
|
||||
it on github, and then create a pull request to ask us to pull your changes
|
||||
into our repo (https://help.github.com/articles/using-pull-requests/)
|
||||
|
||||
We use GitHub's pull request workflow to review the contribution, and either
|
||||
ask you to make any refinements needed or merge it and make them ourselves.
|
||||
|
||||
Things that should go into your PR description:
|
||||
* A changelog entry in the `Notes` section (see below)
|
||||
* References to any bugs fixed by the change (in GitHub's `Fixes` notation)
|
||||
* Describe the why and what is changing in the PR description so it's easy for
|
||||
onlookers and reviewers to onboard and context switch. This information is
|
||||
also helpful when we come back to look at this in 6 months and ask "why did
|
||||
we do it like that?" we have a chance of finding out.
|
||||
* Why didn't it work before? Why does it work now? What use cases does it
|
||||
unlock?
|
||||
* If you find yourself adding information on how the code works or why you
|
||||
chose to do it the way you did, make sure this information is instead
|
||||
written as comments in the code itself.
|
||||
* Sometimes a PR can change considerably as it is developed. In this case,
|
||||
the description should be updated to reflect the most recent state of
|
||||
the PR. (It can be helpful to retain the old content under a suitable
|
||||
heading, for additional context.)
|
||||
* Include both **before** and **after** screenshots to easily compare and discuss
|
||||
what's changing.
|
||||
* Include a step-by-step testing strategy so that a reviewer can check out the
|
||||
code locally and easily get to the point of testing your change.
|
||||
* Add comments to the diff for the reviewer that might help them to understand
|
||||
why the change is necessary or how they might better understand and review it.
|
||||
|
||||
We rely on information in pull request to populate the information that goes
|
||||
into the changelogs our users see, both for the JS SDK itself and also for some
|
||||
projects based on it. This is picked up from both labels on the pull request and
|
||||
the `Notes:` annotation in the description. By default, the PR title will be
|
||||
used for the changelog entry, but you can specify more options, as follows.
|
||||
|
||||
To add a longer, more detailed description of the change for the changelog:
|
||||
|
||||
|
||||
*Fix llama herding bug*
|
||||
|
||||
```
|
||||
Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase
|
||||
```
|
||||
|
||||
For some PRs, it's not useful to have an entry in the user-facing changelog (this is
|
||||
the default for PRs labelled with `T-Task`):
|
||||
|
||||
*Remove outdated comment from `Ungulates.ts`*
|
||||
```
|
||||
Notes: none
|
||||
```
|
||||
|
||||
Sometimes, you're fixing a bug in a downstream project, in which case you want
|
||||
an entry in that project's changelog. You can do that too:
|
||||
|
||||
*Fix another herding bug*
|
||||
```
|
||||
Notes: Fix a bug where the `herd()` function would only work on Tuesdays
|
||||
element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays
|
||||
```
|
||||
|
||||
This example is for Element Web. You can specify:
|
||||
* matrix-react-sdk
|
||||
* element-web
|
||||
* element-desktop
|
||||
|
||||
If your PR introduces a breaking change, use the `Notes` section in the same
|
||||
way, additionally adding the `X-Breaking-Change` label (see below). There's no need
|
||||
to specify in the notes that it's a breaking change - this will be added
|
||||
automatically based on the label - but remember to tell the developer how to
|
||||
migrate:
|
||||
|
||||
*Remove legacy class*
|
||||
|
||||
```
|
||||
Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead.
|
||||
```
|
||||
|
||||
Other metadata can be added using labels.
|
||||
* `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a *major* version bump.
|
||||
* `T-Enhancement`: A new feature - adding this label will mean the change causes a *minor* version bump.
|
||||
* `T-Defect`: A bug fix (in either code or docs).
|
||||
* `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one.
|
||||
|
||||
If you don't have permission to add labels, your PR reviewer(s) can work with you
|
||||
to add them: ask in the PR description or comments.
|
||||
|
||||
We use continuous integration, and all pull requests get automatically tested:
|
||||
if your change breaks the build, then the PR will show that there are failed
|
||||
checks, so please check back after a few minutes.
|
||||
|
||||
Tests
|
||||
-----
|
||||
Your PR should include tests.
|
||||
|
||||
For new user facing features in `matrix-react-sdk` or `element-web`, you
|
||||
must include:
|
||||
|
||||
1. Comprehensive unit tests written in Jest. These are located in `/test`.
|
||||
2. "happy path" end-to-end tests.
|
||||
These are located in `/test/end-to-end-tests` in `matrix-react-sdk`, and
|
||||
are run using `element-web`. Ideally, you would also include tests for edge
|
||||
and error cases.
|
||||
|
||||
Unit tests are expected even when the feature is in labs. It's good practice
|
||||
to write tests alongside the code as it ensures the code is testable from
|
||||
the start, and gives you a fast feedback loop while you're developing the
|
||||
functionality. End-to-end tests should be added prior to the feature
|
||||
leaving labs, but don't have to be present from the start (although it might
|
||||
be beneficial to have some running early, so you can test things faster).
|
||||
|
||||
For bugs in those repos, your change must include at least one unit test or
|
||||
end-to-end test; which is best depends on what sort of test most concisely
|
||||
exercises the area.
|
||||
|
||||
Changes to `matrix-js-sdk` must be accompanied by unit tests written in Jest.
|
||||
These are located in `/spec/`.
|
||||
|
||||
When writing unit tests, please aim for a high level of test coverage
|
||||
for new code - 80% or greater. If you cannot achieve that, please document
|
||||
why it's not possible in your PR.
|
||||
|
||||
Some sections of code are not sensible to add coverage for, such as those
|
||||
which explicitly inhibit noisy logging for tests. Which can be hidden using
|
||||
an istanbul magic comment as [documented here][1]. See example:
|
||||
```javascript
|
||||
/* istanbul ignore if */
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
logger.error("Log line that is noisy enough in tests to want to skip");
|
||||
}
|
||||
```
|
||||
|
||||
Tests validate that your change works as intended and also document
|
||||
concisely what is being changed. Ideally, your new tests fail
|
||||
prior to your change, and succeed once it has been applied. You may
|
||||
find this simpler to achieve if you write the tests first.
|
||||
|
||||
If you're spiking some code that's experimental and not being used to support
|
||||
production features, exceptions can be made to requirements for tests.
|
||||
Note that tests will still be required in order to ship the feature, and it's
|
||||
strongly encouraged to think about tests early in the process, as adding
|
||||
tests later will become progressively more difficult.
|
||||
|
||||
If you're not sure how to approach writing tests for your change, ask for help
|
||||
in [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
|
||||
|
||||
Code style
|
||||
----------
|
||||
The js-sdk aims to target TypeScript/ES6. All new files should be written in
|
||||
TypeScript and existing files should use ES6 principles where possible.
|
||||
|
||||
Members should not be exported as a default export in general - it causes problems
|
||||
with the architecture of the SDK (index file becomes less clear) and could
|
||||
introduce naming problems (as default exports get aliased upon import). In
|
||||
general, avoid using `export default`.
|
||||
|
||||
The remaining code-style for matrix-js-sdk is not formally documented, but
|
||||
contributors are encouraged to read the
|
||||
[code style document for matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md)
|
||||
and follow the principles set out there.
|
||||
|
||||
Please ensure your changes match the cosmetic style of the existing project,
|
||||
and ***never*** mix cosmetic and functional changes in the same commit, as it
|
||||
makes it horribly hard to review otherwise.
|
||||
|
||||
Attribution
|
||||
-----------
|
||||
Everyone who contributes anything to Matrix is welcome to be listed in the
|
||||
AUTHORS.rst file for the project in question. Please feel free to include a
|
||||
change to AUTHORS.rst in your pull request to list yourself and a short
|
||||
description of the area(s) you've worked on. Also, we sometimes have swag to
|
||||
give away to contributors - if you feel that Matrix-branded apparel is missing
|
||||
from your life, please mail us your shipping address to matrix at matrix.org
|
||||
and we'll try to fix it :)
|
||||
|
||||
Sign off
|
||||
--------
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
and you agree to license it under the same terms as the project's license, we've
|
||||
adopted the same lightweight approach that the Linux Kernel
|
||||
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
|
||||
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO (Developer Certificate of Origin:
|
||||
http://developercertificate.org/). This is a simple declaration that you wrote
|
||||
the contribution or otherwise have the right to contribute it to Matrix:
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
If you agree to this for your contribution, then all that's needed is to
|
||||
include the line in your commit or pull request comment:
|
||||
|
||||
```
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
```
|
||||
|
||||
We accept contributions under a legally identifiable name, such as your name on
|
||||
government documentation or common-law names (names claimed by legitimate usage
|
||||
or repute). Unfortunately, we cannot accept anonymous contributions at this
|
||||
time.
|
||||
|
||||
Git allows you to add this signoff automatically when using the `-s` flag to
|
||||
`git commit`, which uses the name and email set in your `user.name` and
|
||||
`user.email` git configs.
|
||||
|
||||
If you forgot to sign off your commits before making your pull request and are
|
||||
on Git 2.17+ you can mass signoff using rebase:
|
||||
|
||||
```
|
||||
git rebase --signoff origin/develop
|
||||
```
|
||||
|
||||
Review expectations
|
||||
===================
|
||||
|
||||
See https://github.com/vector-im/element-meta/wiki/Review-process
|
||||
|
||||
|
||||
Merge Strategy
|
||||
==============
|
||||
|
||||
The preferred method for merging pull requests is squash merging to keep the
|
||||
commit history trim, but it is up to the discretion of the team member merging
|
||||
the change. We do not support rebase merges due to `allchange` being unable to
|
||||
handle them. When merging make sure to leave the default commit title, or
|
||||
at least leave the PR number at the end in brackets like by default.
|
||||
When stacking pull requests, you may wish to do the following:
|
||||
|
||||
1. Branch from develop to your branch (branch1), push commits onto it and open a pull request
|
||||
2. Branch from your base branch (branch1) to your work branch (branch2), push commits and open a pull request configuring the base to be branch1, saying in the description that it is based on your other PR.
|
||||
3. Merge the first PR using a merge commit otherwise your stacked PR will need a rebase. Github will automatically adjust the base branch of your other PR to be develop.
|
||||
|
||||
|
||||
[1]: https://github.com/gotwarlost/istanbul/blob/master/ignoring-code-for-coverage.md
|
||||
|
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "19.3.0",
|
||||
"version": "19.4.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"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",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/jest": "^28.0.0",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "16",
|
||||
"@types/request": "^2.48.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"allchange": "^1.0.6",
|
||||
"babel-jest": "^28.0.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "8.20.0",
|
||||
"eslint": "8.23.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-matrix-org": "^0.5.0",
|
||||
"eslint-plugin-matrix-org": "^0.6.0",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"jest": "^28.0.0",
|
||||
"jest-environment-jsdom": "^28.1.3",
|
||||
"jest": "^29.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-sonar-reporter": "^2.0.0",
|
||||
"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.
|
||||
#
|
||||
# Requires:
|
||||
# github-changelog-generator; install via:
|
||||
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
|
||||
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
|
||||
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
|
||||
# npm; typically installed by Node.js
|
||||
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
|
||||
#
|
||||
# Note: this script is also used to release matrix-react-sdk and element-web.
|
||||
# Note: this script is also used to release matrix-react-sdk, element-web, and element-desktop.
|
||||
|
||||
set -e
|
||||
|
||||
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
||||
if [[ `command -v hub` ]] && [[ `hub --version` =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
|
||||
if [[ $(command -v hub) ]] && [[ $(hub --version) =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
|
||||
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
|
||||
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
|
||||
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
|
||||
@@ -26,7 +23,6 @@ else
|
||||
echo "hub is required: please install it"
|
||||
exit
|
||||
fi
|
||||
npm --version > /dev/null || (echo "npm is required: please install it"; kill $$)
|
||||
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
|
||||
|
||||
USAGE="$0 [-x] [-c changelog_file] vX.Y.Z"
|
||||
@@ -37,7 +33,6 @@ $USAGE
|
||||
|
||||
-c changelog_file: specify name of file containing changelog
|
||||
-x: skip updating the changelog
|
||||
-n: skip publish to NPM
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -59,10 +54,8 @@ if ! git diff-files --quiet; then
|
||||
fi
|
||||
|
||||
skip_changelog=
|
||||
skip_npm=
|
||||
changelog_file="CHANGELOG.md"
|
||||
expected_npm_user="matrixdotorg"
|
||||
while getopts hc:u:xzn f; do
|
||||
while getopts hc:x f; do
|
||||
case $f in
|
||||
h)
|
||||
help
|
||||
@@ -74,21 +67,58 @@ while getopts hc:u:xzn f; do
|
||||
x)
|
||||
skip_changelog=1
|
||||
;;
|
||||
n)
|
||||
skip_npm=1
|
||||
;;
|
||||
u)
|
||||
expected_npm_user="$OPTARG"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift `expr $OPTIND - 1`
|
||||
shift $(expr $OPTIND - 1)
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $USAGE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function check_dependency {
|
||||
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
|
||||
# to have a hard time getting that right. See also
|
||||
# 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
|
||||
yarn install --ignore-scripts --pure-lockfile
|
||||
|
||||
# Login and publish continues to use `npm`, as it seems to have more clearly
|
||||
# defined options and semantics than `yarn` for writing to the registry.
|
||||
if [ -z "$skip_npm" ]; then
|
||||
actual_npm_user=`npm whoami`;
|
||||
if [ $expected_npm_user != $actual_npm_user ]; then
|
||||
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ignore leading v on release
|
||||
release="${1#v}"
|
||||
tag="v${release}"
|
||||
@@ -117,7 +137,7 @@ prerelease=0
|
||||
# see if the version has a hyphen in it. Crude,
|
||||
# but semver doesn't support postreleases so anything
|
||||
# with a hyphen is a prerelease.
|
||||
echo $release | grep -q '-' && prerelease=1
|
||||
echo "$release" | grep -q '-' && prerelease=1
|
||||
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
echo Making a PRE-RELEASE
|
||||
@@ -143,13 +163,13 @@ if [ -z "$skip_changelog" ]; then
|
||||
yarn run allchange "$release"
|
||||
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"
|
||||
git commit "$changelog_file" -m "Prepare changelog for $tag"
|
||||
fi
|
||||
fi
|
||||
latest_changes=`mktemp`
|
||||
cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_changes}"
|
||||
latest_changes=$(mktemp)
|
||||
cat "${changelog_file}" | "$(dirname "$0")/scripts/changelog_head.py" > "${latest_changes}"
|
||||
|
||||
set -x
|
||||
|
||||
@@ -176,19 +196,19 @@ do
|
||||
done
|
||||
|
||||
# commit yarn.lock if it exists, is versioned, and is modified
|
||||
if [[ -f yarn.lock && `git status --porcelain yarn.lock | grep '^ M'` ]];
|
||||
if [[ -f yarn.lock && $(git status --porcelain yarn.lock | grep '^ M') ]];
|
||||
then
|
||||
pkglock='yarn.lock'
|
||||
else
|
||||
pkglock=''
|
||||
fi
|
||||
git commit package.json $pkglock -m "$tag"
|
||||
git commit package.json "$pkglock" -m "$tag"
|
||||
|
||||
|
||||
# figure out if we should be signing this release
|
||||
signing_id=
|
||||
if [ -f release_config.yaml ]; then
|
||||
result=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']" 2> /dev/null || true`
|
||||
result=$(cat release_config.yaml | python -c "import yaml; import sys; print(yaml.load(sys.stdin)['signing_id'])" 2> /dev/null || true)
|
||||
if [ "$?" -eq 0 ]; then
|
||||
signing_id=$result
|
||||
fi
|
||||
@@ -206,8 +226,8 @@ assets=''
|
||||
dodist=0
|
||||
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
|
||||
if [ $dodist -eq 0 ]; then
|
||||
projdir=`pwd`
|
||||
builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'`
|
||||
projdir=$(pwd)
|
||||
builddir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
|
||||
echo "Building distribution copy in $builddir"
|
||||
pushd "$builddir"
|
||||
git clone "$projdir" .
|
||||
@@ -232,7 +252,7 @@ fi
|
||||
if [ -n "$signing_id" ]; then
|
||||
# make a signed tag
|
||||
# gnupg seems to fail to get the right tty device unless we set it here
|
||||
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
|
||||
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=$(tty) git tag -u "$signing_id" -F "${latest_changes}" "$tag"
|
||||
else
|
||||
git tag -a -F "${latest_changes}" "$tag"
|
||||
fi
|
||||
@@ -270,7 +290,7 @@ if [ -n "$signing_id" ]; then
|
||||
curl -L "${gh_project_url}/archive/${tarfile}" -o "${tarfile}"
|
||||
|
||||
# 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
|
||||
|
||||
# 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'
|
||||
fi
|
||||
|
||||
release_text=`mktemp`
|
||||
release_text=$(mktemp)
|
||||
echo "$tag" > "${release_text}"
|
||||
echo >> "${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
|
||||
rm -rf "$builddir"
|
||||
@@ -310,19 +330,6 @@ fi
|
||||
rm "${release_text}"
|
||||
rm "${latest_changes}"
|
||||
|
||||
# Login and publish continues to use `npm`, as it seems to have more clearly
|
||||
# defined options and semantics than `yarn` for writing to the registry.
|
||||
# Tag both releases and prereleases as `next` so the last stable release remains
|
||||
# the default.
|
||||
if [ -z "$skip_npm" ]; then
|
||||
npm publish --tag next
|
||||
if [ $prerelease -eq 0 ]; then
|
||||
# For a release, also add the default `latest` tag.
|
||||
package=$(cat package.json | jq -er .name)
|
||||
npm dist-tag add "$package@$release" latest
|
||||
fi
|
||||
fi
|
||||
|
||||
# if it is a pre-release, leave it on the release branch for now.
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
git checkout "$rel_branch"
|
||||
@@ -339,34 +346,19 @@ git merge "$rel_branch" --no-edit
|
||||
git push origin master
|
||||
|
||||
# finally, merge master back onto develop (if it exists)
|
||||
if [ $(git branch -lr | grep origin/develop -c) -ge 1 ]; then
|
||||
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
|
||||
git checkout develop
|
||||
git pull
|
||||
git merge master --no-edit
|
||||
|
||||
# When merging to develop, we need revert the `main` and `typings` fields if
|
||||
# we adjusted them previously.
|
||||
for i in main typings
|
||||
do
|
||||
# If a `lib` prefixed value is present, it means we adjusted the field
|
||||
# earlier at publish time, so we should revert it now.
|
||||
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
|
||||
# If there's a `src` prefixed value, use that, otherwise delete.
|
||||
# This is used to delete the `typings` field and reset `main` back
|
||||
# to the TypeScript source.
|
||||
src_value=$(jq -r ".matrix_src_$i" package.json)
|
||||
if [ "$src_value" != "null" ]; then
|
||||
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
|
||||
else
|
||||
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$(git ls-files --modified package.json)" ]; then
|
||||
echo "Committing develop package.json"
|
||||
git commit package.json -m "Resetting package fields for development"
|
||||
fi
|
||||
|
||||
git push origin develop
|
||||
fi
|
||||
|
||||
[ -x ./post-release.sh ] && ./post-release.sh
|
||||
|
||||
if [ $has_subprojects -eq 1 ] && [ $prerelease -eq 0 ]; then
|
||||
echo "Resetting subprojects to develop"
|
||||
for proj in $subprojects; do
|
||||
reset_dependency "$proj"
|
||||
done
|
||||
git push origin develop
|
||||
fi
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -17,31 +17,32 @@ limitations under the License.
|
||||
|
||||
/**
|
||||
* A mock implementation of the webstorage api
|
||||
* @constructor
|
||||
*/
|
||||
export function MockStorageApi() {
|
||||
this.data = {};
|
||||
this.keys = [];
|
||||
this.length = 0;
|
||||
export class MockStorageApi {
|
||||
public data: Record<string, string> = {};
|
||||
public keys: string[] = [];
|
||||
public length = 0;
|
||||
|
||||
public setItem(k: string, v: string): void {
|
||||
this.data[k] = v;
|
||||
this.recalc();
|
||||
}
|
||||
|
||||
MockStorageApi.prototype = {
|
||||
setItem: function(k, v) {
|
||||
this.data[k] = v;
|
||||
this._recalc();
|
||||
},
|
||||
getItem: function(k) {
|
||||
public getItem(k: string): string | null {
|
||||
return this.data[k] || null;
|
||||
},
|
||||
removeItem: function(k) {
|
||||
}
|
||||
|
||||
public removeItem(k: string): void {
|
||||
delete this.data[k];
|
||||
this._recalc();
|
||||
},
|
||||
key: function(index) {
|
||||
this.recalc();
|
||||
}
|
||||
|
||||
public key(index: number): string {
|
||||
return this.keys[index];
|
||||
},
|
||||
_recalc: function() {
|
||||
const keys = [];
|
||||
}
|
||||
|
||||
private recalc(): void {
|
||||
const keys: string[] = [];
|
||||
for (const k in this.data) {
|
||||
if (!this.data.hasOwnProperty(k)) {
|
||||
continue;
|
||||
@@ -50,6 +51,5 @@ MockStorageApi.prototype = {
|
||||
}
|
||||
this.keys = keys;
|
||||
this.length = keys.length;
|
||||
},
|
||||
};
|
||||
|
||||
}
|
||||
}
|
@@ -50,7 +50,7 @@ export class TestClient {
|
||||
options?: Partial<ICreateClientOpts>,
|
||||
) {
|
||||
if (sessionStoreBackend === undefined) {
|
||||
sessionStoreBackend = new MockStorageApi();
|
||||
sessionStoreBackend = new MockStorageApi() as unknown as Storage;
|
||||
}
|
||||
|
||||
this.httpBackend = new MockHttpBackend();
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -168,6 +168,7 @@ describe("SlidingSyncSdk", () => {
|
||||
const roomD = "!d_with_notif_count:localhost";
|
||||
const roomE = "!e_with_invite:localhost";
|
||||
const roomF = "!f_calc_room_name:localhost";
|
||||
const roomG = "!g_join_invite_counts:localhost";
|
||||
const data: Record<string, MSC3575RoomData> = {
|
||||
[roomA]: {
|
||||
name: "A",
|
||||
@@ -261,12 +262,25 @@ describe("SlidingSyncSdk", () => {
|
||||
],
|
||||
initial: true,
|
||||
},
|
||||
[roomG]: {
|
||||
name: "G",
|
||||
required_state: [],
|
||||
timeline: [
|
||||
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
|
||||
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
|
||||
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
|
||||
],
|
||||
joined_count: 5,
|
||||
invited_count: 2,
|
||||
initial: true,
|
||||
},
|
||||
};
|
||||
|
||||
it("can be created with required_state and timeline", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
|
||||
const gotRoom = client.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.name).toEqual(data[roomA].name);
|
||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
|
||||
@@ -276,6 +290,7 @@ describe("SlidingSyncSdk", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
|
||||
const gotRoom = client.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.name).toEqual(data[roomB].name);
|
||||
expect(gotRoom.getMyMembership()).toEqual("join");
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
|
||||
@@ -285,6 +300,7 @@ describe("SlidingSyncSdk", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
|
||||
const gotRoom = client.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
|
||||
).toEqual(data[roomC].highlight_count);
|
||||
@@ -294,15 +310,26 @@ describe("SlidingSyncSdk", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
|
||||
const gotRoom = client.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
|
||||
).toEqual(data[roomD].notification_count);
|
||||
});
|
||||
|
||||
it("can be created with an invited/joined_count", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
|
||||
const gotRoom = client.getRoom(roomG);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
|
||||
expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
|
||||
});
|
||||
|
||||
it("can be created with invite_state", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
|
||||
const gotRoom = client.getRoom(roomE);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getMyMembership()).toEqual("invite");
|
||||
expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite);
|
||||
});
|
||||
@@ -311,6 +338,7 @@ describe("SlidingSyncSdk", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
|
||||
const gotRoom = client.getRoom(roomF);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.name,
|
||||
).toEqual(data[roomF].name);
|
||||
@@ -326,6 +354,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
const gotRoom = client.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
const newTimeline = data[roomA].timeline;
|
||||
newTimeline.push(newEvent);
|
||||
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline);
|
||||
@@ -333,6 +362,8 @@ describe("SlidingSyncSdk", () => {
|
||||
|
||||
it("can update with a new required_state event", async () => {
|
||||
let gotRoom = client.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, {
|
||||
required_state: [
|
||||
@@ -343,6 +374,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
gotRoom = client.getRoom(roomB);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
|
||||
});
|
||||
|
||||
@@ -355,6 +387,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
const gotRoom = client.getRoom(roomC);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
|
||||
).toEqual(1);
|
||||
@@ -369,11 +402,25 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
const gotRoom = client.getRoom(roomD);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(
|
||||
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it("can update with a new joined_count", () => {
|
||||
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomG, {
|
||||
name: data[roomD].name,
|
||||
required_state: [],
|
||||
timeline: [],
|
||||
joined_count: 1,
|
||||
});
|
||||
const gotRoom = client.getRoom(roomG);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
expect(gotRoom.getJoinedMemberCount()).toEqual(1);
|
||||
});
|
||||
|
||||
// Regression test for a bug which caused the timeline entries to be out-of-order
|
||||
// when the same room appears twice with different timeline limits. E.g appears in
|
||||
// the list with timeline_limit:1 then appears again as a room subscription with
|
||||
@@ -394,6 +441,7 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
const gotRoom = client.getRoom(roomA);
|
||||
expect(gotRoom).toBeDefined();
|
||||
if (gotRoom == null) { return; }
|
||||
|
||||
logger.log("want:", oldTimeline.map((e) => (e.type + " : " + (e.content || {}).body)));
|
||||
logger.log("got:", gotRoom.getLiveTimeline().getEvents().map(
|
||||
|
@@ -558,6 +558,153 @@ describe("SlidingSync", () => {
|
||||
await httpBackend.flushAllExpected();
|
||||
await responseProcessed;
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
@@ -20,6 +20,7 @@ import * as utils from "../src/utils";
|
||||
|
||||
// try to load the olm library.
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
global.Olm = require('@matrix-org/olm');
|
||||
logger.log('loaded libolm');
|
||||
} catch (e) {
|
||||
@@ -28,6 +29,7 @@ try {
|
||||
|
||||
// also try to set node crypto
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const crypto = require('crypto');
|
||||
utils.setCrypto(crypto);
|
||||
} catch (err) {
|
@@ -24,5 +24,5 @@ limitations under the License.
|
||||
* expect(beaconLivenessEmits.length).toBe(1);
|
||||
* ```
|
||||
*/
|
||||
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, unknown[]>) =>
|
||||
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, any[]>) =>
|
||||
spy.mock.calls.filter((args) => args[0] === eventType);
|
||||
|
@@ -147,9 +147,9 @@ export function mkEventCustom<T>(base: T): T & GeneratedMetadata {
|
||||
interface IPresenceOpts {
|
||||
user?: string;
|
||||
sender?: string;
|
||||
url: string;
|
||||
name: string;
|
||||
ago: number;
|
||||
url?: string;
|
||||
name?: string;
|
||||
ago?: number;
|
||||
presence?: string;
|
||||
event?: boolean;
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
import { logger } from '../../src/logger';
|
||||
import { MemoryStore } from "../../src";
|
||||
import { IStore } from '../../src/store';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -158,8 +159,8 @@ describe("Crypto", function() {
|
||||
let fakeEmitter;
|
||||
|
||||
beforeEach(async function() {
|
||||
const mockStorage = new MockStorageApi();
|
||||
const clientStore = new MemoryStore({ localStorage: mockStorage });
|
||||
const mockStorage = new MockStorageApi() as unknown as Storage;
|
||||
const clientStore = new MemoryStore({ localStorage: mockStorage }) as unknown as IStore;
|
||||
const cryptoStore = new MemoryCryptoStore();
|
||||
|
||||
cryptoStore.storeEndToEndDeviceData({
|
||||
@@ -469,12 +470,12 @@ describe("Crypto", function() {
|
||||
jest.setTimeout(10000);
|
||||
const client = (new TestClient("@a:example.com", "dev")).client;
|
||||
await client.initCrypto();
|
||||
client.crypto.getSecretStorageKey = async () => null;
|
||||
client.crypto.getSecretStorageKey = jest.fn().mockResolvedValue(null);
|
||||
client.crypto.isCrossSigningReady = async () => false;
|
||||
client.crypto.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
|
||||
client.crypto.baseApis.setAccountData = () => null;
|
||||
client.crypto.baseApis.uploadKeySignatures = () => null;
|
||||
client.crypto.baseApis.http.authedRequest = () => null;
|
||||
client.crypto.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
|
||||
client.crypto.baseApis.uploadKeySignatures = jest.fn();
|
||||
client.crypto.baseApis.http.authedRequest = jest.fn();
|
||||
const createSecretStorageKey = async () => {
|
||||
return {
|
||||
keyInfo: undefined, // Returning undefined here used to cause a crash
|
||||
|
@@ -32,8 +32,8 @@ import { ClientEvent, MatrixClient, RoomMember } from '../../../../src';
|
||||
import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo';
|
||||
import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning';
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||
|
||||
const ROOM_ID = '!ROOM:ID';
|
||||
|
||||
|
@@ -34,7 +34,7 @@ import { IAbortablePromise, MatrixScheduler } from '../../../src';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
|
||||
|
||||
const ROOM_ID = '!ROOM:ID';
|
||||
|
||||
|
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
IndexedDBCryptoStore,
|
||||
} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import { CryptoStore } from '../../../src/crypto/store/base';
|
||||
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import { LocalStorageCryptoStore } from '../../../src/crypto/store/localStorage-crypto-store';
|
||||
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
|
||||
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||
|
||||
@@ -26,36 +26,39 @@ import 'jest-localstorage-mock';
|
||||
const requests = [
|
||||
{
|
||||
requestId: "A",
|
||||
requestBody: { session_id: "A", room_id: "A" },
|
||||
requestBody: { session_id: "A", room_id: "A", sender_key: "A", algorithm: "m.megolm.v1.aes-sha2" },
|
||||
state: RoomKeyRequestState.Sent,
|
||||
recipients: [
|
||||
{ userId: "@alice:example.com", deviceId: "*" },
|
||||
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
|
||||
],
|
||||
},
|
||||
{
|
||||
requestId: "B",
|
||||
requestBody: { session_id: "B", room_id: "B" },
|
||||
requestBody: { session_id: "B", room_id: "B", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" },
|
||||
state: RoomKeyRequestState.Sent,
|
||||
recipients: [
|
||||
{ userId: "@alice:example.com", deviceId: "*" },
|
||||
{ userId: "@carrie:example.com", deviceId: "barbazquux" },
|
||||
],
|
||||
},
|
||||
{
|
||||
requestId: "C",
|
||||
requestBody: { session_id: "C", room_id: "C" },
|
||||
requestBody: { session_id: "C", room_id: "C", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" },
|
||||
state: RoomKeyRequestState.Unsent,
|
||||
recipients: [
|
||||
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore",
|
||||
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore",
|
||||
() => new IndexedDBCryptoStore(undefined, "tests")],
|
||||
["MemoryCryptoStore", () => {
|
||||
const store = new IndexedDBCryptoStore(undefined, "tests");
|
||||
// @ts-ignore set private properties
|
||||
store.backend = new MemoryCryptoStore();
|
||||
// @ts-ignore
|
||||
store.backendPromise = Promise.resolve(store.backend);
|
||||
return store;
|
||||
}],
|
||||
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
|
||||
["MemoryCryptoStore", () => new MemoryCryptoStore()],
|
||||
])("Outgoing room key requests [%s]", function(name, dbFactory) {
|
||||
let store;
|
||||
let store: CryptoStore;
|
||||
|
||||
beforeAll(async () => {
|
||||
store = dbFactory();
|
||||
@@ -75,6 +78,15 @@ describe.each([
|
||||
});
|
||||
});
|
||||
|
||||
it("getOutgoingRoomKeyRequestsByTarget retrieves all entries with a given target",
|
||||
async () => {
|
||||
const r = await store.getOutgoingRoomKeyRequestsByTarget(
|
||||
"@becca:example.com", "foobarbaz", [RoomKeyRequestState.Sent],
|
||||
);
|
||||
expect(r).toHaveLength(1);
|
||||
expect(r[0]).toEqual(requests[0]);
|
||||
});
|
||||
|
||||
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
|
||||
async () => {
|
||||
const r =
|
||||
|
@@ -16,14 +16,15 @@ limitations under the License.
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import {
|
||||
DuplicateStrategy,
|
||||
EventTimeline,
|
||||
EventTimelineSet,
|
||||
EventType,
|
||||
Filter,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
Room,
|
||||
DuplicateStrategy,
|
||||
} from '../../src';
|
||||
import { Thread } from "../../src/models/thread";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
@@ -291,4 +292,34 @@ describe('EventTimelineSet', () => {
|
||||
expect(eventTimelineSet.canContain(event)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleRemoteEcho", () => {
|
||||
it("should add to liveTimeline only if the event matches the filter", () => {
|
||||
const filter = new Filter(client.getUserId()!, "test_filter");
|
||||
filter.setDefinition({
|
||||
room: {
|
||||
timeline: {
|
||||
types: [EventType.RoomMessage],
|
||||
},
|
||||
},
|
||||
});
|
||||
const eventTimelineSet = new EventTimelineSet(room, { filter }, client);
|
||||
|
||||
const roomMessageEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: { body: "test" },
|
||||
event_id: "!test1:server",
|
||||
});
|
||||
eventTimelineSet.handleRemoteEcho(roomMessageEvent, "~!local-event-id:server", roomMessageEvent.getId());
|
||||
expect(eventTimelineSet.getLiveTimeline().getEvents()).toContain(roomMessageEvent);
|
||||
|
||||
const roomFilteredEvent = new MatrixEvent({
|
||||
type: "other_event_type",
|
||||
content: { body: "test" },
|
||||
event_id: "!test2:server",
|
||||
});
|
||||
eventTimelineSet.handleRemoteEcho(roomFilteredEvent, "~!local-event-id:server", roomFilteredEvent.getId());
|
||||
expect(eventTimelineSet.getLiveTimeline().getEvents()).not.toContain(roomFilteredEvent);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -36,9 +36,14 @@ import { ReceiptType } from "../../src/@types/read_receipts";
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { makeBeaconInfoContent } from "../../src/content-helpers";
|
||||
import { M_BEACON_INFO } from "../../src/@types/beacon";
|
||||
import { ContentHelpers, Room } from "../../src";
|
||||
import { ContentHelpers, EventTimeline, Room } from "../../src";
|
||||
import { supportsMatrixCall } from "../../src/webrtc/call";
|
||||
import { makeBeaconEvent } from "../test-utils/beacon";
|
||||
import {
|
||||
IGNORE_INVITES_ACCOUNT_EVENT_KEY,
|
||||
POLICIES_ACCOUNT_EVENT_TYPE,
|
||||
PolicyScope,
|
||||
} from "../../src/models/invites-ignorer";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -427,7 +432,7 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
});
|
||||
});
|
||||
await client.startClient();
|
||||
await client.startClient({ filter });
|
||||
await syncPromise;
|
||||
});
|
||||
|
||||
@@ -1412,4 +1417,301 @@ describe("MatrixClient", function() {
|
||||
expect(client.crypto.encryptAndSendToDevices).toHaveBeenLastCalledWith(deviceInfos, payload);
|
||||
});
|
||||
});
|
||||
|
||||
describe("support for ignoring invites", () => {
|
||||
beforeEach(() => {
|
||||
// Mockup `getAccountData`/`setAccountData`.
|
||||
const dataStore = new Map();
|
||||
client.setAccountData = function(eventType, content) {
|
||||
dataStore.set(eventType, content);
|
||||
return Promise.resolve();
|
||||
};
|
||||
client.getAccountData = function(eventType) {
|
||||
const data = dataStore.get(eventType);
|
||||
return new MatrixEvent({
|
||||
content: data,
|
||||
});
|
||||
};
|
||||
|
||||
// Mockup `createRoom`/`getRoom`/`joinRoom`, including state.
|
||||
const rooms = new Map();
|
||||
client.createRoom = function(options = {}) {
|
||||
const roomId = options["_roomId"] || `!room-${rooms.size}:example.org`;
|
||||
const state = new Map();
|
||||
const room = {
|
||||
roomId,
|
||||
_options: options,
|
||||
_state: state,
|
||||
getUnfilteredTimelineSet: function() {
|
||||
return {
|
||||
getLiveTimeline: function() {
|
||||
return {
|
||||
getState: function(direction) {
|
||||
expect(direction).toBe(EventTimeline.FORWARDS);
|
||||
return {
|
||||
getStateEvents: function(type) {
|
||||
const store = state.get(type) || {};
|
||||
return Object.keys(store).map(key => store[key]);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
rooms.set(roomId, room);
|
||||
return Promise.resolve({ room_id: roomId });
|
||||
};
|
||||
client.getRoom = function(roomId) {
|
||||
return rooms.get(roomId);
|
||||
};
|
||||
client.joinRoom = function(roomId) {
|
||||
return this.getRoom(roomId) || this.createRoom({ _roomId: roomId });
|
||||
};
|
||||
|
||||
// Mockup state events
|
||||
client.sendStateEvent = function(roomId, type, content) {
|
||||
const room = this.getRoom(roomId);
|
||||
const state: Map<string, any> = room._state;
|
||||
let store = state.get(type);
|
||||
if (!store) {
|
||||
store = {};
|
||||
state.set(type, store);
|
||||
}
|
||||
const eventId = `$event-${Math.random()}:example.org`;
|
||||
store[eventId] = {
|
||||
getId: function() {
|
||||
return eventId;
|
||||
},
|
||||
getRoomId: function() {
|
||||
return roomId;
|
||||
},
|
||||
getContent: function() {
|
||||
return content;
|
||||
},
|
||||
};
|
||||
return { event_id: eventId };
|
||||
};
|
||||
client.redactEvent = function(roomId, eventId) {
|
||||
const room = this.getRoom(roomId);
|
||||
const state: Map<string, any> = room._state;
|
||||
for (const store of state.values()) {
|
||||
delete store[eventId];
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it("should initialize and return the same `target` consistently", async () => {
|
||||
const target1 = await client.ignoredInvites.getOrCreateTargetRoom();
|
||||
const target2 = await client.ignoredInvites.getOrCreateTargetRoom();
|
||||
expect(target1).toBeTruthy();
|
||||
expect(target1).toBe(target2);
|
||||
});
|
||||
|
||||
it("should initialize and return the same `sources` consistently", async () => {
|
||||
const sources1 = await client.ignoredInvites.getOrCreateSourceRooms();
|
||||
const sources2 = await client.ignoredInvites.getOrCreateSourceRooms();
|
||||
expect(sources1).toBeTruthy();
|
||||
expect(sources1).toHaveLength(1);
|
||||
expect(sources1).toEqual(sources2);
|
||||
});
|
||||
|
||||
it("should initially not reject any invite", async () => {
|
||||
const rule = await client.ignoredInvites.getRuleForInvite({
|
||||
sender: "@foobar:example.org",
|
||||
roomId: "!snafu:somewhere.org",
|
||||
});
|
||||
expect(rule).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should reject invites once we have added a matching rule in the target room (scope: user)", async () => {
|
||||
await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
|
||||
|
||||
// We should reject this invite.
|
||||
const ruleMatch = await client.ignoredInvites.getRuleForInvite({
|
||||
sender: "@foobar:example.org",
|
||||
roomId: "!snafu:somewhere.org",
|
||||
});
|
||||
expect(ruleMatch).toBeTruthy();
|
||||
expect(ruleMatch.getContent()).toMatchObject({
|
||||
recommendation: "m.ban",
|
||||
reason: "just a test",
|
||||
});
|
||||
|
||||
// We should let these invites go through.
|
||||
const ruleWrongServer = await client.ignoredInvites.getRuleForInvite({
|
||||
sender: "@foobar:somewhere.org",
|
||||
roomId: "!snafu:somewhere.org",
|
||||
});
|
||||
expect(ruleWrongServer).toBeFalsy();
|
||||
|
||||
const ruleWrongServerRoom = await client.ignoredInvites.getRuleForInvite({
|
||||
sender: "@foobar:somewhere.org",
|
||||
roomId: "!snafu:example.org",
|
||||
});
|
||||
expect(ruleWrongServerRoom).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should reject invites once we have added a matching rule in the target room (scope: server)", async () => {
|
||||
const REASON = `Just a test ${Math.random()}`;
|
||||
await client.ignoredInvites.addRule(PolicyScope.Server, "example.org", REASON);
|
||||
|
||||
// We should reject these invites.
|
||||
const ruleSenderMatch = await client.ignoredInvites.getRuleForInvite({
|
||||
sender: "@foobar:example.org",
|
||||
roomId: "!snafu:somewhere.org",
|
||||
});
|
||||
expect(ruleSenderMatch).toBeTruthy();
|
||||
expect(ruleSenderMatch.getContent()).toMatchObject({
|
||||
recommendation: "m.ban",
|
||||
reason: REASON,
|
||||
});
|
||||
|
||||
const ruleRoomMatch = await client.ignoredInvites.getRuleForInvite({
|
||||
sender: "@foobar:somewhere.org",
|
||||
roomId: "!snafu:example.org",
|
||||
});
|
||||
expect(ruleRoomMatch).toBeTruthy();
|
||||
expect(ruleRoomMatch.getContent()).toMatchObject({
|
||||
recommendation: "m.ban",
|
||||
reason: REASON,
|
||||
});
|
||||
|
||||
// We should let these invites go through.
|
||||
const ruleWrongServer = await client.ignoredInvites.getRuleForInvite({
|
||||
sender: "@foobar:somewhere.org",
|
||||
roomId: "!snafu:somewhere.org",
|
||||
});
|
||||
expect(ruleWrongServer).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should reject invites once we have added a matching rule in the target room (scope: room)", async () => {
|
||||
const REASON = `Just a test ${Math.random()}`;
|
||||
const BAD_ROOM_ID = "!bad:example.org";
|
||||
const GOOD_ROOM_ID = "!good:example.org";
|
||||
await client.ignoredInvites.addRule(PolicyScope.Room, BAD_ROOM_ID, REASON);
|
||||
|
||||
// We should reject this invite.
|
||||
const ruleSenderMatch = await client.ignoredInvites.getRuleForInvite({
|
||||
sender: "@foobar:example.org",
|
||||
roomId: BAD_ROOM_ID,
|
||||
});
|
||||
expect(ruleSenderMatch).toBeTruthy();
|
||||
expect(ruleSenderMatch.getContent()).toMatchObject({
|
||||
recommendation: "m.ban",
|
||||
reason: REASON,
|
||||
});
|
||||
|
||||
// We should let these invites go through.
|
||||
const ruleWrongRoom = await client.ignoredInvites.getRuleForInvite({
|
||||
sender: BAD_ROOM_ID,
|
||||
roomId: GOOD_ROOM_ID,
|
||||
});
|
||||
expect(ruleWrongRoom).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should reject invites once we have added a matching rule in a non-target source room", async () => {
|
||||
const NEW_SOURCE_ROOM_ID = "!another-source:example.org";
|
||||
|
||||
// Make sure that everything is initialized.
|
||||
await client.ignoredInvites.getOrCreateSourceRooms();
|
||||
await client.joinRoom(NEW_SOURCE_ROOM_ID);
|
||||
await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
|
||||
|
||||
// Add a rule in the new source room.
|
||||
await client.sendStateEvent(NEW_SOURCE_ROOM_ID, PolicyScope.User, {
|
||||
entity: "*:example.org",
|
||||
reason: "just a test",
|
||||
recommendation: "m.ban",
|
||||
});
|
||||
|
||||
// We should reject this invite.
|
||||
const ruleMatch = await client.ignoredInvites.getRuleForInvite({
|
||||
sender: "@foobar:example.org",
|
||||
roomId: "!snafu:somewhere.org",
|
||||
});
|
||||
expect(ruleMatch).toBeTruthy();
|
||||
expect(ruleMatch.getContent()).toMatchObject({
|
||||
recommendation: "m.ban",
|
||||
reason: "just a test",
|
||||
});
|
||||
|
||||
// We should let these invites go through.
|
||||
const ruleWrongServer = await client.ignoredInvites.getRuleForInvite({
|
||||
sender: "@foobar:somewhere.org",
|
||||
roomId: "!snafu:somewhere.org",
|
||||
});
|
||||
expect(ruleWrongServer).toBeFalsy();
|
||||
|
||||
const ruleWrongServerRoom = await client.ignoredInvites.getRuleForInvite({
|
||||
sender: "@foobar:somewhere.org",
|
||||
roomId: "!snafu:example.org",
|
||||
});
|
||||
expect(ruleWrongServerRoom).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not reject invites anymore once we have removed a rule", async () => {
|
||||
await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
|
||||
|
||||
// We should reject this invite.
|
||||
const ruleMatch = await client.ignoredInvites.getRuleForInvite({
|
||||
sender: "@foobar:example.org",
|
||||
roomId: "!snafu:somewhere.org",
|
||||
});
|
||||
expect(ruleMatch).toBeTruthy();
|
||||
expect(ruleMatch.getContent()).toMatchObject({
|
||||
recommendation: "m.ban",
|
||||
reason: "just a test",
|
||||
});
|
||||
|
||||
// After removing the invite, we shouldn't reject it anymore.
|
||||
await client.ignoredInvites.removeRule(ruleMatch);
|
||||
const ruleMatch2 = await client.ignoredInvites.getRuleForInvite({
|
||||
sender: "@foobar:example.org",
|
||||
roomId: "!snafu:somewhere.org",
|
||||
});
|
||||
expect(ruleMatch2).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should add new rules in the target room, rather than any other source room", async () => {
|
||||
const NEW_SOURCE_ROOM_ID = "!another-source:example.org";
|
||||
|
||||
// Make sure that everything is initialized.
|
||||
await client.ignoredInvites.getOrCreateSourceRooms();
|
||||
await client.joinRoom(NEW_SOURCE_ROOM_ID);
|
||||
const newSourceRoom = client.getRoom(NEW_SOURCE_ROOM_ID);
|
||||
|
||||
// Fetch the list of sources and check that we do not have the new room yet.
|
||||
const policies = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent();
|
||||
expect(policies).toBeTruthy();
|
||||
const ignoreInvites = policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name];
|
||||
expect(ignoreInvites).toBeTruthy();
|
||||
expect(ignoreInvites.sources).toBeTruthy();
|
||||
expect(ignoreInvites.sources).not.toContain(NEW_SOURCE_ROOM_ID);
|
||||
|
||||
// Add a source.
|
||||
const added = await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
|
||||
expect(added).toBe(true);
|
||||
const added2 = await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
|
||||
expect(added2).toBe(false);
|
||||
|
||||
// Fetch the list of sources and check that we have added the new room.
|
||||
const policies2 = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent();
|
||||
expect(policies2).toBeTruthy();
|
||||
const ignoreInvites2 = policies2[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name];
|
||||
expect(ignoreInvites2).toBeTruthy();
|
||||
expect(ignoreInvites2.sources).toBeTruthy();
|
||||
expect(ignoreInvites2.sources).toContain(NEW_SOURCE_ROOM_ID);
|
||||
|
||||
// Add a rule.
|
||||
const eventId = await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
|
||||
|
||||
// Check where it shows up.
|
||||
const targetRoomId = ignoreInvites2.target;
|
||||
const targetRoom = client.getRoom(targetRoomId);
|
||||
expect(targetRoom._state.get(PolicyScope.User)[eventId]).toBeTruthy();
|
||||
expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,12 +1,29 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { RoomMember } from "../../src/models/room-member";
|
||||
import { RoomMember, RoomMemberEvent } from "../../src/models/room-member";
|
||||
import { RoomState } from "../../src";
|
||||
|
||||
describe("RoomMember", function() {
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
const userB = "@bertha:bar";
|
||||
const userC = "@clarissa:bar";
|
||||
let member;
|
||||
let member = new RoomMember(roomId, userA);
|
||||
|
||||
beforeEach(function() {
|
||||
member = new RoomMember(roomId, userA);
|
||||
@@ -27,15 +44,15 @@ describe("RoomMember", function() {
|
||||
avatar_url: "mxc://flibble/wibble",
|
||||
},
|
||||
});
|
||||
const url = member.getAvatarUrl(hsUrl);
|
||||
const url = member.getAvatarUrl(hsUrl, 1, 1, '', false, false);
|
||||
// we don't care about how the mxc->http conversion is done, other
|
||||
// than it contains the mxc body.
|
||||
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
|
||||
expect(url?.indexOf("flibble/wibble")).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return nothing if there is no m.room.member and allowDefault=false",
|
||||
function() {
|
||||
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false);
|
||||
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false, false);
|
||||
expect(url).toEqual(null);
|
||||
});
|
||||
});
|
||||
@@ -82,7 +99,7 @@ describe("RoomMember", function() {
|
||||
});
|
||||
let emitCount = 0;
|
||||
|
||||
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
|
||||
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
expect(emitMember).toEqual(member);
|
||||
expect(emitEvent).toEqual(event);
|
||||
@@ -113,7 +130,7 @@ describe("RoomMember", function() {
|
||||
// set the power level to something other than zero or we
|
||||
// won't get an event
|
||||
member.powerLevel = 1;
|
||||
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
|
||||
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
expect(emitMember.userId).toEqual('@alice:bar');
|
||||
expect(emitMember.powerLevel).toEqual(0);
|
||||
@@ -141,7 +158,7 @@ describe("RoomMember", function() {
|
||||
});
|
||||
let emitCount = 0;
|
||||
|
||||
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
|
||||
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
|
||||
emitCount += 1;
|
||||
expect(emitMember.userId).toEqual('@alice:bar');
|
||||
expect(emitMember.powerLevel).toEqual(20);
|
||||
@@ -195,7 +212,7 @@ describe("RoomMember", function() {
|
||||
event: true,
|
||||
});
|
||||
let emitCount = 0;
|
||||
member.on("RoomMember.typing", function(ev, mem) {
|
||||
member.on(RoomMemberEvent.Typing, function(ev, mem) {
|
||||
expect(mem).toEqual(member);
|
||||
expect(ev).toEqual(event);
|
||||
emitCount += 1;
|
||||
@@ -210,7 +227,7 @@ describe("RoomMember", function() {
|
||||
|
||||
describe("isOutOfBand", function() {
|
||||
it("should be set by markOutOfBand", function() {
|
||||
const member = new RoomMember();
|
||||
const member = new RoomMember(roomId, userA);
|
||||
expect(member.isOutOfBand()).toEqual(false);
|
||||
member.markOutOfBand();
|
||||
expect(member.isOutOfBand()).toEqual(true);
|
||||
@@ -266,7 +283,7 @@ describe("RoomMember", function() {
|
||||
getUserIdsWithDisplayName: function(displayName) {
|
||||
return [userA, userC];
|
||||
},
|
||||
};
|
||||
} as unknown as RoomState;
|
||||
expect(member.name).toEqual(userA); // default = user_id
|
||||
member.setMembershipEvent(joinEvent);
|
||||
expect(member.name).toEqual("Alice"); // prefer displayname
|
||||
@@ -278,7 +295,7 @@ describe("RoomMember", function() {
|
||||
|
||||
it("should emit 'RoomMember.membership' if the membership changes", function() {
|
||||
let emitCount = 0;
|
||||
member.on("RoomMember.membership", function(ev, mem) {
|
||||
member.on(RoomMemberEvent.Membership, function(ev, mem) {
|
||||
emitCount += 1;
|
||||
expect(mem).toEqual(member);
|
||||
expect(ev).toEqual(inviteEvent);
|
||||
@@ -291,7 +308,7 @@ describe("RoomMember", function() {
|
||||
|
||||
it("should emit 'RoomMember.name' if the name changes", function() {
|
||||
let emitCount = 0;
|
||||
member.on("RoomMember.name", function(ev, mem) {
|
||||
member.on(RoomMemberEvent.Name, function(ev, mem) {
|
||||
emitCount += 1;
|
||||
expect(mem).toEqual(member);
|
||||
expect(ev).toEqual(joinEvent);
|
||||
@@ -341,7 +358,7 @@ describe("RoomMember", function() {
|
||||
getUserIdsWithDisplayName: function(displayName) {
|
||||
return [userA, userC];
|
||||
},
|
||||
};
|
||||
} as unknown as RoomState;
|
||||
expect(member.name).toEqual(userA); // default = user_id
|
||||
member.setMembershipEvent(joinEvent, roomState);
|
||||
expect(member.name).not.toEqual("Alíce"); // it should disambig.
|
@@ -1,14 +1,37 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MockedObject } from 'jest-mock';
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
|
||||
import { filterEmitCallsByEventType } from "../test-utils/emitter";
|
||||
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
|
||||
import { BeaconEvent, getBeaconInfoIdentifier } from "../../src/models/beacon";
|
||||
import {
|
||||
Beacon,
|
||||
BeaconEvent,
|
||||
getBeaconInfoIdentifier,
|
||||
} from "../../src/models/beacon";
|
||||
import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@types/event";
|
||||
import {
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
} from "../../src/models/event";
|
||||
import { M_BEACON } from "../../src/@types/beacon";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
|
||||
describe("RoomState", function() {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -17,7 +40,7 @@ describe("RoomState", function() {
|
||||
const userC = "@cleo:bar";
|
||||
const userLazy = "@lazy:bar";
|
||||
|
||||
let state;
|
||||
let state = new RoomState(roomId);
|
||||
|
||||
beforeEach(function() {
|
||||
state = new RoomState(roomId);
|
||||
@@ -67,8 +90,8 @@ describe("RoomState", function() {
|
||||
|
||||
it("should return a member which changes as state changes", function() {
|
||||
const member = state.getMember(userB);
|
||||
expect(member.membership).toEqual("join");
|
||||
expect(member.name).toEqual(userB);
|
||||
expect(member?.membership).toEqual("join");
|
||||
expect(member?.name).toEqual(userB);
|
||||
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({
|
||||
@@ -77,14 +100,14 @@ describe("RoomState", function() {
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(member.membership).toEqual("leave");
|
||||
expect(member.name).toEqual("BobGone");
|
||||
expect(member?.membership).toEqual("leave");
|
||||
expect(member?.name).toEqual("BobGone");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSentinelMember", function() {
|
||||
it("should return a member with the user id as name", function() {
|
||||
expect(state.getSentinelMember("@no-one:here").name).toEqual("@no-one:here");
|
||||
expect(state.getSentinelMember("@no-one:here")?.name).toEqual("@no-one:here");
|
||||
});
|
||||
|
||||
it("should return a member which doesn't change when the state is updated",
|
||||
@@ -98,11 +121,11 @@ describe("RoomState", function() {
|
||||
]);
|
||||
const postLeaveUser = state.getSentinelMember(userA);
|
||||
|
||||
expect(preLeaveUser.membership).toEqual("join");
|
||||
expect(preLeaveUser.name).toEqual(userA);
|
||||
expect(preLeaveUser?.membership).toEqual("join");
|
||||
expect(preLeaveUser?.name).toEqual(userA);
|
||||
|
||||
expect(postLeaveUser.membership).toEqual("leave");
|
||||
expect(postLeaveUser.name).toEqual("AliceIsGone");
|
||||
expect(postLeaveUser?.membership).toEqual("leave");
|
||||
expect(postLeaveUser?.name).toEqual("AliceIsGone");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -122,8 +145,8 @@ describe("RoomState", function() {
|
||||
const events = state.getStateEvents("m.room.member");
|
||||
expect(events.length).toEqual(2);
|
||||
// ordering unimportant
|
||||
expect([userA, userB].indexOf(events[0].getStateKey())).not.toEqual(-1);
|
||||
expect([userA, userB].indexOf(events[1].getStateKey())).not.toEqual(-1);
|
||||
expect([userA, userB].indexOf(events[0].getStateKey() as string)).not.toEqual(-1);
|
||||
expect([userA, userB].indexOf(events[1].getStateKey() as string)).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return a single MatrixEvent if a state_key was specified",
|
||||
@@ -146,7 +169,7 @@ describe("RoomState", function() {
|
||||
}),
|
||||
];
|
||||
let emitCount = 0;
|
||||
state.on("RoomState.members", function(ev, st, mem) {
|
||||
state.on(RoomStateEvent.Members, function(ev, st, mem) {
|
||||
expect(ev).toEqual(memberEvents[emitCount]);
|
||||
expect(st).toEqual(state);
|
||||
expect(mem).toEqual(state.getMember(ev.getSender()));
|
||||
@@ -166,7 +189,7 @@ describe("RoomState", function() {
|
||||
}),
|
||||
];
|
||||
let emitCount = 0;
|
||||
state.on("RoomState.newMember", function(ev, st, mem) {
|
||||
state.on(RoomStateEvent.NewMember, function(ev, st, mem) {
|
||||
expect(state.getMember(mem.userId)).toEqual(mem);
|
||||
expect(mem.userId).toEqual(memberEvents[emitCount].getSender());
|
||||
expect(mem.membership).toBeFalsy(); // not defined yet
|
||||
@@ -192,7 +215,7 @@ describe("RoomState", function() {
|
||||
}),
|
||||
];
|
||||
let emitCount = 0;
|
||||
state.on("RoomState.events", function(ev, st) {
|
||||
state.on(RoomStateEvent.Events, function(ev, st) {
|
||||
expect(ev).toEqual(events[emitCount]);
|
||||
expect(st).toEqual(state);
|
||||
emitCount += 1;
|
||||
@@ -272,7 +295,7 @@ describe("RoomState", function() {
|
||||
}),
|
||||
];
|
||||
let emitCount = 0;
|
||||
state.on("RoomState.Marker", function(markerEvent, markerFoundOptions) {
|
||||
state.on(RoomStateEvent.Marker, function(markerEvent, markerFoundOptions) {
|
||||
expect(markerEvent).toEqual(events[emitCount]);
|
||||
expect(markerFoundOptions).toEqual({ timelineWasEmpty: true });
|
||||
emitCount += 1;
|
||||
@@ -296,7 +319,7 @@ describe("RoomState", function() {
|
||||
|
||||
it('does not add redacted beacon info events to state', () => {
|
||||
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
|
||||
const redactionEvent = { event: { type: 'm.room.redaction' } };
|
||||
const redactionEvent = new MatrixEvent({ type: 'm.room.redaction' });
|
||||
redactedBeaconEvent.makeRedacted(redactionEvent);
|
||||
const emitSpy = jest.spyOn(state, 'emit');
|
||||
|
||||
@@ -316,27 +339,27 @@ describe("RoomState", function() {
|
||||
|
||||
state.setStateEvents([beaconEvent]);
|
||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
||||
expect(beaconInstance.isLive).toEqual(true);
|
||||
expect(beaconInstance?.isLive).toEqual(true);
|
||||
|
||||
state.setStateEvents([updatedBeaconEvent]);
|
||||
|
||||
// same Beacon
|
||||
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance);
|
||||
// updated liveness
|
||||
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent)).isLive).toEqual(false);
|
||||
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))?.isLive).toEqual(false);
|
||||
});
|
||||
|
||||
it('destroys and removes redacted beacon events', () => {
|
||||
const beaconId = '$beacon1';
|
||||
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
||||
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
||||
const redactionEvent = { event: { type: 'm.room.redaction', redacts: beaconEvent.getId() } };
|
||||
const redactionEvent = new MatrixEvent({ type: 'm.room.redaction', redacts: beaconEvent.getId() });
|
||||
redactedBeaconEvent.makeRedacted(redactionEvent);
|
||||
|
||||
state.setStateEvents([beaconEvent]);
|
||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
||||
const destroySpy = jest.spyOn(beaconInstance, 'destroy');
|
||||
expect(beaconInstance.isLive).toEqual(true);
|
||||
const destroySpy = jest.spyOn(beaconInstance as Beacon, 'destroy');
|
||||
expect(beaconInstance?.isLive).toEqual(true);
|
||||
|
||||
state.setStateEvents([redactedBeaconEvent]);
|
||||
|
||||
@@ -357,7 +380,7 @@ describe("RoomState", function() {
|
||||
|
||||
// live beacon is now not live
|
||||
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
|
||||
userA, roomId, { isLive: false }, liveBeaconEvent.getId(), '$beacon1',
|
||||
userA, roomId, { isLive: false }, liveBeaconEvent.getId(),
|
||||
);
|
||||
|
||||
state.setStateEvents([updatedLiveBeaconEvent]);
|
||||
@@ -377,8 +400,8 @@ describe("RoomState", function() {
|
||||
state.markOutOfBandMembersStarted();
|
||||
state.setOutOfBandMembers([oobMemberEvent]);
|
||||
const member = state.getMember(userLazy);
|
||||
expect(member.userId).toEqual(userLazy);
|
||||
expect(member.isOutOfBand()).toEqual(true);
|
||||
expect(member?.userId).toEqual(userLazy);
|
||||
expect(member?.isOutOfBand()).toEqual(true);
|
||||
});
|
||||
|
||||
it("should have no effect when not in correct status", function() {
|
||||
@@ -394,7 +417,7 @@ describe("RoomState", function() {
|
||||
user: userLazy, mship: "join", room: roomId, event: true,
|
||||
});
|
||||
let eventReceived = false;
|
||||
state.once('RoomState.newMember', (_, __, member) => {
|
||||
state.once(RoomStateEvent.NewMember, (_event, _state, member) => {
|
||||
expect(member.userId).toEqual(userLazy);
|
||||
eventReceived = true;
|
||||
});
|
||||
@@ -410,8 +433,8 @@ describe("RoomState", function() {
|
||||
state.markOutOfBandMembersStarted();
|
||||
state.setOutOfBandMembers([oobMemberEvent]);
|
||||
const memberA = state.getMember(userA);
|
||||
expect(memberA.events.member.getId()).not.toEqual(oobMemberEvent.getId());
|
||||
expect(memberA.isOutOfBand()).toEqual(false);
|
||||
expect(memberA?.events?.member?.getId()).not.toEqual(oobMemberEvent.getId());
|
||||
expect(memberA?.isOutOfBand()).toEqual(false);
|
||||
});
|
||||
|
||||
it("should emit members when updating a member", function() {
|
||||
@@ -420,7 +443,7 @@ describe("RoomState", function() {
|
||||
user: doesntExistYetUserId, mship: "join", room: roomId, event: true,
|
||||
});
|
||||
let eventReceived = false;
|
||||
state.once('RoomState.members', (_, __, member) => {
|
||||
state.once(RoomStateEvent.Members, (_event, _state, member) => {
|
||||
expect(member.userId).toEqual(doesntExistYetUserId);
|
||||
eventReceived = true;
|
||||
});
|
||||
@@ -443,8 +466,8 @@ describe("RoomState", function() {
|
||||
[userA, userB, userLazy].forEach((userId) => {
|
||||
const member = state.getMember(userId);
|
||||
const memberCopy = copy.getMember(userId);
|
||||
expect(member.name).toEqual(memberCopy.name);
|
||||
expect(member.isOutOfBand()).toEqual(memberCopy.isOutOfBand());
|
||||
expect(member?.name).toEqual(memberCopy?.name);
|
||||
expect(member?.isOutOfBand()).toEqual(memberCopy?.isOutOfBand());
|
||||
});
|
||||
// check member keys
|
||||
expect(Object.keys(state.members)).toEqual(Object.keys(copy.members));
|
||||
@@ -503,19 +526,20 @@ describe("RoomState", function() {
|
||||
it("should say members with power >=50 may send state with power level event " +
|
||||
"but no state default",
|
||||
function() {
|
||||
const powerLevelEvent = {
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
const powerLevelEvent = new MatrixEvent({
|
||||
type: "m.room.power_levels", room_id: roomId, sender: userA,
|
||||
state_key: "",
|
||||
content: {
|
||||
users_default: 10,
|
||||
// state_default: 50, "intentionally left blank"
|
||||
events_default: 25,
|
||||
users: {
|
||||
[userA]: 50,
|
||||
},
|
||||
},
|
||||
};
|
||||
powerLevelEvent.content.users[userA] = 50;
|
||||
});
|
||||
|
||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
||||
state.setStateEvents([powerLevelEvent]);
|
||||
|
||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
|
||||
@@ -523,20 +547,21 @@ describe("RoomState", function() {
|
||||
|
||||
it("should obey state_default",
|
||||
function() {
|
||||
const powerLevelEvent = {
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
const powerLevelEvent = new MatrixEvent({
|
||||
type: "m.room.power_levels", room_id: roomId, sender: userA,
|
||||
state_key: "",
|
||||
content: {
|
||||
users_default: 10,
|
||||
state_default: 30,
|
||||
events_default: 25,
|
||||
users: {
|
||||
[userA]: 30,
|
||||
[userB]: 29,
|
||||
},
|
||||
},
|
||||
};
|
||||
powerLevelEvent.content.users[userA] = 30;
|
||||
powerLevelEvent.content.users[userB] = 29;
|
||||
});
|
||||
|
||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
||||
state.setStateEvents([powerLevelEvent]);
|
||||
|
||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
|
||||
@@ -544,9 +569,9 @@ describe("RoomState", function() {
|
||||
|
||||
it("should honour explicit event power levels in the power_levels event",
|
||||
function() {
|
||||
const powerLevelEvent = {
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
content: {
|
||||
const powerLevelEvent = new MatrixEvent({
|
||||
type: "m.room.power_levels", room_id: roomId, sender: userA,
|
||||
state_key: "", content: {
|
||||
events: {
|
||||
"m.room.other_thing": 76,
|
||||
},
|
||||
@@ -554,13 +579,13 @@ describe("RoomState", function() {
|
||||
state_default: 50,
|
||||
events_default: 25,
|
||||
users: {
|
||||
[userA]: 80,
|
||||
[userB]: 50,
|
||||
},
|
||||
},
|
||||
};
|
||||
powerLevelEvent.content.users[userA] = 80;
|
||||
powerLevelEvent.content.users[userB] = 50;
|
||||
});
|
||||
|
||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
||||
state.setStateEvents([powerLevelEvent]);
|
||||
|
||||
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
|
||||
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(true);
|
||||
@@ -689,20 +714,21 @@ describe("RoomState", function() {
|
||||
|
||||
it("should obey events_default",
|
||||
function() {
|
||||
const powerLevelEvent = {
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
const powerLevelEvent = new MatrixEvent({
|
||||
type: "m.room.power_levels", room_id: roomId, sender: userA,
|
||||
state_key: "",
|
||||
content: {
|
||||
users_default: 10,
|
||||
state_default: 30,
|
||||
events_default: 25,
|
||||
users: {
|
||||
[userA]: 26,
|
||||
[userB]: 24,
|
||||
},
|
||||
},
|
||||
};
|
||||
powerLevelEvent.content.users[userA] = 26;
|
||||
powerLevelEvent.content.users[userB] = 24;
|
||||
});
|
||||
|
||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
||||
state.setStateEvents([powerLevelEvent]);
|
||||
|
||||
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
||||
expect(state.maySendEvent('m.room.message', userB)).toEqual(false);
|
||||
@@ -713,8 +739,9 @@ describe("RoomState", function() {
|
||||
|
||||
it("should honour explicit event power levels in the power_levels event",
|
||||
function() {
|
||||
const powerLevelEvent = {
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
const powerLevelEvent = new MatrixEvent({
|
||||
type: "m.room.power_levels", room_id: roomId, sender: userA,
|
||||
state_key: "",
|
||||
content: {
|
||||
events: {
|
||||
"m.room.other_thing": 33,
|
||||
@@ -723,13 +750,13 @@ describe("RoomState", function() {
|
||||
state_default: 50,
|
||||
events_default: 25,
|
||||
users: {
|
||||
[userA]: 40,
|
||||
[userB]: 30,
|
||||
},
|
||||
},
|
||||
};
|
||||
powerLevelEvent.content.users[userA] = 40;
|
||||
powerLevelEvent.content.users[userB] = 30;
|
||||
});
|
||||
|
||||
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
|
||||
state.setStateEvents([powerLevelEvent]);
|
||||
|
||||
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
|
||||
expect(state.maySendEvent('m.room.message', userB)).toEqual(true);
|
||||
@@ -743,10 +770,10 @@ describe("RoomState", function() {
|
||||
});
|
||||
|
||||
describe('processBeaconEvents', () => {
|
||||
const beacon1 = makeBeaconInfoEvent(userA, roomId, {}, '$beacon1', '$beacon1');
|
||||
const beacon2 = makeBeaconInfoEvent(userB, roomId, {}, '$beacon2', '$beacon2');
|
||||
const beacon1 = makeBeaconInfoEvent(userA, roomId, {}, '$beacon1');
|
||||
const beacon2 = makeBeaconInfoEvent(userB, roomId, {}, '$beacon2');
|
||||
|
||||
const mockClient = { decryptEventIfNeeded: jest.fn() };
|
||||
const mockClient = { decryptEventIfNeeded: jest.fn() } as unknown as MockedObject<MatrixClient>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.decryptEventIfNeeded.mockClear();
|
||||
@@ -816,11 +843,11 @@ describe("RoomState", function() {
|
||||
beaconInfoId: 'some-other-beacon',
|
||||
});
|
||||
|
||||
state.setStateEvents([beacon1, beacon2], mockClient);
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
|
||||
expect(state.beacons.size).toEqual(2);
|
||||
|
||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
||||
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
|
||||
const addLocationsSpy = jest.spyOn(beaconInstance, 'addLocations');
|
||||
|
||||
state.processBeaconEvents([location1, location2, location3], mockClient);
|
||||
@@ -885,7 +912,7 @@ describe("RoomState", function() {
|
||||
});
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
|
||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
|
||||
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
|
||||
state.processBeaconEvents([location, otherRelatedEvent], mockClient);
|
||||
expect(addLocationsSpy).not.toHaveBeenCalled();
|
||||
@@ -945,13 +972,13 @@ describe("RoomState", function() {
|
||||
});
|
||||
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
|
||||
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
|
||||
// this event is a message after decryption
|
||||
decryptingRelatedEvent.type = EventType.RoomMessage;
|
||||
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted);
|
||||
decryptingRelatedEvent.event.type = EventType.RoomMessage;
|
||||
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted, decryptingRelatedEvent);
|
||||
|
||||
expect(addLocationsSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -967,14 +994,14 @@ describe("RoomState", function() {
|
||||
});
|
||||
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
|
||||
state.setStateEvents([beacon1, beacon2]);
|
||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
|
||||
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
|
||||
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
|
||||
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
||||
|
||||
// update type after '''decryption'''
|
||||
decryptingRelatedEvent.event.type = M_BEACON.name;
|
||||
decryptingRelatedEvent.event.content = locationEvent.content;
|
||||
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted);
|
||||
decryptingRelatedEvent.event.content = locationEvent.event.content;
|
||||
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted, decryptingRelatedEvent);
|
||||
|
||||
expect(addLocationsSpy).toHaveBeenCalledWith([decryptingRelatedEvent]);
|
||||
});
|
@@ -288,11 +288,11 @@ describe("Room", function() {
|
||||
room.addLiveEvents(events);
|
||||
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
|
||||
[events[0]],
|
||||
{ timelineWasEmpty: undefined },
|
||||
{ timelineWasEmpty: false },
|
||||
);
|
||||
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
|
||||
[events[1]],
|
||||
{ timelineWasEmpty: undefined },
|
||||
{ timelineWasEmpty: false },
|
||||
);
|
||||
expect(events[0].forwardLooking).toBe(true);
|
||||
expect(events[1].forwardLooking).toBe(true);
|
||||
@@ -426,6 +426,17 @@ describe("Room", function() {
|
||||
// but without the event ID matching we will still have the local event in pending events
|
||||
expect(room.getEventForTxnId(txnId)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should correctly handle remote echoes from other devices", () => {
|
||||
const remoteEvent = utils.mkMessage({
|
||||
room: roomId, user: userA, event: true,
|
||||
});
|
||||
remoteEvent.event.unsigned = { transaction_id: "TXN_ID" };
|
||||
|
||||
// add the remoteEvent
|
||||
room.addLiveEvents([remoteEvent]);
|
||||
expect(room.timeline.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addEphemeralEvents', () => {
|
||||
|
@@ -152,6 +152,9 @@ describe("utils", function() {
|
||||
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 2 }));
|
||||
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { b: 2, a: 1 }));
|
||||
assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 3 }));
|
||||
assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1 }));
|
||||
assert.isFalse(utils.deepCompare({ a: 1 }, { a: 1, b: 2 }));
|
||||
assert.isFalse(utils.deepCompare({ a: 1 }, { b: 1 }));
|
||||
|
||||
assert.isTrue(utils.deepCompare({
|
||||
1: { name: "mhc", age: 28 },
|
||||
|
@@ -201,6 +201,7 @@ import { Thread, THREAD_RELATION_TYPE } from "./models/thread";
|
||||
import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
|
||||
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
|
||||
import { ToDeviceBatch } from "./models/ToDeviceMessage";
|
||||
import { IgnoredInvites } from "./models/invites-ignorer";
|
||||
|
||||
export type Store = IStore;
|
||||
|
||||
@@ -406,8 +407,7 @@ export interface IStartClientOpts {
|
||||
pollTimeout?: number;
|
||||
|
||||
/**
|
||||
* The filter to apply to /sync calls. This will override the opts.initialSyncLimit, which would
|
||||
* normally result in a timeline limit filter.
|
||||
* The filter to apply to /sync calls.
|
||||
*/
|
||||
filter?: Filter;
|
||||
|
||||
@@ -974,6 +974,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
private useE2eForGroupCall = true;
|
||||
private toDeviceMessageQueue: ToDeviceMessageQueue;
|
||||
|
||||
// A manager for determining which invites should be ignored.
|
||||
public readonly ignoredInvites: IgnoredInvites;
|
||||
|
||||
constructor(opts: IMatrixClientCreateOpts) {
|
||||
super();
|
||||
|
||||
@@ -1159,6 +1162,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
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)>}
|
||||
*/
|
||||
export const ENCRYPTION_CLASSES: Record<string, new (params: IParams) => EncryptionAlgorithm> = {};
|
||||
export const ENCRYPTION_CLASSES = new Map<string, new (params: IParams) => EncryptionAlgorithm>();
|
||||
|
||||
type DecryptionClassParams = Omit<IParams, "deviceId" | "config">;
|
||||
|
||||
@@ -44,7 +44,7 @@ type DecryptionClassParams = Omit<IParams, "deviceId" | "config">;
|
||||
*
|
||||
* @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
|
||||
*/
|
||||
export const DECRYPTION_CLASSES: Record<string, new (params: DecryptionClassParams) => DecryptionAlgorithm> = {};
|
||||
export const DECRYPTION_CLASSES = new Map<string, new (params: DecryptionClassParams) => DecryptionAlgorithm>();
|
||||
|
||||
export interface IParams {
|
||||
userId: string;
|
||||
@@ -297,6 +297,6 @@ export function registerAlgorithm(
|
||||
encryptor: new (params: IParams) => EncryptionAlgorithm,
|
||||
decryptor: new (params: Omit<IParams, "deviceId">) => DecryptionAlgorithm,
|
||||
): void {
|
||||
ENCRYPTION_CLASSES[algorithm] = encryptor;
|
||||
DECRYPTION_CLASSES[algorithm] = decryptor;
|
||||
ENCRYPTION_CLASSES.set(algorithm, encryptor);
|
||||
DECRYPTION_CLASSES.set(algorithm, decryptor);
|
||||
}
|
||||
|
@@ -1185,7 +1185,7 @@ class MegolmEncryption extends EncryptionAlgorithm {
|
||||
class MegolmDecryption extends DecryptionAlgorithm {
|
||||
// events which we couldn't decrypt due to unknown sessions / indexes: map from
|
||||
// senderKey|sessionId to Set of MatrixEvents
|
||||
private pendingEvents: Record<string, Map<string, Set<MatrixEvent>>> = {};
|
||||
private pendingEvents = new Map<string, Map<string, Set<MatrixEvent>>>();
|
||||
|
||||
// this gets stubbed out by the unit tests.
|
||||
private olmlib = olmlib;
|
||||
@@ -1337,10 +1337,10 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
const content = event.getWireContent();
|
||||
const senderKey = content.sender_key;
|
||||
const sessionId = content.session_id;
|
||||
if (!this.pendingEvents[senderKey]) {
|
||||
this.pendingEvents[senderKey] = new Map();
|
||||
if (!this.pendingEvents.has(senderKey)) {
|
||||
this.pendingEvents.set(senderKey, new Map<string, Set<MatrixEvent>>());
|
||||
}
|
||||
const senderPendingEvents = this.pendingEvents[senderKey];
|
||||
const senderPendingEvents = this.pendingEvents.get(senderKey);
|
||||
if (!senderPendingEvents.has(sessionId)) {
|
||||
senderPendingEvents.set(sessionId, new Set());
|
||||
}
|
||||
@@ -1358,7 +1358,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
const content = event.getWireContent();
|
||||
const senderKey = content.sender_key;
|
||||
const sessionId = content.session_id;
|
||||
const senderPendingEvents = this.pendingEvents[senderKey];
|
||||
const senderPendingEvents = this.pendingEvents.get(senderKey);
|
||||
const pendingEvents = senderPendingEvents?.get(sessionId);
|
||||
if (!pendingEvents) {
|
||||
return;
|
||||
@@ -1369,7 +1369,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
senderPendingEvents.delete(sessionId);
|
||||
}
|
||||
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
|
||||
*/
|
||||
private async retryDecryption(senderKey: string, sessionId: string): Promise<boolean> {
|
||||
const senderPendingEvents = this.pendingEvents[senderKey];
|
||||
const senderPendingEvents = this.pendingEvents.get(senderKey);
|
||||
if (!senderPendingEvents) {
|
||||
return true;
|
||||
}
|
||||
@@ -1731,16 +1731,16 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
}));
|
||||
|
||||
// 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> {
|
||||
const senderPendingEvents = this.pendingEvents[senderKey];
|
||||
const senderPendingEvents = this.pendingEvents.get(senderKey);
|
||||
if (!senderPendingEvents) {
|
||||
return true;
|
||||
}
|
||||
|
||||
delete this.pendingEvents[senderKey];
|
||||
this.pendingEvents.delete(senderKey);
|
||||
|
||||
await Promise.all([...senderPendingEvents].map(async ([_sessionId, pending]) => {
|
||||
await Promise.all([...pending].map(async (ev) => {
|
||||
@@ -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> {
|
||||
|
@@ -301,9 +301,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
private oneTimeKeyCheckInProgress = false;
|
||||
|
||||
// EncryptionAlgorithm instance for each room
|
||||
private roomEncryptors: Record<string, EncryptionAlgorithm> = {};
|
||||
private roomEncryptors = new Map<string, EncryptionAlgorithm>();
|
||||
// map from algorithm to DecryptionAlgorithm instance, for each room
|
||||
private roomDecryptors: Record<string, Record<string, DecryptionAlgorithm>> = {};
|
||||
private roomDecryptors = new Map<string, Map<string, DecryptionAlgorithm>>();
|
||||
|
||||
private deviceKeys: Record<string, string> = {}; // type: key
|
||||
|
||||
@@ -445,7 +445,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
this.deviceList.on(CryptoEvent.UserCrossSigningUpdated, this.onDeviceListUserCrossSigningUpdated);
|
||||
this.reEmitter.reEmit(this.deviceList, [CryptoEvent.DevicesUpdated, CryptoEvent.WillUpdateDevices]);
|
||||
|
||||
this.supportedAlgorithms = Object.keys(algorithms.DECRYPTION_CLASSES);
|
||||
this.supportedAlgorithms = Array.from(algorithms.DECRYPTION_CLASSES.keys());
|
||||
|
||||
this.outgoingRoomKeyRequestManager = new OutgoingRoomKeyRequestManager(
|
||||
baseApis, this.deviceId, this.cryptoStore,
|
||||
@@ -2550,7 +2550,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
* This should not normally be necessary.
|
||||
*/
|
||||
public forceDiscardSession(roomId: string): void {
|
||||
const alg = this.roomEncryptors[roomId];
|
||||
const alg = this.roomEncryptors.get(roomId);
|
||||
if (alg === undefined) throw new Error("Room not encrypted");
|
||||
if (alg.forceDiscardSession === undefined) {
|
||||
throw new Error("Room encryption algorithm doesn't support session discarding");
|
||||
@@ -2603,7 +2603,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
// the encryption event would appear in both.
|
||||
// If it's called more than twice though,
|
||||
// it signals a bug on client or server.
|
||||
const existingAlg = this.roomEncryptors[roomId];
|
||||
const existingAlg = this.roomEncryptors.get(roomId);
|
||||
if (existingAlg) {
|
||||
return;
|
||||
}
|
||||
@@ -2617,7 +2617,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
storeConfigPromise = this.roomList.setRoomEncryption(roomId, config);
|
||||
}
|
||||
|
||||
const AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm];
|
||||
const AlgClass = algorithms.ENCRYPTION_CLASSES.get(config.algorithm);
|
||||
if (!AlgClass) {
|
||||
throw new Error("Unable to encrypt with " + config.algorithm);
|
||||
}
|
||||
@@ -2631,7 +2631,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
roomId,
|
||||
config,
|
||||
});
|
||||
this.roomEncryptors[roomId] = alg;
|
||||
this.roomEncryptors.set(roomId, alg);
|
||||
|
||||
if (storeConfigPromise) {
|
||||
await storeConfigPromise;
|
||||
@@ -2663,7 +2663,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
public trackRoomDevices(roomId: string): Promise<void> {
|
||||
const trackMembers = async () => {
|
||||
// not an encrypted room
|
||||
if (!this.roomEncryptors[roomId]) {
|
||||
if (!this.roomEncryptors.has(roomId)) {
|
||||
return;
|
||||
}
|
||||
const room = this.clientStore.getRoom(roomId);
|
||||
@@ -2808,7 +2808,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
* @param {module:models/room} room the room the event is in
|
||||
*/
|
||||
public prepareToEncrypt(room: Room): void {
|
||||
const alg = this.roomEncryptors[room.roomId];
|
||||
const alg = this.roomEncryptors.get(room.roomId);
|
||||
if (alg) {
|
||||
alg.prepareToEncrypt(room);
|
||||
}
|
||||
@@ -2831,7 +2831,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
|
||||
const roomId = event.getRoomId();
|
||||
|
||||
const alg = this.roomEncryptors[roomId];
|
||||
const alg = this.roomEncryptors.get(roomId);
|
||||
if (!alg) {
|
||||
// MatrixClient has already checked that this room should be encrypted,
|
||||
// so this is an unexpected situation.
|
||||
@@ -3120,7 +3120,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
private getTrackedE2eRooms(): Room[] {
|
||||
return this.clientStore.getRooms().filter((room) => {
|
||||
// check for rooms with encryption enabled
|
||||
const alg = this.roomEncryptors[room.roomId];
|
||||
const alg = this.roomEncryptors.get(room.roomId);
|
||||
if (!alg) {
|
||||
return false;
|
||||
}
|
||||
@@ -3556,7 +3556,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
|
||||
const roomId = member.roomId;
|
||||
|
||||
const alg = this.roomEncryptors[roomId];
|
||||
const alg = this.roomEncryptors.get(roomId);
|
||||
if (!alg) {
|
||||
// not encrypting in this room
|
||||
return;
|
||||
@@ -3657,11 +3657,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
` for ${roomId} / ${body.session_id} (id ${req.requestId})`);
|
||||
|
||||
if (userId !== this.userId) {
|
||||
if (!this.roomEncryptors[roomId]) {
|
||||
if (!this.roomEncryptors.get(roomId)) {
|
||||
logger.debug(`room key request for unencrypted room ${roomId}`);
|
||||
return;
|
||||
}
|
||||
const encryptor = this.roomEncryptors[roomId];
|
||||
const encryptor = this.roomEncryptors.get(roomId);
|
||||
const device = this.deviceList.getStoredDevice(userId, deviceId);
|
||||
if (!device) {
|
||||
logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`);
|
||||
@@ -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
|
||||
// the keys for the requested events, and can drop the requests.
|
||||
if (!this.roomDecryptors[roomId]) {
|
||||
if (!this.roomDecryptors.has(roomId)) {
|
||||
logger.log(`room key request for unencrypted room ${roomId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const decryptor = this.roomDecryptors[roomId][alg];
|
||||
const decryptor = this.roomDecryptors.get(roomId).get(alg);
|
||||
if (!decryptor) {
|
||||
logger.log(`room key request for unknown alg ${alg} in room ${roomId}`);
|
||||
return;
|
||||
@@ -3768,23 +3768,24 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
* unknown
|
||||
*/
|
||||
public getRoomDecryptor(roomId: string, algorithm: string): DecryptionAlgorithm {
|
||||
let decryptors: Record<string, DecryptionAlgorithm>;
|
||||
let decryptors: Map<string, DecryptionAlgorithm>;
|
||||
let alg: DecryptionAlgorithm;
|
||||
|
||||
roomId = roomId || null;
|
||||
if (roomId) {
|
||||
decryptors = this.roomDecryptors[roomId];
|
||||
decryptors = this.roomDecryptors.get(roomId);
|
||||
if (!decryptors) {
|
||||
this.roomDecryptors[roomId] = decryptors = {};
|
||||
decryptors = new Map<string, DecryptionAlgorithm>();
|
||||
this.roomDecryptors.set(roomId, decryptors);
|
||||
}
|
||||
|
||||
alg = decryptors[algorithm];
|
||||
alg = decryptors.get(algorithm);
|
||||
if (alg) {
|
||||
return alg;
|
||||
}
|
||||
}
|
||||
|
||||
const AlgClass = algorithms.DECRYPTION_CLASSES[algorithm];
|
||||
const AlgClass = algorithms.DECRYPTION_CLASSES.get(algorithm);
|
||||
if (!AlgClass) {
|
||||
throw new algorithms.DecryptionError(
|
||||
'UNKNOWN_ENCRYPTION_ALGORITHM',
|
||||
@@ -3800,7 +3801,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
});
|
||||
|
||||
if (decryptors) {
|
||||
decryptors[algorithm] = alg;
|
||||
decryptors.set(algorithm, alg);
|
||||
}
|
||||
return alg;
|
||||
}
|
||||
@@ -3814,9 +3815,9 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
*/
|
||||
private getRoomDecryptors(algorithm: string): DecryptionAlgorithm[] {
|
||||
const decryptors = [];
|
||||
for (const d of Object.values(this.roomDecryptors)) {
|
||||
if (algorithm in d) {
|
||||
decryptors.push(d[algorithm]);
|
||||
for (const d of this.roomDecryptors.values()) {
|
||||
if (d.has(algorithm)) {
|
||||
decryptors.push(d.get(algorithm));
|
||||
}
|
||||
}
|
||||
return decryptors;
|
||||
|
@@ -26,7 +26,7 @@ import {
|
||||
Mode,
|
||||
OutgoingRoomKeyRequest,
|
||||
} from "./base";
|
||||
import { IRoomKeyRequestBody } from "../index";
|
||||
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
|
||||
import { ICrossSigningKey } from "../../client";
|
||||
import { IOlmDevice } from "../algorithms/megolm";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
@@ -261,7 +261,9 @@ export class Backend implements CryptoStore {
|
||||
const cursor = this.result;
|
||||
if (cursor) {
|
||||
const keyReq = cursor.value;
|
||||
if (keyReq.recipients.includes({ userId, deviceId })) {
|
||||
if (keyReq.recipients.some((recipient: IRoomKeyRequestRecipient) =>
|
||||
recipient.userId === userId && recipient.deviceId === deviceId,
|
||||
)) {
|
||||
results.push(keyReq);
|
||||
}
|
||||
cursor.continue();
|
||||
|
@@ -191,11 +191,13 @@ export class MemoryCryptoStore implements CryptoStore {
|
||||
deviceId: string,
|
||||
wantedStates: number[],
|
||||
): Promise<OutgoingRoomKeyRequest[]> {
|
||||
const results = [];
|
||||
const results: OutgoingRoomKeyRequest[] = [];
|
||||
|
||||
for (const req of this.outgoingRoomKeyRequests) {
|
||||
for (const state of wantedStates) {
|
||||
if (req.state === state && req.recipients.includes({ userId, deviceId })) {
|
||||
if (req.state === state && req.recipients.some(
|
||||
(recipient) => recipient.userId === userId && recipient.deviceId === deviceId,
|
||||
)) {
|
||||
results.push(req);
|
||||
}
|
||||
}
|
||||
|
@@ -86,7 +86,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
private readonly displayPendingEvents: boolean;
|
||||
private liveTimeline: EventTimeline;
|
||||
private timelines: EventTimeline[];
|
||||
private _eventIdToTimeline: Record<string, EventTimeline>;
|
||||
private _eventIdToTimeline = new Map<string, EventTimeline>();
|
||||
private filter?: Filter;
|
||||
|
||||
/**
|
||||
@@ -138,7 +138,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
|
||||
// just a list - *not* ordered.
|
||||
this.timelines = [this.liveTimeline];
|
||||
this._eventIdToTimeline = {};
|
||||
this._eventIdToTimeline = new Map<string, EventTimeline>();
|
||||
|
||||
this.filter = opts.filter;
|
||||
|
||||
@@ -210,7 +210,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
* @return {module:models/event-timeline~EventTimeline} timeline
|
||||
*/
|
||||
public eventIdToTimeline(eventId: string): EventTimeline {
|
||||
return this._eventIdToTimeline[eventId];
|
||||
return this._eventIdToTimeline.get(eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,10 +220,10 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
* @param {String} newEventId event ID of the replacement event
|
||||
*/
|
||||
public replaceEventId(oldEventId: string, newEventId: string): void {
|
||||
const existingTimeline = this._eventIdToTimeline[oldEventId];
|
||||
const existingTimeline = this._eventIdToTimeline.get(oldEventId);
|
||||
if (existingTimeline) {
|
||||
delete this._eventIdToTimeline[oldEventId];
|
||||
this._eventIdToTimeline[newEventId] = existingTimeline;
|
||||
this._eventIdToTimeline.delete(oldEventId);
|
||||
this._eventIdToTimeline.set(newEventId, existingTimeline);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
|
||||
if (resetAllTimelines) {
|
||||
this.timelines = [newTimeline];
|
||||
this._eventIdToTimeline = {};
|
||||
this._eventIdToTimeline = new Map<string, EventTimeline>();
|
||||
} else {
|
||||
this.timelines.push(newTimeline);
|
||||
}
|
||||
@@ -288,7 +288,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
* the given event, or null if unknown
|
||||
*/
|
||||
public getTimelineForEvent(eventId: string): EventTimeline | null {
|
||||
const res = this._eventIdToTimeline[eventId];
|
||||
const res = this._eventIdToTimeline.get(eventId);
|
||||
return (res === undefined) ? null : res;
|
||||
}
|
||||
|
||||
@@ -450,7 +450,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
const event = events[i];
|
||||
const eventId = event.getId();
|
||||
|
||||
const existingTimeline = this._eventIdToTimeline[eventId];
|
||||
const existingTimeline = this._eventIdToTimeline.get(eventId);
|
||||
|
||||
if (!existingTimeline) {
|
||||
// we don't know about this event yet. Just add it to the timeline.
|
||||
@@ -601,7 +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 (duplicateStrategy === DuplicateStrategy.Replace) {
|
||||
debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " + event.getId());
|
||||
@@ -697,7 +697,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
roomState,
|
||||
timelineWasEmpty,
|
||||
});
|
||||
this._eventIdToTimeline[eventId] = timeline;
|
||||
this._eventIdToTimeline.set(eventId, timeline);
|
||||
|
||||
this.relations.aggregateParentEvent(event);
|
||||
this.relations.aggregateChildEvent(event, this);
|
||||
@@ -725,23 +725,15 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
newEventId: string,
|
||||
): void {
|
||||
// XXX: why don't we infer newEventId from localEvent?
|
||||
const existingTimeline = this._eventIdToTimeline[oldEventId];
|
||||
const existingTimeline = this._eventIdToTimeline.get(oldEventId);
|
||||
if (existingTimeline) {
|
||||
delete this._eventIdToTimeline[oldEventId];
|
||||
this._eventIdToTimeline[newEventId] = existingTimeline;
|
||||
} else {
|
||||
if (this.filter) {
|
||||
if (this.filter.filterRoomTimeline([localEvent]).length) {
|
||||
this._eventIdToTimeline.delete(oldEventId);
|
||||
this._eventIdToTimeline.set(newEventId, existingTimeline);
|
||||
} else if (!this.filter || this.filter.filterRoomTimeline([localEvent]).length) {
|
||||
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.
|
||||
*/
|
||||
public removeEvent(eventId: string): MatrixEvent | null {
|
||||
const timeline = this._eventIdToTimeline[eventId];
|
||||
const timeline = this._eventIdToTimeline.get(eventId);
|
||||
if (!timeline) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const removed = timeline.removeEvent(eventId);
|
||||
if (removed) {
|
||||
delete this._eventIdToTimeline[eventId];
|
||||
this._eventIdToTimeline.delete(eventId);
|
||||
const data = {
|
||||
timeline: timeline,
|
||||
};
|
||||
@@ -787,8 +779,8 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
|
||||
return 0;
|
||||
}
|
||||
|
||||
const timeline1 = this._eventIdToTimeline[eventId1];
|
||||
const timeline2 = this._eventIdToTimeline[eventId2];
|
||||
const timeline1 = this._eventIdToTimeline.get(eventId1);
|
||||
const timeline2 = this._eventIdToTimeline.get(eventId2);
|
||||
|
||||
if (timeline1 === undefined) {
|
||||
return null;
|
||||
|
@@ -26,7 +26,7 @@ import { logger } from '../logger';
|
||||
import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
|
||||
import { EVENT_VISIBILITY_CHANGE_TYPE, EventType, MsgType, RelationType } from "../@types/event";
|
||||
import { Crypto, IEventDecryptionResult } from "../crypto";
|
||||
import { deepSortedObjectEntries } from "../utils";
|
||||
import { deepSortedObjectEntries, internaliseString } from "../utils";
|
||||
import { RoomMember } from "./room-member";
|
||||
import { Thread, ThreadEvent, EventHandlerMap as ThreadEventHandlerMap, THREAD_RELATION_TYPE } from "./thread";
|
||||
import { IActionsObject } from '../pushprocessor';
|
||||
@@ -37,14 +37,6 @@ import { 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 */
|
||||
export interface IContent {
|
||||
[key: string]: any;
|
||||
@@ -326,17 +318,17 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
||||
// of space if we don't intern it.
|
||||
["state_key", "type", "sender", "room_id", "membership"].forEach((prop) => {
|
||||
if (typeof event[prop] !== "string") return;
|
||||
event[prop] = intern(event[prop]);
|
||||
event[prop] = internaliseString(event[prop]);
|
||||
});
|
||||
|
||||
["membership", "avatar_url", "displayname"].forEach((prop) => {
|
||||
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) => {
|
||||
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;
|
||||
@@ -796,6 +788,8 @@ export class MatrixEvent extends TypedEventEmitter<EmittedEvents, MatrixEventHan
|
||||
// not a decryption error: log the whole exception as an error
|
||||
// (and don't bother with a retry)
|
||||
const re = options.isRetry ? 're' : '';
|
||||
// For find results: this can produce "Error decrypting event (id=$ev)" and
|
||||
// "Error redecrypting event (id=$ev)".
|
||||
logger.error(
|
||||
`Error ${re}decrypting event ` +
|
||||
`(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 {
|
||||
// A tree of objects to access a set of related children for an event, as in:
|
||||
// this.relations[parentEventId][relationType][relationEventType]
|
||||
private relations: {
|
||||
[parentEventId: string]: {
|
||||
[relationType: RelationType | string]: {
|
||||
[eventType: EventType | string]: Relations;
|
||||
};
|
||||
};
|
||||
} = {};
|
||||
// this.relations.get(parentEventId).get(relationType).get(relationEventType)
|
||||
private relations = new Map<string, Map<RelationType | string, Map<EventType | string, Relations>>>();
|
||||
|
||||
constructor(private readonly client: MatrixClient, private readonly room?: Room) {
|
||||
}
|
||||
@@ -57,14 +51,15 @@ export class RelationsContainer {
|
||||
relationType: RelationType | string,
|
||||
eventType: EventType | string,
|
||||
): Relations | undefined {
|
||||
return this.relations[eventId]?.[relationType]?.[eventType];
|
||||
return this.relations.get(eventId)?.get(relationType)?.get(eventType);
|
||||
}
|
||||
|
||||
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[] = [];
|
||||
for (const relationsRecord of Object.values(relationsForEvent)) {
|
||||
for (const relations of Object.values(relationsRecord)) {
|
||||
for (const relationsRecord of relationsForEvent.values()) {
|
||||
for (const relations of relationsRecord.values()) {
|
||||
events.push(...relations.getRelations());
|
||||
}
|
||||
}
|
||||
@@ -79,11 +74,11 @@ export class RelationsContainer {
|
||||
* @param {MatrixEvent} event The event to check as relation target.
|
||||
*/
|
||||
public aggregateParentEvent(event: MatrixEvent): void {
|
||||
const relationsForEvent = this.relations[event.getId()];
|
||||
const relationsForEvent = this.relations.get(event.getId());
|
||||
if (!relationsForEvent) return;
|
||||
|
||||
for (const relationsWithRelType of Object.values(relationsForEvent)) {
|
||||
for (const relationsWithEventType of Object.values(relationsWithRelType)) {
|
||||
for (const relationsWithRelType of relationsForEvent.values()) {
|
||||
for (const relationsWithEventType of relationsWithRelType.values()) {
|
||||
relationsWithEventType.setTargetEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -123,23 +118,26 @@ export class RelationsContainer {
|
||||
const { event_id: relatesToEventId, rel_type: relationType } = relation;
|
||||
const eventType = event.getType();
|
||||
|
||||
let relationsForEvent = this.relations[relatesToEventId];
|
||||
let relationsForEvent = this.relations.get(relatesToEventId);
|
||||
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) {
|
||||
relationsWithRelType = relationsForEvent[relationType] = {};
|
||||
relationsWithRelType = new Map<EventType | string, Relations>();
|
||||
relationsForEvent.set(relationType, relationsWithRelType);
|
||||
}
|
||||
|
||||
let relationsWithEventType = relationsWithRelType[eventType];
|
||||
let relationsWithEventType = relationsWithRelType.get(eventType);
|
||||
if (!relationsWithEventType) {
|
||||
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
|
||||
relationsWithEventType = new Relations(
|
||||
relationType,
|
||||
eventType,
|
||||
this.client,
|
||||
);
|
||||
relationsWithRelType.set(eventType, relationsWithEventType);
|
||||
|
||||
const room = this.room ?? timelineSet?.room;
|
||||
const relatesToEvent = timelineSet?.findEventById(relatesToEventId)
|
||||
|
@@ -79,7 +79,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
|
||||
public readonly reEmitter = new TypedReEmitter<EmittedEvents, EventHandlerMap>(this);
|
||||
private sentinels: Record<string, RoomMember> = {}; // userId: RoomMember
|
||||
// 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 tokenToInvite: Record<string, MatrixEvent> = {}; // 3pid invite state_key to m.room.member invite
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
const strippedOldName = utils.removeHiddenChars(oldName);
|
||||
|
||||
const existingUserIds = this.displayNameToUserIds[strippedOldName];
|
||||
const existingUserIds = this.displayNameToUserIds.get(strippedOldName);
|
||||
if (existingUserIds) {
|
||||
// remove this user ID from this array
|
||||
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);
|
||||
// an empty stripped displayname (undefined/'') will be set to MXID in room-member.js
|
||||
if (strippedDisplayname) {
|
||||
if (!this.displayNameToUserIds[strippedDisplayname]) {
|
||||
this.displayNameToUserIds[strippedDisplayname] = [];
|
||||
}
|
||||
this.displayNameToUserIds[strippedDisplayname].push(userId);
|
||||
const arr = this.displayNameToUserIds.get(strippedDisplayname) ?? [];
|
||||
arr.push(userId);
|
||||
this.displayNameToUserIds.set(strippedDisplayname, arr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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".
|
||||
*
|
||||
* @param {MatrixEvent} event Event to be added
|
||||
* @param {IAddLiveEventOptions} options addLiveEvent options
|
||||
* @param {IAddLiveEventOptions} addLiveEventOptions addLiveEvent options
|
||||
* @fires module:client~MatrixClient#event:"Room.timeline"
|
||||
* @private
|
||||
*/
|
||||
@@ -2344,7 +2336,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
fromCache = false,
|
||||
): void {
|
||||
let duplicateStrategy = duplicateStrategyOrOpts as DuplicateStrategy;
|
||||
let timelineWasEmpty: boolean;
|
||||
let timelineWasEmpty = false;
|
||||
if (typeof (duplicateStrategyOrOpts) === 'object') {
|
||||
({
|
||||
duplicateStrategy,
|
||||
@@ -2383,10 +2375,25 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
const threadRoots = this.findThreadRoots(events);
|
||||
const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
|
||||
|
||||
const options: IAddLiveEventOptions = {
|
||||
duplicateStrategy,
|
||||
fromCache,
|
||||
timelineWasEmpty,
|
||||
};
|
||||
|
||||
for (const event of events) {
|
||||
// TODO: We should have a filter to say "only add state event types X Y Z to the timeline".
|
||||
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 {
|
||||
shouldLiveInRoom,
|
||||
shouldLiveInThread,
|
||||
@@ -2399,11 +2406,7 @@ export class Room extends TypedEventEmitter<EmittedEvents, RoomEventHandlerMap>
|
||||
eventsByThread[threadId]?.push(event);
|
||||
|
||||
if (shouldLiveInRoom) {
|
||||
this.addLiveEvent(event, {
|
||||
duplicateStrategy,
|
||||
fromCache,
|
||||
timelineWasEmpty,
|
||||
});
|
||||
this.addLiveEvent(event, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -296,13 +296,16 @@ export class SlidingSyncSdk {
|
||||
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) {
|
||||
logger.debug("onLifecycle", state, err);
|
||||
}
|
||||
switch (state) {
|
||||
case SlidingSyncState.Complete:
|
||||
this.purgeNotifications();
|
||||
if (!resp) {
|
||||
break;
|
||||
}
|
||||
// Element won't stop showing the initial loading spinner unless we fire SyncState.Prepared
|
||||
if (!this.lastPos) {
|
||||
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) {
|
||||
const inviteStateEvents = mapEvents(this.client, room.roomId, roomData.invite_state);
|
||||
this.injectRoomEvents(room, inviteStateEvents);
|
||||
@@ -609,6 +619,10 @@ export class SlidingSyncSdk {
|
||||
// we deliberately don't add ephemeral events to the timeline
|
||||
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();
|
||||
if (roomData.initial) {
|
||||
client.store.storeRoom(room);
|
||||
@@ -632,8 +646,6 @@ export class SlidingSyncSdk {
|
||||
client.emit(ClientEvent.Event, e);
|
||||
});
|
||||
|
||||
room.updateMyMembership("join");
|
||||
|
||||
// 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
|
||||
// notification count
|
||||
|
@@ -47,6 +47,8 @@ export interface MSC3575Filter {
|
||||
room_types?: string[];
|
||||
not_room_types?: string[];
|
||||
spaces?: string[];
|
||||
tags?: string[];
|
||||
not_tags?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,6 +84,8 @@ export interface MSC3575RoomData {
|
||||
timeline: (IRoomEvent | IStateEvent)[];
|
||||
notification_count?: number;
|
||||
highlight_count?: number;
|
||||
joined_count?: number;
|
||||
invited_count?: number;
|
||||
invite_state?: IStateEvent[];
|
||||
initial?: boolean;
|
||||
limited?: boolean;
|
||||
@@ -318,7 +322,9 @@ export enum SlidingSyncEvent {
|
||||
|
||||
export type SlidingSyncEventHandlerMap = {
|
||||
[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]: (
|
||||
listIndex: number, joinedCount: number, roomIndexToRoomId: Record<number, string>,
|
||||
) => void;
|
||||
@@ -530,6 +536,65 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
||||
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 {
|
||||
let gapIndex = -1;
|
||||
list.ops.forEach((op: Operation) => {
|
||||
@@ -537,6 +602,10 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
||||
case "DELETE": {
|
||||
logger.debug("DELETE", listIndex, 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;
|
||||
break;
|
||||
}
|
||||
@@ -551,20 +620,9 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
||||
if (this.lists[listIndex].roomIndexToRoomId[op.index]) {
|
||||
// something is in this space, shift items out of the way
|
||||
if (gapIndex < 0) {
|
||||
logger.debug(
|
||||
"cannot work out where gap is, INSERT without previous DELETE! List: ",
|
||||
listIndex,
|
||||
);
|
||||
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) {
|
||||
// we haven't been told where to shift from, so make way for a new room entry.
|
||||
this.addEntry(listIndex, op.index);
|
||||
} else if (gapIndex > op.index) {
|
||||
// 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:
|
||||
// [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,A,B,C] i=1
|
||||
// Terminate. We'll assign into op.index next.
|
||||
for (let i = gapIndex; i > op.index; i--) {
|
||||
if (this.lists[listIndex].isIndexInRange(i)) {
|
||||
this.lists[listIndex].roomIndexToRoomId[i] =
|
||||
this.lists[listIndex].roomIndexToRoomId[
|
||||
i - 1
|
||||
];
|
||||
}
|
||||
}
|
||||
this.shiftRight(listIndex, gapIndex, op.index);
|
||||
} else if (gapIndex < op.index) {
|
||||
// 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
|
||||
for (let i = gapIndex; i < op.index; i++) {
|
||||
if (this.lists[listIndex].isIndexInRange(i)) {
|
||||
this.lists[listIndex].roomIndexToRoomId[i] =
|
||||
this.lists[listIndex].roomIndexToRoomId[
|
||||
i + 1
|
||||
];
|
||||
}
|
||||
}
|
||||
this.shiftLeft(listIndex, op.index, gapIndex);
|
||||
}
|
||||
gapIndex = -1; // forget the gap, we don't need it anymore.
|
||||
}
|
||||
this.lists[listIndex].roomIndexToRoomId[op.index] = op.room_id;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
296
src/sync.ts
296
src/sync.ts
@@ -23,6 +23,8 @@ limitations under the License.
|
||||
* for HTTP and WS at some point.
|
||||
*/
|
||||
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
|
||||
import { User, UserEvent } from "./models/user";
|
||||
import { NotificationCountType, Room, RoomEvent } from "./models/room";
|
||||
import * as utils from "./utils";
|
||||
@@ -100,18 +102,16 @@ const MSC2716_ROOM_VERSIONS = [
|
||||
function getFilterName(userId: string, suffix?: string): string {
|
||||
// scope this on the user ID because people may login on many accounts
|
||||
// and they all need to be stored!
|
||||
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
|
||||
return `FILTER_SYNC_${userId}` + (suffix ? "_" + suffix : "");
|
||||
}
|
||||
|
||||
function debuglog(...params) {
|
||||
if (!DEBUG) {
|
||||
return;
|
||||
}
|
||||
if (!DEBUG) return;
|
||||
logger.log(...params);
|
||||
}
|
||||
|
||||
interface ISyncOptions {
|
||||
filterId?: string;
|
||||
filter?: string;
|
||||
hasSyncedBefore?: boolean;
|
||||
}
|
||||
|
||||
@@ -161,14 +161,14 @@ type WrappedRoom<T> = T & {
|
||||
* updating presence.
|
||||
*/
|
||||
export class SyncApi {
|
||||
private _peekRoom: Room = null;
|
||||
private currentSyncRequest: IAbortablePromise<ISyncResponse> = null;
|
||||
private syncState: SyncState = null;
|
||||
private syncStateData: ISyncStateData = null; // additional data (eg. error object for failed sync)
|
||||
private _peekRoom: Optional<Room> = null;
|
||||
private currentSyncRequest: Optional<IAbortablePromise<ISyncResponse>> = null;
|
||||
private syncState: Optional<SyncState> = null;
|
||||
private syncStateData: Optional<ISyncStateData> = null; // additional data (eg. error object for failed sync)
|
||||
private catchingUp = false;
|
||||
private running = false;
|
||||
private keepAliveTimer: ReturnType<typeof setTimeout> = null;
|
||||
private connectionReturnedDefer: IDeferred<boolean> = null;
|
||||
private keepAliveTimer: Optional<ReturnType<typeof setTimeout>> = null;
|
||||
private connectionReturnedDefer: Optional<IDeferred<boolean>> = null;
|
||||
private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response
|
||||
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
|
||||
@@ -214,7 +214,7 @@ export class SyncApi {
|
||||
* historical messages are shown when we paginate `/messages` again.
|
||||
* @param {Room} room The room where the marker event was sent
|
||||
* @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
|
||||
*/
|
||||
private onMarkerStateEvent(
|
||||
@@ -367,7 +367,7 @@ export class SyncApi {
|
||||
|
||||
// XXX: copypasted from /sync until we kill off this minging v1 API stuff)
|
||||
// handle presence events (User objects)
|
||||
if (response.presence && Array.isArray(response.presence)) {
|
||||
if (Array.isArray(response.presence)) {
|
||||
response.presence.map(client.getEventMapper()).forEach(
|
||||
function(presenceEvent) {
|
||||
let user = client.store.getUser(presenceEvent.getContent().user_id);
|
||||
@@ -542,67 +542,40 @@ export class SyncApi {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point
|
||||
*/
|
||||
public sync(): void {
|
||||
const client = this.client;
|
||||
|
||||
this.running = true;
|
||||
|
||||
if (global.window && global.window.addEventListener) {
|
||||
global.window.addEventListener("online", this.onOnline, false);
|
||||
}
|
||||
|
||||
let savedSyncPromise = Promise.resolve();
|
||||
let savedSyncToken = null;
|
||||
|
||||
// We need to do one-off checks before we can begin the /sync loop.
|
||||
// These are:
|
||||
// 1) We need to get push rules so we can check if events should bing as we get
|
||||
// them from /sync.
|
||||
// 2) We need to get/create a filter which we can use for /sync.
|
||||
// 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.
|
||||
|
||||
const getPushRules = async () => {
|
||||
private getPushRules = async () => {
|
||||
try {
|
||||
debuglog("Getting push rules...");
|
||||
const result = await client.getPushRules();
|
||||
const result = await this.client.getPushRules();
|
||||
debuglog("Got push rules");
|
||||
|
||||
client.pushRules = result;
|
||||
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(savedSyncPromise, err);
|
||||
getPushRules();
|
||||
return;
|
||||
await this.recoverFromSyncStartupError(this.savedSyncPromise, err);
|
||||
return this.getPushRules(); // try again
|
||||
}
|
||||
checkLazyLoadStatus(); // advance to the next stage
|
||||
};
|
||||
|
||||
const buildDefaultFilter = () => {
|
||||
const filter = new Filter(client.credentials.userId);
|
||||
filter.setTimelineLimit(this.opts.initialSyncLimit);
|
||||
return filter;
|
||||
private buildDefaultFilter = () => {
|
||||
return new Filter(this.client.credentials.userId);
|
||||
};
|
||||
|
||||
const checkLazyLoadStatus = async () => {
|
||||
private checkLazyLoadStatus = async () => {
|
||||
debuglog("Checking lazy load status...");
|
||||
if (this.opts.lazyLoadMembers && client.isGuest()) {
|
||||
if (this.opts.lazyLoadMembers && this.client.isGuest()) {
|
||||
this.opts.lazyLoadMembers = false;
|
||||
}
|
||||
if (this.opts.lazyLoadMembers) {
|
||||
debuglog("Checking server lazy load support...");
|
||||
const supported = await client.doesServerSupportLazyLoading();
|
||||
const supported = await this.client.doesServerSupportLazyLoading();
|
||||
if (supported) {
|
||||
debuglog("Enabling lazy load on sync filter...");
|
||||
if (!this.opts.filter) {
|
||||
this.opts.filter = buildDefaultFilter();
|
||||
this.opts.filter = this.buildDefaultFilter();
|
||||
}
|
||||
this.opts.filter.setLazyLoadMembers(true);
|
||||
} else {
|
||||
@@ -626,8 +599,8 @@ export class SyncApi {
|
||||
logger.warn("InvalidStoreError: store is not usable: stopping sync.");
|
||||
return;
|
||||
}
|
||||
if (this.opts.lazyLoadMembers && this.opts.crypto) {
|
||||
this.opts.crypto.enableLazyLoading();
|
||||
if (this.opts.lazyLoadMembers) {
|
||||
this.opts.crypto?.enableLazyLoading();
|
||||
}
|
||||
try {
|
||||
debuglog("Storing client options...");
|
||||
@@ -637,65 +610,60 @@ export class SyncApi {
|
||||
logger.error("Storing client options failed", err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
getFilter(); // Now get the filter and start syncing
|
||||
};
|
||||
|
||||
const getFilter = async () => {
|
||||
private getFilter = async (): Promise<{
|
||||
filterId?: string;
|
||||
filter?: Filter;
|
||||
}> => {
|
||||
debuglog("Getting filter...");
|
||||
let filter;
|
||||
let filter: Filter;
|
||||
if (this.opts.filter) {
|
||||
filter = this.opts.filter;
|
||||
} else {
|
||||
filter = buildDefaultFilter();
|
||||
filter = this.buildDefaultFilter();
|
||||
}
|
||||
|
||||
let filterId;
|
||||
let filterId: string;
|
||||
try {
|
||||
filterId = await client.getOrCreateFilter(getFilterName(client.credentials.userId), filter);
|
||||
filterId = await this.client.getOrCreateFilter(getFilterName(this.client.credentials.userId), filter);
|
||||
} catch (err) {
|
||||
logger.error("Getting filter failed", err);
|
||||
if (this.shouldAbortSync(err)) return;
|
||||
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;
|
||||
await this.recoverFromSyncStartupError(this.savedSyncPromise, err);
|
||||
return this.getFilter(); // try again
|
||||
}
|
||||
// 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...");
|
||||
this.currentSyncRequest = this.doSyncRequest({ filterId }, savedSyncToken);
|
||||
}
|
||||
|
||||
// Now wait for the saved sync to finish...
|
||||
debuglog("Waiting for saved sync before starting sync processing...");
|
||||
await savedSyncPromise;
|
||||
this.doSync({ filterId });
|
||||
return { filter, filterId };
|
||||
};
|
||||
|
||||
if (client.isGuest()) {
|
||||
private savedSyncPromise: Promise<void>;
|
||||
|
||||
/**
|
||||
* Main entry point
|
||||
*/
|
||||
public async sync(): Promise<void> {
|
||||
this.running = true;
|
||||
|
||||
global.window?.addEventListener?.("online", this.onOnline, false);
|
||||
|
||||
if (this.client.isGuest()) {
|
||||
// no push rules for guests, no access to POST filter for guests.
|
||||
this.doSync({});
|
||||
} else {
|
||||
return this.doSync({});
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
const savedSyncTokenPromise = this.client.store.getSavedSyncToken().then(tok => {
|
||||
debuglog("Got saved sync token");
|
||||
savedSyncToken = tok;
|
||||
debuglog("Getting saved sync...");
|
||||
return client.store.getSavedSync();
|
||||
}).then((savedSync) => {
|
||||
return tok;
|
||||
});
|
||||
|
||||
this.savedSyncPromise = this.client.store.getSavedSync().then((savedSync) => {
|
||||
debuglog(`Got reply from saved sync, exists? ${!!savedSync}`);
|
||||
if (savedSync) {
|
||||
return this.syncFromCache(savedSync);
|
||||
@@ -703,11 +671,54 @@ export class SyncApi {
|
||||
}).catch(err => {
|
||||
logger.error("Getting saved sync failed", err);
|
||||
});
|
||||
|
||||
// We need to do one-off checks before we can begin the /sync loop.
|
||||
// These are:
|
||||
// 1) We need to get push rules so we can check if events should bing as we get
|
||||
// them from /sync.
|
||||
// 2) We need to get/create a filter which we can use for /sync.
|
||||
// 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.
|
||||
|
||||
// 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();
|
||||
await this.getPushRules();
|
||||
await this.checkLazyLoadStatus();
|
||||
const { filterId, filter } = await this.getFilter();
|
||||
if (!filter) return; // bail, getFilter failed
|
||||
|
||||
// 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.
|
||||
this.client.resetNotifTimelineSet();
|
||||
|
||||
if (this.currentSyncRequest === null) {
|
||||
let firstSyncFilter = filterId;
|
||||
const savedSyncToken = await savedSyncTokenPromise;
|
||||
|
||||
if (savedSyncToken) {
|
||||
debuglog("Sending first sync request...");
|
||||
} 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());
|
||||
}
|
||||
|
||||
// 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.
|
||||
this.currentSyncRequest = this.doSyncRequest({ filter: firstSyncFilter }, savedSyncToken);
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Some platforms (e.g. React Native) register global.window,
|
||||
// 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.currentSyncRequest?.abort();
|
||||
if (this.keepAliveTimer) {
|
||||
@@ -756,8 +765,7 @@ export class SyncApi {
|
||||
this.client.store.setSyncToken(nextSyncToken);
|
||||
|
||||
// No previous sync, set old token to null
|
||||
const syncEventData = {
|
||||
oldSyncToken: null,
|
||||
const syncEventData: ISyncStateData = {
|
||||
nextSyncToken,
|
||||
catchingUp: false,
|
||||
fromCache: true,
|
||||
@@ -792,21 +800,10 @@ export class SyncApi {
|
||||
* @param {boolean} syncOptions.hasSyncedBefore
|
||||
*/
|
||||
private async doSync(syncOptions: ISyncOptions): Promise<void> {
|
||||
const client = this.client;
|
||||
while (this.running) {
|
||||
const syncToken = this.client.store.getSyncToken();
|
||||
|
||||
if (!this.running) {
|
||||
debuglog("Sync no longer running: exiting.");
|
||||
if (this.connectionReturnedDefer) {
|
||||
this.connectionReturnedDefer.reject();
|
||||
this.connectionReturnedDefer = null;
|
||||
}
|
||||
this.updateSyncState(SyncState.Stopped);
|
||||
return;
|
||||
}
|
||||
|
||||
const syncToken = client.store.getSyncToken();
|
||||
|
||||
let data;
|
||||
let data: ISyncResponse;
|
||||
try {
|
||||
//debuglog('Starting sync since=' + syncToken);
|
||||
if (this.currentSyncRequest === null) {
|
||||
@@ -814,8 +811,9 @@ export class SyncApi {
|
||||
}
|
||||
data = await this.currentSyncRequest;
|
||||
} catch (e) {
|
||||
this.onSyncError(e, syncOptions);
|
||||
return;
|
||||
const abort = await this.onSyncError(e);
|
||||
if (abort) return;
|
||||
continue;
|
||||
} finally {
|
||||
this.currentSyncRequest = null;
|
||||
}
|
||||
@@ -825,12 +823,12 @@ export class SyncApi {
|
||||
// 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);
|
||||
this.client.store.setSyncToken(data.next_batch);
|
||||
|
||||
// Reset after a successful sync
|
||||
this.failedSyncCount = 0;
|
||||
|
||||
await client.store.setSyncData(data);
|
||||
await this.client.store.setSyncData(data);
|
||||
|
||||
const syncEventData = {
|
||||
oldSyncToken: syncToken,
|
||||
@@ -873,7 +871,7 @@ export class SyncApi {
|
||||
// keep emitting SYNCING -> SYNCING for clients who want to do bulk updates
|
||||
this.updateSyncState(SyncState.Syncing, syncEventData);
|
||||
|
||||
if (client.store.wantsSave()) {
|
||||
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
|
||||
@@ -884,11 +882,18 @@ export class SyncApi {
|
||||
}
|
||||
|
||||
// tell databases that everything is now in a consistent state and can be saved.
|
||||
client.store.save();
|
||||
this.client.store.save();
|
||||
}
|
||||
}
|
||||
|
||||
// Begin next sync
|
||||
this.doSync(syncOptions);
|
||||
if (!this.running) {
|
||||
debuglog("Sync no longer running: exiting.");
|
||||
if (this.connectionReturnedDefer) {
|
||||
this.connectionReturnedDefer.reject();
|
||||
this.connectionReturnedDefer = null;
|
||||
}
|
||||
this.updateSyncState(SyncState.Stopped);
|
||||
}
|
||||
}
|
||||
|
||||
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IAbortablePromise<ISyncResponse> {
|
||||
@@ -902,7 +907,7 @@ export class SyncApi {
|
||||
private getSyncParams(syncOptions: ISyncOptions, syncToken: string): ISyncParams {
|
||||
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
|
||||
// as quickly as possible, even if there are no events queued. This
|
||||
// serves two purposes:
|
||||
@@ -918,13 +923,13 @@ export class SyncApi {
|
||||
pollTimeout = 0;
|
||||
}
|
||||
|
||||
let filterId = syncOptions.filterId;
|
||||
if (this.client.isGuest() && !filterId) {
|
||||
filterId = this.getGuestFilter();
|
||||
let filter = syncOptions.filter;
|
||||
if (this.client.isGuest() && !filter) {
|
||||
filter = this.getGuestFilter();
|
||||
}
|
||||
|
||||
const qps: ISyncParams = {
|
||||
filter: filterId,
|
||||
filter,
|
||||
timeout: pollTimeout,
|
||||
};
|
||||
|
||||
@@ -941,7 +946,7 @@ export class SyncApi {
|
||||
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
|
||||
// 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
|
||||
@@ -952,7 +957,7 @@ export class SyncApi {
|
||||
return qps;
|
||||
}
|
||||
|
||||
private onSyncError(err: MatrixError, syncOptions: ISyncOptions): void {
|
||||
private async onSyncError(err: MatrixError): Promise<boolean> {
|
||||
if (!this.running) {
|
||||
debuglog("Sync no longer running: exiting");
|
||||
if (this.connectionReturnedDefer) {
|
||||
@@ -960,14 +965,13 @@ export class SyncApi {
|
||||
this.connectionReturnedDefer = null;
|
||||
}
|
||||
this.updateSyncState(SyncState.Stopped);
|
||||
return;
|
||||
return true; // abort
|
||||
}
|
||||
|
||||
logger.error("/sync error %s", err);
|
||||
logger.error(err);
|
||||
|
||||
if (this.shouldAbortSync(err)) {
|
||||
return;
|
||||
return true; // abort
|
||||
}
|
||||
|
||||
this.failedSyncCount++;
|
||||
@@ -981,20 +985,7 @@ export class SyncApi {
|
||||
// erroneous. We set the state to 'reconnecting'
|
||||
// instead, so that clients can observe this state
|
||||
// if they wish.
|
||||
this.startKeepAlives().then((connDidFail) => {
|
||||
// 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);
|
||||
});
|
||||
const keepAlivePromise = this.startKeepAlives();
|
||||
|
||||
this.currentSyncRequest = null;
|
||||
// Transition from RECONNECTING to ERROR after a given number of failed syncs
|
||||
@@ -1003,6 +994,19 @@ export class SyncApi {
|
||||
SyncState.Error : SyncState.Reconnecting,
|
||||
{ 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.
|
||||
|
||||
// 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(
|
||||
function(presenceEvent) {
|
||||
let user = client.store.getUser(presenceEvent.getSender());
|
||||
@@ -1077,7 +1081,7 @@ export class SyncApi {
|
||||
}
|
||||
|
||||
// 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 prevEventsMap = events.reduce((m, c) => {
|
||||
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
|
||||
// server's for this case, and therefore will assume that our non-zero
|
||||
// count is accurate.
|
||||
if (!encrypted
|
||||
|| (encrypted && room.getUnreadNotificationCount(NotificationCountType.Highlight) <= 0)) {
|
||||
if (!encrypted || room.getUnreadNotificationCount(NotificationCountType.Highlight) <= 0) {
|
||||
room.setUnreadNotificationCount(
|
||||
NotificationCountType.Highlight,
|
||||
joinObj.unread_notifications.highlight_count,
|
||||
@@ -1232,8 +1235,7 @@ export class SyncApi {
|
||||
if (joinObj.isBrandNewRoom) {
|
||||
// set the back-pagination token. Do this *before* adding any
|
||||
// events so that clients can start back-paginating.
|
||||
room.getLiveTimeline().setPaginationToken(
|
||||
joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
|
||||
room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
|
||||
} else if (joinObj.timeline.limited) {
|
||||
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 { 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.
|
||||
* Omits any undefined/null values.
|
||||
@@ -75,8 +99,7 @@ export function decodeParams(query: string): QueryDict {
|
||||
* variables with. E.g. { "$bar": "baz" }.
|
||||
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||
*/
|
||||
export function encodeUri(pathTemplate: string,
|
||||
variables: Record<string, string>): string {
|
||||
export function encodeUri(pathTemplate: string, variables: Record<string, string>): string {
|
||||
for (const key in variables) {
|
||||
if (!variables.hasOwnProperty(key)) {
|
||||
continue;
|
||||
@@ -216,33 +239,24 @@ export function deepCompare(x: any, y: any): boolean {
|
||||
}
|
||||
}
|
||||
} 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
|
||||
let p;
|
||||
for (p in y) {
|
||||
for (const p in y) {
|
||||
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// finally, compare each of x's keys with y
|
||||
for (p in y) { // eslint-disable-line guard-for-in
|
||||
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
|
||||
return false;
|
||||
}
|
||||
if (!deepCompare(x[p], y[p])) {
|
||||
for (const p in x) {
|
||||
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p) || !deepCompare(x[p], y[p])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* jshint +W089 */
|
||||
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
|
||||
* sorts the result by key, recursively. The input object must
|
||||
@@ -328,7 +342,7 @@ export function escapeRegExp(string: string): string {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
export function globToRegexp(glob: string, extended?: any): string {
|
||||
export function globToRegexp(glob: string, extended = false): string {
|
||||
// From
|
||||
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
|
||||
// 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) ])[] = [
|
||||
[/\\\*/g, '.*'],
|
||||
[/\?/g, '.'],
|
||||
extended !== false && [
|
||||
!extended && [
|
||||
/\\\[(!|)(.*)\\]/g,
|
||||
(_match: string, neg: string, pat: string) => [
|
||||
'[',
|
||||
|
@@ -12,6 +12,6 @@
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts",
|
||||
"./spec/**/*.ts",
|
||||
"./spec/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
Reference in New Issue
Block a user