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`
/git-revision.txt
/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)
==================================================================================================
[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",
"version": "12.0.1",
"version": "12.1.0",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",
@@ -15,7 +15,8 @@
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "jsdoc -c jsdoc.json -P package.json",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 57 src spec",
"lint:js": "eslint --max-warnings 7 src spec",
"lint:js-fix": "eslint --fix src spec",
"lint:types": "tsc --noEmit",
"test": "jest",
"test:watch": "jest --watch",

View File

@@ -100,7 +100,7 @@ fi
# global cache here to ensure we get the right thing.
yarn cache clean
# Ensure all dependencies are updated
yarn install --ignore-scripts
yarn install --ignore-scripts --pure-lockfile
if [ -z "$skip_changelog" ]; then
# update_changelog doesn't have a --version flag
@@ -225,7 +225,7 @@ if [ $dodist -eq 0 ]; then
pushd "$builddir"
git clone "$projdir" .
git checkout "$rel_branch"
yarn install
yarn install --pure-lockfile
# We haven't tagged yet, so tell the dist script what version
# it's building
DIST_VERSION="$tag" yarn dist

View File

@@ -91,7 +91,7 @@ export function mkEvent(opts) {
event.state_key = opts.skey;
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
"m.room.power_levels", "m.room.topic",
"com.example.state"].indexOf(opts.type) !== -1) {
"com.example.state"].includes(opts.type)) {
event.state_key = "";
}
return opts.event ? new MatrixEvent(event) : event;

View File

@@ -52,7 +52,7 @@ const ENCRYPTED_EVENT = new MatrixEvent({
origin_server_ts: 1507753886000,
});
const KEY_BACKUP_DATA = {
const CURVE25519_KEY_BACKUP_DATA = {
first_message_index: 0,
forwarded_count: 0,
is_verified: false,
@@ -73,7 +73,26 @@ const KEY_BACKUP_DATA = {
},
};
const BACKUP_INFO = {
const AES256_KEY_BACKUP_DATA = {
first_message_index: 0,
forwarded_count: 0,
is_verified: false,
session_data: {
iv: 'b3Jqqvm5S9QdmXrzssspLQ',
ciphertext: 'GOOASO3E9ThogkG0zMjEduGLM3u9jHZTkS7AvNNbNj3q1znwk4OlaVKXce'
+ '7ynofiiYIiS865VlOqrKEEXv96XzRyUpgn68e3WsicwYl96EtjIEh/iY003PG2Qd'
+ 'EluT899Ax7PydpUHxEktbWckMppYomUR5q8x1KI1SsOQIiJaIGThmIMPANRCFiK0'
+ 'WQj+q+dnhzx4lt9AFqU5bKov8qKnw2qGYP7/+6RmJ0Kpvs8tG6lrcNDEHtFc2r0r'
+ 'KKubDypo0Vc8EWSwsAHdKa36ewRavpreOuE8Z9RLfY0QIR1ecXrMqW0CdGFr7H3P'
+ 'vcjF8sjwvQAavzxEKT1WMGizSMLeKWo2mgZ5cKnwV5HGUAw596JQvKs9laG2U89K'
+ 'YrT0sH30vi62HKzcBLcDkWkUSNYPz7UiZ1MM0L380UA+1ZOXSOmtBA9xxzzbc8Xd'
+ 'fRimVgklGdxrxjzuNLYhL2BvVH4oPWonD9j0bvRwE6XkimdbGQA8HB7UmXXjE8WA'
+ 'RgaDHkfzoA3g3aeQ',
mac: 'uR988UYgGL99jrvLLPX3V1ows+UYbktTmMxPAo2kxnU',
},
};
const CURVE25519_BACKUP_INFO = {
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
version: 1,
auth_data: {
@@ -81,6 +100,14 @@ const BACKUP_INFO = {
},
};
const AES256_BACKUP_INFO = {
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
version: 1,
auth_data: {
// FIXME: add iv and mac
},
};
const keys = {};
function getCrossSigningKey(type) {
@@ -144,7 +171,7 @@ describe("MegolmBackup", function() {
mockCrypto.backupKey.set_recipient_key(
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
);
mockCrypto.backupInfo = BACKUP_INFO;
mockCrypto.backupInfo = CURVE25519_BACKUP_INFO;
mockStorage = new MockStorageApi();
sessionStore = new WebStorageSessionStore(mockStorage);
@@ -228,7 +255,7 @@ describe("MegolmBackup", function() {
});
});
it('sends backups to the server', function() {
it('sends backups to the server (Curve25519 version)', function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
@@ -306,6 +333,88 @@ describe("MegolmBackup", function() {
});
});
it('sends backups to the server (AES-256 version)', function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key());
const client = makeTestClient(sessionStore, cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto()
.then(() => {
return client.crypto.storeSessionBackupPrivateKey(new Uint8Array(32));
})
.then(() => {
return cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice._pickleKey),
},
txn);
});
})
.then(() => {
client.enableKeyBackup({
algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2",
version: 1,
auth_data: {
iv: "PsCAtR7gMc4xBd9YS3A9Ow",
mac: "ZSDsTFEZK7QzlauCLMleUcX96GQZZM7UNtk4sripSqQ",
},
});
let numCalls = 0;
return new Promise((resolve, reject) => {
client.http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
++numCalls;
expect(numCalls).toBeLessThanOrEqual(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe(1);
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
groupSession.session_id(),
);
resolve();
return Promise.resolve({});
};
client.crypto.backupManager.backupGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
);
}).then(() => {
expect(numCalls).toBe(1);
});
});
});
it('signs backups with the cross-signing master key', async function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
@@ -512,30 +621,47 @@ describe("MegolmBackup", function() {
client.stopClient();
});
it('can restore from backup', function() {
it('can restore from backup (Curve25519 version)', function() {
client.http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA);
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
BACKUP_INFO,
CURVE25519_BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
expect(res.untrusted).toBeTruthy(); // keys from backup are untrusted
expect(res.untrusted).toBeTruthy(); // keys from Curve25519 backup are untrusted
});
});
it('can restore backup by room', function() {
it('can restore from backup (AES-256 version)', function() {
client.http.authedRequest = function() {
return Promise.resolve(AES256_KEY_BACKUP_DATA);
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
AES256_BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
expect(res.untrusted).toBeFalsy(); // keys from AES backup are trusted
});
});
it('can restore backup by room (Curve25519 version)', function() {
client.http.authedRequest = function() {
return Promise.resolve({
rooms: {
[ROOM_ID]: {
sessions: {
[SESSION_ID]: KEY_BACKUP_DATA,
[SESSION_ID]: CURVE25519_KEY_BACKUP_DATA,
},
},
},
@@ -543,7 +669,7 @@ describe("MegolmBackup", function() {
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
null, null, BACKUP_INFO,
null, null, CURVE25519_BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
@@ -562,14 +688,14 @@ describe("MegolmBackup", function() {
const cachedNull = await client.crypto.getSessionBackupPrivateKey();
expect(cachedNull).toBeNull();
client.http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA);
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
};
await new Promise((resolve) => {
client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
BACKUP_INFO,
CURVE25519_BACKUP_INFO,
{ cacheCompleteCallback: resolve },
);
});
@@ -578,11 +704,11 @@ describe("MegolmBackup", function() {
});
it("fails if an known algorithm is used", async function() {
const BAD_BACKUP_INFO = Object.assign({}, BACKUP_INFO, {
const BAD_BACKUP_INFO = Object.assign({}, CURVE25519_BACKUP_INFO, {
algorithm: "this.algorithm.does.not.exist",
});
client.http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA);
return Promise.resolve(CURVE25519_KEY_BACKUP_DATA);
};
await expect(client.restoreKeyBackupWithRecoveryKey(

View File

@@ -3,6 +3,7 @@ import { EventStatus, MatrixEvent } from "../../src";
import { EventTimeline } from "../../src/models/event-timeline";
import { RoomState } from "../../src";
import { Room } from "../../src";
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
import { TestClient } from "../TestClient";
describe("Room", function() {
@@ -1456,4 +1457,291 @@ describe("Room", function() {
expect(room.maySendMessage()).toEqual(true);
});
});
describe("getDefaultRoomName", function() {
it("should return 'Empty room' if a user is the only member",
function() {
const room = new Room(roomId, null, userA);
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
});
it("should return a display name if one other member is in the room",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("User B");
});
it("should return a display name if one other member is banned",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "ban",
room: roomId, event: true, name: "User B",
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)");
});
it("should return a display name if one other member is invited",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "invite",
room: roomId, event: true, name: "User B",
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("User B");
});
it("should return 'Empty room (was User B)' if User B left the room",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "leave",
room: roomId, event: true, name: "User B",
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)");
});
it("should return 'User B and User C' if in a room with two other users",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
utils.mkMembership({
user: userC, mship: "join",
room: roomId, event: true, name: "User C",
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("User B and User C");
});
it("should return 'User B and 2 others' if in a room with three other users",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
utils.mkMembership({
user: userC, mship: "join",
room: roomId, event: true, name: "User C",
}),
utils.mkMembership({
user: userD, mship: "join",
room: roomId, event: true, name: "User D",
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("User B and 2 others");
});
describe("io.element.functional_users", function() {
it("should return a display name (default behaviour) if no one is marked as a functional member",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
utils.mkEvent({
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
room: roomId, event: true,
content: {
service_members: [],
},
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("User B");
});
it("should return a display name (default behaviour) if service members is a number (invalid)",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
utils.mkEvent({
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
room: roomId, event: true,
content: {
service_members: 1,
},
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("User B");
});
it("should return a display name (default behaviour) if service members is a string (invalid)",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
utils.mkEvent({
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
room: roomId, event: true,
content: {
service_members: userB,
},
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("User B");
});
it("should return 'Empty room' if the only other member is a functional member",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
utils.mkEvent({
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
room: roomId, event: true,
content: {
service_members: [userB],
},
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
});
it("should return 'User B' if User B is the only other member who isn't a functional member",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
utils.mkMembership({
user: userC, mship: "join",
room: roomId, event: true, name: "User C",
}),
utils.mkEvent({
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
room: roomId, event: true, user: userA,
content: {
service_members: [userC],
},
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("User B");
});
it("should return 'Empty room' if all other members are functional members",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
utils.mkMembership({
user: userC, mship: "join",
room: roomId, event: true, name: "User C",
}),
utils.mkEvent({
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
room: roomId, event: true, user: userA,
content: {
service_members: [userB, userC],
},
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
});
it("should not break if an unjoined user is marked as a service user",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([
utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true, name: "User A",
}),
utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true, name: "User B",
}),
utils.mkEvent({
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
room: roomId, event: true, user: userA,
content: {
service_members: [userC],
},
}),
]);
expect(room.getDefaultRoomName(userA)).toEqual("User B");
});
});
});
});

164
src/@types/PushRules.ts Normal file
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");
/**
* Functional members type for declaring a purpose of room members (e.g. helpful bots).
* Note that this reference is UNSTABLE and subject to breaking changes, including its
* eventual removal.
*
* Schema (TypeScript):
* {
* service_members?: string[]
* }
*
* Example:
* {
* "service_members": [
* "@helperbot:localhost",
* "@reminderbot:alice.tdl"
* ]
* }
*/
export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
"io.element.functional_members",
"io.element.functional_members");
export interface IEncryptedFile {
url: string;
mimetype?: string;

View File

@@ -20,6 +20,12 @@ import "@matrix-org/olm";
export {};
declare global {
// use `number` as the return type in all cases for global.set{Interval,Timeout},
// so we don't accidentally use the methods on NodeJS.Timeout - they only exist in a subset of environments.
// The overload for clear{Interval,Timeout} is resolved as expected.
function setInterval(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
function setTimeout(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
namespace NodeJS {
interface Global {
localStorage: Storage;

View File

@@ -39,3 +39,38 @@ export enum Preset {
}
export type ResizeMethod = "crop" | "scale";
// TODO move to http-api after TSification
export interface IAbortablePromise<T> extends Promise<T> {
abort(): void;
}
export type IdServerUnbindResult = "no-support" | "success";
// Knock and private are reserved keywords which are not yet implemented.
export enum JoinRule {
Public = "public",
Invite = "invite",
/**
* @deprecated Reserved keyword. Should not be used. Not yet implemented.
*/
Private = "private",
Knock = "knock", // MSC2403 - only valid inside experimental room versions at this time.
Restricted = "restricted", // MSC3083 - only valid inside experimental room versions at this time.
}
export enum RestrictedAllowType {
RoomMembership = "m.room_membership", // MSC3083 - only valid inside experimental room versions at this time.
}
export enum GuestAccess {
CanJoin = "can_join",
Forbidden = "forbidden",
}
export enum HistoryVisibility {
Invited = "invited",
Joined = "joined",
Shared = "shared",
WorldReadable = "world_readable",
}

View File

@@ -15,9 +15,12 @@ limitations under the License.
*/
import { Callback } from "../client";
import { IContent } from "../models/event";
import { Preset, Visibility } from "./partials";
import { SearchKey } from "./search";
import { IRoomEventFilter } from "../filter";
// allow camelcase as these are things go onto the wire
// allow camelcase as these are things that go onto the wire
/* eslint-disable camelcase */
export interface IJoinRoomOpts {
@@ -63,12 +66,12 @@ export interface IGuestAccessOpts {
}
export interface ISearchOpts {
keys?: string[];
keys?: SearchKey[];
query: string;
}
export interface IEventSearchOpts {
filter: any; // TODO: Types
filter?: IRoomEventFilter;
term: string;
}
@@ -82,7 +85,7 @@ export interface IInvite3PID {
export interface ICreateRoomStateEvent {
type: string;
state_key?: string; // defaults to an empty string
content: object;
content: IContent;
}
export interface ICreateRoomOpts {
@@ -104,9 +107,11 @@ export interface IRoomDirectoryOptions {
server?: string;
limit?: number;
since?: string;
// TODO: Proper types
filter?: any & {generic_search_term: string};
filter?: {
generic_search_term: string;
};
include_all_networks?: boolean;
third_party_instance_id?: string;
}
export interface IUploadOpts {
@@ -119,4 +124,19 @@ export interface IUploadOpts {
progressHandler?: (state: {loaded: number, total: number}) => void;
}
export interface IAddThreePidOnlyBody {
auth?: {
type: string;
session?: string;
};
client_secret: string;
sid: string;
}
export interface IBindThreePidBody {
client_secret: string;
id_server: string;
id_access_token: string;
sid: string;
}
/* eslint-enable camelcase */

118
src/@types/search.ts Normal file
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;
};
}
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 */
import { MsgType } from "./@types/event";
/**
* Generates the content for a HTML Message event
* @param {string} body the plaintext body of the message
@@ -25,7 +27,7 @@ limitations under the License.
*/
export function makeHtmlMessage(body: string, htmlBody: string) {
return {
msgtype: "m.text",
msgtype: MsgType.Text,
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
@@ -40,7 +42,7 @@ export function makeHtmlMessage(body: string, htmlBody: string) {
*/
export function makeHtmlNotice(body: string, htmlBody: string) {
return {
msgtype: "m.notice",
msgtype: MsgType.Notice,
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
@@ -55,7 +57,7 @@ export function makeHtmlNotice(body: string, htmlBody: string) {
*/
export function makeHtmlEmote(body: string, htmlBody: string) {
return {
msgtype: "m.emote",
msgtype: MsgType.Emote,
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
@@ -69,7 +71,7 @@ export function makeHtmlEmote(body: string, htmlBody: string) {
*/
export function makeTextMessage(body: string) {
return {
msgtype: "m.text",
msgtype: MsgType.Text,
body: body,
};
}
@@ -81,7 +83,7 @@ export function makeTextMessage(body: string) {
*/
export function makeNotice(body: string) {
return {
msgtype: "m.notice",
msgtype: MsgType.Notice,
body: body,
};
}
@@ -93,7 +95,7 @@ export function makeNotice(body: string) {
*/
export function makeEmoteMessage(body: string) {
return {
msgtype: "m.emote",
msgtype: MsgType.Emote,
body: body,
};
}

View File

@@ -102,7 +102,7 @@ export class DeviceList extends EventEmitter {
// The time the save is scheduled for
private savePromiseTime: number = null;
// The timer used to delay the save
private saveTimer: NodeJS.Timeout = null;
private saveTimer: number = null;
// True if we have fetched data from the server or loaded a non-empty
// set of device data from the store
private hasFetched: boolean = null;

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 { MatrixEvent } from "../models/event";
import { EventEmitter } from "events";
@@ -9,10 +25,10 @@ import {
CrossSigningKeys,
ICrossSigningKey,
ICryptoCallbacks,
ISecretStorageKeyInfo,
ISignedKey,
KeySignatures,
} from "../matrix";
import { ISecretStorageKeyInfo } from "./api";
import { IKeyBackupInfo } from "./keybackup";
interface ICrossSigningKeys {
@@ -109,8 +125,8 @@ export class EncryptionSetupBuilder {
* @param {Object} content
* @return {Promise}
*/
public setAccountData(type: string, content: object): Promise<void> {
return this.accountDataClientAdapter.setAccountData(type, content);
public async setAccountData(type: string, content: object): Promise<void> {
await this.accountDataClientAdapter.setAccountData(type, content);
}
/**
@@ -246,7 +262,7 @@ export class EncryptionSetupOperation {
* implementing the methods related to account data in MatrixClient
*/
class AccountDataClientAdapter extends EventEmitter {
public readonly values = new Map<string, object>();
public readonly values = new Map<string, MatrixEvent>();
/**
* @param {Object.<String, MatrixEvent>} existingValues existing account data
@@ -259,7 +275,7 @@ class AccountDataClientAdapter extends EventEmitter {
* @param {String} type
* @return {Promise<Object>} the content of the account data
*/
public getAccountDataFromServer(type: string): Promise<object> {
public getAccountDataFromServer(type: string): Promise<any> {
return Promise.resolve(this.getAccountData(type));
}
@@ -267,7 +283,7 @@ class AccountDataClientAdapter extends EventEmitter {
* @param {String} type
* @return {Object} the content of the account data
*/
public getAccountData(type: string): object {
public getAccountData(type: string): MatrixEvent {
const modifiedValue = this.values.get(type);
if (modifiedValue) {
return modifiedValue;
@@ -284,7 +300,7 @@ class AccountDataClientAdapter extends EventEmitter {
* @param {Object} content
* @return {Promise}
*/
public setAccountData(type: string, content: object): Promise<void> {
public setAccountData(type: string, content: any): Promise<{}> {
const lastEvent = this.values.get(type);
this.values.set(type, content);
// ensure accountData is emitted on the next tick,
@@ -293,6 +309,7 @@ class AccountDataClientAdapter extends EventEmitter {
return Promise.resolve().then(() => {
const event = new MatrixEvent({ type, content });
this.emit("accountData", event, lastEvent);
return {};
});
}
}
@@ -337,7 +354,7 @@ class SSSSCryptoCallbacks {
constructor(private readonly delegateCryptoCallbacks: ICryptoCallbacks) {}
public async getSecretStorageKey(
{ keys }: { keys: Record<string, object> },
{ keys }: { keys: Record<string, ISecretStorageKeyInfo> },
name: string,
): Promise<[string, Uint8Array]> {
for (const keyId of Object.keys(keys)) {

View File

@@ -78,7 +78,7 @@ export enum RoomKeyRequestState {
export class OutgoingRoomKeyRequestManager {
// handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null
// if the callback has been set, or if it is still running.
private sendOutgoingRoomKeyRequestsTimer: NodeJS.Timeout = null;
private sendOutgoingRoomKeyRequestsTimer: number = null;
// sanity check to ensure that we don't end up with two concurrent runs
// of sendOutgoingRoomKeyRequests
@@ -366,7 +366,7 @@ export class OutgoingRoomKeyRequestManager {
});
};
this.sendOutgoingRoomKeyRequestsTimer = global.setTimeout(
this.sendOutgoingRoomKeyRequestsTimer = setTimeout(
startSendingOutgoingRoomKeyRequests,
SEND_KEY_REQUESTS_DELAY_MS,
);

View File

@@ -24,10 +24,10 @@ import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { CryptoStore } from "../client";
/* eslint-disable camelcase */
interface IRoomEncryption {
export interface IRoomEncryption {
algorithm: string;
rotation_period_ms: number;
rotation_period_msgs: number;
rotation_period_ms?: number;
rotation_period_msgs?: number;
}
/* eslint-enable camelcase */

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");
you may not use this file except in compliance with the License.
@@ -14,61 +14,95 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventEmitter } from 'events';
import { logger } from '../logger';
import * as olmlib from './olmlib';
import { randomString } from '../randomstring';
import { encryptAES, decryptAES } from './aes';
import { encryptAES, decryptAES, IEncryptedPayload, calculateKeyCheck } from './aes';
import { encodeBase64 } from "./olmlib";
import { ICryptoCallbacks, MatrixClient, MatrixEvent } from '../matrix';
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api';
import { EventEmitter } from 'stream';
export const SECRET_STORAGE_ALGORITHM_V1_AES
= "m.secret_storage.v1.aes-hmac-sha2";
export const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2";
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
// Some of the key functions use a tuple and some use an object...
export type SecretStorageKeyTuple = [keyId: string, keyInfo: ISecretStorageKeyInfo];
export type SecretStorageKeyObject = {keyId: string, keyInfo: ISecretStorageKeyInfo};
export interface ISecretRequest {
requestId: string;
promise: Promise<string>;
cancel: (reason: string) => void;
}
export interface IAccountDataClient extends EventEmitter {
// Subset of MatrixClient (which also uses any for the event content)
getAccountDataFromServer: (eventType: string) => Promise<Record<string, any>>;
getAccountData: (eventType: string) => MatrixEvent;
setAccountData: (eventType: string, content: any) => Promise<{}>;
}
interface ISecretRequestInternal {
name: string;
devices: string[];
resolve: (reason: string) => void;
reject: (error: Error) => void;
}
interface IDecryptors {
encrypt: (plaintext: string) => Promise<IEncryptedPayload>;
decrypt: (ciphertext: IEncryptedPayload) => Promise<string>;
}
/**
* Implements Secure Secret Storage and Sharing (MSC1946)
* @module crypto/SecretStorage
*/
export class SecretStorage extends EventEmitter {
constructor(baseApis, cryptoCallbacks) {
super();
this._baseApis = baseApis;
this._cryptoCallbacks = cryptoCallbacks;
this._requests = {};
this._incomingRequests = {};
}
export class SecretStorage {
private requests = new Map<string, ISecretRequestInternal>();
async getDefaultKeyId() {
const defaultKey = await this._baseApis.getAccountDataFromServer(
// In it's pure javascript days, this was relying on some proper Javascript-style
// type-abuse where sometimes we'd pass in a fake client object with just the account
// data methods implemented, which is all this class needs unless you use the secret
// sharing code, so it was fine. As a low-touch TypeScript migration, this now has
// an extra, optional param for a real matrix client, so you can not pass it as long
// as you don't request any secrets.
// A better solution would probably be to split this class up into secret storage and
// secret sharing which are really two separate things, even though they share an MSC.
constructor(
private readonly accountDataAdapter: IAccountDataClient,
private readonly cryptoCallbacks: ICryptoCallbacks,
private readonly baseApis?: MatrixClient,
) {}
public async getDefaultKeyId(): Promise<string> {
const defaultKey = await this.accountDataAdapter.getAccountDataFromServer(
'm.secret_storage.default_key',
);
if (!defaultKey) return null;
return defaultKey.key;
}
setDefaultKeyId(keyId) {
return new Promise(async (resolve, reject) => {
const listener = (ev) => {
public setDefaultKeyId(keyId: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
const listener = (ev: MatrixEvent): void => {
if (
ev.getType() === 'm.secret_storage.default_key' &&
ev.getContent().key === keyId
) {
this._baseApis.removeListener('accountData', listener);
this.accountDataAdapter.removeListener('accountData', listener);
resolve();
}
};
this._baseApis.on('accountData', listener);
this.accountDataAdapter.on('accountData', listener);
try {
await this._baseApis.setAccountData(
this.accountDataAdapter.setAccountData(
'm.secret_storage.default_key',
{ key: keyId },
);
} catch (e) {
this._baseApis.removeListener('accountData', listener);
).catch(e => {
this.accountDataAdapter.removeListener('accountData', listener);
reject(e);
}
});
});
}
@@ -85,10 +119,14 @@ export class SecretStorage extends EventEmitter {
* keyId: {string} the ID of the key
* keyInfo: {object} details about the key (iv, mac, passphrase)
*/
async addKey(algorithm, opts, keyId) {
const keyInfo = { algorithm };
public async addKey(
algorithm: string,
opts: IAddSecretStorageKeyOpts,
keyId?: string,
): Promise<SecretStorageKeyObject> {
const keyInfo = { algorithm } as ISecretStorageKeyInfo;
if (!opts) opts = {};
if (!opts) opts = {} as IAddSecretStorageKeyOpts;
if (opts.name) {
keyInfo.name = opts.name;
@@ -99,25 +137,25 @@ export class SecretStorage extends EventEmitter {
keyInfo.passphrase = opts.passphrase;
}
if (opts.key) {
const { iv, mac } = await SecretStorage._calculateKeyCheck(opts.key);
const { iv, mac } = await calculateKeyCheck(opts.key);
keyInfo.iv = iv;
keyInfo.mac = mac;
}
} else {
throw new Error(`Unknown key algorithm ${opts.algorithm}`);
throw new Error(`Unknown key algorithm ${algorithm}`);
}
if (!keyId) {
do {
keyId = randomString(32);
} while (
await this._baseApis.getAccountDataFromServer(
await this.accountDataAdapter.getAccountDataFromServer(
`m.secret_storage.key.${keyId}`,
)
);
}
await this._baseApis.setAccountData(
await this.accountDataAdapter.setAccountData(
`m.secret_storage.key.${keyId}`, keyInfo,
);
@@ -134,8 +172,9 @@ export class SecretStorage extends EventEmitter {
* for. Defaults to the default key ID if not provided.
* @returns {Array?} If the key was found, the return value is an array of
* the form [keyId, keyInfo]. Otherwise, null is returned.
* XXX: why is this an array when addKey returns an object?
*/
async getKey(keyId) {
public async getKey(keyId: string): Promise<SecretStorageKeyTuple | null> {
if (!keyId) {
keyId = await this.getDefaultKeyId();
}
@@ -143,9 +182,9 @@ export class SecretStorage extends EventEmitter {
return null;
}
const keyInfo = await this._baseApis.getAccountDataFromServer(
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
) as ISecretStorageKeyInfo;
return keyInfo ? [keyId, keyInfo] : null;
}
@@ -156,8 +195,8 @@ export class SecretStorage extends EventEmitter {
* for. Defaults to the default key ID if not provided.
* @return {boolean} Whether we have the key.
*/
async hasKey(keyId) {
return !!(await this.getKey(keyId));
public async hasKey(keyId?: string): Promise<boolean> {
return Boolean(await this.getKey(keyId));
}
/**
@@ -168,10 +207,10 @@ export class SecretStorage extends EventEmitter {
*
* @return {boolean} whether or not the key matches
*/
async checkKey(key, info) {
public async checkKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise<boolean> {
if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
if (info.mac) {
const { mac } = await SecretStorage._calculateKeyCheck(key, info.iv);
const { mac } = await calculateKeyCheck(key, info.iv);
return info.mac.replace(/=+$/g, '') === mac.replace(/=+$/g, '');
} else {
// if we have no information, we have to assume the key is right
@@ -182,10 +221,6 @@ export class SecretStorage extends EventEmitter {
}
}
static async _calculateKeyCheck(key, iv) {
return await encryptAES(ZERO_STR, key, "", iv);
}
/**
* Store an encrypted secret on the server
*
@@ -194,7 +229,7 @@ export class SecretStorage extends EventEmitter {
* @param {Array} keys The IDs of the keys to use to encrypt the secret
* or null/undefined to use the default key.
*/
async store(name, secret, keys) {
public async store(name: string, secret: string, keys?: string[]): Promise<void> {
const encrypted = {};
if (!keys) {
@@ -211,9 +246,9 @@ export class SecretStorage extends EventEmitter {
for (const keyId of keys) {
// get key information from key storage
const keyInfo = await this._baseApis.getAccountDataFromServer(
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
) as ISecretStorageKeyInfo;
if (!keyInfo) {
throw new Error("Unknown key: " + keyId);
}
@@ -221,7 +256,7 @@ export class SecretStorage extends EventEmitter {
// encrypt secret, based on the algorithm
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
const keys = { [keyId]: keyInfo };
const [, encryption] = await this._getSecretStorageKey(keys, name);
const [, encryption] = await this.getSecretStorageKey(keys, name);
encrypted[keyId] = await encryption.encrypt(secret);
} else {
logger.warn("unknown algorithm for secret storage key " + keyId
@@ -231,34 +266,7 @@ export class SecretStorage extends EventEmitter {
}
// save encrypted secret
await this._baseApis.setAccountData(name, { encrypted });
}
/**
* Temporary method to fix up existing accounts where secrets
* are incorrectly stored without the 'encrypted' level
*
* @param {string} name The name of the secret
* @param {object} secretInfo The account data object
* @returns {object} The fixed object or null if no fix was performed
*/
async _fixupStoredSecret(name, secretInfo) {
// We assume the secret was only stored passthrough for 1
// key - this was all the broken code supported.
const keys = Object.keys(secretInfo);
if (
keys.length === 1 && keys[0] !== 'encrypted' &&
secretInfo[keys[0]].passthrough
) {
const hasKey = await this.hasKey(keys[0]);
if (hasKey) {
logger.log("Fixing up passthrough secret: " + name);
await this.storePassthrough(name, keys[0]);
const newData = await this._baseApis.getAccountDataFromServer(name);
return newData;
}
}
return null;
await this.accountDataAdapter.setAccountData(name, { encrypted });
}
/**
@@ -268,24 +276,20 @@ export class SecretStorage extends EventEmitter {
*
* @return {string} the contents of the secret
*/
async get(name) {
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
public async get(name: string): Promise<string> {
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
if (!secretInfo) {
return;
}
if (!secretInfo.encrypted) {
// try to fix it up
secretInfo = await this._fixupStoredSecret(name, secretInfo);
if (!secretInfo || !secretInfo.encrypted) {
throw new Error("Content is not encrypted!");
}
}
// get possible keys to decrypt
const keys = {};
for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage
const keyInfo = await this._baseApis.getAccountDataFromServer(
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
const encInfo = secretInfo.encrypted[keyId];
@@ -306,7 +310,7 @@ export class SecretStorage extends EventEmitter {
let decryption;
try {
// fetch private key from app
[keyId, decryption] = await this._getSecretStorageKey(keys, name);
[keyId, decryption] = await this.getSecretStorageKey(keys, name);
const encInfo = secretInfo.encrypted[keyId];
@@ -331,17 +335,13 @@ export class SecretStorage extends EventEmitter {
* with, or null if it is not present or not encrypted with a trusted
* key
*/
async isStored(name, checkKey) {
public async isStored(name: string, checkKey: boolean): Promise<Record<string, ISecretStorageKeyInfo>> {
// check if secret exists
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name);
if (!secretInfo) return null;
if (!secretInfo.encrypted) {
// try to fix it up
secretInfo = await this._fixupStoredSecret(name, secretInfo);
if (!secretInfo || !secretInfo.encrypted) {
return null;
}
}
if (checkKey === undefined) checkKey = true;
@@ -350,7 +350,7 @@ export class SecretStorage extends EventEmitter {
// filter secret encryption keys with supported algorithm
for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage
const keyInfo = await this._baseApis.getAccountDataFromServer(
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
if (!keyInfo) continue;
@@ -371,45 +371,48 @@ export class SecretStorage extends EventEmitter {
*
* @param {string} name the name of the secret to request
* @param {string[]} devices the devices to request the secret from
*
* @return {string} the contents of the secret
*/
request(name, devices) {
const requestId = this._baseApis.makeTxnId();
public request(name: string, devices: string[]): ISecretRequest {
const requestId = this.baseApis.makeTxnId();
const requestControl = this._requests[requestId] = {
let resolve: (string) => void;
let reject: (Error) => void;
const promise = new Promise<string>((res, rej) => {
resolve = res;
reject = rej;
});
this.requests.set(requestId, {
name,
devices,
};
const promise = new Promise((resolve, reject) => {
requestControl.resolve = resolve;
requestControl.reject = reject;
resolve,
reject,
});
const cancel = (reason) => {
const cancel = (reason: string) => {
// send cancellation event
const cancelData = {
action: "request_cancellation",
requesting_device_id: this._baseApis.deviceId,
requesting_device_id: this.baseApis.deviceId,
request_id: requestId,
};
const toDevice = {};
for (const device of devices) {
toDevice[device] = cancelData;
}
this._baseApis.sendToDevice("m.secret.request", {
[this._baseApis.getUserId()]: toDevice,
this.baseApis.sendToDevice("m.secret.request", {
[this.baseApis.getUserId()]: toDevice,
});
// and reject the promise so that anyone waiting on it will be
// notified
requestControl.reject(new Error(reason || "Cancelled"));
reject(new Error(reason || "Cancelled"));
};
// send request to devices
const requestData = {
name,
action: "request",
requesting_device_id: this._baseApis.deviceId,
requesting_device_id: this.baseApis.deviceId,
request_id: requestId,
};
const toDevice = {};
@@ -417,21 +420,21 @@ export class SecretStorage extends EventEmitter {
toDevice[device] = requestData;
}
logger.info(`Request secret ${name} from ${devices}, id ${requestId}`);
this._baseApis.sendToDevice("m.secret.request", {
[this._baseApis.getUserId()]: toDevice,
this.baseApis.sendToDevice("m.secret.request", {
[this.baseApis.getUserId()]: toDevice,
});
return {
request_id: requestId,
requestId,
promise,
cancel,
};
}
async _onRequestReceived(event) {
public async onRequestReceived(event: MatrixEvent): Promise<void> {
const sender = event.getSender();
const content = event.getContent();
if (sender !== this._baseApis.getUserId()
if (sender !== this.baseApis.getUserId()
|| !(content.name && content.action
&& content.requesting_device_id && content.request_id)) {
// ignore requests from anyone else, for now
@@ -440,34 +443,45 @@ export class SecretStorage extends EventEmitter {
const deviceId = content.requesting_device_id;
// check if it's a cancel
if (content.action === "request_cancellation") {
/*
Looks like we intended to emit events when we got cancelations, but
we never put anything in the _incomingRequests object, and the request
itself doesn't use events anyway so if we were to wire up cancellations,
they probably ought to use the same callback interface. I'm leaving them
disabled for now while converting this file to typescript.
if (this._incomingRequests[deviceId]
&& this._incomingRequests[deviceId][content.request_id]) {
logger.info("received request cancellation for secret (" + sender
+ ", " + deviceId + ", " + content.request_id + ")");
this._baseApis.emit("crypto.secrets.requestCancelled", {
logger.info(
"received request cancellation for secret (" + sender +
", " + deviceId + ", " + content.request_id + ")",
);
this.baseApis.emit("crypto.secrets.requestCancelled", {
user_id: sender,
device_id: deviceId,
request_id: content.request_id,
});
}
*/
} else if (content.action === "request") {
if (deviceId === this._baseApis.deviceId) {
if (deviceId === this.baseApis.deviceId) {
// no point in trying to send ourself the secret
return;
}
// check if we have the secret
logger.info("received request for secret (" + sender
+ ", " + deviceId + ", " + content.request_id + ")");
if (!this._cryptoCallbacks.onSecretRequested) {
logger.info(
"received request for secret (" + sender +
", " + deviceId + ", " + content.request_id + ")",
);
if (!this.cryptoCallbacks.onSecretRequested) {
return;
}
const secret = await this._cryptoCallbacks.onSecretRequested(
const secret = await this.cryptoCallbacks.onSecretRequested(
sender,
deviceId,
content.request_id,
content.name,
this._baseApis.checkDeviceTrust(sender, deviceId),
this.baseApis.checkDeviceTrust(sender, deviceId),
);
if (secret) {
logger.info(`Preparing ${content.name} secret for ${deviceId}`);
@@ -480,25 +494,25 @@ export class SecretStorage extends EventEmitter {
};
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._baseApis.crypto.olmDevice.deviceCurve25519Key,
sender_key: this.baseApis.crypto.olmDevice.deviceCurve25519Key,
ciphertext: {},
};
await olmlib.ensureOlmSessionsForDevices(
this._baseApis.crypto.olmDevice,
this._baseApis,
this.baseApis.crypto.olmDevice,
this.baseApis,
{
[sender]: [
this._baseApis.getStoredDevice(sender, deviceId),
this.baseApis.getStoredDevice(sender, deviceId),
],
},
);
await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this._baseApis.getUserId(),
this._baseApis.deviceId,
this._baseApis.crypto.olmDevice,
this.baseApis.getUserId(),
this.baseApis.deviceId,
this.baseApis.crypto.olmDevice,
sender,
this._baseApis.getStoredDevice(sender, deviceId),
this.baseApis.getStoredDevice(sender, deviceId),
payload,
);
const contentMap = {
@@ -508,26 +522,26 @@ export class SecretStorage extends EventEmitter {
};
logger.info(`Sending ${content.name} secret for ${deviceId}`);
this._baseApis.sendToDevice("m.room.encrypted", contentMap);
this.baseApis.sendToDevice("m.room.encrypted", contentMap);
} else {
logger.info(`Request denied for ${content.name} secret for ${deviceId}`);
}
}
}
_onSecretReceived(event) {
if (event.getSender() !== this._baseApis.getUserId()) {
public onSecretReceived(event: MatrixEvent): void {
if (event.getSender() !== this.baseApis.getUserId()) {
// we shouldn't be receiving secrets from anyone else, so ignore
// because someone could be trying to send us bogus data
return;
}
const content = event.getContent();
logger.log("got secret share for request", content.request_id);
const requestControl = this._requests[content.request_id];
const requestControl = this.requests.get(content.request_id);
if (requestControl) {
// make sure that the device that sent it is one of the devices that
// we requested from
const deviceInfo = this._baseApis.crypto.deviceList.getDeviceByIdentityKey(
const deviceInfo = this.baseApis.crypto.deviceList.getDeviceByIdentityKey(
olmlib.OLM_ALGORITHM,
event.getSenderKey(),
);
@@ -550,12 +564,15 @@ export class SecretStorage extends EventEmitter {
}
}
async _getSecretStorageKey(keys, name) {
if (!this._cryptoCallbacks.getSecretStorageKey) {
private async getSecretStorageKey(
keys: Record<string, ISecretStorageKeyInfo>,
name: string,
): Promise<[string, IDecryptors]> {
if (!this.cryptoCallbacks.getSecretStorageKey) {
throw new Error("No getSecretStorageKey callback supplied");
}
const returned = await this._cryptoCallbacks.getSecretStorageKey({ keys }, name);
const returned = await this.cryptoCallbacks.getSecretStorageKey({ keys }, name);
if (!returned) {
throw new Error("getSecretStorageKey callback returned falsey");
@@ -571,10 +588,10 @@ export class SecretStorage extends EventEmitter {
if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
const decryption = {
encrypt: async function(secret) {
encrypt: async function(secret: string): Promise<IEncryptedPayload> {
return await encryptAES(secret, privateKey, name);
},
decrypt: async function(encInfo) {
decrypt: async function(encInfo: IEncryptedPayload): Promise<string> {
return await decryptAES(encInfo, privateKey, name);
},
};

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);
}
// 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 { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "..";
import { DeviceInfo } from "../deviceinfo";
import { IRoomEncryption } from "../RoomList";
/**
* map of registered encryption algorithm classes. A map from string to {@link
@@ -52,7 +53,7 @@ interface IParams {
olmDevice: OlmDevice;
baseApis: MatrixClient;
roomId: string;
config: object;
config: IRoomEncryption & object;
}
/**

View File

@@ -1670,7 +1670,7 @@ class MegolmDecryption extends DecryptionAlgorithm {
*/
public importRoomKey(session: IMegolmSessionData, opts: any = {}): Promise<void> {
const extraSessionData: any = {};
if (opts.untrusted) {
if (opts.untrusted || session.untrusted) {
extraSessionData.untrusted = true;
}
if (session["org.matrix.msc3061.shared_history"]) {

View File

@@ -16,7 +16,6 @@ limitations under the License.
import { DeviceInfo } from "./deviceinfo";
import { IKeyBackupInfo } from "./keybackup";
import { ISecretStorageKeyInfo } from "../matrix";
// TODO: Merge this with crypto.js once converted
@@ -107,14 +106,32 @@ export interface ICreateSecretStorageOpts {
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
}
export interface ISecretStorageKeyInfo {
name: string;
algorithm: string;
// technically the below are specific to AES keys. If we ever introduce another type,
// we can split into separate interfaces.
iv: string;
mac: string;
passphrase: IPassphraseInfo;
}
export interface ISecretStorageKey {
keyId: string;
keyInfo: ISecretStorageKeyInfo;
}
export interface IPassphraseInfo {
algorithm: "m.pbkdf2";
iterations: number;
salt: string;
bits: number;
}
export interface IAddSecretStorageKeyOpts {
// depends on algorithm
// TODO: Types
name: string;
passphrase: IPassphraseInfo;
key: Uint8Array;
}
export interface IImportOpts {

View File

@@ -29,7 +29,10 @@ import { keyFromPassphrase } from './key_passphrase';
import { sleep } from "../utils";
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { encodeRecoveryKey } from './recoverykey';
import { IKeyBackupInfo } from "./keybackup";
import { encryptAES, decryptAES, calculateKeyCheck } from './aes';
import { getCrypto } from '../utils';
import { ICurve25519AuthData, IAes256AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
import { UnstableValue } from "../NamespacedValue";
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
@@ -75,16 +78,29 @@ interface BackupAlgorithmClass {
prepare(
key: string | Uint8Array | null,
): Promise<[Uint8Array, AuthData]>;
checkBackupVersion(info: IKeyBackupInfo): void;
}
interface BackupAlgorithm {
untrusted: boolean;
encryptSession(data: Record<string, any>): Promise<any>;
decryptSessions(ciphertexts: Record<string, any>): Promise<Record<string, any>[]>;
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<Record<string, any>[]>;
authData: AuthData;
keyMatches(key: ArrayLike<number>): Promise<boolean>;
free(): void;
}
export interface IKeyBackup {
rooms: {
[roomId: string]: {
sessions: {
[sessionId: string]: IKeyBackupSession;
};
};
};
}
/**
* Manages the key backup.
*/
@@ -102,6 +118,24 @@ export class BackupManager {
return this.backupInfo && this.backupInfo.version;
}
/**
* Performs a quick check to ensure that the backup info looks sane.
*
* Throws an error if a problem is detected.
*
* @param {IKeyBackupInfo} info the key backup info
*/
public static checkBackupVersion(info: IKeyBackupInfo): void {
const Algorithm = algorithmsByName[info.algorithm];
if (!Algorithm) {
throw new Error("Unknown backup algorithm: " + info.algorithm);
}
if (!(typeof info.auth_data === "object")) {
throw new Error("Invalid backup data returned");
}
return Algorithm.checkBackupVersion(info);
}
public static async makeAlgorithm(info: IKeyBackupInfo, getKey: GetKey): Promise<BackupAlgorithm> {
const Algorithm = algorithmsByName[info.algorithm];
if (!Algorithm) {
@@ -250,7 +284,7 @@ export class BackupManager {
/**
* Check if the given backup info is trusted.
*
* @param {object} backupInfo key backup info dict from /room_keys/version
* @param {IKeyBackupInfo} backupInfo key backup info dict from /room_keys/version
* @return {object} {
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
* sigs: [
@@ -271,7 +305,6 @@ export class BackupManager {
!backupInfo ||
!backupInfo.algorithm ||
!backupInfo.auth_data ||
!backupInfo.auth_data.public_key ||
!backupInfo.auth_data.signatures
) {
logger.info("Key backup is absent or missing required data");
@@ -280,7 +313,7 @@ export class BackupManager {
const trustedPubkey = this.baseApis.crypto.sessionStore.getLocalTrustedBackupPubKey();
if (backupInfo.auth_data.public_key === trustedPubkey) {
if ("public_key" in backupInfo.auth_data && backupInfo.auth_data.public_key === trustedPubkey) {
logger.info("Backup public key " + trustedPubkey + " is trusted locally");
ret.trusted_locally = true;
}
@@ -441,11 +474,11 @@ export class BackupManager {
let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
const data = {};
const rooms: IKeyBackup["rooms"] = {};
for (const session of sessions) {
const roomId = session.sessionData.room_id;
if (data[roomId] === undefined) {
data[roomId] = { sessions: {} };
if (rooms[roomId] === undefined) {
rooms[roomId] = { sessions: {} };
}
const sessionData = await this.baseApis.crypto.olmDevice.exportInboundGroupSession(
@@ -464,7 +497,7 @@ export class BackupManager {
);
const verified = this.baseApis.crypto.checkDeviceInfoTrust(userId, device).isVerified();
data[roomId]['sessions'][session.sessionId] = {
rooms[roomId]['sessions'][session.sessionId] = {
first_message_index: sessionData.first_known_index,
forwarded_count: forwardedCount,
is_verified: verified,
@@ -472,10 +505,7 @@ export class BackupManager {
};
}
await this.baseApis.sendKeyBackup(
undefined, undefined, this.backupInfo.version,
{ rooms: data },
);
await this.baseApis.sendKeyBackup(undefined, undefined, this.backupInfo.version, { rooms });
await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions);
remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
@@ -552,7 +582,7 @@ export class Curve25519 implements BackupAlgorithm {
public static algorithmName = "m.megolm_backup.v1.curve25519-aes-sha2";
constructor(
public authData: AuthData,
public authData: ICurve25519AuthData,
private publicKey: any, // FIXME: PkEncryption
private getKey: () => Promise<Uint8Array>,
) {}
@@ -561,12 +591,12 @@ export class Curve25519 implements BackupAlgorithm {
authData: AuthData,
getKey: () => Promise<Uint8Array>,
): Promise<Curve25519> {
if (!authData || !authData.public_key) {
if (!authData || !("public_key" in authData)) {
throw new Error("auth_data missing required information");
}
const publicKey = new global.Olm.PkEncryption();
publicKey.set_recipient_key(authData.public_key);
return new Curve25519(authData, publicKey, getKey);
return new Curve25519(authData as ICurve25519AuthData, publicKey, getKey);
}
public static async prepare(
@@ -574,7 +604,7 @@ export class Curve25519 implements BackupAlgorithm {
): Promise<[Uint8Array, AuthData]> {
const decryption = new global.Olm.PkDecryption();
try {
const authData: Partial<AuthData> = {};
const authData: Partial<ICurve25519AuthData> = {};
if (!key) {
authData.public_key = decryption.generate_key();
} else if (key instanceof Uint8Array) {
@@ -597,6 +627,14 @@ export class Curve25519 implements BackupAlgorithm {
}
}
public static checkBackupVersion(info: IKeyBackupInfo): void {
if (!("public_key" in info.auth_data)) {
throw new Error("Invalid backup data returned");
}
}
public get untrusted() { return true; }
public async encryptSession(data: Record<string, any>): Promise<any> {
const plainText: Record<string, any> = Object.assign({}, data);
delete plainText.session_id;
@@ -605,7 +643,9 @@ export class Curve25519 implements BackupAlgorithm {
return this.publicKey.encrypt(JSON.stringify(plainText));
}
public async decryptSessions(sessions: Record<string, Record<string, any>>): Promise<Record<string, any>[]> {
public async decryptSessions(
sessions: Record<string, IKeyBackupSession>,
): Promise<Record<string, any>[]> {
const privKey = await this.getKey();
const decryption = new global.Olm.PkDecryption();
try {
@@ -654,8 +694,120 @@ export class Curve25519 implements BackupAlgorithm {
}
}
function randomBytes(size: number): Uint8Array {
const crypto: {randomBytes: (n: number) => Uint8Array} | undefined = getCrypto() as any;
if (crypto) {
// nodejs version
return crypto.randomBytes(size);
}
if (window?.crypto) {
// browser version
const buf = new Uint8Array(size);
window.crypto.getRandomValues(buf);
return buf;
}
throw new Error("No usable crypto implementation");
}
const UNSTABLE_MSC3270_NAME = new UnstableValue(null, "org.matrix.msc3270.v1.aes-hmac-sha2");
export class Aes256 implements BackupAlgorithm {
public static algorithmName = UNSTABLE_MSC3270_NAME.name;
constructor(
public readonly authData: IAes256AuthData,
private readonly key: Uint8Array,
) {}
public static async init(
authData: IAes256AuthData,
getKey: () => Promise<Uint8Array>,
): Promise<Aes256> {
if (!authData) {
throw new Error("auth_data missing");
}
const key = await getKey();
if (authData.mac) {
const { mac } = await calculateKeyCheck(key, authData.iv);
if (authData.mac.replace(/=+$/g, '') !== mac.replace(/=+/g, '')) {
throw new Error("Key does not match");
}
}
return new Aes256(authData, key);
}
public static async prepare(
key: string | Uint8Array | null,
): Promise<[Uint8Array, AuthData]> {
let outKey: Uint8Array;
const authData: Partial<IAes256AuthData> = {};
if (!key) {
outKey = randomBytes(32);
} else if (key instanceof Uint8Array) {
outKey = new Uint8Array(key);
} else {
const derivation = await keyFromPassphrase(key);
authData.private_key_salt = derivation.salt;
authData.private_key_iterations = derivation.iterations;
outKey = derivation.key;
}
const { iv, mac } = await calculateKeyCheck(outKey);
authData.iv = iv;
authData.mac = mac;
return [outKey, authData as AuthData];
}
public static checkBackupVersion(info: IKeyBackupInfo): void {
if (!("iv" in info.auth_data && "mac" in info.auth_data)) {
throw new Error("Invalid backup data returned");
}
}
public get untrusted() { return false; }
async encryptSession(data: Record<string, any>): Promise<any> {
const plainText: Record<string, any> = Object.assign({}, data);
delete plainText.session_id;
delete plainText.room_id;
delete plainText.first_known_index;
return await encryptAES(JSON.stringify(plainText), this.key, data.session_id);
}
async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<Record<string, any>[]> {
const keys = [];
for (const [sessionId, sessionData] of Object.entries(sessions)) {
try {
const decrypted = JSON.parse(await decryptAES(sessionData.session_data, this.key, sessionId));
decrypted.session_id = sessionId;
keys.push(decrypted);
} catch (e) {
logger.log("Failed to decrypt megolm session from backup", e, sessionData);
}
}
return keys;
}
async keyMatches(key: Uint8Array): Promise<boolean> {
if (this.authData.mac) {
const { mac } = await calculateKeyCheck(key, this.authData.iv);
return this.authData.mac.replace(/=+$/g, '') === mac.replace(/=+/g, '');
} else {
// if we have no information, we have to assume the key is right
return true;
}
}
public free(): void {
this.key.fill(0);
}
}
export const algorithmsByName: Record<string, BackupAlgorithmClass> = {
[Curve25519.algorithmName]: Curve25519,
[Aes256.algorithmName]: Aes256,
};
export const DefaultAlgorithm: BackupAlgorithmClass = Curve25519;

View File

@@ -19,7 +19,7 @@ import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
import { decryptAES, encryptAES } from './aes';
import anotherjson from "another-json";
import { logger } from '../logger';
import { ISecretStorageKeyInfo } from "../matrix";
import { ISecretStorageKeyInfo } from "./api";
// FIXME: these types should eventually go in a different file
type Signatures = Record<string, Record<string, string>>;
@@ -36,7 +36,7 @@ export interface IDehydratedDeviceKeyInfo {
passphrase?: string;
}
interface DeviceKeys {
export interface IDeviceKeys {
algorithms: Array<string>;
device_id: string; // eslint-disable-line camelcase
user_id: string; // eslint-disable-line camelcase
@@ -44,7 +44,7 @@ interface DeviceKeys {
signatures?: Signatures;
}
export interface OneTimeKey {
export interface IOneTimeKey {
key: string;
fallback?: boolean;
signatures?: Signatures;
@@ -222,7 +222,7 @@ export class DehydrationManager {
// send the keys to the server
const deviceId = dehydrateResult.device_id;
logger.log("Preparing device keys", deviceId);
const deviceKeys: DeviceKeys = {
const deviceKeys: IDeviceKeys = {
algorithms: this.crypto.supportedAlgorithms,
device_id: deviceId,
user_id: this.crypto.userId,
@@ -244,7 +244,7 @@ export class DehydrationManager {
logger.log("Preparing one-time keys");
const oneTimeKeys = {};
for (const [keyId, key] of Object.entries(otks.curve25519)) {
const k: OneTimeKey = { key };
const k: IOneTimeKey = { key };
const signature = account.sign(anotherjson.stringify(k));
k.signatures = {
[this.crypto.userId]: {
@@ -257,7 +257,7 @@ export class DehydrationManager {
logger.log("Preparing fallback keys");
const fallbackKeys = {};
for (const [keyId, key] of Object.entries(fallbacks.curve25519)) {
const k: OneTimeKey = { key, fallback: true };
const k: IOneTimeKey = { key, fallback: true };
const signature = account.sign(anotherjson.stringify(k));
k.signatures = {
[this.crypto.userId]: {

View File

@@ -33,11 +33,18 @@ import { DeviceInfo, IDevice } from "./deviceinfo";
import * as algorithms from "./algorithms";
import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from './CrossSigning';
import { EncryptionSetupBuilder } from "./EncryptionSetup";
import { SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorage } from './SecretStorage';
import {
SECRET_STORAGE_ALGORITHM_V1_AES,
SecretStorage,
SecretStorageKeyTuple,
ISecretRequest,
SecretStorageKeyObject,
} from './SecretStorage';
import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from "./api";
import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from './verification/QRCode';
import { SAS } from './verification/SAS';
import { SAS as SASVerification } from './verification/SAS';
import { keyFromPassphrase } from './key_passphrase';
import { decodeRecoveryKey, encodeRecoveryKey } from './recoverykey';
import { VerificationRequest } from "./verification/request/VerificationRequest";
@@ -45,8 +52,8 @@ import { InRoomChannel, InRoomRequests } from "./verification/request/InRoomChan
import { ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDeviceChannel";
import { IllegalMethod } from "./verification/IllegalMethod";
import { KeySignatureUploadError } from "../errors";
import { decryptAES, encryptAES } from './aes';
import { DehydrationManager } from './dehydration';
import { decryptAES, encryptAES, calculateKeyCheck } from './aes';
import { DehydrationManager, IDeviceKeys, IOneTimeKey } from './dehydration';
import { BackupManager } from "./backup";
import { IStore } from "../store";
import { Room } from "../models/room";
@@ -54,7 +61,7 @@ import { RoomMember } from "../models/room-member";
import { MatrixEvent } from "../models/event";
import { MatrixClient, IKeysUploadResponse, SessionStore, CryptoStore, ISignedKey } from "../client";
import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base";
import type { RoomList } from "./RoomList";
import type { IRoomEncryption, RoomList } from "./RoomList";
import { IRecoveryKey, IEncryptedEventInfo } from "./api";
import { IKeyBackupInfo } from "./keybackup";
import { ISyncStateData } from "../sync";
@@ -63,7 +70,7 @@ const DeviceVerification = DeviceInfo.DeviceVerification;
const defaultVerificationMethods = {
[ReciprocateQRCode.NAME]: ReciprocateQRCode,
[SAS.NAME]: SAS,
[SASVerification.NAME]: SASVerification,
// These two can't be used for actual verification, but we do
// need to be able to define them here for the verification flows
@@ -75,10 +82,13 @@ const defaultVerificationMethods = {
/**
* verification method names
*/
export const verificationMethods = {
RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME,
SAS: SAS.NAME,
};
// legacy export identifier
export enum verificationMethods {
RECIPROCATE_QR_CODE = ReciprocateQRCode.NAME,
SAS = SASVerification.NAME,
}
export type VerificationMethod = verificationMethods;
export function isCryptoAvailable(): boolean {
return Boolean(global.Olm);
@@ -126,6 +136,7 @@ export interface IMegolmSessionData {
session_id: string;
session_key: string;
algorithm: string;
untrusted?: boolean;
}
/* eslint-enable camelcase */
@@ -134,6 +145,10 @@ interface IDeviceVerificationUpgrade {
crossSigningInfo: CrossSigningInfo;
}
export interface ICheckOwnCrossSigningTrustOpts {
allowPrivateKeyRequests?: boolean;
}
/**
* @typedef {Object} module:crypto~OlmSessionResult
* @property {module:crypto/deviceinfo} device device info
@@ -192,7 +207,7 @@ export class Crypto extends EventEmitter {
private readonly supportedAlgorithms: string[];
private readonly outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager;
private readonly toDeviceVerificationRequests: ToDeviceRequests;
private readonly inRoomVerificationRequests: InRoomRequests;
public readonly inRoomVerificationRequests: InRoomRequests;
private trustCrossSignedDevices = true;
// the last time we did a check for the number of one-time-keys on the server.
@@ -359,7 +374,8 @@ export class Crypto extends EventEmitter {
const cacheCallbacks = createCryptoStoreCacheCallbacks(cryptoStore, this.olmDevice);
this.crossSigningInfo = new CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks);
this.secretStorage = new SecretStorage(baseApis, cryptoCallbacks);
// Yes, we pass the client twice here: see SecretStorage
this.secretStorage = new SecretStorage(baseApis, cryptoCallbacks, baseApis);
this.dehydrationManager = new DehydrationManager(this);
// Assuming no app-supplied callback, default to getting from SSSS.
@@ -789,7 +805,7 @@ export class Crypto extends EventEmitter {
if (key) {
const privateKey = key[1];
builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey);
const { iv, mac } = await SecretStorage._calculateKeyCheck(privateKey);
const { iv, mac } = await calculateKeyCheck(privateKey);
keyInfo.iv = iv;
keyInfo.mac = mac;
@@ -959,6 +975,20 @@ export class Crypto extends EventEmitter {
fixedBackupKey || sessionBackupKey,
));
await builder.addSessionBackupPrivateKeyToCache(decodedBackupKey);
} else if (this.backupManager.getKeyBackupEnabled()) {
// key backup is enabled but we don't have a session backup key in SSSS: see if we have one in
// the cache or the user can provide one, and if so, write it to SSSS
const backupKey = await this.getSessionBackupPrivateKey() || await getKeyBackupPassphrase();
if (!backupKey) {
// This will require user intervention to recover from since we don't have the key
// backup key anywhere. The user should probably just set up a new key backup and
// the key for the new backup will be stored. If we hit this scenario in the wild
// with any frequency, we should do more than just log an error.
logger.error("Key backup is enabled but couldn't get key backup key!");
return;
}
logger.info("Got session backup key from cache/user that wasn't in SSSS: saving to SSSS");
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey));
}
const operation = builder.buildOperation();
@@ -970,15 +1000,19 @@ export class Crypto extends EventEmitter {
logger.log("Secure Secret Storage ready");
}
public addSecretStorageKey(algorithm: string, opts: any, keyID: string): any { // TODO types
public addSecretStorageKey(
algorithm: string,
opts: IAddSecretStorageKeyOpts,
keyID: string,
): Promise<SecretStorageKeyObject> {
return this.secretStorage.addKey(algorithm, opts, keyID);
}
public hasSecretStorageKey(keyID: string): boolean {
public hasSecretStorageKey(keyID: string): Promise<boolean> {
return this.secretStorage.hasKey(keyID);
}
public getSecretStorageKey(keyID?: string): any { // TODO types
public getSecretStorageKey(keyID?: string): Promise<SecretStorageKeyTuple> {
return this.secretStorage.getKey(keyID);
}
@@ -990,11 +1024,14 @@ export class Crypto extends EventEmitter {
return this.secretStorage.get(name);
}
public isSecretStored(name: string, checkKey?: boolean): any { // TODO types
public isSecretStored(
name: string,
checkKey?: boolean,
): Promise<Record<string, ISecretStorageKeyInfo>> {
return this.secretStorage.isStored(name, checkKey);
}
public requestSecret(name: string, devices: string[]): Promise<any> { // TODO types
public requestSecret(name: string, devices: string[]): ISecretRequest {
if (!devices) {
devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(this.userId));
}
@@ -1009,7 +1046,7 @@ export class Crypto extends EventEmitter {
return this.secretStorage.setDefaultKeyId(k);
}
public checkSecretStorageKey(key: string, info: any): Promise<boolean> { // TODO types
public checkSecretStorageKey(key: Uint8Array, info: ISecretStorageKeyInfo): Promise<boolean> {
return this.secretStorage.checkKey(key, info);
}
@@ -1388,7 +1425,7 @@ export class Crypto extends EventEmitter {
*/
async checkOwnCrossSigningTrust({
allowPrivateKeyRequests = false,
} = {}) {
}: ICheckOwnCrossSigningTrustOpts = {}): Promise<void> {
const userId = this.userId;
// Before proceeding, ensure our cross-signing public keys have been
@@ -1742,7 +1779,7 @@ export class Crypto extends EventEmitter {
return this.signObject(deviceKeys).then(() => {
return this.baseApis.uploadKeysRequest({
device_keys: deviceKeys,
device_keys: deviceKeys as Required<IDeviceKeys>,
});
});
}
@@ -1874,9 +1911,9 @@ export class Crypto extends EventEmitter {
private async uploadOneTimeKeys() {
const promises = [];
const fallbackJson = {};
const fallbackJson: Record<string, IOneTimeKey> = {};
if (this.getNeedsNewFallback()) {
const fallbackKeys = await this.olmDevice.getFallbackKey();
const fallbackKeys = await this.olmDevice.getFallbackKey() as Record<string, Record<string, string>>;
for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) {
const k = { key, fallback: true };
fallbackJson["signed_curve25519:" + keyId] = k;
@@ -2222,7 +2259,7 @@ export class Crypto extends EventEmitter {
public async legacyDeviceVerification(
userId: string,
deviceId: string,
method: string,
method: VerificationMethod,
): VerificationRequest {
const transactionId = ToDeviceChannel.makeTransactionId();
const channel = new ToDeviceChannel(
@@ -2435,7 +2472,7 @@ export class Crypto extends EventEmitter {
*/
public async setRoomEncryption(
roomId: string,
config: any, // TODO types
config: IRoomEncryption,
inhibitDeviceQuery?: boolean,
): Promise<void> {
// ignore crypto events with no algorithm defined
@@ -2492,8 +2529,8 @@ export class Crypto extends EventEmitter {
crypto: this,
olmDevice: this.olmDevice,
baseApis: this.baseApis,
roomId: roomId,
config: config,
roomId,
config,
});
this.roomEncryptors[roomId] = alg;
@@ -2848,7 +2885,7 @@ export class Crypto extends EventEmitter {
*/
public async onCryptoEvent(event: MatrixEvent): Promise<void> {
const roomId = event.getRoomId();
const content = event.getContent();
const content = event.getContent<IRoomEncryption>();
try {
// inhibit the device list refresh for now - it will happen once we've
@@ -2996,9 +3033,9 @@ export class Crypto extends EventEmitter {
} else if (event.getType() == "m.room_key_request") {
this.onRoomKeyRequestEvent(event);
} else if (event.getType() === "m.secret.request") {
this.secretStorage._onRequestReceived(event);
this.secretStorage.onRequestReceived(event);
} else if (event.getType() === "m.secret.send") {
this.secretStorage._onSecretReceived(event);
this.secretStorage.onSecretReceived(event);
} else if (event.getType() === "org.matrix.room_key.withheld") {
this.onRoomKeyWithheldEvent(event);
} else if (event.getContent().transaction_id) {

View File

@@ -22,8 +22,8 @@ const DEFAULT_BITSIZE = 256;
/* eslint-disable camelcase */
interface IAuthData {
private_key_salt: string;
private_key_iterations: number;
private_key_salt?: string;
private_key_iterations?: number;
private_key_bits?: number;
}
/* eslint-enable camelcase */

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ISignatures } from "../@types/signed";
import { ISigned } from "../@types/signed";
export interface IKeyBackupSession {
first_message_index: number; // eslint-disable-line camelcase
@@ -24,6 +24,7 @@ export interface IKeyBackupSession {
ciphertext: string;
ephemeral: string;
mac: string;
iv: string;
};
}
@@ -32,15 +33,23 @@ export interface IKeyBackupRoomSessions {
}
/* eslint-disable camelcase */
export interface ICurve25519AuthData {
public_key: string;
private_key_salt?: string;
private_key_iterations?: number;
private_key_bits?: number;
}
export interface IAes256AuthData {
iv: string;
mac: string;
private_key_salt?: string;
private_key_iterations?: number;
}
export interface IKeyBackupInfo {
algorithm: string;
auth_data: {
public_key: string;
signatures: ISignatures;
private_key_salt: string;
private_key_iterations: number;
private_key_bits?: number;
};
auth_data: ISigned & (ICurve25519AuthData | IAes256AuthData);
count?: number;
etag?: string;
version?: string; // number contained within

View File

@@ -28,7 +28,7 @@ import OlmDevice from "./OlmDevice";
import { DeviceInfo } from "./deviceinfo";
import { logger } from '../logger';
import * as utils from "../utils";
import { OneTimeKey } from "./dehydration";
import { IOneTimeKey } from "./dehydration";
import { MatrixClient } from "../client";
enum Algorithm {
@@ -407,7 +407,7 @@ export async function ensureOlmSessionsForDevices(
async function _verifyKeyAndStartSession(
olmDevice: OlmDevice,
oneTimeKey: OneTimeKey,
oneTimeKey: IOneTimeKey,
userId: string,
deviceInfo: DeviceInfo,
): Promise<string> {
@@ -465,7 +465,7 @@ export interface IObject {
*/
export async function verifySignature(
olmDevice: OlmDevice,
obj: OneTimeKey | IObject,
obj: IOneTimeKey | IObject,
signingUserId: string,
signingDeviceId: string,
signingKey: string,

View File

@@ -15,9 +15,9 @@ limitations under the License.
*/
import { MatrixClient } from "./client";
import { MatrixEvent } from "./models/event";
import { IEvent, MatrixEvent } from "./models/event";
export type EventMapper = (obj: any) => MatrixEvent;
export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
export interface MapperOpts {
preventReEmit?: boolean;
@@ -28,7 +28,7 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
const preventReEmit = Boolean(options.preventReEmit);
const decrypt = options.decrypt !== false;
function mapper(plainOldJsObject) {
function mapper(plainOldJsObject: Partial<IEvent>) {
const event = new MatrixEvent(plainOldJsObject);
if (event.isEncrypted()) {
if (!preventReEmit) {

View File

@@ -39,7 +39,7 @@ function setProp(obj: object, keyNesting: string, val: any) {
}
/* eslint-disable camelcase */
interface IFilterDefinition {
export interface IFilterDefinition {
event_fields?: string[];
event_format?: "client" | "federation";
presence?: IFilterComponent;
@@ -47,7 +47,7 @@ interface IFilterDefinition {
room?: IRoomFilter;
}
interface IRoomEventFilter extends IFilterComponent {
export interface IRoomEventFilter extends IFilterComponent {
lazy_load_members?: boolean;
include_redundant_members?: boolean;
}
@@ -86,7 +86,7 @@ export class Filter {
* @param {Object} jsonObj
* @return {Filter}
*/
static fromJson(userId: string, filterId: string, jsonObj: IFilterDefinition): Filter {
public static fromJson(userId: string, filterId: string, jsonObj: IFilterDefinition): Filter {
const filter = new Filter(userId, filterId);
filter.setDefinition(jsonObj);
return filter;

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
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
@@ -393,7 +393,7 @@ MatrixHttpApi.prototype = {
accessToken,
) {
if (!this.opts.idBaseUrl) {
throw new Error("No Identity Server base URL set");
throw new Error("No identity server base URL set");
}
const fullUri = this.opts.idBaseUrl + prefix + path;

View File

@@ -264,9 +264,7 @@ export class InteractiveAuth {
client_secret: this.clientSecret,
};
if (await this.matrixClient.doesServerRequireIdServerParam()) {
const idServerParsedUrl = url.parse(
this.matrixClient.getIdentityServerUrl(),
);
const idServerParsedUrl = new URL(this.matrixClient.getIdentityServerUrl());
creds.id_server = idServerParsedUrl.host;
}
authDict = {
@@ -294,7 +292,7 @@ export class InteractiveAuth {
/**
* get the client secret used for validation sessions
* with the ID server.
* with the identity server.
*
* @return {string} client secret
*/

View File

@@ -20,6 +20,7 @@ import { MatrixScheduler } from "./scheduler";
import { MatrixClient } from "./client";
import { ICreateClientOpts } from "./client";
import { DeviceTrustLevel } from "./crypto/CrossSigning";
import { ISecretStorageKeyInfo } from "./crypto/api";
export * from "./client";
export * from "./http-api";
@@ -122,17 +123,6 @@ export interface ICryptoCallbacks {
getBackupKey?: () => Promise<Uint8Array>;
}
// TODO: Move this to `SecretStorage` once converted
export interface ISecretStorageKeyInfo {
passphrase?: {
algorithm: "m.pbkdf2";
iterations: number;
salt: string;
};
iv?: string;
mac?: string;
}
/**
* Construct a Matrix Client. Similar to {@link module:client.MatrixClient}
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.

View File

@@ -70,8 +70,8 @@ export class MSC3089Branch {
* @param {string} name The new name for this file.
* @returns {Promise<void>} Resolves when complete.
*/
public setName(name: string): Promise<void> {
return this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, {
public async setName(name: string): Promise<void> {
await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, {
...this.indexEvent.getContent(),
name: name,
}, this.id);

View File

@@ -111,8 +111,8 @@ export class MSC3089TreeSpace {
* @param {string} name The new name for the space.
* @returns {Promise<void>} Resolves when complete.
*/
public setName(name: string): Promise<void> {
return this.client.sendStateEvent(this.roomId, EventType.RoomName, { name }, "");
public async setName(name: string): Promise<void> {
await this.client.sendStateEvent(this.roomId, EventType.RoomName, { name }, "");
}
/**
@@ -190,7 +190,7 @@ export class MSC3089TreeSpace {
}
pls['users'] = users;
return this.client.sendStateEvent(this.roomId, EventType.RoomPowerLevels, pls, "");
await this.client.sendStateEvent(this.roomId, EventType.RoomPowerLevels, pls, "");
}
/**

View File

@@ -22,6 +22,7 @@ import { RoomState } from "./room-state";
import { EventTimelineSet } from "./event-timeline-set";
import { MatrixEvent } from "./event";
import { Filter } from "../filter";
import { EventType } from "../@types/event";
export enum Direction {
Backward = "b",
@@ -49,15 +50,16 @@ export class EventTimeline {
* @param {boolean} toStartOfTimeline if true the event's forwardLooking flag is set false
*/
static setEventMetadata(event: MatrixEvent, stateContext: RoomState, toStartOfTimeline: boolean): void {
// set sender and target properties
event.sender = stateContext.getSentinelMember(
event.getSender(),
);
if (event.getType() === "m.room.member") {
event.target = stateContext.getSentinelMember(
event.getStateKey(),
);
// We always check if the event doesn't already have the property. We do
// this to avoid overriding non-sentinel members by sentinel ones when
// adding the event to a filtered timeline
if (!event.sender) {
event.sender = stateContext.getSentinelMember(event.getSender());
}
if (!event.target && event.getType() === EventType.RoomMember) {
event.target = stateContext.getSentinelMember(event.getStateKey());
}
if (event.isState()) {
// room state has no concept of 'old' or 'current', but we want the
// room state to regress back to previous values if toStartOfTimeline
@@ -345,15 +347,16 @@ export class EventTimeline {
*/
public addEvent(event: MatrixEvent, atStart: boolean): void {
const stateContext = atStart ? this.startState : this.endState;
// only call setEventMetadata on the unfiltered timelineSets
const timelineSet = this.getTimelineSet();
if (timelineSet.room &&
timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
if (timelineSet.room) {
EventTimeline.setEventMetadata(event, stateContext, atStart);
// modify state
if (event.isState()) {
// modify state but only on unfiltered timelineSets
if (
event.isState() &&
timelineSet.room.getUnfilteredTimelineSet() === timelineSet
) {
stateContext.setStateEvents([event]);
// it is possible that the act of setting the state event means we
// can set more metadata (specifically sender/target props), so try

View File

@@ -263,6 +263,16 @@ export class MatrixEvent extends EventEmitter {
this.localTimestamp = Date.now() - this.getAge();
}
/**
* Gets the event as though it would appear unencrypted. If the event is already not
* encrypted, it is simply returned as-is.
* @returns {IEvent} The event in wire format.
*/
public getEffectiveEvent(): IEvent {
// clearEvent doesn't have all the fields, so we'll copy what we can from this.event
return Object.assign({}, this.event, this.clearEvent) as IEvent;
}
/**
* Get the event_id for this event.
* @return {string} The event ID, e.g. <code>$143350589368169JsLZx:localhost
@@ -1231,20 +1241,7 @@ export class MatrixEvent extends EventEmitter {
* @return {Object}
*/
public toJSON(): object {
const event: any = {
type: this.getType(),
sender: this.getSender(),
content: this.getContent(),
event_id: this.getId(),
origin_server_ts: this.getTs(),
unsigned: this.getUnsigned(),
room_id: this.getRoomId(),
};
// if this is a redaction then attach the redacts key
if (this.isRedaction()) {
event.redacts = this.event.redacts;
}
const event = this.getEffectiveEvent();
if (!this.isEncrypted()) {
return event;

View File

@@ -25,13 +25,13 @@ import { EventTimeline } from "./event-timeline";
import { getHttpUriForMxc } from "../content-repo";
import * as utils from "../utils";
import { normalize } from "../utils";
import { EventStatus, MatrixEvent } from "./event";
import { EventStatus, IEvent, MatrixEvent } from "./event";
import { RoomMember } from "./room-member";
import { IRoomSummary, RoomSummary } from "./room-summary";
import { logger } from '../logger';
import { ReEmitter } from '../ReEmitter';
import { EventType, RoomCreateTypeField, RoomType } from "../@types/event";
import { IRoomVersionsCapability, MatrixClient, RoomVersionStability } from "../client";
import { EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../@types/event";
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
import { ResizeMethod } from "../@types/partials";
import { Filter } from "../filter";
import { RoomState } from "./room-state";
@@ -64,7 +64,7 @@ function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: stri
interface IOpts {
storageToken?: string;
pendingEventOrdering?: "chronological" | "detached";
pendingEventOrdering?: PendingEventOrdering;
timelineSupport?: boolean;
unstableClientRelationAggregation?: boolean;
lazyLoadMembers?: boolean;
@@ -218,7 +218,7 @@ export class Room extends EventEmitter {
this.setMaxListeners(100);
this.reEmitter = new ReEmitter(this);
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
opts.pendingEventOrdering = opts.pendingEventOrdering || PendingEventOrdering.Chronological;
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
throw new Error(
"opts.pendingEventOrdering MUST be either 'chronological' or " +
@@ -649,7 +649,7 @@ export class Room extends EventEmitter {
}
}
private async loadMembersFromServer(): Promise<object[]> {
private async loadMembersFromServer(): Promise<IEvent[]> {
const lastSyncToken = this.client.store.getSyncToken();
const queryString = utils.encodeParams({
not_membership: "leave",
@@ -665,8 +665,7 @@ export class Room extends EventEmitter {
private async loadMembers(): Promise<{ memberEvents: MatrixEvent[], fromServer: boolean }> {
// were the members loaded from the server?
let fromServer = false;
let rawMembersEvents =
await this.client.store.getOutOfBandMembers(this.roomId);
let rawMembersEvents = await this.client.store.getOutOfBandMembers(this.roomId);
if (rawMembersEvents === null) {
fromServer = true;
rawMembersEvents = await this.loadMembersFromServer();
@@ -713,7 +712,7 @@ export class Room extends EventEmitter {
if (fromServer) {
const oobMembers = this.currentState.getMembers()
.filter((m) => m.isOutOfBand())
.map((m) => m.events.member.event);
.map((m) => m.events.member.event as IEvent);
logger.log(`LL: telling store to write ${oobMembers.length}`
+ ` members for room ${this.roomId}`);
const store = this.client.store;
@@ -2037,24 +2036,45 @@ export class Room extends EventEmitter {
const joinedMemberCount = this.currentState.getJoinedMemberCount();
const invitedMemberCount = this.currentState.getInvitedMemberCount();
// -1 because these numbers include the syncing user
const inviteJoinCount = joinedMemberCount + invitedMemberCount - 1;
let inviteJoinCount = joinedMemberCount + invitedMemberCount - 1;
// get service members (e.g. helper bots) for exclusion
let excludedUserIds: string[] = [];
const mFunctionalMembers = this.currentState.getStateEvents(UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, "");
if (Array.isArray(mFunctionalMembers?.getContent().service_members)) {
excludedUserIds = mFunctionalMembers.getContent().service_members;
}
// get members that are NOT ourselves and are actually in the room.
let otherNames = null;
if (this.summaryHeroes) {
// if we have a summary, the member state events
// should be in the room state
otherNames = this.summaryHeroes.map((userId) => {
otherNames = [];
this.summaryHeroes.forEach((userId) => {
// filter service members
if (excludedUserIds.includes(userId)) {
inviteJoinCount--;
return;
}
const member = this.getMember(userId);
return member ? member.name : userId;
otherNames.push(member ? member.name : userId);
});
} else {
let otherMembers = this.currentState.getMembers().filter((m) => {
return m.userId !== userId &&
(m.membership === "invite" || m.membership === "join");
});
otherMembers = otherMembers.filter(({ userId }) => {
// filter service members
if (excludedUserIds.includes(userId)) {
inviteJoinCount--;
return false;
}
return true;
});
// make sure members have stable order
otherMembers.sort((a, b) => a.userId.localeCompare(b.userId));
otherMembers.sort((a, b) => utils.compare(a.userId, b.userId));
// only 5 first members, immitate summaryHeroes
otherMembers = otherMembers.slice(0, 5);
otherNames = otherMembers.map((m) => m.name);
@@ -2065,7 +2085,7 @@ export class Room extends EventEmitter {
}
const myMembership = this.getMyMembership();
// if I have created a room and invited people throuh
// if I have created a room and invited people through
// 3rd party invites
if (myMembership == 'join') {
const thirdPartyInvites =

View File

@@ -20,27 +20,9 @@ limitations under the License.
import { EventContext } from "./event-context";
import { EventMapper } from "../event-mapper";
import { IResultContext, ISearchResult } from "../@types/search";
import { IRoomEvent } from "../sync-accumulator";
/* eslint-disable camelcase */
interface IContext {
events_before?: IRoomEvent[];
events_after?: IRoomEvent[];
start?: string;
end?: string;
profile_info?: Record<string, {
displayname: string;
avatar_url: string;
}>;
}
/* eslint-enable camelcase */
interface ISearchResult {
rank: number;
result: IRoomEvent;
context: IContext;
}
export class SearchResult {
/**
* Create a SearchResponse from the response to /search
@@ -49,8 +31,9 @@ export class SearchResult {
* @param {function} eventMapper
* @return {SearchResult}
*/
public static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult {
const jsonContext: IContext = jsonObj.context || {};
const jsonContext = jsonObj.context || {} as IResultContext;
const eventsBefore = jsonContext.events_before || [];
const eventsAfter = jsonContext.events_after || [];

View File

@@ -208,7 +208,7 @@ export class User extends EventEmitter {
* @param {MatrixEvent} event The <code>im.vector.user_status</code> event.
* @fires module:client~MatrixClient#event:"User.unstable_statusMessage"
*/
// eslint-disable-next-line camelcase
// eslint-disable-next-line
public unstable_updateStatusMessage(event: MatrixEvent): void {
if (!event.getContent()) this.unstable_statusMessage = "";
else this.unstable_statusMessage = event.getContent()["status"];

View File

@@ -15,6 +15,6 @@ limitations under the License.
*/
export enum SERVICE_TYPES {
IS = 'SERVICE_TYPE_IS', // An Identity Service
IM = 'SERVICE_TYPE_IM', // An Integration Manager
IS = 'SERVICE_TYPE_IS', // An identity server
IM = 'SERVICE_TYPE_IM', // An integration manager
}

View File

@@ -18,10 +18,11 @@ import { EventType } from "../@types/event";
import { Group } from "../models/group";
import { Room } from "../models/room";
import { User } from "../models/user";
import { MatrixEvent } from "../models/event";
import { IEvent, MatrixEvent } from "../models/event";
import { Filter } from "../filter";
import { RoomSummary } from "../models/room-summary";
import { IMinimalEvent, IGroups, IRooms } from "../sync-accumulator";
import { IMinimalEvent, IGroups, IRooms, ISyncResponse } from "../sync-accumulator";
import { IStartClientOpts } from "../client";
export interface ISavedSync {
nextBatch: string;
@@ -35,6 +36,8 @@ export interface ISavedSync {
* @constructor
*/
export interface IStore {
readonly accountData: Record<string, MatrixEvent>; // type : content
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
isNewlyCreated(): Promise<boolean>;
@@ -182,7 +185,7 @@ export interface IStore {
* @param {Object} syncData The sync data
* @return {Promise} An immediately resolved promise.
*/
setSyncData(syncData: object): Promise<void>;
setSyncData(syncData: ISyncResponse): Promise<void>;
/**
* We never want to save because we have nothing to save to.
@@ -194,7 +197,7 @@ export interface IStore {
/**
* Save does nothing as there is no backing data store.
*/
save(force: boolean): void;
save(force?: boolean): void;
/**
* Startup does nothing.
@@ -222,13 +225,13 @@ export interface IStore {
*/
deleteAllData(): Promise<void>;
getOutOfBandMembers(roomId: string): Promise<MatrixEvent[] | null>;
getOutOfBandMembers(roomId: string): Promise<IEvent[] | null>;
setOutOfBandMembers(roomId: string, membershipEvents: MatrixEvent[]): Promise<void>;
setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void>;
clearOutOfBandMembers(roomId: string): Promise<void>;
getClientOptions(): Promise<object>;
getClientOptions(): Promise<IStartClientOpts>;
storeClientOptions(options: object): Promise<void>;
storeClientOptions(options: IStartClientOpts): Promise<void>;
}

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 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,14 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { SyncAccumulator } from "../sync-accumulator";
import { IMinimalEvent, ISyncData, ISyncResponse, SyncAccumulator } from "../sync-accumulator";
import * as utils from "../utils";
import * as IndexedDBHelpers from "../indexeddb-helpers";
import { logger } from '../logger';
import { IEvent, IStartClientOpts } from "..";
import { ISavedSync } from "./index";
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
const VERSION = 3;
function createDatabase(db) {
function createDatabase(db: IDBDatabase): void {
// Make user store, clobber based on user ID. (userId property of User objects)
db.createObjectStore("users", { keyPath: ["userId"] });
@@ -35,7 +36,7 @@ function createDatabase(db) {
db.createObjectStore("sync", { keyPath: ["clobber"] });
}
function upgradeSchemaV2(db) {
function upgradeSchemaV2(db: IDBDatabase): void {
const oobMembersStore = db.createObjectStore(
"oob_membership_events", {
keyPath: ["room_id", "state_key"],
@@ -43,7 +44,7 @@ function upgradeSchemaV2(db) {
oobMembersStore.createIndex("room", "room_id");
}
function upgradeSchemaV3(db) {
function upgradeSchemaV3(db: IDBDatabase): void {
db.createObjectStore("client_options",
{ keyPath: ["clobber"] });
}
@@ -58,16 +59,20 @@ function upgradeSchemaV3(db) {
* @return {Promise<T[]>} Resolves to an array of whatever you returned from
* resultMapper.
*/
function selectQuery(store, keyRange, resultMapper) {
function selectQuery<T>(
store: IDBObjectStore,
keyRange: IDBKeyRange | IDBValidKey | undefined,
resultMapper: (cursor: IDBCursorWithValue) => T,
): Promise<T[]> {
const query = store.openCursor(keyRange);
return new Promise((resolve, reject) => {
const results = [];
query.onerror = (event) => {
reject(new Error("Query failed: " + event.target.errorCode));
query.onerror = () => {
reject(new Error("Query failed: " + query.error));
};
// collect results
query.onsuccess = (event) => {
const cursor = event.target.result;
query.onsuccess = () => {
const cursor = query.result;
if (!cursor) {
resolve(results);
return; // end of results
@@ -78,88 +83,84 @@ function selectQuery(store, keyRange, resultMapper) {
});
}
function txnAsPromise(txn) {
function txnAsPromise(txn: IDBTransaction): Promise<Event> {
return new Promise((resolve, reject) => {
txn.oncomplete = function(event) {
resolve(event);
};
txn.onerror = function(event) {
reject(event.target.error);
txn.onerror = function() {
reject(txn.error);
};
});
}
function reqAsEventPromise(req) {
function reqAsEventPromise(req: IDBRequest): Promise<Event> {
return new Promise((resolve, reject) => {
req.onsuccess = function(event) {
resolve(event);
};
req.onerror = function(event) {
reject(event.target.error);
req.onerror = function() {
reject(req.error);
};
});
}
function reqAsPromise(req) {
function reqAsPromise(req: IDBRequest): Promise<IDBRequest> {
return new Promise((resolve, reject) => {
req.onsuccess = () => resolve(req);
req.onerror = (err) => reject(err);
});
}
function reqAsCursorPromise(req) {
return reqAsEventPromise(req).then((event) => event.target.result);
function reqAsCursorPromise(req: IDBRequest<IDBCursor | null>): Promise<IDBCursor> {
return reqAsEventPromise(req).then((event) => req.result);
}
/**
export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
public static exists(indexedDB: IDBFactory, dbName: string): boolean {
dbName = "matrix-js-sdk:" + (dbName || "default");
return IndexedDBHelpers.exists(indexedDB, dbName);
}
private readonly dbName: string;
private readonly syncAccumulator: SyncAccumulator;
private db: IDBDatabase = null;
private disconnected = true;
private _isNewlyCreated = false;
/**
* Does the actual reading from and writing to the indexeddb
*
* Construct a new Indexed Database store backend. This requires a call to
* <code>connect()</code> before this store can be used.
* @constructor
* @param {Object} indexedDBInterface The Indexed DB interface e.g
* @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.
*/
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;
}
constructor(private readonly indexedDB: IDBFactory, dbName: string) {
this.dbName = "matrix-js-sdk:" + (dbName || "default");
this.syncAccumulator = new SyncAccumulator();
}
LocalIndexedDBStoreBackend.exists = function(indexedDB, dbName) {
dbName = "matrix-js-sdk:" + (dbName || "default");
return IndexedDBHelpers.exists(indexedDB, dbName);
};
LocalIndexedDBStoreBackend.prototype = {
/**
* Attempt to connect to the database. This can fail if the user does not
* grant permission.
* @return {Promise} Resolves if successfully connected.
*/
connect: function() {
if (!this._disconnected) {
logger.log(
`LocalIndexedDBStoreBackend.connect: already connected or connecting`,
);
public connect(): Promise<void> {
if (!this.disconnected) {
logger.log(`LocalIndexedDBStoreBackend.connect: already connected or connecting`);
return Promise.resolve();
}
this._disconnected = false;
this.disconnected = false;
logger.log(
`LocalIndexedDBStoreBackend.connect: connecting...`,
);
const req = this.indexedDB.open(this._dbName, VERSION);
logger.log(`LocalIndexedDBStoreBackend.connect: connecting...`);
const req = this.indexedDB.open(this.dbName, VERSION);
req.onupgradeneeded = (ev) => {
const db = ev.target.result;
const db = req.result;
const oldVersion = ev.oldVersion;
logger.log(
`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`,
@@ -178,19 +179,13 @@ LocalIndexedDBStoreBackend.prototype = {
};
req.onblocked = () => {
logger.log(
`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`,
);
logger.log(`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`);
};
logger.log(
`LocalIndexedDBStoreBackend.connect: awaiting connection...`,
);
return reqAsEventPromise(req).then((ev) => {
logger.log(
`LocalIndexedDBStoreBackend.connect: connected`,
);
this.db = ev.target.result;
logger.log(`LocalIndexedDBStoreBackend.connect: awaiting connection...`);
return reqAsEventPromise(req).then(() => {
logger.log(`LocalIndexedDBStoreBackend.connect: connected`);
this.db = req.result;
// add a poorly-named listener for when deleteDatabase is called
// so we can close our db connections.
@@ -198,27 +193,26 @@ LocalIndexedDBStoreBackend.prototype = {
this.db.close();
};
return this._init();
return this.init();
});
},
/** @return {bool} whether or not the database was newly created in this session. */
isNewlyCreated: function() {
}
/** @return {boolean} whether or not the database was newly created in this session. */
public isNewlyCreated(): Promise<boolean> {
return Promise.resolve(this._isNewlyCreated);
},
}
/**
* Having connected, load initial data from the database and prepare for use
* @return {Promise} Resolves on success
*/
_init: function() {
private init() {
return Promise.all([
this._loadAccountData(),
this._loadSyncData(),
this.loadAccountData(),
this.loadSyncData(),
]).then(([accountData, syncData]) => {
logger.log(
`LocalIndexedDBStoreBackend: loaded initial data`,
);
this._syncAccumulator.accumulate({
logger.log(`LocalIndexedDBStoreBackend: loaded initial data`);
this.syncAccumulator.accumulate({
next_batch: syncData.nextBatch,
rooms: syncData.roomsData,
groups: syncData.groupsData,
@@ -227,7 +221,7 @@ LocalIndexedDBStoreBackend.prototype = {
},
}, true);
});
},
}
/**
* Returns the out-of-band membership events for this room that
@@ -236,8 +230,8 @@ LocalIndexedDBStoreBackend.prototype = {
* @returns {Promise<event[]>} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
getOutOfBandMembers: function(roomId) {
return new Promise((resolve, reject) =>{
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
return new Promise<IEvent[] | null>((resolve, reject) =>{
const tx = this.db.transaction(["oob_membership_events"], "readonly");
const store = tx.objectStore("oob_membership_events");
const roomIndex = store.index("room");
@@ -252,8 +246,8 @@ LocalIndexedDBStoreBackend.prototype = {
// were all known already
let oobWritten = false;
request.onsuccess = (event) => {
const cursor = event.target.result;
request.onsuccess = () => {
const cursor = request.result;
if (!cursor) {
// Unknown room
if (!membershipEvents.length && !oobWritten) {
@@ -273,11 +267,10 @@ LocalIndexedDBStoreBackend.prototype = {
reject(err);
};
}).then((events) => {
logger.log(`LL: got ${events && events.length}` +
` membershipEvents from storage for room ${roomId} ...`);
logger.log(`LL: got ${events && events.length} membershipEvents from storage for room ${roomId} ...`);
return events;
});
},
}
/**
* Stores the out-of-band membership events for this room. Note that
@@ -286,7 +279,7 @@ LocalIndexedDBStoreBackend.prototype = {
* @param {string} roomId
* @param {event[]} membershipEvents the membership events to store
*/
setOutOfBandMembers: async function(roomId, membershipEvents) {
public async setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
logger.log(`LL: backend about to store ${membershipEvents.length}` +
` members for ${roomId}`);
const tx = this.db.transaction(["oob_membership_events"], "readwrite");
@@ -307,9 +300,9 @@ LocalIndexedDBStoreBackend.prototype = {
store.put(markerObject);
await txnAsPromise(tx);
logger.log(`LL: backend done storing for ${roomId}!`);
},
}
clearOutOfBandMembers: async function(roomId) {
public async clearOutOfBandMembers(roomId: string): Promise<void> {
// the approach to delete all members for a room
// is to get the min and max state key from the index
// for that room, and then delete between those
@@ -341,45 +334,39 @@ LocalIndexedDBStoreBackend.prototype = {
[roomId, maxStateKey],
);
logger.log(`LL: Deleting all users + marker in storage for ` +
`room ${roomId}, with key range:`,
logger.log(`LL: Deleting all users + marker in storage for room ${roomId}, with key range:`,
[roomId, minStateKey], [roomId, maxStateKey]);
await reqAsPromise(writeStore.delete(membersKeyRange));
},
}
/**
* Clear the entire database. This should be used when logging out of a client
* to prevent mixing data between accounts.
* @return {Promise} Resolved when the database is cleared.
*/
clearDatabase: function() {
return new Promise((resolve, reject) => {
logger.log(`Removing indexeddb instance: ${this._dbName}`);
const req = this.indexedDB.deleteDatabase(this._dbName);
public clearDatabase(): Promise<void> {
return new Promise((resolve) => {
logger.log(`Removing indexeddb instance: ${this.dbName}`);
const req = this.indexedDB.deleteDatabase(this.dbName);
req.onblocked = () => {
logger.log(
`can't yet delete indexeddb ${this._dbName}` +
` because it is open elsewhere`,
);
logger.log(`can't yet delete indexeddb ${this.dbName} because it is open elsewhere`);
};
req.onerror = (ev) => {
req.onerror = () => {
// in firefox, with indexedDB disabled, this fails with a
// DOMError. We treat this as non-fatal, so that we can still
// use the app.
logger.warn(
`unable to delete js-sdk store indexeddb: ${ev.target.error}`,
);
logger.warn(`unable to delete js-sdk store indexeddb: ${req.error}`);
resolve();
};
req.onsuccess = () => {
logger.log(`Removed indexeddb instance: ${this._dbName}`);
logger.log(`Removed indexeddb instance: ${this.dbName}`);
resolve();
};
});
},
}
/**
* @param {boolean=} copy If false, the data returned is from internal
@@ -390,10 +377,8 @@ LocalIndexedDBStoreBackend.prototype = {
* client state to where it was at the last save, or null if there
* is no saved sync data.
*/
getSavedSync: function(copy) {
if (copy === undefined) copy = true;
const data = this._syncAccumulator.getJSON();
public getSavedSync(copy = true): Promise<ISavedSync> {
const data = this.syncAccumulator.getJSON();
if (!data.nextBatch) return Promise.resolve(null);
if (copy) {
// We must deep copy the stored data so that the /sync processing code doesn't
@@ -402,29 +387,27 @@ LocalIndexedDBStoreBackend.prototype = {
} else {
return Promise.resolve(data);
}
},
}
getNextBatchToken: function() {
return Promise.resolve(this._syncAccumulator.getNextBatchToken());
},
public getNextBatchToken(): Promise<string> {
return Promise.resolve(this.syncAccumulator.getNextBatchToken());
}
setSyncData: function(syncData) {
public setSyncData(syncData: ISyncResponse): Promise<void> {
return Promise.resolve().then(() => {
this._syncAccumulator.accumulate(syncData);
this.syncAccumulator.accumulate(syncData);
});
},
}
syncToDatabase: function(userTuples) {
const syncData = this._syncAccumulator.getJSON(true);
public async syncToDatabase(userTuples: UserTuple[]): Promise<void> {
const syncData = this.syncAccumulator.getJSON(true);
return Promise.all([
this._persistUserPresenceEvents(userTuples),
this._persistAccountData(syncData.accountData),
this._persistSyncData(
syncData.nextBatch, syncData.roomsData, syncData.groupsData,
),
await Promise.all([
this.persistUserPresenceEvents(userTuples),
this.persistAccountData(syncData.accountData),
this.persistSyncData(syncData.nextBatch, syncData.roomsData, syncData.groupsData),
]);
},
}
/**
* Persist rooms /sync data along with the next batch token.
@@ -433,20 +416,24 @@ LocalIndexedDBStoreBackend.prototype = {
* @param {Object} groupsData The 'groups' /sync data from a SyncAccumulator
* @return {Promise} Resolves if the data was persisted.
*/
_persistSyncData: function(nextBatch, roomsData, groupsData) {
private persistSyncData(
nextBatch: string,
roomsData: ISyncResponse["rooms"],
groupsData: ISyncResponse["groups"],
): Promise<void> {
logger.log("Persisting sync data up to", nextBatch);
return utils.promiseTry(() => {
return utils.promiseTry<void>(() => {
const txn = this.db.transaction(["sync"], "readwrite");
const store = txn.objectStore("sync");
store.put({
clobber: "-", // constant key so will always clobber
nextBatch: nextBatch,
roomsData: roomsData,
groupsData: groupsData,
nextBatch,
roomsData,
groupsData,
}); // put == UPSERT
return txnAsPromise(txn);
return txnAsPromise(txn).then();
});
},
}
/**
* Persist a list of account data events. Events with the same 'type' will
@@ -454,16 +441,16 @@ LocalIndexedDBStoreBackend.prototype = {
* @param {Object[]} accountData An array of raw user-scoped account data events
* @return {Promise} Resolves if the events were persisted.
*/
_persistAccountData: function(accountData) {
return utils.promiseTry(() => {
private persistAccountData(accountData: IMinimalEvent[]): Promise<void> {
return utils.promiseTry<void>(() => {
const txn = this.db.transaction(["accountData"], "readwrite");
const store = txn.objectStore("accountData");
for (let i = 0; i < accountData.length; i++) {
store.put(accountData[i]); // put == UPSERT
}
return txnAsPromise(txn);
return txnAsPromise(txn).then();
});
},
}
/**
* Persist a list of [user id, presence event] they are for.
@@ -473,8 +460,8 @@ LocalIndexedDBStoreBackend.prototype = {
* @param {Object[]} tuples An array of [userid, event] tuples
* @return {Promise} Resolves if the users were persisted.
*/
_persistUserPresenceEvents: function(tuples) {
return utils.promiseTry(() => {
private persistUserPresenceEvents(tuples: UserTuple[]): Promise<void> {
return utils.promiseTry<void>(() => {
const txn = this.db.transaction(["users"], "readwrite");
const store = txn.objectStore("users");
for (const tuple of tuples) {
@@ -483,9 +470,9 @@ LocalIndexedDBStoreBackend.prototype = {
event: tuple[1],
}); // put == UPSERT
}
return txnAsPromise(txn);
return txnAsPromise(txn).then();
});
},
}
/**
* Load all user presence events from the database. This is not cached.
@@ -493,64 +480,56 @@ LocalIndexedDBStoreBackend.prototype = {
* sync.
* @return {Promise<Object[]>} A list of presence events in their raw form.
*/
getUserPresenceEvents: function() {
return utils.promiseTry(() => {
public getUserPresenceEvents(): Promise<UserTuple[]> {
return utils.promiseTry<UserTuple[]>(() => {
const txn = this.db.transaction(["users"], "readonly");
const store = txn.objectStore("users");
return selectQuery(store, undefined, (cursor) => {
return [cursor.value.userId, cursor.value.event];
});
});
},
}
/**
* Load all the account data events from the database. This is not cached.
* @return {Promise<Object[]>} A list of raw global account events.
*/
_loadAccountData: function() {
logger.log(
`LocalIndexedDBStoreBackend: loading account data...`,
);
return utils.promiseTry(() => {
private loadAccountData(): Promise<IMinimalEvent[]> {
logger.log(`LocalIndexedDBStoreBackend: loading account data...`);
return utils.promiseTry<IMinimalEvent[]>(() => {
const txn = this.db.transaction(["accountData"], "readonly");
const store = txn.objectStore("accountData");
return selectQuery(store, undefined, (cursor) => {
return cursor.value;
}).then((result) => {
logger.log(
`LocalIndexedDBStoreBackend: loaded account data`,
);
}).then((result: IMinimalEvent[]) => {
logger.log(`LocalIndexedDBStoreBackend: loaded account data`);
return result;
});
});
},
}
/**
* Load the sync data from the database.
* @return {Promise<Object>} An object with "roomsData" and "nextBatch" keys.
*/
_loadSyncData: function() {
logger.log(
`LocalIndexedDBStoreBackend: loading sync data...`,
);
return utils.promiseTry(() => {
private loadSyncData(): Promise<ISyncData> {
logger.log(`LocalIndexedDBStoreBackend: loading sync data...`);
return utils.promiseTry<ISyncData>(() => {
const txn = this.db.transaction(["sync"], "readonly");
const store = txn.objectStore("sync");
return selectQuery(store, undefined, (cursor) => {
return cursor.value;
}).then((results) => {
logger.log(
`LocalIndexedDBStoreBackend: loaded sync data`,
);
}).then((results: ISyncData[]) => {
logger.log(`LocalIndexedDBStoreBackend: loaded sync data`);
if (results.length > 1) {
logger.warn("loadSyncData: More than 1 sync row found.");
}
return (results.length > 0 ? results[0] : {});
return results.length > 0 ? results[0] : {} as ISyncData;
});
});
},
}
getClientOptions: function() {
public getClientOptions(): Promise<IStartClientOpts> {
return Promise.resolve().then(() => {
const txn = this.db.transaction(["client_options"], "readonly");
const store = txn.objectStore("client_options");
@@ -560,9 +539,9 @@ LocalIndexedDBStoreBackend.prototype = {
}
}).then((results) => results[0]);
});
},
}
storeClientOptions: async function(options) {
public async storeClientOptions(options: IStartClientOpts): Promise<void> {
const txn = this.db.transaction(["client_options"], "readwrite");
const store = txn.objectStore("client_options");
store.put({
@@ -570,5 +549,5 @@ LocalIndexedDBStoreBackend.prototype = {
options: options,
}); // put == UPSERT
await txnAsPromise(txn);
},
};
}
}

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 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,9 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend.js";
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend";
import { logger } from '../logger';
interface ICmd {
command: string;
seq: number;
args?: any[];
}
/**
* This class lives in the webworker and drives a LocalIndexedDBStoreBackend
* controlled by messages from the main process.
@@ -35,16 +39,13 @@ import { logger } from '../logger';
*
*/
export class IndexedDBStoreWorker {
private backend: LocalIndexedDBStoreBackend = null;
/**
* @param {function} postMessage The web worker postMessage function that
* should be used to communicate back to the main script.
*/
constructor(postMessage) {
this.backend = null;
this.postMessage = postMessage;
this.onMessage = this.onMessage.bind(this);
}
constructor(private readonly postMessage: InstanceType<typeof Worker>["postMessage"]) {}
/**
* Passes a message event from the main script into the class. This method
@@ -52,17 +53,15 @@ export class IndexedDBStoreWorker {
*
* @param {Object} ev The message event
*/
onMessage(ev) {
const msg = ev.data;
public onMessage = (ev: MessageEvent): void => {
const msg: ICmd = ev.data;
let prom;
switch (msg.command) {
case '_setupWorker':
this.backend = new LocalIndexedDBStoreBackend(
// this is the 'indexedDB' global (where global != window
// because it's a web worker and there is no window).
indexedDB, msg.args[0],
);
this.backend = new LocalIndexedDBStoreBackend(indexedDB, msg.args[0]);
prom = Promise.resolve();
break;
case 'connect':
@@ -72,23 +71,16 @@ export class IndexedDBStoreWorker {
prom = this.backend.isNewlyCreated();
break;
case 'clearDatabase':
prom = this.backend.clearDatabase().then((result) => {
// This returns special classes which can't be cloned
// across to the main script, so don't try.
return {};
});
prom = this.backend.clearDatabase();
break;
case 'getSavedSync':
prom = this.backend.getSavedSync(false);
break;
case 'setSyncData':
prom = this.backend.setSyncData(...msg.args);
prom = this.backend.setSyncData(msg.args[0]);
break;
case 'syncToDatabase':
prom = this.backend.syncToDatabase(...msg.args).then(() => {
// This also returns IndexedDB events which are not cloneable
return {};
});
prom = this.backend.syncToDatabase(msg.args[0]);
break;
case 'getUserPresenceEvents':
prom = this.backend.getUserPresenceEvents();
@@ -130,7 +122,7 @@ export class IndexedDBStoreWorker {
result: ret,
});
}, (err) => {
logger.error("Error running command: "+msg.command);
logger.error("Error running command: " + msg.command);
logger.error(err);
this.postMessage.call(null, {
command: 'cmd_fail',
@@ -142,5 +134,5 @@ export class IndexedDBStoreWorker {
},
});
});
}
};
}

View File

@@ -19,12 +19,14 @@ limitations under the License.
import { EventEmitter } from 'events';
import { MemoryStore, IOpts as IBaseOpts } from "./memory";
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend.js";
import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend.js";
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend";
import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend";
import { User } from "../models/user";
import { MatrixEvent } from "../models/event";
import { IEvent, MatrixEvent } from "../models/event";
import { logger } from '../logger';
import { ISavedSync } from "./index";
import { IIndexedDBBackend } from "./indexeddb-backend";
import { ISyncResponse } from "../sync-accumulator";
/**
* This is an internal module. See {@link IndexedDBStore} for the public class.
@@ -41,8 +43,7 @@ const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
interface IOpts extends IBaseOpts {
indexedDB: IDBFactory;
dbName?: string;
workerScript?: string;
workerApi?: typeof Worker;
workerFactory?: () => Worker;
}
export class IndexedDBStore extends MemoryStore {
@@ -50,8 +51,7 @@ export class IndexedDBStore extends MemoryStore {
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
}
// TODO these should conform to one interface
public readonly backend: LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend;
public readonly backend: IIndexedDBBackend;
private startedUp = false;
private syncTs = 0;
@@ -110,16 +110,8 @@ export class IndexedDBStore extends MemoryStore {
throw new Error('Missing required option: indexedDB');
}
if (opts.workerScript) {
// try & find a webworker-compatible API
let workerApi = opts.workerApi;
if (!workerApi) {
// default to the global Worker object (which is where it in a browser)
workerApi = global.Worker;
}
this.backend = new RemoteIndexedDBStoreBackend(
opts.workerScript, opts.dbName, workerApi,
);
if (opts.workerFactory) {
this.backend = new RemoteIndexedDBStoreBackend(opts.workerFactory, opts.dbName);
} else {
this.backend = new LocalIndexedDBStoreBackend(opts.indexedDB, opts.dbName);
}
@@ -222,7 +214,7 @@ export class IndexedDBStore extends MemoryStore {
// work out changed users (this doesn't handle deletions but you
// can't 'delete' users as they are just presence events).
const userTuples = [];
const userTuples: [userId: string, presenceEvent: Partial<IEvent>][] = [];
for (const u of this.getUsers()) {
if (this.userModifiedMap[u.userId] === u.getLastModifiedTime()) continue;
if (!u.events.presence) continue;
@@ -236,7 +228,7 @@ export class IndexedDBStore extends MemoryStore {
return this.backend.syncToDatabase(userTuples);
});
public setSyncData = this.degradable((syncData: object): Promise<void> => {
public setSyncData = this.degradable((syncData: ISyncResponse): Promise<void> => {
return this.backend.setSyncData(syncData);
}, "setSyncData");
@@ -247,7 +239,7 @@ export class IndexedDBStore extends MemoryStore {
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
public getOutOfBandMembers = this.degradable((roomId: string): Promise<MatrixEvent[]> => {
public getOutOfBandMembers = this.degradable((roomId: string): Promise<IEvent[]> => {
return this.backend.getOutOfBandMembers(roomId);
}, "getOutOfBandMembers");
@@ -259,7 +251,7 @@ export class IndexedDBStore extends MemoryStore {
* @param {event[]} membershipEvents the membership events to store
* @returns {Promise} when all members have been stored
*/
public setOutOfBandMembers = this.degradable((roomId: string, membershipEvents: MatrixEvent[]): Promise<void> => {
public setOutOfBandMembers = this.degradable((roomId: string, membershipEvents: IEvent[]): Promise<void> => {
super.setOutOfBandMembers(roomId, membershipEvents);
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
}, "setOutOfBandMembers");

View File

@@ -23,12 +23,13 @@ import { EventType } from "../@types/event";
import { Group } from "../models/group";
import { Room } from "../models/room";
import { User } from "../models/user";
import { MatrixEvent } from "../models/event";
import { IEvent, MatrixEvent } from "../models/event";
import { RoomState } from "../models/room-state";
import { RoomMember } from "../models/room-member";
import { Filter } from "../filter";
import { ISavedSync, IStore } from "./index";
import { RoomSummary } from "../models/room-summary";
import { ISyncResponse } from "../sync-accumulator";
function isValidFilterId(filterId: string): boolean {
const isValidStr = typeof filterId === "string" &&
@@ -59,9 +60,9 @@ export class MemoryStore implements IStore {
// filterId: Filter
// }
private filters: Record<string, Record<string, Filter>> = {};
private accountData: Record<string, MatrixEvent> = {}; // type : content
public accountData: Record<string, MatrixEvent> = {}; // type : content
private readonly localStorage: Storage;
private oobMembers: Record<string, MatrixEvent[]> = {}; // roomId: [member events]
private oobMembers: Record<string, IEvent[]> = {}; // roomId: [member events]
private clientOptions = {};
constructor(opts: IOpts = {}) {
@@ -340,7 +341,7 @@ export class MemoryStore implements IStore {
* @param {Object} syncData The sync data
* @return {Promise} An immediately resolved promise.
*/
public setSyncData(syncData: object): Promise<void> {
public setSyncData(syncData: ISyncResponse): Promise<void> {
return Promise.resolve();
}
@@ -415,7 +416,7 @@ export class MemoryStore implements IStore {
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
public getOutOfBandMembers(roomId: string): Promise<MatrixEvent[] | null> {
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
return Promise.resolve(this.oobMembers[roomId] || null);
}
@@ -427,7 +428,7 @@ export class MemoryStore implements IStore {
* @param {event[]} membershipEvents the membership events to store
* @returns {Promise} when all members have been stored
*/
public setOutOfBandMembers(roomId: string, membershipEvents: MatrixEvent[]): Promise<void> {
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
this.oobMembers[roomId] = membershipEvents;
return Promise.resolve();
}

View File

@@ -23,19 +23,21 @@ import { EventType } from "../@types/event";
import { Group } from "../models/group";
import { Room } from "../models/room";
import { User } from "../models/user";
import { MatrixEvent } from "../models/event";
import { IEvent, MatrixEvent } from "../models/event";
import { Filter } from "../filter";
import { ISavedSync, IStore } from "./index";
import { RoomSummary } from "../models/room-summary";
import { ISyncResponse } from "../sync-accumulator";
/**
* Construct a stub store. This does no-ops on most store methods.
* @constructor
*/
export class StubStore implements IStore {
public readonly accountData = {}; // stub
private fromToken: string = null;
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
/** @return {Promise<boolean>} whether or not the database was newly created in this session. */
public isNewlyCreated(): Promise<boolean> {
return Promise.resolve(true);
}
@@ -212,7 +214,7 @@ export class StubStore implements IStore {
* @param {Object} syncData The sync data
* @return {Promise} An immediately resolved promise.
*/
public setSyncData(syncData: object): Promise<void> {
public setSyncData(syncData: ISyncResponse): Promise<void> {
return Promise.resolve();
}
@@ -264,11 +266,11 @@ export class StubStore implements IStore {
return Promise.resolve();
}
public getOutOfBandMembers(): Promise<MatrixEvent[]> {
public getOutOfBandMembers(): Promise<IEvent[]> {
return Promise.resolve(null);
}
public setOutOfBandMembers(roomId: string, membershipEvents: MatrixEvent[]): Promise<void> {
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
return Promise.resolve();
}

View File

@@ -40,8 +40,8 @@ export interface IEphemeral {
/* eslint-disable camelcase */
interface IUnreadNotificationCounts {
highlight_count: number;
notification_count: number;
highlight_count?: number;
notification_count?: number;
}
export interface IRoomEvent extends IMinimalEvent {
@@ -64,7 +64,7 @@ interface IState {
export interface ITimeline {
events: Array<IRoomEvent | IStateEvent>;
limited: boolean;
limited?: boolean;
prev_batch: string;
}
@@ -169,6 +169,13 @@ interface IRoom {
};
}
export interface ISyncData {
nextBatch: string;
accountData: IMinimalEvent[];
roomsData: IRooms;
groupsData: IGroups;
}
/**
* The purpose of this class is to accumulate /sync responses such that a
* complete "initial" JSON response can be returned which accurately represents
@@ -544,8 +551,8 @@ export class SyncAccumulator {
* /sync response from the 'rooms' key onwards. The "accountData" key is
* a list of raw events which represent global account data.
*/
public getJSON(forDatabase = false): object {
const data = {
public getJSON(forDatabase = false): ISyncData {
const data: IRooms = {
join: {},
invite: {},
// always empty. This is set by /sync when a room was previously
@@ -575,7 +582,7 @@ export class SyncAccumulator {
prev_batch: null,
},
unread_notifications: roomData._unreadNotifications,
summary: roomData._summary,
summary: roomData._summary as IRoomSummary,
};
// Add account data
Object.keys(roomData._accountData).forEach((evType) => {
@@ -678,7 +685,7 @@ export class SyncAccumulator {
});
// Add account data
const accData = [];
const accData: IMinimalEvent[] = [];
Object.keys(this.accountData).forEach((evType) => {
accData.push(this.accountData[evType]);
});

View File

@@ -136,7 +136,7 @@ export class SyncApi {
private syncStateData: ISyncStateData = null; // additional data (eg. error object for failed sync)
private catchingUp = false;
private running = false;
private keepAliveTimer: NodeJS.Timeout = null;
private keepAliveTimer: number = null;
private connectionReturnedDefer: IDeferred<boolean> = null;
private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response
private failedSyncCount = 0; // Number of consecutive failed /sync requests
@@ -319,7 +319,7 @@ export class SyncApi {
this._peekRoom = this.createRoom(roomId);
return this.client.roomInitialSync(roomId, 20).then((response) => {
// make sure things are init'd
response.messages = response.messages || {};
response.messages = response.messages || { chunk: [] };
response.messages.chunk = response.messages.chunk || [];
response.state = response.state || [];
@@ -330,8 +330,7 @@ export class SyncApi {
const stateEvents = response.state.map(client.getEventMapper());
const messages = response.messages.chunk.map(client.getEventMapper());
// XXX: copypasted from /sync until we kill off this
// minging v1 API stuff)
// XXX: copypasted from /sync until we kill off this minging v1 API stuff)
// handle presence events (User objects)
if (response.presence && Array.isArray(response.presence)) {
response.presence.map(client.getEventMapper()).forEach(
@@ -644,12 +643,12 @@ export class SyncApi {
// Now wait for the saved sync to finish...
debuglog("Waiting for saved sync before starting sync processing...");
await savedSyncPromise;
this._sync({ filterId });
this.doSync({ filterId });
};
if (client.isGuest()) {
// no push rules for guests, no access to POST filter for guests.
this._sync({});
this.doSync({});
} else {
// Pull the saved sync token out first, before the worker starts sending
// all the sync data which could take a while. This will let us send our
@@ -755,7 +754,7 @@ export class SyncApi {
* @param {string} syncOptions.filterId
* @param {boolean} syncOptions.hasSyncedBefore
*/
private async _sync(syncOptions: ISyncOptions): Promise<void> {
private async doSync(syncOptions: ISyncOptions): Promise<void> {
const client = this.client;
if (!this.running) {
@@ -852,7 +851,7 @@ export class SyncApi {
}
// Begin next sync
this._sync(syncOptions);
this.doSync(syncOptions);
}
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IRequestPromise<ISyncResponse> {
@@ -957,7 +956,7 @@ export class SyncApi {
catchingUp: true,
});
}
this._sync(syncOptions);
this.doSync(syncOptions);
});
this.currentSyncRequest = null;

View File

@@ -31,15 +31,28 @@ import type NodeCrypto from "crypto";
* @return {string} The encoded string e.g. foo=bar&baz=taz
*/
export function encodeParams(params: Record<string, string>): string {
let qs = "";
for (const key in params) {
if (!params.hasOwnProperty(key)) {
continue;
return new URLSearchParams(params).toString();
}
export type QueryDict = Record<string, string | string[]>;
/**
* Decode a query string in `application/x-www-form-urlencoded` format.
* @param {string} query A query string to decode e.g.
* foo=bar&via=server1&server2
* @return {Object} The decoded object, if any keys occurred multiple times
* then the value will be an array of strings, else it will be an array.
* This behaviour matches Node's qs.parse but is built on URLSearchParams
* for native web compatibility
*/
export function decodeParams(query: string): QueryDict {
const o: QueryDict = {};
const params = new URLSearchParams(query);
for (const key of params.keys()) {
const val = params.getAll(key);
o[key] = val.length === 1 ? val[0] : val;
}
qs += "&" + encodeURIComponent(key) + "=" +
encodeURIComponent(params[key]);
}
return qs.substring(1);
return o;
}
/**
@@ -116,10 +129,10 @@ export function isFunction(value: any) {
* @throws If the object is missing keys.
*/
// note using 'keys' here would shadow the 'keys' function defined above
export function checkObjectHasKeys(obj: object, keys_: string[]) {
for (let i = 0; i < keys_.length; i++) {
if (!obj.hasOwnProperty(keys_[i])) {
throw new Error("Missing required key: " + keys_[i]);
export function checkObjectHasKeys(obj: object, keys: string[]) {
for (let i = 0; i < keys.length; i++) {
if (!obj.hasOwnProperty(keys[i])) {
throw new Error("Missing required key: " + keys[i]);
}
}
}
@@ -464,7 +477,7 @@ export async function promiseMapSeries<T>(
}
}
export function promiseTry<T>(fn: () => T): Promise<T> {
export function promiseTry<T>(fn: () => T | Promise<T>): Promise<T> {
return new Promise((resolve) => resolve(fn()));
}
@@ -670,3 +683,13 @@ export function lexicographicCompare(a: string, b: string): number {
// hidden the operation in this function.
return (a < b) ? -1 : ((a === b) ? 0 : 1);
}
const collator = new Intl.Collator();
/**
* Performant language-sensitive string comparison
* @param a the first string to compare
* @param b the second string to compare
*/
export function compare(a: string, b: string): number {
return collator.compare(a, b);
}

View File

@@ -288,7 +288,7 @@ export class MatrixCall extends EventEmitter {
// yet, null if we have but they didn't send a party ID.
private opponentPartyId: string;
private opponentCaps: CallCapabilities;
private inviteTimeout: NodeJS.Timeout; // in the browser it's 'number'
private inviteTimeout: number;
// The logic of when & if a call is on hold is nontrivial and explained in is*OnHold
// This flag represents whether we want the other party to be on hold

View File

@@ -47,12 +47,12 @@ export class CallEventHandler {
public start() {
this.client.on("sync", this.evaluateEventBuffer);
this.client.on("event", this.onEvent);
this.client.on("Room.timeline", this.onRoomTimeline);
}
public stop() {
this.client.removeListener("sync", this.evaluateEventBuffer);
this.client.removeListener("event", this.onEvent);
this.client.removeListener("Room.timeline", this.onRoomTimeline);
}
private evaluateEventBuffer = async () => {
@@ -89,7 +89,7 @@ export class CallEventHandler {
}
};
private onEvent = (event: MatrixEvent) => {
private onRoomTimeline = (event: MatrixEvent) => {
this.client.decryptEventIfNeeded(event);
// any call events or ones that might be once they're decrypted
if (this.eventIsACall(event) || event.isBeingDecrypted()) {

2865
yarn.lock

File diff suppressed because it is too large Load Diff