You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-26 17:03:12 +03:00
Merge remote-tracking branch 'upstream/develop' into improve-event-clear-event-state
This commit is contained in:
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,3 +1,7 @@
|
||||
<!-- Please read https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst before submitting your pull request -->
|
||||
<!-- Please read https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md before submitting your pull request -->
|
||||
|
||||
<!-- Include a Sign-Off as described in https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst#sign-off -->
|
||||
<!-- Include a Sign-Off as described in https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md#sign-off -->
|
||||
|
||||
<!-- To specify text for the changelog entry (otherwise the PR title will be used):
|
||||
Notes:
|
||||
-->
|
||||
|
||||
194
CONTRIBUTING.md
Normal file
194
CONTRIBUTING.md
Normal file
@@ -0,0 +1,194 @@
|
||||
Contributing code to matrix-js-sdk
|
||||
==================================
|
||||
|
||||
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
|
||||
willing to license their contributions under the same license as the project
|
||||
itself. We follow a simple 'inbound=outbound' model for contributions: the act
|
||||
of submitting an 'inbound' contribution means that the contributor agrees to
|
||||
license the code under the same terms as the project's overall 'outbound'
|
||||
license - in this case, Apache Software License v2 (see
|
||||
[LICENSE](LICENSE)).
|
||||
|
||||
How to contribute
|
||||
-----------------
|
||||
|
||||
The preferred and easiest way to contribute changes to the project is to fork
|
||||
it on github, and then create a pull request to ask us to pull your changes
|
||||
into our repo (https://help.github.com/articles/using-pull-requests/)
|
||||
|
||||
We use GitHub's pull request workflow to review the contribution, and either
|
||||
ask you to make any refinements needed or merge it and make them ourselves.
|
||||
|
||||
Things that should go into your PR description:
|
||||
* A changelog entry in the `Notes` section (see below)
|
||||
* References to any bugs fixed by the change (in GitHub's `Fixes` notation)
|
||||
* Notes for the reviewer that might help them to understand why the change is
|
||||
necessary or how they might better review it.
|
||||
|
||||
Things that should *not* go into your PR description:
|
||||
* Any information on how the code works or why you chose to do it the way
|
||||
you did. If this isn't obvious from your code, you haven't written enough
|
||||
comments.
|
||||
|
||||
We rely on information in pull request to populate the information that goes
|
||||
into the changelogs our users see, both for the JS SDK itself and also for some
|
||||
projects based on it. This is picked up from both labels on the pull request and
|
||||
the `Notes:` annotation in the description. By default, the PR title will be
|
||||
used for the changelog entry, but you can specify more options, as follows.
|
||||
|
||||
To add a longer, more detailed description of the change for the changelog:
|
||||
|
||||
|
||||
*Fix llama herding bug*
|
||||
|
||||
```
|
||||
Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase
|
||||
```
|
||||
|
||||
For some PRs, it's not useful to have an entry in the user-facing changelog (this is
|
||||
the default for PRs labelled with `T-Task`):
|
||||
|
||||
*Remove outdated comment from `Ungulates.ts`*
|
||||
```
|
||||
Notes: none
|
||||
```
|
||||
|
||||
Sometimes, you're fixing a bug in a downstream project, in which case you want
|
||||
an entry in that project's changelog. You can do that too:
|
||||
|
||||
*Fix another herding bug*
|
||||
```
|
||||
Notes: Fix a bug where the `herd()` function would only work on Tuesdays
|
||||
element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays
|
||||
```
|
||||
|
||||
This example is for Element Web. You can specify:
|
||||
* matrix-react-sdk
|
||||
* element-web
|
||||
* element-desktop
|
||||
|
||||
If your PR introduces a breaking change, use the `Notes` section in the same
|
||||
way, additionally adding the `X-Breaking-Change` label (see below). There's no need
|
||||
to specify in the notes that it's a breaking change - this will be added
|
||||
automatically based on the label - but remember to tell the developer how to
|
||||
migrate:
|
||||
|
||||
*Remove legacy class*
|
||||
|
||||
```
|
||||
Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead.
|
||||
```
|
||||
|
||||
Other metadata can be added using labels.
|
||||
* `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a *major* version bump.
|
||||
* `T-Enhancement`: A new feature - adding this label will mean the change causes a *minor* version bump.
|
||||
* `T-Defect`: A bug fix (in either code or docs).
|
||||
* `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one.
|
||||
|
||||
If you don't have permission to add labels, your PR reviewer(s) can work with you
|
||||
to add them: ask in the PR description or comments.
|
||||
|
||||
We use continuous integration, and all pull requests get automatically tested:
|
||||
if your change breaks the build, then the PR will show that there are failed
|
||||
checks, so please check back after a few minutes.
|
||||
|
||||
Code style
|
||||
----------
|
||||
The js-sdk aims to target TypeScript/ES6. All new files should be written in
|
||||
TypeScript and existing files should use ES6 principles where possible.
|
||||
|
||||
Members should not be exported as a default export in general - it causes problems
|
||||
with the architecture of the SDK (index file becomes less clear) and could
|
||||
introduce naming problems (as default exports get aliased upon import). In
|
||||
general, avoid using `export default`.
|
||||
|
||||
The remaining code-style for matrix-js-sdk is not formally documented, but
|
||||
contributors are encouraged to read the
|
||||
[code style document for matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md)
|
||||
and follow the principles set out there.
|
||||
|
||||
Please ensure your changes match the cosmetic style of the existing project,
|
||||
and ***never*** mix cosmetic and functional changes in the same commit, as it
|
||||
makes it horribly hard to review otherwise.
|
||||
|
||||
Attribution
|
||||
-----------
|
||||
Everyone who contributes anything to Matrix is welcome to be listed in the
|
||||
AUTHORS.rst file for the project in question. Please feel free to include a
|
||||
change to AUTHORS.rst in your pull request to list yourself and a short
|
||||
description of the area(s) you've worked on. Also, we sometimes have swag to
|
||||
give away to contributors - if you feel that Matrix-branded apparel is missing
|
||||
from your life, please mail us your shipping address to matrix at matrix.org
|
||||
and we'll try to fix it :)
|
||||
|
||||
Sign off
|
||||
--------
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
and you agree to license it under the same terms as the project's license, we've
|
||||
adopted the same lightweight approach that the Linux Kernel
|
||||
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
|
||||
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO (Developer Certificate of Origin:
|
||||
http://developercertificate.org/). This is a simple declaration that you wrote
|
||||
the contribution or otherwise have the right to contribute it to Matrix:
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
If you agree to this for your contribution, then all that's needed is to
|
||||
include the line in your commit or pull request comment:
|
||||
|
||||
```
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
```
|
||||
|
||||
We accept contributions under a legally identifiable name, such as your name on
|
||||
government documentation or common-law names (names claimed by legitimate usage
|
||||
or repute). Unfortunately, we cannot accept anonymous contributions at this
|
||||
time.
|
||||
|
||||
Git allows you to add this signoff automatically when using the `-s` flag to
|
||||
`git commit`, which uses the name and email set in your `user.name` and
|
||||
`user.email` git configs.
|
||||
|
||||
If you forgot to sign off your commits before making your pull request and are
|
||||
on Git 2.17+ you can mass signoff using rebase:
|
||||
|
||||
```
|
||||
git rebase --signoff origin/develop
|
||||
```
|
||||
131
CONTRIBUTING.rst
131
CONTRIBUTING.rst
@@ -1,131 +0,0 @@
|
||||
Contributing code to matrix-js-sdk
|
||||
==================================
|
||||
|
||||
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
|
||||
willing to license their contributions under the same license as the project
|
||||
itself. We follow a simple 'inbound=outbound' model for contributions: the act
|
||||
of submitting an 'inbound' contribution means that the contributor agrees to
|
||||
license the code under the same terms as the project's overall 'outbound'
|
||||
license - in this case, Apache Software License v2 (see `<LICENSE>`_).
|
||||
|
||||
How to contribute
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The preferred and easiest way to contribute changes to the project is to fork
|
||||
it on github, and then create a pull request to ask us to pull your changes
|
||||
into our repo (https://help.github.com/articles/using-pull-requests/)
|
||||
|
||||
**The single biggest thing you need to know is: please base your changes on
|
||||
the develop branch - /not/ master.**
|
||||
|
||||
We use the master branch to track the most recent release, so that folks who
|
||||
blindly clone the repo and automatically check out master get something that
|
||||
works. Develop is the unstable branch where all the development actually
|
||||
happens: the workflow is that contributors should fork the develop branch to
|
||||
make a 'feature' branch for a particular contribution, and then make a pull
|
||||
request to merge this back into the matrix.org 'official' develop branch. We
|
||||
use GitHub's pull request workflow to review the contribution, and either ask
|
||||
you to make any refinements needed or merge it and make them ourselves. The
|
||||
changes will then land on master when we next do a release.
|
||||
|
||||
We use continuous integration, and all pull requests get automatically tested:
|
||||
if your change breaks the build, then the PR will show that there are failed
|
||||
checks, so please check back after a few minutes.
|
||||
|
||||
Code style
|
||||
~~~~~~~~~~
|
||||
|
||||
The js-sdk aims to target TypeScript/ES6. All new files should be written in
|
||||
TypeScript and existing files should use ES6 principles where possible.
|
||||
|
||||
Members should not be exported as a default export in general - it causes problems
|
||||
with the architecture of the SDK (index file becomes less clear) and could
|
||||
introduce naming problems (as default exports get aliased upon import). In
|
||||
general, avoid using `export default`.
|
||||
|
||||
The remaining code-style for matrix-js-sdk is not formally documented, but
|
||||
contributors are encouraged to read the code style document for matrix-react-sdk
|
||||
(`<https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md>`_)
|
||||
and follow the principles set out there.
|
||||
|
||||
Please ensure your changes match the cosmetic style of the existing project,
|
||||
and **never** mix cosmetic and functional changes in the same commit, as it
|
||||
makes it horribly hard to review otherwise.
|
||||
|
||||
Attribution
|
||||
~~~~~~~~~~~
|
||||
|
||||
Everyone who contributes anything to Matrix is welcome to be listed in the
|
||||
AUTHORS.rst file for the project in question. Please feel free to include a
|
||||
change to AUTHORS.rst in your pull request to list yourself and a short
|
||||
description of the area(s) you've worked on. Also, we sometimes have swag to
|
||||
give away to contributors - if you feel that Matrix-branded apparel is missing
|
||||
from your life, please mail us your shipping address to matrix at matrix.org
|
||||
and we'll try to fix it :)
|
||||
|
||||
Sign off
|
||||
~~~~~~~~
|
||||
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
and you agree to license it under the same terms as the project's license, we've
|
||||
adopted the same lightweight approach that the Linux Kernel
|
||||
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
|
||||
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO (Developer Certificate of Origin:
|
||||
http://developercertificate.org/). This is a simple declaration that you wrote
|
||||
the contribution or otherwise have the right to contribute it to Matrix::
|
||||
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
|
||||
If you agree to this for your contribution, then all that's needed is to
|
||||
include the line in your commit or pull request comment::
|
||||
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
|
||||
We accept contributions under a legally identifiable name, such as your name on
|
||||
government documentation or common-law names (names claimed by legitimate usage
|
||||
or repute). Unfortunately, we cannot accept anonymous contributions at this
|
||||
time.
|
||||
|
||||
Git allows you to add this signoff automatically when using the ``-s`` flag to
|
||||
``git commit``, which uses the name and email set in your ``user.name`` and
|
||||
``user.email`` git configs.
|
||||
|
||||
If you forgot to sign off your commits before making your pull request and are
|
||||
on Git 2.17+ you can mass signoff using rebase::
|
||||
|
||||
git rebase --signoff origin/develop
|
||||
@@ -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",
|
||||
@@ -28,7 +29,7 @@
|
||||
"keywords": [
|
||||
"matrix-org"
|
||||
],
|
||||
"main": "./lib/index.js",
|
||||
"main": "./src/index.ts",
|
||||
"browser": "./lib/browser-index.js",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_src_browser": "./src/browser-index.js",
|
||||
@@ -110,6 +111,5 @@
|
||||
"coverageReporters": [
|
||||
"text"
|
||||
]
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { EventStatus, MatrixEvent } from "../../src";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import { RoomState } from "../../src";
|
||||
import { Room } from "../../src";
|
||||
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("Room", function() {
|
||||
@@ -1456,4 +1457,291 @@ describe("Room", function() {
|
||||
expect(room.maySendMessage()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDefaultRoomName", function() {
|
||||
it("should return 'Empty room' if a user is the only member",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
|
||||
});
|
||||
|
||||
it("should return a display name if one other member is in the room",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||
});
|
||||
|
||||
it("should return a display name if one other member is banned",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "ban",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)");
|
||||
});
|
||||
|
||||
it("should return a display name if one other member is invited",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "invite",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||
});
|
||||
|
||||
it("should return 'Empty room (was User B)' if User B left the room",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "leave",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)");
|
||||
});
|
||||
|
||||
it("should return 'User B and User C' if in a room with two other users",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userC, mship: "join",
|
||||
room: roomId, event: true, name: "User C",
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("User B and User C");
|
||||
});
|
||||
|
||||
it("should return 'User B and 2 others' if in a room with three other users",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userC, mship: "join",
|
||||
room: roomId, event: true, name: "User C",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userD, mship: "join",
|
||||
room: roomId, event: true, name: "User D",
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("User B and 2 others");
|
||||
});
|
||||
|
||||
describe("io.element.functional_users", function() {
|
||||
it("should return a display name (default behaviour) if no one is marked as a functional member",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||
room: roomId, event: true,
|
||||
content: {
|
||||
service_members: [],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||
});
|
||||
|
||||
it("should return a display name (default behaviour) if service members is a number (invalid)",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||
room: roomId, event: true,
|
||||
content: {
|
||||
service_members: 1,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||
});
|
||||
|
||||
it("should return a display name (default behaviour) if service members is a string (invalid)",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||
room: roomId, event: true,
|
||||
content: {
|
||||
service_members: userB,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||
});
|
||||
|
||||
it("should return 'Empty room' if the only other member is a functional member",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||
room: roomId, event: true,
|
||||
content: {
|
||||
service_members: [userB],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
|
||||
});
|
||||
|
||||
it("should return 'User B' if User B is the only other member who isn't a functional member",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userC, mship: "join",
|
||||
room: roomId, event: true, name: "User C",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||
room: roomId, event: true, user: userA,
|
||||
content: {
|
||||
service_members: [userC],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||
});
|
||||
|
||||
it("should return 'Empty room' if all other members are functional members",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userC, mship: "join",
|
||||
room: roomId, event: true, name: "User C",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||
room: roomId, event: true, user: userA,
|
||||
content: {
|
||||
service_members: [userB, userC],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
|
||||
});
|
||||
|
||||
it("should not break if an unjoined user is marked as a service user",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([
|
||||
utils.mkMembership({
|
||||
user: userA, mship: "join",
|
||||
room: roomId, event: true, name: "User A",
|
||||
}),
|
||||
utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true, name: "User B",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||
room: roomId, event: true, user: userA,
|
||||
content: {
|
||||
service_members: [userC],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
164
src/@types/PushRules.ts
Normal file
164
src/@types/PushRules.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// allow camelcase as these are things that go onto the wire
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
export enum PushRuleActionName {
|
||||
DontNotify = "dont_notify",
|
||||
Notify = "notify",
|
||||
Coalesce = "coalesce",
|
||||
}
|
||||
|
||||
export enum TweakName {
|
||||
Highlight = "highlight",
|
||||
Sound = "sound",
|
||||
}
|
||||
|
||||
export type Tweak<N extends TweakName, V> = {
|
||||
set_tweak: N;
|
||||
value: V;
|
||||
};
|
||||
|
||||
export type TweakHighlight = Tweak<TweakName.Highlight, boolean>;
|
||||
export type TweakSound = Tweak<TweakName.Sound, string>;
|
||||
|
||||
export type Tweaks = TweakHighlight | TweakSound;
|
||||
|
||||
export enum ConditionOperator {
|
||||
ExactEquals = "==",
|
||||
LessThan = "<",
|
||||
GreaterThan = ">",
|
||||
GreaterThanOrEqual = ">=",
|
||||
LessThanOrEqual = "<=",
|
||||
}
|
||||
|
||||
export type PushRuleAction = Tweaks | PushRuleActionName;
|
||||
|
||||
export type MemberCountCondition
|
||||
<N extends number, Op extends ConditionOperator = ConditionOperator.ExactEquals>
|
||||
= `${Op}${N}` | (Op extends ConditionOperator.ExactEquals ? `${N}` : never);
|
||||
|
||||
export type AnyMemberCountCondition = MemberCountCondition<number, ConditionOperator>;
|
||||
|
||||
export const DMMemberCountCondition: MemberCountCondition<2> = "2";
|
||||
|
||||
export function isDmMemberCountCondition(condition: AnyMemberCountCondition): boolean {
|
||||
return condition === "==2" || condition === "2";
|
||||
}
|
||||
|
||||
export enum ConditionKind {
|
||||
EventMatch = "event_match",
|
||||
ContainsDisplayName = "contains_display_name",
|
||||
RoomMemberCount = "room_member_count",
|
||||
SenderNotificationPermission = "sender_notification_permission",
|
||||
}
|
||||
|
||||
export interface IPushRuleCondition<N extends ConditionKind | string> {
|
||||
[k: string]: any; // for custom conditions, there can be other fields here
|
||||
kind: N;
|
||||
}
|
||||
|
||||
export interface IEventMatchCondition extends IPushRuleCondition<ConditionKind.EventMatch> {
|
||||
key: string;
|
||||
pattern: string;
|
||||
}
|
||||
|
||||
export interface IContainsDisplayNameCondition extends IPushRuleCondition<ConditionKind.ContainsDisplayName> {
|
||||
// no additional fields
|
||||
}
|
||||
|
||||
export interface IRoomMemberCountCondition extends IPushRuleCondition<ConditionKind.RoomMemberCount> {
|
||||
is: AnyMemberCountCondition;
|
||||
}
|
||||
|
||||
export interface ISenderNotificationPermissionCondition
|
||||
extends IPushRuleCondition<ConditionKind.SenderNotificationPermission> {
|
||||
key: string;
|
||||
}
|
||||
|
||||
export type PushRuleCondition = IPushRuleCondition<string>
|
||||
| IEventMatchCondition
|
||||
| IContainsDisplayNameCondition
|
||||
| IRoomMemberCountCondition
|
||||
| ISenderNotificationPermissionCondition;
|
||||
|
||||
export enum PushRuleKind {
|
||||
Override = "override",
|
||||
ContentSpecific = "content",
|
||||
RoomSpecific = "room",
|
||||
SenderSpecific = "sender",
|
||||
Underride = "underride",
|
||||
}
|
||||
|
||||
export enum RuleId {
|
||||
Master = ".m.rule.master",
|
||||
ContainsDisplayName = ".m.rule.contains_display_name",
|
||||
ContainsUserName = ".m.rule.contains_user_name",
|
||||
AtRoomNotification = ".m.rule.roomnotif",
|
||||
DM = ".m.rule.room_one_to_one",
|
||||
EncryptedDM = ".m.rule.encrypted_room_one_to_one",
|
||||
Message = ".m.rule.message",
|
||||
EncryptedMessage = ".m.rule.encrypted",
|
||||
InviteToSelf = ".m.rule.invite_for_me",
|
||||
MemberEvent = ".m.rule.member_event",
|
||||
IncomingCall = ".m.rule.call",
|
||||
SuppressNotices = ".m.rule.suppress_notices",
|
||||
Tombstone = ".m.rule.tombstone",
|
||||
}
|
||||
|
||||
export type PushRuleSet = {
|
||||
[k in PushRuleKind]?: IPushRule[];
|
||||
};
|
||||
|
||||
export interface IPushRule {
|
||||
actions: PushRuleAction[];
|
||||
conditions?: PushRuleCondition[];
|
||||
default: boolean;
|
||||
enabled: boolean;
|
||||
pattern?: string;
|
||||
rule_id: RuleId | string;
|
||||
}
|
||||
|
||||
export interface IAnnotatedPushRule extends IPushRule {
|
||||
kind: PushRuleKind;
|
||||
}
|
||||
|
||||
export interface IPushRules {
|
||||
global: PushRuleSet;
|
||||
device?: PushRuleSet;
|
||||
}
|
||||
|
||||
export interface IPusher {
|
||||
app_display_name: string;
|
||||
app_id: string;
|
||||
data: {
|
||||
format?: string; // TODO: Types
|
||||
url?: string; // TODO: Required if kind==http
|
||||
brand?: string; // TODO: For email notifications only?
|
||||
};
|
||||
device_display_name: string;
|
||||
kind: string; // TODO: Types
|
||||
lang: string;
|
||||
profile_tag?: string;
|
||||
pushkey: string;
|
||||
}
|
||||
|
||||
export interface IPusherRequest extends IPusher {
|
||||
append?: boolean;
|
||||
}
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
@@ -144,6 +144,28 @@ export const UNSTABLE_MSC3089_LEAF = new UnstableValue("m.leaf", "org.matrix.msc
|
||||
*/
|
||||
export const UNSTABLE_MSC3089_BRANCH = new UnstableValue("m.branch", "org.matrix.msc3089.branch");
|
||||
|
||||
/**
|
||||
* Functional members type for declaring a purpose of room members (e.g. helpful bots).
|
||||
* Note that this reference is UNSTABLE and subject to breaking changes, including its
|
||||
* eventual removal.
|
||||
*
|
||||
* Schema (TypeScript):
|
||||
* {
|
||||
* service_members?: string[]
|
||||
* }
|
||||
*
|
||||
* Example:
|
||||
* {
|
||||
* "service_members": [
|
||||
* "@helperbot:localhost",
|
||||
* "@reminderbot:alice.tdl"
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
|
||||
"io.element.functional_members",
|
||||
"io.element.functional_members");
|
||||
|
||||
export interface IEncryptedFile {
|
||||
url: string;
|
||||
mimetype?: string;
|
||||
|
||||
6
src/@types/global.d.ts
vendored
6
src/@types/global.d.ts
vendored
@@ -20,6 +20,12 @@ import "@matrix-org/olm";
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
// use `number` as the return type in all cases for global.set{Interval,Timeout},
|
||||
// so we don't accidentally use the methods on NodeJS.Timeout - they only exist in a subset of environments.
|
||||
// The overload for clear{Interval,Timeout} is resolved as expected.
|
||||
function setInterval(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
|
||||
function setTimeout(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
|
||||
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
localStorage: Storage;
|
||||
|
||||
@@ -39,3 +39,38 @@ export enum Preset {
|
||||
}
|
||||
|
||||
export type ResizeMethod = "crop" | "scale";
|
||||
|
||||
// TODO move to http-api after TSification
|
||||
export interface IAbortablePromise<T> extends Promise<T> {
|
||||
abort(): void;
|
||||
}
|
||||
|
||||
export type IdServerUnbindResult = "no-support" | "success";
|
||||
|
||||
// Knock and private are reserved keywords which are not yet implemented.
|
||||
export enum JoinRule {
|
||||
Public = "public",
|
||||
Invite = "invite",
|
||||
/**
|
||||
* @deprecated Reserved keyword. Should not be used. Not yet implemented.
|
||||
*/
|
||||
Private = "private",
|
||||
Knock = "knock", // MSC2403 - only valid inside experimental room versions at this time.
|
||||
Restricted = "restricted", // MSC3083 - only valid inside experimental room versions at this time.
|
||||
}
|
||||
|
||||
export enum RestrictedAllowType {
|
||||
RoomMembership = "m.room_membership", // MSC3083 - only valid inside experimental room versions at this time.
|
||||
}
|
||||
|
||||
export enum GuestAccess {
|
||||
CanJoin = "can_join",
|
||||
Forbidden = "forbidden",
|
||||
}
|
||||
|
||||
export enum HistoryVisibility {
|
||||
Invited = "invited",
|
||||
Joined = "joined",
|
||||
Shared = "shared",
|
||||
WorldReadable = "world_readable",
|
||||
}
|
||||
|
||||
@@ -15,9 +15,12 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { Callback } from "../client";
|
||||
import { IContent } from "../models/event";
|
||||
import { Preset, Visibility } from "./partials";
|
||||
import { SearchKey } from "./search";
|
||||
import { IRoomEventFilter } from "../filter";
|
||||
|
||||
// allow camelcase as these are things go onto the wire
|
||||
// allow camelcase as these are things that go onto the wire
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
export interface IJoinRoomOpts {
|
||||
@@ -63,12 +66,12 @@ export interface IGuestAccessOpts {
|
||||
}
|
||||
|
||||
export interface ISearchOpts {
|
||||
keys?: string[];
|
||||
keys?: SearchKey[];
|
||||
query: string;
|
||||
}
|
||||
|
||||
export interface IEventSearchOpts {
|
||||
filter: any; // TODO: Types
|
||||
filter?: IRoomEventFilter;
|
||||
term: string;
|
||||
}
|
||||
|
||||
@@ -82,7 +85,7 @@ export interface IInvite3PID {
|
||||
export interface ICreateRoomStateEvent {
|
||||
type: string;
|
||||
state_key?: string; // defaults to an empty string
|
||||
content: object;
|
||||
content: IContent;
|
||||
}
|
||||
|
||||
export interface ICreateRoomOpts {
|
||||
@@ -104,9 +107,11 @@ export interface IRoomDirectoryOptions {
|
||||
server?: string;
|
||||
limit?: number;
|
||||
since?: string;
|
||||
|
||||
// TODO: Proper types
|
||||
filter?: any & {generic_search_term: string};
|
||||
filter?: {
|
||||
generic_search_term: string;
|
||||
};
|
||||
include_all_networks?: boolean;
|
||||
third_party_instance_id?: string;
|
||||
}
|
||||
|
||||
export interface IUploadOpts {
|
||||
@@ -119,4 +124,19 @@ export interface IUploadOpts {
|
||||
progressHandler?: (state: {loaded: number, total: number}) => void;
|
||||
}
|
||||
|
||||
export interface IAddThreePidOnlyBody {
|
||||
auth?: {
|
||||
type: string;
|
||||
session?: string;
|
||||
};
|
||||
client_secret: string;
|
||||
sid: string;
|
||||
}
|
||||
|
||||
export interface IBindThreePidBody {
|
||||
client_secret: string;
|
||||
id_server: string;
|
||||
id_access_token: string;
|
||||
sid: string;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
118
src/@types/search.ts
Normal file
118
src/@types/search.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Types relating to the /search API
|
||||
|
||||
import { IRoomEvent, IStateEvent } from "../sync-accumulator";
|
||||
import { IRoomEventFilter } from "../filter";
|
||||
import { SearchResult } from "../models/search-result";
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface IEventWithRoomId extends IRoomEvent {
|
||||
room_id: string;
|
||||
}
|
||||
|
||||
export interface IStateEventWithRoomId extends IStateEvent {
|
||||
room_id: string;
|
||||
}
|
||||
|
||||
export interface IMatrixProfile {
|
||||
avatar_url?: string;
|
||||
displayname?: string;
|
||||
}
|
||||
|
||||
export interface IResultContext {
|
||||
events_before: IEventWithRoomId[];
|
||||
events_after: IEventWithRoomId[];
|
||||
profile_info: Record<string, IMatrixProfile>;
|
||||
start?: string;
|
||||
end?: string;
|
||||
}
|
||||
|
||||
export interface ISearchResult {
|
||||
rank: number;
|
||||
result: IEventWithRoomId;
|
||||
context: IResultContext;
|
||||
}
|
||||
|
||||
enum GroupKey {
|
||||
RoomId = "room_id",
|
||||
Sender = "sender",
|
||||
}
|
||||
|
||||
export interface IResultRoomEvents {
|
||||
count: number;
|
||||
highlights: string[];
|
||||
results: ISearchResult[];
|
||||
state?: { [roomId: string]: IStateEventWithRoomId[] };
|
||||
groups?: {
|
||||
[groupKey in GroupKey]: {
|
||||
[value: string]: {
|
||||
next_batch?: string;
|
||||
order: number;
|
||||
results: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
next_batch?: string;
|
||||
}
|
||||
|
||||
interface IResultCategories {
|
||||
room_events: IResultRoomEvents;
|
||||
}
|
||||
|
||||
export type SearchKey = "content.body" | "content.name" | "content.topic";
|
||||
|
||||
export enum SearchOrderBy {
|
||||
Recent = "recent",
|
||||
Rank = "rank",
|
||||
}
|
||||
|
||||
export interface ISearchRequestBody {
|
||||
search_categories: {
|
||||
room_events: {
|
||||
search_term: string;
|
||||
keys?: SearchKey[];
|
||||
filter?: IRoomEventFilter;
|
||||
order_by?: SearchOrderBy;
|
||||
event_context?: {
|
||||
before_limit?: number;
|
||||
after_limit?: number;
|
||||
include_profile?: boolean;
|
||||
};
|
||||
include_state?: boolean;
|
||||
groupings?: {
|
||||
group_by: {
|
||||
key: GroupKey;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ISearchResponse {
|
||||
search_categories: IResultCategories;
|
||||
}
|
||||
|
||||
export interface ISearchResults {
|
||||
_query?: ISearchRequestBody;
|
||||
results: SearchResult[];
|
||||
highlights: string[];
|
||||
count?: number;
|
||||
next_batch?: string;
|
||||
pendingRequest?: Promise<ISearchResults>;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
40
src/@types/spaces.ts
Normal file
40
src/@types/spaces.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IPublicRoomsChunkRoom } from "../client";
|
||||
|
||||
// Types relating to Rooms of type `m.space` and related APIs
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface ISpaceSummaryRoom extends IPublicRoomsChunkRoom {
|
||||
num_refs: number;
|
||||
room_type: string;
|
||||
}
|
||||
|
||||
export interface ISpaceSummaryEvent {
|
||||
room_id: string;
|
||||
event_id: string;
|
||||
origin_server_ts: number;
|
||||
type: string;
|
||||
state_key: string;
|
||||
content: {
|
||||
order?: string;
|
||||
suggested?: boolean;
|
||||
auto_join?: boolean;
|
||||
via?: string[];
|
||||
};
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
40
src/@types/synapse.ts
Normal file
40
src/@types/synapse.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IdServerUnbindResult } from "./partials";
|
||||
|
||||
// Types relating to Synapse Admin APIs
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface ISynapseAdminWhoisResponse {
|
||||
user_id: string;
|
||||
devices: {
|
||||
[deviceId: string]: {
|
||||
sessions: {
|
||||
connections: {
|
||||
ip: string;
|
||||
last_seen: number; // millis since epoch
|
||||
user_agent: string;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ISynapseAdminDeactivateResponse {
|
||||
id_server_unbind_result: IdServerUnbindResult;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
28
src/@types/threepids.ts
Normal file
28
src/@types/threepids.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export enum ThreepidMedium {
|
||||
Email = "email",
|
||||
Phone = "msisdn",
|
||||
}
|
||||
|
||||
// TODO: Are these types universal, or specific to just /account/3pid?
|
||||
export interface IThreepid {
|
||||
medium: ThreepidMedium;
|
||||
address: string;
|
||||
validated_at: number; // eslint-disable-line camelcase
|
||||
added_at: number; // eslint-disable-line camelcase
|
||||
}
|
||||
996
src/client.ts
996
src/client.ts
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,8 @@ limitations under the License.
|
||||
|
||||
/** @module ContentHelpers */
|
||||
|
||||
import { MsgType } from "./@types/event";
|
||||
|
||||
/**
|
||||
* Generates the content for a HTML Message event
|
||||
* @param {string} body the plaintext body of the message
|
||||
@@ -25,7 +27,7 @@ limitations under the License.
|
||||
*/
|
||||
export function makeHtmlMessage(body: string, htmlBody: string) {
|
||||
return {
|
||||
msgtype: "m.text",
|
||||
msgtype: MsgType.Text,
|
||||
format: "org.matrix.custom.html",
|
||||
body: body,
|
||||
formatted_body: htmlBody,
|
||||
@@ -40,7 +42,7 @@ export function makeHtmlMessage(body: string, htmlBody: string) {
|
||||
*/
|
||||
export function makeHtmlNotice(body: string, htmlBody: string) {
|
||||
return {
|
||||
msgtype: "m.notice",
|
||||
msgtype: MsgType.Notice,
|
||||
format: "org.matrix.custom.html",
|
||||
body: body,
|
||||
formatted_body: htmlBody,
|
||||
@@ -55,7 +57,7 @@ export function makeHtmlNotice(body: string, htmlBody: string) {
|
||||
*/
|
||||
export function makeHtmlEmote(body: string, htmlBody: string) {
|
||||
return {
|
||||
msgtype: "m.emote",
|
||||
msgtype: MsgType.Emote,
|
||||
format: "org.matrix.custom.html",
|
||||
body: body,
|
||||
formatted_body: htmlBody,
|
||||
@@ -69,7 +71,7 @@ export function makeHtmlEmote(body: string, htmlBody: string) {
|
||||
*/
|
||||
export function makeTextMessage(body: string) {
|
||||
return {
|
||||
msgtype: "m.text",
|
||||
msgtype: MsgType.Text,
|
||||
body: body,
|
||||
};
|
||||
}
|
||||
@@ -81,7 +83,7 @@ export function makeTextMessage(body: string) {
|
||||
*/
|
||||
export function makeNotice(body: string) {
|
||||
return {
|
||||
msgtype: "m.notice",
|
||||
msgtype: MsgType.Notice,
|
||||
body: body,
|
||||
};
|
||||
}
|
||||
@@ -93,7 +95,7 @@ export function makeNotice(body: string) {
|
||||
*/
|
||||
export function makeEmoteMessage(body: string) {
|
||||
return {
|
||||
msgtype: "m.emote",
|
||||
msgtype: MsgType.Emote,
|
||||
body: body,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ export class DeviceList extends EventEmitter {
|
||||
// The time the save is scheduled for
|
||||
private savePromiseTime: number = null;
|
||||
// The timer used to delay the save
|
||||
private saveTimer: NodeJS.Timeout = null;
|
||||
private saveTimer: number = null;
|
||||
// True if we have fetched data from the server or loaded a non-empty
|
||||
// set of device data from the store
|
||||
private hasFetched: boolean = null;
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { EventEmitter } from "events";
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,7 +300,7 @@ class AccountDataClientAdapter extends EventEmitter {
|
||||
* @param {Object} content
|
||||
* @return {Promise}
|
||||
*/
|
||||
public setAccountData(type: string, content: any): 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 {};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export enum RoomKeyRequestState {
|
||||
export class OutgoingRoomKeyRequestManager {
|
||||
// handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null
|
||||
// if the callback has been set, or if it is still running.
|
||||
private sendOutgoingRoomKeyRequestsTimer: NodeJS.Timeout = null;
|
||||
private sendOutgoingRoomKeyRequestsTimer: number = null;
|
||||
|
||||
// sanity check to ensure that we don't end up with two concurrent runs
|
||||
// of sendOutgoingRoomKeyRequests
|
||||
@@ -366,7 +366,7 @@ export class OutgoingRoomKeyRequestManager {
|
||||
});
|
||||
};
|
||||
|
||||
this.sendOutgoingRoomKeyRequestsTimer = global.setTimeout(
|
||||
this.sendOutgoingRoomKeyRequestsTimer = setTimeout(
|
||||
startSendingOutgoingRoomKeyRequests,
|
||||
SEND_KEY_REQUESTS_DELAY_MS,
|
||||
);
|
||||
|
||||
@@ -24,10 +24,10 @@ import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { CryptoStore } from "../client";
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IRoomEncryption {
|
||||
export interface IRoomEncryption {
|
||||
algorithm: string;
|
||||
rotation_period_ms: number;
|
||||
rotation_period_msgs: number;
|
||||
rotation_period_ms?: number;
|
||||
rotation_period_msgs?: number;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
|
||||
@@ -37,9 +37,9 @@ export interface ISecretRequest {
|
||||
|
||||
export interface IAccountDataClient extends EventEmitter {
|
||||
// Subset of MatrixClient (which also uses any for the event content)
|
||||
getAccountDataFromServer: (eventType: string) => Promise<any>;
|
||||
getAccountDataFromServer: (eventType: string) => Promise<Record<string, any>>;
|
||||
getAccountData: (eventType: string) => MatrixEvent;
|
||||
setAccountData: (eventType: string, content: any) => Promise<void>;
|
||||
setAccountData: (eventType: string, content: any) => Promise<{}>;
|
||||
}
|
||||
|
||||
interface ISecretRequestInternal {
|
||||
@@ -174,7 +174,7 @@ export class SecretStorage {
|
||||
* the form [keyId, keyInfo]. Otherwise, null is returned.
|
||||
* XXX: why is this an array when addKey returns an object?
|
||||
*/
|
||||
public async getKey(keyId: string): Promise<SecretStorageKeyTuple> {
|
||||
public async getKey(keyId: string): Promise<SecretStorageKeyTuple | null> {
|
||||
if (!keyId) {
|
||||
keyId = await this.getDefaultKeyId();
|
||||
}
|
||||
@@ -184,7 +184,7 @@ export class SecretStorage {
|
||||
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
) as ISecretStorageKeyInfo;
|
||||
return keyInfo ? [keyId, keyInfo] : null;
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ export class SecretStorage {
|
||||
// get key information from key storage
|
||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
) as ISecretStorageKeyInfo;
|
||||
if (!keyInfo) {
|
||||
throw new Error("Unknown key: " + keyId);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,7 @@ import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { encodeRecoveryKey } from './recoverykey';
|
||||
import { encryptAES, decryptAES, calculateKeyCheck } from './aes';
|
||||
import { getCrypto } from '../utils';
|
||||
import { ICurve25519AuthData, IAes256AuthData, IKeyBackupInfo } from "./keybackup";
|
||||
import { ICurve25519AuthData, IAes256AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
|
||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||
@@ -85,12 +85,22 @@ interface BackupAlgorithmClass {
|
||||
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.
|
||||
*/
|
||||
@@ -464,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(
|
||||
@@ -487,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,
|
||||
@@ -495,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();
|
||||
@@ -636,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 {
|
||||
@@ -766,14 +775,12 @@ export class Aes256 implements BackupAlgorithm {
|
||||
return await encryptAES(JSON.stringify(plainText), this.key, data.session_id);
|
||||
}
|
||||
|
||||
async decryptSessions(sessions: Record<string, any>): Promise<Record<string, any>[]> {
|
||||
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,
|
||||
));
|
||||
const decrypted = JSON.parse(await decryptAES(sessionData.session_data, this.key, sessionId));
|
||||
decrypted.session_id = sessionId;
|
||||
keys.push(decrypted);
|
||||
} catch (e) {
|
||||
|
||||
@@ -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]: {
|
||||
|
||||
@@ -44,7 +44,7 @@ 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";
|
||||
@@ -53,7 +53,7 @@ import { ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDevi
|
||||
import { IllegalMethod } from "./verification/IllegalMethod";
|
||||
import { KeySignatureUploadError } from "../errors";
|
||||
import { decryptAES, encryptAES, calculateKeyCheck } from './aes';
|
||||
import { DehydrationManager } from './dehydration';
|
||||
import { DehydrationManager, IDeviceKeys, IOneTimeKey } from './dehydration';
|
||||
import { BackupManager } from "./backup";
|
||||
import { IStore } from "../store";
|
||||
import { Room } from "../models/room";
|
||||
@@ -61,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";
|
||||
@@ -70,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
|
||||
@@ -82,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);
|
||||
@@ -142,6 +145,10 @@ interface IDeviceVerificationUpgrade {
|
||||
crossSigningInfo: CrossSigningInfo;
|
||||
}
|
||||
|
||||
export interface ICheckOwnCrossSigningTrustOpts {
|
||||
allowPrivateKeyRequests?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} module:crypto~OlmSessionResult
|
||||
* @property {module:crypto/deviceinfo} device device info
|
||||
@@ -1418,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
|
||||
@@ -1772,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>,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1904,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;
|
||||
@@ -2252,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(
|
||||
@@ -2465,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
|
||||
@@ -2522,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;
|
||||
|
||||
@@ -2878,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
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface IKeyBackupSession {
|
||||
ciphertext: string;
|
||||
ephemeral: string;
|
||||
mac: string;
|
||||
iv: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import OlmDevice from "./OlmDevice";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { logger } from '../logger';
|
||||
import * as utils from "../utils";
|
||||
import { OneTimeKey } from "./dehydration";
|
||||
import { IOneTimeKey } from "./dehydration";
|
||||
import { MatrixClient } from "../client";
|
||||
|
||||
enum Algorithm {
|
||||
@@ -407,7 +407,7 @@ export async function ensureOlmSessionsForDevices(
|
||||
|
||||
async function _verifyKeyAndStartSession(
|
||||
olmDevice: OlmDevice,
|
||||
oneTimeKey: OneTimeKey,
|
||||
oneTimeKey: IOneTimeKey,
|
||||
userId: string,
|
||||
deviceInfo: DeviceInfo,
|
||||
): Promise<string> {
|
||||
@@ -465,7 +465,7 @@ export interface IObject {
|
||||
*/
|
||||
export async function verifySignature(
|
||||
olmDevice: OlmDevice,
|
||||
obj: OneTimeKey | IObject,
|
||||
obj: IOneTimeKey | IObject,
|
||||
signingUserId: string,
|
||||
signingDeviceId: string,
|
||||
signingKey: string,
|
||||
|
||||
@@ -15,9 +15,9 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "./client";
|
||||
import { MatrixEvent } from "./models/event";
|
||||
import { IEvent, MatrixEvent } from "./models/event";
|
||||
|
||||
export type EventMapper = (obj: any) => MatrixEvent;
|
||||
export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
|
||||
|
||||
export interface MapperOpts {
|
||||
preventReEmit?: boolean;
|
||||
@@ -28,7 +28,7 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
||||
const preventReEmit = Boolean(options.preventReEmit);
|
||||
const decrypt = options.decrypt !== false;
|
||||
|
||||
function mapper(plainOldJsObject) {
|
||||
function mapper(plainOldJsObject: Partial<IEvent>) {
|
||||
const event = new MatrixEvent(plainOldJsObject);
|
||||
if (event.isEncrypted()) {
|
||||
if (!preventReEmit) {
|
||||
|
||||
@@ -39,7 +39,7 @@ function setProp(obj: object, keyNesting: string, val: any) {
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IFilterDefinition {
|
||||
export interface IFilterDefinition {
|
||||
event_fields?: string[];
|
||||
event_format?: "client" | "federation";
|
||||
presence?: IFilterComponent;
|
||||
@@ -47,7 +47,7 @@ interface IFilterDefinition {
|
||||
room?: IRoomFilter;
|
||||
}
|
||||
|
||||
interface IRoomEventFilter extends IFilterComponent {
|
||||
export interface IRoomEventFilter extends IFilterComponent {
|
||||
lazy_load_members?: boolean;
|
||||
include_redundant_members?: boolean;
|
||||
}
|
||||
@@ -86,7 +86,7 @@ export class Filter {
|
||||
* @param {Object} jsonObj
|
||||
* @return {Filter}
|
||||
*/
|
||||
static fromJson(userId: string, filterId: string, jsonObj: IFilterDefinition): Filter {
|
||||
public static fromJson(userId: string, filterId: string, jsonObj: IFilterDefinition): Filter {
|
||||
const filter = new Filter(userId, filterId);
|
||||
filter.setDefinition(jsonObj);
|
||||
return filter;
|
||||
|
||||
@@ -121,7 +121,7 @@ MatrixHttpApi.prototype = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Upload content to the Home Server
|
||||
* Upload content to the homeserver
|
||||
*
|
||||
* @param {object} file The object to upload. On a browser, something that
|
||||
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
|
||||
@@ -393,7 +393,7 @@ MatrixHttpApi.prototype = {
|
||||
accessToken,
|
||||
) {
|
||||
if (!this.opts.idBaseUrl) {
|
||||
throw new Error("No Identity Server base URL set");
|
||||
throw new Error("No identity server base URL set");
|
||||
}
|
||||
|
||||
const fullUri = this.opts.idBaseUrl + prefix + path;
|
||||
|
||||
@@ -18,7 +18,6 @@ limitations under the License.
|
||||
|
||||
/** @module interactive-auth */
|
||||
|
||||
import url from "url";
|
||||
import * as utils from "./utils";
|
||||
import { logger } from './logger';
|
||||
|
||||
@@ -187,9 +186,7 @@ InteractiveAuth.prototype = {
|
||||
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 = {
|
||||
@@ -217,7 +214,7 @@ InteractiveAuth.prototype = {
|
||||
|
||||
/**
|
||||
* get the client secret used for validation sessions
|
||||
* with the ID server.
|
||||
* with the identity server.
|
||||
*
|
||||
* @return {string} client secret
|
||||
*/
|
||||
|
||||
@@ -70,8 +70,8 @@ export class MSC3089Branch {
|
||||
* @param {string} name The new name for this file.
|
||||
* @returns {Promise<void>} Resolves when complete.
|
||||
*/
|
||||
public setName(name: string): Promise<void> {
|
||||
return this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, {
|
||||
public async setName(name: string): Promise<void> {
|
||||
await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, {
|
||||
...this.indexEvent.getContent(),
|
||||
name: name,
|
||||
}, this.id);
|
||||
|
||||
@@ -111,8 +111,8 @@ export class MSC3089TreeSpace {
|
||||
* @param {string} name The new name for the space.
|
||||
* @returns {Promise<void>} Resolves when complete.
|
||||
*/
|
||||
public setName(name: string): Promise<void> {
|
||||
return this.client.sendStateEvent(this.roomId, EventType.RoomName, { name }, "");
|
||||
public async setName(name: string): Promise<void> {
|
||||
await this.client.sendStateEvent(this.roomId, EventType.RoomName, { name }, "");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,7 +190,7 @@ export class MSC3089TreeSpace {
|
||||
}
|
||||
pls['users'] = users;
|
||||
|
||||
return this.client.sendStateEvent(this.roomId, EventType.RoomPowerLevels, pls, "");
|
||||
await this.client.sendStateEvent(this.roomId, EventType.RoomPowerLevels, pls, "");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -262,6 +262,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
|
||||
@@ -1232,20 +1242,7 @@ export class MatrixEvent extends EventEmitter {
|
||||
* @return {Object}
|
||||
*/
|
||||
public toJSON(): object {
|
||||
const event: any = {
|
||||
type: this.getType(),
|
||||
sender: this.getSender(),
|
||||
content: this.getContent(),
|
||||
event_id: this.getId(),
|
||||
origin_server_ts: this.getTs(),
|
||||
unsigned: this.getUnsigned(),
|
||||
room_id: this.getRoomId(),
|
||||
};
|
||||
|
||||
// if this is a redaction then attach the redacts key
|
||||
if (this.isRedaction()) {
|
||||
event.redacts = this.event.redacts;
|
||||
}
|
||||
const event = this.getEffectiveEvent();
|
||||
|
||||
if (!this.isEncrypted()) {
|
||||
return event;
|
||||
|
||||
@@ -25,13 +25,13 @@ import { EventTimeline } from "./event-timeline";
|
||||
import { getHttpUriForMxc } from "../content-repo";
|
||||
import * as utils from "../utils";
|
||||
import { normalize } from "../utils";
|
||||
import { EventStatus, MatrixEvent } from "./event";
|
||||
import { EventStatus, IEvent, MatrixEvent } from "./event";
|
||||
import { RoomMember } from "./room-member";
|
||||
import { IRoomSummary, RoomSummary } from "./room-summary";
|
||||
import { logger } from '../logger';
|
||||
import { ReEmitter } from '../ReEmitter';
|
||||
import { EventType, RoomCreateTypeField, RoomType } from "../@types/event";
|
||||
import { IRoomVersionsCapability, MatrixClient, RoomVersionStability } from "../client";
|
||||
import { EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../@types/event";
|
||||
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
|
||||
import { ResizeMethod } from "../@types/partials";
|
||||
import { Filter } from "../filter";
|
||||
import { RoomState } from "./room-state";
|
||||
@@ -64,7 +64,7 @@ function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: stri
|
||||
|
||||
interface IOpts {
|
||||
storageToken?: string;
|
||||
pendingEventOrdering?: "chronological" | "detached";
|
||||
pendingEventOrdering?: PendingEventOrdering;
|
||||
timelineSupport?: boolean;
|
||||
unstableClientRelationAggregation?: boolean;
|
||||
lazyLoadMembers?: boolean;
|
||||
@@ -218,7 +218,7 @@ export class Room extends EventEmitter {
|
||||
this.setMaxListeners(100);
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
|
||||
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
|
||||
opts.pendingEventOrdering = opts.pendingEventOrdering || PendingEventOrdering.Chronological;
|
||||
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
|
||||
throw new Error(
|
||||
"opts.pendingEventOrdering MUST be either 'chronological' or " +
|
||||
@@ -649,7 +649,7 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private async loadMembersFromServer(): Promise<object[]> {
|
||||
private async loadMembersFromServer(): Promise<IEvent[]> {
|
||||
const lastSyncToken = this.client.store.getSyncToken();
|
||||
const queryString = utils.encodeParams({
|
||||
not_membership: "leave",
|
||||
@@ -665,8 +665,7 @@ export class Room extends EventEmitter {
|
||||
private async loadMembers(): Promise<{ memberEvents: MatrixEvent[], fromServer: boolean }> {
|
||||
// were the members loaded from the server?
|
||||
let fromServer = false;
|
||||
let rawMembersEvents =
|
||||
await this.client.store.getOutOfBandMembers(this.roomId);
|
||||
let rawMembersEvents = await this.client.store.getOutOfBandMembers(this.roomId);
|
||||
if (rawMembersEvents === null) {
|
||||
fromServer = true;
|
||||
rawMembersEvents = await this.loadMembersFromServer();
|
||||
@@ -713,7 +712,7 @@ export class Room extends EventEmitter {
|
||||
if (fromServer) {
|
||||
const oobMembers = this.currentState.getMembers()
|
||||
.filter((m) => m.isOutOfBand())
|
||||
.map((m) => m.events.member.event);
|
||||
.map((m) => m.events.member.event as IEvent);
|
||||
logger.log(`LL: telling store to write ${oobMembers.length}`
|
||||
+ ` members for room ${this.roomId}`);
|
||||
const store = this.client.store;
|
||||
@@ -2037,24 +2036,45 @@ export class Room extends EventEmitter {
|
||||
const joinedMemberCount = this.currentState.getJoinedMemberCount();
|
||||
const invitedMemberCount = this.currentState.getInvitedMemberCount();
|
||||
// -1 because these numbers include the syncing user
|
||||
const inviteJoinCount = joinedMemberCount + invitedMemberCount - 1;
|
||||
let inviteJoinCount = joinedMemberCount + invitedMemberCount - 1;
|
||||
|
||||
// get service members (e.g. helper bots) for exclusion
|
||||
let excludedUserIds: string[] = [];
|
||||
const mFunctionalMembers = this.currentState.getStateEvents(UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, "");
|
||||
if (Array.isArray(mFunctionalMembers?.getContent().service_members)) {
|
||||
excludedUserIds = mFunctionalMembers.getContent().service_members;
|
||||
}
|
||||
|
||||
// get members that are NOT ourselves and are actually in the room.
|
||||
let otherNames = null;
|
||||
if (this.summaryHeroes) {
|
||||
// if we have a summary, the member state events
|
||||
// should be in the room state
|
||||
otherNames = this.summaryHeroes.map((userId) => {
|
||||
otherNames = [];
|
||||
this.summaryHeroes.forEach((userId) => {
|
||||
// filter service members
|
||||
if (excludedUserIds.includes(userId)) {
|
||||
inviteJoinCount--;
|
||||
return;
|
||||
}
|
||||
const member = this.getMember(userId);
|
||||
return member ? member.name : userId;
|
||||
otherNames.push(member ? member.name : userId);
|
||||
});
|
||||
} else {
|
||||
let otherMembers = this.currentState.getMembers().filter((m) => {
|
||||
return m.userId !== userId &&
|
||||
(m.membership === "invite" || m.membership === "join");
|
||||
});
|
||||
otherMembers = otherMembers.filter(({ userId }) => {
|
||||
// filter service members
|
||||
if (excludedUserIds.includes(userId)) {
|
||||
inviteJoinCount--;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// make sure members have stable order
|
||||
otherMembers.sort((a, b) => a.userId.localeCompare(b.userId));
|
||||
otherMembers.sort((a, b) => utils.compare(a.userId, b.userId));
|
||||
// only 5 first members, immitate summaryHeroes
|
||||
otherMembers = otherMembers.slice(0, 5);
|
||||
otherNames = otherMembers.map((m) => m.name);
|
||||
@@ -2065,7 +2085,7 @@ export class Room extends EventEmitter {
|
||||
}
|
||||
|
||||
const myMembership = this.getMyMembership();
|
||||
// if I have created a room and invited people throuh
|
||||
// if I have created a room and invited people through
|
||||
// 3rd party invites
|
||||
if (myMembership == 'join') {
|
||||
const thirdPartyInvites =
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module models/search-result
|
||||
*/
|
||||
|
||||
import { EventContext } from "./event-context";
|
||||
|
||||
/**
|
||||
* Construct a new SearchResult
|
||||
*
|
||||
* @param {number} rank where this SearchResult ranks in the results
|
||||
* @param {event-context.EventContext} eventContext the matching event and its
|
||||
* context
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
export function SearchResult(rank, eventContext) {
|
||||
this.rank = rank;
|
||||
this.context = eventContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a SearchResponse from the response to /search
|
||||
* @static
|
||||
* @param {Object} jsonObj
|
||||
* @param {function} eventMapper
|
||||
* @return {SearchResult}
|
||||
*/
|
||||
|
||||
SearchResult.fromJson = function(jsonObj, eventMapper) {
|
||||
const jsonContext = jsonObj.context || {};
|
||||
const events_before = jsonContext.events_before || [];
|
||||
const events_after = jsonContext.events_after || [];
|
||||
|
||||
const context = new EventContext(eventMapper(jsonObj.result));
|
||||
|
||||
context.setPaginateToken(jsonContext.start, true);
|
||||
context.addEvents(events_before.map(eventMapper), true);
|
||||
context.addEvents(events_after.map(eventMapper), false);
|
||||
context.setPaginateToken(jsonContext.end, false);
|
||||
|
||||
return new SearchResult(jsonObj.rank, context);
|
||||
};
|
||||
|
||||
60
src/models/search-result.ts
Normal file
60
src/models/search-result.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright 2015 - 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module models/search-result
|
||||
*/
|
||||
|
||||
import { EventContext } from "./event-context";
|
||||
import { EventMapper } from "../event-mapper";
|
||||
import { IResultContext, ISearchResult } from "../@types/search";
|
||||
|
||||
export class SearchResult {
|
||||
/**
|
||||
* Create a SearchResponse from the response to /search
|
||||
* @static
|
||||
* @param {Object} jsonObj
|
||||
* @param {function} eventMapper
|
||||
* @return {SearchResult}
|
||||
*/
|
||||
|
||||
static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult {
|
||||
const jsonContext = jsonObj.context || {} as IResultContext;
|
||||
const eventsBefore = jsonContext.events_before || [];
|
||||
const eventsAfter = jsonContext.events_after || [];
|
||||
|
||||
const context = new EventContext(eventMapper(jsonObj.result));
|
||||
|
||||
context.setPaginateToken(jsonContext.start, true);
|
||||
context.addEvents(eventsBefore.map(eventMapper), true);
|
||||
context.addEvents(eventsAfter.map(eventMapper), false);
|
||||
context.setPaginateToken(jsonContext.end, false);
|
||||
|
||||
return new SearchResult(jsonObj.rank, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new SearchResult
|
||||
*
|
||||
* @param {number} rank where this SearchResult ranks in the results
|
||||
* @param {event-context.EventContext} context the matching event and its
|
||||
* context
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
constructor(public readonly rank: number, public readonly context: EventContext) {}
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ export class User extends EventEmitter {
|
||||
* @param {MatrixEvent} event The <code>im.vector.user_status</code> event.
|
||||
* @fires module:client~MatrixClient#event:"User.unstable_statusMessage"
|
||||
*/
|
||||
// eslint-disable-next-line camelcase
|
||||
// eslint-disable-next-line
|
||||
public unstable_updateStatusMessage(event: MatrixEvent): void {
|
||||
if (!event.getContent()) this.unstable_statusMessage = "";
|
||||
else this.unstable_statusMessage = event.getContent()["status"];
|
||||
|
||||
@@ -15,6 +15,6 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
export enum SERVICE_TYPES {
|
||||
IS = 'SERVICE_TYPE_IS', // An Identity Service
|
||||
IM = 'SERVICE_TYPE_IM', // An Integration Manager
|
||||
IS = 'SERVICE_TYPE_IS', // An identity server
|
||||
IM = 'SERVICE_TYPE_IM', // An integration manager
|
||||
}
|
||||
|
||||
@@ -18,10 +18,11 @@ import { EventType } from "../@types/event";
|
||||
import { Group } from "../models/group";
|
||||
import { Room } from "../models/room";
|
||||
import { User } from "../models/user";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { IEvent, MatrixEvent } from "../models/event";
|
||||
import { Filter } from "../filter";
|
||||
import { RoomSummary } from "../models/room-summary";
|
||||
import { IMinimalEvent, IGroups, IRooms } from "../sync-accumulator";
|
||||
import { IMinimalEvent, IGroups, IRooms, ISyncResponse } from "../sync-accumulator";
|
||||
import { IStartClientOpts } from "../client";
|
||||
|
||||
export interface ISavedSync {
|
||||
nextBatch: string;
|
||||
@@ -35,6 +36,8 @@ export interface ISavedSync {
|
||||
* @constructor
|
||||
*/
|
||||
export interface IStore {
|
||||
readonly accountData: Record<string, MatrixEvent>; // type : content
|
||||
|
||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
||||
isNewlyCreated(): Promise<boolean>;
|
||||
|
||||
@@ -182,7 +185,7 @@ export interface IStore {
|
||||
* @param {Object} syncData The sync data
|
||||
* @return {Promise} An immediately resolved promise.
|
||||
*/
|
||||
setSyncData(syncData: object): Promise<void>;
|
||||
setSyncData(syncData: ISyncResponse): Promise<void>;
|
||||
|
||||
/**
|
||||
* We never want to save because we have nothing to save to.
|
||||
@@ -194,7 +197,7 @@ export interface IStore {
|
||||
/**
|
||||
* Save does nothing as there is no backing data store.
|
||||
*/
|
||||
save(force: boolean): void;
|
||||
save(force?: boolean): void;
|
||||
|
||||
/**
|
||||
* Startup does nothing.
|
||||
@@ -222,13 +225,13 @@ export interface IStore {
|
||||
*/
|
||||
deleteAllData(): Promise<void>;
|
||||
|
||||
getOutOfBandMembers(roomId: string): Promise<MatrixEvent[] | null>;
|
||||
getOutOfBandMembers(roomId: string): Promise<IEvent[] | null>;
|
||||
|
||||
setOutOfBandMembers(roomId: string, membershipEvents: MatrixEvent[]): Promise<void>;
|
||||
setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void>;
|
||||
|
||||
clearOutOfBandMembers(roomId: string): Promise<void>;
|
||||
|
||||
getClientOptions(): Promise<object>;
|
||||
getClientOptions(): Promise<IStartClientOpts>;
|
||||
|
||||
storeClientOptions(options: object): Promise<void>;
|
||||
storeClientOptions(options: IStartClientOpts): Promise<void>;
|
||||
}
|
||||
|
||||
36
src/store/indexeddb-backend.ts
Normal file
36
src/store/indexeddb-backend.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ISavedSync } from "./index";
|
||||
import { IEvent, IStartClientOpts, ISyncResponse } from "..";
|
||||
|
||||
export interface IIndexedDBBackend {
|
||||
connect(): Promise<void>;
|
||||
syncToDatabase(userTuples: UserTuple[]): Promise<void>;
|
||||
isNewlyCreated(): Promise<boolean>;
|
||||
setSyncData(syncData: ISyncResponse): Promise<void>;
|
||||
getSavedSync(): Promise<ISavedSync>;
|
||||
getNextBatchToken(): Promise<string>;
|
||||
clearDatabase(): Promise<void>;
|
||||
getOutOfBandMembers(roomId: string): Promise<IEvent[] | null>;
|
||||
setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void>;
|
||||
clearOutOfBandMembers(roomId: string): Promise<void>;
|
||||
getUserPresenceEvents(): Promise<UserTuple[]>;
|
||||
getClientOptions(): Promise<IStartClientOpts>;
|
||||
storeClientOptions(options: IStartClientOpts): Promise<void>;
|
||||
}
|
||||
|
||||
export type UserTuple = [userId: string, presenceEvent: Partial<IEvent>];
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -16,14 +14,17 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { SyncAccumulator } from "../sync-accumulator";
|
||||
import { IMinimalEvent, ISyncData, ISyncResponse, SyncAccumulator } from "../sync-accumulator";
|
||||
import * as utils from "../utils";
|
||||
import * as IndexedDBHelpers from "../indexeddb-helpers";
|
||||
import { logger } from '../logger';
|
||||
import { IEvent, IStartClientOpts } from "..";
|
||||
import { ISavedSync } from "./index";
|
||||
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
|
||||
|
||||
const VERSION = 3;
|
||||
|
||||
function createDatabase(db) {
|
||||
function createDatabase(db: IDBDatabase): void {
|
||||
// Make user store, clobber based on user ID. (userId property of User objects)
|
||||
db.createObjectStore("users", { keyPath: ["userId"] });
|
||||
|
||||
@@ -35,7 +36,7 @@ function createDatabase(db) {
|
||||
db.createObjectStore("sync", { keyPath: ["clobber"] });
|
||||
}
|
||||
|
||||
function upgradeSchemaV2(db) {
|
||||
function upgradeSchemaV2(db: IDBDatabase): void {
|
||||
const oobMembersStore = db.createObjectStore(
|
||||
"oob_membership_events", {
|
||||
keyPath: ["room_id", "state_key"],
|
||||
@@ -43,7 +44,7 @@ function upgradeSchemaV2(db) {
|
||||
oobMembersStore.createIndex("room", "room_id");
|
||||
}
|
||||
|
||||
function upgradeSchemaV3(db) {
|
||||
function upgradeSchemaV3(db: IDBDatabase): void {
|
||||
db.createObjectStore("client_options",
|
||||
{ keyPath: ["clobber"] });
|
||||
}
|
||||
@@ -58,16 +59,20 @@ function upgradeSchemaV3(db) {
|
||||
* @return {Promise<T[]>} Resolves to an array of whatever you returned from
|
||||
* resultMapper.
|
||||
*/
|
||||
function selectQuery(store, keyRange, resultMapper) {
|
||||
function selectQuery<T>(
|
||||
store: IDBObjectStore,
|
||||
keyRange: IDBKeyRange | IDBValidKey | undefined,
|
||||
resultMapper: (cursor: IDBCursorWithValue) => T,
|
||||
): Promise<T[]> {
|
||||
const query = store.openCursor(keyRange);
|
||||
return new Promise((resolve, reject) => {
|
||||
const results = [];
|
||||
query.onerror = (event) => {
|
||||
reject(new Error("Query failed: " + event.target.errorCode));
|
||||
query.onerror = () => {
|
||||
reject(new Error("Query failed: " + query.error));
|
||||
};
|
||||
// collect results
|
||||
query.onsuccess = (event) => {
|
||||
const cursor = event.target.result;
|
||||
query.onsuccess = () => {
|
||||
const cursor = query.result;
|
||||
if (!cursor) {
|
||||
resolve(results);
|
||||
return; // end of results
|
||||
@@ -78,88 +83,84 @@ function selectQuery(store, keyRange, resultMapper) {
|
||||
});
|
||||
}
|
||||
|
||||
function txnAsPromise(txn) {
|
||||
function txnAsPromise(txn: IDBTransaction): Promise<Event> {
|
||||
return new Promise((resolve, reject) => {
|
||||
txn.oncomplete = function(event) {
|
||||
resolve(event);
|
||||
};
|
||||
txn.onerror = function(event) {
|
||||
reject(event.target.error);
|
||||
txn.onerror = function() {
|
||||
reject(txn.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function reqAsEventPromise(req) {
|
||||
function reqAsEventPromise(req: IDBRequest): Promise<Event> {
|
||||
return new Promise((resolve, reject) => {
|
||||
req.onsuccess = function(event) {
|
||||
resolve(event);
|
||||
};
|
||||
req.onerror = function(event) {
|
||||
reject(event.target.error);
|
||||
req.onerror = function() {
|
||||
reject(req.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function reqAsPromise(req) {
|
||||
function reqAsPromise(req: IDBRequest): Promise<IDBRequest> {
|
||||
return new Promise((resolve, reject) => {
|
||||
req.onsuccess = () => resolve(req);
|
||||
req.onerror = (err) => reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
function reqAsCursorPromise(req) {
|
||||
return reqAsEventPromise(req).then((event) => event.target.result);
|
||||
function reqAsCursorPromise(req: IDBRequest<IDBCursor | null>): Promise<IDBCursor> {
|
||||
return reqAsEventPromise(req).then((event) => req.result);
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from '../logger';
|
||||
import { defer } from '../utils';
|
||||
|
||||
/**
|
||||
* An IndexedDB store backend where the actual backend sits in a web
|
||||
* worker.
|
||||
*
|
||||
* Construct a new Indexed Database store backend. This requires a call to
|
||||
* <code>connect()</code> before this store can be used.
|
||||
* @constructor
|
||||
* @param {string} workerScript URL to the worker script
|
||||
* @param {string=} dbName Optional database name. The same name must be used
|
||||
* to open the same database.
|
||||
* @param {Object} workerApi The web worker compatible interface object
|
||||
*/
|
||||
export function RemoteIndexedDBStoreBackend(
|
||||
workerScript, dbName, workerApi,
|
||||
) {
|
||||
this._workerScript = workerScript;
|
||||
this._dbName = dbName;
|
||||
this._workerApi = workerApi;
|
||||
this._worker = null;
|
||||
this._nextSeq = 0;
|
||||
// The currently in-flight requests to the actual backend
|
||||
this._inFlight = {
|
||||
// seq: promise,
|
||||
};
|
||||
// Once we start connecting, we keep the promise and re-use it
|
||||
// if we try to connect again
|
||||
this._startPromise = null;
|
||||
}
|
||||
|
||||
RemoteIndexedDBStoreBackend.prototype = {
|
||||
/**
|
||||
* Attempt to connect to the database. This can fail if the user does not
|
||||
* grant permission.
|
||||
* @return {Promise} Resolves if successfully connected.
|
||||
*/
|
||||
connect: function() {
|
||||
return this._ensureStarted().then(() => this._doCmd('connect'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the entire database. This should be used when logging out of a client
|
||||
* to prevent mixing data between accounts.
|
||||
* @return {Promise} Resolved when the database is cleared.
|
||||
*/
|
||||
clearDatabase: function() {
|
||||
return this._ensureStarted().then(() => this._doCmd('clearDatabase'));
|
||||
},
|
||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
||||
isNewlyCreated: function() {
|
||||
return this._doCmd('isNewlyCreated');
|
||||
},
|
||||
/**
|
||||
* @return {Promise} Resolves with a sync response to restore the
|
||||
* client state to where it was at the last save, or null if there
|
||||
* is no saved sync data.
|
||||
*/
|
||||
getSavedSync: function() {
|
||||
return this._doCmd('getSavedSync');
|
||||
},
|
||||
|
||||
getNextBatchToken: function() {
|
||||
return this._doCmd('getNextBatchToken');
|
||||
},
|
||||
|
||||
setSyncData: function(syncData) {
|
||||
return this._doCmd('setSyncData', [syncData]);
|
||||
},
|
||||
|
||||
syncToDatabase: function(users) {
|
||||
return this._doCmd('syncToDatabase', [users]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the out-of-band membership events for this room that
|
||||
* were previously loaded.
|
||||
* @param {string} roomId
|
||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||
* @returns {null} in case the members for this room haven't been stored yet
|
||||
*/
|
||||
getOutOfBandMembers: function(roomId) {
|
||||
return this._doCmd('getOutOfBandMembers', [roomId]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores the out-of-band membership events for this room. Note that
|
||||
* it still makes sense to store an empty array as the OOB status for the room is
|
||||
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
|
||||
* @param {string} roomId
|
||||
* @param {event[]} membershipEvents the membership events to store
|
||||
* @returns {Promise} when all members have been stored
|
||||
*/
|
||||
setOutOfBandMembers: function(roomId, membershipEvents) {
|
||||
return this._doCmd('setOutOfBandMembers', [roomId, membershipEvents]);
|
||||
},
|
||||
|
||||
clearOutOfBandMembers: function(roomId) {
|
||||
return this._doCmd('clearOutOfBandMembers', [roomId]);
|
||||
},
|
||||
|
||||
getClientOptions: function() {
|
||||
return this._doCmd('getClientOptions');
|
||||
},
|
||||
|
||||
storeClientOptions: function(options) {
|
||||
return this._doCmd('storeClientOptions', [options]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all user presence events from the database. This is not cached.
|
||||
* @return {Promise<Object[]>} A list of presence events in their raw form.
|
||||
*/
|
||||
getUserPresenceEvents: function() {
|
||||
return this._doCmd('getUserPresenceEvents');
|
||||
},
|
||||
|
||||
_ensureStarted: function() {
|
||||
if (this._startPromise === null) {
|
||||
this._worker = new this._workerApi(this._workerScript);
|
||||
this._worker.onmessage = this._onWorkerMessage.bind(this);
|
||||
|
||||
// tell the worker the db name.
|
||||
this._startPromise = this._doCmd('_setupWorker', [this._dbName]).then(() => {
|
||||
logger.log("IndexedDB worker is ready");
|
||||
});
|
||||
}
|
||||
return this._startPromise;
|
||||
},
|
||||
|
||||
_doCmd: function(cmd, args) {
|
||||
// wrap in a q so if the postMessage throws,
|
||||
// the promise automatically gets rejected
|
||||
return Promise.resolve().then(() => {
|
||||
const seq = this._nextSeq++;
|
||||
const def = defer();
|
||||
|
||||
this._inFlight[seq] = def;
|
||||
|
||||
this._worker.postMessage({
|
||||
command: cmd,
|
||||
seq: seq,
|
||||
args: args,
|
||||
});
|
||||
|
||||
return def.promise;
|
||||
});
|
||||
},
|
||||
|
||||
_onWorkerMessage: function(ev) {
|
||||
const msg = ev.data;
|
||||
|
||||
if (msg.command == 'cmd_success' || msg.command == 'cmd_fail') {
|
||||
if (msg.seq === undefined) {
|
||||
logger.error("Got reply from worker with no seq");
|
||||
return;
|
||||
}
|
||||
|
||||
const def = this._inFlight[msg.seq];
|
||||
if (def === undefined) {
|
||||
logger.error("Got reply for unknown seq " + msg.seq);
|
||||
return;
|
||||
}
|
||||
delete this._inFlight[msg.seq];
|
||||
|
||||
if (msg.command == 'cmd_success') {
|
||||
def.resolve(msg.result);
|
||||
} else {
|
||||
const error = new Error(msg.error.message);
|
||||
error.name = msg.error.name;
|
||||
def.reject(error);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Unrecognised message from worker: " + msg);
|
||||
}
|
||||
},
|
||||
};
|
||||
192
src/store/indexeddb-remote-backend.ts
Normal file
192
src/store/indexeddb-remote-backend.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { defer, IDeferred } from "../utils";
|
||||
import { ISavedSync } from "./index";
|
||||
import { IStartClientOpts } from "../client";
|
||||
import { IEvent, ISyncResponse } from "..";
|
||||
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
|
||||
|
||||
export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||
private worker: Worker;
|
||||
private nextSeq = 0;
|
||||
// The currently in-flight requests to the actual backend
|
||||
private inFlight: Record<number, IDeferred<any>> = {}; // seq: promise
|
||||
// Once we start connecting, we keep the promise and re-use it
|
||||
// if we try to connect again
|
||||
private startPromise: Promise<void> = null;
|
||||
|
||||
/**
|
||||
* An IndexedDB store backend where the actual backend sits in a web
|
||||
* worker.
|
||||
*
|
||||
* Construct a new Indexed Database store backend. This requires a call to
|
||||
* <code>connect()</code> before this store can be used.
|
||||
* @constructor
|
||||
* @param {Function} workerFactory Factory which produces a Worker
|
||||
* @param {string=} dbName Optional database name. The same name must be used
|
||||
* to open the same database.
|
||||
*/
|
||||
constructor(
|
||||
private readonly workerFactory: () => Worker,
|
||||
private readonly dbName: string,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Attempt to connect to the database. This can fail if the user does not
|
||||
* grant permission.
|
||||
* @return {Promise} Resolves if successfully connected.
|
||||
*/
|
||||
public connect(): Promise<void> {
|
||||
return this.ensureStarted().then(() => this.doCmd('connect'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the entire database. This should be used when logging out of a client
|
||||
* to prevent mixing data between accounts.
|
||||
* @return {Promise} Resolved when the database is cleared.
|
||||
*/
|
||||
public clearDatabase(): Promise<void> {
|
||||
return this.ensureStarted().then(() => this.doCmd('clearDatabase'));
|
||||
}
|
||||
|
||||
/** @return {Promise<boolean>} whether or not the database was newly created in this session. */
|
||||
public isNewlyCreated(): Promise<boolean> {
|
||||
return this.doCmd('isNewlyCreated');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Promise} Resolves with a sync response to restore the
|
||||
* client state to where it was at the last save, or null if there
|
||||
* is no saved sync data.
|
||||
*/
|
||||
public getSavedSync(): Promise<ISavedSync> {
|
||||
return this.doCmd('getSavedSync');
|
||||
}
|
||||
|
||||
public getNextBatchToken(): Promise<string> {
|
||||
return this.doCmd('getNextBatchToken');
|
||||
}
|
||||
|
||||
public setSyncData(syncData: ISyncResponse): Promise<void> {
|
||||
return this.doCmd('setSyncData', [syncData]);
|
||||
}
|
||||
|
||||
public syncToDatabase(userTuples: UserTuple[]): Promise<void> {
|
||||
return this.doCmd('syncToDatabase', [userTuples]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the out-of-band membership events for this room that
|
||||
* were previously loaded.
|
||||
* @param {string} roomId
|
||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||
* @returns {null} in case the members for this room haven't been stored yet
|
||||
*/
|
||||
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
|
||||
return this.doCmd('getOutOfBandMembers', [roomId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the out-of-band membership events for this room. Note that
|
||||
* it still makes sense to store an empty array as the OOB status for the room is
|
||||
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
|
||||
* @param {string} roomId
|
||||
* @param {event[]} membershipEvents the membership events to store
|
||||
* @returns {Promise} when all members have been stored
|
||||
*/
|
||||
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
||||
return this.doCmd('setOutOfBandMembers', [roomId, membershipEvents]);
|
||||
}
|
||||
|
||||
public clearOutOfBandMembers(roomId: string): Promise<void> {
|
||||
return this.doCmd('clearOutOfBandMembers', [roomId]);
|
||||
}
|
||||
|
||||
public getClientOptions(): Promise<IStartClientOpts> {
|
||||
return this.doCmd('getClientOptions');
|
||||
}
|
||||
|
||||
public storeClientOptions(options: IStartClientOpts): Promise<void> {
|
||||
return this.doCmd('storeClientOptions', [options]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all user presence events from the database. This is not cached.
|
||||
* @return {Promise<Object[]>} A list of presence events in their raw form.
|
||||
*/
|
||||
public getUserPresenceEvents(): Promise<UserTuple[]> {
|
||||
return this.doCmd('getUserPresenceEvents');
|
||||
}
|
||||
|
||||
private ensureStarted(): Promise<void> {
|
||||
if (this.startPromise === null) {
|
||||
this.worker = this.workerFactory();
|
||||
this.worker.onmessage = this.onWorkerMessage;
|
||||
|
||||
// tell the worker the db name.
|
||||
this.startPromise = this.doCmd('_setupWorker', [this.dbName]).then(() => {
|
||||
logger.log("IndexedDB worker is ready");
|
||||
});
|
||||
}
|
||||
return this.startPromise;
|
||||
}
|
||||
|
||||
private doCmd<T>(command: string, args?: any): Promise<T> {
|
||||
// wrap in a q so if the postMessage throws,
|
||||
// the promise automatically gets rejected
|
||||
return Promise.resolve().then(() => {
|
||||
const seq = this.nextSeq++;
|
||||
const def = defer<T>();
|
||||
|
||||
this.inFlight[seq] = def;
|
||||
|
||||
this.worker.postMessage({ command, seq, args });
|
||||
|
||||
return def.promise;
|
||||
});
|
||||
}
|
||||
|
||||
private onWorkerMessage = (ev: MessageEvent): void => {
|
||||
const msg = ev.data;
|
||||
|
||||
if (msg.command == 'cmd_success' || msg.command == 'cmd_fail') {
|
||||
if (msg.seq === undefined) {
|
||||
logger.error("Got reply from worker with no seq");
|
||||
return;
|
||||
}
|
||||
|
||||
const def = this.inFlight[msg.seq];
|
||||
if (def === undefined) {
|
||||
logger.error("Got reply for unknown seq " + msg.seq);
|
||||
return;
|
||||
}
|
||||
delete this.inFlight[msg.seq];
|
||||
|
||||
if (msg.command == 'cmd_success') {
|
||||
def.resolve(msg.result);
|
||||
} else {
|
||||
const error = new Error(msg.error.message);
|
||||
error.name = msg.error.name;
|
||||
def.reject(error);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Unrecognised message from worker: ", msg);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -16,9 +14,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend.js";
|
||||
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend";
|
||||
import { logger } from '../logger';
|
||||
|
||||
interface ICmd {
|
||||
command: string;
|
||||
seq: number;
|
||||
args?: any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* This class lives in the webworker and drives a LocalIndexedDBStoreBackend
|
||||
* controlled by messages from the main process.
|
||||
@@ -35,16 +39,13 @@ import { logger } from '../logger';
|
||||
*
|
||||
*/
|
||||
export class IndexedDBStoreWorker {
|
||||
private backend: LocalIndexedDBStoreBackend = null;
|
||||
|
||||
/**
|
||||
* @param {function} postMessage The web worker postMessage function that
|
||||
* should be used to communicate back to the main script.
|
||||
*/
|
||||
constructor(postMessage) {
|
||||
this.backend = null;
|
||||
this.postMessage = postMessage;
|
||||
|
||||
this.onMessage = this.onMessage.bind(this);
|
||||
}
|
||||
constructor(private readonly postMessage: InstanceType<typeof Worker>["postMessage"]) {}
|
||||
|
||||
/**
|
||||
* Passes a message event from the main script into the class. This method
|
||||
@@ -52,17 +53,15 @@ export class IndexedDBStoreWorker {
|
||||
*
|
||||
* @param {Object} ev The message event
|
||||
*/
|
||||
onMessage(ev) {
|
||||
const msg = ev.data;
|
||||
public onMessage = (ev: MessageEvent): void => {
|
||||
const msg: ICmd = ev.data;
|
||||
let prom;
|
||||
|
||||
switch (msg.command) {
|
||||
case '_setupWorker':
|
||||
this.backend = new LocalIndexedDBStoreBackend(
|
||||
// this is the 'indexedDB' global (where global != window
|
||||
// because it's a web worker and there is no window).
|
||||
indexedDB, msg.args[0],
|
||||
);
|
||||
this.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();
|
||||
@@ -142,5 +134,5 @@ export class IndexedDBStoreWorker {
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -19,12 +19,14 @@ limitations under the License.
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { MemoryStore, IOpts as IBaseOpts } from "./memory";
|
||||
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend.js";
|
||||
import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend.js";
|
||||
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend";
|
||||
import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend";
|
||||
import { User } from "../models/user";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { IEvent, MatrixEvent } from "../models/event";
|
||||
import { logger } from '../logger';
|
||||
import { ISavedSync } from "./index";
|
||||
import { IIndexedDBBackend } from "./indexeddb-backend";
|
||||
import { ISyncResponse } from "../sync-accumulator";
|
||||
|
||||
/**
|
||||
* This is an internal module. See {@link IndexedDBStore} for the public class.
|
||||
@@ -41,8 +43,7 @@ const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
|
||||
interface IOpts extends IBaseOpts {
|
||||
indexedDB: IDBFactory;
|
||||
dbName?: string;
|
||||
workerScript?: string;
|
||||
workerApi?: typeof Worker;
|
||||
workerFactory?: () => Worker;
|
||||
}
|
||||
|
||||
export class IndexedDBStore extends MemoryStore {
|
||||
@@ -50,8 +51,7 @@ export class IndexedDBStore extends MemoryStore {
|
||||
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
|
||||
}
|
||||
|
||||
// TODO these should conform to one interface
|
||||
public readonly backend: LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend;
|
||||
public readonly backend: IIndexedDBBackend;
|
||||
|
||||
private startedUp = false;
|
||||
private syncTs = 0;
|
||||
@@ -110,16 +110,8 @@ export class IndexedDBStore extends MemoryStore {
|
||||
throw new Error('Missing required option: indexedDB');
|
||||
}
|
||||
|
||||
if (opts.workerScript) {
|
||||
// try & find a webworker-compatible API
|
||||
let workerApi = opts.workerApi;
|
||||
if (!workerApi) {
|
||||
// default to the global Worker object (which is where it in a browser)
|
||||
workerApi = global.Worker;
|
||||
}
|
||||
this.backend = new RemoteIndexedDBStoreBackend(
|
||||
opts.workerScript, opts.dbName, workerApi,
|
||||
);
|
||||
if (opts.workerFactory) {
|
||||
this.backend = new RemoteIndexedDBStoreBackend(opts.workerFactory, opts.dbName);
|
||||
} else {
|
||||
this.backend = new LocalIndexedDBStoreBackend(opts.indexedDB, opts.dbName);
|
||||
}
|
||||
@@ -222,7 +214,7 @@ export class IndexedDBStore extends MemoryStore {
|
||||
|
||||
// work out changed users (this doesn't handle deletions but you
|
||||
// can't 'delete' users as they are just presence events).
|
||||
const userTuples = [];
|
||||
const userTuples: [userId: string, presenceEvent: Partial<IEvent>][] = [];
|
||||
for (const u of this.getUsers()) {
|
||||
if (this.userModifiedMap[u.userId] === u.getLastModifiedTime()) continue;
|
||||
if (!u.events.presence) continue;
|
||||
@@ -236,7 +228,7 @@ export class IndexedDBStore extends MemoryStore {
|
||||
return this.backend.syncToDatabase(userTuples);
|
||||
});
|
||||
|
||||
public setSyncData = this.degradable((syncData: object): Promise<void> => {
|
||||
public setSyncData = this.degradable((syncData: ISyncResponse): Promise<void> => {
|
||||
return this.backend.setSyncData(syncData);
|
||||
}, "setSyncData");
|
||||
|
||||
@@ -247,7 +239,7 @@ export class IndexedDBStore extends MemoryStore {
|
||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||
* @returns {null} in case the members for this room haven't been stored yet
|
||||
*/
|
||||
public getOutOfBandMembers = this.degradable((roomId: string): Promise<MatrixEvent[]> => {
|
||||
public getOutOfBandMembers = this.degradable((roomId: string): Promise<IEvent[]> => {
|
||||
return this.backend.getOutOfBandMembers(roomId);
|
||||
}, "getOutOfBandMembers");
|
||||
|
||||
@@ -259,7 +251,7 @@ export class IndexedDBStore extends MemoryStore {
|
||||
* @param {event[]} membershipEvents the membership events to store
|
||||
* @returns {Promise} when all members have been stored
|
||||
*/
|
||||
public setOutOfBandMembers = this.degradable((roomId: string, membershipEvents: MatrixEvent[]): Promise<void> => {
|
||||
public setOutOfBandMembers = this.degradable((roomId: string, membershipEvents: IEvent[]): Promise<void> => {
|
||||
super.setOutOfBandMembers(roomId, membershipEvents);
|
||||
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
|
||||
}, "setOutOfBandMembers");
|
||||
|
||||
@@ -23,12 +23,13 @@ import { EventType } from "../@types/event";
|
||||
import { Group } from "../models/group";
|
||||
import { Room } from "../models/room";
|
||||
import { User } from "../models/user";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { IEvent, MatrixEvent } from "../models/event";
|
||||
import { RoomState } from "../models/room-state";
|
||||
import { RoomMember } from "../models/room-member";
|
||||
import { Filter } from "../filter";
|
||||
import { ISavedSync, IStore } from "./index";
|
||||
import { RoomSummary } from "../models/room-summary";
|
||||
import { ISyncResponse } from "../sync-accumulator";
|
||||
|
||||
function isValidFilterId(filterId: string): boolean {
|
||||
const isValidStr = typeof filterId === "string" &&
|
||||
@@ -59,9 +60,9 @@ export class MemoryStore implements IStore {
|
||||
// filterId: Filter
|
||||
// }
|
||||
private filters: Record<string, Record<string, Filter>> = {};
|
||||
private accountData: Record<string, MatrixEvent> = {}; // type : content
|
||||
public accountData: Record<string, MatrixEvent> = {}; // type : content
|
||||
private readonly localStorage: Storage;
|
||||
private oobMembers: Record<string, MatrixEvent[]> = {}; // roomId: [member events]
|
||||
private oobMembers: Record<string, IEvent[]> = {}; // roomId: [member events]
|
||||
private clientOptions = {};
|
||||
|
||||
constructor(opts: IOpts = {}) {
|
||||
@@ -340,7 +341,7 @@ export class MemoryStore implements IStore {
|
||||
* @param {Object} syncData The sync data
|
||||
* @return {Promise} An immediately resolved promise.
|
||||
*/
|
||||
public setSyncData(syncData: object): Promise<void> {
|
||||
public setSyncData(syncData: ISyncResponse): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -415,7 +416,7 @@ export class MemoryStore implements IStore {
|
||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||
* @returns {null} in case the members for this room haven't been stored yet
|
||||
*/
|
||||
public getOutOfBandMembers(roomId: string): Promise<MatrixEvent[] | null> {
|
||||
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
|
||||
return Promise.resolve(this.oobMembers[roomId] || null);
|
||||
}
|
||||
|
||||
@@ -427,7 +428,7 @@ export class MemoryStore implements IStore {
|
||||
* @param {event[]} membershipEvents the membership events to store
|
||||
* @returns {Promise} when all members have been stored
|
||||
*/
|
||||
public setOutOfBandMembers(roomId: string, membershipEvents: MatrixEvent[]): Promise<void> {
|
||||
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
||||
this.oobMembers[roomId] = membershipEvents;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -23,19 +23,21 @@ import { EventType } from "../@types/event";
|
||||
import { Group } from "../models/group";
|
||||
import { Room } from "../models/room";
|
||||
import { User } from "../models/user";
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { IEvent, MatrixEvent } from "../models/event";
|
||||
import { Filter } from "../filter";
|
||||
import { ISavedSync, IStore } from "./index";
|
||||
import { RoomSummary } from "../models/room-summary";
|
||||
import { ISyncResponse } from "../sync-accumulator";
|
||||
|
||||
/**
|
||||
* Construct a stub store. This does no-ops on most store methods.
|
||||
* @constructor
|
||||
*/
|
||||
export class StubStore implements IStore {
|
||||
public readonly accountData = {}; // stub
|
||||
private fromToken: string = null;
|
||||
|
||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
||||
/** @return {Promise<boolean>} whether or not the database was newly created in this session. */
|
||||
public isNewlyCreated(): Promise<boolean> {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
@@ -212,7 +214,7 @@ export class StubStore implements IStore {
|
||||
* @param {Object} syncData The sync data
|
||||
* @return {Promise} An immediately resolved promise.
|
||||
*/
|
||||
public setSyncData(syncData: object): Promise<void> {
|
||||
public setSyncData(syncData: ISyncResponse): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -264,11 +266,11 @@ export class StubStore implements IStore {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public getOutOfBandMembers(): Promise<MatrixEvent[]> {
|
||||
public getOutOfBandMembers(): Promise<IEvent[]> {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
public setOutOfBandMembers(roomId: string, membershipEvents: MatrixEvent[]): Promise<void> {
|
||||
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@ export interface IEphemeral {
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IUnreadNotificationCounts {
|
||||
highlight_count: number;
|
||||
notification_count: number;
|
||||
highlight_count?: number;
|
||||
notification_count?: number;
|
||||
}
|
||||
|
||||
export interface IRoomEvent extends IMinimalEvent {
|
||||
@@ -64,7 +64,7 @@ interface IState {
|
||||
|
||||
export interface ITimeline {
|
||||
events: Array<IRoomEvent | IStateEvent>;
|
||||
limited: boolean;
|
||||
limited?: boolean;
|
||||
prev_batch: string;
|
||||
}
|
||||
|
||||
@@ -169,6 +169,13 @@ interface IRoom {
|
||||
};
|
||||
}
|
||||
|
||||
export interface ISyncData {
|
||||
nextBatch: string;
|
||||
accountData: IMinimalEvent[];
|
||||
roomsData: IRooms;
|
||||
groupsData: IGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* The purpose of this class is to accumulate /sync responses such that a
|
||||
* complete "initial" JSON response can be returned which accurately represents
|
||||
@@ -544,8 +551,8 @@ export class SyncAccumulator {
|
||||
* /sync response from the 'rooms' key onwards. The "accountData" key is
|
||||
* a list of raw events which represent global account data.
|
||||
*/
|
||||
public getJSON(forDatabase = false): object {
|
||||
const data = {
|
||||
public getJSON(forDatabase = false): ISyncData {
|
||||
const data: IRooms = {
|
||||
join: {},
|
||||
invite: {},
|
||||
// always empty. This is set by /sync when a room was previously
|
||||
@@ -575,7 +582,7 @@ export class SyncAccumulator {
|
||||
prev_batch: null,
|
||||
},
|
||||
unread_notifications: roomData._unreadNotifications,
|
||||
summary: roomData._summary,
|
||||
summary: roomData._summary as IRoomSummary,
|
||||
};
|
||||
// Add account data
|
||||
Object.keys(roomData._accountData).forEach((evType) => {
|
||||
@@ -678,7 +685,7 @@ export class SyncAccumulator {
|
||||
});
|
||||
|
||||
// Add account data
|
||||
const accData = [];
|
||||
const accData: IMinimalEvent[] = [];
|
||||
Object.keys(this.accountData).forEach((evType) => {
|
||||
accData.push(this.accountData[evType]);
|
||||
});
|
||||
|
||||
17
src/sync.ts
17
src/sync.ts
@@ -135,7 +135,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
|
||||
@@ -318,7 +318,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 || [];
|
||||
|
||||
@@ -329,8 +329,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(
|
||||
@@ -643,12 +642,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
|
||||
@@ -754,7 +753,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) {
|
||||
@@ -851,7 +850,7 @@ export class SyncApi {
|
||||
}
|
||||
|
||||
// Begin next sync
|
||||
this._sync(syncOptions);
|
||||
this.doSync(syncOptions);
|
||||
}
|
||||
|
||||
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IRequestPromise<ISyncResponse> {
|
||||
@@ -956,7 +955,7 @@ export class SyncApi {
|
||||
catchingUp: true,
|
||||
});
|
||||
}
|
||||
this._sync(syncOptions);
|
||||
this.doSync(syncOptions);
|
||||
});
|
||||
|
||||
this.currentSyncRequest = null;
|
||||
|
||||
47
src/utils.ts
47
src/utils.ts
@@ -31,15 +31,28 @@ import type NodeCrypto from "crypto";
|
||||
* @return {string} The encoded string e.g. foo=bar&baz=taz
|
||||
*/
|
||||
export function encodeParams(params: Record<string, string>): string {
|
||||
let qs = "";
|
||||
for (const key in params) {
|
||||
if (!params.hasOwnProperty(key)) {
|
||||
continue;
|
||||
return new URLSearchParams(params).toString();
|
||||
}
|
||||
qs += "&" + encodeURIComponent(key) + "=" +
|
||||
encodeURIComponent(params[key]);
|
||||
|
||||
export type QueryDict = Record<string, string | string[]>;
|
||||
|
||||
/**
|
||||
* Decode a query string in `application/x-www-form-urlencoded` format.
|
||||
* @param {string} query A query string to decode e.g.
|
||||
* foo=bar&via=server1&server2
|
||||
* @return {Object} The decoded object, if any keys occurred multiple times
|
||||
* then the value will be an array of strings, else it will be an array.
|
||||
* This behaviour matches Node's qs.parse but is built on URLSearchParams
|
||||
* for native web compatibility
|
||||
*/
|
||||
export function decodeParams(query: string): QueryDict {
|
||||
const o: QueryDict = {};
|
||||
const params = new URLSearchParams(query);
|
||||
for (const key of params.keys()) {
|
||||
const val = params.getAll(key);
|
||||
o[key] = val.length === 1 ? val[0] : val;
|
||||
}
|
||||
return qs.substring(1);
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,10 +129,10 @@ export function isFunction(value: any) {
|
||||
* @throws If the object is missing keys.
|
||||
*/
|
||||
// note using 'keys' here would shadow the 'keys' function defined above
|
||||
export function checkObjectHasKeys(obj: object, keys_: string[]) {
|
||||
for (let i = 0; i < keys_.length; i++) {
|
||||
if (!obj.hasOwnProperty(keys_[i])) {
|
||||
throw new Error("Missing required key: " + keys_[i]);
|
||||
export function checkObjectHasKeys(obj: object, keys: string[]) {
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
if (!obj.hasOwnProperty(keys[i])) {
|
||||
throw new Error("Missing required key: " + keys[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -464,7 +477,7 @@ export async function promiseMapSeries<T>(
|
||||
}
|
||||
}
|
||||
|
||||
export function promiseTry<T>(fn: () => T): Promise<T> {
|
||||
export function promiseTry<T>(fn: () => T | Promise<T>): Promise<T> {
|
||||
return new Promise((resolve) => resolve(fn()));
|
||||
}
|
||||
|
||||
@@ -670,3 +683,13 @@ export function lexicographicCompare(a: string, b: string): number {
|
||||
// hidden the operation in this function.
|
||||
return (a < b) ? -1 : ((a === b) ? 0 : 1);
|
||||
}
|
||||
|
||||
const collator = new Intl.Collator();
|
||||
/**
|
||||
* Performant language-sensitive string comparison
|
||||
* @param a the first string to compare
|
||||
* @param b the second string to compare
|
||||
*/
|
||||
export function compare(a: string, b: string): number {
|
||||
return collator.compare(a, b);
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ export class MatrixCall extends EventEmitter {
|
||||
// yet, null if we have but they didn't send a party ID.
|
||||
private opponentPartyId: string;
|
||||
private opponentCaps: CallCapabilities;
|
||||
private inviteTimeout: NodeJS.Timeout; // in the browser it's 'number'
|
||||
private inviteTimeout: number;
|
||||
|
||||
// The logic of when & if a call is on hold is nontrivial and explained in is*OnHold
|
||||
// This flag represents whether we want the other party to be on hold
|
||||
|
||||
@@ -47,12 +47,12 @@ export class CallEventHandler {
|
||||
|
||||
public start() {
|
||||
this.client.on("sync", this.evaluateEventBuffer);
|
||||
this.client.on("event", this.onEvent);
|
||||
this.client.on("Room.timeline", this.onRoomTimeline);
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.client.removeListener("sync", this.evaluateEventBuffer);
|
||||
this.client.removeListener("event", this.onEvent);
|
||||
this.client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
}
|
||||
|
||||
private evaluateEventBuffer = async () => {
|
||||
@@ -89,7 +89,7 @@ export class CallEventHandler {
|
||||
}
|
||||
};
|
||||
|
||||
private onEvent = (event: MatrixEvent) => {
|
||||
private onRoomTimeline = (event: MatrixEvent) => {
|
||||
this.client.decryptEventIfNeeded(event);
|
||||
// any call events or ones that might be once they're decrypted
|
||||
if (this.eventIsACall(event) || event.isBeingDecrypted()) {
|
||||
|
||||
Reference in New Issue
Block a user