1
0
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:
Michael Telatynski
2021-07-23 23:46:15 +01:00
66 changed files with 4485 additions and 2662 deletions

View File

@@ -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
View File

@@ -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/

View File

@@ -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
View 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
```

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View File

@@ -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;

View File

@@ -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(

View File

@@ -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
View 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 */

View File

@@ -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;

View File

@@ -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;

View File

@@ -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",
}

View File

@@ -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
View 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 */

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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,
}; };
} }

View File

@@ -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;

View File

@@ -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)) {

View File

@@ -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,
); );

View File

@@ -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 */

View File

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

View File

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

View File

@@ -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;
} }
/** /**

View File

@@ -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"]) {

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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]: {

View File

@@ -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) {

View File

@@ -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 */

View File

@@ -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

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
*/ */

View File

@@ -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.

View File

@@ -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);

View File

@@ -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, "");
} }
/** /**

View File

@@ -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

View File

@@ -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;

View File

@@ -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 =

View File

@@ -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 || [];

View File

@@ -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"];

View File

@@ -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
} }

View File

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

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

View File

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

View File

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

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

View File

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

View File

@@ -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");

View File

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

View File

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

View File

@@ -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]);
}); });

View File

@@ -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;

View File

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

View File

@@ -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

View File

@@ -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()) {

2865
yarn.lock

File diff suppressed because it is too large Load Diff