You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-06 12:02:40 +03:00
Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/ts/12
Conflicts: src/client.ts src/interactive-auth.ts src/models/search-result.ts
This commit is contained in:
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,3 +1,7 @@
|
||||
<!-- Please read https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst before submitting your pull request -->
|
||||
<!-- Please read https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md before submitting your pull request -->
|
||||
|
||||
<!-- Include a Sign-Off as described in https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst#sign-off -->
|
||||
<!-- Include a Sign-Off as described in https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md#sign-off -->
|
||||
|
||||
<!-- To specify text for the changelog entry (otherwise the PR title will be used):
|
||||
Notes:
|
||||
-->
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -16,3 +16,6 @@ out
|
||||
# version file and tarball created by `npm pack` / `yarn pack`
|
||||
/git-revision.txt
|
||||
/matrix-js-sdk-*.tgz
|
||||
|
||||
.vscode
|
||||
.vscode/
|
||||
|
41
CHANGELOG.md
41
CHANGELOG.md
@@ -1,3 +1,44 @@
|
||||
Changes in [12.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v12.1.0) (2021-07-19)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v12.1.0-rc.1...v12.1.0)
|
||||
|
||||
* No changes from rc.1
|
||||
|
||||
Changes in [12.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v12.1.0-rc.1) (2021-07-14)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v12.0.1...v12.1.0-rc.1)
|
||||
|
||||
* Add VS Code to gitignore
|
||||
[\#1783](https://github.com/matrix-org/matrix-js-sdk/pull/1783)
|
||||
* Make `Crypto::inRoomVerificationRequests` public
|
||||
[\#1781](https://github.com/matrix-org/matrix-js-sdk/pull/1781)
|
||||
* Call `setEventMetadata()` for filtered `timelineSet`s
|
||||
[\#1765](https://github.com/matrix-org/matrix-js-sdk/pull/1765)
|
||||
* Symmetric backup
|
||||
[\#1775](https://github.com/matrix-org/matrix-js-sdk/pull/1775)
|
||||
* Attempt to fix megolm key not being in SSSS
|
||||
[\#1776](https://github.com/matrix-org/matrix-js-sdk/pull/1776)
|
||||
* Convert SecretStorage to TypeScript
|
||||
[\#1774](https://github.com/matrix-org/matrix-js-sdk/pull/1774)
|
||||
* Strip hash from urls being previewed to de-duplicate
|
||||
[\#1721](https://github.com/matrix-org/matrix-js-sdk/pull/1721)
|
||||
* Do not generate a lockfile when running in CI
|
||||
[\#1773](https://github.com/matrix-org/matrix-js-sdk/pull/1773)
|
||||
* Tidy up secret requesting code
|
||||
[\#1766](https://github.com/matrix-org/matrix-js-sdk/pull/1766)
|
||||
* Convert Sync and SyncAccumulator to Typescript
|
||||
[\#1763](https://github.com/matrix-org/matrix-js-sdk/pull/1763)
|
||||
* Convert EventTimeline, EventTimelineSet and TimelineWindow to TS
|
||||
[\#1762](https://github.com/matrix-org/matrix-js-sdk/pull/1762)
|
||||
* Comply with new member-delimiter-style rule
|
||||
[\#1764](https://github.com/matrix-org/matrix-js-sdk/pull/1764)
|
||||
* Do not honor string power levels
|
||||
[\#1754](https://github.com/matrix-org/matrix-js-sdk/pull/1754)
|
||||
* Typescriptify some crypto stuffs
|
||||
[\#1508](https://github.com/matrix-org/matrix-js-sdk/pull/1508)
|
||||
* Make filterId read/write and optional
|
||||
[\#1760](https://github.com/matrix-org/matrix-js-sdk/pull/1760)
|
||||
|
||||
Changes in [12.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v12.0.1) (2021-07-05)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v12.0.1-rc.1...v12.0.1)
|
||||
|
194
CONTRIBUTING.md
Normal file
194
CONTRIBUTING.md
Normal file
@@ -0,0 +1,194 @@
|
||||
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)).
|
||||
|
||||
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)
|
||||
* Notes for the reviewer that might help them to understand why the change is
|
||||
necessary or how they might better review it.
|
||||
|
||||
Things that should *not* go into your PR description:
|
||||
* Any information on how the code works or why you chose to do it the way
|
||||
you did. If this isn't obvious from your code, you haven't written enough
|
||||
comments.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
```
|
131
CONTRIBUTING.rst
131
CONTRIBUTING.rst
@@ -1,131 +0,0 @@
|
||||
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>`_).
|
||||
|
||||
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/)
|
||||
|
||||
**The single biggest thing you need to know is: please base your changes on
|
||||
the develop branch - /not/ master.**
|
||||
|
||||
We use the master branch to track the most recent release, so that folks who
|
||||
blindly clone the repo and automatically check out master get something that
|
||||
works. Develop is the unstable branch where all the development actually
|
||||
happens: the workflow is that contributors should fork the develop branch to
|
||||
make a 'feature' branch for a particular contribution, and then make a pull
|
||||
request to merge this back into the matrix.org 'official' develop branch. 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. The
|
||||
changes will then land on master when we next do a release.
|
||||
|
||||
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.
|
||||
|
||||
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
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "12.0.1",
|
||||
"version": "12.1.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
@@ -15,7 +15,8 @@
|
||||
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
|
||||
"gendoc": "jsdoc -c jsdoc.json -P package.json",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 57 src spec",
|
||||
"lint:js": "eslint --max-warnings 7 src spec",
|
||||
"lint:js-fix": "eslint --fix src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
|
@@ -100,7 +100,7 @@ fi
|
||||
# global cache here to ensure we get the right thing.
|
||||
yarn cache clean
|
||||
# Ensure all dependencies are updated
|
||||
yarn install --ignore-scripts
|
||||
yarn install --ignore-scripts --pure-lockfile
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
# update_changelog doesn't have a --version flag
|
||||
@@ -225,7 +225,7 @@ if [ $dodist -eq 0 ]; then
|
||||
pushd "$builddir"
|
||||
git clone "$projdir" .
|
||||
git checkout "$rel_branch"
|
||||
yarn install
|
||||
yarn install --pure-lockfile
|
||||
# We haven't tagged yet, so tell the dist script what version
|
||||
# it's building
|
||||
DIST_VERSION="$tag" yarn dist
|
||||
|
@@ -91,7 +91,7 @@ export function mkEvent(opts) {
|
||||
event.state_key = opts.skey;
|
||||
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
|
||||
"m.room.power_levels", "m.room.topic",
|
||||
"com.example.state"].indexOf(opts.type) !== -1) {
|
||||
"com.example.state"].includes(opts.type)) {
|
||||
event.state_key = "";
|
||||
}
|
||||
return opts.event ? new MatrixEvent(event) : event;
|
||||
|
@@ -52,7 +52,7 @@ const ENCRYPTED_EVENT = new MatrixEvent({
|
||||
origin_server_ts: 1507753886000,
|
||||
});
|
||||
|
||||
const KEY_BACKUP_DATA = {
|
||||
const CURVE25519_KEY_BACKUP_DATA = {
|
||||
first_message_index: 0,
|
||||
forwarded_count: 0,
|
||||
is_verified: false,
|
||||
@@ -73,7 +73,26 @@ const KEY_BACKUP_DATA = {
|
||||
},
|
||||
};
|
||||
|
||||
const BACKUP_INFO = {
|
||||
const AES256_KEY_BACKUP_DATA = {
|
||||
first_message_index: 0,
|
||||
forwarded_count: 0,
|
||||
is_verified: false,
|
||||
session_data: {
|
||||
iv: 'b3Jqqvm5S9QdmXrzssspLQ',
|
||||
ciphertext: 'GOOASO3E9ThogkG0zMjEduGLM3u9jHZTkS7AvNNbNj3q1znwk4OlaVKXce'
|
||||
+ '7ynofiiYIiS865VlOqrKEEXv96XzRyUpgn68e3WsicwYl96EtjIEh/iY003PG2Qd'
|
||||
+ 'EluT899Ax7PydpUHxEktbWckMppYomUR5q8x1KI1SsOQIiJaIGThmIMPANRCFiK0'
|
||||
+ 'WQj+q+dnhzx4lt9AFqU5bKov8qKnw2qGYP7/+6RmJ0Kpvs8tG6lrcNDEHtFc2r0r'
|
||||
+ 'KKubDypo0Vc8EWSwsAHdKa36ewRavpreOuE8Z9RLfY0QIR1ecXrMqW0CdGFr7H3P'
|
||||
+ 'vcjF8sjwvQAavzxEKT1WMGizSMLeKWo2mgZ5cKnwV5HGUAw596JQvKs9laG2U89K'
|
||||
+ 'YrT0sH30vi62HKzcBLcDkWkUSNYPz7UiZ1MM0L380UA+1ZOXSOmtBA9xxzzbc8Xd'
|
||||
+ 'fRimVgklGdxrxjzuNLYhL2BvVH4oPWonD9j0bvRwE6XkimdbGQA8HB7UmXXjE8WA'
|
||||
+ 'RgaDHkfzoA3g3aeQ',
|
||||
mac: 'uR988UYgGL99jrvLLPX3V1ows+UYbktTmMxPAo2kxnU',
|
||||
},
|
||||
};
|
||||
|
||||
const CURVE25519_BACKUP_INFO = {
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
@@ -81,6 +100,14 @@ const BACKUP_INFO = {
|
||||
},
|
||||
};
|
||||
|
||||
const AES256_BACKUP_INFO = {
|
||||
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
// FIXME: add iv and mac
|
||||
},
|
||||
};
|
||||
|
||||
const keys = {};
|
||||
|
||||
function getCrossSigningKey(type) {
|
||||
@@ -144,7 +171,7 @@ describe("MegolmBackup", function() {
|
||||
mockCrypto.backupKey.set_recipient_key(
|
||||
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
);
|
||||
mockCrypto.backupInfo = BACKUP_INFO;
|
||||
mockCrypto.backupInfo = CURVE25519_BACKUP_INFO;
|
||||
|
||||
mockStorage = new MockStorageApi();
|
||||
sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
@@ -228,7 +255,7 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('sends backups to the server', function() {
|
||||
it('sends backups to the server (Curve25519 version)', function() {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
const ibGroupSession = new Olm.InboundGroupSession();
|
||||
@@ -306,6 +333,88 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('sends backups to the server (AES-256 version)', function() {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
const ibGroupSession = new Olm.InboundGroupSession();
|
||||
ibGroupSession.create(groupSession.session_key());
|
||||
|
||||
const client = makeTestClient(sessionStore, cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
olmDevice: olmDevice,
|
||||
baseApis: client,
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
|
||||
return client.initCrypto()
|
||||
.then(() => {
|
||||
return client.crypto.storeSessionBackupPrivateKey(new Uint8Array(32));
|
||||
})
|
||||
.then(() => {
|
||||
return cryptoStore.doTxn(
|
||||
"readwrite",
|
||||
[cryptoStore.STORE_SESSION],
|
||||
(txn) => {
|
||||
cryptoStore.addEndToEndInboundGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
{
|
||||
forwardingCurve25519KeyChain: undefined,
|
||||
keysClaimed: {
|
||||
ed25519: "SENDER_ED25519",
|
||||
},
|
||||
room_id: ROOM_ID,
|
||||
session: ibGroupSession.pickle(olmDevice._pickleKey),
|
||||
},
|
||||
txn);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
client.enableKeyBackup({
|
||||
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
iv: "PsCAtR7gMc4xBd9YS3A9Ow",
|
||||
mac: "ZSDsTFEZK7QzlauCLMleUcX96GQZZM7UNtk4sripSqQ",
|
||||
},
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(1);
|
||||
if (numCalls >= 2) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({});
|
||||
}
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe(1);
|
||||
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
resolve();
|
||||
return Promise.resolve({});
|
||||
};
|
||||
client.crypto.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
groupSession.session_id(),
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('signs backups with the cross-signing master key', async function() {
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
@@ -512,30 +621,47 @@ describe("MegolmBackup", function() {
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it('can restore from backup', function() {
|
||||
it('can restore from backup (Curve25519 version)', function() {
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve(KEY_BACKUP_DATA);
|
||||
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
|
||||
};
|
||||
return client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
BACKUP_INFO,
|
||||
CURVE25519_BACKUP_INFO,
|
||||
).then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
expect(res.untrusted).toBeTruthy(); // keys from backup are untrusted
|
||||
expect(res.untrusted).toBeTruthy(); // keys from Curve25519 backup are untrusted
|
||||
});
|
||||
});
|
||||
|
||||
it('can restore backup by room', function() {
|
||||
it('can restore from backup (AES-256 version)', function() {
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve(AES256_KEY_BACKUP_DATA);
|
||||
};
|
||||
return client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
AES256_BACKUP_INFO,
|
||||
).then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
expect(res.untrusted).toBeFalsy(); // keys from AES backup are trusted
|
||||
});
|
||||
});
|
||||
|
||||
it('can restore backup by room (Curve25519 version)', function() {
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve({
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
sessions: {
|
||||
[SESSION_ID]: KEY_BACKUP_DATA,
|
||||
[SESSION_ID]: CURVE25519_KEY_BACKUP_DATA,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -543,7 +669,7 @@ describe("MegolmBackup", function() {
|
||||
};
|
||||
return client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
null, null, BACKUP_INFO,
|
||||
null, null, CURVE25519_BACKUP_INFO,
|
||||
).then(() => {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
@@ -562,14 +688,14 @@ describe("MegolmBackup", function() {
|
||||
const cachedNull = await client.crypto.getSessionBackupPrivateKey();
|
||||
expect(cachedNull).toBeNull();
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve(KEY_BACKUP_DATA);
|
||||
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
|
||||
};
|
||||
await new Promise((resolve) => {
|
||||
client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
BACKUP_INFO,
|
||||
CURVE25519_BACKUP_INFO,
|
||||
{ cacheCompleteCallback: resolve },
|
||||
);
|
||||
});
|
||||
@@ -578,11 +704,11 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
|
||||
it("fails if an known algorithm is used", async function() {
|
||||
const BAD_BACKUP_INFO = Object.assign({}, BACKUP_INFO, {
|
||||
const BAD_BACKUP_INFO = Object.assign({}, CURVE25519_BACKUP_INFO, {
|
||||
algorithm: "this.algorithm.does.not.exist",
|
||||
});
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve(KEY_BACKUP_DATA);
|
||||
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
|
||||
};
|
||||
|
||||
await expect(client.restoreKeyBackupWithRecoveryKey(
|
||||
|
@@ -3,6 +3,7 @@ import { EventStatus, MatrixEvent } from "../../src";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import { RoomState } from "../../src";
|
||||
import { Room } from "../../src";
|
||||
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("Room", function() {
|
||||
@@ -1456,4 +1457,291 @@ describe("Room", function() {
|
||||
expect(room.maySendMessage()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDefaultRoomName", function() {
|
||||
it("should return 'Empty room' if a user is the only member",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
|
||||
});
|
||||
|
||||
it("should return a display name if one other member is in the room",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||
});
|
||||
|
||||
it("should return a display name if one other member is banned",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "ban",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)");
|
||||
});
|
||||
|
||||
it("should return a display name if one other member is invited",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "invite",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||
});
|
||||
|
||||
it("should return 'Empty room (was User B)' if User B left the room",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "leave",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)");
|
||||
});
|
||||
|
||||
it("should return 'User B and User C' if in a room with two other users",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userC, mship: "join",
|
||||
room: roomId, event: true, name: "User C",
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("User B and User C");
|
||||
});
|
||||
|
||||
it("should return 'User B and 2 others' if in a room with three other users",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userC, mship: "join",
|
||||
room: roomId, event: true, name: "User C",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userD, mship: "join",
|
||||
room: roomId, event: true, name: "User D",
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("User B and 2 others");
|
||||
});
|
||||
|
||||
describe("io.element.functional_users", function() {
|
||||
it("should return a display name (default behaviour) if no one is marked as a functional member",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||
room: roomId, event: true,
|
||||
content: {
|
||||
service_members: [],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||
});
|
||||
|
||||
it("should return a display name (default behaviour) if service members is a number (invalid)",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||
room: roomId, event: true,
|
||||
content: {
|
||||
service_members: 1,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||
});
|
||||
|
||||
it("should return a display name (default behaviour) if service members is a string (invalid)",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||
room: roomId, event: true,
|
||||
content: {
|
||||
service_members: userB,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||
});
|
||||
|
||||
it("should return 'Empty room' if the only other member is a functional member",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||
room: roomId, event: true,
|
||||
content: {
|
||||
service_members: [userB],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
|
||||
});
|
||||
|
||||
it("should return 'User B' if User B is the only other member who isn't a functional member",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userC, mship: "join",
|
||||
room: roomId, event: true, name: "User C",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||
room: roomId, event: true, user: userA,
|
||||
content: {
|
||||
service_members: [userC],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||
});
|
||||
|
||||
it("should return 'Empty room' if all other members are functional members",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userC, mship: "join",
|
||||
room: roomId, event: true, name: "User C",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||
room: roomId, event: true, user: userA,
|
||||
content: {
|
||||
service_members: [userB, userC],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
|
||||
});
|
||||
|
||||
it("should not break if an unjoined user is marked as a service user",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||
room: roomId, event: true, user: userA,
|
||||
content: {
|
||||
service_members: [userC],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
164
src/@types/PushRules.ts
Normal file
164
src/@types/PushRules.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
Copyright 2021 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.
|
||||
*/
|
||||
|
||||
// allow camelcase as these are things that go onto the wire
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
export enum PushRuleActionName {
|
||||
DontNotify = "dont_notify",
|
||||
Notify = "notify",
|
||||
Coalesce = "coalesce",
|
||||
}
|
||||
|
||||
export enum TweakName {
|
||||
Highlight = "highlight",
|
||||
Sound = "sound",
|
||||
}
|
||||
|
||||
export type Tweak<N extends TweakName, V> = {
|
||||
set_tweak: N;
|
||||
value: V;
|
||||
};
|
||||
|
||||
export type TweakHighlight = Tweak<TweakName.Highlight, boolean>;
|
||||
export type TweakSound = Tweak<TweakName.Sound, string>;
|
||||
|
||||
export type Tweaks = TweakHighlight | TweakSound;
|
||||
|
||||
export enum ConditionOperator {
|
||||
ExactEquals = "==",
|
||||
LessThan = "<",
|
||||
GreaterThan = ">",
|
||||
GreaterThanOrEqual = ">=",
|
||||
LessThanOrEqual = "<=",
|
||||
}
|
||||
|
||||
export type PushRuleAction = Tweaks | PushRuleActionName;
|
||||
|
||||
export type MemberCountCondition
|
||||
<N extends number, Op extends ConditionOperator = ConditionOperator.ExactEquals>
|
||||
= `${Op}${N}` | (Op extends ConditionOperator.ExactEquals ? `${N}` : never);
|
||||
|
||||
export type AnyMemberCountCondition = MemberCountCondition<number, ConditionOperator>;
|
||||
|
||||
export const DMMemberCountCondition: MemberCountCondition<2> = "2";
|
||||
|
||||
export function isDmMemberCountCondition(condition: AnyMemberCountCondition): boolean {
|
||||
return condition === "==2" || condition === "2";
|
||||
}
|
||||
|
||||
export enum ConditionKind {
|
||||
EventMatch = "event_match",
|
||||
ContainsDisplayName = "contains_display_name",
|
||||
RoomMemberCount = "room_member_count",
|
||||
SenderNotificationPermission = "sender_notification_permission",
|
||||
}
|
||||
|
||||
export interface IPushRuleCondition<N extends ConditionKind | string> {
|
||||
[k: string]: any; // for custom conditions, there can be other fields here
|
||||
kind: N;
|
||||
}
|
||||
|
||||
export interface IEventMatchCondition extends IPushRuleCondition<ConditionKind.EventMatch> {
|
||||
key: string;
|
||||
pattern: string;
|
||||
}
|
||||
|
||||
export interface IContainsDisplayNameCondition extends IPushRuleCondition<ConditionKind.ContainsDisplayName> {
|
||||
// no additional fields
|
||||
}
|
||||
|
||||
export interface IRoomMemberCountCondition extends IPushRuleCondition<ConditionKind.RoomMemberCount> {
|
||||
is: AnyMemberCountCondition;
|
||||
}
|
||||
|
||||
export interface ISenderNotificationPermissionCondition
|
||||
extends IPushRuleCondition<ConditionKind.SenderNotificationPermission> {
|
||||
key: string;
|
||||
}
|
||||
|
||||
export type PushRuleCondition = IPushRuleCondition<string>
|
||||
| IEventMatchCondition
|
||||
| IContainsDisplayNameCondition
|
||||
| IRoomMemberCountCondition
|
||||
| ISenderNotificationPermissionCondition;
|
||||
|
||||
export enum PushRuleKind {
|
||||
Override = "override",
|
||||
ContentSpecific = "content",
|
||||
RoomSpecific = "room",
|
||||
SenderSpecific = "sender",
|
||||
Underride = "underride",
|
||||
}
|
||||
|
||||
export enum RuleId {
|
||||
Master = ".m.rule.master",
|
||||
ContainsDisplayName = ".m.rule.contains_display_name",
|
||||
ContainsUserName = ".m.rule.contains_user_name",
|
||||
AtRoomNotification = ".m.rule.roomnotif",
|
||||
DM = ".m.rule.room_one_to_one",
|
||||
EncryptedDM = ".m.rule.encrypted_room_one_to_one",
|
||||
Message = ".m.rule.message",
|
||||
EncryptedMessage = ".m.rule.encrypted",
|
||||
InviteToSelf = ".m.rule.invite_for_me",
|
||||
MemberEvent = ".m.rule.member_event",
|
||||
IncomingCall = ".m.rule.call",
|
||||
SuppressNotices = ".m.rule.suppress_notices",
|
||||
Tombstone = ".m.rule.tombstone",
|
||||
}
|
||||
|
||||
export type PushRuleSet = {
|
||||
[k in PushRuleKind]?: IPushRule[];
|
||||
};
|
||||
|
||||
export interface IPushRule {
|
||||
actions: PushRuleAction[];
|
||||
conditions?: PushRuleCondition[];
|
||||
default: boolean;
|
||||
enabled: boolean;
|
||||
pattern?: string;
|
||||
rule_id: RuleId | string;
|
||||
}
|
||||
|
||||
export interface IAnnotatedPushRule extends IPushRule {
|
||||
kind: PushRuleKind;
|
||||
}
|
||||
|
||||
export interface IPushRules {
|
||||
global: PushRuleSet;
|
||||
device?: PushRuleSet;
|
||||
}
|
||||
|
||||
export interface IPusher {
|
||||
app_display_name: string;
|
||||
app_id: string;
|
||||
data: {
|
||||
format?: string; // TODO: Types
|
||||
url?: string; // TODO: Required if kind==http
|
||||
brand?: string; // TODO: For email notifications only?
|
||||
};
|
||||
device_display_name: string;
|
||||
kind: string; // TODO: Types
|
||||
lang: string;
|
||||
profile_tag?: string;
|
||||
pushkey: string;
|
||||
}
|
||||
|
||||
export interface IPusherRequest extends IPusher {
|
||||
append?: boolean;
|
||||
}
|
||||
|
||||
/* eslint-enable camelcase */
|
@@ -144,6 +144,28 @@ export const UNSTABLE_MSC3089_LEAF = new UnstableValue("m.leaf", "org.matrix.msc
|
||||
*/
|
||||
export const UNSTABLE_MSC3089_BRANCH = new UnstableValue("m.branch", "org.matrix.msc3089.branch");
|
||||
|
||||
/**
|
||||
* Functional members type for declaring a purpose of room members (e.g. helpful bots).
|
||||
* Note that this reference is UNSTABLE and subject to breaking changes, including its
|
||||
* eventual removal.
|
||||
*
|
||||
* Schema (TypeScript):
|
||||
* {
|
||||
* service_members?: string[]
|
||||
* }
|
||||
*
|
||||
* Example:
|
||||
* {
|
||||
* "service_members": [
|
||||
* "@helperbot:localhost",
|
||||
* "@reminderbot:alice.tdl"
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
|
||||
"io.element.functional_members",
|
||||
"io.element.functional_members");
|
||||
|
||||
export interface IEncryptedFile {
|
||||
url: string;
|
||||
mimetype?: string;
|
||||
|
6
src/@types/global.d.ts
vendored
6
src/@types/global.d.ts
vendored
@@ -20,6 +20,12 @@ import "@matrix-org/olm";
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
// use `number` as the return type in all cases for global.set{Interval,Timeout},
|
||||
// so we don't accidentally use the methods on NodeJS.Timeout - they only exist in a subset of environments.
|
||||
// The overload for clear{Interval,Timeout} is resolved as expected.
|
||||
function setInterval(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
|
||||
function setTimeout(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
|
||||
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
localStorage: Storage;
|
||||
|
@@ -39,3 +39,38 @@ export enum Preset {
|
||||
}
|
||||
|
||||
export type ResizeMethod = "crop" | "scale";
|
||||
|
||||
// TODO move to http-api after TSification
|
||||
export interface IAbortablePromise<T> extends Promise<T> {
|
||||
abort(): void;
|
||||
}
|
||||
|
||||
export type IdServerUnbindResult = "no-support" | "success";
|
||||
|
||||
// Knock and private are reserved keywords which are not yet implemented.
|
||||
export enum JoinRule {
|
||||
Public = "public",
|
||||
Invite = "invite",
|
||||
/**
|
||||
* @deprecated Reserved keyword. Should not be used. Not yet implemented.
|
||||
*/
|
||||
Private = "private",
|
||||
Knock = "knock", // MSC2403 - only valid inside experimental room versions at this time.
|
||||
Restricted = "restricted", // MSC3083 - only valid inside experimental room versions at this time.
|
||||
}
|
||||
|
||||
export enum RestrictedAllowType {
|
||||
RoomMembership = "m.room_membership", // MSC3083 - only valid inside experimental room versions at this time.
|
||||
}
|
||||
|
||||
export enum GuestAccess {
|
||||
CanJoin = "can_join",
|
||||
Forbidden = "forbidden",
|
||||
}
|
||||
|
||||
export enum HistoryVisibility {
|
||||
Invited = "invited",
|
||||
Joined = "joined",
|
||||
Shared = "shared",
|
||||
WorldReadable = "world_readable",
|
||||
}
|
||||
|
@@ -15,9 +15,12 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { Callback } from "../client";
|
||||
import { IContent } from "../models/event";
|
||||
import { Preset, Visibility } from "./partials";
|
||||
import { SearchKey } from "./search";
|
||||
import { IRoomEventFilter } from "../filter";
|
||||
|
||||
// allow camelcase as these are things go onto the wire
|
||||
// allow camelcase as these are things that go onto the wire
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
export interface IJoinRoomOpts {
|
||||
@@ -63,12 +66,12 @@ export interface IGuestAccessOpts {
|
||||
}
|
||||
|
||||
export interface ISearchOpts {
|
||||
keys?: string[];
|
||||
keys?: SearchKey[];
|
||||
query: string;
|
||||
}
|
||||
|
||||
export interface IEventSearchOpts {
|
||||
filter: any; // TODO: Types
|
||||
filter?: IRoomEventFilter;
|
||||
term: string;
|
||||
}
|
||||
|
||||
@@ -82,7 +85,7 @@ export interface IInvite3PID {
|
||||
export interface ICreateRoomStateEvent {
|
||||
type: string;
|
||||
state_key?: string; // defaults to an empty string
|
||||
content: object;
|
||||
content: IContent;
|
||||
}
|
||||
|
||||
export interface ICreateRoomOpts {
|
||||
@@ -104,9 +107,11 @@ export interface IRoomDirectoryOptions {
|
||||
server?: string;
|
||||
limit?: number;
|
||||
since?: string;
|
||||
|
||||
// TODO: Proper types
|
||||
filter?: any & {generic_search_term: string};
|
||||
filter?: {
|
||||
generic_search_term: string;
|
||||
};
|
||||
include_all_networks?: boolean;
|
||||
third_party_instance_id?: string;
|
||||
}
|
||||
|
||||
export interface IUploadOpts {
|
||||
@@ -119,4 +124,19 @@ export interface IUploadOpts {
|
||||
progressHandler?: (state: {loaded: number, total: number}) => void;
|
||||
}
|
||||
|
||||
export interface IAddThreePidOnlyBody {
|
||||
auth?: {
|
||||
type: string;
|
||||
session?: string;
|
||||
};
|
||||
client_secret: string;
|
||||
sid: string;
|
||||
}
|
||||
|
||||
export interface IBindThreePidBody {
|
||||
client_secret: string;
|
||||
id_server: string;
|
||||
id_access_token: string;
|
||||
sid: string;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
118
src/@types/search.ts
Normal file
118
src/@types/search.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
Copyright 2021 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.
|
||||
*/
|
||||
|
||||
// Types relating to the /search API
|
||||
|
||||
import { IRoomEvent, IStateEvent } from "../sync-accumulator";
|
||||
import { IRoomEventFilter } from "../filter";
|
||||
import { SearchResult } from "../models/search-result";
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface IEventWithRoomId extends IRoomEvent {
|
||||
room_id: string;
|
||||
}
|
||||
|
||||
export interface IStateEventWithRoomId extends IStateEvent {
|
||||
room_id: string;
|
||||
}
|
||||
|
||||
export interface IMatrixProfile {
|
||||
avatar_url?: string;
|
||||
displayname?: string;
|
||||
}
|
||||
|
||||
export interface IResultContext {
|
||||
events_before: IEventWithRoomId[];
|
||||
events_after: IEventWithRoomId[];
|
||||
profile_info: Record<string, IMatrixProfile>;
|
||||
start?: string;
|
||||
end?: string;
|
||||
}
|
||||
|
||||
export interface ISearchResult {
|
||||
rank: number;
|
||||
result: IEventWithRoomId;
|
||||
context: IResultContext;
|
||||
}
|
||||
|
||||
enum GroupKey {
|
||||
RoomId = "room_id",
|
||||
Sender = "sender",
|
||||
}
|
||||
|
||||
export interface IResultRoomEvents {
|
||||
count: number;
|
||||
highlights: string[];
|
||||
results: ISearchResult[];
|
||||
state?: { [roomId: string]: IStateEventWithRoomId[] };
|
||||
groups?: {
|
||||
[groupKey in GroupKey]: {
|
||||
[value: string]: {
|
||||
next_batch?: string;
|
||||
order: number;
|
||||
results: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
next_batch?: string;
|
||||
}
|
||||
|
||||
interface IResultCategories {
|
||||
room_events: IResultRoomEvents;
|
||||
}
|
||||
|
||||
export type SearchKey = "content.body" | "content.name" | "content.topic";
|
||||
|
||||
export enum SearchOrderBy {
|
||||
Recent = "recent",
|
||||
Rank = "rank",
|
||||
}
|
||||
|
||||
export interface ISearchRequestBody {
|
||||
search_categories: {
|
||||
room_events: {
|
||||
search_term: string;
|
||||
keys?: SearchKey[];
|
||||
filter?: IRoomEventFilter;
|
||||
order_by?: SearchOrderBy;
|
||||
event_context?: {
|
||||
before_limit?: number;
|
||||
after_limit?: number;
|
||||
include_profile?: boolean;
|
||||
};
|
||||
include_state?: boolean;
|
||||
groupings?: {
|
||||
group_by: {
|
||||
key: GroupKey;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ISearchResponse {
|
||||
search_categories: IResultCategories;
|
||||
}
|
||||
|
||||
export interface ISearchResults {
|
||||
_query?: ISearchRequestBody;
|
||||
results: SearchResult[];
|
||||
highlights: string[];
|
||||
count?: number;
|
||||
next_batch?: string;
|
||||
pendingRequest?: Promise<ISearchResults>;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
@@ -19,3 +19,7 @@ export interface ISignatures {
|
||||
[keyId: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ISigned {
|
||||
signatures?: ISignatures;
|
||||
}
|
||||
|
40
src/@types/spaces.ts
Normal file
40
src/@types/spaces.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright 2021 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 { IPublicRoomsChunkRoom } from "../client";
|
||||
|
||||
// Types relating to Rooms of type `m.space` and related APIs
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface ISpaceSummaryRoom extends IPublicRoomsChunkRoom {
|
||||
num_refs: number;
|
||||
room_type: string;
|
||||
}
|
||||
|
||||
export interface ISpaceSummaryEvent {
|
||||
room_id: string;
|
||||
event_id: string;
|
||||
origin_server_ts: number;
|
||||
type: string;
|
||||
state_key: string;
|
||||
content: {
|
||||
order?: string;
|
||||
suggested?: boolean;
|
||||
auto_join?: boolean;
|
||||
via?: string[];
|
||||
};
|
||||
}
|
||||
/* eslint-enable camelcase */
|
40
src/@types/synapse.ts
Normal file
40
src/@types/synapse.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright 2021 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 { IdServerUnbindResult } from "./partials";
|
||||
|
||||
// Types relating to Synapse Admin APIs
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface ISynapseAdminWhoisResponse {
|
||||
user_id: string;
|
||||
devices: {
|
||||
[deviceId: string]: {
|
||||
sessions: {
|
||||
connections: {
|
||||
ip: string;
|
||||
last_seen: number; // millis since epoch
|
||||
user_agent: string;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ISynapseAdminDeactivateResponse {
|
||||
id_server_unbind_result: IdServerUnbindResult;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
28
src/@types/threepids.ts
Normal file
28
src/@types/threepids.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export enum ThreepidMedium {
|
||||
Email = "email",
|
||||
Phone = "msisdn",
|
||||
}
|
||||
|
||||
// TODO: Are these types universal, or specific to just /account/3pid?
|
||||
export interface IThreepid {
|
||||
medium: ThreepidMedium;
|
||||
address: string;
|
||||
validated_at: number; // eslint-disable-line camelcase
|
||||
added_at: number; // eslint-disable-line camelcase
|
||||
}
|
1067
src/client.ts
1067
src/client.ts
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,8 @@ limitations under the License.
|
||||
|
||||
/** @module ContentHelpers */
|
||||
|
||||
import { MsgType } from "./@types/event";
|
||||
|
||||
/**
|
||||
* Generates the content for a HTML Message event
|
||||
* @param {string} body the plaintext body of the message
|
||||
@@ -25,7 +27,7 @@ limitations under the License.
|
||||
*/
|
||||
export function makeHtmlMessage(body: string, htmlBody: string) {
|
||||
return {
|
||||
msgtype: "m.text",
|
||||
msgtype: MsgType.Text,
|
||||
format: "org.matrix.custom.html",
|
||||
body: body,
|
||||
formatted_body: htmlBody,
|
||||
@@ -40,7 +42,7 @@ export function makeHtmlMessage(body: string, htmlBody: string) {
|
||||
*/
|
||||
export function makeHtmlNotice(body: string, htmlBody: string) {
|
||||
return {
|
||||
msgtype: "m.notice",
|
||||
msgtype: MsgType.Notice,
|
||||
format: "org.matrix.custom.html",
|
||||
body: body,
|
||||
formatted_body: htmlBody,
|
||||
@@ -55,7 +57,7 @@ export function makeHtmlNotice(body: string, htmlBody: string) {
|
||||
*/
|
||||
export function makeHtmlEmote(body: string, htmlBody: string) {
|
||||
return {
|
||||
msgtype: "m.emote",
|
||||
msgtype: MsgType.Emote,
|
||||
format: "org.matrix.custom.html",
|
||||
body: body,
|
||||
formatted_body: htmlBody,
|
||||
@@ -69,7 +71,7 @@ export function makeHtmlEmote(body: string, htmlBody: string) {
|
||||
*/
|
||||
export function makeTextMessage(body: string) {
|
||||
return {
|
||||
msgtype: "m.text",
|
||||
msgtype: MsgType.Text,
|
||||
body: body,
|
||||
};
|
||||
}
|
||||
@@ -81,7 +83,7 @@ export function makeTextMessage(body: string) {
|
||||
*/
|
||||
export function makeNotice(body: string) {
|
||||
return {
|
||||
msgtype: "m.notice",
|
||||
msgtype: MsgType.Notice,
|
||||
body: body,
|
||||
};
|
||||
}
|
||||
@@ -93,7 +95,7 @@ export function makeNotice(body: string) {
|
||||
*/
|
||||
export function makeEmoteMessage(body: string) {
|
||||
return {
|
||||
msgtype: "m.emote",
|
||||
msgtype: MsgType.Emote,
|
||||
body: body,
|
||||
};
|
||||
}
|
||||
|
@@ -102,7 +102,7 @@ export class DeviceList extends EventEmitter {
|
||||
// The time the save is scheduled for
|
||||
private savePromiseTime: number = null;
|
||||
// The timer used to delay the save
|
||||
private saveTimer: NodeJS.Timeout = null;
|
||||
private saveTimer: number = null;
|
||||
// True if we have fetched data from the server or loaded a non-empty
|
||||
// set of device data from the store
|
||||
private hasFetched: boolean = null;
|
||||
|
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
Copyright 2021 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 { logger } from "../logger";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { EventEmitter } from "events";
|
||||
@@ -9,10 +25,10 @@ import {
|
||||
CrossSigningKeys,
|
||||
ICrossSigningKey,
|
||||
ICryptoCallbacks,
|
||||
ISecretStorageKeyInfo,
|
||||
ISignedKey,
|
||||
KeySignatures,
|
||||
} from "../matrix";
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
|
||||
interface ICrossSigningKeys {
|
||||
@@ -109,8 +125,8 @@ export class EncryptionSetupBuilder {
|
||||
* @param {Object} content
|
||||
* @return {Promise}
|
||||
*/
|
||||
public setAccountData(type: string, content: object): Promise<void> {
|
||||
return this.accountDataClientAdapter.setAccountData(type, content);
|
||||
public async setAccountData(type: string, content: object): Promise<void> {
|
||||
await this.accountDataClientAdapter.setAccountData(type, content);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,7 +262,7 @@ export class EncryptionSetupOperation {
|
||||
* implementing the methods related to account data in MatrixClient
|
||||
*/
|
||||
class AccountDataClientAdapter extends EventEmitter {
|
||||
public readonly values = new Map<string, object>();
|
||||
public readonly values = new Map<string, MatrixEvent>();
|
||||
|
||||
/**
|
||||
* @param {Object.<String, MatrixEvent>} existingValues existing account data
|
||||
@@ -259,7 +275,7 @@ class AccountDataClientAdapter extends EventEmitter {
|
||||
* @param {String} type
|
||||
* @return {Promise<Object>} the content of the account data
|
||||
*/
|
||||
public getAccountDataFromServer(type: string): Promise<object> {
|
||||
public getAccountDataFromServer(type: string): Promise<any> {
|
||||
return Promise.resolve(this.getAccountData(type));
|
||||
}
|
||||
|
||||
@@ -267,7 +283,7 @@ class AccountDataClientAdapter extends EventEmitter {
|
||||
* @param {String} type
|
||||
* @return {Object} the content of the account data
|
||||
*/
|
||||
public getAccountData(type: string): object {
|
||||
public getAccountData(type: string): MatrixEvent {
|
||||
const modifiedValue = this.values.get(type);
|
||||
if (modifiedValue) {
|
||||
return modifiedValue;
|
||||
@@ -284,7 +300,7 @@ class AccountDataClientAdapter extends EventEmitter {
|
||||
* @param {Object} content
|
||||
* @return {Promise}
|
||||
*/
|
||||
public setAccountData(type: string, content: object): Promise<void> {
|
||||
public setAccountData(type: string, content: any): Promise<{}> {
|
||||
const lastEvent = this.values.get(type);
|
||||
this.values.set(type, content);
|
||||
// ensure accountData is emitted on the next tick,
|
||||
@@ -293,6 +309,7 @@ class AccountDataClientAdapter extends EventEmitter {
|
||||
return Promise.resolve().then(() => {
|
||||
const event = new MatrixEvent({ type, content });
|
||||
this.emit("accountData", event, lastEvent);
|
||||
return {};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -337,7 +354,7 @@ class SSSSCryptoCallbacks {
|
||||
constructor(private readonly delegateCryptoCallbacks: ICryptoCallbacks) {}
|
||||
|
||||
public async getSecretStorageKey(
|
||||
{ keys }: { keys: Record<string, object> },
|
||||
{ keys }: { keys: Record<string, ISecretStorageKeyInfo> },
|
||||
name: string,
|
||||
): Promise<[string, Uint8Array]> {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
|
@@ -78,7 +78,7 @@ export enum RoomKeyRequestState {
|
||||
export class OutgoingRoomKeyRequestManager {
|
||||
// handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null
|
||||
// if the callback has been set, or if it is still running.
|
||||
private sendOutgoingRoomKeyRequestsTimer: NodeJS.Timeout = null;
|
||||
private sendOutgoingRoomKeyRequestsTimer: number = null;
|
||||
|
||||
// sanity check to ensure that we don't end up with two concurrent runs
|
||||
// of sendOutgoingRoomKeyRequests
|
||||
@@ -366,7 +366,7 @@ export class OutgoingRoomKeyRequestManager {
|
||||
});
|
||||
};
|
||||
|
||||
this.sendOutgoingRoomKeyRequestsTimer = global.setTimeout(
|
||||
this.sendOutgoingRoomKeyRequestsTimer = setTimeout(
|
||||
startSendingOutgoingRoomKeyRequests,
|
||||
SEND_KEY_REQUESTS_DELAY_MS,
|
||||
);
|
||||
|
@@ -24,10 +24,10 @@ import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { CryptoStore } from "../client";
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IRoomEncryption {
|
||||
export interface IRoomEncryption {
|
||||
algorithm: string;
|
||||
rotation_period_ms: number;
|
||||
rotation_period_msgs: number;
|
||||
rotation_period_ms?: number;
|
||||
rotation_period_msgs?: number;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,61 +14,95 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { logger } from '../logger';
|
||||
import * as olmlib from './olmlib';
|
||||
import { randomString } from '../randomstring';
|
||||
import { encryptAES, decryptAES } from './aes';
|
||||
import { encryptAES, decryptAES, IEncryptedPayload, calculateKeyCheck } from './aes';
|
||||
import { encodeBase64 } from "./olmlib";
|
||||
import { ICryptoCallbacks, MatrixClient, MatrixEvent } from '../matrix';
|
||||
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api';
|
||||
import { EventEmitter } from 'stream';
|
||||
|
||||
export const SECRET_STORAGE_ALGORITHM_V1_AES
|
||||
= "m.secret_storage.v1.aes-hmac-sha2";
|
||||
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
|
||||
|
||||
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
|
||||
// Some of the key functions use a tuple and some use an object...
|
||||
export type SecretStorageKeyTuple = [keyId: string, keyInfo: ISecretStorageKeyInfo];
|
||||
export type SecretStorageKeyObject = {keyId: string, keyInfo: ISecretStorageKeyInfo};
|
||||
|
||||
export interface ISecretRequest {
|
||||
requestId: string;
|
||||
promise: Promise<string>;
|
||||
cancel: (reason: string) => void;
|
||||
}
|
||||
|
||||
export interface IAccountDataClient extends EventEmitter {
|
||||
// Subset of MatrixClient (which also uses any for the event content)
|
||||
getAccountDataFromServer: (eventType: string) => Promise<Record<string, any>>;
|
||||
getAccountData: (eventType: string) => MatrixEvent;
|
||||
setAccountData: (eventType: string, content: any) => Promise<{}>;
|
||||
}
|
||||
|
||||
interface ISecretRequestInternal {
|
||||
name: string;
|
||||
devices: string[];
|
||||
resolve: (reason: string) => void;
|
||||
reject: (error: Error) => void;
|
||||
}
|
||||
|
||||
interface IDecryptors {
|
||||
encrypt: (plaintext: string) => Promise<IEncryptedPayload>;
|
||||
decrypt: (ciphertext: IEncryptedPayload) => Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements Secure Secret Storage and Sharing (MSC1946)
|
||||
* @module crypto/SecretStorage
|
||||
*/
|
||||
export class SecretStorage extends EventEmitter {
|
||||
constructor(baseApis, cryptoCallbacks) {
|
||||
super();
|
||||
this._baseApis = baseApis;
|
||||
this._cryptoCallbacks = cryptoCallbacks;
|
||||
this._requests = {};
|
||||
this._incomingRequests = {};
|
||||
}
|
||||
export class SecretStorage {
|
||||
private requests = new Map<string, ISecretRequestInternal>();
|
||||
|
||||
async getDefaultKeyId() {
|
||||
const defaultKey = await this._baseApis.getAccountDataFromServer(
|
||||
// In it's pure javascript days, this was relying on some proper Javascript-style
|
||||
// type-abuse where sometimes we'd pass in a fake client object with just the account
|
||||
// data methods implemented, which is all this class needs unless you use the secret
|
||||
// sharing code, so it was fine. As a low-touch TypeScript migration, this now has
|
||||
// an extra, optional param for a real matrix client, so you can not pass it as long
|
||||
// as you don't request any secrets.
|
||||
// A better solution would probably be to split this class up into secret storage and
|
||||
// secret sharing which are really two separate things, even though they share an MSC.
|
||||
constructor(
|
||||
private readonly accountDataAdapter: IAccountDataClient,
|
||||
private readonly cryptoCallbacks: ICryptoCallbacks,
|
||||
private readonly baseApis?: MatrixClient,
|
||||
) {}
|
||||
|
||||
public async getDefaultKeyId(): Promise<string> {
|
||||
const defaultKey = await this.accountDataAdapter.getAccountDataFromServer(
|
||||
'm.secret_storage.default_key',
|
||||
);
|
||||
if (!defaultKey) return null;
|
||||
return defaultKey.key;
|
||||
}
|
||||
|
||||
setDefaultKeyId(keyId) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const listener = (ev) => {
|
||||
public setDefaultKeyId(keyId: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const listener = (ev: MatrixEvent): void => {
|
||||
if (
|
||||
ev.getType() === 'm.secret_storage.default_key' &&
|
||||
ev.getContent().key === keyId
|
||||
) {
|
||||
this._baseApis.removeListener('accountData', listener);
|
||||
this.accountDataAdapter.removeListener('accountData', listener);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
this._baseApis.on('accountData', listener);
|
||||
this.accountDataAdapter.on('accountData', listener);
|
||||
|
||||
try {
|
||||
await this._baseApis.setAccountData(
|
||||
'm.secret_storage.default_key',
|
||||
{ key: keyId },
|
||||
);
|
||||
} catch (e) {
|
||||
this._baseApis.removeListener('accountData', listener);
|
||||
this.accountDataAdapter.setAccountData(
|
||||
'm.secret_storage.default_key',
|
||||
{ key: keyId },
|
||||
).catch(e => {
|
||||
this.accountDataAdapter.removeListener('accountData', listener);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,10 +119,14 @@ export class SecretStorage extends EventEmitter {
|
||||
* keyId: {string} the ID of the key
|
||||
* keyInfo: {object} details about the key (iv, mac, passphrase)
|
||||
*/
|
||||
async addKey(algorithm, opts, keyId) {
|
||||
const keyInfo = { algorithm };
|
||||
public async addKey(
|
||||
algorithm: string,
|
||||
opts: IAddSecretStorageKeyOpts,
|
||||
keyId?: string,
|
||||
): Promise<SecretStorageKeyObject> {
|
||||
const keyInfo = { algorithm } as ISecretStorageKeyInfo;
|
||||
|
||||
if (!opts) opts = {};
|
||||
if (!opts) opts = {} as IAddSecretStorageKeyOpts;
|
||||
|
||||
if (opts.name) {
|
||||
keyInfo.name = opts.name;
|
||||
@@ -99,25 +137,25 @@ export class SecretStorage extends EventEmitter {
|
||||
keyInfo.passphrase = opts.passphrase;
|
||||
}
|
||||
if (opts.key) {
|
||||
const { iv, mac } = await SecretStorage._calculateKeyCheck(opts.key);
|
||||
const { iv, mac } = await calculateKeyCheck(opts.key);
|
||||
keyInfo.iv = iv;
|
||||
keyInfo.mac = mac;
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown key algorithm ${opts.algorithm}`);
|
||||
throw new Error(`Unknown key algorithm ${algorithm}`);
|
||||
}
|
||||
|
||||
if (!keyId) {
|
||||
do {
|
||||
keyId = randomString(32);
|
||||
} while (
|
||||
await this._baseApis.getAccountDataFromServer(
|
||||
await this.accountDataAdapter.getAccountDataFromServer(
|
||||
`m.secret_storage.key.${keyId}`,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await this._baseApis.setAccountData(
|
||||
await this.accountDataAdapter.setAccountData(
|
||||
`m.secret_storage.key.${keyId}`, keyInfo,
|
||||
);
|
||||
|
||||
@@ -134,8 +172,9 @@ export class SecretStorage extends EventEmitter {
|
||||
* for. Defaults to the default key ID if not provided.
|
||||
* @returns {Array?} If the key was found, the return value is an array of
|
||||
* the form [keyId, keyInfo]. Otherwise, null is returned.
|
||||
* XXX: why is this an array when addKey returns an object?
|
||||
*/
|
||||
async getKey(keyId) {
|
||||
public async getKey(keyId: string): Promise<SecretStorageKeyTuple | null> {
|
||||
if (!keyId) {
|
||||
keyId = await this.getDefaultKeyId();
|
||||
}
|
||||
@@ -143,9 +182,9 @@ export class SecretStorage extends EventEmitter {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
) as ISecretStorageKeyInfo;
|
||||
return keyInfo ? [keyId, keyInfo] : null;
|
||||
}
|
||||
|
||||
@@ -156,8 +195,8 @@ export class SecretStorage extends EventEmitter {
|
||||
* for. Defaults to the default key ID if not provided.
|
||||
* @return {boolean} Whether we have the key.
|
||||
*/
|
||||
async hasKey(keyId) {
|
||||
return !!(await this.getKey(keyId));
|
||||
public async hasKey(keyId?: string): Promise<boolean> {
|
||||
return Boolean(await this.getKey(keyId));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,10 +207,10 @@ export class SecretStorage extends EventEmitter {
|
||||
*
|
||||
* @return {boolean} whether or not the key matches
|
||||
*/
|
||||
async checkKey(key, info) {
|
||||
public async checkKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise<boolean> {
|
||||
if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
if (info.mac) {
|
||||
const { mac } = await SecretStorage._calculateKeyCheck(key, info.iv);
|
||||
const { mac } = await calculateKeyCheck(key, info.iv);
|
||||
return info.mac.replace(/=+$/g, '') === mac.replace(/=+$/g, '');
|
||||
} else {
|
||||
// if we have no information, we have to assume the key is right
|
||||
@@ -182,10 +221,6 @@ export class SecretStorage extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
static async _calculateKeyCheck(key, iv) {
|
||||
return await encryptAES(ZERO_STR, key, "", iv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store an encrypted secret on the server
|
||||
*
|
||||
@@ -194,7 +229,7 @@ export class SecretStorage extends EventEmitter {
|
||||
* @param {Array} keys The IDs of the keys to use to encrypt the secret
|
||||
* or null/undefined to use the default key.
|
||||
*/
|
||||
async store(name, secret, keys) {
|
||||
public async store(name: string, secret: string, keys?: string[]): Promise<void> {
|
||||
const encrypted = {};
|
||||
|
||||
if (!keys) {
|
||||
@@ -211,9 +246,9 @@ export class SecretStorage extends EventEmitter {
|
||||
|
||||
for (const keyId of keys) {
|
||||
// get key information from key storage
|
||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
) as ISecretStorageKeyInfo;
|
||||
if (!keyInfo) {
|
||||
throw new Error("Unknown key: " + keyId);
|
||||
}
|
||||
@@ -221,7 +256,7 @@ export class SecretStorage extends EventEmitter {
|
||||
// encrypt secret, based on the algorithm
|
||||
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
const keys = { [keyId]: keyInfo };
|
||||
const [, encryption] = await this._getSecretStorageKey(keys, name);
|
||||
const [, encryption] = await this.getSecretStorageKey(keys, name);
|
||||
encrypted[keyId] = await encryption.encrypt(secret);
|
||||
} else {
|
||||
logger.warn("unknown algorithm for secret storage key " + keyId
|
||||
@@ -231,34 +266,7 @@ export class SecretStorage extends EventEmitter {
|
||||
}
|
||||
|
||||
// save encrypted secret
|
||||
await this._baseApis.setAccountData(name, { encrypted });
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary method to fix up existing accounts where secrets
|
||||
* are incorrectly stored without the 'encrypted' level
|
||||
*
|
||||
* @param {string} name The name of the secret
|
||||
* @param {object} secretInfo The account data object
|
||||
* @returns {object} The fixed object or null if no fix was performed
|
||||
*/
|
||||
async _fixupStoredSecret(name, secretInfo) {
|
||||
// We assume the secret was only stored passthrough for 1
|
||||
// key - this was all the broken code supported.
|
||||
const keys = Object.keys(secretInfo);
|
||||
if (
|
||||
keys.length === 1 && keys[0] !== 'encrypted' &&
|
||||
secretInfo[keys[0]].passthrough
|
||||
) {
|
||||
const hasKey = await this.hasKey(keys[0]);
|
||||
if (hasKey) {
|
||||
logger.log("Fixing up passthrough secret: " + name);
|
||||
await this.storePassthrough(name, keys[0]);
|
||||
const newData = await this._baseApis.getAccountDataFromServer(name);
|
||||
return newData;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
await this.accountDataAdapter.setAccountData(name, { encrypted });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,24 +276,20 @@ export class SecretStorage extends EventEmitter {
|
||||
*
|
||||
* @return {string} the contents of the secret
|
||||
*/
|
||||
async get(name) {
|
||||
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
|
||||
public async get(name: string): Promise<string> {
|
||||
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
|
||||
if (!secretInfo) {
|
||||
return;
|
||||
}
|
||||
if (!secretInfo.encrypted) {
|
||||
// try to fix it up
|
||||
secretInfo = await this._fixupStoredSecret(name, secretInfo);
|
||||
if (!secretInfo || !secretInfo.encrypted) {
|
||||
throw new Error("Content is not encrypted!");
|
||||
}
|
||||
throw new Error("Content is not encrypted!");
|
||||
}
|
||||
|
||||
// get possible keys to decrypt
|
||||
const keys = {};
|
||||
for (const keyId of Object.keys(secretInfo.encrypted)) {
|
||||
// get key information from key storage
|
||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
const encInfo = secretInfo.encrypted[keyId];
|
||||
@@ -306,7 +310,7 @@ export class SecretStorage extends EventEmitter {
|
||||
let decryption;
|
||||
try {
|
||||
// fetch private key from app
|
||||
[keyId, decryption] = await this._getSecretStorageKey(keys, name);
|
||||
[keyId, decryption] = await this.getSecretStorageKey(keys, name);
|
||||
|
||||
const encInfo = secretInfo.encrypted[keyId];
|
||||
|
||||
@@ -331,16 +335,12 @@ export class SecretStorage extends EventEmitter {
|
||||
* with, or null if it is not present or not encrypted with a trusted
|
||||
* key
|
||||
*/
|
||||
async isStored(name, checkKey) {
|
||||
public async isStored(name: string, checkKey: boolean): Promise<Record<string, ISecretStorageKeyInfo>> {
|
||||
// check if secret exists
|
||||
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
|
||||
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
|
||||
if (!secretInfo) return null;
|
||||
if (!secretInfo.encrypted) {
|
||||
// try to fix it up
|
||||
secretInfo = await this._fixupStoredSecret(name, secretInfo);
|
||||
if (!secretInfo || !secretInfo.encrypted) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (checkKey === undefined) checkKey = true;
|
||||
@@ -350,7 +350,7 @@ export class SecretStorage extends EventEmitter {
|
||||
// filter secret encryption keys with supported algorithm
|
||||
for (const keyId of Object.keys(secretInfo.encrypted)) {
|
||||
// get key information from key storage
|
||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
if (!keyInfo) continue;
|
||||
@@ -371,45 +371,48 @@ export class SecretStorage extends EventEmitter {
|
||||
*
|
||||
* @param {string} name the name of the secret to request
|
||||
* @param {string[]} devices the devices to request the secret from
|
||||
*
|
||||
* @return {string} the contents of the secret
|
||||
*/
|
||||
request(name, devices) {
|
||||
const requestId = this._baseApis.makeTxnId();
|
||||
public request(name: string, devices: string[]): ISecretRequest {
|
||||
const requestId = this.baseApis.makeTxnId();
|
||||
|
||||
const requestControl = this._requests[requestId] = {
|
||||
let resolve: (string) => void;
|
||||
let reject: (Error) => void;
|
||||
const promise = new Promise<string>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
this.requests.set(requestId, {
|
||||
name,
|
||||
devices,
|
||||
};
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
requestControl.resolve = resolve;
|
||||
requestControl.reject = reject;
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
const cancel = (reason) => {
|
||||
|
||||
const cancel = (reason: string) => {
|
||||
// send cancellation event
|
||||
const cancelData = {
|
||||
action: "request_cancellation",
|
||||
requesting_device_id: this._baseApis.deviceId,
|
||||
requesting_device_id: this.baseApis.deviceId,
|
||||
request_id: requestId,
|
||||
};
|
||||
const toDevice = {};
|
||||
for (const device of devices) {
|
||||
toDevice[device] = cancelData;
|
||||
}
|
||||
this._baseApis.sendToDevice("m.secret.request", {
|
||||
[this._baseApis.getUserId()]: toDevice,
|
||||
this.baseApis.sendToDevice("m.secret.request", {
|
||||
[this.baseApis.getUserId()]: toDevice,
|
||||
});
|
||||
|
||||
// and reject the promise so that anyone waiting on it will be
|
||||
// notified
|
||||
requestControl.reject(new Error(reason || "Cancelled"));
|
||||
reject(new Error(reason || "Cancelled"));
|
||||
};
|
||||
|
||||
// send request to devices
|
||||
const requestData = {
|
||||
name,
|
||||
action: "request",
|
||||
requesting_device_id: this._baseApis.deviceId,
|
||||
requesting_device_id: this.baseApis.deviceId,
|
||||
request_id: requestId,
|
||||
};
|
||||
const toDevice = {};
|
||||
@@ -417,21 +420,21 @@ export class SecretStorage extends EventEmitter {
|
||||
toDevice[device] = requestData;
|
||||
}
|
||||
logger.info(`Request secret ${name} from ${devices}, id ${requestId}`);
|
||||
this._baseApis.sendToDevice("m.secret.request", {
|
||||
[this._baseApis.getUserId()]: toDevice,
|
||||
this.baseApis.sendToDevice("m.secret.request", {
|
||||
[this.baseApis.getUserId()]: toDevice,
|
||||
});
|
||||
|
||||
return {
|
||||
request_id: requestId,
|
||||
requestId,
|
||||
promise,
|
||||
cancel,
|
||||
};
|
||||
}
|
||||
|
||||
async _onRequestReceived(event) {
|
||||
public async onRequestReceived(event: MatrixEvent): Promise<void> {
|
||||
const sender = event.getSender();
|
||||
const content = event.getContent();
|
||||
if (sender !== this._baseApis.getUserId()
|
||||
if (sender !== this.baseApis.getUserId()
|
||||
|| !(content.name && content.action
|
||||
&& content.requesting_device_id && content.request_id)) {
|
||||
// ignore requests from anyone else, for now
|
||||
@@ -440,34 +443,45 @@ export class SecretStorage extends EventEmitter {
|
||||
const deviceId = content.requesting_device_id;
|
||||
// check if it's a cancel
|
||||
if (content.action === "request_cancellation") {
|
||||
/*
|
||||
Looks like we intended to emit events when we got cancelations, but
|
||||
we never put anything in the _incomingRequests object, and the request
|
||||
itself doesn't use events anyway so if we were to wire up cancellations,
|
||||
they probably ought to use the same callback interface. I'm leaving them
|
||||
disabled for now while converting this file to typescript.
|
||||
if (this._incomingRequests[deviceId]
|
||||
&& this._incomingRequests[deviceId][content.request_id]) {
|
||||
logger.info("received request cancellation for secret (" + sender
|
||||
+ ", " + deviceId + ", " + content.request_id + ")");
|
||||
this._baseApis.emit("crypto.secrets.requestCancelled", {
|
||||
logger.info(
|
||||
"received request cancellation for secret (" + sender +
|
||||
", " + deviceId + ", " + content.request_id + ")",
|
||||
);
|
||||
this.baseApis.emit("crypto.secrets.requestCancelled", {
|
||||
user_id: sender,
|
||||
device_id: deviceId,
|
||||
request_id: content.request_id,
|
||||
});
|
||||
}
|
||||
*/
|
||||
} else if (content.action === "request") {
|
||||
if (deviceId === this._baseApis.deviceId) {
|
||||
if (deviceId === this.baseApis.deviceId) {
|
||||
// no point in trying to send ourself the secret
|
||||
return;
|
||||
}
|
||||
|
||||
// check if we have the secret
|
||||
logger.info("received request for secret (" + sender
|
||||
+ ", " + deviceId + ", " + content.request_id + ")");
|
||||
if (!this._cryptoCallbacks.onSecretRequested) {
|
||||
logger.info(
|
||||
"received request for secret (" + sender +
|
||||
", " + deviceId + ", " + content.request_id + ")",
|
||||
);
|
||||
if (!this.cryptoCallbacks.onSecretRequested) {
|
||||
return;
|
||||
}
|
||||
const secret = await this._cryptoCallbacks.onSecretRequested(
|
||||
const secret = await this.cryptoCallbacks.onSecretRequested(
|
||||
sender,
|
||||
deviceId,
|
||||
content.request_id,
|
||||
content.name,
|
||||
this._baseApis.checkDeviceTrust(sender, deviceId),
|
||||
this.baseApis.checkDeviceTrust(sender, deviceId),
|
||||
);
|
||||
if (secret) {
|
||||
logger.info(`Preparing ${content.name} secret for ${deviceId}`);
|
||||
@@ -480,25 +494,25 @@ export class SecretStorage extends EventEmitter {
|
||||
};
|
||||
const encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: this._baseApis.crypto.olmDevice.deviceCurve25519Key,
|
||||
sender_key: this.baseApis.crypto.olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
};
|
||||
await olmlib.ensureOlmSessionsForDevices(
|
||||
this._baseApis.crypto.olmDevice,
|
||||
this._baseApis,
|
||||
this.baseApis.crypto.olmDevice,
|
||||
this.baseApis,
|
||||
{
|
||||
[sender]: [
|
||||
this._baseApis.getStoredDevice(sender, deviceId),
|
||||
this.baseApis.getStoredDevice(sender, deviceId),
|
||||
],
|
||||
},
|
||||
);
|
||||
await olmlib.encryptMessageForDevice(
|
||||
encryptedContent.ciphertext,
|
||||
this._baseApis.getUserId(),
|
||||
this._baseApis.deviceId,
|
||||
this._baseApis.crypto.olmDevice,
|
||||
this.baseApis.getUserId(),
|
||||
this.baseApis.deviceId,
|
||||
this.baseApis.crypto.olmDevice,
|
||||
sender,
|
||||
this._baseApis.getStoredDevice(sender, deviceId),
|
||||
this.baseApis.getStoredDevice(sender, deviceId),
|
||||
payload,
|
||||
);
|
||||
const contentMap = {
|
||||
@@ -508,26 +522,26 @@ export class SecretStorage extends EventEmitter {
|
||||
};
|
||||
|
||||
logger.info(`Sending ${content.name} secret for ${deviceId}`);
|
||||
this._baseApis.sendToDevice("m.room.encrypted", contentMap);
|
||||
this.baseApis.sendToDevice("m.room.encrypted", contentMap);
|
||||
} else {
|
||||
logger.info(`Request denied for ${content.name} secret for ${deviceId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onSecretReceived(event) {
|
||||
if (event.getSender() !== this._baseApis.getUserId()) {
|
||||
public onSecretReceived(event: MatrixEvent): void {
|
||||
if (event.getSender() !== this.baseApis.getUserId()) {
|
||||
// we shouldn't be receiving secrets from anyone else, so ignore
|
||||
// because someone could be trying to send us bogus data
|
||||
return;
|
||||
}
|
||||
const content = event.getContent();
|
||||
logger.log("got secret share for request", content.request_id);
|
||||
const requestControl = this._requests[content.request_id];
|
||||
const requestControl = this.requests.get(content.request_id);
|
||||
if (requestControl) {
|
||||
// make sure that the device that sent it is one of the devices that
|
||||
// we requested from
|
||||
const deviceInfo = this._baseApis.crypto.deviceList.getDeviceByIdentityKey(
|
||||
const deviceInfo = this.baseApis.crypto.deviceList.getDeviceByIdentityKey(
|
||||
olmlib.OLM_ALGORITHM,
|
||||
event.getSenderKey(),
|
||||
);
|
||||
@@ -550,12 +564,15 @@ export class SecretStorage extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
async _getSecretStorageKey(keys, name) {
|
||||
if (!this._cryptoCallbacks.getSecretStorageKey) {
|
||||
private async getSecretStorageKey(
|
||||
keys: Record<string, ISecretStorageKeyInfo>,
|
||||
name: string,
|
||||
): Promise<[string, IDecryptors]> {
|
||||
if (!this.cryptoCallbacks.getSecretStorageKey) {
|
||||
throw new Error("No getSecretStorageKey callback supplied");
|
||||
}
|
||||
|
||||
const returned = await this._cryptoCallbacks.getSecretStorageKey({ keys }, name);
|
||||
const returned = await this.cryptoCallbacks.getSecretStorageKey({ keys }, name);
|
||||
|
||||
if (!returned) {
|
||||
throw new Error("getSecretStorageKey callback returned falsey");
|
||||
@@ -571,10 +588,10 @@ export class SecretStorage extends EventEmitter {
|
||||
|
||||
if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
const decryption = {
|
||||
encrypt: async function(secret) {
|
||||
encrypt: async function(secret: string): Promise<IEncryptedPayload> {
|
||||
return await encryptAES(secret, privateKey, name);
|
||||
},
|
||||
decrypt: async function(encInfo) {
|
||||
decrypt: async function(encInfo: IEncryptedPayload): Promise<string> {
|
||||
return await decryptAES(encInfo, privateKey, name);
|
||||
},
|
||||
};
|
@@ -261,3 +261,16 @@ export function decryptAES(data: IEncryptedPayload, key: Uint8Array, name: strin
|
||||
return subtleCrypto ? decryptBrowser(data, key, name) : decryptNode(data, key, name);
|
||||
}
|
||||
|
||||
// string of zeroes, for calculating the key check
|
||||
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
|
||||
|
||||
/** Calculate the MAC for checking the key.
|
||||
*
|
||||
* @param {Uint8Array} key the key to use
|
||||
* @param {string} [iv] The initialization vector as a base64-encoded string.
|
||||
* If omitted, a random initialization vector will be created.
|
||||
* @return {Promise<object>} An object that contains, `mac` and `iv` properties.
|
||||
*/
|
||||
export function calculateKeyCheck(key: Uint8Array, iv?: string): Promise<IEncryptedPayload> {
|
||||
return encryptAES(ZERO_STR, key, "", iv);
|
||||
}
|
||||
|
@@ -26,6 +26,7 @@ import { OlmDevice } from "../OlmDevice";
|
||||
import { MatrixEvent, RoomMember } from "../..";
|
||||
import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "..";
|
||||
import { DeviceInfo } from "../deviceinfo";
|
||||
import { IRoomEncryption } from "../RoomList";
|
||||
|
||||
/**
|
||||
* map of registered encryption algorithm classes. A map from string to {@link
|
||||
@@ -52,7 +53,7 @@ interface IParams {
|
||||
olmDevice: OlmDevice;
|
||||
baseApis: MatrixClient;
|
||||
roomId: string;
|
||||
config: object;
|
||||
config: IRoomEncryption & object;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1670,7 +1670,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
||||
*/
|
||||
public importRoomKey(session: IMegolmSessionData, opts: any = {}): Promise<void> {
|
||||
const extraSessionData: any = {};
|
||||
if (opts.untrusted) {
|
||||
if (opts.untrusted || session.untrusted) {
|
||||
extraSessionData.untrusted = true;
|
||||
}
|
||||
if (session["org.matrix.msc3061.shared_history"]) {
|
||||
|
@@ -16,7 +16,6 @@ limitations under the License.
|
||||
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
import { ISecretStorageKeyInfo } from "../matrix";
|
||||
|
||||
// TODO: Merge this with crypto.js once converted
|
||||
|
||||
@@ -107,14 +106,32 @@ export interface ICreateSecretStorageOpts {
|
||||
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
export interface ISecretStorageKeyInfo {
|
||||
name: string;
|
||||
algorithm: string;
|
||||
// technically the below are specific to AES keys. If we ever introduce another type,
|
||||
// we can split into separate interfaces.
|
||||
iv: string;
|
||||
mac: string;
|
||||
passphrase: IPassphraseInfo;
|
||||
}
|
||||
|
||||
export interface ISecretStorageKey {
|
||||
keyId: string;
|
||||
keyInfo: ISecretStorageKeyInfo;
|
||||
}
|
||||
|
||||
export interface IPassphraseInfo {
|
||||
algorithm: "m.pbkdf2";
|
||||
iterations: number;
|
||||
salt: string;
|
||||
bits: number;
|
||||
}
|
||||
|
||||
export interface IAddSecretStorageKeyOpts {
|
||||
// depends on algorithm
|
||||
// TODO: Types
|
||||
name: string;
|
||||
passphrase: IPassphraseInfo;
|
||||
key: Uint8Array;
|
||||
}
|
||||
|
||||
export interface IImportOpts {
|
||||
|
@@ -29,7 +29,10 @@ import { keyFromPassphrase } from './key_passphrase';
|
||||
import { sleep } from "../utils";
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { encodeRecoveryKey } from './recoverykey';
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
import { encryptAES, decryptAES, calculateKeyCheck } from './aes';
|
||||
import { getCrypto } from '../utils';
|
||||
import { ICurve25519AuthData, IAes256AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
|
||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||
|
||||
@@ -75,16 +78,29 @@ interface BackupAlgorithmClass {
|
||||
prepare(
|
||||
key: string | Uint8Array | null,
|
||||
): Promise<[Uint8Array, AuthData]>;
|
||||
|
||||
checkBackupVersion(info: IKeyBackupInfo): void;
|
||||
}
|
||||
|
||||
interface BackupAlgorithm {
|
||||
untrusted: boolean;
|
||||
encryptSession(data: Record<string, any>): Promise<any>;
|
||||
decryptSessions(ciphertexts: Record<string, any>): Promise<Record<string, any>[]>;
|
||||
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<Record<string, any>[]>;
|
||||
authData: AuthData;
|
||||
keyMatches(key: ArrayLike<number>): Promise<boolean>;
|
||||
free(): void;
|
||||
}
|
||||
|
||||
export interface IKeyBackup {
|
||||
rooms: {
|
||||
[roomId: string]: {
|
||||
sessions: {
|
||||
[sessionId: string]: IKeyBackupSession;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the key backup.
|
||||
*/
|
||||
@@ -102,6 +118,24 @@ export class BackupManager {
|
||||
return this.backupInfo && this.backupInfo.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a quick check to ensure that the backup info looks sane.
|
||||
*
|
||||
* Throws an error if a problem is detected.
|
||||
*
|
||||
* @param {IKeyBackupInfo} info the key backup info
|
||||
*/
|
||||
public static checkBackupVersion(info: IKeyBackupInfo): void {
|
||||
const Algorithm = algorithmsByName[info.algorithm];
|
||||
if (!Algorithm) {
|
||||
throw new Error("Unknown backup algorithm: " + info.algorithm);
|
||||
}
|
||||
if (!(typeof info.auth_data === "object")) {
|
||||
throw new Error("Invalid backup data returned");
|
||||
}
|
||||
return Algorithm.checkBackupVersion(info);
|
||||
}
|
||||
|
||||
public static async makeAlgorithm(info: IKeyBackupInfo, getKey: GetKey): Promise<BackupAlgorithm> {
|
||||
const Algorithm = algorithmsByName[info.algorithm];
|
||||
if (!Algorithm) {
|
||||
@@ -250,7 +284,7 @@ export class BackupManager {
|
||||
/**
|
||||
* Check if the given backup info is trusted.
|
||||
*
|
||||
* @param {object} backupInfo key backup info dict from /room_keys/version
|
||||
* @param {IKeyBackupInfo} backupInfo key backup info dict from /room_keys/version
|
||||
* @return {object} {
|
||||
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
|
||||
* sigs: [
|
||||
@@ -271,7 +305,6 @@ export class BackupManager {
|
||||
!backupInfo ||
|
||||
!backupInfo.algorithm ||
|
||||
!backupInfo.auth_data ||
|
||||
!backupInfo.auth_data.public_key ||
|
||||
!backupInfo.auth_data.signatures
|
||||
) {
|
||||
logger.info("Key backup is absent or missing required data");
|
||||
@@ -280,7 +313,7 @@ export class BackupManager {
|
||||
|
||||
const trustedPubkey = this.baseApis.crypto.sessionStore.getLocalTrustedBackupPubKey();
|
||||
|
||||
if (backupInfo.auth_data.public_key === trustedPubkey) {
|
||||
if ("public_key" in backupInfo.auth_data && backupInfo.auth_data.public_key === trustedPubkey) {
|
||||
logger.info("Backup public key " + trustedPubkey + " is trusted locally");
|
||||
ret.trusted_locally = true;
|
||||
}
|
||||
@@ -441,11 +474,11 @@ export class BackupManager {
|
||||
let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
|
||||
const data = {};
|
||||
const rooms: IKeyBackup["rooms"] = {};
|
||||
for (const session of sessions) {
|
||||
const roomId = session.sessionData.room_id;
|
||||
if (data[roomId] === undefined) {
|
||||
data[roomId] = { sessions: {} };
|
||||
if (rooms[roomId] === undefined) {
|
||||
rooms[roomId] = { sessions: {} };
|
||||
}
|
||||
|
||||
const sessionData = await this.baseApis.crypto.olmDevice.exportInboundGroupSession(
|
||||
@@ -464,7 +497,7 @@ export class BackupManager {
|
||||
);
|
||||
const verified = this.baseApis.crypto.checkDeviceInfoTrust(userId, device).isVerified();
|
||||
|
||||
data[roomId]['sessions'][session.sessionId] = {
|
||||
rooms[roomId]['sessions'][session.sessionId] = {
|
||||
first_message_index: sessionData.first_known_index,
|
||||
forwarded_count: forwardedCount,
|
||||
is_verified: verified,
|
||||
@@ -472,10 +505,7 @@ export class BackupManager {
|
||||
};
|
||||
}
|
||||
|
||||
await this.baseApis.sendKeyBackup(
|
||||
undefined, undefined, this.backupInfo.version,
|
||||
{ rooms: data },
|
||||
);
|
||||
await this.baseApis.sendKeyBackup(undefined, undefined, this.backupInfo.version, { rooms });
|
||||
|
||||
await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions);
|
||||
remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
||||
@@ -552,7 +582,7 @@ export class Curve25519 implements BackupAlgorithm {
|
||||
public static algorithmName = "m.megolm_backup.v1.curve25519-aes-sha2";
|
||||
|
||||
constructor(
|
||||
public authData: AuthData,
|
||||
public authData: ICurve25519AuthData,
|
||||
private publicKey: any, // FIXME: PkEncryption
|
||||
private getKey: () => Promise<Uint8Array>,
|
||||
) {}
|
||||
@@ -561,12 +591,12 @@ export class Curve25519 implements BackupAlgorithm {
|
||||
authData: AuthData,
|
||||
getKey: () => Promise<Uint8Array>,
|
||||
): Promise<Curve25519> {
|
||||
if (!authData || !authData.public_key) {
|
||||
if (!authData || !("public_key" in authData)) {
|
||||
throw new Error("auth_data missing required information");
|
||||
}
|
||||
const publicKey = new global.Olm.PkEncryption();
|
||||
publicKey.set_recipient_key(authData.public_key);
|
||||
return new Curve25519(authData, publicKey, getKey);
|
||||
return new Curve25519(authData as ICurve25519AuthData, publicKey, getKey);
|
||||
}
|
||||
|
||||
public static async prepare(
|
||||
@@ -574,7 +604,7 @@ export class Curve25519 implements BackupAlgorithm {
|
||||
): Promise<[Uint8Array, AuthData]> {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
try {
|
||||
const authData: Partial<AuthData> = {};
|
||||
const authData: Partial<ICurve25519AuthData> = {};
|
||||
if (!key) {
|
||||
authData.public_key = decryption.generate_key();
|
||||
} else if (key instanceof Uint8Array) {
|
||||
@@ -597,6 +627,14 @@ export class Curve25519 implements BackupAlgorithm {
|
||||
}
|
||||
}
|
||||
|
||||
public static checkBackupVersion(info: IKeyBackupInfo): void {
|
||||
if (!("public_key" in info.auth_data)) {
|
||||
throw new Error("Invalid backup data returned");
|
||||
}
|
||||
}
|
||||
|
||||
public get untrusted() { return true; }
|
||||
|
||||
public async encryptSession(data: Record<string, any>): Promise<any> {
|
||||
const plainText: Record<string, any> = Object.assign({}, data);
|
||||
delete plainText.session_id;
|
||||
@@ -605,7 +643,9 @@ export class Curve25519 implements BackupAlgorithm {
|
||||
return this.publicKey.encrypt(JSON.stringify(plainText));
|
||||
}
|
||||
|
||||
public async decryptSessions(sessions: Record<string, Record<string, any>>): Promise<Record<string, any>[]> {
|
||||
public async decryptSessions(
|
||||
sessions: Record<string, IKeyBackupSession>,
|
||||
): Promise<Record<string, any>[]> {
|
||||
const privKey = await this.getKey();
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
try {
|
||||
@@ -654,8 +694,120 @@ export class Curve25519 implements BackupAlgorithm {
|
||||
}
|
||||
}
|
||||
|
||||
function randomBytes(size: number): Uint8Array {
|
||||
const crypto: {randomBytes: (n: number) => Uint8Array} | undefined = getCrypto() as any;
|
||||
if (crypto) {
|
||||
// nodejs version
|
||||
return crypto.randomBytes(size);
|
||||
}
|
||||
if (window?.crypto) {
|
||||
// browser version
|
||||
const buf = new Uint8Array(size);
|
||||
window.crypto.getRandomValues(buf);
|
||||
return buf;
|
||||
}
|
||||
throw new Error("No usable crypto implementation");
|
||||
}
|
||||
|
||||
const UNSTABLE_MSC3270_NAME = new UnstableValue(null, "org.matrix.msc3270.v1.aes-hmac-sha2");
|
||||
|
||||
export class Aes256 implements BackupAlgorithm {
|
||||
public static algorithmName = UNSTABLE_MSC3270_NAME.name;
|
||||
|
||||
constructor(
|
||||
public readonly authData: IAes256AuthData,
|
||||
private readonly key: Uint8Array,
|
||||
) {}
|
||||
|
||||
public static async init(
|
||||
authData: IAes256AuthData,
|
||||
getKey: () => Promise<Uint8Array>,
|
||||
): Promise<Aes256> {
|
||||
if (!authData) {
|
||||
throw new Error("auth_data missing");
|
||||
}
|
||||
const key = await getKey();
|
||||
if (authData.mac) {
|
||||
const { mac } = await calculateKeyCheck(key, authData.iv);
|
||||
if (authData.mac.replace(/=+$/g, '') !== mac.replace(/=+/g, '')) {
|
||||
throw new Error("Key does not match");
|
||||
}
|
||||
}
|
||||
return new Aes256(authData, key);
|
||||
}
|
||||
|
||||
public static async prepare(
|
||||
key: string | Uint8Array | null,
|
||||
): Promise<[Uint8Array, AuthData]> {
|
||||
let outKey: Uint8Array;
|
||||
const authData: Partial<IAes256AuthData> = {};
|
||||
if (!key) {
|
||||
outKey = randomBytes(32);
|
||||
} else if (key instanceof Uint8Array) {
|
||||
outKey = new Uint8Array(key);
|
||||
} else {
|
||||
const derivation = await keyFromPassphrase(key);
|
||||
authData.private_key_salt = derivation.salt;
|
||||
authData.private_key_iterations = derivation.iterations;
|
||||
outKey = derivation.key;
|
||||
}
|
||||
|
||||
const { iv, mac } = await calculateKeyCheck(outKey);
|
||||
authData.iv = iv;
|
||||
authData.mac = mac;
|
||||
|
||||
return [outKey, authData as AuthData];
|
||||
}
|
||||
|
||||
public static checkBackupVersion(info: IKeyBackupInfo): void {
|
||||
if (!("iv" in info.auth_data && "mac" in info.auth_data)) {
|
||||
throw new Error("Invalid backup data returned");
|
||||
}
|
||||
}
|
||||
|
||||
public get untrusted() { return false; }
|
||||
|
||||
async encryptSession(data: Record<string, any>): Promise<any> {
|
||||
const plainText: Record<string, any> = Object.assign({}, data);
|
||||
delete plainText.session_id;
|
||||
delete plainText.room_id;
|
||||
delete plainText.first_known_index;
|
||||
return await encryptAES(JSON.stringify(plainText), this.key, data.session_id);
|
||||
}
|
||||
|
||||
async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<Record<string, any>[]> {
|
||||
const keys = [];
|
||||
|
||||
for (const [sessionId, sessionData] of Object.entries(sessions)) {
|
||||
try {
|
||||
const decrypted = JSON.parse(await decryptAES(sessionData.session_data, this.key, sessionId));
|
||||
decrypted.session_id = sessionId;
|
||||
keys.push(decrypted);
|
||||
} catch (e) {
|
||||
logger.log("Failed to decrypt megolm session from backup", e, sessionData);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
async keyMatches(key: Uint8Array): Promise<boolean> {
|
||||
if (this.authData.mac) {
|
||||
const { mac } = await calculateKeyCheck(key, this.authData.iv);
|
||||
return this.authData.mac.replace(/=+$/g, '') === mac.replace(/=+/g, '');
|
||||
} else {
|
||||
// if we have no information, we have to assume the key is right
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public free(): void {
|
||||
this.key.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
export const algorithmsByName: Record<string, BackupAlgorithmClass> = {
|
||||
[Curve25519.algorithmName]: Curve25519,
|
||||
[Aes256.algorithmName]: Aes256,
|
||||
};
|
||||
|
||||
export const DefaultAlgorithm: BackupAlgorithmClass = Curve25519;
|
||||
|
@@ -19,7 +19,7 @@ import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
|
||||
import { decryptAES, encryptAES } from './aes';
|
||||
import anotherjson from "another-json";
|
||||
import { logger } from '../logger';
|
||||
import { ISecretStorageKeyInfo } from "../matrix";
|
||||
import { ISecretStorageKeyInfo } from "./api";
|
||||
|
||||
// FIXME: these types should eventually go in a different file
|
||||
type Signatures = Record<string, Record<string, string>>;
|
||||
@@ -36,7 +36,7 @@ export interface IDehydratedDeviceKeyInfo {
|
||||
passphrase?: string;
|
||||
}
|
||||
|
||||
interface DeviceKeys {
|
||||
export interface IDeviceKeys {
|
||||
algorithms: Array<string>;
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
user_id: string; // eslint-disable-line camelcase
|
||||
@@ -44,7 +44,7 @@ interface DeviceKeys {
|
||||
signatures?: Signatures;
|
||||
}
|
||||
|
||||
export interface OneTimeKey {
|
||||
export interface IOneTimeKey {
|
||||
key: string;
|
||||
fallback?: boolean;
|
||||
signatures?: Signatures;
|
||||
@@ -222,7 +222,7 @@ export class DehydrationManager {
|
||||
// send the keys to the server
|
||||
const deviceId = dehydrateResult.device_id;
|
||||
logger.log("Preparing device keys", deviceId);
|
||||
const deviceKeys: DeviceKeys = {
|
||||
const deviceKeys: IDeviceKeys = {
|
||||
algorithms: this.crypto.supportedAlgorithms,
|
||||
device_id: deviceId,
|
||||
user_id: this.crypto.userId,
|
||||
@@ -244,7 +244,7 @@ export class DehydrationManager {
|
||||
logger.log("Preparing one-time keys");
|
||||
const oneTimeKeys = {};
|
||||
for (const [keyId, key] of Object.entries(otks.curve25519)) {
|
||||
const k: OneTimeKey = { key };
|
||||
const k: IOneTimeKey = { key };
|
||||
const signature = account.sign(anotherjson.stringify(k));
|
||||
k.signatures = {
|
||||
[this.crypto.userId]: {
|
||||
@@ -257,7 +257,7 @@ export class DehydrationManager {
|
||||
logger.log("Preparing fallback keys");
|
||||
const fallbackKeys = {};
|
||||
for (const [keyId, key] of Object.entries(fallbacks.curve25519)) {
|
||||
const k: OneTimeKey = { key, fallback: true };
|
||||
const k: IOneTimeKey = { key, fallback: true };
|
||||
const signature = account.sign(anotherjson.stringify(k));
|
||||
k.signatures = {
|
||||
[this.crypto.userId]: {
|
||||
|
@@ -33,11 +33,18 @@ import { DeviceInfo, IDevice } from "./deviceinfo";
|
||||
import * as algorithms from "./algorithms";
|
||||
import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning';
|
||||
import { EncryptionSetupBuilder } from "./EncryptionSetup";
|
||||
import { SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorage } from './SecretStorage';
|
||||
import {
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
SecretStorage,
|
||||
SecretStorageKeyTuple,
|
||||
ISecretRequest,
|
||||
SecretStorageKeyObject,
|
||||
} from './SecretStorage';
|
||||
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from "./api";
|
||||
import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager';
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from './verification/QRCode';
|
||||
import { SAS } from './verification/SAS';
|
||||
import { SAS as SASVerification } from './verification/SAS';
|
||||
import { keyFromPassphrase } from './key_passphrase';
|
||||
import { decodeRecoveryKey, encodeRecoveryKey } from './recoverykey';
|
||||
import { VerificationRequest } from "./verification/request/VerificationRequest";
|
||||
@@ -45,8 +52,8 @@ import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChan
|
||||
import { ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel";
|
||||
import { IllegalMethod } from "./verification/IllegalMethod";
|
||||
import { KeySignatureUploadError } from "../errors";
|
||||
import { decryptAES, encryptAES } from './aes';
|
||||
import { DehydrationManager } from './dehydration';
|
||||
import { decryptAES, encryptAES, calculateKeyCheck } from './aes';
|
||||
import { DehydrationManager, IDeviceKeys, IOneTimeKey } from './dehydration';
|
||||
import { BackupManager } from "./backup";
|
||||
import { IStore } from "../store";
|
||||
import { Room } from "../models/room";
|
||||
@@ -54,7 +61,7 @@ import { RoomMember } from "../models/room-member";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { MatrixClient, IKeysUploadResponse, SessionStore, CryptoStore, ISignedKey } from "../client";
|
||||
import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base";
|
||||
import type { RoomList } from "./RoomList";
|
||||
import type { IRoomEncryption, RoomList } from "./RoomList";
|
||||
import { IRecoveryKey, IEncryptedEventInfo } from "./api";
|
||||
import { IKeyBackupInfo } from "./keybackup";
|
||||
import { ISyncStateData } from "../sync";
|
||||
@@ -63,7 +70,7 @@ const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
const defaultVerificationMethods = {
|
||||
[ReciprocateQRCode.NAME]: ReciprocateQRCode,
|
||||
[SAS.NAME]: SAS,
|
||||
[SASVerification.NAME]: SASVerification,
|
||||
|
||||
// These two can't be used for actual verification, but we do
|
||||
// need to be able to define them here for the verification flows
|
||||
@@ -75,10 +82,13 @@ const defaultVerificationMethods = {
|
||||
/**
|
||||
* verification method names
|
||||
*/
|
||||
export const verificationMethods = {
|
||||
RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME,
|
||||
SAS: SAS.NAME,
|
||||
};
|
||||
// legacy export identifier
|
||||
export enum verificationMethods {
|
||||
RECIPROCATE_QR_CODE = ReciprocateQRCode.NAME,
|
||||
SAS = SASVerification.NAME,
|
||||
}
|
||||
|
||||
export type VerificationMethod = verificationMethods;
|
||||
|
||||
export function isCryptoAvailable(): boolean {
|
||||
return Boolean(global.Olm);
|
||||
@@ -126,6 +136,7 @@ export interface IMegolmSessionData {
|
||||
session_id: string;
|
||||
session_key: string;
|
||||
algorithm: string;
|
||||
untrusted?: boolean;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@@ -134,6 +145,10 @@ interface IDeviceVerificationUpgrade {
|
||||
crossSigningInfo: CrossSigningInfo;
|
||||
}
|
||||
|
||||
export interface ICheckOwnCrossSigningTrustOpts {
|
||||
allowPrivateKeyRequests?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} module:crypto~OlmSessionResult
|
||||
* @property {module:crypto/deviceinfo} device device info
|
||||
@@ -192,7 +207,7 @@ export class Crypto extends EventEmitter {
|
||||
private readonly supportedAlgorithms: string[];
|
||||
private readonly outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager;
|
||||
private readonly toDeviceVerificationRequests: ToDeviceRequests;
|
||||
private readonly inRoomVerificationRequests: InRoomRequests;
|
||||
public readonly inRoomVerificationRequests: InRoomRequests;
|
||||
|
||||
private trustCrossSignedDevices = true;
|
||||
// the last time we did a check for the number of one-time-keys on the server.
|
||||
@@ -359,7 +374,8 @@ export class Crypto extends EventEmitter {
|
||||
const cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore, this.olmDevice);
|
||||
|
||||
this.crossSigningInfo = new CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks);
|
||||
this.secretStorage = new SecretStorage(baseApis, cryptoCallbacks);
|
||||
// Yes, we pass the client twice here: see SecretStorage
|
||||
this.secretStorage = new SecretStorage(baseApis, cryptoCallbacks, baseApis);
|
||||
this.dehydrationManager = new DehydrationManager(this);
|
||||
|
||||
// Assuming no app-supplied callback, default to getting from SSSS.
|
||||
@@ -789,7 +805,7 @@ export class Crypto extends EventEmitter {
|
||||
if (key) {
|
||||
const privateKey = key[1];
|
||||
builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey);
|
||||
const { iv, mac } = await SecretStorage._calculateKeyCheck(privateKey);
|
||||
const { iv, mac } = await calculateKeyCheck(privateKey);
|
||||
keyInfo.iv = iv;
|
||||
keyInfo.mac = mac;
|
||||
|
||||
@@ -959,6 +975,20 @@ export class Crypto extends EventEmitter {
|
||||
fixedBackupKey || sessionBackupKey,
|
||||
));
|
||||
await builder.addSessionBackupPrivateKeyToCache(decodedBackupKey);
|
||||
} else if (this.backupManager.getKeyBackupEnabled()) {
|
||||
// key backup is enabled but we don't have a session backup key in SSSS: see if we have one in
|
||||
// the cache or the user can provide one, and if so, write it to SSSS
|
||||
const backupKey = await this.getSessionBackupPrivateKey() || await getKeyBackupPassphrase();
|
||||
if (!backupKey) {
|
||||
// This will require user intervention to recover from since we don't have the key
|
||||
// backup key anywhere. The user should probably just set up a new key backup and
|
||||
// the key for the new backup will be stored. If we hit this scenario in the wild
|
||||
// with any frequency, we should do more than just log an error.
|
||||
logger.error("Key backup is enabled but couldn't get key backup key!");
|
||||
return;
|
||||
}
|
||||
logger.info("Got session backup key from cache/user that wasn't in SSSS: saving to SSSS");
|
||||
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey));
|
||||
}
|
||||
|
||||
const operation = builder.buildOperation();
|
||||
@@ -970,15 +1000,19 @@ export class Crypto extends EventEmitter {
|
||||
logger.log("Secure Secret Storage ready");
|
||||
}
|
||||
|
||||
public addSecretStorageKey(algorithm: string, opts: any, keyID: string): any { // TODO types
|
||||
public addSecretStorageKey(
|
||||
algorithm: string,
|
||||
opts: IAddSecretStorageKeyOpts,
|
||||
keyID: string,
|
||||
): Promise<SecretStorageKeyObject> {
|
||||
return this.secretStorage.addKey(algorithm, opts, keyID);
|
||||
}
|
||||
|
||||
public hasSecretStorageKey(keyID: string): boolean {
|
||||
public hasSecretStorageKey(keyID: string): Promise<boolean> {
|
||||
return this.secretStorage.hasKey(keyID);
|
||||
}
|
||||
|
||||
public getSecretStorageKey(keyID?: string): any { // TODO types
|
||||
public getSecretStorageKey(keyID?: string): Promise<SecretStorageKeyTuple> {
|
||||
return this.secretStorage.getKey(keyID);
|
||||
}
|
||||
|
||||
@@ -990,11 +1024,14 @@ export class Crypto extends EventEmitter {
|
||||
return this.secretStorage.get(name);
|
||||
}
|
||||
|
||||
public isSecretStored(name: string, checkKey?: boolean): any { // TODO types
|
||||
public isSecretStored(
|
||||
name: string,
|
||||
checkKey?: boolean,
|
||||
): Promise<Record<string, ISecretStorageKeyInfo>> {
|
||||
return this.secretStorage.isStored(name, checkKey);
|
||||
}
|
||||
|
||||
public requestSecret(name: string, devices: string[]): Promise<any> { // TODO types
|
||||
public requestSecret(name: string, devices: string[]): ISecretRequest {
|
||||
if (!devices) {
|
||||
devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(this.userId));
|
||||
}
|
||||
@@ -1009,7 +1046,7 @@ export class Crypto extends EventEmitter {
|
||||
return this.secretStorage.setDefaultKeyId(k);
|
||||
}
|
||||
|
||||
public checkSecretStorageKey(key: string, info: any): Promise<boolean> { // TODO types
|
||||
public checkSecretStorageKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise<boolean> {
|
||||
return this.secretStorage.checkKey(key, info);
|
||||
}
|
||||
|
||||
@@ -1388,7 +1425,7 @@ export class Crypto extends EventEmitter {
|
||||
*/
|
||||
async checkOwnCrossSigningTrust({
|
||||
allowPrivateKeyRequests = false,
|
||||
} = {}) {
|
||||
}: ICheckOwnCrossSigningTrustOpts = {}): Promise<void> {
|
||||
const userId = this.userId;
|
||||
|
||||
// Before proceeding, ensure our cross-signing public keys have been
|
||||
@@ -1742,7 +1779,7 @@ export class Crypto extends EventEmitter {
|
||||
|
||||
return this.signObject(deviceKeys).then(() => {
|
||||
return this.baseApis.uploadKeysRequest({
|
||||
device_keys: deviceKeys,
|
||||
device_keys: deviceKeys as Required<IDeviceKeys>,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1874,9 +1911,9 @@ export class Crypto extends EventEmitter {
|
||||
private async uploadOneTimeKeys() {
|
||||
const promises = [];
|
||||
|
||||
const fallbackJson = {};
|
||||
const fallbackJson: Record<string, IOneTimeKey> = {};
|
||||
if (this.getNeedsNewFallback()) {
|
||||
const fallbackKeys = await this.olmDevice.getFallbackKey();
|
||||
const fallbackKeys = await this.olmDevice.getFallbackKey() as Record<string, Record<string, string>>;
|
||||
for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) {
|
||||
const k = { key, fallback: true };
|
||||
fallbackJson["signed_curve25519:" + keyId] = k;
|
||||
@@ -2222,7 +2259,7 @@ export class Crypto extends EventEmitter {
|
||||
public async legacyDeviceVerification(
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
method: string,
|
||||
method: VerificationMethod,
|
||||
): VerificationRequest {
|
||||
const transactionId = ToDeviceChannel.makeTransactionId();
|
||||
const channel = new ToDeviceChannel(
|
||||
@@ -2435,7 +2472,7 @@ export class Crypto extends EventEmitter {
|
||||
*/
|
||||
public async setRoomEncryption(
|
||||
roomId: string,
|
||||
config: any, // TODO types
|
||||
config: IRoomEncryption,
|
||||
inhibitDeviceQuery?: boolean,
|
||||
): Promise<void> {
|
||||
// ignore crypto events with no algorithm defined
|
||||
@@ -2492,8 +2529,8 @@ export class Crypto extends EventEmitter {
|
||||
crypto: this,
|
||||
olmDevice: this.olmDevice,
|
||||
baseApis: this.baseApis,
|
||||
roomId: roomId,
|
||||
config: config,
|
||||
roomId,
|
||||
config,
|
||||
});
|
||||
this.roomEncryptors[roomId] = alg;
|
||||
|
||||
@@ -2848,7 +2885,7 @@ export class Crypto extends EventEmitter {
|
||||
*/
|
||||
public async onCryptoEvent(event: MatrixEvent): Promise<void> {
|
||||
const roomId = event.getRoomId();
|
||||
const content = event.getContent();
|
||||
const content = event.getContent<IRoomEncryption>();
|
||||
|
||||
try {
|
||||
// inhibit the device list refresh for now - it will happen once we've
|
||||
@@ -2996,9 +3033,9 @@ export class Crypto extends EventEmitter {
|
||||
} else if (event.getType() == "m.room_key_request") {
|
||||
this.onRoomKeyRequestEvent(event);
|
||||
} else if (event.getType() === "m.secret.request") {
|
||||
this.secretStorage._onRequestReceived(event);
|
||||
this.secretStorage.onRequestReceived(event);
|
||||
} else if (event.getType() === "m.secret.send") {
|
||||
this.secretStorage._onSecretReceived(event);
|
||||
this.secretStorage.onSecretReceived(event);
|
||||
} else if (event.getType() === "org.matrix.room_key.withheld") {
|
||||
this.onRoomKeyWithheldEvent(event);
|
||||
} else if (event.getContent().transaction_id) {
|
||||
|
@@ -22,8 +22,8 @@ const DEFAULT_BITSIZE = 256;
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IAuthData {
|
||||
private_key_salt: string;
|
||||
private_key_iterations: number;
|
||||
private_key_salt?: string;
|
||||
private_key_iterations?: number;
|
||||
private_key_bits?: number;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ISignatures } from "../@types/signed";
|
||||
import { ISigned } from "../@types/signed";
|
||||
|
||||
export interface IKeyBackupSession {
|
||||
first_message_index: number; // eslint-disable-line camelcase
|
||||
@@ -24,6 +24,7 @@ export interface IKeyBackupSession {
|
||||
ciphertext: string;
|
||||
ephemeral: string;
|
||||
mac: string;
|
||||
iv: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,15 +33,23 @@ export interface IKeyBackupRoomSessions {
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface ICurve25519AuthData {
|
||||
public_key: string;
|
||||
private_key_salt?: string;
|
||||
private_key_iterations?: number;
|
||||
private_key_bits?: number;
|
||||
}
|
||||
|
||||
export interface IAes256AuthData {
|
||||
iv: string;
|
||||
mac: string;
|
||||
private_key_salt?: string;
|
||||
private_key_iterations?: number;
|
||||
}
|
||||
|
||||
export interface IKeyBackupInfo {
|
||||
algorithm: string;
|
||||
auth_data: {
|
||||
public_key: string;
|
||||
signatures: ISignatures;
|
||||
private_key_salt: string;
|
||||
private_key_iterations: number;
|
||||
private_key_bits?: number;
|
||||
};
|
||||
auth_data: ISigned & (ICurve25519AuthData | IAes256AuthData);
|
||||
count?: number;
|
||||
etag?: string;
|
||||
version?: string; // number contained within
|
||||
|
@@ -28,7 +28,7 @@ import OlmDevice from "./OlmDevice";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { logger } from '../logger';
|
||||
import * as utils from "../utils";
|
||||
import { OneTimeKey } from "./dehydration";
|
||||
import { IOneTimeKey } from "./dehydration";
|
||||
import { MatrixClient } from "../client";
|
||||
|
||||
enum Algorithm {
|
||||
@@ -407,7 +407,7 @@ export async function ensureOlmSessionsForDevices(
|
||||
|
||||
async function _verifyKeyAndStartSession(
|
||||
olmDevice: OlmDevice,
|
||||
oneTimeKey: OneTimeKey,
|
||||
oneTimeKey: IOneTimeKey,
|
||||
userId: string,
|
||||
deviceInfo: DeviceInfo,
|
||||
): Promise<string> {
|
||||
@@ -465,7 +465,7 @@ export interface IObject {
|
||||
*/
|
||||
export async function verifySignature(
|
||||
olmDevice: OlmDevice,
|
||||
obj: OneTimeKey | IObject,
|
||||
obj: IOneTimeKey | IObject,
|
||||
signingUserId: string,
|
||||
signingDeviceId: string,
|
||||
signingKey: string,
|
||||
|
@@ -15,9 +15,9 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "./client";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
import { IEvent, MatrixEvent } from "./models/event";
|
||||
|
||||
export type EventMapper = (obj: any) => MatrixEvent;
|
||||
export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
|
||||
|
||||
export interface MapperOpts {
|
||||
preventReEmit?: boolean;
|
||||
@@ -28,7 +28,7 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
||||
const preventReEmit = Boolean(options.preventReEmit);
|
||||
const decrypt = options.decrypt !== false;
|
||||
|
||||
function mapper(plainOldJsObject) {
|
||||
function mapper(plainOldJsObject: Partial<IEvent>) {
|
||||
const event = new MatrixEvent(plainOldJsObject);
|
||||
if (event.isEncrypted()) {
|
||||
if (!preventReEmit) {
|
||||
|
@@ -39,7 +39,7 @@ function setProp(obj: object, keyNesting: string, val: any) {
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IFilterDefinition {
|
||||
export interface IFilterDefinition {
|
||||
event_fields?: string[];
|
||||
event_format?: "client" | "federation";
|
||||
presence?: IFilterComponent;
|
||||
@@ -47,7 +47,7 @@ interface IFilterDefinition {
|
||||
room?: IRoomFilter;
|
||||
}
|
||||
|
||||
interface IRoomEventFilter extends IFilterComponent {
|
||||
export interface IRoomEventFilter extends IFilterComponent {
|
||||
lazy_load_members?: boolean;
|
||||
include_redundant_members?: boolean;
|
||||
}
|
||||
@@ -86,7 +86,7 @@ export class Filter {
|
||||
* @param {Object} jsonObj
|
||||
* @return {Filter}
|
||||
*/
|
||||
static fromJson(userId: string, filterId: string, jsonObj: IFilterDefinition): Filter {
|
||||
public static fromJson(userId: string, filterId: string, jsonObj: IFilterDefinition): Filter {
|
||||
const filter = new Filter(userId, filterId);
|
||||
filter.setDefinition(jsonObj);
|
||||
return filter;
|
||||
|
@@ -121,7 +121,7 @@ MatrixHttpApi.prototype = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Upload content to the Home Server
|
||||
* Upload content to the homeserver
|
||||
*
|
||||
* @param {object} file The object to upload. On a browser, something that
|
||||
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
|
||||
@@ -393,7 +393,7 @@ MatrixHttpApi.prototype = {
|
||||
accessToken,
|
||||
) {
|
||||
if (!this.opts.idBaseUrl) {
|
||||
throw new Error("No Identity Server base URL set");
|
||||
throw new Error("No identity server base URL set");
|
||||
}
|
||||
|
||||
const fullUri = this.opts.idBaseUrl + prefix + path;
|
||||
|
@@ -264,9 +264,7 @@ export class InteractiveAuth {
|
||||
client_secret: this.clientSecret,
|
||||
};
|
||||
if (await this.matrixClient.doesServerRequireIdServerParam()) {
|
||||
const idServerParsedUrl = url.parse(
|
||||
this.matrixClient.getIdentityServerUrl(),
|
||||
);
|
||||
const idServerParsedUrl = new URL(this.matrixClient.getIdentityServerUrl());
|
||||
creds.id_server = idServerParsedUrl.host;
|
||||
}
|
||||
authDict = {
|
||||
@@ -294,7 +292,7 @@ export class InteractiveAuth {
|
||||
|
||||
/**
|
||||
* get the client secret used for validation sessions
|
||||
* with the ID server.
|
||||
* with the identity server.
|
||||
*
|
||||
* @return {string} client secret
|
||||
*/
|
||||
|
@@ -20,6 +20,7 @@ import { MatrixScheduler } from "./scheduler";
|
||||
import { MatrixClient } from "./client";
|
||||
import { ICreateClientOpts } from "./client";
|
||||
import { DeviceTrustLevel } from "./crypto/CrossSigning";
|
||||
import { ISecretStorageKeyInfo } from "./crypto/api";
|
||||
|
||||
export * from "./client";
|
||||
export * from "./http-api";
|
||||
@@ -122,17 +123,6 @@ export interface ICryptoCallbacks {
|
||||
getBackupKey?: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
// TODO: Move this to `SecretStorage` once converted
|
||||
export interface ISecretStorageKeyInfo {
|
||||
passphrase?: {
|
||||
algorithm: "m.pbkdf2";
|
||||
iterations: number;
|
||||
salt: string;
|
||||
};
|
||||
iv?: string;
|
||||
mac?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Matrix Client. Similar to {@link module:client.MatrixClient}
|
||||
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
|
||||
|
@@ -70,8 +70,8 @@ export class MSC3089Branch {
|
||||
* @param {string} name The new name for this file.
|
||||
* @returns {Promise<void>} Resolves when complete.
|
||||
*/
|
||||
public setName(name: string): Promise<void> {
|
||||
return this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, {
|
||||
public async setName(name: string): Promise<void> {
|
||||
await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, {
|
||||
...this.indexEvent.getContent(),
|
||||
name: name,
|
||||
}, this.id);
|
||||
|
@@ -111,8 +111,8 @@ export class MSC3089TreeSpace {
|
||||
* @param {string} name The new name for the space.
|
||||
* @returns {Promise<void>} Resolves when complete.
|
||||
*/
|
||||
public setName(name: string): Promise<void> {
|
||||
return this.client.sendStateEvent(this.roomId, EventType.RoomName, { name }, "");
|
||||
public async setName(name: string): Promise<void> {
|
||||
await this.client.sendStateEvent(this.roomId, EventType.RoomName, { name }, "");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,7 +190,7 @@ export class MSC3089TreeSpace {
|
||||
}
|
||||
pls['users'] = users;
|
||||
|
||||
return this.client.sendStateEvent(this.roomId, EventType.RoomPowerLevels, pls, "");
|
||||
await this.client.sendStateEvent(this.roomId, EventType.RoomPowerLevels, pls, "");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -22,6 +22,7 @@ import { RoomState } from "./room-state";
|
||||
import { EventTimelineSet } from "./event-timeline-set";
|
||||
import { MatrixEvent } from "./event";
|
||||
import { Filter } from "../filter";
|
||||
import { EventType } from "../@types/event";
|
||||
|
||||
export enum Direction {
|
||||
Backward = "b",
|
||||
@@ -49,15 +50,16 @@ export class EventTimeline {
|
||||
* @param {boolean} toStartOfTimeline if true the event's forwardLooking flag is set false
|
||||
*/
|
||||
static setEventMetadata(event: MatrixEvent, stateContext: RoomState, toStartOfTimeline: boolean): void {
|
||||
// set sender and target properties
|
||||
event.sender = stateContext.getSentinelMember(
|
||||
event.getSender(),
|
||||
);
|
||||
if (event.getType() === "m.room.member") {
|
||||
event.target = stateContext.getSentinelMember(
|
||||
event.getStateKey(),
|
||||
);
|
||||
// We always check if the event doesn't already have the property. We do
|
||||
// this to avoid overriding non-sentinel members by sentinel ones when
|
||||
// adding the event to a filtered timeline
|
||||
if (!event.sender) {
|
||||
event.sender = stateContext.getSentinelMember(event.getSender());
|
||||
}
|
||||
if (!event.target && event.getType() === EventType.RoomMember) {
|
||||
event.target = stateContext.getSentinelMember(event.getStateKey());
|
||||
}
|
||||
|
||||
if (event.isState()) {
|
||||
// room state has no concept of 'old' or 'current', but we want the
|
||||
// room state to regress back to previous values if toStartOfTimeline
|
||||
@@ -345,15 +347,16 @@ export class EventTimeline {
|
||||
*/
|
||||
public addEvent(event: MatrixEvent, atStart: boolean): void {
|
||||
const stateContext = atStart ? this.startState : this.endState;
|
||||
|
||||
// only call setEventMetadata on the unfiltered timelineSets
|
||||
const timelineSet = this.getTimelineSet();
|
||||
if (timelineSet.room &&
|
||||
timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
|
||||
|
||||
if (timelineSet.room) {
|
||||
EventTimeline.setEventMetadata(event, stateContext, atStart);
|
||||
|
||||
// modify state
|
||||
if (event.isState()) {
|
||||
// modify state but only on unfiltered timelineSets
|
||||
if (
|
||||
event.isState() &&
|
||||
timelineSet.room.getUnfilteredTimelineSet() === timelineSet
|
||||
) {
|
||||
stateContext.setStateEvents([event]);
|
||||
// it is possible that the act of setting the state event means we
|
||||
// can set more metadata (specifically sender/target props), so try
|
||||
|
@@ -263,6 +263,16 @@ export class MatrixEvent extends EventEmitter {
|
||||
this.localTimestamp = Date.now() - this.getAge();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the event as though it would appear unencrypted. If the event is already not
|
||||
* encrypted, it is simply returned as-is.
|
||||
* @returns {IEvent} The event in wire format.
|
||||
*/
|
||||
public getEffectiveEvent(): IEvent {
|
||||
// clearEvent doesn't have all the fields, so we'll copy what we can from this.event
|
||||
return Object.assign({}, this.event, this.clearEvent) as IEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the event_id for this event.
|
||||
* @return {string} The event ID, e.g. <code>$143350589368169JsLZx:localhost
|
||||
@@ -1231,20 +1241,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
* @return {Object}
|
||||
*/
|
||||
public toJSON(): object {
|
||||
const event: any = {
|
||||
type: this.getType(),
|
||||
sender: this.getSender(),
|
||||
content: this.getContent(),
|
||||
event_id: this.getId(),
|
||||
origin_server_ts: this.getTs(),
|
||||
unsigned: this.getUnsigned(),
|
||||
room_id: this.getRoomId(),
|
||||
};
|
||||
|
||||
// if this is a redaction then attach the redacts key
|
||||
if (this.isRedaction()) {
|
||||
event.redacts = this.event.redacts;
|
||||
}
|
||||
const event = this.getEffectiveEvent();
|
||||
|
||||
if (!this.isEncrypted()) {
|
||||
return event;
|
||||
|
@@ -25,13 +25,13 @@ import { EventTimeline } from "./event-timeline";
|
||||
import { getHttpUriForMxc } from "../content-repo";
|
||||
import * as utils from "../utils";
|
||||
import { normalize } from "../utils";
|
||||
import { EventStatus, MatrixEvent } from "./event";
|
||||
import { EventStatus, IEvent, MatrixEvent } from "./event";
|
||||
import { RoomMember } from "./room-member";
|
||||
import { IRoomSummary, RoomSummary } from "./room-summary";
|
||||
import { logger } from '../logger';
|
||||
import { ReEmitter } from '../ReEmitter';
|
||||
import { EventType, RoomCreateTypeField, RoomType } from "../@types/event";
|
||||
import { IRoomVersionsCapability, MatrixClient, RoomVersionStability } from "../client";
|
||||
import { EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../@types/event";
|
||||
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
|
||||
import { ResizeMethod } from "../@types/partials";
|
||||
import { Filter } from "../filter";
|
||||
import { RoomState } from "./room-state";
|
||||
@@ -64,7 +64,7 @@ function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: stri
|
||||
|
||||
interface IOpts {
|
||||
storageToken?: string;
|
||||
pendingEventOrdering?: "chronological" | "detached";
|
||||
pendingEventOrdering?: PendingEventOrdering;
|
||||
timelineSupport?: boolean;
|
||||
unstableClientRelationAggregation?: boolean;
|
||||
lazyLoadMembers?: boolean;
|
||||
@@ -218,7 +218,7 @@ export class Room extends EventEmitter {
|
||||
this.setMaxListeners(100);
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
|
||||
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
|
||||
opts.pendingEventOrdering = opts.pendingEventOrdering || PendingEventOrdering.Chronological;
|
||||
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
|
||||
throw new Error(
|
||||
"opts.pendingEventOrdering MUST be either 'chronological' or " +
|
||||
@@ -649,7 +649,7 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private async loadMembersFromServer(): Promise<object[]> {
|
||||
private async loadMembersFromServer(): Promise<IEvent[]> {
|
||||
const lastSyncToken = this.client.store.getSyncToken();
|
||||
const queryString = utils.encodeParams({
|
||||
not_membership: "leave",
|
||||
@@ -665,8 +665,7 @@ export class Room extends EventEmitter {
|
||||
private async loadMembers(): Promise<{ memberEvents: MatrixEvent[], fromServer: boolean }> {
|
||||
// were the members loaded from the server?
|
||||
let fromServer = false;
|
||||
let rawMembersEvents =
|
||||
await this.client.store.getOutOfBandMembers(this.roomId);
|
||||
let rawMembersEvents = await this.client.store.getOutOfBandMembers(this.roomId);
|
||||
if (rawMembersEvents === null) {
|
||||
fromServer = true;
|
||||
rawMembersEvents = await this.loadMembersFromServer();
|
||||
@@ -713,7 +712,7 @@ export class Room extends EventEmitter {
|
||||
if (fromServer) {
|
||||
const oobMembers = this.currentState.getMembers()
|
||||
.filter((m) => m.isOutOfBand())
|
||||
.map((m) => m.events.member.event);
|
||||
.map((m) => m.events.member.event as IEvent);
|
||||
logger.log(`LL: telling store to write ${oobMembers.length}`
|
||||
+ ` members for room ${this.roomId}`);
|
||||
const store = this.client.store;
|
||||
@@ -2037,24 +2036,45 @@ export class Room extends EventEmitter {
|
||||
const joinedMemberCount = this.currentState.getJoinedMemberCount();
|
||||
const invitedMemberCount = this.currentState.getInvitedMemberCount();
|
||||
// -1 because these numbers include the syncing user
|
||||
const inviteJoinCount = joinedMemberCount + invitedMemberCount - 1;
|
||||
let inviteJoinCount = joinedMemberCount + invitedMemberCount - 1;
|
||||
|
||||
// get service members (e.g. helper bots) for exclusion
|
||||
let excludedUserIds: string[] = [];
|
||||
const mFunctionalMembers = this.currentState.getStateEvents(UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, "");
|
||||
if (Array.isArray(mFunctionalMembers?.getContent().service_members)) {
|
||||
excludedUserIds = mFunctionalMembers.getContent().service_members;
|
||||
}
|
||||
|
||||
// get members that are NOT ourselves and are actually in the room.
|
||||
let otherNames = null;
|
||||
if (this.summaryHeroes) {
|
||||
// if we have a summary, the member state events
|
||||
// should be in the room state
|
||||
otherNames = this.summaryHeroes.map((userId) => {
|
||||
otherNames = [];
|
||||
this.summaryHeroes.forEach((userId) => {
|
||||
// filter service members
|
||||
if (excludedUserIds.includes(userId)) {
|
||||
inviteJoinCount--;
|
||||
return;
|
||||
}
|
||||
const member = this.getMember(userId);
|
||||
return member ? member.name : userId;
|
||||
otherNames.push(member ? member.name : userId);
|
||||
});
|
||||
} else {
|
||||
let otherMembers = this.currentState.getMembers().filter((m) => {
|
||||
return m.userId !== userId &&
|
||||
(m.membership === "invite" || m.membership === "join");
|
||||
});
|
||||
otherMembers = otherMembers.filter(({ userId }) => {
|
||||
// filter service members
|
||||
if (excludedUserIds.includes(userId)) {
|
||||
inviteJoinCount--;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// make sure members have stable order
|
||||
otherMembers.sort((a, b) => a.userId.localeCompare(b.userId));
|
||||
otherMembers.sort((a, b) => utils.compare(a.userId, b.userId));
|
||||
// only 5 first members, immitate summaryHeroes
|
||||
otherMembers = otherMembers.slice(0, 5);
|
||||
otherNames = otherMembers.map((m) => m.name);
|
||||
@@ -2065,7 +2085,7 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
|
||||
const myMembership = this.getMyMembership();
|
||||
// if I have created a room and invited people throuh
|
||||
// if I have created a room and invited people through
|
||||
// 3rd party invites
|
||||
if (myMembership == 'join') {
|
||||
const thirdPartyInvites =
|
||||
|
@@ -20,27 +20,9 @@ limitations under the License.
|
||||
|
||||
import { EventContext } from "./event-context";
|
||||
import { EventMapper } from "../event-mapper";
|
||||
import { IResultContext, ISearchResult } from "../@types/search";
|
||||
import { IRoomEvent } from "../sync-accumulator";
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IContext {
|
||||
events_before?: IRoomEvent[];
|
||||
events_after?: IRoomEvent[];
|
||||
start?: string;
|
||||
end?: string;
|
||||
profile_info?: Record<string, {
|
||||
displayname: string;
|
||||
avatar_url: string;
|
||||
}>;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
interface ISearchResult {
|
||||
rank: number;
|
||||
result: IRoomEvent;
|
||||
context: IContext;
|
||||
}
|
||||
|
||||
export class SearchResult {
|
||||
/**
|
||||
* Create a SearchResponse from the response to /search
|
||||
@@ -49,8 +31,9 @@ export class SearchResult {
|
||||
* @param {function} eventMapper
|
||||
* @return {SearchResult}
|
||||
*/
|
||||
|
||||
public static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult {
|
||||
const jsonContext: IContext = jsonObj.context || {};
|
||||
const jsonContext = jsonObj.context || {} as IResultContext;
|
||||
const eventsBefore = jsonContext.events_before || [];
|
||||
const eventsAfter = jsonContext.events_after || [];
|
||||
|
||||
|
@@ -208,7 +208,7 @@ export class User extends EventEmitter {
|
||||
* @param {MatrixEvent} event The <code>im.vector.user_status</code> event.
|
||||
* @fires module:client~MatrixClient#event:"User.unstable_statusMessage"
|
||||
*/
|
||||
// eslint-disable-next-line camelcase
|
||||
// eslint-disable-next-line
|
||||
public unstable_updateStatusMessage(event: MatrixEvent): void {
|
||||
if (!event.getContent()) this.unstable_statusMessage = "";
|
||||
else this.unstable_statusMessage = event.getContent()["status"];
|
||||
|
@@ -15,6 +15,6 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
export enum SERVICE_TYPES {
|
||||
IS = 'SERVICE_TYPE_IS', // An Identity Service
|
||||
IM = 'SERVICE_TYPE_IM', // An Integration Manager
|
||||
IS = 'SERVICE_TYPE_IS', // An identity server
|
||||
IM = 'SERVICE_TYPE_IM', // An integration manager
|
||||
}
|
||||
|
@@ -18,10 +18,11 @@ import { EventType } from "../@types/event";
|
||||
import { Group } from "../models/group";
|
||||
import { Room } from "../models/room";
|
||||
import { User } from "../models/user";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { IEvent, MatrixEvent } from "../models/event";
|
||||
import { Filter } from "../filter";
|
||||
import { RoomSummary } from "../models/room-summary";
|
||||
import { IMinimalEvent, IGroups, IRooms } from "../sync-accumulator";
|
||||
import { IMinimalEvent, IGroups, IRooms, ISyncResponse } from "../sync-accumulator";
|
||||
import { IStartClientOpts } from "../client";
|
||||
|
||||
export interface ISavedSync {
|
||||
nextBatch: string;
|
||||
@@ -35,6 +36,8 @@ export interface ISavedSync {
|
||||
* @constructor
|
||||
*/
|
||||
export interface IStore {
|
||||
readonly accountData: Record<string, MatrixEvent>; // type : content
|
||||
|
||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
||||
isNewlyCreated(): Promise<boolean>;
|
||||
|
||||
@@ -182,7 +185,7 @@ export interface IStore {
|
||||
* @param {Object} syncData The sync data
|
||||
* @return {Promise} An immediately resolved promise.
|
||||
*/
|
||||
setSyncData(syncData: object): Promise<void>;
|
||||
setSyncData(syncData: ISyncResponse): Promise<void>;
|
||||
|
||||
/**
|
||||
* We never want to save because we have nothing to save to.
|
||||
@@ -194,7 +197,7 @@ export interface IStore {
|
||||
/**
|
||||
* Save does nothing as there is no backing data store.
|
||||
*/
|
||||
save(force: boolean): void;
|
||||
save(force?: boolean): void;
|
||||
|
||||
/**
|
||||
* Startup does nothing.
|
||||
@@ -222,13 +225,13 @@ export interface IStore {
|
||||
*/
|
||||
deleteAllData(): Promise<void>;
|
||||
|
||||
getOutOfBandMembers(roomId: string): Promise<MatrixEvent[] | null>;
|
||||
getOutOfBandMembers(roomId: string): Promise<IEvent[] | null>;
|
||||
|
||||
setOutOfBandMembers(roomId: string, membershipEvents: MatrixEvent[]): Promise<void>;
|
||||
setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void>;
|
||||
|
||||
clearOutOfBandMembers(roomId: string): Promise<void>;
|
||||
|
||||
getClientOptions(): Promise<object>;
|
||||
getClientOptions(): Promise<IStartClientOpts>;
|
||||
|
||||
storeClientOptions(options: object): Promise<void>;
|
||||
storeClientOptions(options: IStartClientOpts): Promise<void>;
|
||||
}
|
||||
|
36
src/store/indexeddb-backend.ts
Normal file
36
src/store/indexeddb-backend.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright 2021 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 { ISavedSync } from "./index";
|
||||
import { IEvent, IStartClientOpts, ISyncResponse } from "..";
|
||||
|
||||
export interface IIndexedDBBackend {
|
||||
connect(): Promise<void>;
|
||||
syncToDatabase(userTuples: UserTuple[]): Promise<void>;
|
||||
isNewlyCreated(): Promise<boolean>;
|
||||
setSyncData(syncData: ISyncResponse): Promise<void>;
|
||||
getSavedSync(): Promise<ISavedSync>;
|
||||
getNextBatchToken(): Promise<string>;
|
||||
clearDatabase(): Promise<void>;
|
||||
getOutOfBandMembers(roomId: string): Promise<IEvent[] | null>;
|
||||
setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void>;
|
||||
clearOutOfBandMembers(roomId: string): Promise<void>;
|
||||
getUserPresenceEvents(): Promise<UserTuple[]>;
|
||||
getClientOptions(): Promise<IStartClientOpts>;
|
||||
storeClientOptions(options: IStartClientOpts): Promise<void>;
|
||||
}
|
||||
|
||||
export type UserTuple = [userId: string, presenceEvent: Partial<IEvent>];
|
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2017 - 2021 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.
|
||||
@@ -16,14 +14,17 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { SyncAccumulator } from "../sync-accumulator";
|
||||
import { IMinimalEvent, ISyncData, ISyncResponse, SyncAccumulator } from "../sync-accumulator";
|
||||
import * as utils from "../utils";
|
||||
import * as IndexedDBHelpers from "../indexeddb-helpers";
|
||||
import { logger } from '../logger';
|
||||
import { IEvent, IStartClientOpts } from "..";
|
||||
import { ISavedSync } from "./index";
|
||||
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
|
||||
|
||||
const VERSION = 3;
|
||||
|
||||
function createDatabase(db) {
|
||||
function createDatabase(db: IDBDatabase): void {
|
||||
// Make user store, clobber based on user ID. (userId property of User objects)
|
||||
db.createObjectStore("users", { keyPath: ["userId"] });
|
||||
|
||||
@@ -35,7 +36,7 @@ function createDatabase(db) {
|
||||
db.createObjectStore("sync", { keyPath: ["clobber"] });
|
||||
}
|
||||
|
||||
function upgradeSchemaV2(db) {
|
||||
function upgradeSchemaV2(db: IDBDatabase): void {
|
||||
const oobMembersStore = db.createObjectStore(
|
||||
"oob_membership_events", {
|
||||
keyPath: ["room_id", "state_key"],
|
||||
@@ -43,7 +44,7 @@ function upgradeSchemaV2(db) {
|
||||
oobMembersStore.createIndex("room", "room_id");
|
||||
}
|
||||
|
||||
function upgradeSchemaV3(db) {
|
||||
function upgradeSchemaV3(db: IDBDatabase): void {
|
||||
db.createObjectStore("client_options",
|
||||
{ keyPath: ["clobber"] });
|
||||
}
|
||||
@@ -58,16 +59,20 @@ function upgradeSchemaV3(db) {
|
||||
* @return {Promise<T[]>} Resolves to an array of whatever you returned from
|
||||
* resultMapper.
|
||||
*/
|
||||
function selectQuery(store, keyRange, resultMapper) {
|
||||
function selectQuery<T>(
|
||||
store: IDBObjectStore,
|
||||
keyRange: IDBKeyRange | IDBValidKey | undefined,
|
||||
resultMapper: (cursor: IDBCursorWithValue) => T,
|
||||
): Promise<T[]> {
|
||||
const query = store.openCursor(keyRange);
|
||||
return new Promise((resolve, reject) => {
|
||||
const results = [];
|
||||
query.onerror = (event) => {
|
||||
reject(new Error("Query failed: " + event.target.errorCode));
|
||||
query.onerror = () => {
|
||||
reject(new Error("Query failed: " + query.error));
|
||||
};
|
||||
// collect results
|
||||
query.onsuccess = (event) => {
|
||||
const cursor = event.target.result;
|
||||
query.onsuccess = () => {
|
||||
const cursor = query.result;
|
||||
if (!cursor) {
|
||||
resolve(results);
|
||||
return; // end of results
|
||||
@@ -78,88 +83,84 @@ function selectQuery(store, keyRange, resultMapper) {
|
||||
});
|
||||
}
|
||||
|
||||
function txnAsPromise(txn) {
|
||||
function txnAsPromise(txn: IDBTransaction): Promise<Event> {
|
||||
return new Promise((resolve, reject) => {
|
||||
txn.oncomplete = function(event) {
|
||||
resolve(event);
|
||||
};
|
||||
txn.onerror = function(event) {
|
||||
reject(event.target.error);
|
||||
txn.onerror = function() {
|
||||
reject(txn.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function reqAsEventPromise(req) {
|
||||
function reqAsEventPromise(req: IDBRequest): Promise<Event> {
|
||||
return new Promise((resolve, reject) => {
|
||||
req.onsuccess = function(event) {
|
||||
resolve(event);
|
||||
};
|
||||
req.onerror = function(event) {
|
||||
reject(event.target.error);
|
||||
req.onerror = function() {
|
||||
reject(req.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function reqAsPromise(req) {
|
||||
function reqAsPromise(req: IDBRequest): Promise<IDBRequest> {
|
||||
return new Promise((resolve, reject) => {
|
||||
req.onsuccess = () => resolve(req);
|
||||
req.onerror = (err) => reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
function reqAsCursorPromise(req) {
|
||||
return reqAsEventPromise(req).then((event) => event.target.result);
|
||||
function reqAsCursorPromise(req: IDBRequest<IDBCursor | null>): Promise<IDBCursor> {
|
||||
return reqAsEventPromise(req).then((event) => req.result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the actual reading from and writing to the indexeddb
|
||||
*
|
||||
* Construct a new Indexed Database store backend. This requires a call to
|
||||
* <code>connect()</code> before this store can be used.
|
||||
* @constructor
|
||||
* @param {Object} indexedDBInterface The Indexed DB interface e.g
|
||||
* <code>window.indexedDB</code>
|
||||
* @param {string=} dbName Optional database name. The same name must be used
|
||||
* to open the same database.
|
||||
*/
|
||||
export function LocalIndexedDBStoreBackend(
|
||||
indexedDBInterface, dbName,
|
||||
) {
|
||||
this.indexedDB = indexedDBInterface;
|
||||
this._dbName = "matrix-js-sdk:" + (dbName || "default");
|
||||
this.db = null;
|
||||
this._disconnected = true;
|
||||
this._syncAccumulator = new SyncAccumulator();
|
||||
this._isNewlyCreated = false;
|
||||
}
|
||||
export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
public static exists(indexedDB: IDBFactory, dbName: string): boolean {
|
||||
dbName = "matrix-js-sdk:" + (dbName || "default");
|
||||
return IndexedDBHelpers.exists(indexedDB, dbName);
|
||||
}
|
||||
|
||||
LocalIndexedDBStoreBackend.exists = function(indexedDB, dbName) {
|
||||
dbName = "matrix-js-sdk:" + (dbName || "default");
|
||||
return IndexedDBHelpers.exists(indexedDB, dbName);
|
||||
};
|
||||
private readonly dbName: string;
|
||||
private readonly syncAccumulator: SyncAccumulator;
|
||||
private db: IDBDatabase = null;
|
||||
private disconnected = true;
|
||||
private _isNewlyCreated = false;
|
||||
|
||||
/**
|
||||
* Does the actual reading from and writing to the indexeddb
|
||||
*
|
||||
* Construct a new Indexed Database store backend. This requires a call to
|
||||
* <code>connect()</code> before this store can be used.
|
||||
* @constructor
|
||||
* @param {Object} indexedDB The Indexed DB interface e.g
|
||||
* <code>window.indexedDB</code>
|
||||
* @param {string=} dbName Optional database name. The same name must be used
|
||||
* to open the same database.
|
||||
*/
|
||||
constructor(private readonly indexedDB: IDBFactory, dbName: string) {
|
||||
this.dbName = "matrix-js-sdk:" + (dbName || "default");
|
||||
this.syncAccumulator = new SyncAccumulator();
|
||||
}
|
||||
|
||||
LocalIndexedDBStoreBackend.prototype = {
|
||||
/**
|
||||
* Attempt to connect to the database. This can fail if the user does not
|
||||
* grant permission.
|
||||
* @return {Promise} Resolves if successfully connected.
|
||||
*/
|
||||
connect: function() {
|
||||
if (!this._disconnected) {
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend.connect: already connected or connecting`,
|
||||
);
|
||||
public connect(): Promise<void> {
|
||||
if (!this.disconnected) {
|
||||
logger.log(`LocalIndexedDBStoreBackend.connect: already connected or connecting`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this._disconnected = false;
|
||||
this.disconnected = false;
|
||||
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend.connect: connecting...`,
|
||||
);
|
||||
const req = this.indexedDB.open(this._dbName, VERSION);
|
||||
logger.log(`LocalIndexedDBStoreBackend.connect: connecting...`);
|
||||
const req = this.indexedDB.open(this.dbName, VERSION);
|
||||
req.onupgradeneeded = (ev) => {
|
||||
const db = ev.target.result;
|
||||
const db = req.result;
|
||||
const oldVersion = ev.oldVersion;
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`,
|
||||
@@ -178,19 +179,13 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
};
|
||||
|
||||
req.onblocked = () => {
|
||||
logger.log(
|
||||
`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`,
|
||||
);
|
||||
logger.log(`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`);
|
||||
};
|
||||
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend.connect: awaiting connection...`,
|
||||
);
|
||||
return reqAsEventPromise(req).then((ev) => {
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend.connect: connected`,
|
||||
);
|
||||
this.db = ev.target.result;
|
||||
logger.log(`LocalIndexedDBStoreBackend.connect: awaiting connection...`);
|
||||
return reqAsEventPromise(req).then(() => {
|
||||
logger.log(`LocalIndexedDBStoreBackend.connect: connected`);
|
||||
this.db = req.result;
|
||||
|
||||
// add a poorly-named listener for when deleteDatabase is called
|
||||
// so we can close our db connections.
|
||||
@@ -198,27 +193,26 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
this.db.close();
|
||||
};
|
||||
|
||||
return this._init();
|
||||
return this.init();
|
||||
});
|
||||
},
|
||||
/** @return {bool} whether or not the database was newly created in this session. */
|
||||
isNewlyCreated: function() {
|
||||
}
|
||||
|
||||
/** @return {boolean} whether or not the database was newly created in this session. */
|
||||
public isNewlyCreated(): Promise<boolean> {
|
||||
return Promise.resolve(this._isNewlyCreated);
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Having connected, load initial data from the database and prepare for use
|
||||
* @return {Promise} Resolves on success
|
||||
*/
|
||||
_init: function() {
|
||||
private init() {
|
||||
return Promise.all([
|
||||
this._loadAccountData(),
|
||||
this._loadSyncData(),
|
||||
this.loadAccountData(),
|
||||
this.loadSyncData(),
|
||||
]).then(([accountData, syncData]) => {
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend: loaded initial data`,
|
||||
);
|
||||
this._syncAccumulator.accumulate({
|
||||
logger.log(`LocalIndexedDBStoreBackend: loaded initial data`);
|
||||
this.syncAccumulator.accumulate({
|
||||
next_batch: syncData.nextBatch,
|
||||
rooms: syncData.roomsData,
|
||||
groups: syncData.groupsData,
|
||||
@@ -227,7 +221,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
},
|
||||
}, true);
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the out-of-band membership events for this room that
|
||||
@@ -236,8 +230,8 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
* @returns {Promise<event[]>} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||
* @returns {null} in case the members for this room haven't been stored yet
|
||||
*/
|
||||
getOutOfBandMembers: function(roomId) {
|
||||
return new Promise((resolve, reject) =>{
|
||||
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
|
||||
return new Promise<IEvent[] | null>((resolve, reject) =>{
|
||||
const tx = this.db.transaction(["oob_membership_events"], "readonly");
|
||||
const store = tx.objectStore("oob_membership_events");
|
||||
const roomIndex = store.index("room");
|
||||
@@ -252,8 +246,8 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
// were all known already
|
||||
let oobWritten = false;
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = event.target.result;
|
||||
request.onsuccess = () => {
|
||||
const cursor = request.result;
|
||||
if (!cursor) {
|
||||
// Unknown room
|
||||
if (!membershipEvents.length && !oobWritten) {
|
||||
@@ -273,11 +267,10 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
reject(err);
|
||||
};
|
||||
}).then((events) => {
|
||||
logger.log(`LL: got ${events && events.length}` +
|
||||
` membershipEvents from storage for room ${roomId} ...`);
|
||||
logger.log(`LL: got ${events && events.length} membershipEvents from storage for room ${roomId} ...`);
|
||||
return events;
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the out-of-band membership events for this room. Note that
|
||||
@@ -286,7 +279,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
* @param {string} roomId
|
||||
* @param {event[]} membershipEvents the membership events to store
|
||||
*/
|
||||
setOutOfBandMembers: async function(roomId, membershipEvents) {
|
||||
public async setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
||||
logger.log(`LL: backend about to store ${membershipEvents.length}` +
|
||||
` members for ${roomId}`);
|
||||
const tx = this.db.transaction(["oob_membership_events"], "readwrite");
|
||||
@@ -307,9 +300,9 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
store.put(markerObject);
|
||||
await txnAsPromise(tx);
|
||||
logger.log(`LL: backend done storing for ${roomId}!`);
|
||||
},
|
||||
}
|
||||
|
||||
clearOutOfBandMembers: async function(roomId) {
|
||||
public async clearOutOfBandMembers(roomId: string): Promise<void> {
|
||||
// the approach to delete all members for a room
|
||||
// is to get the min and max state key from the index
|
||||
// for that room, and then delete between those
|
||||
@@ -324,11 +317,11 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
const roomRange = IDBKeyRange.only(roomId);
|
||||
|
||||
const minStateKeyProm = reqAsCursorPromise(
|
||||
roomIndex.openKeyCursor(roomRange, "next"),
|
||||
).then((cursor) => cursor && cursor.primaryKey[1]);
|
||||
roomIndex.openKeyCursor(roomRange, "next"),
|
||||
).then((cursor) => cursor && cursor.primaryKey[1]);
|
||||
const maxStateKeyProm = reqAsCursorPromise(
|
||||
roomIndex.openKeyCursor(roomRange, "prev"),
|
||||
).then((cursor) => cursor && cursor.primaryKey[1]);
|
||||
roomIndex.openKeyCursor(roomRange, "prev"),
|
||||
).then((cursor) => cursor && cursor.primaryKey[1]);
|
||||
const [minStateKey, maxStateKey] = await Promise.all(
|
||||
[minStateKeyProm, maxStateKeyProm]);
|
||||
|
||||
@@ -341,45 +334,39 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
[roomId, maxStateKey],
|
||||
);
|
||||
|
||||
logger.log(`LL: Deleting all users + marker in storage for ` +
|
||||
`room ${roomId}, with key range:`,
|
||||
logger.log(`LL: Deleting all users + marker in storage for room ${roomId}, with key range:`,
|
||||
[roomId, minStateKey], [roomId, maxStateKey]);
|
||||
await reqAsPromise(writeStore.delete(membersKeyRange));
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the entire database. This should be used when logging out of a client
|
||||
* to prevent mixing data between accounts.
|
||||
* @return {Promise} Resolved when the database is cleared.
|
||||
*/
|
||||
clearDatabase: function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.log(`Removing indexeddb instance: ${this._dbName}`);
|
||||
const req = this.indexedDB.deleteDatabase(this._dbName);
|
||||
public clearDatabase(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
logger.log(`Removing indexeddb instance: ${this.dbName}`);
|
||||
const req = this.indexedDB.deleteDatabase(this.dbName);
|
||||
|
||||
req.onblocked = () => {
|
||||
logger.log(
|
||||
`can't yet delete indexeddb ${this._dbName}` +
|
||||
` because it is open elsewhere`,
|
||||
);
|
||||
logger.log(`can't yet delete indexeddb ${this.dbName} because it is open elsewhere`);
|
||||
};
|
||||
|
||||
req.onerror = (ev) => {
|
||||
req.onerror = () => {
|
||||
// in firefox, with indexedDB disabled, this fails with a
|
||||
// DOMError. We treat this as non-fatal, so that we can still
|
||||
// use the app.
|
||||
logger.warn(
|
||||
`unable to delete js-sdk store indexeddb: ${ev.target.error}`,
|
||||
);
|
||||
logger.warn(`unable to delete js-sdk store indexeddb: ${req.error}`);
|
||||
resolve();
|
||||
};
|
||||
|
||||
req.onsuccess = () => {
|
||||
logger.log(`Removed indexeddb instance: ${this._dbName}`);
|
||||
logger.log(`Removed indexeddb instance: ${this.dbName}`);
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean=} copy If false, the data returned is from internal
|
||||
@@ -390,10 +377,8 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
* client state to where it was at the last save, or null if there
|
||||
* is no saved sync data.
|
||||
*/
|
||||
getSavedSync: function(copy) {
|
||||
if (copy === undefined) copy = true;
|
||||
|
||||
const data = this._syncAccumulator.getJSON();
|
||||
public getSavedSync(copy = true): Promise<ISavedSync> {
|
||||
const data = this.syncAccumulator.getJSON();
|
||||
if (!data.nextBatch) return Promise.resolve(null);
|
||||
if (copy) {
|
||||
// We must deep copy the stored data so that the /sync processing code doesn't
|
||||
@@ -402,29 +387,27 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
} else {
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
getNextBatchToken: function() {
|
||||
return Promise.resolve(this._syncAccumulator.getNextBatchToken());
|
||||
},
|
||||
public getNextBatchToken(): Promise<string> {
|
||||
return Promise.resolve(this.syncAccumulator.getNextBatchToken());
|
||||
}
|
||||
|
||||
setSyncData: function(syncData) {
|
||||
public setSyncData(syncData: ISyncResponse): Promise<void> {
|
||||
return Promise.resolve().then(() => {
|
||||
this._syncAccumulator.accumulate(syncData);
|
||||
this.syncAccumulator.accumulate(syncData);
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
syncToDatabase: function(userTuples) {
|
||||
const syncData = this._syncAccumulator.getJSON(true);
|
||||
public async syncToDatabase(userTuples: UserTuple[]): Promise<void> {
|
||||
const syncData = this.syncAccumulator.getJSON(true);
|
||||
|
||||
return Promise.all([
|
||||
this._persistUserPresenceEvents(userTuples),
|
||||
this._persistAccountData(syncData.accountData),
|
||||
this._persistSyncData(
|
||||
syncData.nextBatch, syncData.roomsData, syncData.groupsData,
|
||||
),
|
||||
await Promise.all([
|
||||
this.persistUserPresenceEvents(userTuples),
|
||||
this.persistAccountData(syncData.accountData),
|
||||
this.persistSyncData(syncData.nextBatch, syncData.roomsData, syncData.groupsData),
|
||||
]);
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist rooms /sync data along with the next batch token.
|
||||
@@ -433,20 +416,24 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
* @param {Object} groupsData The 'groups' /sync data from a SyncAccumulator
|
||||
* @return {Promise} Resolves if the data was persisted.
|
||||
*/
|
||||
_persistSyncData: function(nextBatch, roomsData, groupsData) {
|
||||
private persistSyncData(
|
||||
nextBatch: string,
|
||||
roomsData: ISyncResponse["rooms"],
|
||||
groupsData: ISyncResponse["groups"],
|
||||
): Promise<void> {
|
||||
logger.log("Persisting sync data up to", nextBatch);
|
||||
return utils.promiseTry(() => {
|
||||
return utils.promiseTry<void>(() => {
|
||||
const txn = this.db.transaction(["sync"], "readwrite");
|
||||
const store = txn.objectStore("sync");
|
||||
store.put({
|
||||
clobber: "-", // constant key so will always clobber
|
||||
nextBatch: nextBatch,
|
||||
roomsData: roomsData,
|
||||
groupsData: groupsData,
|
||||
nextBatch,
|
||||
roomsData,
|
||||
groupsData,
|
||||
}); // put == UPSERT
|
||||
return txnAsPromise(txn);
|
||||
return txnAsPromise(txn).then();
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a list of account data events. Events with the same 'type' will
|
||||
@@ -454,16 +441,16 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
* @param {Object[]} accountData An array of raw user-scoped account data events
|
||||
* @return {Promise} Resolves if the events were persisted.
|
||||
*/
|
||||
_persistAccountData: function(accountData) {
|
||||
return utils.promiseTry(() => {
|
||||
private persistAccountData(accountData: IMinimalEvent[]): Promise<void> {
|
||||
return utils.promiseTry<void>(() => {
|
||||
const txn = this.db.transaction(["accountData"], "readwrite");
|
||||
const store = txn.objectStore("accountData");
|
||||
for (let i = 0; i < accountData.length; i++) {
|
||||
store.put(accountData[i]); // put == UPSERT
|
||||
}
|
||||
return txnAsPromise(txn);
|
||||
return txnAsPromise(txn).then();
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a list of [user id, presence event] they are for.
|
||||
@@ -473,8 +460,8 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
* @param {Object[]} tuples An array of [userid, event] tuples
|
||||
* @return {Promise} Resolves if the users were persisted.
|
||||
*/
|
||||
_persistUserPresenceEvents: function(tuples) {
|
||||
return utils.promiseTry(() => {
|
||||
private persistUserPresenceEvents(tuples: UserTuple[]): Promise<void> {
|
||||
return utils.promiseTry<void>(() => {
|
||||
const txn = this.db.transaction(["users"], "readwrite");
|
||||
const store = txn.objectStore("users");
|
||||
for (const tuple of tuples) {
|
||||
@@ -483,9 +470,9 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
event: tuple[1],
|
||||
}); // put == UPSERT
|
||||
}
|
||||
return txnAsPromise(txn);
|
||||
return txnAsPromise(txn).then();
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all user presence events from the database. This is not cached.
|
||||
@@ -493,64 +480,56 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
* sync.
|
||||
* @return {Promise<Object[]>} A list of presence events in their raw form.
|
||||
*/
|
||||
getUserPresenceEvents: function() {
|
||||
return utils.promiseTry(() => {
|
||||
public getUserPresenceEvents(): Promise<UserTuple[]> {
|
||||
return utils.promiseTry<UserTuple[]>(() => {
|
||||
const txn = this.db.transaction(["users"], "readonly");
|
||||
const store = txn.objectStore("users");
|
||||
return selectQuery(store, undefined, (cursor) => {
|
||||
return [cursor.value.userId, cursor.value.event];
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all the account data events from the database. This is not cached.
|
||||
* @return {Promise<Object[]>} A list of raw global account events.
|
||||
*/
|
||||
_loadAccountData: function() {
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend: loading account data...`,
|
||||
);
|
||||
return utils.promiseTry(() => {
|
||||
private loadAccountData(): Promise<IMinimalEvent[]> {
|
||||
logger.log(`LocalIndexedDBStoreBackend: loading account data...`);
|
||||
return utils.promiseTry<IMinimalEvent[]>(() => {
|
||||
const txn = this.db.transaction(["accountData"], "readonly");
|
||||
const store = txn.objectStore("accountData");
|
||||
return selectQuery(store, undefined, (cursor) => {
|
||||
return cursor.value;
|
||||
}).then((result) => {
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend: loaded account data`,
|
||||
);
|
||||
}).then((result: IMinimalEvent[]) => {
|
||||
logger.log(`LocalIndexedDBStoreBackend: loaded account data`);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the sync data from the database.
|
||||
* @return {Promise<Object>} An object with "roomsData" and "nextBatch" keys.
|
||||
*/
|
||||
_loadSyncData: function() {
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend: loading sync data...`,
|
||||
);
|
||||
return utils.promiseTry(() => {
|
||||
private loadSyncData(): Promise<ISyncData> {
|
||||
logger.log(`LocalIndexedDBStoreBackend: loading sync data...`);
|
||||
return utils.promiseTry<ISyncData>(() => {
|
||||
const txn = this.db.transaction(["sync"], "readonly");
|
||||
const store = txn.objectStore("sync");
|
||||
return selectQuery(store, undefined, (cursor) => {
|
||||
return cursor.value;
|
||||
}).then((results) => {
|
||||
logger.log(
|
||||
`LocalIndexedDBStoreBackend: loaded sync data`,
|
||||
);
|
||||
}).then((results: ISyncData[]) => {
|
||||
logger.log(`LocalIndexedDBStoreBackend: loaded sync data`);
|
||||
if (results.length > 1) {
|
||||
logger.warn("loadSyncData: More than 1 sync row found.");
|
||||
}
|
||||
return (results.length > 0 ? results[0] : {});
|
||||
return results.length > 0 ? results[0] : {} as ISyncData;
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
getClientOptions: function() {
|
||||
public getClientOptions(): Promise<IStartClientOpts> {
|
||||
return Promise.resolve().then(() => {
|
||||
const txn = this.db.transaction(["client_options"], "readonly");
|
||||
const store = txn.objectStore("client_options");
|
||||
@@ -560,9 +539,9 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
}
|
||||
}).then((results) => results[0]);
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
storeClientOptions: async function(options) {
|
||||
public async storeClientOptions(options: IStartClientOpts): Promise<void> {
|
||||
const txn = this.db.transaction(["client_options"], "readwrite");
|
||||
const store = txn.objectStore("client_options");
|
||||
store.put({
|
||||
@@ -570,5 +549,5 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
options: options,
|
||||
}); // put == UPSERT
|
||||
await txnAsPromise(txn);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,196 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 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 { logger } from '../logger';
|
||||
import { defer } from '../utils';
|
||||
|
||||
/**
|
||||
* An IndexedDB store backend where the actual backend sits in a web
|
||||
* worker.
|
||||
*
|
||||
* Construct a new Indexed Database store backend. This requires a call to
|
||||
* <code>connect()</code> before this store can be used.
|
||||
* @constructor
|
||||
* @param {string} workerScript URL to the worker script
|
||||
* @param {string=} dbName Optional database name. The same name must be used
|
||||
* to open the same database.
|
||||
* @param {Object} workerApi The web worker compatible interface object
|
||||
*/
|
||||
export function RemoteIndexedDBStoreBackend(
|
||||
workerScript, dbName, workerApi,
|
||||
) {
|
||||
this._workerScript = workerScript;
|
||||
this._dbName = dbName;
|
||||
this._workerApi = workerApi;
|
||||
this._worker = null;
|
||||
this._nextSeq = 0;
|
||||
// The currently in-flight requests to the actual backend
|
||||
this._inFlight = {
|
||||
// seq: promise,
|
||||
};
|
||||
// Once we start connecting, we keep the promise and re-use it
|
||||
// if we try to connect again
|
||||
this._startPromise = null;
|
||||
}
|
||||
|
||||
RemoteIndexedDBStoreBackend.prototype = {
|
||||
/**
|
||||
* Attempt to connect to the database. This can fail if the user does not
|
||||
* grant permission.
|
||||
* @return {Promise} Resolves if successfully connected.
|
||||
*/
|
||||
connect: function() {
|
||||
return this._ensureStarted().then(() => this._doCmd('connect'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the entire database. This should be used when logging out of a client
|
||||
* to prevent mixing data between accounts.
|
||||
* @return {Promise} Resolved when the database is cleared.
|
||||
*/
|
||||
clearDatabase: function() {
|
||||
return this._ensureStarted().then(() => this._doCmd('clearDatabase'));
|
||||
},
|
||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
||||
isNewlyCreated: function() {
|
||||
return this._doCmd('isNewlyCreated');
|
||||
},
|
||||
/**
|
||||
* @return {Promise} Resolves with a sync response to restore the
|
||||
* client state to where it was at the last save, or null if there
|
||||
* is no saved sync data.
|
||||
*/
|
||||
getSavedSync: function() {
|
||||
return this._doCmd('getSavedSync');
|
||||
},
|
||||
|
||||
getNextBatchToken: function() {
|
||||
return this._doCmd('getNextBatchToken');
|
||||
},
|
||||
|
||||
setSyncData: function(syncData) {
|
||||
return this._doCmd('setSyncData', [syncData]);
|
||||
},
|
||||
|
||||
syncToDatabase: function(users) {
|
||||
return this._doCmd('syncToDatabase', [users]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the out-of-band membership events for this room that
|
||||
* were previously loaded.
|
||||
* @param {string} roomId
|
||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||
* @returns {null} in case the members for this room haven't been stored yet
|
||||
*/
|
||||
getOutOfBandMembers: function(roomId) {
|
||||
return this._doCmd('getOutOfBandMembers', [roomId]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores the out-of-band membership events for this room. Note that
|
||||
* it still makes sense to store an empty array as the OOB status for the room is
|
||||
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
|
||||
* @param {string} roomId
|
||||
* @param {event[]} membershipEvents the membership events to store
|
||||
* @returns {Promise} when all members have been stored
|
||||
*/
|
||||
setOutOfBandMembers: function(roomId, membershipEvents) {
|
||||
return this._doCmd('setOutOfBandMembers', [roomId, membershipEvents]);
|
||||
},
|
||||
|
||||
clearOutOfBandMembers: function(roomId) {
|
||||
return this._doCmd('clearOutOfBandMembers', [roomId]);
|
||||
},
|
||||
|
||||
getClientOptions: function() {
|
||||
return this._doCmd('getClientOptions');
|
||||
},
|
||||
|
||||
storeClientOptions: function(options) {
|
||||
return this._doCmd('storeClientOptions', [options]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all user presence events from the database. This is not cached.
|
||||
* @return {Promise<Object[]>} A list of presence events in their raw form.
|
||||
*/
|
||||
getUserPresenceEvents: function() {
|
||||
return this._doCmd('getUserPresenceEvents');
|
||||
},
|
||||
|
||||
_ensureStarted: function() {
|
||||
if (this._startPromise === null) {
|
||||
this._worker = new this._workerApi(this._workerScript);
|
||||
this._worker.onmessage = this._onWorkerMessage.bind(this);
|
||||
|
||||
// tell the worker the db name.
|
||||
this._startPromise = this._doCmd('_setupWorker', [this._dbName]).then(() => {
|
||||
logger.log("IndexedDB worker is ready");
|
||||
});
|
||||
}
|
||||
return this._startPromise;
|
||||
},
|
||||
|
||||
_doCmd: function(cmd, args) {
|
||||
// wrap in a q so if the postMessage throws,
|
||||
// the promise automatically gets rejected
|
||||
return Promise.resolve().then(() => {
|
||||
const seq = this._nextSeq++;
|
||||
const def = defer();
|
||||
|
||||
this._inFlight[seq] = def;
|
||||
|
||||
this._worker.postMessage({
|
||||
command: cmd,
|
||||
seq: seq,
|
||||
args: args,
|
||||
});
|
||||
|
||||
return def.promise;
|
||||
});
|
||||
},
|
||||
|
||||
_onWorkerMessage: function(ev) {
|
||||
const msg = ev.data;
|
||||
|
||||
if (msg.command == 'cmd_success' || msg.command == 'cmd_fail') {
|
||||
if (msg.seq === undefined) {
|
||||
logger.error("Got reply from worker with no seq");
|
||||
return;
|
||||
}
|
||||
|
||||
const def = this._inFlight[msg.seq];
|
||||
if (def === undefined) {
|
||||
logger.error("Got reply for unknown seq " + msg.seq);
|
||||
return;
|
||||
}
|
||||
delete this._inFlight[msg.seq];
|
||||
|
||||
if (msg.command == 'cmd_success') {
|
||||
def.resolve(msg.result);
|
||||
} else {
|
||||
const error = new Error(msg.error.message);
|
||||
error.name = msg.error.name;
|
||||
def.reject(error);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Unrecognised message from worker: " + msg);
|
||||
}
|
||||
},
|
||||
};
|
192
src/store/indexeddb-remote-backend.ts
Normal file
192
src/store/indexeddb-remote-backend.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
Copyright 2017 - 2021 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 { logger } from "../logger";
|
||||
import { defer, IDeferred } from "../utils";
|
||||
import { ISavedSync } from "./index";
|
||||
import { IStartClientOpts } from "../client";
|
||||
import { IEvent, ISyncResponse } from "..";
|
||||
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
|
||||
|
||||
export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
private worker: Worker;
|
||||
private nextSeq = 0;
|
||||
// The currently in-flight requests to the actual backend
|
||||
private inFlight: Record<number, IDeferred<any>> = {}; // seq: promise
|
||||
// Once we start connecting, we keep the promise and re-use it
|
||||
// if we try to connect again
|
||||
private startPromise: Promise<void> = null;
|
||||
|
||||
/**
|
||||
* An IndexedDB store backend where the actual backend sits in a web
|
||||
* worker.
|
||||
*
|
||||
* Construct a new Indexed Database store backend. This requires a call to
|
||||
* <code>connect()</code> before this store can be used.
|
||||
* @constructor
|
||||
* @param {Function} workerFactory Factory which produces a Worker
|
||||
* @param {string=} dbName Optional database name. The same name must be used
|
||||
* to open the same database.
|
||||
*/
|
||||
constructor(
|
||||
private readonly workerFactory: () => Worker,
|
||||
private readonly dbName: string,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Attempt to connect to the database. This can fail if the user does not
|
||||
* grant permission.
|
||||
* @return {Promise} Resolves if successfully connected.
|
||||
*/
|
||||
public connect(): Promise<void> {
|
||||
return this.ensureStarted().then(() => this.doCmd('connect'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the entire database. This should be used when logging out of a client
|
||||
* to prevent mixing data between accounts.
|
||||
* @return {Promise} Resolved when the database is cleared.
|
||||
*/
|
||||
public clearDatabase(): Promise<void> {
|
||||
return this.ensureStarted().then(() => this.doCmd('clearDatabase'));
|
||||
}
|
||||
|
||||
/** @return {Promise<boolean>} whether or not the database was newly created in this session. */
|
||||
public isNewlyCreated(): Promise<boolean> {
|
||||
return this.doCmd('isNewlyCreated');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Promise} Resolves with a sync response to restore the
|
||||
* client state to where it was at the last save, or null if there
|
||||
* is no saved sync data.
|
||||
*/
|
||||
public getSavedSync(): Promise<ISavedSync> {
|
||||
return this.doCmd('getSavedSync');
|
||||
}
|
||||
|
||||
public getNextBatchToken(): Promise<string> {
|
||||
return this.doCmd('getNextBatchToken');
|
||||
}
|
||||
|
||||
public setSyncData(syncData: ISyncResponse): Promise<void> {
|
||||
return this.doCmd('setSyncData', [syncData]);
|
||||
}
|
||||
|
||||
public syncToDatabase(userTuples: UserTuple[]): Promise<void> {
|
||||
return this.doCmd('syncToDatabase', [userTuples]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the out-of-band membership events for this room that
|
||||
* were previously loaded.
|
||||
* @param {string} roomId
|
||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||
* @returns {null} in case the members for this room haven't been stored yet
|
||||
*/
|
||||
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
|
||||
return this.doCmd('getOutOfBandMembers', [roomId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the out-of-band membership events for this room. Note that
|
||||
* it still makes sense to store an empty array as the OOB status for the room is
|
||||
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
|
||||
* @param {string} roomId
|
||||
* @param {event[]} membershipEvents the membership events to store
|
||||
* @returns {Promise} when all members have been stored
|
||||
*/
|
||||
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
||||
return this.doCmd('setOutOfBandMembers', [roomId, membershipEvents]);
|
||||
}
|
||||
|
||||
public clearOutOfBandMembers(roomId: string): Promise<void> {
|
||||
return this.doCmd('clearOutOfBandMembers', [roomId]);
|
||||
}
|
||||
|
||||
public getClientOptions(): Promise<IStartClientOpts> {
|
||||
return this.doCmd('getClientOptions');
|
||||
}
|
||||
|
||||
public storeClientOptions(options: IStartClientOpts): Promise<void> {
|
||||
return this.doCmd('storeClientOptions', [options]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all user presence events from the database. This is not cached.
|
||||
* @return {Promise<Object[]>} A list of presence events in their raw form.
|
||||
*/
|
||||
public getUserPresenceEvents(): Promise<UserTuple[]> {
|
||||
return this.doCmd('getUserPresenceEvents');
|
||||
}
|
||||
|
||||
private ensureStarted(): Promise<void> {
|
||||
if (this.startPromise === null) {
|
||||
this.worker = this.workerFactory();
|
||||
this.worker.onmessage = this.onWorkerMessage;
|
||||
|
||||
// tell the worker the db name.
|
||||
this.startPromise = this.doCmd('_setupWorker', [this.dbName]).then(() => {
|
||||
logger.log("IndexedDB worker is ready");
|
||||
});
|
||||
}
|
||||
return this.startPromise;
|
||||
}
|
||||
|
||||
private doCmd<T>(command: string, args?: any): Promise<T> {
|
||||
// wrap in a q so if the postMessage throws,
|
||||
// the promise automatically gets rejected
|
||||
return Promise.resolve().then(() => {
|
||||
const seq = this.nextSeq++;
|
||||
const def = defer<T>();
|
||||
|
||||
this.inFlight[seq] = def;
|
||||
|
||||
this.worker.postMessage({ command, seq, args });
|
||||
|
||||
return def.promise;
|
||||
});
|
||||
}
|
||||
|
||||
private onWorkerMessage = (ev: MessageEvent): void => {
|
||||
const msg = ev.data;
|
||||
|
||||
if (msg.command == 'cmd_success' || msg.command == 'cmd_fail') {
|
||||
if (msg.seq === undefined) {
|
||||
logger.error("Got reply from worker with no seq");
|
||||
return;
|
||||
}
|
||||
|
||||
const def = this.inFlight[msg.seq];
|
||||
if (def === undefined) {
|
||||
logger.error("Got reply for unknown seq " + msg.seq);
|
||||
return;
|
||||
}
|
||||
delete this.inFlight[msg.seq];
|
||||
|
||||
if (msg.command == 'cmd_success') {
|
||||
def.resolve(msg.result);
|
||||
} else {
|
||||
const error = new Error(msg.error.message);
|
||||
error.name = msg.error.name;
|
||||
def.reject(error);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Unrecognised message from worker: ", msg);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2017 - 2021 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.
|
||||
@@ -16,9 +14,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend.js";
|
||||
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend";
|
||||
import { logger } from '../logger';
|
||||
|
||||
interface ICmd {
|
||||
command: string;
|
||||
seq: number;
|
||||
args?: any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* This class lives in the webworker and drives a LocalIndexedDBStoreBackend
|
||||
* controlled by messages from the main process.
|
||||
@@ -35,16 +39,13 @@ import { logger } from '../logger';
|
||||
*
|
||||
*/
|
||||
export class IndexedDBStoreWorker {
|
||||
private backend: LocalIndexedDBStoreBackend = null;
|
||||
|
||||
/**
|
||||
* @param {function} postMessage The web worker postMessage function that
|
||||
* should be used to communicate back to the main script.
|
||||
*/
|
||||
constructor(postMessage) {
|
||||
this.backend = null;
|
||||
this.postMessage = postMessage;
|
||||
|
||||
this.onMessage = this.onMessage.bind(this);
|
||||
}
|
||||
constructor(private readonly postMessage: InstanceType<typeof Worker>["postMessage"]) {}
|
||||
|
||||
/**
|
||||
* Passes a message event from the main script into the class. This method
|
||||
@@ -52,17 +53,15 @@ export class IndexedDBStoreWorker {
|
||||
*
|
||||
* @param {Object} ev The message event
|
||||
*/
|
||||
onMessage(ev) {
|
||||
const msg = ev.data;
|
||||
public onMessage = (ev: MessageEvent): void => {
|
||||
const msg: ICmd = ev.data;
|
||||
let prom;
|
||||
|
||||
switch (msg.command) {
|
||||
case '_setupWorker':
|
||||
this.backend = new LocalIndexedDBStoreBackend(
|
||||
// this is the 'indexedDB' global (where global != window
|
||||
// because it's a web worker and there is no window).
|
||||
indexedDB, msg.args[0],
|
||||
);
|
||||
// this is the 'indexedDB' global (where global != window
|
||||
// because it's a web worker and there is no window).
|
||||
this.backend = new LocalIndexedDBStoreBackend(indexedDB, msg.args[0]);
|
||||
prom = Promise.resolve();
|
||||
break;
|
||||
case 'connect':
|
||||
@@ -72,23 +71,16 @@ export class IndexedDBStoreWorker {
|
||||
prom = this.backend.isNewlyCreated();
|
||||
break;
|
||||
case 'clearDatabase':
|
||||
prom = this.backend.clearDatabase().then((result) => {
|
||||
// This returns special classes which can't be cloned
|
||||
// across to the main script, so don't try.
|
||||
return {};
|
||||
});
|
||||
prom = this.backend.clearDatabase();
|
||||
break;
|
||||
case 'getSavedSync':
|
||||
prom = this.backend.getSavedSync(false);
|
||||
break;
|
||||
case 'setSyncData':
|
||||
prom = this.backend.setSyncData(...msg.args);
|
||||
prom = this.backend.setSyncData(msg.args[0]);
|
||||
break;
|
||||
case 'syncToDatabase':
|
||||
prom = this.backend.syncToDatabase(...msg.args).then(() => {
|
||||
// This also returns IndexedDB events which are not cloneable
|
||||
return {};
|
||||
});
|
||||
prom = this.backend.syncToDatabase(msg.args[0]);
|
||||
break;
|
||||
case 'getUserPresenceEvents':
|
||||
prom = this.backend.getUserPresenceEvents();
|
||||
@@ -130,7 +122,7 @@ export class IndexedDBStoreWorker {
|
||||
result: ret,
|
||||
});
|
||||
}, (err) => {
|
||||
logger.error("Error running command: "+msg.command);
|
||||
logger.error("Error running command: " + msg.command);
|
||||
logger.error(err);
|
||||
this.postMessage.call(null, {
|
||||
command: 'cmd_fail',
|
||||
@@ -142,5 +134,5 @@ export class IndexedDBStoreWorker {
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
@@ -19,12 +19,14 @@ limitations under the License.
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { MemoryStore, IOpts as IBaseOpts } from "./memory";
|
||||
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend.js";
|
||||
import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend.js";
|
||||
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend";
|
||||
import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend";
|
||||
import { User } from "../models/user";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { IEvent, MatrixEvent } from "../models/event";
|
||||
import { logger } from '../logger';
|
||||
import { ISavedSync } from "./index";
|
||||
import { IIndexedDBBackend } from "./indexeddb-backend";
|
||||
import { ISyncResponse } from "../sync-accumulator";
|
||||
|
||||
/**
|
||||
* This is an internal module. See {@link IndexedDBStore} for the public class.
|
||||
@@ -41,8 +43,7 @@ const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
|
||||
interface IOpts extends IBaseOpts {
|
||||
indexedDB: IDBFactory;
|
||||
dbName?: string;
|
||||
workerScript?: string;
|
||||
workerApi?: typeof Worker;
|
||||
workerFactory?: () => Worker;
|
||||
}
|
||||
|
||||
export class IndexedDBStore extends MemoryStore {
|
||||
@@ -50,8 +51,7 @@ export class IndexedDBStore extends MemoryStore {
|
||||
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
|
||||
}
|
||||
|
||||
// TODO these should conform to one interface
|
||||
public readonly backend: LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend;
|
||||
public readonly backend: IIndexedDBBackend;
|
||||
|
||||
private startedUp = false;
|
||||
private syncTs = 0;
|
||||
@@ -110,16 +110,8 @@ export class IndexedDBStore extends MemoryStore {
|
||||
throw new Error('Missing required option: indexedDB');
|
||||
}
|
||||
|
||||
if (opts.workerScript) {
|
||||
// try & find a webworker-compatible API
|
||||
let workerApi = opts.workerApi;
|
||||
if (!workerApi) {
|
||||
// default to the global Worker object (which is where it in a browser)
|
||||
workerApi = global.Worker;
|
||||
}
|
||||
this.backend = new RemoteIndexedDBStoreBackend(
|
||||
opts.workerScript, opts.dbName, workerApi,
|
||||
);
|
||||
if (opts.workerFactory) {
|
||||
this.backend = new RemoteIndexedDBStoreBackend(opts.workerFactory, opts.dbName);
|
||||
} else {
|
||||
this.backend = new LocalIndexedDBStoreBackend(opts.indexedDB, opts.dbName);
|
||||
}
|
||||
@@ -222,7 +214,7 @@ export class IndexedDBStore extends MemoryStore {
|
||||
|
||||
// work out changed users (this doesn't handle deletions but you
|
||||
// can't 'delete' users as they are just presence events).
|
||||
const userTuples = [];
|
||||
const userTuples: [userId: string, presenceEvent: Partial<IEvent>][] = [];
|
||||
for (const u of this.getUsers()) {
|
||||
if (this.userModifiedMap[u.userId] === u.getLastModifiedTime()) continue;
|
||||
if (!u.events.presence) continue;
|
||||
@@ -236,7 +228,7 @@ export class IndexedDBStore extends MemoryStore {
|
||||
return this.backend.syncToDatabase(userTuples);
|
||||
});
|
||||
|
||||
public setSyncData = this.degradable((syncData: object): Promise<void> => {
|
||||
public setSyncData = this.degradable((syncData: ISyncResponse): Promise<void> => {
|
||||
return this.backend.setSyncData(syncData);
|
||||
}, "setSyncData");
|
||||
|
||||
@@ -247,7 +239,7 @@ export class IndexedDBStore extends MemoryStore {
|
||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||
* @returns {null} in case the members for this room haven't been stored yet
|
||||
*/
|
||||
public getOutOfBandMembers = this.degradable((roomId: string): Promise<MatrixEvent[]> => {
|
||||
public getOutOfBandMembers = this.degradable((roomId: string): Promise<IEvent[]> => {
|
||||
return this.backend.getOutOfBandMembers(roomId);
|
||||
}, "getOutOfBandMembers");
|
||||
|
||||
@@ -259,7 +251,7 @@ export class IndexedDBStore extends MemoryStore {
|
||||
* @param {event[]} membershipEvents the membership events to store
|
||||
* @returns {Promise} when all members have been stored
|
||||
*/
|
||||
public setOutOfBandMembers = this.degradable((roomId: string, membershipEvents: MatrixEvent[]): Promise<void> => {
|
||||
public setOutOfBandMembers = this.degradable((roomId: string, membershipEvents: IEvent[]): Promise<void> => {
|
||||
super.setOutOfBandMembers(roomId, membershipEvents);
|
||||
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
|
||||
}, "setOutOfBandMembers");
|
||||
|
@@ -23,12 +23,13 @@ import { EventType } from "../@types/event";
|
||||
import { Group } from "../models/group";
|
||||
import { Room } from "../models/room";
|
||||
import { User } from "../models/user";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { IEvent, MatrixEvent } from "../models/event";
|
||||
import { RoomState } from "../models/room-state";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { Filter } from "../filter";
|
||||
import { ISavedSync, IStore } from "./index";
|
||||
import { RoomSummary } from "../models/room-summary";
|
||||
import { ISyncResponse } from "../sync-accumulator";
|
||||
|
||||
function isValidFilterId(filterId: string): boolean {
|
||||
const isValidStr = typeof filterId === "string" &&
|
||||
@@ -59,9 +60,9 @@ export class MemoryStore implements IStore {
|
||||
// filterId: Filter
|
||||
// }
|
||||
private filters: Record<string, Record<string, Filter>> = {};
|
||||
private accountData: Record<string, MatrixEvent> = {}; // type : content
|
||||
public accountData: Record<string, MatrixEvent> = {}; // type : content
|
||||
private readonly localStorage: Storage;
|
||||
private oobMembers: Record<string, MatrixEvent[]> = {}; // roomId: [member events]
|
||||
private oobMembers: Record<string, IEvent[]> = {}; // roomId: [member events]
|
||||
private clientOptions = {};
|
||||
|
||||
constructor(opts: IOpts = {}) {
|
||||
@@ -340,7 +341,7 @@ export class MemoryStore implements IStore {
|
||||
* @param {Object} syncData The sync data
|
||||
* @return {Promise} An immediately resolved promise.
|
||||
*/
|
||||
public setSyncData(syncData: object): Promise<void> {
|
||||
public setSyncData(syncData: ISyncResponse): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -415,7 +416,7 @@ export class MemoryStore implements IStore {
|
||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||
* @returns {null} in case the members for this room haven't been stored yet
|
||||
*/
|
||||
public getOutOfBandMembers(roomId: string): Promise<MatrixEvent[] | null> {
|
||||
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
|
||||
return Promise.resolve(this.oobMembers[roomId] || null);
|
||||
}
|
||||
|
||||
@@ -427,7 +428,7 @@ export class MemoryStore implements IStore {
|
||||
* @param {event[]} membershipEvents the membership events to store
|
||||
* @returns {Promise} when all members have been stored
|
||||
*/
|
||||
public setOutOfBandMembers(roomId: string, membershipEvents: MatrixEvent[]): Promise<void> {
|
||||
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
||||
this.oobMembers[roomId] = membershipEvents;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@@ -23,19 +23,21 @@ import { EventType } from "../@types/event";
|
||||
import { Group } from "../models/group";
|
||||
import { Room } from "../models/room";
|
||||
import { User } from "../models/user";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { IEvent, MatrixEvent } from "../models/event";
|
||||
import { Filter } from "../filter";
|
||||
import { ISavedSync, IStore } from "./index";
|
||||
import { RoomSummary } from "../models/room-summary";
|
||||
import { ISyncResponse } from "../sync-accumulator";
|
||||
|
||||
/**
|
||||
* Construct a stub store. This does no-ops on most store methods.
|
||||
* @constructor
|
||||
*/
|
||||
export class StubStore implements IStore {
|
||||
public readonly accountData = {}; // stub
|
||||
private fromToken: string = null;
|
||||
|
||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
||||
/** @return {Promise<boolean>} whether or not the database was newly created in this session. */
|
||||
public isNewlyCreated(): Promise<boolean> {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
@@ -212,7 +214,7 @@ export class StubStore implements IStore {
|
||||
* @param {Object} syncData The sync data
|
||||
* @return {Promise} An immediately resolved promise.
|
||||
*/
|
||||
public setSyncData(syncData: object): Promise<void> {
|
||||
public setSyncData(syncData: ISyncResponse): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -264,11 +266,11 @@ export class StubStore implements IStore {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public getOutOfBandMembers(): Promise<MatrixEvent[]> {
|
||||
public getOutOfBandMembers(): Promise<IEvent[]> {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
public setOutOfBandMembers(roomId: string, membershipEvents: MatrixEvent[]): Promise<void> {
|
||||
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
@@ -40,8 +40,8 @@ export interface IEphemeral {
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IUnreadNotificationCounts {
|
||||
highlight_count: number;
|
||||
notification_count: number;
|
||||
highlight_count?: number;
|
||||
notification_count?: number;
|
||||
}
|
||||
|
||||
export interface IRoomEvent extends IMinimalEvent {
|
||||
@@ -64,7 +64,7 @@ interface IState {
|
||||
|
||||
export interface ITimeline {
|
||||
events: Array<IRoomEvent | IStateEvent>;
|
||||
limited: boolean;
|
||||
limited?: boolean;
|
||||
prev_batch: string;
|
||||
}
|
||||
|
||||
@@ -169,6 +169,13 @@ interface IRoom {
|
||||
};
|
||||
}
|
||||
|
||||
export interface ISyncData {
|
||||
nextBatch: string;
|
||||
accountData: IMinimalEvent[];
|
||||
roomsData: IRooms;
|
||||
groupsData: IGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* The purpose of this class is to accumulate /sync responses such that a
|
||||
* complete "initial" JSON response can be returned which accurately represents
|
||||
@@ -544,8 +551,8 @@ export class SyncAccumulator {
|
||||
* /sync response from the 'rooms' key onwards. The "accountData" key is
|
||||
* a list of raw events which represent global account data.
|
||||
*/
|
||||
public getJSON(forDatabase = false): object {
|
||||
const data = {
|
||||
public getJSON(forDatabase = false): ISyncData {
|
||||
const data: IRooms = {
|
||||
join: {},
|
||||
invite: {},
|
||||
// always empty. This is set by /sync when a room was previously
|
||||
@@ -575,7 +582,7 @@ export class SyncAccumulator {
|
||||
prev_batch: null,
|
||||
},
|
||||
unread_notifications: roomData._unreadNotifications,
|
||||
summary: roomData._summary,
|
||||
summary: roomData._summary as IRoomSummary,
|
||||
};
|
||||
// Add account data
|
||||
Object.keys(roomData._accountData).forEach((evType) => {
|
||||
@@ -678,7 +685,7 @@ export class SyncAccumulator {
|
||||
});
|
||||
|
||||
// Add account data
|
||||
const accData = [];
|
||||
const accData: IMinimalEvent[] = [];
|
||||
Object.keys(this.accountData).forEach((evType) => {
|
||||
accData.push(this.accountData[evType]);
|
||||
});
|
||||
|
17
src/sync.ts
17
src/sync.ts
@@ -136,7 +136,7 @@ export class SyncApi {
|
||||
private syncStateData: ISyncStateData = null; // additional data (eg. error object for failed sync)
|
||||
private catchingUp = false;
|
||||
private running = false;
|
||||
private keepAliveTimer: NodeJS.Timeout = null;
|
||||
private keepAliveTimer: number = null;
|
||||
private connectionReturnedDefer: IDeferred<boolean> = null;
|
||||
private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response
|
||||
private failedSyncCount = 0; // Number of consecutive failed /sync requests
|
||||
@@ -319,7 +319,7 @@ export class SyncApi {
|
||||
this._peekRoom = this.createRoom(roomId);
|
||||
return this.client.roomInitialSync(roomId, 20).then((response) => {
|
||||
// make sure things are init'd
|
||||
response.messages = response.messages || {};
|
||||
response.messages = response.messages || { chunk: [] };
|
||||
response.messages.chunk = response.messages.chunk || [];
|
||||
response.state = response.state || [];
|
||||
|
||||
@@ -330,8 +330,7 @@ export class SyncApi {
|
||||
const stateEvents = response.state.map(client.getEventMapper());
|
||||
const messages = response.messages.chunk.map(client.getEventMapper());
|
||||
|
||||
// XXX: copypasted from /sync until we kill off this
|
||||
// minging v1 API stuff)
|
||||
// XXX: copypasted from /sync until we kill off this minging v1 API stuff)
|
||||
// handle presence events (User objects)
|
||||
if (response.presence && Array.isArray(response.presence)) {
|
||||
response.presence.map(client.getEventMapper()).forEach(
|
||||
@@ -644,12 +643,12 @@ export class SyncApi {
|
||||
// Now wait for the saved sync to finish...
|
||||
debuglog("Waiting for saved sync before starting sync processing...");
|
||||
await savedSyncPromise;
|
||||
this._sync({ filterId });
|
||||
this.doSync({ filterId });
|
||||
};
|
||||
|
||||
if (client.isGuest()) {
|
||||
// no push rules for guests, no access to POST filter for guests.
|
||||
this._sync({});
|
||||
this.doSync({});
|
||||
} else {
|
||||
// Pull the saved sync token out first, before the worker starts sending
|
||||
// all the sync data which could take a while. This will let us send our
|
||||
@@ -755,7 +754,7 @@ export class SyncApi {
|
||||
* @param {string} syncOptions.filterId
|
||||
* @param {boolean} syncOptions.hasSyncedBefore
|
||||
*/
|
||||
private async _sync(syncOptions: ISyncOptions): Promise<void> {
|
||||
private async doSync(syncOptions: ISyncOptions): Promise<void> {
|
||||
const client = this.client;
|
||||
|
||||
if (!this.running) {
|
||||
@@ -852,7 +851,7 @@ export class SyncApi {
|
||||
}
|
||||
|
||||
// Begin next sync
|
||||
this._sync(syncOptions);
|
||||
this.doSync(syncOptions);
|
||||
}
|
||||
|
||||
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IRequestPromise<ISyncResponse> {
|
||||
@@ -957,7 +956,7 @@ export class SyncApi {
|
||||
catchingUp: true,
|
||||
});
|
||||
}
|
||||
this._sync(syncOptions);
|
||||
this.doSync(syncOptions);
|
||||
});
|
||||
|
||||
this.currentSyncRequest = null;
|
||||
|
49
src/utils.ts
49
src/utils.ts
@@ -31,15 +31,28 @@ import type NodeCrypto from "crypto";
|
||||
* @return {string} The encoded string e.g. foo=bar&baz=taz
|
||||
*/
|
||||
export function encodeParams(params: Record<string, string>): string {
|
||||
let qs = "";
|
||||
for (const key in params) {
|
||||
if (!params.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
qs += "&" + encodeURIComponent(key) + "=" +
|
||||
encodeURIComponent(params[key]);
|
||||
return new URLSearchParams(params).toString();
|
||||
}
|
||||
|
||||
export type QueryDict = Record<string, string | string[]>;
|
||||
|
||||
/**
|
||||
* Decode a query string in `application/x-www-form-urlencoded` format.
|
||||
* @param {string} query A query string to decode e.g.
|
||||
* foo=bar&via=server1&server2
|
||||
* @return {Object} The decoded object, if any keys occurred multiple times
|
||||
* then the value will be an array of strings, else it will be an array.
|
||||
* This behaviour matches Node's qs.parse but is built on URLSearchParams
|
||||
* for native web compatibility
|
||||
*/
|
||||
export function decodeParams(query: string): QueryDict {
|
||||
const o: QueryDict = {};
|
||||
const params = new URLSearchParams(query);
|
||||
for (const key of params.keys()) {
|
||||
const val = params.getAll(key);
|
||||
o[key] = val.length === 1 ? val[0] : val;
|
||||
}
|
||||
return qs.substring(1);
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,10 +129,10 @@ export function isFunction(value: any) {
|
||||
* @throws If the object is missing keys.
|
||||
*/
|
||||
// note using 'keys' here would shadow the 'keys' function defined above
|
||||
export function checkObjectHasKeys(obj: object, keys_: string[]) {
|
||||
for (let i = 0; i < keys_.length; i++) {
|
||||
if (!obj.hasOwnProperty(keys_[i])) {
|
||||
throw new Error("Missing required key: " + keys_[i]);
|
||||
export function checkObjectHasKeys(obj: object, keys: string[]) {
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
if (!obj.hasOwnProperty(keys[i])) {
|
||||
throw new Error("Missing required key: " + keys[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -464,7 +477,7 @@ export async function promiseMapSeries<T>(
|
||||
}
|
||||
}
|
||||
|
||||
export function promiseTry<T>(fn: () => T): Promise<T> {
|
||||
export function promiseTry<T>(fn: () => T | Promise<T>): Promise<T> {
|
||||
return new Promise((resolve) => resolve(fn()));
|
||||
}
|
||||
|
||||
@@ -670,3 +683,13 @@ export function lexicographicCompare(a: string, b: string): number {
|
||||
// hidden the operation in this function.
|
||||
return (a < b) ? -1 : ((a === b) ? 0 : 1);
|
||||
}
|
||||
|
||||
const collator = new Intl.Collator();
|
||||
/**
|
||||
* Performant language-sensitive string comparison
|
||||
* @param a the first string to compare
|
||||
* @param b the second string to compare
|
||||
*/
|
||||
export function compare(a: string, b: string): number {
|
||||
return collator.compare(a, b);
|
||||
}
|
||||
|
@@ -288,7 +288,7 @@ export class MatrixCall extends EventEmitter {
|
||||
// yet, null if we have but they didn't send a party ID.
|
||||
private opponentPartyId: string;
|
||||
private opponentCaps: CallCapabilities;
|
||||
private inviteTimeout: NodeJS.Timeout; // in the browser it's 'number'
|
||||
private inviteTimeout: number;
|
||||
|
||||
// The logic of when & if a call is on hold is nontrivial and explained in is*OnHold
|
||||
// This flag represents whether we want the other party to be on hold
|
||||
|
@@ -47,12 +47,12 @@ export class CallEventHandler {
|
||||
|
||||
public start() {
|
||||
this.client.on("sync", this.evaluateEventBuffer);
|
||||
this.client.on("event", this.onEvent);
|
||||
this.client.on("Room.timeline", this.onRoomTimeline);
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.client.removeListener("sync", this.evaluateEventBuffer);
|
||||
this.client.removeListener("event", this.onEvent);
|
||||
this.client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
}
|
||||
|
||||
private evaluateEventBuffer = async () => {
|
||||
@@ -89,7 +89,7 @@ export class CallEventHandler {
|
||||
}
|
||||
};
|
||||
|
||||
private onEvent = (event: MatrixEvent) => {
|
||||
private onRoomTimeline = (event: MatrixEvent) => {
|
||||
this.client.decryptEventIfNeeded(event);
|
||||
// any call events or ones that might be once they're decrypted
|
||||
if (this.eventIsACall(event) || event.isBeingDecrypted()) {
|
||||
|
Reference in New Issue
Block a user