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`
|
# version file and tarball created by `npm pack` / `yarn pack`
|
||||||
/git-revision.txt
|
/git-revision.txt
|
||||||
/matrix-js-sdk-*.tgz
|
/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)
|
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)
|
[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",
|
"name": "matrix-js-sdk",
|
||||||
"version": "12.0.1",
|
"version": "12.1.0",
|
||||||
"description": "Matrix Client-Server SDK for Javascript",
|
"description": "Matrix Client-Server SDK for Javascript",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepublishOnly": "yarn build",
|
"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",
|
"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",
|
"gendoc": "jsdoc -c jsdoc.json -P package.json",
|
||||||
"lint": "yarn lint:types && yarn lint:js",
|
"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",
|
"lint:types": "tsc --noEmit",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
|
@@ -100,7 +100,7 @@ fi
|
|||||||
# global cache here to ensure we get the right thing.
|
# global cache here to ensure we get the right thing.
|
||||||
yarn cache clean
|
yarn cache clean
|
||||||
# Ensure all dependencies are updated
|
# Ensure all dependencies are updated
|
||||||
yarn install --ignore-scripts
|
yarn install --ignore-scripts --pure-lockfile
|
||||||
|
|
||||||
if [ -z "$skip_changelog" ]; then
|
if [ -z "$skip_changelog" ]; then
|
||||||
# update_changelog doesn't have a --version flag
|
# update_changelog doesn't have a --version flag
|
||||||
@@ -225,7 +225,7 @@ if [ $dodist -eq 0 ]; then
|
|||||||
pushd "$builddir"
|
pushd "$builddir"
|
||||||
git clone "$projdir" .
|
git clone "$projdir" .
|
||||||
git checkout "$rel_branch"
|
git checkout "$rel_branch"
|
||||||
yarn install
|
yarn install --pure-lockfile
|
||||||
# We haven't tagged yet, so tell the dist script what version
|
# We haven't tagged yet, so tell the dist script what version
|
||||||
# it's building
|
# it's building
|
||||||
DIST_VERSION="$tag" yarn dist
|
DIST_VERSION="$tag" yarn dist
|
||||||
|
@@ -91,7 +91,7 @@ export function mkEvent(opts) {
|
|||||||
event.state_key = opts.skey;
|
event.state_key = opts.skey;
|
||||||
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
|
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
|
||||||
"m.room.power_levels", "m.room.topic",
|
"m.room.power_levels", "m.room.topic",
|
||||||
"com.example.state"].indexOf(opts.type) !== -1) {
|
"com.example.state"].includes(opts.type)) {
|
||||||
event.state_key = "";
|
event.state_key = "";
|
||||||
}
|
}
|
||||||
return opts.event ? new MatrixEvent(event) : event;
|
return opts.event ? new MatrixEvent(event) : event;
|
||||||
|
@@ -52,7 +52,7 @@ const ENCRYPTED_EVENT = new MatrixEvent({
|
|||||||
origin_server_ts: 1507753886000,
|
origin_server_ts: 1507753886000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const KEY_BACKUP_DATA = {
|
const CURVE25519_KEY_BACKUP_DATA = {
|
||||||
first_message_index: 0,
|
first_message_index: 0,
|
||||||
forwarded_count: 0,
|
forwarded_count: 0,
|
||||||
is_verified: false,
|
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",
|
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||||
version: 1,
|
version: 1,
|
||||||
auth_data: {
|
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 = {};
|
const keys = {};
|
||||||
|
|
||||||
function getCrossSigningKey(type) {
|
function getCrossSigningKey(type) {
|
||||||
@@ -144,7 +171,7 @@ describe("MegolmBackup", function() {
|
|||||||
mockCrypto.backupKey.set_recipient_key(
|
mockCrypto.backupKey.set_recipient_key(
|
||||||
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||||
);
|
);
|
||||||
mockCrypto.backupInfo = BACKUP_INFO;
|
mockCrypto.backupInfo = CURVE25519_BACKUP_INFO;
|
||||||
|
|
||||||
mockStorage = new MockStorageApi();
|
mockStorage = new MockStorageApi();
|
||||||
sessionStore = new WebStorageSessionStore(mockStorage);
|
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();
|
const groupSession = new Olm.OutboundGroupSession();
|
||||||
groupSession.create();
|
groupSession.create();
|
||||||
const ibGroupSession = new Olm.InboundGroupSession();
|
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() {
|
it('signs backups with the cross-signing master key', async function() {
|
||||||
const groupSession = new Olm.OutboundGroupSession();
|
const groupSession = new Olm.OutboundGroupSession();
|
||||||
groupSession.create();
|
groupSession.create();
|
||||||
@@ -512,30 +621,47 @@ describe("MegolmBackup", function() {
|
|||||||
client.stopClient();
|
client.stopClient();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can restore from backup', function() {
|
it('can restore from backup (Curve25519 version)', function() {
|
||||||
client.http.authedRequest = function() {
|
client.http.authedRequest = function() {
|
||||||
return Promise.resolve(KEY_BACKUP_DATA);
|
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
|
||||||
};
|
};
|
||||||
return client.restoreKeyBackupWithRecoveryKey(
|
return client.restoreKeyBackupWithRecoveryKey(
|
||||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||||
ROOM_ID,
|
ROOM_ID,
|
||||||
SESSION_ID,
|
SESSION_ID,
|
||||||
BACKUP_INFO,
|
CURVE25519_BACKUP_INFO,
|
||||||
).then(() => {
|
).then(() => {
|
||||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
expect(res.clearEvent.content).toEqual('testytest');
|
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() {
|
client.http.authedRequest = function() {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
rooms: {
|
rooms: {
|
||||||
[ROOM_ID]: {
|
[ROOM_ID]: {
|
||||||
sessions: {
|
sessions: {
|
||||||
[SESSION_ID]: KEY_BACKUP_DATA,
|
[SESSION_ID]: CURVE25519_KEY_BACKUP_DATA,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -543,7 +669,7 @@ describe("MegolmBackup", function() {
|
|||||||
};
|
};
|
||||||
return client.restoreKeyBackupWithRecoveryKey(
|
return client.restoreKeyBackupWithRecoveryKey(
|
||||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||||
null, null, BACKUP_INFO,
|
null, null, CURVE25519_BACKUP_INFO,
|
||||||
).then(() => {
|
).then(() => {
|
||||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
@@ -562,14 +688,14 @@ describe("MegolmBackup", function() {
|
|||||||
const cachedNull = await client.crypto.getSessionBackupPrivateKey();
|
const cachedNull = await client.crypto.getSessionBackupPrivateKey();
|
||||||
expect(cachedNull).toBeNull();
|
expect(cachedNull).toBeNull();
|
||||||
client.http.authedRequest = function() {
|
client.http.authedRequest = function() {
|
||||||
return Promise.resolve(KEY_BACKUP_DATA);
|
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
|
||||||
};
|
};
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
client.restoreKeyBackupWithRecoveryKey(
|
client.restoreKeyBackupWithRecoveryKey(
|
||||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||||
ROOM_ID,
|
ROOM_ID,
|
||||||
SESSION_ID,
|
SESSION_ID,
|
||||||
BACKUP_INFO,
|
CURVE25519_BACKUP_INFO,
|
||||||
{ cacheCompleteCallback: resolve },
|
{ cacheCompleteCallback: resolve },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -578,11 +704,11 @@ describe("MegolmBackup", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("fails if an known algorithm is used", async 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",
|
algorithm: "this.algorithm.does.not.exist",
|
||||||
});
|
});
|
||||||
client.http.authedRequest = function() {
|
client.http.authedRequest = function() {
|
||||||
return Promise.resolve(KEY_BACKUP_DATA);
|
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(client.restoreKeyBackupWithRecoveryKey(
|
await expect(client.restoreKeyBackupWithRecoveryKey(
|
||||||
|
@@ -3,6 +3,7 @@ import { EventStatus, MatrixEvent } from "../../src";
|
|||||||
import { EventTimeline } from "../../src/models/event-timeline";
|
import { EventTimeline } from "../../src/models/event-timeline";
|
||||||
import { RoomState } from "../../src";
|
import { RoomState } from "../../src";
|
||||||
import { Room } from "../../src";
|
import { Room } from "../../src";
|
||||||
|
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
|
|
||||||
describe("Room", function() {
|
describe("Room", function() {
|
||||||
@@ -1456,4 +1457,291 @@ describe("Room", function() {
|
|||||||
expect(room.maySendMessage()).toEqual(true);
|
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");
|
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 {
|
export interface IEncryptedFile {
|
||||||
url: string;
|
url: string;
|
||||||
mimetype?: 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 {};
|
export {};
|
||||||
|
|
||||||
declare global {
|
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 {
|
namespace NodeJS {
|
||||||
interface Global {
|
interface Global {
|
||||||
localStorage: Storage;
|
localStorage: Storage;
|
||||||
|
@@ -39,3 +39,38 @@ export enum Preset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ResizeMethod = "crop" | "scale";
|
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 { Callback } from "../client";
|
||||||
|
import { IContent } from "../models/event";
|
||||||
import { Preset, Visibility } from "./partials";
|
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 */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
export interface IJoinRoomOpts {
|
export interface IJoinRoomOpts {
|
||||||
@@ -63,12 +66,12 @@ export interface IGuestAccessOpts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ISearchOpts {
|
export interface ISearchOpts {
|
||||||
keys?: string[];
|
keys?: SearchKey[];
|
||||||
query: string;
|
query: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEventSearchOpts {
|
export interface IEventSearchOpts {
|
||||||
filter: any; // TODO: Types
|
filter?: IRoomEventFilter;
|
||||||
term: string;
|
term: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +85,7 @@ export interface IInvite3PID {
|
|||||||
export interface ICreateRoomStateEvent {
|
export interface ICreateRoomStateEvent {
|
||||||
type: string;
|
type: string;
|
||||||
state_key?: string; // defaults to an empty string
|
state_key?: string; // defaults to an empty string
|
||||||
content: object;
|
content: IContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICreateRoomOpts {
|
export interface ICreateRoomOpts {
|
||||||
@@ -104,9 +107,11 @@ export interface IRoomDirectoryOptions {
|
|||||||
server?: string;
|
server?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
since?: string;
|
since?: string;
|
||||||
|
filter?: {
|
||||||
// TODO: Proper types
|
generic_search_term: string;
|
||||||
filter?: any & {generic_search_term: string};
|
};
|
||||||
|
include_all_networks?: boolean;
|
||||||
|
third_party_instance_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUploadOpts {
|
export interface IUploadOpts {
|
||||||
@@ -119,4 +124,19 @@ export interface IUploadOpts {
|
|||||||
progressHandler?: (state: {loaded: number, total: number}) => void;
|
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 */
|
/* 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;
|
[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 */
|
/** @module ContentHelpers */
|
||||||
|
|
||||||
|
import { MsgType } from "./@types/event";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the content for a HTML Message event
|
* Generates the content for a HTML Message event
|
||||||
* @param {string} body the plaintext body of the message
|
* @param {string} body the plaintext body of the message
|
||||||
@@ -25,7 +27,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
export function makeHtmlMessage(body: string, htmlBody: string) {
|
export function makeHtmlMessage(body: string, htmlBody: string) {
|
||||||
return {
|
return {
|
||||||
msgtype: "m.text",
|
msgtype: MsgType.Text,
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
body: body,
|
body: body,
|
||||||
formatted_body: htmlBody,
|
formatted_body: htmlBody,
|
||||||
@@ -40,7 +42,7 @@ export function makeHtmlMessage(body: string, htmlBody: string) {
|
|||||||
*/
|
*/
|
||||||
export function makeHtmlNotice(body: string, htmlBody: string) {
|
export function makeHtmlNotice(body: string, htmlBody: string) {
|
||||||
return {
|
return {
|
||||||
msgtype: "m.notice",
|
msgtype: MsgType.Notice,
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
body: body,
|
body: body,
|
||||||
formatted_body: htmlBody,
|
formatted_body: htmlBody,
|
||||||
@@ -55,7 +57,7 @@ export function makeHtmlNotice(body: string, htmlBody: string) {
|
|||||||
*/
|
*/
|
||||||
export function makeHtmlEmote(body: string, htmlBody: string) {
|
export function makeHtmlEmote(body: string, htmlBody: string) {
|
||||||
return {
|
return {
|
||||||
msgtype: "m.emote",
|
msgtype: MsgType.Emote,
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
body: body,
|
body: body,
|
||||||
formatted_body: htmlBody,
|
formatted_body: htmlBody,
|
||||||
@@ -69,7 +71,7 @@ export function makeHtmlEmote(body: string, htmlBody: string) {
|
|||||||
*/
|
*/
|
||||||
export function makeTextMessage(body: string) {
|
export function makeTextMessage(body: string) {
|
||||||
return {
|
return {
|
||||||
msgtype: "m.text",
|
msgtype: MsgType.Text,
|
||||||
body: body,
|
body: body,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -81,7 +83,7 @@ export function makeTextMessage(body: string) {
|
|||||||
*/
|
*/
|
||||||
export function makeNotice(body: string) {
|
export function makeNotice(body: string) {
|
||||||
return {
|
return {
|
||||||
msgtype: "m.notice",
|
msgtype: MsgType.Notice,
|
||||||
body: body,
|
body: body,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -93,7 +95,7 @@ export function makeNotice(body: string) {
|
|||||||
*/
|
*/
|
||||||
export function makeEmoteMessage(body: string) {
|
export function makeEmoteMessage(body: string) {
|
||||||
return {
|
return {
|
||||||
msgtype: "m.emote",
|
msgtype: MsgType.Emote,
|
||||||
body: body,
|
body: body,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -102,7 +102,7 @@ export class DeviceList extends EventEmitter {
|
|||||||
// The time the save is scheduled for
|
// The time the save is scheduled for
|
||||||
private savePromiseTime: number = null;
|
private savePromiseTime: number = null;
|
||||||
// The timer used to delay the save
|
// 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
|
// True if we have fetched data from the server or loaded a non-empty
|
||||||
// set of device data from the store
|
// set of device data from the store
|
||||||
private hasFetched: boolean = null;
|
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 { logger } from "../logger";
|
||||||
import { MatrixEvent } from "../models/event";
|
import { MatrixEvent } from "../models/event";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
@@ -9,10 +25,10 @@ import {
|
|||||||
CrossSigningKeys,
|
CrossSigningKeys,
|
||||||
ICrossSigningKey,
|
ICrossSigningKey,
|
||||||
ICryptoCallbacks,
|
ICryptoCallbacks,
|
||||||
ISecretStorageKeyInfo,
|
|
||||||
ISignedKey,
|
ISignedKey,
|
||||||
KeySignatures,
|
KeySignatures,
|
||||||
} from "../matrix";
|
} from "../matrix";
|
||||||
|
import { ISecretStorageKeyInfo } from "./api";
|
||||||
import { IKeyBackupInfo } from "./keybackup";
|
import { IKeyBackupInfo } from "./keybackup";
|
||||||
|
|
||||||
interface ICrossSigningKeys {
|
interface ICrossSigningKeys {
|
||||||
@@ -109,8 +125,8 @@ export class EncryptionSetupBuilder {
|
|||||||
* @param {Object} content
|
* @param {Object} content
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
public setAccountData(type: string, content: object): Promise<void> {
|
public async setAccountData(type: string, content: object): Promise<void> {
|
||||||
return this.accountDataClientAdapter.setAccountData(type, content);
|
await this.accountDataClientAdapter.setAccountData(type, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -246,7 +262,7 @@ export class EncryptionSetupOperation {
|
|||||||
* implementing the methods related to account data in MatrixClient
|
* implementing the methods related to account data in MatrixClient
|
||||||
*/
|
*/
|
||||||
class AccountDataClientAdapter extends EventEmitter {
|
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
|
* @param {Object.<String, MatrixEvent>} existingValues existing account data
|
||||||
@@ -259,7 +275,7 @@ class AccountDataClientAdapter extends EventEmitter {
|
|||||||
* @param {String} type
|
* @param {String} type
|
||||||
* @return {Promise<Object>} the content of the account data
|
* @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));
|
return Promise.resolve(this.getAccountData(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +283,7 @@ class AccountDataClientAdapter extends EventEmitter {
|
|||||||
* @param {String} type
|
* @param {String} type
|
||||||
* @return {Object} the content of the account data
|
* @return {Object} the content of the account data
|
||||||
*/
|
*/
|
||||||
public getAccountData(type: string): object {
|
public getAccountData(type: string): MatrixEvent {
|
||||||
const modifiedValue = this.values.get(type);
|
const modifiedValue = this.values.get(type);
|
||||||
if (modifiedValue) {
|
if (modifiedValue) {
|
||||||
return modifiedValue;
|
return modifiedValue;
|
||||||
@@ -284,7 +300,7 @@ class AccountDataClientAdapter extends EventEmitter {
|
|||||||
* @param {Object} content
|
* @param {Object} content
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
public setAccountData(type: string, content: object): Promise<void> {
|
public setAccountData(type: string, content: any): Promise<{}> {
|
||||||
const lastEvent = this.values.get(type);
|
const lastEvent = this.values.get(type);
|
||||||
this.values.set(type, content);
|
this.values.set(type, content);
|
||||||
// ensure accountData is emitted on the next tick,
|
// ensure accountData is emitted on the next tick,
|
||||||
@@ -293,6 +309,7 @@ class AccountDataClientAdapter extends EventEmitter {
|
|||||||
return Promise.resolve().then(() => {
|
return Promise.resolve().then(() => {
|
||||||
const event = new MatrixEvent({ type, content });
|
const event = new MatrixEvent({ type, content });
|
||||||
this.emit("accountData", event, lastEvent);
|
this.emit("accountData", event, lastEvent);
|
||||||
|
return {};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,7 +354,7 @@ class SSSSCryptoCallbacks {
|
|||||||
constructor(private readonly delegateCryptoCallbacks: ICryptoCallbacks) {}
|
constructor(private readonly delegateCryptoCallbacks: ICryptoCallbacks) {}
|
||||||
|
|
||||||
public async getSecretStorageKey(
|
public async getSecretStorageKey(
|
||||||
{ keys }: { keys: Record<string, object> },
|
{ keys }: { keys: Record<string, ISecretStorageKeyInfo> },
|
||||||
name: string,
|
name: string,
|
||||||
): Promise<[string, Uint8Array]> {
|
): Promise<[string, Uint8Array]> {
|
||||||
for (const keyId of Object.keys(keys)) {
|
for (const keyId of Object.keys(keys)) {
|
||||||
|
@@ -78,7 +78,7 @@ export enum RoomKeyRequestState {
|
|||||||
export class OutgoingRoomKeyRequestManager {
|
export class OutgoingRoomKeyRequestManager {
|
||||||
// handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null
|
// handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null
|
||||||
// if the callback has been set, or if it is still running.
|
// 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
|
// sanity check to ensure that we don't end up with two concurrent runs
|
||||||
// of sendOutgoingRoomKeyRequests
|
// of sendOutgoingRoomKeyRequests
|
||||||
@@ -366,7 +366,7 @@ export class OutgoingRoomKeyRequestManager {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sendOutgoingRoomKeyRequestsTimer = global.setTimeout(
|
this.sendOutgoingRoomKeyRequestsTimer = setTimeout(
|
||||||
startSendingOutgoingRoomKeyRequests,
|
startSendingOutgoingRoomKeyRequests,
|
||||||
SEND_KEY_REQUESTS_DELAY_MS,
|
SEND_KEY_REQUESTS_DELAY_MS,
|
||||||
);
|
);
|
||||||
|
@@ -24,10 +24,10 @@ import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
|||||||
import { CryptoStore } from "../client";
|
import { CryptoStore } from "../client";
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
interface IRoomEncryption {
|
export interface IRoomEncryption {
|
||||||
algorithm: string;
|
algorithm: string;
|
||||||
rotation_period_ms: number;
|
rotation_period_ms?: number;
|
||||||
rotation_period_msgs: number;
|
rotation_period_msgs?: number;
|
||||||
}
|
}
|
||||||
/* eslint-enable camelcase */
|
/* 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -14,61 +14,95 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import * as olmlib from './olmlib';
|
import * as olmlib from './olmlib';
|
||||||
import { randomString } from '../randomstring';
|
import { randomString } from '../randomstring';
|
||||||
import { encryptAES, decryptAES } from './aes';
|
import { encryptAES, decryptAES, IEncryptedPayload, calculateKeyCheck } from './aes';
|
||||||
import { encodeBase64 } from "./olmlib";
|
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
|
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
|
||||||
= "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)
|
* Implements Secure Secret Storage and Sharing (MSC1946)
|
||||||
* @module crypto/SecretStorage
|
* @module crypto/SecretStorage
|
||||||
*/
|
*/
|
||||||
export class SecretStorage extends EventEmitter {
|
export class SecretStorage {
|
||||||
constructor(baseApis, cryptoCallbacks) {
|
private requests = new Map<string, ISecretRequestInternal>();
|
||||||
super();
|
|
||||||
this._baseApis = baseApis;
|
|
||||||
this._cryptoCallbacks = cryptoCallbacks;
|
|
||||||
this._requests = {};
|
|
||||||
this._incomingRequests = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDefaultKeyId() {
|
// In it's pure javascript days, this was relying on some proper Javascript-style
|
||||||
const defaultKey = await this._baseApis.getAccountDataFromServer(
|
// 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',
|
'm.secret_storage.default_key',
|
||||||
);
|
);
|
||||||
if (!defaultKey) return null;
|
if (!defaultKey) return null;
|
||||||
return defaultKey.key;
|
return defaultKey.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
setDefaultKeyId(keyId) {
|
public setDefaultKeyId(keyId: string): Promise<void> {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const listener = (ev) => {
|
const listener = (ev: MatrixEvent): void => {
|
||||||
if (
|
if (
|
||||||
ev.getType() === 'm.secret_storage.default_key' &&
|
ev.getType() === 'm.secret_storage.default_key' &&
|
||||||
ev.getContent().key === keyId
|
ev.getContent().key === keyId
|
||||||
) {
|
) {
|
||||||
this._baseApis.removeListener('accountData', listener);
|
this.accountDataAdapter.removeListener('accountData', listener);
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this._baseApis.on('accountData', listener);
|
this.accountDataAdapter.on('accountData', listener);
|
||||||
|
|
||||||
try {
|
this.accountDataAdapter.setAccountData(
|
||||||
await this._baseApis.setAccountData(
|
'm.secret_storage.default_key',
|
||||||
'm.secret_storage.default_key',
|
{ key: keyId },
|
||||||
{ key: keyId },
|
).catch(e => {
|
||||||
);
|
this.accountDataAdapter.removeListener('accountData', listener);
|
||||||
} catch (e) {
|
|
||||||
this._baseApis.removeListener('accountData', listener);
|
|
||||||
reject(e);
|
reject(e);
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,10 +119,14 @@ export class SecretStorage extends EventEmitter {
|
|||||||
* keyId: {string} the ID of the key
|
* keyId: {string} the ID of the key
|
||||||
* keyInfo: {object} details about the key (iv, mac, passphrase)
|
* keyInfo: {object} details about the key (iv, mac, passphrase)
|
||||||
*/
|
*/
|
||||||
async addKey(algorithm, opts, keyId) {
|
public async addKey(
|
||||||
const keyInfo = { algorithm };
|
algorithm: string,
|
||||||
|
opts: IAddSecretStorageKeyOpts,
|
||||||
|
keyId?: string,
|
||||||
|
): Promise<SecretStorageKeyObject> {
|
||||||
|
const keyInfo = { algorithm } as ISecretStorageKeyInfo;
|
||||||
|
|
||||||
if (!opts) opts = {};
|
if (!opts) opts = {} as IAddSecretStorageKeyOpts;
|
||||||
|
|
||||||
if (opts.name) {
|
if (opts.name) {
|
||||||
keyInfo.name = opts.name;
|
keyInfo.name = opts.name;
|
||||||
@@ -99,25 +137,25 @@ export class SecretStorage extends EventEmitter {
|
|||||||
keyInfo.passphrase = opts.passphrase;
|
keyInfo.passphrase = opts.passphrase;
|
||||||
}
|
}
|
||||||
if (opts.key) {
|
if (opts.key) {
|
||||||
const { iv, mac } = await SecretStorage._calculateKeyCheck(opts.key);
|
const { iv, mac } = await calculateKeyCheck(opts.key);
|
||||||
keyInfo.iv = iv;
|
keyInfo.iv = iv;
|
||||||
keyInfo.mac = mac;
|
keyInfo.mac = mac;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown key algorithm ${opts.algorithm}`);
|
throw new Error(`Unknown key algorithm ${algorithm}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!keyId) {
|
if (!keyId) {
|
||||||
do {
|
do {
|
||||||
keyId = randomString(32);
|
keyId = randomString(32);
|
||||||
} while (
|
} while (
|
||||||
await this._baseApis.getAccountDataFromServer(
|
await this.accountDataAdapter.getAccountDataFromServer(
|
||||||
`m.secret_storage.key.${keyId}`,
|
`m.secret_storage.key.${keyId}`,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this._baseApis.setAccountData(
|
await this.accountDataAdapter.setAccountData(
|
||||||
`m.secret_storage.key.${keyId}`, keyInfo,
|
`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.
|
* for. Defaults to the default key ID if not provided.
|
||||||
* @returns {Array?} If the key was found, the return value is an array of
|
* @returns {Array?} If the key was found, the return value is an array of
|
||||||
* the form [keyId, keyInfo]. Otherwise, null is returned.
|
* 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) {
|
if (!keyId) {
|
||||||
keyId = await this.getDefaultKeyId();
|
keyId = await this.getDefaultKeyId();
|
||||||
}
|
}
|
||||||
@@ -143,9 +182,9 @@ export class SecretStorage extends EventEmitter {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||||
"m.secret_storage.key." + keyId,
|
"m.secret_storage.key." + keyId,
|
||||||
);
|
) as ISecretStorageKeyInfo;
|
||||||
return keyInfo ? [keyId, keyInfo] : null;
|
return keyInfo ? [keyId, keyInfo] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,8 +195,8 @@ export class SecretStorage extends EventEmitter {
|
|||||||
* for. Defaults to the default key ID if not provided.
|
* for. Defaults to the default key ID if not provided.
|
||||||
* @return {boolean} Whether we have the key.
|
* @return {boolean} Whether we have the key.
|
||||||
*/
|
*/
|
||||||
async hasKey(keyId) {
|
public async hasKey(keyId?: string): Promise<boolean> {
|
||||||
return !!(await this.getKey(keyId));
|
return Boolean(await this.getKey(keyId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -168,10 +207,10 @@ export class SecretStorage extends EventEmitter {
|
|||||||
*
|
*
|
||||||
* @return {boolean} whether or not the key matches
|
* @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.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||||
if (info.mac) {
|
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, '');
|
return info.mac.replace(/=+$/g, '') === mac.replace(/=+$/g, '');
|
||||||
} else {
|
} else {
|
||||||
// if we have no information, we have to assume the key is right
|
// 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
|
* 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
|
* @param {Array} keys The IDs of the keys to use to encrypt the secret
|
||||||
* or null/undefined to use the default key.
|
* 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 = {};
|
const encrypted = {};
|
||||||
|
|
||||||
if (!keys) {
|
if (!keys) {
|
||||||
@@ -211,9 +246,9 @@ export class SecretStorage extends EventEmitter {
|
|||||||
|
|
||||||
for (const keyId of keys) {
|
for (const keyId of keys) {
|
||||||
// get key information from key storage
|
// get key information from key storage
|
||||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||||
"m.secret_storage.key." + keyId,
|
"m.secret_storage.key." + keyId,
|
||||||
);
|
) as ISecretStorageKeyInfo;
|
||||||
if (!keyInfo) {
|
if (!keyInfo) {
|
||||||
throw new Error("Unknown key: " + keyId);
|
throw new Error("Unknown key: " + keyId);
|
||||||
}
|
}
|
||||||
@@ -221,7 +256,7 @@ export class SecretStorage extends EventEmitter {
|
|||||||
// encrypt secret, based on the algorithm
|
// encrypt secret, based on the algorithm
|
||||||
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||||
const keys = { [keyId]: keyInfo };
|
const keys = { [keyId]: keyInfo };
|
||||||
const [, encryption] = await this._getSecretStorageKey(keys, name);
|
const [, encryption] = await this.getSecretStorageKey(keys, name);
|
||||||
encrypted[keyId] = await encryption.encrypt(secret);
|
encrypted[keyId] = await encryption.encrypt(secret);
|
||||||
} else {
|
} else {
|
||||||
logger.warn("unknown algorithm for secret storage key " + keyId
|
logger.warn("unknown algorithm for secret storage key " + keyId
|
||||||
@@ -231,34 +266,7 @@ export class SecretStorage extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// save encrypted secret
|
// save encrypted secret
|
||||||
await this._baseApis.setAccountData(name, { encrypted });
|
await this.accountDataAdapter.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -268,24 +276,20 @@ export class SecretStorage extends EventEmitter {
|
|||||||
*
|
*
|
||||||
* @return {string} the contents of the secret
|
* @return {string} the contents of the secret
|
||||||
*/
|
*/
|
||||||
async get(name) {
|
public async get(name: string): Promise<string> {
|
||||||
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
|
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
|
||||||
if (!secretInfo) {
|
if (!secretInfo) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!secretInfo.encrypted) {
|
if (!secretInfo.encrypted) {
|
||||||
// try to fix it up
|
throw new Error("Content is not encrypted!");
|
||||||
secretInfo = await this._fixupStoredSecret(name, secretInfo);
|
|
||||||
if (!secretInfo || !secretInfo.encrypted) {
|
|
||||||
throw new Error("Content is not encrypted!");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get possible keys to decrypt
|
// get possible keys to decrypt
|
||||||
const keys = {};
|
const keys = {};
|
||||||
for (const keyId of Object.keys(secretInfo.encrypted)) {
|
for (const keyId of Object.keys(secretInfo.encrypted)) {
|
||||||
// get key information from key storage
|
// get key information from key storage
|
||||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||||
"m.secret_storage.key." + keyId,
|
"m.secret_storage.key." + keyId,
|
||||||
);
|
);
|
||||||
const encInfo = secretInfo.encrypted[keyId];
|
const encInfo = secretInfo.encrypted[keyId];
|
||||||
@@ -306,7 +310,7 @@ export class SecretStorage extends EventEmitter {
|
|||||||
let decryption;
|
let decryption;
|
||||||
try {
|
try {
|
||||||
// fetch private key from app
|
// fetch private key from app
|
||||||
[keyId, decryption] = await this._getSecretStorageKey(keys, name);
|
[keyId, decryption] = await this.getSecretStorageKey(keys, name);
|
||||||
|
|
||||||
const encInfo = secretInfo.encrypted[keyId];
|
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
|
* with, or null if it is not present or not encrypted with a trusted
|
||||||
* key
|
* key
|
||||||
*/
|
*/
|
||||||
async isStored(name, checkKey) {
|
public async isStored(name: string, checkKey: boolean): Promise<Record<string, ISecretStorageKeyInfo>> {
|
||||||
// check if secret exists
|
// check if secret exists
|
||||||
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
|
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
|
||||||
if (!secretInfo) return null;
|
if (!secretInfo) return null;
|
||||||
if (!secretInfo.encrypted) {
|
if (!secretInfo.encrypted) {
|
||||||
// try to fix it up
|
return null;
|
||||||
secretInfo = await this._fixupStoredSecret(name, secretInfo);
|
|
||||||
if (!secretInfo || !secretInfo.encrypted) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkKey === undefined) checkKey = true;
|
if (checkKey === undefined) checkKey = true;
|
||||||
@@ -350,7 +350,7 @@ export class SecretStorage extends EventEmitter {
|
|||||||
// filter secret encryption keys with supported algorithm
|
// filter secret encryption keys with supported algorithm
|
||||||
for (const keyId of Object.keys(secretInfo.encrypted)) {
|
for (const keyId of Object.keys(secretInfo.encrypted)) {
|
||||||
// get key information from key storage
|
// get key information from key storage
|
||||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||||
"m.secret_storage.key." + keyId,
|
"m.secret_storage.key." + keyId,
|
||||||
);
|
);
|
||||||
if (!keyInfo) continue;
|
if (!keyInfo) continue;
|
||||||
@@ -371,45 +371,48 @@ export class SecretStorage extends EventEmitter {
|
|||||||
*
|
*
|
||||||
* @param {string} name the name of the secret to request
|
* @param {string} name the name of the secret to request
|
||||||
* @param {string[]} devices the devices to request the secret from
|
* @param {string[]} devices the devices to request the secret from
|
||||||
*
|
|
||||||
* @return {string} the contents of the secret
|
|
||||||
*/
|
*/
|
||||||
request(name, devices) {
|
public request(name: string, devices: string[]): ISecretRequest {
|
||||||
const requestId = this._baseApis.makeTxnId();
|
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,
|
name,
|
||||||
devices,
|
devices,
|
||||||
};
|
resolve,
|
||||||
const promise = new Promise((resolve, reject) => {
|
reject,
|
||||||
requestControl.resolve = resolve;
|
|
||||||
requestControl.reject = reject;
|
|
||||||
});
|
});
|
||||||
const cancel = (reason) => {
|
|
||||||
|
const cancel = (reason: string) => {
|
||||||
// send cancellation event
|
// send cancellation event
|
||||||
const cancelData = {
|
const cancelData = {
|
||||||
action: "request_cancellation",
|
action: "request_cancellation",
|
||||||
requesting_device_id: this._baseApis.deviceId,
|
requesting_device_id: this.baseApis.deviceId,
|
||||||
request_id: requestId,
|
request_id: requestId,
|
||||||
};
|
};
|
||||||
const toDevice = {};
|
const toDevice = {};
|
||||||
for (const device of devices) {
|
for (const device of devices) {
|
||||||
toDevice[device] = cancelData;
|
toDevice[device] = cancelData;
|
||||||
}
|
}
|
||||||
this._baseApis.sendToDevice("m.secret.request", {
|
this.baseApis.sendToDevice("m.secret.request", {
|
||||||
[this._baseApis.getUserId()]: toDevice,
|
[this.baseApis.getUserId()]: toDevice,
|
||||||
});
|
});
|
||||||
|
|
||||||
// and reject the promise so that anyone waiting on it will be
|
// and reject the promise so that anyone waiting on it will be
|
||||||
// notified
|
// notified
|
||||||
requestControl.reject(new Error(reason || "Cancelled"));
|
reject(new Error(reason || "Cancelled"));
|
||||||
};
|
};
|
||||||
|
|
||||||
// send request to devices
|
// send request to devices
|
||||||
const requestData = {
|
const requestData = {
|
||||||
name,
|
name,
|
||||||
action: "request",
|
action: "request",
|
||||||
requesting_device_id: this._baseApis.deviceId,
|
requesting_device_id: this.baseApis.deviceId,
|
||||||
request_id: requestId,
|
request_id: requestId,
|
||||||
};
|
};
|
||||||
const toDevice = {};
|
const toDevice = {};
|
||||||
@@ -417,21 +420,21 @@ export class SecretStorage extends EventEmitter {
|
|||||||
toDevice[device] = requestData;
|
toDevice[device] = requestData;
|
||||||
}
|
}
|
||||||
logger.info(`Request secret ${name} from ${devices}, id ${requestId}`);
|
logger.info(`Request secret ${name} from ${devices}, id ${requestId}`);
|
||||||
this._baseApis.sendToDevice("m.secret.request", {
|
this.baseApis.sendToDevice("m.secret.request", {
|
||||||
[this._baseApis.getUserId()]: toDevice,
|
[this.baseApis.getUserId()]: toDevice,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
request_id: requestId,
|
requestId,
|
||||||
promise,
|
promise,
|
||||||
cancel,
|
cancel,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onRequestReceived(event) {
|
public async onRequestReceived(event: MatrixEvent): Promise<void> {
|
||||||
const sender = event.getSender();
|
const sender = event.getSender();
|
||||||
const content = event.getContent();
|
const content = event.getContent();
|
||||||
if (sender !== this._baseApis.getUserId()
|
if (sender !== this.baseApis.getUserId()
|
||||||
|| !(content.name && content.action
|
|| !(content.name && content.action
|
||||||
&& content.requesting_device_id && content.request_id)) {
|
&& content.requesting_device_id && content.request_id)) {
|
||||||
// ignore requests from anyone else, for now
|
// ignore requests from anyone else, for now
|
||||||
@@ -440,34 +443,45 @@ export class SecretStorage extends EventEmitter {
|
|||||||
const deviceId = content.requesting_device_id;
|
const deviceId = content.requesting_device_id;
|
||||||
// check if it's a cancel
|
// check if it's a cancel
|
||||||
if (content.action === "request_cancellation") {
|
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]
|
if (this._incomingRequests[deviceId]
|
||||||
&& this._incomingRequests[deviceId][content.request_id]) {
|
&& this._incomingRequests[deviceId][content.request_id]) {
|
||||||
logger.info("received request cancellation for secret (" + sender
|
logger.info(
|
||||||
+ ", " + deviceId + ", " + content.request_id + ")");
|
"received request cancellation for secret (" + sender +
|
||||||
this._baseApis.emit("crypto.secrets.requestCancelled", {
|
", " + deviceId + ", " + content.request_id + ")",
|
||||||
|
);
|
||||||
|
this.baseApis.emit("crypto.secrets.requestCancelled", {
|
||||||
user_id: sender,
|
user_id: sender,
|
||||||
device_id: deviceId,
|
device_id: deviceId,
|
||||||
request_id: content.request_id,
|
request_id: content.request_id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
} else if (content.action === "request") {
|
} else if (content.action === "request") {
|
||||||
if (deviceId === this._baseApis.deviceId) {
|
if (deviceId === this.baseApis.deviceId) {
|
||||||
// no point in trying to send ourself the secret
|
// no point in trying to send ourself the secret
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if we have the secret
|
// check if we have the secret
|
||||||
logger.info("received request for secret (" + sender
|
logger.info(
|
||||||
+ ", " + deviceId + ", " + content.request_id + ")");
|
"received request for secret (" + sender +
|
||||||
if (!this._cryptoCallbacks.onSecretRequested) {
|
", " + deviceId + ", " + content.request_id + ")",
|
||||||
|
);
|
||||||
|
if (!this.cryptoCallbacks.onSecretRequested) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const secret = await this._cryptoCallbacks.onSecretRequested(
|
const secret = await this.cryptoCallbacks.onSecretRequested(
|
||||||
sender,
|
sender,
|
||||||
deviceId,
|
deviceId,
|
||||||
content.request_id,
|
content.request_id,
|
||||||
content.name,
|
content.name,
|
||||||
this._baseApis.checkDeviceTrust(sender, deviceId),
|
this.baseApis.checkDeviceTrust(sender, deviceId),
|
||||||
);
|
);
|
||||||
if (secret) {
|
if (secret) {
|
||||||
logger.info(`Preparing ${content.name} secret for ${deviceId}`);
|
logger.info(`Preparing ${content.name} secret for ${deviceId}`);
|
||||||
@@ -480,25 +494,25 @@ export class SecretStorage extends EventEmitter {
|
|||||||
};
|
};
|
||||||
const encryptedContent = {
|
const encryptedContent = {
|
||||||
algorithm: olmlib.OLM_ALGORITHM,
|
algorithm: olmlib.OLM_ALGORITHM,
|
||||||
sender_key: this._baseApis.crypto.olmDevice.deviceCurve25519Key,
|
sender_key: this.baseApis.crypto.olmDevice.deviceCurve25519Key,
|
||||||
ciphertext: {},
|
ciphertext: {},
|
||||||
};
|
};
|
||||||
await olmlib.ensureOlmSessionsForDevices(
|
await olmlib.ensureOlmSessionsForDevices(
|
||||||
this._baseApis.crypto.olmDevice,
|
this.baseApis.crypto.olmDevice,
|
||||||
this._baseApis,
|
this.baseApis,
|
||||||
{
|
{
|
||||||
[sender]: [
|
[sender]: [
|
||||||
this._baseApis.getStoredDevice(sender, deviceId),
|
this.baseApis.getStoredDevice(sender, deviceId),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
await olmlib.encryptMessageForDevice(
|
await olmlib.encryptMessageForDevice(
|
||||||
encryptedContent.ciphertext,
|
encryptedContent.ciphertext,
|
||||||
this._baseApis.getUserId(),
|
this.baseApis.getUserId(),
|
||||||
this._baseApis.deviceId,
|
this.baseApis.deviceId,
|
||||||
this._baseApis.crypto.olmDevice,
|
this.baseApis.crypto.olmDevice,
|
||||||
sender,
|
sender,
|
||||||
this._baseApis.getStoredDevice(sender, deviceId),
|
this.baseApis.getStoredDevice(sender, deviceId),
|
||||||
payload,
|
payload,
|
||||||
);
|
);
|
||||||
const contentMap = {
|
const contentMap = {
|
||||||
@@ -508,26 +522,26 @@ export class SecretStorage extends EventEmitter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
logger.info(`Sending ${content.name} secret for ${deviceId}`);
|
logger.info(`Sending ${content.name} secret for ${deviceId}`);
|
||||||
this._baseApis.sendToDevice("m.room.encrypted", contentMap);
|
this.baseApis.sendToDevice("m.room.encrypted", contentMap);
|
||||||
} else {
|
} else {
|
||||||
logger.info(`Request denied for ${content.name} secret for ${deviceId}`);
|
logger.info(`Request denied for ${content.name} secret for ${deviceId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSecretReceived(event) {
|
public onSecretReceived(event: MatrixEvent): void {
|
||||||
if (event.getSender() !== this._baseApis.getUserId()) {
|
if (event.getSender() !== this.baseApis.getUserId()) {
|
||||||
// we shouldn't be receiving secrets from anyone else, so ignore
|
// we shouldn't be receiving secrets from anyone else, so ignore
|
||||||
// because someone could be trying to send us bogus data
|
// because someone could be trying to send us bogus data
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const content = event.getContent();
|
const content = event.getContent();
|
||||||
logger.log("got secret share for request", content.request_id);
|
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) {
|
if (requestControl) {
|
||||||
// make sure that the device that sent it is one of the devices that
|
// make sure that the device that sent it is one of the devices that
|
||||||
// we requested from
|
// we requested from
|
||||||
const deviceInfo = this._baseApis.crypto.deviceList.getDeviceByIdentityKey(
|
const deviceInfo = this.baseApis.crypto.deviceList.getDeviceByIdentityKey(
|
||||||
olmlib.OLM_ALGORITHM,
|
olmlib.OLM_ALGORITHM,
|
||||||
event.getSenderKey(),
|
event.getSenderKey(),
|
||||||
);
|
);
|
||||||
@@ -550,12 +564,15 @@ export class SecretStorage extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _getSecretStorageKey(keys, name) {
|
private async getSecretStorageKey(
|
||||||
if (!this._cryptoCallbacks.getSecretStorageKey) {
|
keys: Record<string, ISecretStorageKeyInfo>,
|
||||||
|
name: string,
|
||||||
|
): Promise<[string, IDecryptors]> {
|
||||||
|
if (!this.cryptoCallbacks.getSecretStorageKey) {
|
||||||
throw new Error("No getSecretStorageKey callback supplied");
|
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) {
|
if (!returned) {
|
||||||
throw new Error("getSecretStorageKey callback returned falsey");
|
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) {
|
if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||||
const decryption = {
|
const decryption = {
|
||||||
encrypt: async function(secret) {
|
encrypt: async function(secret: string): Promise<IEncryptedPayload> {
|
||||||
return await encryptAES(secret, privateKey, name);
|
return await encryptAES(secret, privateKey, name);
|
||||||
},
|
},
|
||||||
decrypt: async function(encInfo) {
|
decrypt: async function(encInfo: IEncryptedPayload): Promise<string> {
|
||||||
return await decryptAES(encInfo, privateKey, name);
|
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);
|
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 { MatrixEvent, RoomMember } from "../..";
|
||||||
import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "..";
|
import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "..";
|
||||||
import { DeviceInfo } from "../deviceinfo";
|
import { DeviceInfo } from "../deviceinfo";
|
||||||
|
import { IRoomEncryption } from "../RoomList";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* map of registered encryption algorithm classes. A map from string to {@link
|
* map of registered encryption algorithm classes. A map from string to {@link
|
||||||
@@ -52,7 +53,7 @@ interface IParams {
|
|||||||
olmDevice: OlmDevice;
|
olmDevice: OlmDevice;
|
||||||
baseApis: MatrixClient;
|
baseApis: MatrixClient;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
config: object;
|
config: IRoomEncryption & object;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1670,7 +1670,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
|
|||||||
*/
|
*/
|
||||||
public importRoomKey(session: IMegolmSessionData, opts: any = {}): Promise<void> {
|
public importRoomKey(session: IMegolmSessionData, opts: any = {}): Promise<void> {
|
||||||
const extraSessionData: any = {};
|
const extraSessionData: any = {};
|
||||||
if (opts.untrusted) {
|
if (opts.untrusted || session.untrusted) {
|
||||||
extraSessionData.untrusted = true;
|
extraSessionData.untrusted = true;
|
||||||
}
|
}
|
||||||
if (session["org.matrix.msc3061.shared_history"]) {
|
if (session["org.matrix.msc3061.shared_history"]) {
|
||||||
|
@@ -16,7 +16,6 @@ limitations under the License.
|
|||||||
|
|
||||||
import { DeviceInfo } from "./deviceinfo";
|
import { DeviceInfo } from "./deviceinfo";
|
||||||
import { IKeyBackupInfo } from "./keybackup";
|
import { IKeyBackupInfo } from "./keybackup";
|
||||||
import { ISecretStorageKeyInfo } from "../matrix";
|
|
||||||
|
|
||||||
// TODO: Merge this with crypto.js once converted
|
// TODO: Merge this with crypto.js once converted
|
||||||
|
|
||||||
@@ -107,14 +106,32 @@ export interface ICreateSecretStorageOpts {
|
|||||||
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
|
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 {
|
export interface ISecretStorageKey {
|
||||||
keyId: string;
|
keyId: string;
|
||||||
keyInfo: ISecretStorageKeyInfo;
|
keyInfo: ISecretStorageKeyInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IPassphraseInfo {
|
||||||
|
algorithm: "m.pbkdf2";
|
||||||
|
iterations: number;
|
||||||
|
salt: string;
|
||||||
|
bits: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IAddSecretStorageKeyOpts {
|
export interface IAddSecretStorageKeyOpts {
|
||||||
// depends on algorithm
|
name: string;
|
||||||
// TODO: Types
|
passphrase: IPassphraseInfo;
|
||||||
|
key: Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IImportOpts {
|
export interface IImportOpts {
|
||||||
|
@@ -29,7 +29,10 @@ import { keyFromPassphrase } from './key_passphrase';
|
|||||||
import { sleep } from "../utils";
|
import { sleep } from "../utils";
|
||||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||||
import { encodeRecoveryKey } from './recoverykey';
|
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;
|
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||||
|
|
||||||
@@ -75,16 +78,29 @@ interface BackupAlgorithmClass {
|
|||||||
prepare(
|
prepare(
|
||||||
key: string | Uint8Array | null,
|
key: string | Uint8Array | null,
|
||||||
): Promise<[Uint8Array, AuthData]>;
|
): Promise<[Uint8Array, AuthData]>;
|
||||||
|
|
||||||
|
checkBackupVersion(info: IKeyBackupInfo): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BackupAlgorithm {
|
interface BackupAlgorithm {
|
||||||
|
untrusted: boolean;
|
||||||
encryptSession(data: Record<string, any>): Promise<any>;
|
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;
|
authData: AuthData;
|
||||||
keyMatches(key: ArrayLike<number>): Promise<boolean>;
|
keyMatches(key: ArrayLike<number>): Promise<boolean>;
|
||||||
free(): void;
|
free(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IKeyBackup {
|
||||||
|
rooms: {
|
||||||
|
[roomId: string]: {
|
||||||
|
sessions: {
|
||||||
|
[sessionId: string]: IKeyBackupSession;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages the key backup.
|
* Manages the key backup.
|
||||||
*/
|
*/
|
||||||
@@ -102,6 +118,24 @@ export class BackupManager {
|
|||||||
return this.backupInfo && this.backupInfo.version;
|
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> {
|
public static async makeAlgorithm(info: IKeyBackupInfo, getKey: GetKey): Promise<BackupAlgorithm> {
|
||||||
const Algorithm = algorithmsByName[info.algorithm];
|
const Algorithm = algorithmsByName[info.algorithm];
|
||||||
if (!Algorithm) {
|
if (!Algorithm) {
|
||||||
@@ -250,7 +284,7 @@ export class BackupManager {
|
|||||||
/**
|
/**
|
||||||
* Check if the given backup info is trusted.
|
* 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} {
|
* @return {object} {
|
||||||
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
|
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
|
||||||
* sigs: [
|
* sigs: [
|
||||||
@@ -271,7 +305,6 @@ export class BackupManager {
|
|||||||
!backupInfo ||
|
!backupInfo ||
|
||||||
!backupInfo.algorithm ||
|
!backupInfo.algorithm ||
|
||||||
!backupInfo.auth_data ||
|
!backupInfo.auth_data ||
|
||||||
!backupInfo.auth_data.public_key ||
|
|
||||||
!backupInfo.auth_data.signatures
|
!backupInfo.auth_data.signatures
|
||||||
) {
|
) {
|
||||||
logger.info("Key backup is absent or missing required data");
|
logger.info("Key backup is absent or missing required data");
|
||||||
@@ -280,7 +313,7 @@ export class BackupManager {
|
|||||||
|
|
||||||
const trustedPubkey = this.baseApis.crypto.sessionStore.getLocalTrustedBackupPubKey();
|
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");
|
logger.info("Backup public key " + trustedPubkey + " is trusted locally");
|
||||||
ret.trusted_locally = true;
|
ret.trusted_locally = true;
|
||||||
}
|
}
|
||||||
@@ -441,11 +474,11 @@ export class BackupManager {
|
|||||||
let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
||||||
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
|
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||||
|
|
||||||
const data = {};
|
const rooms: IKeyBackup["rooms"] = {};
|
||||||
for (const session of sessions) {
|
for (const session of sessions) {
|
||||||
const roomId = session.sessionData.room_id;
|
const roomId = session.sessionData.room_id;
|
||||||
if (data[roomId] === undefined) {
|
if (rooms[roomId] === undefined) {
|
||||||
data[roomId] = { sessions: {} };
|
rooms[roomId] = { sessions: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionData = await this.baseApis.crypto.olmDevice.exportInboundGroupSession(
|
const sessionData = await this.baseApis.crypto.olmDevice.exportInboundGroupSession(
|
||||||
@@ -464,7 +497,7 @@ export class BackupManager {
|
|||||||
);
|
);
|
||||||
const verified = this.baseApis.crypto.checkDeviceInfoTrust(userId, device).isVerified();
|
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,
|
first_message_index: sessionData.first_known_index,
|
||||||
forwarded_count: forwardedCount,
|
forwarded_count: forwardedCount,
|
||||||
is_verified: verified,
|
is_verified: verified,
|
||||||
@@ -472,10 +505,7 @@ export class BackupManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.baseApis.sendKeyBackup(
|
await this.baseApis.sendKeyBackup(undefined, undefined, this.backupInfo.version, { rooms });
|
||||||
undefined, undefined, this.backupInfo.version,
|
|
||||||
{ rooms: data },
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions);
|
await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions);
|
||||||
remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
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";
|
public static algorithmName = "m.megolm_backup.v1.curve25519-aes-sha2";
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public authData: AuthData,
|
public authData: ICurve25519AuthData,
|
||||||
private publicKey: any, // FIXME: PkEncryption
|
private publicKey: any, // FIXME: PkEncryption
|
||||||
private getKey: () => Promise<Uint8Array>,
|
private getKey: () => Promise<Uint8Array>,
|
||||||
) {}
|
) {}
|
||||||
@@ -561,12 +591,12 @@ export class Curve25519 implements BackupAlgorithm {
|
|||||||
authData: AuthData,
|
authData: AuthData,
|
||||||
getKey: () => Promise<Uint8Array>,
|
getKey: () => Promise<Uint8Array>,
|
||||||
): Promise<Curve25519> {
|
): Promise<Curve25519> {
|
||||||
if (!authData || !authData.public_key) {
|
if (!authData || !("public_key" in authData)) {
|
||||||
throw new Error("auth_data missing required information");
|
throw new Error("auth_data missing required information");
|
||||||
}
|
}
|
||||||
const publicKey = new global.Olm.PkEncryption();
|
const publicKey = new global.Olm.PkEncryption();
|
||||||
publicKey.set_recipient_key(authData.public_key);
|
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(
|
public static async prepare(
|
||||||
@@ -574,7 +604,7 @@ export class Curve25519 implements BackupAlgorithm {
|
|||||||
): Promise<[Uint8Array, AuthData]> {
|
): Promise<[Uint8Array, AuthData]> {
|
||||||
const decryption = new global.Olm.PkDecryption();
|
const decryption = new global.Olm.PkDecryption();
|
||||||
try {
|
try {
|
||||||
const authData: Partial<AuthData> = {};
|
const authData: Partial<ICurve25519AuthData> = {};
|
||||||
if (!key) {
|
if (!key) {
|
||||||
authData.public_key = decryption.generate_key();
|
authData.public_key = decryption.generate_key();
|
||||||
} else if (key instanceof Uint8Array) {
|
} 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> {
|
public async encryptSession(data: Record<string, any>): Promise<any> {
|
||||||
const plainText: Record<string, any> = Object.assign({}, data);
|
const plainText: Record<string, any> = Object.assign({}, data);
|
||||||
delete plainText.session_id;
|
delete plainText.session_id;
|
||||||
@@ -605,7 +643,9 @@ export class Curve25519 implements BackupAlgorithm {
|
|||||||
return this.publicKey.encrypt(JSON.stringify(plainText));
|
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 privKey = await this.getKey();
|
||||||
const decryption = new global.Olm.PkDecryption();
|
const decryption = new global.Olm.PkDecryption();
|
||||||
try {
|
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> = {
|
export const algorithmsByName: Record<string, BackupAlgorithmClass> = {
|
||||||
[Curve25519.algorithmName]: Curve25519,
|
[Curve25519.algorithmName]: Curve25519,
|
||||||
|
[Aes256.algorithmName]: Aes256,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DefaultAlgorithm: BackupAlgorithmClass = Curve25519;
|
export const DefaultAlgorithm: BackupAlgorithmClass = Curve25519;
|
||||||
|
@@ -19,7 +19,7 @@ import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
|
|||||||
import { decryptAES, encryptAES } from './aes';
|
import { decryptAES, encryptAES } from './aes';
|
||||||
import anotherjson from "another-json";
|
import anotherjson from "another-json";
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import { ISecretStorageKeyInfo } from "../matrix";
|
import { ISecretStorageKeyInfo } from "./api";
|
||||||
|
|
||||||
// FIXME: these types should eventually go in a different file
|
// FIXME: these types should eventually go in a different file
|
||||||
type Signatures = Record<string, Record<string, string>>;
|
type Signatures = Record<string, Record<string, string>>;
|
||||||
@@ -36,7 +36,7 @@ export interface IDehydratedDeviceKeyInfo {
|
|||||||
passphrase?: string;
|
passphrase?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeviceKeys {
|
export interface IDeviceKeys {
|
||||||
algorithms: Array<string>;
|
algorithms: Array<string>;
|
||||||
device_id: string; // eslint-disable-line camelcase
|
device_id: string; // eslint-disable-line camelcase
|
||||||
user_id: string; // eslint-disable-line camelcase
|
user_id: string; // eslint-disable-line camelcase
|
||||||
@@ -44,7 +44,7 @@ interface DeviceKeys {
|
|||||||
signatures?: Signatures;
|
signatures?: Signatures;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OneTimeKey {
|
export interface IOneTimeKey {
|
||||||
key: string;
|
key: string;
|
||||||
fallback?: boolean;
|
fallback?: boolean;
|
||||||
signatures?: Signatures;
|
signatures?: Signatures;
|
||||||
@@ -222,7 +222,7 @@ export class DehydrationManager {
|
|||||||
// send the keys to the server
|
// send the keys to the server
|
||||||
const deviceId = dehydrateResult.device_id;
|
const deviceId = dehydrateResult.device_id;
|
||||||
logger.log("Preparing device keys", deviceId);
|
logger.log("Preparing device keys", deviceId);
|
||||||
const deviceKeys: DeviceKeys = {
|
const deviceKeys: IDeviceKeys = {
|
||||||
algorithms: this.crypto.supportedAlgorithms,
|
algorithms: this.crypto.supportedAlgorithms,
|
||||||
device_id: deviceId,
|
device_id: deviceId,
|
||||||
user_id: this.crypto.userId,
|
user_id: this.crypto.userId,
|
||||||
@@ -244,7 +244,7 @@ export class DehydrationManager {
|
|||||||
logger.log("Preparing one-time keys");
|
logger.log("Preparing one-time keys");
|
||||||
const oneTimeKeys = {};
|
const oneTimeKeys = {};
|
||||||
for (const [keyId, key] of Object.entries(otks.curve25519)) {
|
for (const [keyId, key] of Object.entries(otks.curve25519)) {
|
||||||
const k: OneTimeKey = { key };
|
const k: IOneTimeKey = { key };
|
||||||
const signature = account.sign(anotherjson.stringify(k));
|
const signature = account.sign(anotherjson.stringify(k));
|
||||||
k.signatures = {
|
k.signatures = {
|
||||||
[this.crypto.userId]: {
|
[this.crypto.userId]: {
|
||||||
@@ -257,7 +257,7 @@ export class DehydrationManager {
|
|||||||
logger.log("Preparing fallback keys");
|
logger.log("Preparing fallback keys");
|
||||||
const fallbackKeys = {};
|
const fallbackKeys = {};
|
||||||
for (const [keyId, key] of Object.entries(fallbacks.curve25519)) {
|
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));
|
const signature = account.sign(anotherjson.stringify(k));
|
||||||
k.signatures = {
|
k.signatures = {
|
||||||
[this.crypto.userId]: {
|
[this.crypto.userId]: {
|
||||||
|
@@ -33,11 +33,18 @@ import { DeviceInfo, IDevice } from "./deviceinfo";
|
|||||||
import * as algorithms from "./algorithms";
|
import * as algorithms from "./algorithms";
|
||||||
import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning';
|
import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning';
|
||||||
import { EncryptionSetupBuilder } from "./EncryptionSetup";
|
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 { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager';
|
||||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||||
import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from './verification/QRCode';
|
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 { keyFromPassphrase } from './key_passphrase';
|
||||||
import { decodeRecoveryKey, encodeRecoveryKey } from './recoverykey';
|
import { decodeRecoveryKey, encodeRecoveryKey } from './recoverykey';
|
||||||
import { VerificationRequest } from "./verification/request/VerificationRequest";
|
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 { ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel";
|
||||||
import { IllegalMethod } from "./verification/IllegalMethod";
|
import { IllegalMethod } from "./verification/IllegalMethod";
|
||||||
import { KeySignatureUploadError } from "../errors";
|
import { KeySignatureUploadError } from "../errors";
|
||||||
import { decryptAES, encryptAES } from './aes';
|
import { decryptAES, encryptAES, calculateKeyCheck } from './aes';
|
||||||
import { DehydrationManager } from './dehydration';
|
import { DehydrationManager, IDeviceKeys, IOneTimeKey } from './dehydration';
|
||||||
import { BackupManager } from "./backup";
|
import { BackupManager } from "./backup";
|
||||||
import { IStore } from "../store";
|
import { IStore } from "../store";
|
||||||
import { Room } from "../models/room";
|
import { Room } from "../models/room";
|
||||||
@@ -54,7 +61,7 @@ import { RoomMember } from "../models/room-member";
|
|||||||
import { MatrixEvent } from "../models/event";
|
import { MatrixEvent } from "../models/event";
|
||||||
import { MatrixClient, IKeysUploadResponse, SessionStore, CryptoStore, ISignedKey } from "../client";
|
import { MatrixClient, IKeysUploadResponse, SessionStore, CryptoStore, ISignedKey } from "../client";
|
||||||
import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base";
|
import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base";
|
||||||
import type { RoomList } from "./RoomList";
|
import type { IRoomEncryption, RoomList } from "./RoomList";
|
||||||
import { IRecoveryKey, IEncryptedEventInfo } from "./api";
|
import { IRecoveryKey, IEncryptedEventInfo } from "./api";
|
||||||
import { IKeyBackupInfo } from "./keybackup";
|
import { IKeyBackupInfo } from "./keybackup";
|
||||||
import { ISyncStateData } from "../sync";
|
import { ISyncStateData } from "../sync";
|
||||||
@@ -63,7 +70,7 @@ const DeviceVerification = DeviceInfo.DeviceVerification;
|
|||||||
|
|
||||||
const defaultVerificationMethods = {
|
const defaultVerificationMethods = {
|
||||||
[ReciprocateQRCode.NAME]: ReciprocateQRCode,
|
[ReciprocateQRCode.NAME]: ReciprocateQRCode,
|
||||||
[SAS.NAME]: SAS,
|
[SASVerification.NAME]: SASVerification,
|
||||||
|
|
||||||
// These two can't be used for actual verification, but we do
|
// These two can't be used for actual verification, but we do
|
||||||
// need to be able to define them here for the verification flows
|
// need to be able to define them here for the verification flows
|
||||||
@@ -75,10 +82,13 @@ const defaultVerificationMethods = {
|
|||||||
/**
|
/**
|
||||||
* verification method names
|
* verification method names
|
||||||
*/
|
*/
|
||||||
export const verificationMethods = {
|
// legacy export identifier
|
||||||
RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME,
|
export enum verificationMethods {
|
||||||
SAS: SAS.NAME,
|
RECIPROCATE_QR_CODE = ReciprocateQRCode.NAME,
|
||||||
};
|
SAS = SASVerification.NAME,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VerificationMethod = verificationMethods;
|
||||||
|
|
||||||
export function isCryptoAvailable(): boolean {
|
export function isCryptoAvailable(): boolean {
|
||||||
return Boolean(global.Olm);
|
return Boolean(global.Olm);
|
||||||
@@ -126,6 +136,7 @@ export interface IMegolmSessionData {
|
|||||||
session_id: string;
|
session_id: string;
|
||||||
session_key: string;
|
session_key: string;
|
||||||
algorithm: string;
|
algorithm: string;
|
||||||
|
untrusted?: boolean;
|
||||||
}
|
}
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
@@ -134,6 +145,10 @@ interface IDeviceVerificationUpgrade {
|
|||||||
crossSigningInfo: CrossSigningInfo;
|
crossSigningInfo: CrossSigningInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ICheckOwnCrossSigningTrustOpts {
|
||||||
|
allowPrivateKeyRequests?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} module:crypto~OlmSessionResult
|
* @typedef {Object} module:crypto~OlmSessionResult
|
||||||
* @property {module:crypto/deviceinfo} device device info
|
* @property {module:crypto/deviceinfo} device device info
|
||||||
@@ -192,7 +207,7 @@ export class Crypto extends EventEmitter {
|
|||||||
private readonly supportedAlgorithms: string[];
|
private readonly supportedAlgorithms: string[];
|
||||||
private readonly outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager;
|
private readonly outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager;
|
||||||
private readonly toDeviceVerificationRequests: ToDeviceRequests;
|
private readonly toDeviceVerificationRequests: ToDeviceRequests;
|
||||||
private readonly inRoomVerificationRequests: InRoomRequests;
|
public readonly inRoomVerificationRequests: InRoomRequests;
|
||||||
|
|
||||||
private trustCrossSignedDevices = true;
|
private trustCrossSignedDevices = true;
|
||||||
// the last time we did a check for the number of one-time-keys on the server.
|
// 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);
|
const cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore, this.olmDevice);
|
||||||
|
|
||||||
this.crossSigningInfo = new CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks);
|
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);
|
this.dehydrationManager = new DehydrationManager(this);
|
||||||
|
|
||||||
// Assuming no app-supplied callback, default to getting from SSSS.
|
// Assuming no app-supplied callback, default to getting from SSSS.
|
||||||
@@ -789,7 +805,7 @@ export class Crypto extends EventEmitter {
|
|||||||
if (key) {
|
if (key) {
|
||||||
const privateKey = key[1];
|
const privateKey = key[1];
|
||||||
builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey);
|
builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey);
|
||||||
const { iv, mac } = await SecretStorage._calculateKeyCheck(privateKey);
|
const { iv, mac } = await calculateKeyCheck(privateKey);
|
||||||
keyInfo.iv = iv;
|
keyInfo.iv = iv;
|
||||||
keyInfo.mac = mac;
|
keyInfo.mac = mac;
|
||||||
|
|
||||||
@@ -959,6 +975,20 @@ export class Crypto extends EventEmitter {
|
|||||||
fixedBackupKey || sessionBackupKey,
|
fixedBackupKey || sessionBackupKey,
|
||||||
));
|
));
|
||||||
await builder.addSessionBackupPrivateKeyToCache(decodedBackupKey);
|
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();
|
const operation = builder.buildOperation();
|
||||||
@@ -970,15 +1000,19 @@ export class Crypto extends EventEmitter {
|
|||||||
logger.log("Secure Secret Storage ready");
|
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);
|
return this.secretStorage.addKey(algorithm, opts, keyID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasSecretStorageKey(keyID: string): boolean {
|
public hasSecretStorageKey(keyID: string): Promise<boolean> {
|
||||||
return this.secretStorage.hasKey(keyID);
|
return this.secretStorage.hasKey(keyID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSecretStorageKey(keyID?: string): any { // TODO types
|
public getSecretStorageKey(keyID?: string): Promise<SecretStorageKeyTuple> {
|
||||||
return this.secretStorage.getKey(keyID);
|
return this.secretStorage.getKey(keyID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -990,11 +1024,14 @@ export class Crypto extends EventEmitter {
|
|||||||
return this.secretStorage.get(name);
|
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);
|
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) {
|
if (!devices) {
|
||||||
devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(this.userId));
|
devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(this.userId));
|
||||||
}
|
}
|
||||||
@@ -1009,7 +1046,7 @@ export class Crypto extends EventEmitter {
|
|||||||
return this.secretStorage.setDefaultKeyId(k);
|
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);
|
return this.secretStorage.checkKey(key, info);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1388,7 +1425,7 @@ export class Crypto extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
async checkOwnCrossSigningTrust({
|
async checkOwnCrossSigningTrust({
|
||||||
allowPrivateKeyRequests = false,
|
allowPrivateKeyRequests = false,
|
||||||
} = {}) {
|
}: ICheckOwnCrossSigningTrustOpts = {}): Promise<void> {
|
||||||
const userId = this.userId;
|
const userId = this.userId;
|
||||||
|
|
||||||
// Before proceeding, ensure our cross-signing public keys have been
|
// 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.signObject(deviceKeys).then(() => {
|
||||||
return this.baseApis.uploadKeysRequest({
|
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() {
|
private async uploadOneTimeKeys() {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
|
||||||
const fallbackJson = {};
|
const fallbackJson: Record<string, IOneTimeKey> = {};
|
||||||
if (this.getNeedsNewFallback()) {
|
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)) {
|
for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) {
|
||||||
const k = { key, fallback: true };
|
const k = { key, fallback: true };
|
||||||
fallbackJson["signed_curve25519:" + keyId] = k;
|
fallbackJson["signed_curve25519:" + keyId] = k;
|
||||||
@@ -2222,7 +2259,7 @@ export class Crypto extends EventEmitter {
|
|||||||
public async legacyDeviceVerification(
|
public async legacyDeviceVerification(
|
||||||
userId: string,
|
userId: string,
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
method: string,
|
method: VerificationMethod,
|
||||||
): VerificationRequest {
|
): VerificationRequest {
|
||||||
const transactionId = ToDeviceChannel.makeTransactionId();
|
const transactionId = ToDeviceChannel.makeTransactionId();
|
||||||
const channel = new ToDeviceChannel(
|
const channel = new ToDeviceChannel(
|
||||||
@@ -2435,7 +2472,7 @@ export class Crypto extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
public async setRoomEncryption(
|
public async setRoomEncryption(
|
||||||
roomId: string,
|
roomId: string,
|
||||||
config: any, // TODO types
|
config: IRoomEncryption,
|
||||||
inhibitDeviceQuery?: boolean,
|
inhibitDeviceQuery?: boolean,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// ignore crypto events with no algorithm defined
|
// ignore crypto events with no algorithm defined
|
||||||
@@ -2492,8 +2529,8 @@ export class Crypto extends EventEmitter {
|
|||||||
crypto: this,
|
crypto: this,
|
||||||
olmDevice: this.olmDevice,
|
olmDevice: this.olmDevice,
|
||||||
baseApis: this.baseApis,
|
baseApis: this.baseApis,
|
||||||
roomId: roomId,
|
roomId,
|
||||||
config: config,
|
config,
|
||||||
});
|
});
|
||||||
this.roomEncryptors[roomId] = alg;
|
this.roomEncryptors[roomId] = alg;
|
||||||
|
|
||||||
@@ -2848,7 +2885,7 @@ export class Crypto extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
public async onCryptoEvent(event: MatrixEvent): Promise<void> {
|
public async onCryptoEvent(event: MatrixEvent): Promise<void> {
|
||||||
const roomId = event.getRoomId();
|
const roomId = event.getRoomId();
|
||||||
const content = event.getContent();
|
const content = event.getContent<IRoomEncryption>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// inhibit the device list refresh for now - it will happen once we've
|
// 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") {
|
} else if (event.getType() == "m.room_key_request") {
|
||||||
this.onRoomKeyRequestEvent(event);
|
this.onRoomKeyRequestEvent(event);
|
||||||
} else if (event.getType() === "m.secret.request") {
|
} else if (event.getType() === "m.secret.request") {
|
||||||
this.secretStorage._onRequestReceived(event);
|
this.secretStorage.onRequestReceived(event);
|
||||||
} else if (event.getType() === "m.secret.send") {
|
} else if (event.getType() === "m.secret.send") {
|
||||||
this.secretStorage._onSecretReceived(event);
|
this.secretStorage.onSecretReceived(event);
|
||||||
} else if (event.getType() === "org.matrix.room_key.withheld") {
|
} else if (event.getType() === "org.matrix.room_key.withheld") {
|
||||||
this.onRoomKeyWithheldEvent(event);
|
this.onRoomKeyWithheldEvent(event);
|
||||||
} else if (event.getContent().transaction_id) {
|
} else if (event.getContent().transaction_id) {
|
||||||
|
@@ -22,8 +22,8 @@ const DEFAULT_BITSIZE = 256;
|
|||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
interface IAuthData {
|
interface IAuthData {
|
||||||
private_key_salt: string;
|
private_key_salt?: string;
|
||||||
private_key_iterations: number;
|
private_key_iterations?: number;
|
||||||
private_key_bits?: number;
|
private_key_bits?: number;
|
||||||
}
|
}
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ISignatures } from "../@types/signed";
|
import { ISigned } from "../@types/signed";
|
||||||
|
|
||||||
export interface IKeyBackupSession {
|
export interface IKeyBackupSession {
|
||||||
first_message_index: number; // eslint-disable-line camelcase
|
first_message_index: number; // eslint-disable-line camelcase
|
||||||
@@ -24,6 +24,7 @@ export interface IKeyBackupSession {
|
|||||||
ciphertext: string;
|
ciphertext: string;
|
||||||
ephemeral: string;
|
ephemeral: string;
|
||||||
mac: string;
|
mac: string;
|
||||||
|
iv: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,15 +33,23 @@ export interface IKeyBackupRoomSessions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* 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 {
|
export interface IKeyBackupInfo {
|
||||||
algorithm: string;
|
algorithm: string;
|
||||||
auth_data: {
|
auth_data: ISigned & (ICurve25519AuthData | IAes256AuthData);
|
||||||
public_key: string;
|
|
||||||
signatures: ISignatures;
|
|
||||||
private_key_salt: string;
|
|
||||||
private_key_iterations: number;
|
|
||||||
private_key_bits?: number;
|
|
||||||
};
|
|
||||||
count?: number;
|
count?: number;
|
||||||
etag?: string;
|
etag?: string;
|
||||||
version?: string; // number contained within
|
version?: string; // number contained within
|
||||||
|
@@ -28,7 +28,7 @@ import OlmDevice from "./OlmDevice";
|
|||||||
import { DeviceInfo } from "./deviceinfo";
|
import { DeviceInfo } from "./deviceinfo";
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import * as utils from "../utils";
|
import * as utils from "../utils";
|
||||||
import { OneTimeKey } from "./dehydration";
|
import { IOneTimeKey } from "./dehydration";
|
||||||
import { MatrixClient } from "../client";
|
import { MatrixClient } from "../client";
|
||||||
|
|
||||||
enum Algorithm {
|
enum Algorithm {
|
||||||
@@ -407,7 +407,7 @@ export async function ensureOlmSessionsForDevices(
|
|||||||
|
|
||||||
async function _verifyKeyAndStartSession(
|
async function _verifyKeyAndStartSession(
|
||||||
olmDevice: OlmDevice,
|
olmDevice: OlmDevice,
|
||||||
oneTimeKey: OneTimeKey,
|
oneTimeKey: IOneTimeKey,
|
||||||
userId: string,
|
userId: string,
|
||||||
deviceInfo: DeviceInfo,
|
deviceInfo: DeviceInfo,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
@@ -465,7 +465,7 @@ export interface IObject {
|
|||||||
*/
|
*/
|
||||||
export async function verifySignature(
|
export async function verifySignature(
|
||||||
olmDevice: OlmDevice,
|
olmDevice: OlmDevice,
|
||||||
obj: OneTimeKey | IObject,
|
obj: IOneTimeKey | IObject,
|
||||||
signingUserId: string,
|
signingUserId: string,
|
||||||
signingDeviceId: string,
|
signingDeviceId: string,
|
||||||
signingKey: string,
|
signingKey: string,
|
||||||
|
@@ -15,9 +15,9 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixClient } from "./client";
|
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 {
|
export interface MapperOpts {
|
||||||
preventReEmit?: boolean;
|
preventReEmit?: boolean;
|
||||||
@@ -28,7 +28,7 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
|||||||
const preventReEmit = Boolean(options.preventReEmit);
|
const preventReEmit = Boolean(options.preventReEmit);
|
||||||
const decrypt = options.decrypt !== false;
|
const decrypt = options.decrypt !== false;
|
||||||
|
|
||||||
function mapper(plainOldJsObject) {
|
function mapper(plainOldJsObject: Partial<IEvent>) {
|
||||||
const event = new MatrixEvent(plainOldJsObject);
|
const event = new MatrixEvent(plainOldJsObject);
|
||||||
if (event.isEncrypted()) {
|
if (event.isEncrypted()) {
|
||||||
if (!preventReEmit) {
|
if (!preventReEmit) {
|
||||||
|
@@ -39,7 +39,7 @@ function setProp(obj: object, keyNesting: string, val: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
interface IFilterDefinition {
|
export interface IFilterDefinition {
|
||||||
event_fields?: string[];
|
event_fields?: string[];
|
||||||
event_format?: "client" | "federation";
|
event_format?: "client" | "federation";
|
||||||
presence?: IFilterComponent;
|
presence?: IFilterComponent;
|
||||||
@@ -47,7 +47,7 @@ interface IFilterDefinition {
|
|||||||
room?: IRoomFilter;
|
room?: IRoomFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRoomEventFilter extends IFilterComponent {
|
export interface IRoomEventFilter extends IFilterComponent {
|
||||||
lazy_load_members?: boolean;
|
lazy_load_members?: boolean;
|
||||||
include_redundant_members?: boolean;
|
include_redundant_members?: boolean;
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ export class Filter {
|
|||||||
* @param {Object} jsonObj
|
* @param {Object} jsonObj
|
||||||
* @return {Filter}
|
* @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);
|
const filter = new Filter(userId, filterId);
|
||||||
filter.setDefinition(jsonObj);
|
filter.setDefinition(jsonObj);
|
||||||
return filter;
|
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
|
* @param {object} file The object to upload. On a browser, something that
|
||||||
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
|
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
|
||||||
@@ -393,7 +393,7 @@ MatrixHttpApi.prototype = {
|
|||||||
accessToken,
|
accessToken,
|
||||||
) {
|
) {
|
||||||
if (!this.opts.idBaseUrl) {
|
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;
|
const fullUri = this.opts.idBaseUrl + prefix + path;
|
||||||
|
@@ -264,9 +264,7 @@ export class InteractiveAuth {
|
|||||||
client_secret: this.clientSecret,
|
client_secret: this.clientSecret,
|
||||||
};
|
};
|
||||||
if (await this.matrixClient.doesServerRequireIdServerParam()) {
|
if (await this.matrixClient.doesServerRequireIdServerParam()) {
|
||||||
const idServerParsedUrl = url.parse(
|
const idServerParsedUrl = new URL(this.matrixClient.getIdentityServerUrl());
|
||||||
this.matrixClient.getIdentityServerUrl(),
|
|
||||||
);
|
|
||||||
creds.id_server = idServerParsedUrl.host;
|
creds.id_server = idServerParsedUrl.host;
|
||||||
}
|
}
|
||||||
authDict = {
|
authDict = {
|
||||||
@@ -294,7 +292,7 @@ export class InteractiveAuth {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* get the client secret used for validation sessions
|
* get the client secret used for validation sessions
|
||||||
* with the ID server.
|
* with the identity server.
|
||||||
*
|
*
|
||||||
* @return {string} client secret
|
* @return {string} client secret
|
||||||
*/
|
*/
|
||||||
|
@@ -20,6 +20,7 @@ import { MatrixScheduler } from "./scheduler";
|
|||||||
import { MatrixClient } from "./client";
|
import { MatrixClient } from "./client";
|
||||||
import { ICreateClientOpts } from "./client";
|
import { ICreateClientOpts } from "./client";
|
||||||
import { DeviceTrustLevel } from "./crypto/CrossSigning";
|
import { DeviceTrustLevel } from "./crypto/CrossSigning";
|
||||||
|
import { ISecretStorageKeyInfo } from "./crypto/api";
|
||||||
|
|
||||||
export * from "./client";
|
export * from "./client";
|
||||||
export * from "./http-api";
|
export * from "./http-api";
|
||||||
@@ -122,17 +123,6 @@ export interface ICryptoCallbacks {
|
|||||||
getBackupKey?: () => Promise<Uint8Array>;
|
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}
|
* Construct a Matrix Client. Similar to {@link module:client.MatrixClient}
|
||||||
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
|
* 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.
|
* @param {string} name The new name for this file.
|
||||||
* @returns {Promise<void>} Resolves when complete.
|
* @returns {Promise<void>} Resolves when complete.
|
||||||
*/
|
*/
|
||||||
public setName(name: string): Promise<void> {
|
public async setName(name: string): Promise<void> {
|
||||||
return this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, {
|
await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, {
|
||||||
...this.indexEvent.getContent(),
|
...this.indexEvent.getContent(),
|
||||||
name: name,
|
name: name,
|
||||||
}, this.id);
|
}, this.id);
|
||||||
|
@@ -111,8 +111,8 @@ export class MSC3089TreeSpace {
|
|||||||
* @param {string} name The new name for the space.
|
* @param {string} name The new name for the space.
|
||||||
* @returns {Promise<void>} Resolves when complete.
|
* @returns {Promise<void>} Resolves when complete.
|
||||||
*/
|
*/
|
||||||
public setName(name: string): Promise<void> {
|
public async setName(name: string): Promise<void> {
|
||||||
return this.client.sendStateEvent(this.roomId, EventType.RoomName, { name }, "");
|
await this.client.sendStateEvent(this.roomId, EventType.RoomName, { name }, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -190,7 +190,7 @@ export class MSC3089TreeSpace {
|
|||||||
}
|
}
|
||||||
pls['users'] = users;
|
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 { EventTimelineSet } from "./event-timeline-set";
|
||||||
import { MatrixEvent } from "./event";
|
import { MatrixEvent } from "./event";
|
||||||
import { Filter } from "../filter";
|
import { Filter } from "../filter";
|
||||||
|
import { EventType } from "../@types/event";
|
||||||
|
|
||||||
export enum Direction {
|
export enum Direction {
|
||||||
Backward = "b",
|
Backward = "b",
|
||||||
@@ -49,15 +50,16 @@ export class EventTimeline {
|
|||||||
* @param {boolean} toStartOfTimeline if true the event's forwardLooking flag is set false
|
* @param {boolean} toStartOfTimeline if true the event's forwardLooking flag is set false
|
||||||
*/
|
*/
|
||||||
static setEventMetadata(event: MatrixEvent, stateContext: RoomState, toStartOfTimeline: boolean): void {
|
static setEventMetadata(event: MatrixEvent, stateContext: RoomState, toStartOfTimeline: boolean): void {
|
||||||
// set sender and target properties
|
// We always check if the event doesn't already have the property. We do
|
||||||
event.sender = stateContext.getSentinelMember(
|
// this to avoid overriding non-sentinel members by sentinel ones when
|
||||||
event.getSender(),
|
// adding the event to a filtered timeline
|
||||||
);
|
if (!event.sender) {
|
||||||
if (event.getType() === "m.room.member") {
|
event.sender = stateContext.getSentinelMember(event.getSender());
|
||||||
event.target = stateContext.getSentinelMember(
|
|
||||||
event.getStateKey(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
if (!event.target && event.getType() === EventType.RoomMember) {
|
||||||
|
event.target = stateContext.getSentinelMember(event.getStateKey());
|
||||||
|
}
|
||||||
|
|
||||||
if (event.isState()) {
|
if (event.isState()) {
|
||||||
// room state has no concept of 'old' or 'current', but we want the
|
// room state has no concept of 'old' or 'current', but we want the
|
||||||
// room state to regress back to previous values if toStartOfTimeline
|
// room state to regress back to previous values if toStartOfTimeline
|
||||||
@@ -345,15 +347,16 @@ export class EventTimeline {
|
|||||||
*/
|
*/
|
||||||
public addEvent(event: MatrixEvent, atStart: boolean): void {
|
public addEvent(event: MatrixEvent, atStart: boolean): void {
|
||||||
const stateContext = atStart ? this.startState : this.endState;
|
const stateContext = atStart ? this.startState : this.endState;
|
||||||
|
|
||||||
// only call setEventMetadata on the unfiltered timelineSets
|
|
||||||
const timelineSet = this.getTimelineSet();
|
const timelineSet = this.getTimelineSet();
|
||||||
if (timelineSet.room &&
|
|
||||||
timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
|
if (timelineSet.room) {
|
||||||
EventTimeline.setEventMetadata(event, stateContext, atStart);
|
EventTimeline.setEventMetadata(event, stateContext, atStart);
|
||||||
|
|
||||||
// modify state
|
// modify state but only on unfiltered timelineSets
|
||||||
if (event.isState()) {
|
if (
|
||||||
|
event.isState() &&
|
||||||
|
timelineSet.room.getUnfilteredTimelineSet() === timelineSet
|
||||||
|
) {
|
||||||
stateContext.setStateEvents([event]);
|
stateContext.setStateEvents([event]);
|
||||||
// it is possible that the act of setting the state event means we
|
// it is possible that the act of setting the state event means we
|
||||||
// can set more metadata (specifically sender/target props), so try
|
// 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();
|
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.
|
* Get the event_id for this event.
|
||||||
* @return {string} The event ID, e.g. <code>$143350589368169JsLZx:localhost
|
* @return {string} The event ID, e.g. <code>$143350589368169JsLZx:localhost
|
||||||
@@ -1231,20 +1241,7 @@ export class MatrixEvent extends EventEmitter {
|
|||||||
* @return {Object}
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
public toJSON(): object {
|
public toJSON(): object {
|
||||||
const event: any = {
|
const event = this.getEffectiveEvent();
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isEncrypted()) {
|
if (!this.isEncrypted()) {
|
||||||
return event;
|
return event;
|
||||||
|
@@ -25,13 +25,13 @@ import { EventTimeline } from "./event-timeline";
|
|||||||
import { getHttpUriForMxc } from "../content-repo";
|
import { getHttpUriForMxc } from "../content-repo";
|
||||||
import * as utils from "../utils";
|
import * as utils from "../utils";
|
||||||
import { normalize } from "../utils";
|
import { normalize } from "../utils";
|
||||||
import { EventStatus, MatrixEvent } from "./event";
|
import { EventStatus, IEvent, MatrixEvent } from "./event";
|
||||||
import { RoomMember } from "./room-member";
|
import { RoomMember } from "./room-member";
|
||||||
import { IRoomSummary, RoomSummary } from "./room-summary";
|
import { IRoomSummary, RoomSummary } from "./room-summary";
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import { ReEmitter } from '../ReEmitter';
|
import { ReEmitter } from '../ReEmitter';
|
||||||
import { EventType, RoomCreateTypeField, RoomType } from "../@types/event";
|
import { EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../@types/event";
|
||||||
import { IRoomVersionsCapability, MatrixClient, RoomVersionStability } from "../client";
|
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
|
||||||
import { ResizeMethod } from "../@types/partials";
|
import { ResizeMethod } from "../@types/partials";
|
||||||
import { Filter } from "../filter";
|
import { Filter } from "../filter";
|
||||||
import { RoomState } from "./room-state";
|
import { RoomState } from "./room-state";
|
||||||
@@ -64,7 +64,7 @@ function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: stri
|
|||||||
|
|
||||||
interface IOpts {
|
interface IOpts {
|
||||||
storageToken?: string;
|
storageToken?: string;
|
||||||
pendingEventOrdering?: "chronological" | "detached";
|
pendingEventOrdering?: PendingEventOrdering;
|
||||||
timelineSupport?: boolean;
|
timelineSupport?: boolean;
|
||||||
unstableClientRelationAggregation?: boolean;
|
unstableClientRelationAggregation?: boolean;
|
||||||
lazyLoadMembers?: boolean;
|
lazyLoadMembers?: boolean;
|
||||||
@@ -218,7 +218,7 @@ export class Room extends EventEmitter {
|
|||||||
this.setMaxListeners(100);
|
this.setMaxListeners(100);
|
||||||
this.reEmitter = new ReEmitter(this);
|
this.reEmitter = new ReEmitter(this);
|
||||||
|
|
||||||
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
|
opts.pendingEventOrdering = opts.pendingEventOrdering || PendingEventOrdering.Chronological;
|
||||||
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
|
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"opts.pendingEventOrdering MUST be either 'chronological' or " +
|
"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 lastSyncToken = this.client.store.getSyncToken();
|
||||||
const queryString = utils.encodeParams({
|
const queryString = utils.encodeParams({
|
||||||
not_membership: "leave",
|
not_membership: "leave",
|
||||||
@@ -665,8 +665,7 @@ export class Room extends EventEmitter {
|
|||||||
private async loadMembers(): Promise<{ memberEvents: MatrixEvent[], fromServer: boolean }> {
|
private async loadMembers(): Promise<{ memberEvents: MatrixEvent[], fromServer: boolean }> {
|
||||||
// were the members loaded from the server?
|
// were the members loaded from the server?
|
||||||
let fromServer = false;
|
let fromServer = false;
|
||||||
let rawMembersEvents =
|
let rawMembersEvents = await this.client.store.getOutOfBandMembers(this.roomId);
|
||||||
await this.client.store.getOutOfBandMembers(this.roomId);
|
|
||||||
if (rawMembersEvents === null) {
|
if (rawMembersEvents === null) {
|
||||||
fromServer = true;
|
fromServer = true;
|
||||||
rawMembersEvents = await this.loadMembersFromServer();
|
rawMembersEvents = await this.loadMembersFromServer();
|
||||||
@@ -713,7 +712,7 @@ export class Room extends EventEmitter {
|
|||||||
if (fromServer) {
|
if (fromServer) {
|
||||||
const oobMembers = this.currentState.getMembers()
|
const oobMembers = this.currentState.getMembers()
|
||||||
.filter((m) => m.isOutOfBand())
|
.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}`
|
logger.log(`LL: telling store to write ${oobMembers.length}`
|
||||||
+ ` members for room ${this.roomId}`);
|
+ ` members for room ${this.roomId}`);
|
||||||
const store = this.client.store;
|
const store = this.client.store;
|
||||||
@@ -2037,24 +2036,45 @@ export class Room extends EventEmitter {
|
|||||||
const joinedMemberCount = this.currentState.getJoinedMemberCount();
|
const joinedMemberCount = this.currentState.getJoinedMemberCount();
|
||||||
const invitedMemberCount = this.currentState.getInvitedMemberCount();
|
const invitedMemberCount = this.currentState.getInvitedMemberCount();
|
||||||
// -1 because these numbers include the syncing user
|
// -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.
|
// get members that are NOT ourselves and are actually in the room.
|
||||||
let otherNames = null;
|
let otherNames = null;
|
||||||
if (this.summaryHeroes) {
|
if (this.summaryHeroes) {
|
||||||
// if we have a summary, the member state events
|
// if we have a summary, the member state events
|
||||||
// should be in the room state
|
// 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);
|
const member = this.getMember(userId);
|
||||||
return member ? member.name : userId;
|
otherNames.push(member ? member.name : userId);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let otherMembers = this.currentState.getMembers().filter((m) => {
|
let otherMembers = this.currentState.getMembers().filter((m) => {
|
||||||
return m.userId !== userId &&
|
return m.userId !== userId &&
|
||||||
(m.membership === "invite" || m.membership === "join");
|
(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
|
// 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
|
// only 5 first members, immitate summaryHeroes
|
||||||
otherMembers = otherMembers.slice(0, 5);
|
otherMembers = otherMembers.slice(0, 5);
|
||||||
otherNames = otherMembers.map((m) => m.name);
|
otherNames = otherMembers.map((m) => m.name);
|
||||||
@@ -2065,7 +2085,7 @@ export class Room extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const myMembership = this.getMyMembership();
|
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
|
// 3rd party invites
|
||||||
if (myMembership == 'join') {
|
if (myMembership == 'join') {
|
||||||
const thirdPartyInvites =
|
const thirdPartyInvites =
|
||||||
|
@@ -20,27 +20,9 @@ limitations under the License.
|
|||||||
|
|
||||||
import { EventContext } from "./event-context";
|
import { EventContext } from "./event-context";
|
||||||
import { EventMapper } from "../event-mapper";
|
import { EventMapper } from "../event-mapper";
|
||||||
|
import { IResultContext, ISearchResult } from "../@types/search";
|
||||||
import { IRoomEvent } from "../sync-accumulator";
|
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 {
|
export class SearchResult {
|
||||||
/**
|
/**
|
||||||
* Create a SearchResponse from the response to /search
|
* Create a SearchResponse from the response to /search
|
||||||
@@ -49,8 +31,9 @@ export class SearchResult {
|
|||||||
* @param {function} eventMapper
|
* @param {function} eventMapper
|
||||||
* @return {SearchResult}
|
* @return {SearchResult}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): 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 eventsBefore = jsonContext.events_before || [];
|
||||||
const eventsAfter = jsonContext.events_after || [];
|
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.
|
* @param {MatrixEvent} event The <code>im.vector.user_status</code> event.
|
||||||
* @fires module:client~MatrixClient#event:"User.unstable_statusMessage"
|
* @fires module:client~MatrixClient#event:"User.unstable_statusMessage"
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
public unstable_updateStatusMessage(event: MatrixEvent): void {
|
public unstable_updateStatusMessage(event: MatrixEvent): void {
|
||||||
if (!event.getContent()) this.unstable_statusMessage = "";
|
if (!event.getContent()) this.unstable_statusMessage = "";
|
||||||
else this.unstable_statusMessage = event.getContent()["status"];
|
else this.unstable_statusMessage = event.getContent()["status"];
|
||||||
|
@@ -15,6 +15,6 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export enum SERVICE_TYPES {
|
export enum SERVICE_TYPES {
|
||||||
IS = 'SERVICE_TYPE_IS', // An Identity Service
|
IS = 'SERVICE_TYPE_IS', // An identity server
|
||||||
IM = 'SERVICE_TYPE_IM', // An Integration Manager
|
IM = 'SERVICE_TYPE_IM', // An integration manager
|
||||||
}
|
}
|
||||||
|
@@ -18,10 +18,11 @@ import { EventType } from "../@types/event";
|
|||||||
import { Group } from "../models/group";
|
import { Group } from "../models/group";
|
||||||
import { Room } from "../models/room";
|
import { Room } from "../models/room";
|
||||||
import { User } from "../models/user";
|
import { User } from "../models/user";
|
||||||
import { MatrixEvent } from "../models/event";
|
import { IEvent, MatrixEvent } from "../models/event";
|
||||||
import { Filter } from "../filter";
|
import { Filter } from "../filter";
|
||||||
import { RoomSummary } from "../models/room-summary";
|
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 {
|
export interface ISavedSync {
|
||||||
nextBatch: string;
|
nextBatch: string;
|
||||||
@@ -35,6 +36,8 @@ export interface ISavedSync {
|
|||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export interface IStore {
|
export interface IStore {
|
||||||
|
readonly accountData: Record<string, MatrixEvent>; // type : content
|
||||||
|
|
||||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
||||||
isNewlyCreated(): Promise<boolean>;
|
isNewlyCreated(): Promise<boolean>;
|
||||||
|
|
||||||
@@ -182,7 +185,7 @@ export interface IStore {
|
|||||||
* @param {Object} syncData The sync data
|
* @param {Object} syncData The sync data
|
||||||
* @return {Promise} An immediately resolved promise.
|
* @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.
|
* 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 does nothing as there is no backing data store.
|
||||||
*/
|
*/
|
||||||
save(force: boolean): void;
|
save(force?: boolean): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Startup does nothing.
|
* Startup does nothing.
|
||||||
@@ -222,13 +225,13 @@ export interface IStore {
|
|||||||
*/
|
*/
|
||||||
deleteAllData(): Promise<void>;
|
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>;
|
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 2017 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -16,14 +14,17 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SyncAccumulator } from "../sync-accumulator";
|
import { IMinimalEvent, ISyncData, ISyncResponse, SyncAccumulator } from "../sync-accumulator";
|
||||||
import * as utils from "../utils";
|
import * as utils from "../utils";
|
||||||
import * as IndexedDBHelpers from "../indexeddb-helpers";
|
import * as IndexedDBHelpers from "../indexeddb-helpers";
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
|
import { IEvent, IStartClientOpts } from "..";
|
||||||
|
import { ISavedSync } from "./index";
|
||||||
|
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
|
||||||
|
|
||||||
const VERSION = 3;
|
const VERSION = 3;
|
||||||
|
|
||||||
function createDatabase(db) {
|
function createDatabase(db: IDBDatabase): void {
|
||||||
// Make user store, clobber based on user ID. (userId property of User objects)
|
// Make user store, clobber based on user ID. (userId property of User objects)
|
||||||
db.createObjectStore("users", { keyPath: ["userId"] });
|
db.createObjectStore("users", { keyPath: ["userId"] });
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ function createDatabase(db) {
|
|||||||
db.createObjectStore("sync", { keyPath: ["clobber"] });
|
db.createObjectStore("sync", { keyPath: ["clobber"] });
|
||||||
}
|
}
|
||||||
|
|
||||||
function upgradeSchemaV2(db) {
|
function upgradeSchemaV2(db: IDBDatabase): void {
|
||||||
const oobMembersStore = db.createObjectStore(
|
const oobMembersStore = db.createObjectStore(
|
||||||
"oob_membership_events", {
|
"oob_membership_events", {
|
||||||
keyPath: ["room_id", "state_key"],
|
keyPath: ["room_id", "state_key"],
|
||||||
@@ -43,7 +44,7 @@ function upgradeSchemaV2(db) {
|
|||||||
oobMembersStore.createIndex("room", "room_id");
|
oobMembersStore.createIndex("room", "room_id");
|
||||||
}
|
}
|
||||||
|
|
||||||
function upgradeSchemaV3(db) {
|
function upgradeSchemaV3(db: IDBDatabase): void {
|
||||||
db.createObjectStore("client_options",
|
db.createObjectStore("client_options",
|
||||||
{ keyPath: ["clobber"] });
|
{ keyPath: ["clobber"] });
|
||||||
}
|
}
|
||||||
@@ -58,16 +59,20 @@ function upgradeSchemaV3(db) {
|
|||||||
* @return {Promise<T[]>} Resolves to an array of whatever you returned from
|
* @return {Promise<T[]>} Resolves to an array of whatever you returned from
|
||||||
* resultMapper.
|
* 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);
|
const query = store.openCursor(keyRange);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const results = [];
|
const results = [];
|
||||||
query.onerror = (event) => {
|
query.onerror = () => {
|
||||||
reject(new Error("Query failed: " + event.target.errorCode));
|
reject(new Error("Query failed: " + query.error));
|
||||||
};
|
};
|
||||||
// collect results
|
// collect results
|
||||||
query.onsuccess = (event) => {
|
query.onsuccess = () => {
|
||||||
const cursor = event.target.result;
|
const cursor = query.result;
|
||||||
if (!cursor) {
|
if (!cursor) {
|
||||||
resolve(results);
|
resolve(results);
|
||||||
return; // end of 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) => {
|
return new Promise((resolve, reject) => {
|
||||||
txn.oncomplete = function(event) {
|
txn.oncomplete = function(event) {
|
||||||
resolve(event);
|
resolve(event);
|
||||||
};
|
};
|
||||||
txn.onerror = function(event) {
|
txn.onerror = function() {
|
||||||
reject(event.target.error);
|
reject(txn.error);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reqAsEventPromise(req) {
|
function reqAsEventPromise(req: IDBRequest): Promise<Event> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
req.onsuccess = function(event) {
|
req.onsuccess = function(event) {
|
||||||
resolve(event);
|
resolve(event);
|
||||||
};
|
};
|
||||||
req.onerror = function(event) {
|
req.onerror = function() {
|
||||||
reject(event.target.error);
|
reject(req.error);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reqAsPromise(req) {
|
function reqAsPromise(req: IDBRequest): Promise<IDBRequest> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
req.onsuccess = () => resolve(req);
|
req.onsuccess = () => resolve(req);
|
||||||
req.onerror = (err) => reject(err);
|
req.onerror = (err) => reject(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reqAsCursorPromise(req) {
|
function reqAsCursorPromise(req: IDBRequest<IDBCursor | null>): Promise<IDBCursor> {
|
||||||
return reqAsEventPromise(req).then((event) => event.target.result);
|
return reqAsEventPromise(req).then((event) => req.result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||||
* Does the actual reading from and writing to the indexeddb
|
public static exists(indexedDB: IDBFactory, dbName: string): boolean {
|
||||||
*
|
dbName = "matrix-js-sdk:" + (dbName || "default");
|
||||||
* Construct a new Indexed Database store backend. This requires a call to
|
return IndexedDBHelpers.exists(indexedDB, dbName);
|
||||||
* <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;
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalIndexedDBStoreBackend.exists = function(indexedDB, dbName) {
|
private readonly dbName: string;
|
||||||
dbName = "matrix-js-sdk:" + (dbName || "default");
|
private readonly syncAccumulator: SyncAccumulator;
|
||||||
return IndexedDBHelpers.exists(indexedDB, dbName);
|
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
|
* Attempt to connect to the database. This can fail if the user does not
|
||||||
* grant permission.
|
* grant permission.
|
||||||
* @return {Promise} Resolves if successfully connected.
|
* @return {Promise} Resolves if successfully connected.
|
||||||
*/
|
*/
|
||||||
connect: function() {
|
public connect(): Promise<void> {
|
||||||
if (!this._disconnected) {
|
if (!this.disconnected) {
|
||||||
logger.log(
|
logger.log(`LocalIndexedDBStoreBackend.connect: already connected or connecting`);
|
||||||
`LocalIndexedDBStoreBackend.connect: already connected or connecting`,
|
|
||||||
);
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
this._disconnected = false;
|
this.disconnected = false;
|
||||||
|
|
||||||
logger.log(
|
logger.log(`LocalIndexedDBStoreBackend.connect: connecting...`);
|
||||||
`LocalIndexedDBStoreBackend.connect: connecting...`,
|
const req = this.indexedDB.open(this.dbName, VERSION);
|
||||||
);
|
|
||||||
const req = this.indexedDB.open(this._dbName, VERSION);
|
|
||||||
req.onupgradeneeded = (ev) => {
|
req.onupgradeneeded = (ev) => {
|
||||||
const db = ev.target.result;
|
const db = req.result;
|
||||||
const oldVersion = ev.oldVersion;
|
const oldVersion = ev.oldVersion;
|
||||||
logger.log(
|
logger.log(
|
||||||
`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`,
|
`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`,
|
||||||
@@ -178,19 +179,13 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
req.onblocked = () => {
|
req.onblocked = () => {
|
||||||
logger.log(
|
logger.log(`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`);
|
||||||
`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.log(
|
logger.log(`LocalIndexedDBStoreBackend.connect: awaiting connection...`);
|
||||||
`LocalIndexedDBStoreBackend.connect: awaiting connection...`,
|
return reqAsEventPromise(req).then(() => {
|
||||||
);
|
logger.log(`LocalIndexedDBStoreBackend.connect: connected`);
|
||||||
return reqAsEventPromise(req).then((ev) => {
|
this.db = req.result;
|
||||||
logger.log(
|
|
||||||
`LocalIndexedDBStoreBackend.connect: connected`,
|
|
||||||
);
|
|
||||||
this.db = ev.target.result;
|
|
||||||
|
|
||||||
// add a poorly-named listener for when deleteDatabase is called
|
// add a poorly-named listener for when deleteDatabase is called
|
||||||
// so we can close our db connections.
|
// so we can close our db connections.
|
||||||
@@ -198,27 +193,26 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
this.db.close();
|
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);
|
return Promise.resolve(this._isNewlyCreated);
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Having connected, load initial data from the database and prepare for use
|
* Having connected, load initial data from the database and prepare for use
|
||||||
* @return {Promise} Resolves on success
|
* @return {Promise} Resolves on success
|
||||||
*/
|
*/
|
||||||
_init: function() {
|
private init() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
this._loadAccountData(),
|
this.loadAccountData(),
|
||||||
this._loadSyncData(),
|
this.loadSyncData(),
|
||||||
]).then(([accountData, syncData]) => {
|
]).then(([accountData, syncData]) => {
|
||||||
logger.log(
|
logger.log(`LocalIndexedDBStoreBackend: loaded initial data`);
|
||||||
`LocalIndexedDBStoreBackend: loaded initial data`,
|
this.syncAccumulator.accumulate({
|
||||||
);
|
|
||||||
this._syncAccumulator.accumulate({
|
|
||||||
next_batch: syncData.nextBatch,
|
next_batch: syncData.nextBatch,
|
||||||
rooms: syncData.roomsData,
|
rooms: syncData.roomsData,
|
||||||
groups: syncData.groupsData,
|
groups: syncData.groupsData,
|
||||||
@@ -227,7 +221,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
},
|
},
|
||||||
}, true);
|
}, true);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the out-of-band membership events for this room that
|
* 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 {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
|
* @returns {null} in case the members for this room haven't been stored yet
|
||||||
*/
|
*/
|
||||||
getOutOfBandMembers: function(roomId) {
|
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
|
||||||
return new Promise((resolve, reject) =>{
|
return new Promise<IEvent[] | null>((resolve, reject) =>{
|
||||||
const tx = this.db.transaction(["oob_membership_events"], "readonly");
|
const tx = this.db.transaction(["oob_membership_events"], "readonly");
|
||||||
const store = tx.objectStore("oob_membership_events");
|
const store = tx.objectStore("oob_membership_events");
|
||||||
const roomIndex = store.index("room");
|
const roomIndex = store.index("room");
|
||||||
@@ -252,8 +246,8 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
// were all known already
|
// were all known already
|
||||||
let oobWritten = false;
|
let oobWritten = false;
|
||||||
|
|
||||||
request.onsuccess = (event) => {
|
request.onsuccess = () => {
|
||||||
const cursor = event.target.result;
|
const cursor = request.result;
|
||||||
if (!cursor) {
|
if (!cursor) {
|
||||||
// Unknown room
|
// Unknown room
|
||||||
if (!membershipEvents.length && !oobWritten) {
|
if (!membershipEvents.length && !oobWritten) {
|
||||||
@@ -273,11 +267,10 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
reject(err);
|
reject(err);
|
||||||
};
|
};
|
||||||
}).then((events) => {
|
}).then((events) => {
|
||||||
logger.log(`LL: got ${events && events.length}` +
|
logger.log(`LL: got ${events && events.length} membershipEvents from storage for room ${roomId} ...`);
|
||||||
` membershipEvents from storage for room ${roomId} ...`);
|
|
||||||
return events;
|
return events;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the out-of-band membership events for this room. Note that
|
* Stores the out-of-band membership events for this room. Note that
|
||||||
@@ -286,7 +279,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
* @param {string} roomId
|
* @param {string} roomId
|
||||||
* @param {event[]} membershipEvents the membership events to store
|
* @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}` +
|
logger.log(`LL: backend about to store ${membershipEvents.length}` +
|
||||||
` members for ${roomId}`);
|
` members for ${roomId}`);
|
||||||
const tx = this.db.transaction(["oob_membership_events"], "readwrite");
|
const tx = this.db.transaction(["oob_membership_events"], "readwrite");
|
||||||
@@ -307,9 +300,9 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
store.put(markerObject);
|
store.put(markerObject);
|
||||||
await txnAsPromise(tx);
|
await txnAsPromise(tx);
|
||||||
logger.log(`LL: backend done storing for ${roomId}!`);
|
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
|
// the approach to delete all members for a room
|
||||||
// is to get the min and max state key from the index
|
// is to get the min and max state key from the index
|
||||||
// for that room, and then delete between those
|
// for that room, and then delete between those
|
||||||
@@ -324,11 +317,11 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
const roomRange = IDBKeyRange.only(roomId);
|
const roomRange = IDBKeyRange.only(roomId);
|
||||||
|
|
||||||
const minStateKeyProm = reqAsCursorPromise(
|
const minStateKeyProm = reqAsCursorPromise(
|
||||||
roomIndex.openKeyCursor(roomRange, "next"),
|
roomIndex.openKeyCursor(roomRange, "next"),
|
||||||
).then((cursor) => cursor && cursor.primaryKey[1]);
|
).then((cursor) => cursor && cursor.primaryKey[1]);
|
||||||
const maxStateKeyProm = reqAsCursorPromise(
|
const maxStateKeyProm = reqAsCursorPromise(
|
||||||
roomIndex.openKeyCursor(roomRange, "prev"),
|
roomIndex.openKeyCursor(roomRange, "prev"),
|
||||||
).then((cursor) => cursor && cursor.primaryKey[1]);
|
).then((cursor) => cursor && cursor.primaryKey[1]);
|
||||||
const [minStateKey, maxStateKey] = await Promise.all(
|
const [minStateKey, maxStateKey] = await Promise.all(
|
||||||
[minStateKeyProm, maxStateKeyProm]);
|
[minStateKeyProm, maxStateKeyProm]);
|
||||||
|
|
||||||
@@ -341,45 +334,39 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
[roomId, maxStateKey],
|
[roomId, maxStateKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.log(`LL: Deleting all users + marker in storage for ` +
|
logger.log(`LL: Deleting all users + marker in storage for room ${roomId}, with key range:`,
|
||||||
`room ${roomId}, with key range:`,
|
|
||||||
[roomId, minStateKey], [roomId, maxStateKey]);
|
[roomId, minStateKey], [roomId, maxStateKey]);
|
||||||
await reqAsPromise(writeStore.delete(membersKeyRange));
|
await reqAsPromise(writeStore.delete(membersKeyRange));
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear the entire database. This should be used when logging out of a client
|
* Clear the entire database. This should be used when logging out of a client
|
||||||
* to prevent mixing data between accounts.
|
* to prevent mixing data between accounts.
|
||||||
* @return {Promise} Resolved when the database is cleared.
|
* @return {Promise} Resolved when the database is cleared.
|
||||||
*/
|
*/
|
||||||
clearDatabase: function() {
|
public clearDatabase(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve) => {
|
||||||
logger.log(`Removing indexeddb instance: ${this._dbName}`);
|
logger.log(`Removing indexeddb instance: ${this.dbName}`);
|
||||||
const req = this.indexedDB.deleteDatabase(this._dbName);
|
const req = this.indexedDB.deleteDatabase(this.dbName);
|
||||||
|
|
||||||
req.onblocked = () => {
|
req.onblocked = () => {
|
||||||
logger.log(
|
logger.log(`can't yet delete indexeddb ${this.dbName} because it is open elsewhere`);
|
||||||
`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
|
// in firefox, with indexedDB disabled, this fails with a
|
||||||
// DOMError. We treat this as non-fatal, so that we can still
|
// DOMError. We treat this as non-fatal, so that we can still
|
||||||
// use the app.
|
// use the app.
|
||||||
logger.warn(
|
logger.warn(`unable to delete js-sdk store indexeddb: ${req.error}`);
|
||||||
`unable to delete js-sdk store indexeddb: ${ev.target.error}`,
|
|
||||||
);
|
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
req.onsuccess = () => {
|
req.onsuccess = () => {
|
||||||
logger.log(`Removed indexeddb instance: ${this._dbName}`);
|
logger.log(`Removed indexeddb instance: ${this.dbName}`);
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean=} copy If false, the data returned is from internal
|
* @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
|
* client state to where it was at the last save, or null if there
|
||||||
* is no saved sync data.
|
* is no saved sync data.
|
||||||
*/
|
*/
|
||||||
getSavedSync: function(copy) {
|
public getSavedSync(copy = true): Promise<ISavedSync> {
|
||||||
if (copy === undefined) copy = true;
|
const data = this.syncAccumulator.getJSON();
|
||||||
|
|
||||||
const data = this._syncAccumulator.getJSON();
|
|
||||||
if (!data.nextBatch) return Promise.resolve(null);
|
if (!data.nextBatch) return Promise.resolve(null);
|
||||||
if (copy) {
|
if (copy) {
|
||||||
// We must deep copy the stored data so that the /sync processing code doesn't
|
// We must deep copy the stored data so that the /sync processing code doesn't
|
||||||
@@ -402,29 +387,27 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
} else {
|
} else {
|
||||||
return Promise.resolve(data);
|
return Promise.resolve(data);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
getNextBatchToken: function() {
|
public getNextBatchToken(): Promise<string> {
|
||||||
return Promise.resolve(this._syncAccumulator.getNextBatchToken());
|
return Promise.resolve(this.syncAccumulator.getNextBatchToken());
|
||||||
},
|
}
|
||||||
|
|
||||||
setSyncData: function(syncData) {
|
public setSyncData(syncData: ISyncResponse): Promise<void> {
|
||||||
return Promise.resolve().then(() => {
|
return Promise.resolve().then(() => {
|
||||||
this._syncAccumulator.accumulate(syncData);
|
this.syncAccumulator.accumulate(syncData);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
syncToDatabase: function(userTuples) {
|
public async syncToDatabase(userTuples: UserTuple[]): Promise<void> {
|
||||||
const syncData = this._syncAccumulator.getJSON(true);
|
const syncData = this.syncAccumulator.getJSON(true);
|
||||||
|
|
||||||
return Promise.all([
|
await Promise.all([
|
||||||
this._persistUserPresenceEvents(userTuples),
|
this.persistUserPresenceEvents(userTuples),
|
||||||
this._persistAccountData(syncData.accountData),
|
this.persistAccountData(syncData.accountData),
|
||||||
this._persistSyncData(
|
this.persistSyncData(syncData.nextBatch, syncData.roomsData, syncData.groupsData),
|
||||||
syncData.nextBatch, syncData.roomsData, syncData.groupsData,
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist rooms /sync data along with the next batch token.
|
* 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
|
* @param {Object} groupsData The 'groups' /sync data from a SyncAccumulator
|
||||||
* @return {Promise} Resolves if the data was persisted.
|
* @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);
|
logger.log("Persisting sync data up to", nextBatch);
|
||||||
return utils.promiseTry(() => {
|
return utils.promiseTry<void>(() => {
|
||||||
const txn = this.db.transaction(["sync"], "readwrite");
|
const txn = this.db.transaction(["sync"], "readwrite");
|
||||||
const store = txn.objectStore("sync");
|
const store = txn.objectStore("sync");
|
||||||
store.put({
|
store.put({
|
||||||
clobber: "-", // constant key so will always clobber
|
clobber: "-", // constant key so will always clobber
|
||||||
nextBatch: nextBatch,
|
nextBatch,
|
||||||
roomsData: roomsData,
|
roomsData,
|
||||||
groupsData: groupsData,
|
groupsData,
|
||||||
}); // put == UPSERT
|
}); // put == UPSERT
|
||||||
return txnAsPromise(txn);
|
return txnAsPromise(txn).then();
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist a list of account data events. Events with the same 'type' will
|
* 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
|
* @param {Object[]} accountData An array of raw user-scoped account data events
|
||||||
* @return {Promise} Resolves if the events were persisted.
|
* @return {Promise} Resolves if the events were persisted.
|
||||||
*/
|
*/
|
||||||
_persistAccountData: function(accountData) {
|
private persistAccountData(accountData: IMinimalEvent[]): Promise<void> {
|
||||||
return utils.promiseTry(() => {
|
return utils.promiseTry<void>(() => {
|
||||||
const txn = this.db.transaction(["accountData"], "readwrite");
|
const txn = this.db.transaction(["accountData"], "readwrite");
|
||||||
const store = txn.objectStore("accountData");
|
const store = txn.objectStore("accountData");
|
||||||
for (let i = 0; i < accountData.length; i++) {
|
for (let i = 0; i < accountData.length; i++) {
|
||||||
store.put(accountData[i]); // put == UPSERT
|
store.put(accountData[i]); // put == UPSERT
|
||||||
}
|
}
|
||||||
return txnAsPromise(txn);
|
return txnAsPromise(txn).then();
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist a list of [user id, presence event] they are for.
|
* 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
|
* @param {Object[]} tuples An array of [userid, event] tuples
|
||||||
* @return {Promise} Resolves if the users were persisted.
|
* @return {Promise} Resolves if the users were persisted.
|
||||||
*/
|
*/
|
||||||
_persistUserPresenceEvents: function(tuples) {
|
private persistUserPresenceEvents(tuples: UserTuple[]): Promise<void> {
|
||||||
return utils.promiseTry(() => {
|
return utils.promiseTry<void>(() => {
|
||||||
const txn = this.db.transaction(["users"], "readwrite");
|
const txn = this.db.transaction(["users"], "readwrite");
|
||||||
const store = txn.objectStore("users");
|
const store = txn.objectStore("users");
|
||||||
for (const tuple of tuples) {
|
for (const tuple of tuples) {
|
||||||
@@ -483,9 +470,9 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
event: tuple[1],
|
event: tuple[1],
|
||||||
}); // put == UPSERT
|
}); // put == UPSERT
|
||||||
}
|
}
|
||||||
return txnAsPromise(txn);
|
return txnAsPromise(txn).then();
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load all user presence events from the database. This is not cached.
|
* Load all user presence events from the database. This is not cached.
|
||||||
@@ -493,64 +480,56 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
* sync.
|
* sync.
|
||||||
* @return {Promise<Object[]>} A list of presence events in their raw form.
|
* @return {Promise<Object[]>} A list of presence events in their raw form.
|
||||||
*/
|
*/
|
||||||
getUserPresenceEvents: function() {
|
public getUserPresenceEvents(): Promise<UserTuple[]> {
|
||||||
return utils.promiseTry(() => {
|
return utils.promiseTry<UserTuple[]>(() => {
|
||||||
const txn = this.db.transaction(["users"], "readonly");
|
const txn = this.db.transaction(["users"], "readonly");
|
||||||
const store = txn.objectStore("users");
|
const store = txn.objectStore("users");
|
||||||
return selectQuery(store, undefined, (cursor) => {
|
return selectQuery(store, undefined, (cursor) => {
|
||||||
return [cursor.value.userId, cursor.value.event];
|
return [cursor.value.userId, cursor.value.event];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load all the account data events from the database. This is not cached.
|
* Load all the account data events from the database. This is not cached.
|
||||||
* @return {Promise<Object[]>} A list of raw global account events.
|
* @return {Promise<Object[]>} A list of raw global account events.
|
||||||
*/
|
*/
|
||||||
_loadAccountData: function() {
|
private loadAccountData(): Promise<IMinimalEvent[]> {
|
||||||
logger.log(
|
logger.log(`LocalIndexedDBStoreBackend: loading account data...`);
|
||||||
`LocalIndexedDBStoreBackend: loading account data...`,
|
return utils.promiseTry<IMinimalEvent[]>(() => {
|
||||||
);
|
|
||||||
return utils.promiseTry(() => {
|
|
||||||
const txn = this.db.transaction(["accountData"], "readonly");
|
const txn = this.db.transaction(["accountData"], "readonly");
|
||||||
const store = txn.objectStore("accountData");
|
const store = txn.objectStore("accountData");
|
||||||
return selectQuery(store, undefined, (cursor) => {
|
return selectQuery(store, undefined, (cursor) => {
|
||||||
return cursor.value;
|
return cursor.value;
|
||||||
}).then((result) => {
|
}).then((result: IMinimalEvent[]) => {
|
||||||
logger.log(
|
logger.log(`LocalIndexedDBStoreBackend: loaded account data`);
|
||||||
`LocalIndexedDBStoreBackend: loaded account data`,
|
|
||||||
);
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the sync data from the database.
|
* Load the sync data from the database.
|
||||||
* @return {Promise<Object>} An object with "roomsData" and "nextBatch" keys.
|
* @return {Promise<Object>} An object with "roomsData" and "nextBatch" keys.
|
||||||
*/
|
*/
|
||||||
_loadSyncData: function() {
|
private loadSyncData(): Promise<ISyncData> {
|
||||||
logger.log(
|
logger.log(`LocalIndexedDBStoreBackend: loading sync data...`);
|
||||||
`LocalIndexedDBStoreBackend: loading sync data...`,
|
return utils.promiseTry<ISyncData>(() => {
|
||||||
);
|
|
||||||
return utils.promiseTry(() => {
|
|
||||||
const txn = this.db.transaction(["sync"], "readonly");
|
const txn = this.db.transaction(["sync"], "readonly");
|
||||||
const store = txn.objectStore("sync");
|
const store = txn.objectStore("sync");
|
||||||
return selectQuery(store, undefined, (cursor) => {
|
return selectQuery(store, undefined, (cursor) => {
|
||||||
return cursor.value;
|
return cursor.value;
|
||||||
}).then((results) => {
|
}).then((results: ISyncData[]) => {
|
||||||
logger.log(
|
logger.log(`LocalIndexedDBStoreBackend: loaded sync data`);
|
||||||
`LocalIndexedDBStoreBackend: loaded sync data`,
|
|
||||||
);
|
|
||||||
if (results.length > 1) {
|
if (results.length > 1) {
|
||||||
logger.warn("loadSyncData: More than 1 sync row found.");
|
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(() => {
|
return Promise.resolve().then(() => {
|
||||||
const txn = this.db.transaction(["client_options"], "readonly");
|
const txn = this.db.transaction(["client_options"], "readonly");
|
||||||
const store = txn.objectStore("client_options");
|
const store = txn.objectStore("client_options");
|
||||||
@@ -560,9 +539,9 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
}
|
}
|
||||||
}).then((results) => results[0]);
|
}).then((results) => results[0]);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
storeClientOptions: async function(options) {
|
public async storeClientOptions(options: IStartClientOpts): Promise<void> {
|
||||||
const txn = this.db.transaction(["client_options"], "readwrite");
|
const txn = this.db.transaction(["client_options"], "readwrite");
|
||||||
const store = txn.objectStore("client_options");
|
const store = txn.objectStore("client_options");
|
||||||
store.put({
|
store.put({
|
||||||
@@ -570,5 +549,5 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
options: options,
|
options: options,
|
||||||
}); // put == UPSERT
|
}); // put == UPSERT
|
||||||
await txnAsPromise(txn);
|
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 2017 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -16,9 +14,15 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend.js";
|
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend";
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
|
|
||||||
|
interface ICmd {
|
||||||
|
command: string;
|
||||||
|
seq: number;
|
||||||
|
args?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class lives in the webworker and drives a LocalIndexedDBStoreBackend
|
* This class lives in the webworker and drives a LocalIndexedDBStoreBackend
|
||||||
* controlled by messages from the main process.
|
* controlled by messages from the main process.
|
||||||
@@ -35,16 +39,13 @@ import { logger } from '../logger';
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export class IndexedDBStoreWorker {
|
export class IndexedDBStoreWorker {
|
||||||
|
private backend: LocalIndexedDBStoreBackend = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {function} postMessage The web worker postMessage function that
|
* @param {function} postMessage The web worker postMessage function that
|
||||||
* should be used to communicate back to the main script.
|
* should be used to communicate back to the main script.
|
||||||
*/
|
*/
|
||||||
constructor(postMessage) {
|
constructor(private readonly postMessage: InstanceType<typeof Worker>["postMessage"]) {}
|
||||||
this.backend = null;
|
|
||||||
this.postMessage = postMessage;
|
|
||||||
|
|
||||||
this.onMessage = this.onMessage.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Passes a message event from the main script into the class. This method
|
* 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
|
* @param {Object} ev The message event
|
||||||
*/
|
*/
|
||||||
onMessage(ev) {
|
public onMessage = (ev: MessageEvent): void => {
|
||||||
const msg = ev.data;
|
const msg: ICmd = ev.data;
|
||||||
let prom;
|
let prom;
|
||||||
|
|
||||||
switch (msg.command) {
|
switch (msg.command) {
|
||||||
case '_setupWorker':
|
case '_setupWorker':
|
||||||
this.backend = new LocalIndexedDBStoreBackend(
|
// this is the 'indexedDB' global (where global != window
|
||||||
// this is the 'indexedDB' global (where global != window
|
// because it's a web worker and there is no window).
|
||||||
// because it's a web worker and there is no window).
|
this.backend = new LocalIndexedDBStoreBackend(indexedDB, msg.args[0]);
|
||||||
indexedDB, msg.args[0],
|
|
||||||
);
|
|
||||||
prom = Promise.resolve();
|
prom = Promise.resolve();
|
||||||
break;
|
break;
|
||||||
case 'connect':
|
case 'connect':
|
||||||
@@ -72,23 +71,16 @@ export class IndexedDBStoreWorker {
|
|||||||
prom = this.backend.isNewlyCreated();
|
prom = this.backend.isNewlyCreated();
|
||||||
break;
|
break;
|
||||||
case 'clearDatabase':
|
case 'clearDatabase':
|
||||||
prom = this.backend.clearDatabase().then((result) => {
|
prom = this.backend.clearDatabase();
|
||||||
// This returns special classes which can't be cloned
|
|
||||||
// across to the main script, so don't try.
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case 'getSavedSync':
|
case 'getSavedSync':
|
||||||
prom = this.backend.getSavedSync(false);
|
prom = this.backend.getSavedSync(false);
|
||||||
break;
|
break;
|
||||||
case 'setSyncData':
|
case 'setSyncData':
|
||||||
prom = this.backend.setSyncData(...msg.args);
|
prom = this.backend.setSyncData(msg.args[0]);
|
||||||
break;
|
break;
|
||||||
case 'syncToDatabase':
|
case 'syncToDatabase':
|
||||||
prom = this.backend.syncToDatabase(...msg.args).then(() => {
|
prom = this.backend.syncToDatabase(msg.args[0]);
|
||||||
// This also returns IndexedDB events which are not cloneable
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case 'getUserPresenceEvents':
|
case 'getUserPresenceEvents':
|
||||||
prom = this.backend.getUserPresenceEvents();
|
prom = this.backend.getUserPresenceEvents();
|
||||||
@@ -130,7 +122,7 @@ export class IndexedDBStoreWorker {
|
|||||||
result: ret,
|
result: ret,
|
||||||
});
|
});
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
logger.error("Error running command: "+msg.command);
|
logger.error("Error running command: " + msg.command);
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
this.postMessage.call(null, {
|
this.postMessage.call(null, {
|
||||||
command: 'cmd_fail',
|
command: 'cmd_fail',
|
||||||
@@ -142,5 +134,5 @@ export class IndexedDBStoreWorker {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
}
|
}
|
@@ -19,12 +19,14 @@ limitations under the License.
|
|||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
import { MemoryStore, IOpts as IBaseOpts } from "./memory";
|
import { MemoryStore, IOpts as IBaseOpts } from "./memory";
|
||||||
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend.js";
|
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend";
|
||||||
import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend.js";
|
import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend";
|
||||||
import { User } from "../models/user";
|
import { User } from "../models/user";
|
||||||
import { MatrixEvent } from "../models/event";
|
import { IEvent, MatrixEvent } from "../models/event";
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import { ISavedSync } from "./index";
|
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.
|
* 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 {
|
interface IOpts extends IBaseOpts {
|
||||||
indexedDB: IDBFactory;
|
indexedDB: IDBFactory;
|
||||||
dbName?: string;
|
dbName?: string;
|
||||||
workerScript?: string;
|
workerFactory?: () => Worker;
|
||||||
workerApi?: typeof Worker;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IndexedDBStore extends MemoryStore {
|
export class IndexedDBStore extends MemoryStore {
|
||||||
@@ -50,8 +51,7 @@ export class IndexedDBStore extends MemoryStore {
|
|||||||
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
|
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO these should conform to one interface
|
public readonly backend: IIndexedDBBackend;
|
||||||
public readonly backend: LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend;
|
|
||||||
|
|
||||||
private startedUp = false;
|
private startedUp = false;
|
||||||
private syncTs = 0;
|
private syncTs = 0;
|
||||||
@@ -110,16 +110,8 @@ export class IndexedDBStore extends MemoryStore {
|
|||||||
throw new Error('Missing required option: indexedDB');
|
throw new Error('Missing required option: indexedDB');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.workerScript) {
|
if (opts.workerFactory) {
|
||||||
// try & find a webworker-compatible API
|
this.backend = new RemoteIndexedDBStoreBackend(opts.workerFactory, opts.dbName);
|
||||||
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,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
this.backend = new LocalIndexedDBStoreBackend(opts.indexedDB, opts.dbName);
|
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
|
// work out changed users (this doesn't handle deletions but you
|
||||||
// can't 'delete' users as they are just presence events).
|
// 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()) {
|
for (const u of this.getUsers()) {
|
||||||
if (this.userModifiedMap[u.userId] === u.getLastModifiedTime()) continue;
|
if (this.userModifiedMap[u.userId] === u.getLastModifiedTime()) continue;
|
||||||
if (!u.events.presence) continue;
|
if (!u.events.presence) continue;
|
||||||
@@ -236,7 +228,7 @@ export class IndexedDBStore extends MemoryStore {
|
|||||||
return this.backend.syncToDatabase(userTuples);
|
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);
|
return this.backend.setSyncData(syncData);
|
||||||
}, "setSyncData");
|
}, "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 {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
|
* @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);
|
return this.backend.getOutOfBandMembers(roomId);
|
||||||
}, "getOutOfBandMembers");
|
}, "getOutOfBandMembers");
|
||||||
|
|
||||||
@@ -259,7 +251,7 @@ export class IndexedDBStore extends MemoryStore {
|
|||||||
* @param {event[]} membershipEvents the membership events to store
|
* @param {event[]} membershipEvents the membership events to store
|
||||||
* @returns {Promise} when all members have been stored
|
* @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);
|
super.setOutOfBandMembers(roomId, membershipEvents);
|
||||||
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
|
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
|
||||||
}, "setOutOfBandMembers");
|
}, "setOutOfBandMembers");
|
||||||
|
@@ -23,12 +23,13 @@ import { EventType } from "../@types/event";
|
|||||||
import { Group } from "../models/group";
|
import { Group } from "../models/group";
|
||||||
import { Room } from "../models/room";
|
import { Room } from "../models/room";
|
||||||
import { User } from "../models/user";
|
import { User } from "../models/user";
|
||||||
import { MatrixEvent } from "../models/event";
|
import { IEvent, MatrixEvent } from "../models/event";
|
||||||
import { RoomState } from "../models/room-state";
|
import { RoomState } from "../models/room-state";
|
||||||
import { RoomMember } from "../models/room-member";
|
import { RoomMember } from "../models/room-member";
|
||||||
import { Filter } from "../filter";
|
import { Filter } from "../filter";
|
||||||
import { ISavedSync, IStore } from "./index";
|
import { ISavedSync, IStore } from "./index";
|
||||||
import { RoomSummary } from "../models/room-summary";
|
import { RoomSummary } from "../models/room-summary";
|
||||||
|
import { ISyncResponse } from "../sync-accumulator";
|
||||||
|
|
||||||
function isValidFilterId(filterId: string): boolean {
|
function isValidFilterId(filterId: string): boolean {
|
||||||
const isValidStr = typeof filterId === "string" &&
|
const isValidStr = typeof filterId === "string" &&
|
||||||
@@ -59,9 +60,9 @@ export class MemoryStore implements IStore {
|
|||||||
// filterId: Filter
|
// filterId: Filter
|
||||||
// }
|
// }
|
||||||
private filters: Record<string, Record<string, 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 readonly localStorage: Storage;
|
||||||
private oobMembers: Record<string, MatrixEvent[]> = {}; // roomId: [member events]
|
private oobMembers: Record<string, IEvent[]> = {}; // roomId: [member events]
|
||||||
private clientOptions = {};
|
private clientOptions = {};
|
||||||
|
|
||||||
constructor(opts: IOpts = {}) {
|
constructor(opts: IOpts = {}) {
|
||||||
@@ -340,7 +341,7 @@ export class MemoryStore implements IStore {
|
|||||||
* @param {Object} syncData The sync data
|
* @param {Object} syncData The sync data
|
||||||
* @return {Promise} An immediately resolved promise.
|
* @return {Promise} An immediately resolved promise.
|
||||||
*/
|
*/
|
||||||
public setSyncData(syncData: object): Promise<void> {
|
public setSyncData(syncData: ISyncResponse): Promise<void> {
|
||||||
return Promise.resolve();
|
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 {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
|
* @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);
|
return Promise.resolve(this.oobMembers[roomId] || null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +428,7 @@ export class MemoryStore implements IStore {
|
|||||||
* @param {event[]} membershipEvents the membership events to store
|
* @param {event[]} membershipEvents the membership events to store
|
||||||
* @returns {Promise} when all members have been stored
|
* @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;
|
this.oobMembers[roomId] = membershipEvents;
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
@@ -23,19 +23,21 @@ import { EventType } from "../@types/event";
|
|||||||
import { Group } from "../models/group";
|
import { Group } from "../models/group";
|
||||||
import { Room } from "../models/room";
|
import { Room } from "../models/room";
|
||||||
import { User } from "../models/user";
|
import { User } from "../models/user";
|
||||||
import { MatrixEvent } from "../models/event";
|
import { IEvent, MatrixEvent } from "../models/event";
|
||||||
import { Filter } from "../filter";
|
import { Filter } from "../filter";
|
||||||
import { ISavedSync, IStore } from "./index";
|
import { ISavedSync, IStore } from "./index";
|
||||||
import { RoomSummary } from "../models/room-summary";
|
import { RoomSummary } from "../models/room-summary";
|
||||||
|
import { ISyncResponse } from "../sync-accumulator";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a stub store. This does no-ops on most store methods.
|
* Construct a stub store. This does no-ops on most store methods.
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export class StubStore implements IStore {
|
export class StubStore implements IStore {
|
||||||
|
public readonly accountData = {}; // stub
|
||||||
private fromToken: string = null;
|
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> {
|
public isNewlyCreated(): Promise<boolean> {
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
@@ -212,7 +214,7 @@ export class StubStore implements IStore {
|
|||||||
* @param {Object} syncData The sync data
|
* @param {Object} syncData The sync data
|
||||||
* @return {Promise} An immediately resolved promise.
|
* @return {Promise} An immediately resolved promise.
|
||||||
*/
|
*/
|
||||||
public setSyncData(syncData: object): Promise<void> {
|
public setSyncData(syncData: ISyncResponse): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,11 +266,11 @@ export class StubStore implements IStore {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getOutOfBandMembers(): Promise<MatrixEvent[]> {
|
public getOutOfBandMembers(): Promise<IEvent[]> {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setOutOfBandMembers(roomId: string, membershipEvents: MatrixEvent[]): Promise<void> {
|
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -40,8 +40,8 @@ export interface IEphemeral {
|
|||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
interface IUnreadNotificationCounts {
|
interface IUnreadNotificationCounts {
|
||||||
highlight_count: number;
|
highlight_count?: number;
|
||||||
notification_count: number;
|
notification_count?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRoomEvent extends IMinimalEvent {
|
export interface IRoomEvent extends IMinimalEvent {
|
||||||
@@ -64,7 +64,7 @@ interface IState {
|
|||||||
|
|
||||||
export interface ITimeline {
|
export interface ITimeline {
|
||||||
events: Array<IRoomEvent | IStateEvent>;
|
events: Array<IRoomEvent | IStateEvent>;
|
||||||
limited: boolean;
|
limited?: boolean;
|
||||||
prev_batch: string;
|
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
|
* The purpose of this class is to accumulate /sync responses such that a
|
||||||
* complete "initial" JSON response can be returned which accurately represents
|
* 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
|
* /sync response from the 'rooms' key onwards. The "accountData" key is
|
||||||
* a list of raw events which represent global account data.
|
* a list of raw events which represent global account data.
|
||||||
*/
|
*/
|
||||||
public getJSON(forDatabase = false): object {
|
public getJSON(forDatabase = false): ISyncData {
|
||||||
const data = {
|
const data: IRooms = {
|
||||||
join: {},
|
join: {},
|
||||||
invite: {},
|
invite: {},
|
||||||
// always empty. This is set by /sync when a room was previously
|
// always empty. This is set by /sync when a room was previously
|
||||||
@@ -575,7 +582,7 @@ export class SyncAccumulator {
|
|||||||
prev_batch: null,
|
prev_batch: null,
|
||||||
},
|
},
|
||||||
unread_notifications: roomData._unreadNotifications,
|
unread_notifications: roomData._unreadNotifications,
|
||||||
summary: roomData._summary,
|
summary: roomData._summary as IRoomSummary,
|
||||||
};
|
};
|
||||||
// Add account data
|
// Add account data
|
||||||
Object.keys(roomData._accountData).forEach((evType) => {
|
Object.keys(roomData._accountData).forEach((evType) => {
|
||||||
@@ -678,7 +685,7 @@ export class SyncAccumulator {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add account data
|
// Add account data
|
||||||
const accData = [];
|
const accData: IMinimalEvent[] = [];
|
||||||
Object.keys(this.accountData).forEach((evType) => {
|
Object.keys(this.accountData).forEach((evType) => {
|
||||||
accData.push(this.accountData[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 syncStateData: ISyncStateData = null; // additional data (eg. error object for failed sync)
|
||||||
private catchingUp = false;
|
private catchingUp = false;
|
||||||
private running = false;
|
private running = false;
|
||||||
private keepAliveTimer: NodeJS.Timeout = null;
|
private keepAliveTimer: number = null;
|
||||||
private connectionReturnedDefer: IDeferred<boolean> = null;
|
private connectionReturnedDefer: IDeferred<boolean> = null;
|
||||||
private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response
|
private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response
|
||||||
private failedSyncCount = 0; // Number of consecutive failed /sync requests
|
private failedSyncCount = 0; // Number of consecutive failed /sync requests
|
||||||
@@ -319,7 +319,7 @@ export class SyncApi {
|
|||||||
this._peekRoom = this.createRoom(roomId);
|
this._peekRoom = this.createRoom(roomId);
|
||||||
return this.client.roomInitialSync(roomId, 20).then((response) => {
|
return this.client.roomInitialSync(roomId, 20).then((response) => {
|
||||||
// make sure things are init'd
|
// make sure things are init'd
|
||||||
response.messages = response.messages || {};
|
response.messages = response.messages || { chunk: [] };
|
||||||
response.messages.chunk = response.messages.chunk || [];
|
response.messages.chunk = response.messages.chunk || [];
|
||||||
response.state = response.state || [];
|
response.state = response.state || [];
|
||||||
|
|
||||||
@@ -330,8 +330,7 @@ export class SyncApi {
|
|||||||
const stateEvents = response.state.map(client.getEventMapper());
|
const stateEvents = response.state.map(client.getEventMapper());
|
||||||
const messages = response.messages.chunk.map(client.getEventMapper());
|
const messages = response.messages.chunk.map(client.getEventMapper());
|
||||||
|
|
||||||
// XXX: copypasted from /sync until we kill off this
|
// XXX: copypasted from /sync until we kill off this minging v1 API stuff)
|
||||||
// minging v1 API stuff)
|
|
||||||
// handle presence events (User objects)
|
// handle presence events (User objects)
|
||||||
if (response.presence && Array.isArray(response.presence)) {
|
if (response.presence && Array.isArray(response.presence)) {
|
||||||
response.presence.map(client.getEventMapper()).forEach(
|
response.presence.map(client.getEventMapper()).forEach(
|
||||||
@@ -644,12 +643,12 @@ export class SyncApi {
|
|||||||
// Now wait for the saved sync to finish...
|
// Now wait for the saved sync to finish...
|
||||||
debuglog("Waiting for saved sync before starting sync processing...");
|
debuglog("Waiting for saved sync before starting sync processing...");
|
||||||
await savedSyncPromise;
|
await savedSyncPromise;
|
||||||
this._sync({ filterId });
|
this.doSync({ filterId });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (client.isGuest()) {
|
if (client.isGuest()) {
|
||||||
// no push rules for guests, no access to POST filter for guests.
|
// no push rules for guests, no access to POST filter for guests.
|
||||||
this._sync({});
|
this.doSync({});
|
||||||
} else {
|
} else {
|
||||||
// Pull the saved sync token out first, before the worker starts sending
|
// 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
|
// 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 {string} syncOptions.filterId
|
||||||
* @param {boolean} syncOptions.hasSyncedBefore
|
* @param {boolean} syncOptions.hasSyncedBefore
|
||||||
*/
|
*/
|
||||||
private async _sync(syncOptions: ISyncOptions): Promise<void> {
|
private async doSync(syncOptions: ISyncOptions): Promise<void> {
|
||||||
const client = this.client;
|
const client = this.client;
|
||||||
|
|
||||||
if (!this.running) {
|
if (!this.running) {
|
||||||
@@ -852,7 +851,7 @@ export class SyncApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Begin next sync
|
// Begin next sync
|
||||||
this._sync(syncOptions);
|
this.doSync(syncOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IRequestPromise<ISyncResponse> {
|
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IRequestPromise<ISyncResponse> {
|
||||||
@@ -957,7 +956,7 @@ export class SyncApi {
|
|||||||
catchingUp: true,
|
catchingUp: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this._sync(syncOptions);
|
this.doSync(syncOptions);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.currentSyncRequest = null;
|
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
|
* @return {string} The encoded string e.g. foo=bar&baz=taz
|
||||||
*/
|
*/
|
||||||
export function encodeParams(params: Record<string, string>): string {
|
export function encodeParams(params: Record<string, string>): string {
|
||||||
let qs = "";
|
return new URLSearchParams(params).toString();
|
||||||
for (const key in params) {
|
}
|
||||||
if (!params.hasOwnProperty(key)) {
|
|
||||||
continue;
|
export type QueryDict = Record<string, string | string[]>;
|
||||||
}
|
|
||||||
qs += "&" + encodeURIComponent(key) + "=" +
|
/**
|
||||||
encodeURIComponent(params[key]);
|
* 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.
|
* @throws If the object is missing keys.
|
||||||
*/
|
*/
|
||||||
// note using 'keys' here would shadow the 'keys' function defined above
|
// note using 'keys' here would shadow the 'keys' function defined above
|
||||||
export function checkObjectHasKeys(obj: object, keys_: string[]) {
|
export function checkObjectHasKeys(obj: object, keys: string[]) {
|
||||||
for (let i = 0; i < keys_.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
if (!obj.hasOwnProperty(keys_[i])) {
|
if (!obj.hasOwnProperty(keys[i])) {
|
||||||
throw new Error("Missing required key: " + 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()));
|
return new Promise((resolve) => resolve(fn()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -670,3 +683,13 @@ export function lexicographicCompare(a: string, b: string): number {
|
|||||||
// hidden the operation in this function.
|
// hidden the operation in this function.
|
||||||
return (a < b) ? -1 : ((a === b) ? 0 : 1);
|
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.
|
// yet, null if we have but they didn't send a party ID.
|
||||||
private opponentPartyId: string;
|
private opponentPartyId: string;
|
||||||
private opponentCaps: CallCapabilities;
|
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
|
// 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
|
// This flag represents whether we want the other party to be on hold
|
||||||
|
@@ -47,12 +47,12 @@ export class CallEventHandler {
|
|||||||
|
|
||||||
public start() {
|
public start() {
|
||||||
this.client.on("sync", this.evaluateEventBuffer);
|
this.client.on("sync", this.evaluateEventBuffer);
|
||||||
this.client.on("event", this.onEvent);
|
this.client.on("Room.timeline", this.onRoomTimeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
public stop() {
|
public stop() {
|
||||||
this.client.removeListener("sync", this.evaluateEventBuffer);
|
this.client.removeListener("sync", this.evaluateEventBuffer);
|
||||||
this.client.removeListener("event", this.onEvent);
|
this.client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
private evaluateEventBuffer = async () => {
|
private evaluateEventBuffer = async () => {
|
||||||
@@ -89,7 +89,7 @@ export class CallEventHandler {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onEvent = (event: MatrixEvent) => {
|
private onRoomTimeline = (event: MatrixEvent) => {
|
||||||
this.client.decryptEventIfNeeded(event);
|
this.client.decryptEventIfNeeded(event);
|
||||||
// any call events or ones that might be once they're decrypted
|
// any call events or ones that might be once they're decrypted
|
||||||
if (this.eventIsACall(event) || event.isBeingDecrypted()) {
|
if (this.eventIsACall(event) || event.isBeingDecrypted()) {
|
||||||
|
Reference in New Issue
Block a user