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",
|
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
|
||||||
"gendoc": "jsdoc -c jsdoc.json -P package.json",
|
"gendoc": "jsdoc -c jsdoc.json -P package.json",
|
||||||
"lint": "yarn lint:types && yarn lint:js",
|
"lint": "yarn lint:types && yarn lint:js",
|
||||||
"lint:js": "eslint --max-warnings 57 src spec",
|
"lint:js": "eslint --max-warnings 7 src spec",
|
||||||
|
"lint:js-fix": "eslint --fix src spec",
|
||||||
"lint:types": "tsc --noEmit",
|
"lint:types": "tsc --noEmit",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
@@ -28,7 +29,7 @@
|
|||||||
"keywords": [
|
"keywords": [
|
||||||
"matrix-org"
|
"matrix-org"
|
||||||
],
|
],
|
||||||
"main": "./lib/index.js",
|
"main": "./src/index.ts",
|
||||||
"browser": "./lib/browser-index.js",
|
"browser": "./lib/browser-index.js",
|
||||||
"matrix_src_main": "./src/index.ts",
|
"matrix_src_main": "./src/index.ts",
|
||||||
"matrix_src_browser": "./src/browser-index.js",
|
"matrix_src_browser": "./src/browser-index.js",
|
||||||
@@ -110,6 +111,5 @@
|
|||||||
"coverageReporters": [
|
"coverageReporters": [
|
||||||
"text"
|
"text"
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"typings": "./lib/index.d.ts"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export function mkEvent(opts) {
|
|||||||
event.state_key = opts.skey;
|
event.state_key = opts.skey;
|
||||||
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
|
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
|
||||||
"m.room.power_levels", "m.room.topic",
|
"m.room.power_levels", "m.room.topic",
|
||||||
"com.example.state"].indexOf(opts.type) !== -1) {
|
"com.example.state"].includes(opts.type)) {
|
||||||
event.state_key = "";
|
event.state_key = "";
|
||||||
}
|
}
|
||||||
return opts.event ? new MatrixEvent(event) : event;
|
return opts.event ? new MatrixEvent(event) : event;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { EventStatus, MatrixEvent } from "../../src";
|
|||||||
import { EventTimeline } from "../../src/models/event-timeline";
|
import { EventTimeline } from "../../src/models/event-timeline";
|
||||||
import { RoomState } from "../../src";
|
import { RoomState } from "../../src";
|
||||||
import { Room } from "../../src";
|
import { Room } from "../../src";
|
||||||
|
import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event";
|
||||||
import { TestClient } from "../TestClient";
|
import { TestClient } from "../TestClient";
|
||||||
|
|
||||||
describe("Room", function() {
|
describe("Room", function() {
|
||||||
@@ -1456,4 +1457,291 @@ describe("Room", function() {
|
|||||||
expect(room.maySendMessage()).toEqual(true);
|
expect(room.maySendMessage()).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getDefaultRoomName", function() {
|
||||||
|
it("should return 'Empty room' if a user is the only member",
|
||||||
|
function() {
|
||||||
|
const room = new Room(roomId, null, userA);
|
||||||
|
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a display name if one other member is in the room",
|
||||||
|
function() {
|
||||||
|
const room = new Room(roomId, null, userA);
|
||||||
|
room.addLiveEvents([
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userA, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User A",
|
||||||
|
}),
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userB, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User B",
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a display name if one other member is banned",
|
||||||
|
function() {
|
||||||
|
const room = new Room(roomId, null, userA);
|
||||||
|
room.addLiveEvents([
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userA, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User A",
|
||||||
|
}),
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userB, mship: "ban",
|
||||||
|
room: roomId, event: true, name: "User B",
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a display name if one other member is invited",
|
||||||
|
function() {
|
||||||
|
const room = new Room(roomId, null, userA);
|
||||||
|
room.addLiveEvents([
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userA, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User A",
|
||||||
|
}),
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userB, mship: "invite",
|
||||||
|
room: roomId, event: true, name: "User B",
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 'Empty room (was User B)' if User B left the room",
|
||||||
|
function() {
|
||||||
|
const room = new Room(roomId, null, userA);
|
||||||
|
room.addLiveEvents([
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userA, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User A",
|
||||||
|
}),
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userB, mship: "leave",
|
||||||
|
room: roomId, event: true, name: "User B",
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(room.getDefaultRoomName(userA)).toEqual("Empty room (was User B)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 'User B and User C' if in a room with two other users",
|
||||||
|
function() {
|
||||||
|
const room = new Room(roomId, null, userA);
|
||||||
|
room.addLiveEvents([
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userA, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User A",
|
||||||
|
}),
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userB, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User B",
|
||||||
|
}),
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userC, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User C",
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(room.getDefaultRoomName(userA)).toEqual("User B and User C");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 'User B and 2 others' if in a room with three other users",
|
||||||
|
function() {
|
||||||
|
const room = new Room(roomId, null, userA);
|
||||||
|
room.addLiveEvents([
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userA, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User A",
|
||||||
|
}),
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userB, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User B",
|
||||||
|
}),
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userC, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User C",
|
||||||
|
}),
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userD, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User D",
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(room.getDefaultRoomName(userA)).toEqual("User B and 2 others");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("io.element.functional_users", function() {
|
||||||
|
it("should return a display name (default behaviour) if no one is marked as a functional member",
|
||||||
|
function() {
|
||||||
|
const room = new Room(roomId, null, userA);
|
||||||
|
room.addLiveEvents([
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userA, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User A",
|
||||||
|
}),
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userB, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User B",
|
||||||
|
}),
|
||||||
|
utils.mkEvent({
|
||||||
|
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||||
|
room: roomId, event: true,
|
||||||
|
content: {
|
||||||
|
service_members: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a display name (default behaviour) if service members is a number (invalid)",
|
||||||
|
function() {
|
||||||
|
const room = new Room(roomId, null, userA);
|
||||||
|
room.addLiveEvents([
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userA, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User A",
|
||||||
|
}),
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userB, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User B",
|
||||||
|
}),
|
||||||
|
utils.mkEvent({
|
||||||
|
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||||
|
room: roomId, event: true,
|
||||||
|
content: {
|
||||||
|
service_members: 1,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a display name (default behaviour) if service members is a string (invalid)",
|
||||||
|
function() {
|
||||||
|
const room = new Room(roomId, null, userA);
|
||||||
|
room.addLiveEvents([
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userA, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User A",
|
||||||
|
}),
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userB, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User B",
|
||||||
|
}),
|
||||||
|
utils.mkEvent({
|
||||||
|
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||||
|
room: roomId, event: true,
|
||||||
|
content: {
|
||||||
|
service_members: userB,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 'Empty room' if the only other member is a functional member",
|
||||||
|
function() {
|
||||||
|
const room = new Room(roomId, null, userA);
|
||||||
|
room.addLiveEvents([
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userA, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User A",
|
||||||
|
}),
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userB, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User B",
|
||||||
|
}),
|
||||||
|
utils.mkEvent({
|
||||||
|
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||||
|
room: roomId, event: true,
|
||||||
|
content: {
|
||||||
|
service_members: [userB],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 'User B' if User B is the only other member who isn't a functional member",
|
||||||
|
function() {
|
||||||
|
const room = new Room(roomId, null, userA);
|
||||||
|
room.addLiveEvents([
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userA, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User A",
|
||||||
|
}),
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userB, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User B",
|
||||||
|
}),
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userC, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User C",
|
||||||
|
}),
|
||||||
|
utils.mkEvent({
|
||||||
|
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||||
|
room: roomId, event: true, user: userA,
|
||||||
|
content: {
|
||||||
|
service_members: [userC],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 'Empty room' if all other members are functional members",
|
||||||
|
function() {
|
||||||
|
const room = new Room(roomId, null, userA);
|
||||||
|
room.addLiveEvents([
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userA, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User A",
|
||||||
|
}),
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userB, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User B",
|
||||||
|
}),
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userC, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User C",
|
||||||
|
}),
|
||||||
|
utils.mkEvent({
|
||||||
|
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||||
|
room: roomId, event: true, user: userA,
|
||||||
|
content: {
|
||||||
|
service_members: [userB, userC],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(room.getDefaultRoomName(userA)).toEqual("Empty room");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not break if an unjoined user is marked as a service user",
|
||||||
|
function() {
|
||||||
|
const room = new Room(roomId, null, userA);
|
||||||
|
room.addLiveEvents([
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userA, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User A",
|
||||||
|
}),
|
||||||
|
utils.mkMembership({
|
||||||
|
user: userB, mship: "join",
|
||||||
|
room: roomId, event: true, name: "User B",
|
||||||
|
}),
|
||||||
|
utils.mkEvent({
|
||||||
|
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, skey: "",
|
||||||
|
room: roomId, event: true, user: userA,
|
||||||
|
content: {
|
||||||
|
service_members: [userC],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(room.getDefaultRoomName(userA)).toEqual("User B");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
164
src/@types/PushRules.ts
Normal file
164
src/@types/PushRules.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// allow camelcase as these are things that go onto the wire
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
|
export enum PushRuleActionName {
|
||||||
|
DontNotify = "dont_notify",
|
||||||
|
Notify = "notify",
|
||||||
|
Coalesce = "coalesce",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TweakName {
|
||||||
|
Highlight = "highlight",
|
||||||
|
Sound = "sound",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Tweak<N extends TweakName, V> = {
|
||||||
|
set_tweak: N;
|
||||||
|
value: V;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TweakHighlight = Tweak<TweakName.Highlight, boolean>;
|
||||||
|
export type TweakSound = Tweak<TweakName.Sound, string>;
|
||||||
|
|
||||||
|
export type Tweaks = TweakHighlight | TweakSound;
|
||||||
|
|
||||||
|
export enum ConditionOperator {
|
||||||
|
ExactEquals = "==",
|
||||||
|
LessThan = "<",
|
||||||
|
GreaterThan = ">",
|
||||||
|
GreaterThanOrEqual = ">=",
|
||||||
|
LessThanOrEqual = "<=",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PushRuleAction = Tweaks | PushRuleActionName;
|
||||||
|
|
||||||
|
export type MemberCountCondition
|
||||||
|
<N extends number, Op extends ConditionOperator = ConditionOperator.ExactEquals>
|
||||||
|
= `${Op}${N}` | (Op extends ConditionOperator.ExactEquals ? `${N}` : never);
|
||||||
|
|
||||||
|
export type AnyMemberCountCondition = MemberCountCondition<number, ConditionOperator>;
|
||||||
|
|
||||||
|
export const DMMemberCountCondition: MemberCountCondition<2> = "2";
|
||||||
|
|
||||||
|
export function isDmMemberCountCondition(condition: AnyMemberCountCondition): boolean {
|
||||||
|
return condition === "==2" || condition === "2";
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ConditionKind {
|
||||||
|
EventMatch = "event_match",
|
||||||
|
ContainsDisplayName = "contains_display_name",
|
||||||
|
RoomMemberCount = "room_member_count",
|
||||||
|
SenderNotificationPermission = "sender_notification_permission",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPushRuleCondition<N extends ConditionKind | string> {
|
||||||
|
[k: string]: any; // for custom conditions, there can be other fields here
|
||||||
|
kind: N;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEventMatchCondition extends IPushRuleCondition<ConditionKind.EventMatch> {
|
||||||
|
key: string;
|
||||||
|
pattern: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IContainsDisplayNameCondition extends IPushRuleCondition<ConditionKind.ContainsDisplayName> {
|
||||||
|
// no additional fields
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRoomMemberCountCondition extends IPushRuleCondition<ConditionKind.RoomMemberCount> {
|
||||||
|
is: AnyMemberCountCondition;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISenderNotificationPermissionCondition
|
||||||
|
extends IPushRuleCondition<ConditionKind.SenderNotificationPermission> {
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PushRuleCondition = IPushRuleCondition<string>
|
||||||
|
| IEventMatchCondition
|
||||||
|
| IContainsDisplayNameCondition
|
||||||
|
| IRoomMemberCountCondition
|
||||||
|
| ISenderNotificationPermissionCondition;
|
||||||
|
|
||||||
|
export enum PushRuleKind {
|
||||||
|
Override = "override",
|
||||||
|
ContentSpecific = "content",
|
||||||
|
RoomSpecific = "room",
|
||||||
|
SenderSpecific = "sender",
|
||||||
|
Underride = "underride",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RuleId {
|
||||||
|
Master = ".m.rule.master",
|
||||||
|
ContainsDisplayName = ".m.rule.contains_display_name",
|
||||||
|
ContainsUserName = ".m.rule.contains_user_name",
|
||||||
|
AtRoomNotification = ".m.rule.roomnotif",
|
||||||
|
DM = ".m.rule.room_one_to_one",
|
||||||
|
EncryptedDM = ".m.rule.encrypted_room_one_to_one",
|
||||||
|
Message = ".m.rule.message",
|
||||||
|
EncryptedMessage = ".m.rule.encrypted",
|
||||||
|
InviteToSelf = ".m.rule.invite_for_me",
|
||||||
|
MemberEvent = ".m.rule.member_event",
|
||||||
|
IncomingCall = ".m.rule.call",
|
||||||
|
SuppressNotices = ".m.rule.suppress_notices",
|
||||||
|
Tombstone = ".m.rule.tombstone",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PushRuleSet = {
|
||||||
|
[k in PushRuleKind]?: IPushRule[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IPushRule {
|
||||||
|
actions: PushRuleAction[];
|
||||||
|
conditions?: PushRuleCondition[];
|
||||||
|
default: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
pattern?: string;
|
||||||
|
rule_id: RuleId | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAnnotatedPushRule extends IPushRule {
|
||||||
|
kind: PushRuleKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPushRules {
|
||||||
|
global: PushRuleSet;
|
||||||
|
device?: PushRuleSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPusher {
|
||||||
|
app_display_name: string;
|
||||||
|
app_id: string;
|
||||||
|
data: {
|
||||||
|
format?: string; // TODO: Types
|
||||||
|
url?: string; // TODO: Required if kind==http
|
||||||
|
brand?: string; // TODO: For email notifications only?
|
||||||
|
};
|
||||||
|
device_display_name: string;
|
||||||
|
kind: string; // TODO: Types
|
||||||
|
lang: string;
|
||||||
|
profile_tag?: string;
|
||||||
|
pushkey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPusherRequest extends IPusher {
|
||||||
|
append?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-enable camelcase */
|
||||||
@@ -144,6 +144,28 @@ export const UNSTABLE_MSC3089_LEAF = new UnstableValue("m.leaf", "org.matrix.msc
|
|||||||
*/
|
*/
|
||||||
export const UNSTABLE_MSC3089_BRANCH = new UnstableValue("m.branch", "org.matrix.msc3089.branch");
|
export const UNSTABLE_MSC3089_BRANCH = new UnstableValue("m.branch", "org.matrix.msc3089.branch");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functional members type for declaring a purpose of room members (e.g. helpful bots).
|
||||||
|
* Note that this reference is UNSTABLE and subject to breaking changes, including its
|
||||||
|
* eventual removal.
|
||||||
|
*
|
||||||
|
* Schema (TypeScript):
|
||||||
|
* {
|
||||||
|
* service_members?: string[]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* {
|
||||||
|
* "service_members": [
|
||||||
|
* "@helperbot:localhost",
|
||||||
|
* "@reminderbot:alice.tdl"
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
|
||||||
|
"io.element.functional_members",
|
||||||
|
"io.element.functional_members");
|
||||||
|
|
||||||
export interface IEncryptedFile {
|
export interface IEncryptedFile {
|
||||||
url: string;
|
url: string;
|
||||||
mimetype?: string;
|
mimetype?: string;
|
||||||
|
|||||||
6
src/@types/global.d.ts
vendored
6
src/@types/global.d.ts
vendored
@@ -20,6 +20,12 @@ import "@matrix-org/olm";
|
|||||||
export {};
|
export {};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
// use `number` as the return type in all cases for global.set{Interval,Timeout},
|
||||||
|
// so we don't accidentally use the methods on NodeJS.Timeout - they only exist in a subset of environments.
|
||||||
|
// The overload for clear{Interval,Timeout} is resolved as expected.
|
||||||
|
function setInterval(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
|
||||||
|
function setTimeout(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
|
||||||
|
|
||||||
namespace NodeJS {
|
namespace NodeJS {
|
||||||
interface Global {
|
interface Global {
|
||||||
localStorage: Storage;
|
localStorage: Storage;
|
||||||
|
|||||||
@@ -39,3 +39,38 @@ export enum Preset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ResizeMethod = "crop" | "scale";
|
export type ResizeMethod = "crop" | "scale";
|
||||||
|
|
||||||
|
// TODO move to http-api after TSification
|
||||||
|
export interface IAbortablePromise<T> extends Promise<T> {
|
||||||
|
abort(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IdServerUnbindResult = "no-support" | "success";
|
||||||
|
|
||||||
|
// Knock and private are reserved keywords which are not yet implemented.
|
||||||
|
export enum JoinRule {
|
||||||
|
Public = "public",
|
||||||
|
Invite = "invite",
|
||||||
|
/**
|
||||||
|
* @deprecated Reserved keyword. Should not be used. Not yet implemented.
|
||||||
|
*/
|
||||||
|
Private = "private",
|
||||||
|
Knock = "knock", // MSC2403 - only valid inside experimental room versions at this time.
|
||||||
|
Restricted = "restricted", // MSC3083 - only valid inside experimental room versions at this time.
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RestrictedAllowType {
|
||||||
|
RoomMembership = "m.room_membership", // MSC3083 - only valid inside experimental room versions at this time.
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum GuestAccess {
|
||||||
|
CanJoin = "can_join",
|
||||||
|
Forbidden = "forbidden",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum HistoryVisibility {
|
||||||
|
Invited = "invited",
|
||||||
|
Joined = "joined",
|
||||||
|
Shared = "shared",
|
||||||
|
WorldReadable = "world_readable",
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Callback } from "../client";
|
import { Callback } from "../client";
|
||||||
|
import { IContent } from "../models/event";
|
||||||
import { Preset, Visibility } from "./partials";
|
import { Preset, Visibility } from "./partials";
|
||||||
|
import { SearchKey } from "./search";
|
||||||
|
import { IRoomEventFilter } from "../filter";
|
||||||
|
|
||||||
// allow camelcase as these are things go onto the wire
|
// allow camelcase as these are things that go onto the wire
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
export interface IJoinRoomOpts {
|
export interface IJoinRoomOpts {
|
||||||
@@ -63,12 +66,12 @@ export interface IGuestAccessOpts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ISearchOpts {
|
export interface ISearchOpts {
|
||||||
keys?: string[];
|
keys?: SearchKey[];
|
||||||
query: string;
|
query: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEventSearchOpts {
|
export interface IEventSearchOpts {
|
||||||
filter: any; // TODO: Types
|
filter?: IRoomEventFilter;
|
||||||
term: string;
|
term: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +85,7 @@ export interface IInvite3PID {
|
|||||||
export interface ICreateRoomStateEvent {
|
export interface ICreateRoomStateEvent {
|
||||||
type: string;
|
type: string;
|
||||||
state_key?: string; // defaults to an empty string
|
state_key?: string; // defaults to an empty string
|
||||||
content: object;
|
content: IContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICreateRoomOpts {
|
export interface ICreateRoomOpts {
|
||||||
@@ -104,9 +107,11 @@ export interface IRoomDirectoryOptions {
|
|||||||
server?: string;
|
server?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
since?: string;
|
since?: string;
|
||||||
|
filter?: {
|
||||||
// TODO: Proper types
|
generic_search_term: string;
|
||||||
filter?: any & {generic_search_term: string};
|
};
|
||||||
|
include_all_networks?: boolean;
|
||||||
|
third_party_instance_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUploadOpts {
|
export interface IUploadOpts {
|
||||||
@@ -119,4 +124,19 @@ export interface IUploadOpts {
|
|||||||
progressHandler?: (state: {loaded: number, total: number}) => void;
|
progressHandler?: (state: {loaded: number, total: number}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IAddThreePidOnlyBody {
|
||||||
|
auth?: {
|
||||||
|
type: string;
|
||||||
|
session?: string;
|
||||||
|
};
|
||||||
|
client_secret: string;
|
||||||
|
sid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBindThreePidBody {
|
||||||
|
client_secret: string;
|
||||||
|
id_server: string;
|
||||||
|
id_access_token: string;
|
||||||
|
sid: string;
|
||||||
|
}
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|||||||
118
src/@types/search.ts
Normal file
118
src/@types/search.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Types relating to the /search API
|
||||||
|
|
||||||
|
import { IRoomEvent, IStateEvent } from "../sync-accumulator";
|
||||||
|
import { IRoomEventFilter } from "../filter";
|
||||||
|
import { SearchResult } from "../models/search-result";
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
export interface IEventWithRoomId extends IRoomEvent {
|
||||||
|
room_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IStateEventWithRoomId extends IStateEvent {
|
||||||
|
room_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMatrixProfile {
|
||||||
|
avatar_url?: string;
|
||||||
|
displayname?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IResultContext {
|
||||||
|
events_before: IEventWithRoomId[];
|
||||||
|
events_after: IEventWithRoomId[];
|
||||||
|
profile_info: Record<string, IMatrixProfile>;
|
||||||
|
start?: string;
|
||||||
|
end?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISearchResult {
|
||||||
|
rank: number;
|
||||||
|
result: IEventWithRoomId;
|
||||||
|
context: IResultContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GroupKey {
|
||||||
|
RoomId = "room_id",
|
||||||
|
Sender = "sender",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IResultRoomEvents {
|
||||||
|
count: number;
|
||||||
|
highlights: string[];
|
||||||
|
results: ISearchResult[];
|
||||||
|
state?: { [roomId: string]: IStateEventWithRoomId[] };
|
||||||
|
groups?: {
|
||||||
|
[groupKey in GroupKey]: {
|
||||||
|
[value: string]: {
|
||||||
|
next_batch?: string;
|
||||||
|
order: number;
|
||||||
|
results: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
next_batch?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IResultCategories {
|
||||||
|
room_events: IResultRoomEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SearchKey = "content.body" | "content.name" | "content.topic";
|
||||||
|
|
||||||
|
export enum SearchOrderBy {
|
||||||
|
Recent = "recent",
|
||||||
|
Rank = "rank",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISearchRequestBody {
|
||||||
|
search_categories: {
|
||||||
|
room_events: {
|
||||||
|
search_term: string;
|
||||||
|
keys?: SearchKey[];
|
||||||
|
filter?: IRoomEventFilter;
|
||||||
|
order_by?: SearchOrderBy;
|
||||||
|
event_context?: {
|
||||||
|
before_limit?: number;
|
||||||
|
after_limit?: number;
|
||||||
|
include_profile?: boolean;
|
||||||
|
};
|
||||||
|
include_state?: boolean;
|
||||||
|
groupings?: {
|
||||||
|
group_by: {
|
||||||
|
key: GroupKey;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISearchResponse {
|
||||||
|
search_categories: IResultCategories;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISearchResults {
|
||||||
|
_query?: ISearchRequestBody;
|
||||||
|
results: SearchResult[];
|
||||||
|
highlights: string[];
|
||||||
|
count?: number;
|
||||||
|
next_batch?: string;
|
||||||
|
pendingRequest?: Promise<ISearchResults>;
|
||||||
|
}
|
||||||
|
/* eslint-enable camelcase */
|
||||||
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
|
||||||
|
}
|
||||||
1002
src/client.ts
1002
src/client.ts
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,8 @@ limitations under the License.
|
|||||||
|
|
||||||
/** @module ContentHelpers */
|
/** @module ContentHelpers */
|
||||||
|
|
||||||
|
import { MsgType } from "./@types/event";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the content for a HTML Message event
|
* Generates the content for a HTML Message event
|
||||||
* @param {string} body the plaintext body of the message
|
* @param {string} body the plaintext body of the message
|
||||||
@@ -25,7 +27,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
export function makeHtmlMessage(body: string, htmlBody: string) {
|
export function makeHtmlMessage(body: string, htmlBody: string) {
|
||||||
return {
|
return {
|
||||||
msgtype: "m.text",
|
msgtype: MsgType.Text,
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
body: body,
|
body: body,
|
||||||
formatted_body: htmlBody,
|
formatted_body: htmlBody,
|
||||||
@@ -40,7 +42,7 @@ export function makeHtmlMessage(body: string, htmlBody: string) {
|
|||||||
*/
|
*/
|
||||||
export function makeHtmlNotice(body: string, htmlBody: string) {
|
export function makeHtmlNotice(body: string, htmlBody: string) {
|
||||||
return {
|
return {
|
||||||
msgtype: "m.notice",
|
msgtype: MsgType.Notice,
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
body: body,
|
body: body,
|
||||||
formatted_body: htmlBody,
|
formatted_body: htmlBody,
|
||||||
@@ -55,7 +57,7 @@ export function makeHtmlNotice(body: string, htmlBody: string) {
|
|||||||
*/
|
*/
|
||||||
export function makeHtmlEmote(body: string, htmlBody: string) {
|
export function makeHtmlEmote(body: string, htmlBody: string) {
|
||||||
return {
|
return {
|
||||||
msgtype: "m.emote",
|
msgtype: MsgType.Emote,
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
body: body,
|
body: body,
|
||||||
formatted_body: htmlBody,
|
formatted_body: htmlBody,
|
||||||
@@ -69,7 +71,7 @@ export function makeHtmlEmote(body: string, htmlBody: string) {
|
|||||||
*/
|
*/
|
||||||
export function makeTextMessage(body: string) {
|
export function makeTextMessage(body: string) {
|
||||||
return {
|
return {
|
||||||
msgtype: "m.text",
|
msgtype: MsgType.Text,
|
||||||
body: body,
|
body: body,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -81,7 +83,7 @@ export function makeTextMessage(body: string) {
|
|||||||
*/
|
*/
|
||||||
export function makeNotice(body: string) {
|
export function makeNotice(body: string) {
|
||||||
return {
|
return {
|
||||||
msgtype: "m.notice",
|
msgtype: MsgType.Notice,
|
||||||
body: body,
|
body: body,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -93,7 +95,7 @@ export function makeNotice(body: string) {
|
|||||||
*/
|
*/
|
||||||
export function makeEmoteMessage(body: string) {
|
export function makeEmoteMessage(body: string) {
|
||||||
return {
|
return {
|
||||||
msgtype: "m.emote",
|
msgtype: MsgType.Emote,
|
||||||
body: body,
|
body: body,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export class DeviceList extends EventEmitter {
|
|||||||
// The time the save is scheduled for
|
// The time the save is scheduled for
|
||||||
private savePromiseTime: number = null;
|
private savePromiseTime: number = null;
|
||||||
// The timer used to delay the save
|
// The timer used to delay the save
|
||||||
private saveTimer: NodeJS.Timeout = null;
|
private saveTimer: number = null;
|
||||||
// True if we have fetched data from the server or loaded a non-empty
|
// True if we have fetched data from the server or loaded a non-empty
|
||||||
// set of device data from the store
|
// set of device data from the store
|
||||||
private hasFetched: boolean = null;
|
private hasFetched: boolean = null;
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { MatrixEvent } from "../models/event";
|
import { MatrixEvent } from "../models/event";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
@@ -109,8 +125,8 @@ export class EncryptionSetupBuilder {
|
|||||||
* @param {Object} content
|
* @param {Object} content
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
public setAccountData(type: string, content: object): Promise<void> {
|
public async setAccountData(type: string, content: object): Promise<void> {
|
||||||
return this.accountDataClientAdapter.setAccountData(type, content);
|
await this.accountDataClientAdapter.setAccountData(type, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -284,7 +300,7 @@ class AccountDataClientAdapter extends EventEmitter {
|
|||||||
* @param {Object} content
|
* @param {Object} content
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
public setAccountData(type: string, content: any): Promise<void> {
|
public setAccountData(type: string, content: any): Promise<{}> {
|
||||||
const lastEvent = this.values.get(type);
|
const lastEvent = this.values.get(type);
|
||||||
this.values.set(type, content);
|
this.values.set(type, content);
|
||||||
// ensure accountData is emitted on the next tick,
|
// ensure accountData is emitted on the next tick,
|
||||||
@@ -293,6 +309,7 @@ class AccountDataClientAdapter extends EventEmitter {
|
|||||||
return Promise.resolve().then(() => {
|
return Promise.resolve().then(() => {
|
||||||
const event = new MatrixEvent({ type, content });
|
const event = new MatrixEvent({ type, content });
|
||||||
this.emit("accountData", event, lastEvent);
|
this.emit("accountData", event, lastEvent);
|
||||||
|
return {};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export enum RoomKeyRequestState {
|
|||||||
export class OutgoingRoomKeyRequestManager {
|
export class OutgoingRoomKeyRequestManager {
|
||||||
// handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null
|
// handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null
|
||||||
// if the callback has been set, or if it is still running.
|
// if the callback has been set, or if it is still running.
|
||||||
private sendOutgoingRoomKeyRequestsTimer: NodeJS.Timeout = null;
|
private sendOutgoingRoomKeyRequestsTimer: number = null;
|
||||||
|
|
||||||
// sanity check to ensure that we don't end up with two concurrent runs
|
// sanity check to ensure that we don't end up with two concurrent runs
|
||||||
// of sendOutgoingRoomKeyRequests
|
// of sendOutgoingRoomKeyRequests
|
||||||
@@ -366,7 +366,7 @@ export class OutgoingRoomKeyRequestManager {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sendOutgoingRoomKeyRequestsTimer = global.setTimeout(
|
this.sendOutgoingRoomKeyRequestsTimer = setTimeout(
|
||||||
startSendingOutgoingRoomKeyRequests,
|
startSendingOutgoingRoomKeyRequests,
|
||||||
SEND_KEY_REQUESTS_DELAY_MS,
|
SEND_KEY_REQUESTS_DELAY_MS,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
|||||||
import { CryptoStore } from "../client";
|
import { CryptoStore } from "../client";
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
interface IRoomEncryption {
|
export interface IRoomEncryption {
|
||||||
algorithm: string;
|
algorithm: string;
|
||||||
rotation_period_ms: number;
|
rotation_period_ms?: number;
|
||||||
rotation_period_msgs: number;
|
rotation_period_msgs?: number;
|
||||||
}
|
}
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ export interface ISecretRequest {
|
|||||||
|
|
||||||
export interface IAccountDataClient extends EventEmitter {
|
export interface IAccountDataClient extends EventEmitter {
|
||||||
// Subset of MatrixClient (which also uses any for the event content)
|
// 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;
|
getAccountData: (eventType: string) => MatrixEvent;
|
||||||
setAccountData: (eventType: string, content: any) => Promise<void>;
|
setAccountData: (eventType: string, content: any) => Promise<{}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISecretRequestInternal {
|
interface ISecretRequestInternal {
|
||||||
@@ -174,7 +174,7 @@ export class SecretStorage {
|
|||||||
* the form [keyId, keyInfo]. Otherwise, null is returned.
|
* the form [keyId, keyInfo]. Otherwise, null is returned.
|
||||||
* XXX: why is this an array when addKey returns an object?
|
* 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) {
|
if (!keyId) {
|
||||||
keyId = await this.getDefaultKeyId();
|
keyId = await this.getDefaultKeyId();
|
||||||
}
|
}
|
||||||
@@ -184,7 +184,7 @@ export class SecretStorage {
|
|||||||
|
|
||||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||||
"m.secret_storage.key." + keyId,
|
"m.secret_storage.key." + keyId,
|
||||||
);
|
) as ISecretStorageKeyInfo;
|
||||||
return keyInfo ? [keyId, keyInfo] : null;
|
return keyInfo ? [keyId, keyInfo] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@ export class SecretStorage {
|
|||||||
// get key information from key storage
|
// get key information from key storage
|
||||||
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
|
||||||
"m.secret_storage.key." + keyId,
|
"m.secret_storage.key." + keyId,
|
||||||
);
|
) as ISecretStorageKeyInfo;
|
||||||
if (!keyInfo) {
|
if (!keyInfo) {
|
||||||
throw new Error("Unknown key: " + keyId);
|
throw new Error("Unknown key: " + keyId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { OlmDevice } from "../OlmDevice";
|
|||||||
import { MatrixEvent, RoomMember } from "../..";
|
import { MatrixEvent, RoomMember } from "../..";
|
||||||
import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "..";
|
import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "..";
|
||||||
import { DeviceInfo } from "../deviceinfo";
|
import { DeviceInfo } from "../deviceinfo";
|
||||||
|
import { IRoomEncryption } from "../RoomList";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* map of registered encryption algorithm classes. A map from string to {@link
|
* map of registered encryption algorithm classes. A map from string to {@link
|
||||||
@@ -52,7 +53,7 @@ interface IParams {
|
|||||||
olmDevice: OlmDevice;
|
olmDevice: OlmDevice;
|
||||||
baseApis: MatrixClient;
|
baseApis: MatrixClient;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
config: object;
|
config: IRoomEncryption & object;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
|||||||
import { encodeRecoveryKey } from './recoverykey';
|
import { encodeRecoveryKey } from './recoverykey';
|
||||||
import { encryptAES, decryptAES, calculateKeyCheck } from './aes';
|
import { encryptAES, decryptAES, calculateKeyCheck } from './aes';
|
||||||
import { getCrypto } from '../utils';
|
import { getCrypto } from '../utils';
|
||||||
import { ICurve25519AuthData, IAes256AuthData, IKeyBackupInfo } from "./keybackup";
|
import { ICurve25519AuthData, IAes256AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
|
||||||
import { UnstableValue } from "../NamespacedValue";
|
import { UnstableValue } from "../NamespacedValue";
|
||||||
|
|
||||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||||
@@ -85,12 +85,22 @@ interface BackupAlgorithmClass {
|
|||||||
interface BackupAlgorithm {
|
interface BackupAlgorithm {
|
||||||
untrusted: boolean;
|
untrusted: boolean;
|
||||||
encryptSession(data: Record<string, any>): Promise<any>;
|
encryptSession(data: Record<string, any>): Promise<any>;
|
||||||
decryptSessions(ciphertexts: Record<string, any>): Promise<Record<string, any>[]>;
|
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<Record<string, any>[]>;
|
||||||
authData: AuthData;
|
authData: AuthData;
|
||||||
keyMatches(key: ArrayLike<number>): Promise<boolean>;
|
keyMatches(key: ArrayLike<number>): Promise<boolean>;
|
||||||
free(): void;
|
free(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IKeyBackup {
|
||||||
|
rooms: {
|
||||||
|
[roomId: string]: {
|
||||||
|
sessions: {
|
||||||
|
[sessionId: string]: IKeyBackupSession;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages the key backup.
|
* Manages the key backup.
|
||||||
*/
|
*/
|
||||||
@@ -464,11 +474,11 @@ export class BackupManager {
|
|||||||
let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
||||||
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
|
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||||
|
|
||||||
const data = {};
|
const rooms: IKeyBackup["rooms"] = {};
|
||||||
for (const session of sessions) {
|
for (const session of sessions) {
|
||||||
const roomId = session.sessionData.room_id;
|
const roomId = session.sessionData.room_id;
|
||||||
if (data[roomId] === undefined) {
|
if (rooms[roomId] === undefined) {
|
||||||
data[roomId] = { sessions: {} };
|
rooms[roomId] = { sessions: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionData = await this.baseApis.crypto.olmDevice.exportInboundGroupSession(
|
const sessionData = await this.baseApis.crypto.olmDevice.exportInboundGroupSession(
|
||||||
@@ -487,7 +497,7 @@ export class BackupManager {
|
|||||||
);
|
);
|
||||||
const verified = this.baseApis.crypto.checkDeviceInfoTrust(userId, device).isVerified();
|
const verified = this.baseApis.crypto.checkDeviceInfoTrust(userId, device).isVerified();
|
||||||
|
|
||||||
data[roomId]['sessions'][session.sessionId] = {
|
rooms[roomId]['sessions'][session.sessionId] = {
|
||||||
first_message_index: sessionData.first_known_index,
|
first_message_index: sessionData.first_known_index,
|
||||||
forwarded_count: forwardedCount,
|
forwarded_count: forwardedCount,
|
||||||
is_verified: verified,
|
is_verified: verified,
|
||||||
@@ -495,10 +505,7 @@ export class BackupManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.baseApis.sendKeyBackup(
|
await this.baseApis.sendKeyBackup(undefined, undefined, this.backupInfo.version, { rooms });
|
||||||
undefined, undefined, this.backupInfo.version,
|
|
||||||
{ rooms: data },
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions);
|
await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions);
|
||||||
remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
||||||
@@ -636,7 +643,9 @@ export class Curve25519 implements BackupAlgorithm {
|
|||||||
return this.publicKey.encrypt(JSON.stringify(plainText));
|
return this.publicKey.encrypt(JSON.stringify(plainText));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async decryptSessions(sessions: Record<string, Record<string, any>>): Promise<Record<string, any>[]> {
|
public async decryptSessions(
|
||||||
|
sessions: Record<string, IKeyBackupSession>,
|
||||||
|
): Promise<Record<string, any>[]> {
|
||||||
const privKey = await this.getKey();
|
const privKey = await this.getKey();
|
||||||
const decryption = new global.Olm.PkDecryption();
|
const decryption = new global.Olm.PkDecryption();
|
||||||
try {
|
try {
|
||||||
@@ -766,14 +775,12 @@ export class Aes256 implements BackupAlgorithm {
|
|||||||
return await encryptAES(JSON.stringify(plainText), this.key, data.session_id);
|
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 = [];
|
const keys = [];
|
||||||
|
|
||||||
for (const [sessionId, sessionData] of Object.entries(sessions)) {
|
for (const [sessionId, sessionData] of Object.entries(sessions)) {
|
||||||
try {
|
try {
|
||||||
const decrypted = JSON.parse(await decryptAES(
|
const decrypted = JSON.parse(await decryptAES(sessionData.session_data, this.key, sessionId));
|
||||||
sessionData.session_data, this.key, sessionId,
|
|
||||||
));
|
|
||||||
decrypted.session_id = sessionId;
|
decrypted.session_id = sessionId;
|
||||||
keys.push(decrypted);
|
keys.push(decrypted);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export interface IDehydratedDeviceKeyInfo {
|
|||||||
passphrase?: string;
|
passphrase?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeviceKeys {
|
export interface IDeviceKeys {
|
||||||
algorithms: Array<string>;
|
algorithms: Array<string>;
|
||||||
device_id: string; // eslint-disable-line camelcase
|
device_id: string; // eslint-disable-line camelcase
|
||||||
user_id: string; // eslint-disable-line camelcase
|
user_id: string; // eslint-disable-line camelcase
|
||||||
@@ -44,7 +44,7 @@ interface DeviceKeys {
|
|||||||
signatures?: Signatures;
|
signatures?: Signatures;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OneTimeKey {
|
export interface IOneTimeKey {
|
||||||
key: string;
|
key: string;
|
||||||
fallback?: boolean;
|
fallback?: boolean;
|
||||||
signatures?: Signatures;
|
signatures?: Signatures;
|
||||||
@@ -222,7 +222,7 @@ export class DehydrationManager {
|
|||||||
// send the keys to the server
|
// send the keys to the server
|
||||||
const deviceId = dehydrateResult.device_id;
|
const deviceId = dehydrateResult.device_id;
|
||||||
logger.log("Preparing device keys", deviceId);
|
logger.log("Preparing device keys", deviceId);
|
||||||
const deviceKeys: DeviceKeys = {
|
const deviceKeys: IDeviceKeys = {
|
||||||
algorithms: this.crypto.supportedAlgorithms,
|
algorithms: this.crypto.supportedAlgorithms,
|
||||||
device_id: deviceId,
|
device_id: deviceId,
|
||||||
user_id: this.crypto.userId,
|
user_id: this.crypto.userId,
|
||||||
@@ -244,7 +244,7 @@ export class DehydrationManager {
|
|||||||
logger.log("Preparing one-time keys");
|
logger.log("Preparing one-time keys");
|
||||||
const oneTimeKeys = {};
|
const oneTimeKeys = {};
|
||||||
for (const [keyId, key] of Object.entries(otks.curve25519)) {
|
for (const [keyId, key] of Object.entries(otks.curve25519)) {
|
||||||
const k: OneTimeKey = { key };
|
const k: IOneTimeKey = { key };
|
||||||
const signature = account.sign(anotherjson.stringify(k));
|
const signature = account.sign(anotherjson.stringify(k));
|
||||||
k.signatures = {
|
k.signatures = {
|
||||||
[this.crypto.userId]: {
|
[this.crypto.userId]: {
|
||||||
@@ -257,7 +257,7 @@ export class DehydrationManager {
|
|||||||
logger.log("Preparing fallback keys");
|
logger.log("Preparing fallback keys");
|
||||||
const fallbackKeys = {};
|
const fallbackKeys = {};
|
||||||
for (const [keyId, key] of Object.entries(fallbacks.curve25519)) {
|
for (const [keyId, key] of Object.entries(fallbacks.curve25519)) {
|
||||||
const k: OneTimeKey = { key, fallback: true };
|
const k: IOneTimeKey = { key, fallback: true };
|
||||||
const signature = account.sign(anotherjson.stringify(k));
|
const signature = account.sign(anotherjson.stringify(k));
|
||||||
k.signatures = {
|
k.signatures = {
|
||||||
[this.crypto.userId]: {
|
[this.crypto.userId]: {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from "./api";
|
|||||||
import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager';
|
import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager';
|
||||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||||
import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from './verification/QRCode';
|
import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from './verification/QRCode';
|
||||||
import { SAS } from './verification/SAS';
|
import { SAS as SASVerification } from './verification/SAS';
|
||||||
import { keyFromPassphrase } from './key_passphrase';
|
import { keyFromPassphrase } from './key_passphrase';
|
||||||
import { decodeRecoveryKey, encodeRecoveryKey } from './recoverykey';
|
import { decodeRecoveryKey, encodeRecoveryKey } from './recoverykey';
|
||||||
import { VerificationRequest } from "./verification/request/VerificationRequest";
|
import { VerificationRequest } from "./verification/request/VerificationRequest";
|
||||||
@@ -53,7 +53,7 @@ import { ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDevi
|
|||||||
import { IllegalMethod } from "./verification/IllegalMethod";
|
import { IllegalMethod } from "./verification/IllegalMethod";
|
||||||
import { KeySignatureUploadError } from "../errors";
|
import { KeySignatureUploadError } from "../errors";
|
||||||
import { decryptAES, encryptAES, calculateKeyCheck } from './aes';
|
import { decryptAES, encryptAES, calculateKeyCheck } from './aes';
|
||||||
import { DehydrationManager } from './dehydration';
|
import { DehydrationManager, IDeviceKeys, IOneTimeKey } from './dehydration';
|
||||||
import { BackupManager } from "./backup";
|
import { BackupManager } from "./backup";
|
||||||
import { IStore } from "../store";
|
import { IStore } from "../store";
|
||||||
import { Room } from "../models/room";
|
import { Room } from "../models/room";
|
||||||
@@ -61,7 +61,7 @@ import { RoomMember } from "../models/room-member";
|
|||||||
import { MatrixEvent } from "../models/event";
|
import { MatrixEvent } from "../models/event";
|
||||||
import { MatrixClient, IKeysUploadResponse, SessionStore, CryptoStore, ISignedKey } from "../client";
|
import { MatrixClient, IKeysUploadResponse, SessionStore, CryptoStore, ISignedKey } from "../client";
|
||||||
import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base";
|
import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base";
|
||||||
import type { RoomList } from "./RoomList";
|
import type { IRoomEncryption, RoomList } from "./RoomList";
|
||||||
import { IRecoveryKey, IEncryptedEventInfo } from "./api";
|
import { IRecoveryKey, IEncryptedEventInfo } from "./api";
|
||||||
import { IKeyBackupInfo } from "./keybackup";
|
import { IKeyBackupInfo } from "./keybackup";
|
||||||
import { ISyncStateData } from "../sync";
|
import { ISyncStateData } from "../sync";
|
||||||
@@ -70,7 +70,7 @@ const DeviceVerification = DeviceInfo.DeviceVerification;
|
|||||||
|
|
||||||
const defaultVerificationMethods = {
|
const defaultVerificationMethods = {
|
||||||
[ReciprocateQRCode.NAME]: ReciprocateQRCode,
|
[ReciprocateQRCode.NAME]: ReciprocateQRCode,
|
||||||
[SAS.NAME]: SAS,
|
[SASVerification.NAME]: SASVerification,
|
||||||
|
|
||||||
// These two can't be used for actual verification, but we do
|
// These two can't be used for actual verification, but we do
|
||||||
// need to be able to define them here for the verification flows
|
// need to be able to define them here for the verification flows
|
||||||
@@ -82,10 +82,13 @@ const defaultVerificationMethods = {
|
|||||||
/**
|
/**
|
||||||
* verification method names
|
* verification method names
|
||||||
*/
|
*/
|
||||||
export const verificationMethods = {
|
// legacy export identifier
|
||||||
RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME,
|
export enum verificationMethods {
|
||||||
SAS: SAS.NAME,
|
RECIPROCATE_QR_CODE = ReciprocateQRCode.NAME,
|
||||||
};
|
SAS = SASVerification.NAME,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VerificationMethod = verificationMethods;
|
||||||
|
|
||||||
export function isCryptoAvailable(): boolean {
|
export function isCryptoAvailable(): boolean {
|
||||||
return Boolean(global.Olm);
|
return Boolean(global.Olm);
|
||||||
@@ -142,6 +145,10 @@ interface IDeviceVerificationUpgrade {
|
|||||||
crossSigningInfo: CrossSigningInfo;
|
crossSigningInfo: CrossSigningInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ICheckOwnCrossSigningTrustOpts {
|
||||||
|
allowPrivateKeyRequests?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} module:crypto~OlmSessionResult
|
* @typedef {Object} module:crypto~OlmSessionResult
|
||||||
* @property {module:crypto/deviceinfo} device device info
|
* @property {module:crypto/deviceinfo} device device info
|
||||||
@@ -1418,7 +1425,7 @@ export class Crypto extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
async checkOwnCrossSigningTrust({
|
async checkOwnCrossSigningTrust({
|
||||||
allowPrivateKeyRequests = false,
|
allowPrivateKeyRequests = false,
|
||||||
} = {}) {
|
}: ICheckOwnCrossSigningTrustOpts = {}): Promise<void> {
|
||||||
const userId = this.userId;
|
const userId = this.userId;
|
||||||
|
|
||||||
// Before proceeding, ensure our cross-signing public keys have been
|
// Before proceeding, ensure our cross-signing public keys have been
|
||||||
@@ -1772,7 +1779,7 @@ export class Crypto extends EventEmitter {
|
|||||||
|
|
||||||
return this.signObject(deviceKeys).then(() => {
|
return this.signObject(deviceKeys).then(() => {
|
||||||
return this.baseApis.uploadKeysRequest({
|
return this.baseApis.uploadKeysRequest({
|
||||||
device_keys: deviceKeys,
|
device_keys: deviceKeys as Required<IDeviceKeys>,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1904,9 +1911,9 @@ export class Crypto extends EventEmitter {
|
|||||||
private async uploadOneTimeKeys() {
|
private async uploadOneTimeKeys() {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
|
||||||
const fallbackJson = {};
|
const fallbackJson: Record<string, IOneTimeKey> = {};
|
||||||
if (this.getNeedsNewFallback()) {
|
if (this.getNeedsNewFallback()) {
|
||||||
const fallbackKeys = await this.olmDevice.getFallbackKey();
|
const fallbackKeys = await this.olmDevice.getFallbackKey() as Record<string, Record<string, string>>;
|
||||||
for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) {
|
for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) {
|
||||||
const k = { key, fallback: true };
|
const k = { key, fallback: true };
|
||||||
fallbackJson["signed_curve25519:" + keyId] = k;
|
fallbackJson["signed_curve25519:" + keyId] = k;
|
||||||
@@ -2252,7 +2259,7 @@ export class Crypto extends EventEmitter {
|
|||||||
public async legacyDeviceVerification(
|
public async legacyDeviceVerification(
|
||||||
userId: string,
|
userId: string,
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
method: string,
|
method: VerificationMethod,
|
||||||
): VerificationRequest {
|
): VerificationRequest {
|
||||||
const transactionId = ToDeviceChannel.makeTransactionId();
|
const transactionId = ToDeviceChannel.makeTransactionId();
|
||||||
const channel = new ToDeviceChannel(
|
const channel = new ToDeviceChannel(
|
||||||
@@ -2465,7 +2472,7 @@ export class Crypto extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
public async setRoomEncryption(
|
public async setRoomEncryption(
|
||||||
roomId: string,
|
roomId: string,
|
||||||
config: any, // TODO types
|
config: IRoomEncryption,
|
||||||
inhibitDeviceQuery?: boolean,
|
inhibitDeviceQuery?: boolean,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// ignore crypto events with no algorithm defined
|
// ignore crypto events with no algorithm defined
|
||||||
@@ -2522,8 +2529,8 @@ export class Crypto extends EventEmitter {
|
|||||||
crypto: this,
|
crypto: this,
|
||||||
olmDevice: this.olmDevice,
|
olmDevice: this.olmDevice,
|
||||||
baseApis: this.baseApis,
|
baseApis: this.baseApis,
|
||||||
roomId: roomId,
|
roomId,
|
||||||
config: config,
|
config,
|
||||||
});
|
});
|
||||||
this.roomEncryptors[roomId] = alg;
|
this.roomEncryptors[roomId] = alg;
|
||||||
|
|
||||||
@@ -2878,7 +2885,7 @@ export class Crypto extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
public async onCryptoEvent(event: MatrixEvent): Promise<void> {
|
public async onCryptoEvent(event: MatrixEvent): Promise<void> {
|
||||||
const roomId = event.getRoomId();
|
const roomId = event.getRoomId();
|
||||||
const content = event.getContent();
|
const content = event.getContent<IRoomEncryption>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// inhibit the device list refresh for now - it will happen once we've
|
// inhibit the device list refresh for now - it will happen once we've
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export interface IKeyBackupSession {
|
|||||||
ciphertext: string;
|
ciphertext: string;
|
||||||
ephemeral: string;
|
ephemeral: string;
|
||||||
mac: string;
|
mac: string;
|
||||||
|
iv: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import OlmDevice from "./OlmDevice";
|
|||||||
import { DeviceInfo } from "./deviceinfo";
|
import { DeviceInfo } from "./deviceinfo";
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import * as utils from "../utils";
|
import * as utils from "../utils";
|
||||||
import { OneTimeKey } from "./dehydration";
|
import { IOneTimeKey } from "./dehydration";
|
||||||
import { MatrixClient } from "../client";
|
import { MatrixClient } from "../client";
|
||||||
|
|
||||||
enum Algorithm {
|
enum Algorithm {
|
||||||
@@ -407,7 +407,7 @@ export async function ensureOlmSessionsForDevices(
|
|||||||
|
|
||||||
async function _verifyKeyAndStartSession(
|
async function _verifyKeyAndStartSession(
|
||||||
olmDevice: OlmDevice,
|
olmDevice: OlmDevice,
|
||||||
oneTimeKey: OneTimeKey,
|
oneTimeKey: IOneTimeKey,
|
||||||
userId: string,
|
userId: string,
|
||||||
deviceInfo: DeviceInfo,
|
deviceInfo: DeviceInfo,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
@@ -465,7 +465,7 @@ export interface IObject {
|
|||||||
*/
|
*/
|
||||||
export async function verifySignature(
|
export async function verifySignature(
|
||||||
olmDevice: OlmDevice,
|
olmDevice: OlmDevice,
|
||||||
obj: OneTimeKey | IObject,
|
obj: IOneTimeKey | IObject,
|
||||||
signingUserId: string,
|
signingUserId: string,
|
||||||
signingDeviceId: string,
|
signingDeviceId: string,
|
||||||
signingKey: string,
|
signingKey: string,
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixClient } from "./client";
|
import { MatrixClient } from "./client";
|
||||||
import { MatrixEvent } from "./models/event";
|
import { IEvent, MatrixEvent } from "./models/event";
|
||||||
|
|
||||||
export type EventMapper = (obj: any) => MatrixEvent;
|
export type EventMapper = (obj: Partial<IEvent>) => MatrixEvent;
|
||||||
|
|
||||||
export interface MapperOpts {
|
export interface MapperOpts {
|
||||||
preventReEmit?: boolean;
|
preventReEmit?: boolean;
|
||||||
@@ -28,7 +28,7 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event
|
|||||||
const preventReEmit = Boolean(options.preventReEmit);
|
const preventReEmit = Boolean(options.preventReEmit);
|
||||||
const decrypt = options.decrypt !== false;
|
const decrypt = options.decrypt !== false;
|
||||||
|
|
||||||
function mapper(plainOldJsObject) {
|
function mapper(plainOldJsObject: Partial<IEvent>) {
|
||||||
const event = new MatrixEvent(plainOldJsObject);
|
const event = new MatrixEvent(plainOldJsObject);
|
||||||
if (event.isEncrypted()) {
|
if (event.isEncrypted()) {
|
||||||
if (!preventReEmit) {
|
if (!preventReEmit) {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function setProp(obj: object, keyNesting: string, val: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
interface IFilterDefinition {
|
export interface IFilterDefinition {
|
||||||
event_fields?: string[];
|
event_fields?: string[];
|
||||||
event_format?: "client" | "federation";
|
event_format?: "client" | "federation";
|
||||||
presence?: IFilterComponent;
|
presence?: IFilterComponent;
|
||||||
@@ -47,7 +47,7 @@ interface IFilterDefinition {
|
|||||||
room?: IRoomFilter;
|
room?: IRoomFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRoomEventFilter extends IFilterComponent {
|
export interface IRoomEventFilter extends IFilterComponent {
|
||||||
lazy_load_members?: boolean;
|
lazy_load_members?: boolean;
|
||||||
include_redundant_members?: boolean;
|
include_redundant_members?: boolean;
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ export class Filter {
|
|||||||
* @param {Object} jsonObj
|
* @param {Object} jsonObj
|
||||||
* @return {Filter}
|
* @return {Filter}
|
||||||
*/
|
*/
|
||||||
static fromJson(userId: string, filterId: string, jsonObj: IFilterDefinition): Filter {
|
public static fromJson(userId: string, filterId: string, jsonObj: IFilterDefinition): Filter {
|
||||||
const filter = new Filter(userId, filterId);
|
const filter = new Filter(userId, filterId);
|
||||||
filter.setDefinition(jsonObj);
|
filter.setDefinition(jsonObj);
|
||||||
return filter;
|
return filter;
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ MatrixHttpApi.prototype = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload content to the Home Server
|
* Upload content to the homeserver
|
||||||
*
|
*
|
||||||
* @param {object} file The object to upload. On a browser, something that
|
* @param {object} file The object to upload. On a browser, something that
|
||||||
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
|
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
|
||||||
@@ -393,7 +393,7 @@ MatrixHttpApi.prototype = {
|
|||||||
accessToken,
|
accessToken,
|
||||||
) {
|
) {
|
||||||
if (!this.opts.idBaseUrl) {
|
if (!this.opts.idBaseUrl) {
|
||||||
throw new Error("No Identity Server base URL set");
|
throw new Error("No identity server base URL set");
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullUri = this.opts.idBaseUrl + prefix + path;
|
const fullUri = this.opts.idBaseUrl + prefix + path;
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ limitations under the License.
|
|||||||
|
|
||||||
/** @module interactive-auth */
|
/** @module interactive-auth */
|
||||||
|
|
||||||
import url from "url";
|
|
||||||
import * as utils from "./utils";
|
import * as utils from "./utils";
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
|
|
||||||
@@ -187,9 +186,7 @@ InteractiveAuth.prototype = {
|
|||||||
client_secret: this._clientSecret,
|
client_secret: this._clientSecret,
|
||||||
};
|
};
|
||||||
if (await this._matrixClient.doesServerRequireIdServerParam()) {
|
if (await this._matrixClient.doesServerRequireIdServerParam()) {
|
||||||
const idServerParsedUrl = url.parse(
|
const idServerParsedUrl = new URL(this._matrixClient.getIdentityServerUrl());
|
||||||
this._matrixClient.getIdentityServerUrl(),
|
|
||||||
);
|
|
||||||
creds.id_server = idServerParsedUrl.host;
|
creds.id_server = idServerParsedUrl.host;
|
||||||
}
|
}
|
||||||
authDict = {
|
authDict = {
|
||||||
@@ -217,7 +214,7 @@ InteractiveAuth.prototype = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* get the client secret used for validation sessions
|
* get the client secret used for validation sessions
|
||||||
* with the ID server.
|
* with the identity server.
|
||||||
*
|
*
|
||||||
* @return {string} client secret
|
* @return {string} client secret
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ export class MSC3089Branch {
|
|||||||
* @param {string} name The new name for this file.
|
* @param {string} name The new name for this file.
|
||||||
* @returns {Promise<void>} Resolves when complete.
|
* @returns {Promise<void>} Resolves when complete.
|
||||||
*/
|
*/
|
||||||
public setName(name: string): Promise<void> {
|
public async setName(name: string): Promise<void> {
|
||||||
return this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, {
|
await this.client.sendStateEvent(this.roomId, UNSTABLE_MSC3089_BRANCH.name, {
|
||||||
...this.indexEvent.getContent(),
|
...this.indexEvent.getContent(),
|
||||||
name: name,
|
name: name,
|
||||||
}, this.id);
|
}, this.id);
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ export class MSC3089TreeSpace {
|
|||||||
* @param {string} name The new name for the space.
|
* @param {string} name The new name for the space.
|
||||||
* @returns {Promise<void>} Resolves when complete.
|
* @returns {Promise<void>} Resolves when complete.
|
||||||
*/
|
*/
|
||||||
public setName(name: string): Promise<void> {
|
public async setName(name: string): Promise<void> {
|
||||||
return this.client.sendStateEvent(this.roomId, EventType.RoomName, { name }, "");
|
await this.client.sendStateEvent(this.roomId, EventType.RoomName, { name }, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -190,7 +190,7 @@ export class MSC3089TreeSpace {
|
|||||||
}
|
}
|
||||||
pls['users'] = users;
|
pls['users'] = users;
|
||||||
|
|
||||||
return this.client.sendStateEvent(this.roomId, EventType.RoomPowerLevels, pls, "");
|
await this.client.sendStateEvent(this.roomId, EventType.RoomPowerLevels, pls, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -262,6 +262,16 @@ export class MatrixEvent extends EventEmitter {
|
|||||||
this.localTimestamp = Date.now() - this.getAge();
|
this.localTimestamp = Date.now() - this.getAge();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the event as though it would appear unencrypted. If the event is already not
|
||||||
|
* encrypted, it is simply returned as-is.
|
||||||
|
* @returns {IEvent} The event in wire format.
|
||||||
|
*/
|
||||||
|
public getEffectiveEvent(): IEvent {
|
||||||
|
// clearEvent doesn't have all the fields, so we'll copy what we can from this.event
|
||||||
|
return Object.assign({}, this.event, this.clearEvent) as IEvent;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the event_id for this event.
|
* Get the event_id for this event.
|
||||||
* @return {string} The event ID, e.g. <code>$143350589368169JsLZx:localhost
|
* @return {string} The event ID, e.g. <code>$143350589368169JsLZx:localhost
|
||||||
@@ -1232,20 +1242,7 @@ export class MatrixEvent extends EventEmitter {
|
|||||||
* @return {Object}
|
* @return {Object}
|
||||||
*/
|
*/
|
||||||
public toJSON(): object {
|
public toJSON(): object {
|
||||||
const event: any = {
|
const event = this.getEffectiveEvent();
|
||||||
type: this.getType(),
|
|
||||||
sender: this.getSender(),
|
|
||||||
content: this.getContent(),
|
|
||||||
event_id: this.getId(),
|
|
||||||
origin_server_ts: this.getTs(),
|
|
||||||
unsigned: this.getUnsigned(),
|
|
||||||
room_id: this.getRoomId(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// if this is a redaction then attach the redacts key
|
|
||||||
if (this.isRedaction()) {
|
|
||||||
event.redacts = this.event.redacts;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isEncrypted()) {
|
if (!this.isEncrypted()) {
|
||||||
return event;
|
return event;
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ import { EventTimeline } from "./event-timeline";
|
|||||||
import { getHttpUriForMxc } from "../content-repo";
|
import { getHttpUriForMxc } from "../content-repo";
|
||||||
import * as utils from "../utils";
|
import * as utils from "../utils";
|
||||||
import { normalize } from "../utils";
|
import { normalize } from "../utils";
|
||||||
import { EventStatus, MatrixEvent } from "./event";
|
import { EventStatus, IEvent, MatrixEvent } from "./event";
|
||||||
import { RoomMember } from "./room-member";
|
import { RoomMember } from "./room-member";
|
||||||
import { IRoomSummary, RoomSummary } from "./room-summary";
|
import { IRoomSummary, RoomSummary } from "./room-summary";
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import { ReEmitter } from '../ReEmitter';
|
import { ReEmitter } from '../ReEmitter';
|
||||||
import { EventType, RoomCreateTypeField, RoomType } from "../@types/event";
|
import { EventType, RoomCreateTypeField, RoomType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../@types/event";
|
||||||
import { IRoomVersionsCapability, MatrixClient, RoomVersionStability } from "../client";
|
import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
|
||||||
import { ResizeMethod } from "../@types/partials";
|
import { ResizeMethod } from "../@types/partials";
|
||||||
import { Filter } from "../filter";
|
import { Filter } from "../filter";
|
||||||
import { RoomState } from "./room-state";
|
import { RoomState } from "./room-state";
|
||||||
@@ -64,7 +64,7 @@ function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: stri
|
|||||||
|
|
||||||
interface IOpts {
|
interface IOpts {
|
||||||
storageToken?: string;
|
storageToken?: string;
|
||||||
pendingEventOrdering?: "chronological" | "detached";
|
pendingEventOrdering?: PendingEventOrdering;
|
||||||
timelineSupport?: boolean;
|
timelineSupport?: boolean;
|
||||||
unstableClientRelationAggregation?: boolean;
|
unstableClientRelationAggregation?: boolean;
|
||||||
lazyLoadMembers?: boolean;
|
lazyLoadMembers?: boolean;
|
||||||
@@ -218,7 +218,7 @@ export class Room extends EventEmitter {
|
|||||||
this.setMaxListeners(100);
|
this.setMaxListeners(100);
|
||||||
this.reEmitter = new ReEmitter(this);
|
this.reEmitter = new ReEmitter(this);
|
||||||
|
|
||||||
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
|
opts.pendingEventOrdering = opts.pendingEventOrdering || PendingEventOrdering.Chronological;
|
||||||
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
|
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"opts.pendingEventOrdering MUST be either 'chronological' or " +
|
"opts.pendingEventOrdering MUST be either 'chronological' or " +
|
||||||
@@ -649,7 +649,7 @@ export class Room extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadMembersFromServer(): Promise<object[]> {
|
private async loadMembersFromServer(): Promise<IEvent[]> {
|
||||||
const lastSyncToken = this.client.store.getSyncToken();
|
const lastSyncToken = this.client.store.getSyncToken();
|
||||||
const queryString = utils.encodeParams({
|
const queryString = utils.encodeParams({
|
||||||
not_membership: "leave",
|
not_membership: "leave",
|
||||||
@@ -665,8 +665,7 @@ export class Room extends EventEmitter {
|
|||||||
private async loadMembers(): Promise<{ memberEvents: MatrixEvent[], fromServer: boolean }> {
|
private async loadMembers(): Promise<{ memberEvents: MatrixEvent[], fromServer: boolean }> {
|
||||||
// were the members loaded from the server?
|
// were the members loaded from the server?
|
||||||
let fromServer = false;
|
let fromServer = false;
|
||||||
let rawMembersEvents =
|
let rawMembersEvents = await this.client.store.getOutOfBandMembers(this.roomId);
|
||||||
await this.client.store.getOutOfBandMembers(this.roomId);
|
|
||||||
if (rawMembersEvents === null) {
|
if (rawMembersEvents === null) {
|
||||||
fromServer = true;
|
fromServer = true;
|
||||||
rawMembersEvents = await this.loadMembersFromServer();
|
rawMembersEvents = await this.loadMembersFromServer();
|
||||||
@@ -713,7 +712,7 @@ export class Room extends EventEmitter {
|
|||||||
if (fromServer) {
|
if (fromServer) {
|
||||||
const oobMembers = this.currentState.getMembers()
|
const oobMembers = this.currentState.getMembers()
|
||||||
.filter((m) => m.isOutOfBand())
|
.filter((m) => m.isOutOfBand())
|
||||||
.map((m) => m.events.member.event);
|
.map((m) => m.events.member.event as IEvent);
|
||||||
logger.log(`LL: telling store to write ${oobMembers.length}`
|
logger.log(`LL: telling store to write ${oobMembers.length}`
|
||||||
+ ` members for room ${this.roomId}`);
|
+ ` members for room ${this.roomId}`);
|
||||||
const store = this.client.store;
|
const store = this.client.store;
|
||||||
@@ -2037,24 +2036,45 @@ export class Room extends EventEmitter {
|
|||||||
const joinedMemberCount = this.currentState.getJoinedMemberCount();
|
const joinedMemberCount = this.currentState.getJoinedMemberCount();
|
||||||
const invitedMemberCount = this.currentState.getInvitedMemberCount();
|
const invitedMemberCount = this.currentState.getInvitedMemberCount();
|
||||||
// -1 because these numbers include the syncing user
|
// -1 because these numbers include the syncing user
|
||||||
const inviteJoinCount = joinedMemberCount + invitedMemberCount - 1;
|
let inviteJoinCount = joinedMemberCount + invitedMemberCount - 1;
|
||||||
|
|
||||||
|
// get service members (e.g. helper bots) for exclusion
|
||||||
|
let excludedUserIds: string[] = [];
|
||||||
|
const mFunctionalMembers = this.currentState.getStateEvents(UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, "");
|
||||||
|
if (Array.isArray(mFunctionalMembers?.getContent().service_members)) {
|
||||||
|
excludedUserIds = mFunctionalMembers.getContent().service_members;
|
||||||
|
}
|
||||||
|
|
||||||
// get members that are NOT ourselves and are actually in the room.
|
// get members that are NOT ourselves and are actually in the room.
|
||||||
let otherNames = null;
|
let otherNames = null;
|
||||||
if (this.summaryHeroes) {
|
if (this.summaryHeroes) {
|
||||||
// if we have a summary, the member state events
|
// if we have a summary, the member state events
|
||||||
// should be in the room state
|
// should be in the room state
|
||||||
otherNames = this.summaryHeroes.map((userId) => {
|
otherNames = [];
|
||||||
|
this.summaryHeroes.forEach((userId) => {
|
||||||
|
// filter service members
|
||||||
|
if (excludedUserIds.includes(userId)) {
|
||||||
|
inviteJoinCount--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
const member = this.getMember(userId);
|
const member = this.getMember(userId);
|
||||||
return member ? member.name : userId;
|
otherNames.push(member ? member.name : userId);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let otherMembers = this.currentState.getMembers().filter((m) => {
|
let otherMembers = this.currentState.getMembers().filter((m) => {
|
||||||
return m.userId !== userId &&
|
return m.userId !== userId &&
|
||||||
(m.membership === "invite" || m.membership === "join");
|
(m.membership === "invite" || m.membership === "join");
|
||||||
});
|
});
|
||||||
|
otherMembers = otherMembers.filter(({ userId }) => {
|
||||||
|
// filter service members
|
||||||
|
if (excludedUserIds.includes(userId)) {
|
||||||
|
inviteJoinCount--;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
// make sure members have stable order
|
// make sure members have stable order
|
||||||
otherMembers.sort((a, b) => a.userId.localeCompare(b.userId));
|
otherMembers.sort((a, b) => utils.compare(a.userId, b.userId));
|
||||||
// only 5 first members, immitate summaryHeroes
|
// only 5 first members, immitate summaryHeroes
|
||||||
otherMembers = otherMembers.slice(0, 5);
|
otherMembers = otherMembers.slice(0, 5);
|
||||||
otherNames = otherMembers.map((m) => m.name);
|
otherNames = otherMembers.map((m) => m.name);
|
||||||
@@ -2065,7 +2085,7 @@ export class Room extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const myMembership = this.getMyMembership();
|
const myMembership = this.getMyMembership();
|
||||||
// if I have created a room and invited people throuh
|
// if I have created a room and invited people through
|
||||||
// 3rd party invites
|
// 3rd party invites
|
||||||
if (myMembership == 'join') {
|
if (myMembership == 'join') {
|
||||||
const thirdPartyInvites =
|
const thirdPartyInvites =
|
||||||
|
|||||||
@@ -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.
|
* @param {MatrixEvent} event The <code>im.vector.user_status</code> event.
|
||||||
* @fires module:client~MatrixClient#event:"User.unstable_statusMessage"
|
* @fires module:client~MatrixClient#event:"User.unstable_statusMessage"
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line
|
||||||
public unstable_updateStatusMessage(event: MatrixEvent): void {
|
public unstable_updateStatusMessage(event: MatrixEvent): void {
|
||||||
if (!event.getContent()) this.unstable_statusMessage = "";
|
if (!event.getContent()) this.unstable_statusMessage = "";
|
||||||
else this.unstable_statusMessage = event.getContent()["status"];
|
else this.unstable_statusMessage = event.getContent()["status"];
|
||||||
|
|||||||
@@ -15,6 +15,6 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export enum SERVICE_TYPES {
|
export enum SERVICE_TYPES {
|
||||||
IS = 'SERVICE_TYPE_IS', // An Identity Service
|
IS = 'SERVICE_TYPE_IS', // An identity server
|
||||||
IM = 'SERVICE_TYPE_IM', // An Integration Manager
|
IM = 'SERVICE_TYPE_IM', // An integration manager
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,11 @@ import { EventType } from "../@types/event";
|
|||||||
import { Group } from "../models/group";
|
import { Group } from "../models/group";
|
||||||
import { Room } from "../models/room";
|
import { Room } from "../models/room";
|
||||||
import { User } from "../models/user";
|
import { User } from "../models/user";
|
||||||
import { MatrixEvent } from "../models/event";
|
import { IEvent, MatrixEvent } from "../models/event";
|
||||||
import { Filter } from "../filter";
|
import { Filter } from "../filter";
|
||||||
import { RoomSummary } from "../models/room-summary";
|
import { RoomSummary } from "../models/room-summary";
|
||||||
import { IMinimalEvent, IGroups, IRooms } from "../sync-accumulator";
|
import { IMinimalEvent, IGroups, IRooms, ISyncResponse } from "../sync-accumulator";
|
||||||
|
import { IStartClientOpts } from "../client";
|
||||||
|
|
||||||
export interface ISavedSync {
|
export interface ISavedSync {
|
||||||
nextBatch: string;
|
nextBatch: string;
|
||||||
@@ -35,6 +36,8 @@ export interface ISavedSync {
|
|||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export interface IStore {
|
export interface IStore {
|
||||||
|
readonly accountData: Record<string, MatrixEvent>; // type : content
|
||||||
|
|
||||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
||||||
isNewlyCreated(): Promise<boolean>;
|
isNewlyCreated(): Promise<boolean>;
|
||||||
|
|
||||||
@@ -182,7 +185,7 @@ export interface IStore {
|
|||||||
* @param {Object} syncData The sync data
|
* @param {Object} syncData The sync data
|
||||||
* @return {Promise} An immediately resolved promise.
|
* @return {Promise} An immediately resolved promise.
|
||||||
*/
|
*/
|
||||||
setSyncData(syncData: object): Promise<void>;
|
setSyncData(syncData: ISyncResponse): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We never want to save because we have nothing to save to.
|
* We never want to save because we have nothing to save to.
|
||||||
@@ -194,7 +197,7 @@ export interface IStore {
|
|||||||
/**
|
/**
|
||||||
* Save does nothing as there is no backing data store.
|
* Save does nothing as there is no backing data store.
|
||||||
*/
|
*/
|
||||||
save(force: boolean): void;
|
save(force?: boolean): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Startup does nothing.
|
* Startup does nothing.
|
||||||
@@ -222,13 +225,13 @@ export interface IStore {
|
|||||||
*/
|
*/
|
||||||
deleteAllData(): Promise<void>;
|
deleteAllData(): Promise<void>;
|
||||||
|
|
||||||
getOutOfBandMembers(roomId: string): Promise<MatrixEvent[] | null>;
|
getOutOfBandMembers(roomId: string): Promise<IEvent[] | null>;
|
||||||
|
|
||||||
setOutOfBandMembers(roomId: string, membershipEvents: MatrixEvent[]): Promise<void>;
|
setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void>;
|
||||||
|
|
||||||
clearOutOfBandMembers(roomId: string): Promise<void>;
|
clearOutOfBandMembers(roomId: string): Promise<void>;
|
||||||
|
|
||||||
getClientOptions(): Promise<object>;
|
getClientOptions(): Promise<IStartClientOpts>;
|
||||||
|
|
||||||
storeClientOptions(options: object): Promise<void>;
|
storeClientOptions(options: IStartClientOpts): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/store/indexeddb-backend.ts
Normal file
36
src/store/indexeddb-backend.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ISavedSync } from "./index";
|
||||||
|
import { IEvent, IStartClientOpts, ISyncResponse } from "..";
|
||||||
|
|
||||||
|
export interface IIndexedDBBackend {
|
||||||
|
connect(): Promise<void>;
|
||||||
|
syncToDatabase(userTuples: UserTuple[]): Promise<void>;
|
||||||
|
isNewlyCreated(): Promise<boolean>;
|
||||||
|
setSyncData(syncData: ISyncResponse): Promise<void>;
|
||||||
|
getSavedSync(): Promise<ISavedSync>;
|
||||||
|
getNextBatchToken(): Promise<string>;
|
||||||
|
clearDatabase(): Promise<void>;
|
||||||
|
getOutOfBandMembers(roomId: string): Promise<IEvent[] | null>;
|
||||||
|
setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void>;
|
||||||
|
clearOutOfBandMembers(roomId: string): Promise<void>;
|
||||||
|
getUserPresenceEvents(): Promise<UserTuple[]>;
|
||||||
|
getClientOptions(): Promise<IStartClientOpts>;
|
||||||
|
storeClientOptions(options: IStartClientOpts): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserTuple = [userId: string, presenceEvent: Partial<IEvent>];
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -16,14 +14,17 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SyncAccumulator } from "../sync-accumulator";
|
import { IMinimalEvent, ISyncData, ISyncResponse, SyncAccumulator } from "../sync-accumulator";
|
||||||
import * as utils from "../utils";
|
import * as utils from "../utils";
|
||||||
import * as IndexedDBHelpers from "../indexeddb-helpers";
|
import * as IndexedDBHelpers from "../indexeddb-helpers";
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
|
import { IEvent, IStartClientOpts } from "..";
|
||||||
|
import { ISavedSync } from "./index";
|
||||||
|
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
|
||||||
|
|
||||||
const VERSION = 3;
|
const VERSION = 3;
|
||||||
|
|
||||||
function createDatabase(db) {
|
function createDatabase(db: IDBDatabase): void {
|
||||||
// Make user store, clobber based on user ID. (userId property of User objects)
|
// Make user store, clobber based on user ID. (userId property of User objects)
|
||||||
db.createObjectStore("users", { keyPath: ["userId"] });
|
db.createObjectStore("users", { keyPath: ["userId"] });
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ function createDatabase(db) {
|
|||||||
db.createObjectStore("sync", { keyPath: ["clobber"] });
|
db.createObjectStore("sync", { keyPath: ["clobber"] });
|
||||||
}
|
}
|
||||||
|
|
||||||
function upgradeSchemaV2(db) {
|
function upgradeSchemaV2(db: IDBDatabase): void {
|
||||||
const oobMembersStore = db.createObjectStore(
|
const oobMembersStore = db.createObjectStore(
|
||||||
"oob_membership_events", {
|
"oob_membership_events", {
|
||||||
keyPath: ["room_id", "state_key"],
|
keyPath: ["room_id", "state_key"],
|
||||||
@@ -43,7 +44,7 @@ function upgradeSchemaV2(db) {
|
|||||||
oobMembersStore.createIndex("room", "room_id");
|
oobMembersStore.createIndex("room", "room_id");
|
||||||
}
|
}
|
||||||
|
|
||||||
function upgradeSchemaV3(db) {
|
function upgradeSchemaV3(db: IDBDatabase): void {
|
||||||
db.createObjectStore("client_options",
|
db.createObjectStore("client_options",
|
||||||
{ keyPath: ["clobber"] });
|
{ keyPath: ["clobber"] });
|
||||||
}
|
}
|
||||||
@@ -58,16 +59,20 @@ function upgradeSchemaV3(db) {
|
|||||||
* @return {Promise<T[]>} Resolves to an array of whatever you returned from
|
* @return {Promise<T[]>} Resolves to an array of whatever you returned from
|
||||||
* resultMapper.
|
* resultMapper.
|
||||||
*/
|
*/
|
||||||
function selectQuery(store, keyRange, resultMapper) {
|
function selectQuery<T>(
|
||||||
|
store: IDBObjectStore,
|
||||||
|
keyRange: IDBKeyRange | IDBValidKey | undefined,
|
||||||
|
resultMapper: (cursor: IDBCursorWithValue) => T,
|
||||||
|
): Promise<T[]> {
|
||||||
const query = store.openCursor(keyRange);
|
const query = store.openCursor(keyRange);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const results = [];
|
const results = [];
|
||||||
query.onerror = (event) => {
|
query.onerror = () => {
|
||||||
reject(new Error("Query failed: " + event.target.errorCode));
|
reject(new Error("Query failed: " + query.error));
|
||||||
};
|
};
|
||||||
// collect results
|
// collect results
|
||||||
query.onsuccess = (event) => {
|
query.onsuccess = () => {
|
||||||
const cursor = event.target.result;
|
const cursor = query.result;
|
||||||
if (!cursor) {
|
if (!cursor) {
|
||||||
resolve(results);
|
resolve(results);
|
||||||
return; // end of results
|
return; // end of results
|
||||||
@@ -78,88 +83,84 @@ function selectQuery(store, keyRange, resultMapper) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function txnAsPromise(txn) {
|
function txnAsPromise(txn: IDBTransaction): Promise<Event> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
txn.oncomplete = function(event) {
|
txn.oncomplete = function(event) {
|
||||||
resolve(event);
|
resolve(event);
|
||||||
};
|
};
|
||||||
txn.onerror = function(event) {
|
txn.onerror = function() {
|
||||||
reject(event.target.error);
|
reject(txn.error);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reqAsEventPromise(req) {
|
function reqAsEventPromise(req: IDBRequest): Promise<Event> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
req.onsuccess = function(event) {
|
req.onsuccess = function(event) {
|
||||||
resolve(event);
|
resolve(event);
|
||||||
};
|
};
|
||||||
req.onerror = function(event) {
|
req.onerror = function() {
|
||||||
reject(event.target.error);
|
reject(req.error);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reqAsPromise(req) {
|
function reqAsPromise(req: IDBRequest): Promise<IDBRequest> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
req.onsuccess = () => resolve(req);
|
req.onsuccess = () => resolve(req);
|
||||||
req.onerror = (err) => reject(err);
|
req.onerror = (err) => reject(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reqAsCursorPromise(req) {
|
function reqAsCursorPromise(req: IDBRequest<IDBCursor | null>): Promise<IDBCursor> {
|
||||||
return reqAsEventPromise(req).then((event) => event.target.result);
|
return reqAsEventPromise(req).then((event) => req.result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export class LocalIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||||
* Does the actual reading from and writing to the indexeddb
|
public static exists(indexedDB: IDBFactory, dbName: string): boolean {
|
||||||
*
|
dbName = "matrix-js-sdk:" + (dbName || "default");
|
||||||
* Construct a new Indexed Database store backend. This requires a call to
|
return IndexedDBHelpers.exists(indexedDB, dbName);
|
||||||
* <code>connect()</code> before this store can be used.
|
}
|
||||||
* @constructor
|
|
||||||
* @param {Object} indexedDBInterface The Indexed DB interface e.g
|
|
||||||
* <code>window.indexedDB</code>
|
|
||||||
* @param {string=} dbName Optional database name. The same name must be used
|
|
||||||
* to open the same database.
|
|
||||||
*/
|
|
||||||
export function LocalIndexedDBStoreBackend(
|
|
||||||
indexedDBInterface, dbName,
|
|
||||||
) {
|
|
||||||
this.indexedDB = indexedDBInterface;
|
|
||||||
this._dbName = "matrix-js-sdk:" + (dbName || "default");
|
|
||||||
this.db = null;
|
|
||||||
this._disconnected = true;
|
|
||||||
this._syncAccumulator = new SyncAccumulator();
|
|
||||||
this._isNewlyCreated = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalIndexedDBStoreBackend.exists = function(indexedDB, dbName) {
|
private readonly dbName: string;
|
||||||
dbName = "matrix-js-sdk:" + (dbName || "default");
|
private readonly syncAccumulator: SyncAccumulator;
|
||||||
return IndexedDBHelpers.exists(indexedDB, dbName);
|
private db: IDBDatabase = null;
|
||||||
};
|
private disconnected = true;
|
||||||
|
private _isNewlyCreated = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does the actual reading from and writing to the indexeddb
|
||||||
|
*
|
||||||
|
* Construct a new Indexed Database store backend. This requires a call to
|
||||||
|
* <code>connect()</code> before this store can be used.
|
||||||
|
* @constructor
|
||||||
|
* @param {Object} indexedDB The Indexed DB interface e.g
|
||||||
|
* <code>window.indexedDB</code>
|
||||||
|
* @param {string=} dbName Optional database name. The same name must be used
|
||||||
|
* to open the same database.
|
||||||
|
*/
|
||||||
|
constructor(private readonly indexedDB: IDBFactory, dbName: string) {
|
||||||
|
this.dbName = "matrix-js-sdk:" + (dbName || "default");
|
||||||
|
this.syncAccumulator = new SyncAccumulator();
|
||||||
|
}
|
||||||
|
|
||||||
LocalIndexedDBStoreBackend.prototype = {
|
|
||||||
/**
|
/**
|
||||||
* Attempt to connect to the database. This can fail if the user does not
|
* Attempt to connect to the database. This can fail if the user does not
|
||||||
* grant permission.
|
* grant permission.
|
||||||
* @return {Promise} Resolves if successfully connected.
|
* @return {Promise} Resolves if successfully connected.
|
||||||
*/
|
*/
|
||||||
connect: function() {
|
public connect(): Promise<void> {
|
||||||
if (!this._disconnected) {
|
if (!this.disconnected) {
|
||||||
logger.log(
|
logger.log(`LocalIndexedDBStoreBackend.connect: already connected or connecting`);
|
||||||
`LocalIndexedDBStoreBackend.connect: already connected or connecting`,
|
|
||||||
);
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
this._disconnected = false;
|
this.disconnected = false;
|
||||||
|
|
||||||
logger.log(
|
logger.log(`LocalIndexedDBStoreBackend.connect: connecting...`);
|
||||||
`LocalIndexedDBStoreBackend.connect: connecting...`,
|
const req = this.indexedDB.open(this.dbName, VERSION);
|
||||||
);
|
|
||||||
const req = this.indexedDB.open(this._dbName, VERSION);
|
|
||||||
req.onupgradeneeded = (ev) => {
|
req.onupgradeneeded = (ev) => {
|
||||||
const db = ev.target.result;
|
const db = req.result;
|
||||||
const oldVersion = ev.oldVersion;
|
const oldVersion = ev.oldVersion;
|
||||||
logger.log(
|
logger.log(
|
||||||
`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`,
|
`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`,
|
||||||
@@ -178,19 +179,13 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
req.onblocked = () => {
|
req.onblocked = () => {
|
||||||
logger.log(
|
logger.log(`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`);
|
||||||
`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.log(
|
logger.log(`LocalIndexedDBStoreBackend.connect: awaiting connection...`);
|
||||||
`LocalIndexedDBStoreBackend.connect: awaiting connection...`,
|
return reqAsEventPromise(req).then(() => {
|
||||||
);
|
logger.log(`LocalIndexedDBStoreBackend.connect: connected`);
|
||||||
return reqAsEventPromise(req).then((ev) => {
|
this.db = req.result;
|
||||||
logger.log(
|
|
||||||
`LocalIndexedDBStoreBackend.connect: connected`,
|
|
||||||
);
|
|
||||||
this.db = ev.target.result;
|
|
||||||
|
|
||||||
// add a poorly-named listener for when deleteDatabase is called
|
// add a poorly-named listener for when deleteDatabase is called
|
||||||
// so we can close our db connections.
|
// so we can close our db connections.
|
||||||
@@ -198,27 +193,26 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
this.db.close();
|
this.db.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
return this._init();
|
return this.init();
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
/** @return {bool} whether or not the database was newly created in this session. */
|
|
||||||
isNewlyCreated: function() {
|
/** @return {boolean} whether or not the database was newly created in this session. */
|
||||||
|
public isNewlyCreated(): Promise<boolean> {
|
||||||
return Promise.resolve(this._isNewlyCreated);
|
return Promise.resolve(this._isNewlyCreated);
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Having connected, load initial data from the database and prepare for use
|
* Having connected, load initial data from the database and prepare for use
|
||||||
* @return {Promise} Resolves on success
|
* @return {Promise} Resolves on success
|
||||||
*/
|
*/
|
||||||
_init: function() {
|
private init() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
this._loadAccountData(),
|
this.loadAccountData(),
|
||||||
this._loadSyncData(),
|
this.loadSyncData(),
|
||||||
]).then(([accountData, syncData]) => {
|
]).then(([accountData, syncData]) => {
|
||||||
logger.log(
|
logger.log(`LocalIndexedDBStoreBackend: loaded initial data`);
|
||||||
`LocalIndexedDBStoreBackend: loaded initial data`,
|
this.syncAccumulator.accumulate({
|
||||||
);
|
|
||||||
this._syncAccumulator.accumulate({
|
|
||||||
next_batch: syncData.nextBatch,
|
next_batch: syncData.nextBatch,
|
||||||
rooms: syncData.roomsData,
|
rooms: syncData.roomsData,
|
||||||
groups: syncData.groupsData,
|
groups: syncData.groupsData,
|
||||||
@@ -227,7 +221,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
},
|
},
|
||||||
}, true);
|
}, true);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the out-of-band membership events for this room that
|
* Returns the out-of-band membership events for this room that
|
||||||
@@ -236,8 +230,8 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
* @returns {Promise<event[]>} the events, potentially an empty array if OOB loading didn't yield any new members
|
* @returns {Promise<event[]>} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||||
* @returns {null} in case the members for this room haven't been stored yet
|
* @returns {null} in case the members for this room haven't been stored yet
|
||||||
*/
|
*/
|
||||||
getOutOfBandMembers: function(roomId) {
|
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
|
||||||
return new Promise((resolve, reject) =>{
|
return new Promise<IEvent[] | null>((resolve, reject) =>{
|
||||||
const tx = this.db.transaction(["oob_membership_events"], "readonly");
|
const tx = this.db.transaction(["oob_membership_events"], "readonly");
|
||||||
const store = tx.objectStore("oob_membership_events");
|
const store = tx.objectStore("oob_membership_events");
|
||||||
const roomIndex = store.index("room");
|
const roomIndex = store.index("room");
|
||||||
@@ -252,8 +246,8 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
// were all known already
|
// were all known already
|
||||||
let oobWritten = false;
|
let oobWritten = false;
|
||||||
|
|
||||||
request.onsuccess = (event) => {
|
request.onsuccess = () => {
|
||||||
const cursor = event.target.result;
|
const cursor = request.result;
|
||||||
if (!cursor) {
|
if (!cursor) {
|
||||||
// Unknown room
|
// Unknown room
|
||||||
if (!membershipEvents.length && !oobWritten) {
|
if (!membershipEvents.length && !oobWritten) {
|
||||||
@@ -273,11 +267,10 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
reject(err);
|
reject(err);
|
||||||
};
|
};
|
||||||
}).then((events) => {
|
}).then((events) => {
|
||||||
logger.log(`LL: got ${events && events.length}` +
|
logger.log(`LL: got ${events && events.length} membershipEvents from storage for room ${roomId} ...`);
|
||||||
` membershipEvents from storage for room ${roomId} ...`);
|
|
||||||
return events;
|
return events;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the out-of-band membership events for this room. Note that
|
* Stores the out-of-band membership events for this room. Note that
|
||||||
@@ -286,7 +279,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
* @param {string} roomId
|
* @param {string} roomId
|
||||||
* @param {event[]} membershipEvents the membership events to store
|
* @param {event[]} membershipEvents the membership events to store
|
||||||
*/
|
*/
|
||||||
setOutOfBandMembers: async function(roomId, membershipEvents) {
|
public async setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
||||||
logger.log(`LL: backend about to store ${membershipEvents.length}` +
|
logger.log(`LL: backend about to store ${membershipEvents.length}` +
|
||||||
` members for ${roomId}`);
|
` members for ${roomId}`);
|
||||||
const tx = this.db.transaction(["oob_membership_events"], "readwrite");
|
const tx = this.db.transaction(["oob_membership_events"], "readwrite");
|
||||||
@@ -307,9 +300,9 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
store.put(markerObject);
|
store.put(markerObject);
|
||||||
await txnAsPromise(tx);
|
await txnAsPromise(tx);
|
||||||
logger.log(`LL: backend done storing for ${roomId}!`);
|
logger.log(`LL: backend done storing for ${roomId}!`);
|
||||||
},
|
}
|
||||||
|
|
||||||
clearOutOfBandMembers: async function(roomId) {
|
public async clearOutOfBandMembers(roomId: string): Promise<void> {
|
||||||
// the approach to delete all members for a room
|
// the approach to delete all members for a room
|
||||||
// is to get the min and max state key from the index
|
// is to get the min and max state key from the index
|
||||||
// for that room, and then delete between those
|
// for that room, and then delete between those
|
||||||
@@ -324,11 +317,11 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
const roomRange = IDBKeyRange.only(roomId);
|
const roomRange = IDBKeyRange.only(roomId);
|
||||||
|
|
||||||
const minStateKeyProm = reqAsCursorPromise(
|
const minStateKeyProm = reqAsCursorPromise(
|
||||||
roomIndex.openKeyCursor(roomRange, "next"),
|
roomIndex.openKeyCursor(roomRange, "next"),
|
||||||
).then((cursor) => cursor && cursor.primaryKey[1]);
|
).then((cursor) => cursor && cursor.primaryKey[1]);
|
||||||
const maxStateKeyProm = reqAsCursorPromise(
|
const maxStateKeyProm = reqAsCursorPromise(
|
||||||
roomIndex.openKeyCursor(roomRange, "prev"),
|
roomIndex.openKeyCursor(roomRange, "prev"),
|
||||||
).then((cursor) => cursor && cursor.primaryKey[1]);
|
).then((cursor) => cursor && cursor.primaryKey[1]);
|
||||||
const [minStateKey, maxStateKey] = await Promise.all(
|
const [minStateKey, maxStateKey] = await Promise.all(
|
||||||
[minStateKeyProm, maxStateKeyProm]);
|
[minStateKeyProm, maxStateKeyProm]);
|
||||||
|
|
||||||
@@ -341,45 +334,39 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
[roomId, maxStateKey],
|
[roomId, maxStateKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.log(`LL: Deleting all users + marker in storage for ` +
|
logger.log(`LL: Deleting all users + marker in storage for room ${roomId}, with key range:`,
|
||||||
`room ${roomId}, with key range:`,
|
|
||||||
[roomId, minStateKey], [roomId, maxStateKey]);
|
[roomId, minStateKey], [roomId, maxStateKey]);
|
||||||
await reqAsPromise(writeStore.delete(membersKeyRange));
|
await reqAsPromise(writeStore.delete(membersKeyRange));
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear the entire database. This should be used when logging out of a client
|
* Clear the entire database. This should be used when logging out of a client
|
||||||
* to prevent mixing data between accounts.
|
* to prevent mixing data between accounts.
|
||||||
* @return {Promise} Resolved when the database is cleared.
|
* @return {Promise} Resolved when the database is cleared.
|
||||||
*/
|
*/
|
||||||
clearDatabase: function() {
|
public clearDatabase(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve) => {
|
||||||
logger.log(`Removing indexeddb instance: ${this._dbName}`);
|
logger.log(`Removing indexeddb instance: ${this.dbName}`);
|
||||||
const req = this.indexedDB.deleteDatabase(this._dbName);
|
const req = this.indexedDB.deleteDatabase(this.dbName);
|
||||||
|
|
||||||
req.onblocked = () => {
|
req.onblocked = () => {
|
||||||
logger.log(
|
logger.log(`can't yet delete indexeddb ${this.dbName} because it is open elsewhere`);
|
||||||
`can't yet delete indexeddb ${this._dbName}` +
|
|
||||||
` because it is open elsewhere`,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
req.onerror = (ev) => {
|
req.onerror = () => {
|
||||||
// in firefox, with indexedDB disabled, this fails with a
|
// in firefox, with indexedDB disabled, this fails with a
|
||||||
// DOMError. We treat this as non-fatal, so that we can still
|
// DOMError. We treat this as non-fatal, so that we can still
|
||||||
// use the app.
|
// use the app.
|
||||||
logger.warn(
|
logger.warn(`unable to delete js-sdk store indexeddb: ${req.error}`);
|
||||||
`unable to delete js-sdk store indexeddb: ${ev.target.error}`,
|
|
||||||
);
|
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
req.onsuccess = () => {
|
req.onsuccess = () => {
|
||||||
logger.log(`Removed indexeddb instance: ${this._dbName}`);
|
logger.log(`Removed indexeddb instance: ${this.dbName}`);
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean=} copy If false, the data returned is from internal
|
* @param {boolean=} copy If false, the data returned is from internal
|
||||||
@@ -390,10 +377,8 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
* client state to where it was at the last save, or null if there
|
* client state to where it was at the last save, or null if there
|
||||||
* is no saved sync data.
|
* is no saved sync data.
|
||||||
*/
|
*/
|
||||||
getSavedSync: function(copy) {
|
public getSavedSync(copy = true): Promise<ISavedSync> {
|
||||||
if (copy === undefined) copy = true;
|
const data = this.syncAccumulator.getJSON();
|
||||||
|
|
||||||
const data = this._syncAccumulator.getJSON();
|
|
||||||
if (!data.nextBatch) return Promise.resolve(null);
|
if (!data.nextBatch) return Promise.resolve(null);
|
||||||
if (copy) {
|
if (copy) {
|
||||||
// We must deep copy the stored data so that the /sync processing code doesn't
|
// We must deep copy the stored data so that the /sync processing code doesn't
|
||||||
@@ -402,29 +387,27 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
} else {
|
} else {
|
||||||
return Promise.resolve(data);
|
return Promise.resolve(data);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
getNextBatchToken: function() {
|
public getNextBatchToken(): Promise<string> {
|
||||||
return Promise.resolve(this._syncAccumulator.getNextBatchToken());
|
return Promise.resolve(this.syncAccumulator.getNextBatchToken());
|
||||||
},
|
}
|
||||||
|
|
||||||
setSyncData: function(syncData) {
|
public setSyncData(syncData: ISyncResponse): Promise<void> {
|
||||||
return Promise.resolve().then(() => {
|
return Promise.resolve().then(() => {
|
||||||
this._syncAccumulator.accumulate(syncData);
|
this.syncAccumulator.accumulate(syncData);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
syncToDatabase: function(userTuples) {
|
public async syncToDatabase(userTuples: UserTuple[]): Promise<void> {
|
||||||
const syncData = this._syncAccumulator.getJSON(true);
|
const syncData = this.syncAccumulator.getJSON(true);
|
||||||
|
|
||||||
return Promise.all([
|
await Promise.all([
|
||||||
this._persistUserPresenceEvents(userTuples),
|
this.persistUserPresenceEvents(userTuples),
|
||||||
this._persistAccountData(syncData.accountData),
|
this.persistAccountData(syncData.accountData),
|
||||||
this._persistSyncData(
|
this.persistSyncData(syncData.nextBatch, syncData.roomsData, syncData.groupsData),
|
||||||
syncData.nextBatch, syncData.roomsData, syncData.groupsData,
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist rooms /sync data along with the next batch token.
|
* Persist rooms /sync data along with the next batch token.
|
||||||
@@ -433,20 +416,24 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
* @param {Object} groupsData The 'groups' /sync data from a SyncAccumulator
|
* @param {Object} groupsData The 'groups' /sync data from a SyncAccumulator
|
||||||
* @return {Promise} Resolves if the data was persisted.
|
* @return {Promise} Resolves if the data was persisted.
|
||||||
*/
|
*/
|
||||||
_persistSyncData: function(nextBatch, roomsData, groupsData) {
|
private persistSyncData(
|
||||||
|
nextBatch: string,
|
||||||
|
roomsData: ISyncResponse["rooms"],
|
||||||
|
groupsData: ISyncResponse["groups"],
|
||||||
|
): Promise<void> {
|
||||||
logger.log("Persisting sync data up to", nextBatch);
|
logger.log("Persisting sync data up to", nextBatch);
|
||||||
return utils.promiseTry(() => {
|
return utils.promiseTry<void>(() => {
|
||||||
const txn = this.db.transaction(["sync"], "readwrite");
|
const txn = this.db.transaction(["sync"], "readwrite");
|
||||||
const store = txn.objectStore("sync");
|
const store = txn.objectStore("sync");
|
||||||
store.put({
|
store.put({
|
||||||
clobber: "-", // constant key so will always clobber
|
clobber: "-", // constant key so will always clobber
|
||||||
nextBatch: nextBatch,
|
nextBatch,
|
||||||
roomsData: roomsData,
|
roomsData,
|
||||||
groupsData: groupsData,
|
groupsData,
|
||||||
}); // put == UPSERT
|
}); // put == UPSERT
|
||||||
return txnAsPromise(txn);
|
return txnAsPromise(txn).then();
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist a list of account data events. Events with the same 'type' will
|
* Persist a list of account data events. Events with the same 'type' will
|
||||||
@@ -454,16 +441,16 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
* @param {Object[]} accountData An array of raw user-scoped account data events
|
* @param {Object[]} accountData An array of raw user-scoped account data events
|
||||||
* @return {Promise} Resolves if the events were persisted.
|
* @return {Promise} Resolves if the events were persisted.
|
||||||
*/
|
*/
|
||||||
_persistAccountData: function(accountData) {
|
private persistAccountData(accountData: IMinimalEvent[]): Promise<void> {
|
||||||
return utils.promiseTry(() => {
|
return utils.promiseTry<void>(() => {
|
||||||
const txn = this.db.transaction(["accountData"], "readwrite");
|
const txn = this.db.transaction(["accountData"], "readwrite");
|
||||||
const store = txn.objectStore("accountData");
|
const store = txn.objectStore("accountData");
|
||||||
for (let i = 0; i < accountData.length; i++) {
|
for (let i = 0; i < accountData.length; i++) {
|
||||||
store.put(accountData[i]); // put == UPSERT
|
store.put(accountData[i]); // put == UPSERT
|
||||||
}
|
}
|
||||||
return txnAsPromise(txn);
|
return txnAsPromise(txn).then();
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist a list of [user id, presence event] they are for.
|
* Persist a list of [user id, presence event] they are for.
|
||||||
@@ -473,8 +460,8 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
* @param {Object[]} tuples An array of [userid, event] tuples
|
* @param {Object[]} tuples An array of [userid, event] tuples
|
||||||
* @return {Promise} Resolves if the users were persisted.
|
* @return {Promise} Resolves if the users were persisted.
|
||||||
*/
|
*/
|
||||||
_persistUserPresenceEvents: function(tuples) {
|
private persistUserPresenceEvents(tuples: UserTuple[]): Promise<void> {
|
||||||
return utils.promiseTry(() => {
|
return utils.promiseTry<void>(() => {
|
||||||
const txn = this.db.transaction(["users"], "readwrite");
|
const txn = this.db.transaction(["users"], "readwrite");
|
||||||
const store = txn.objectStore("users");
|
const store = txn.objectStore("users");
|
||||||
for (const tuple of tuples) {
|
for (const tuple of tuples) {
|
||||||
@@ -483,9 +470,9 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
event: tuple[1],
|
event: tuple[1],
|
||||||
}); // put == UPSERT
|
}); // put == UPSERT
|
||||||
}
|
}
|
||||||
return txnAsPromise(txn);
|
return txnAsPromise(txn).then();
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load all user presence events from the database. This is not cached.
|
* Load all user presence events from the database. This is not cached.
|
||||||
@@ -493,64 +480,56 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
* sync.
|
* sync.
|
||||||
* @return {Promise<Object[]>} A list of presence events in their raw form.
|
* @return {Promise<Object[]>} A list of presence events in their raw form.
|
||||||
*/
|
*/
|
||||||
getUserPresenceEvents: function() {
|
public getUserPresenceEvents(): Promise<UserTuple[]> {
|
||||||
return utils.promiseTry(() => {
|
return utils.promiseTry<UserTuple[]>(() => {
|
||||||
const txn = this.db.transaction(["users"], "readonly");
|
const txn = this.db.transaction(["users"], "readonly");
|
||||||
const store = txn.objectStore("users");
|
const store = txn.objectStore("users");
|
||||||
return selectQuery(store, undefined, (cursor) => {
|
return selectQuery(store, undefined, (cursor) => {
|
||||||
return [cursor.value.userId, cursor.value.event];
|
return [cursor.value.userId, cursor.value.event];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load all the account data events from the database. This is not cached.
|
* Load all the account data events from the database. This is not cached.
|
||||||
* @return {Promise<Object[]>} A list of raw global account events.
|
* @return {Promise<Object[]>} A list of raw global account events.
|
||||||
*/
|
*/
|
||||||
_loadAccountData: function() {
|
private loadAccountData(): Promise<IMinimalEvent[]> {
|
||||||
logger.log(
|
logger.log(`LocalIndexedDBStoreBackend: loading account data...`);
|
||||||
`LocalIndexedDBStoreBackend: loading account data...`,
|
return utils.promiseTry<IMinimalEvent[]>(() => {
|
||||||
);
|
|
||||||
return utils.promiseTry(() => {
|
|
||||||
const txn = this.db.transaction(["accountData"], "readonly");
|
const txn = this.db.transaction(["accountData"], "readonly");
|
||||||
const store = txn.objectStore("accountData");
|
const store = txn.objectStore("accountData");
|
||||||
return selectQuery(store, undefined, (cursor) => {
|
return selectQuery(store, undefined, (cursor) => {
|
||||||
return cursor.value;
|
return cursor.value;
|
||||||
}).then((result) => {
|
}).then((result: IMinimalEvent[]) => {
|
||||||
logger.log(
|
logger.log(`LocalIndexedDBStoreBackend: loaded account data`);
|
||||||
`LocalIndexedDBStoreBackend: loaded account data`,
|
|
||||||
);
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the sync data from the database.
|
* Load the sync data from the database.
|
||||||
* @return {Promise<Object>} An object with "roomsData" and "nextBatch" keys.
|
* @return {Promise<Object>} An object with "roomsData" and "nextBatch" keys.
|
||||||
*/
|
*/
|
||||||
_loadSyncData: function() {
|
private loadSyncData(): Promise<ISyncData> {
|
||||||
logger.log(
|
logger.log(`LocalIndexedDBStoreBackend: loading sync data...`);
|
||||||
`LocalIndexedDBStoreBackend: loading sync data...`,
|
return utils.promiseTry<ISyncData>(() => {
|
||||||
);
|
|
||||||
return utils.promiseTry(() => {
|
|
||||||
const txn = this.db.transaction(["sync"], "readonly");
|
const txn = this.db.transaction(["sync"], "readonly");
|
||||||
const store = txn.objectStore("sync");
|
const store = txn.objectStore("sync");
|
||||||
return selectQuery(store, undefined, (cursor) => {
|
return selectQuery(store, undefined, (cursor) => {
|
||||||
return cursor.value;
|
return cursor.value;
|
||||||
}).then((results) => {
|
}).then((results: ISyncData[]) => {
|
||||||
logger.log(
|
logger.log(`LocalIndexedDBStoreBackend: loaded sync data`);
|
||||||
`LocalIndexedDBStoreBackend: loaded sync data`,
|
|
||||||
);
|
|
||||||
if (results.length > 1) {
|
if (results.length > 1) {
|
||||||
logger.warn("loadSyncData: More than 1 sync row found.");
|
logger.warn("loadSyncData: More than 1 sync row found.");
|
||||||
}
|
}
|
||||||
return (results.length > 0 ? results[0] : {});
|
return results.length > 0 ? results[0] : {} as ISyncData;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
getClientOptions: function() {
|
public getClientOptions(): Promise<IStartClientOpts> {
|
||||||
return Promise.resolve().then(() => {
|
return Promise.resolve().then(() => {
|
||||||
const txn = this.db.transaction(["client_options"], "readonly");
|
const txn = this.db.transaction(["client_options"], "readonly");
|
||||||
const store = txn.objectStore("client_options");
|
const store = txn.objectStore("client_options");
|
||||||
@@ -560,9 +539,9 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
}
|
}
|
||||||
}).then((results) => results[0]);
|
}).then((results) => results[0]);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
storeClientOptions: async function(options) {
|
public async storeClientOptions(options: IStartClientOpts): Promise<void> {
|
||||||
const txn = this.db.transaction(["client_options"], "readwrite");
|
const txn = this.db.transaction(["client_options"], "readwrite");
|
||||||
const store = txn.objectStore("client_options");
|
const store = txn.objectStore("client_options");
|
||||||
store.put({
|
store.put({
|
||||||
@@ -570,5 +549,5 @@ LocalIndexedDBStoreBackend.prototype = {
|
|||||||
options: options,
|
options: options,
|
||||||
}); // put == UPSERT
|
}); // put == UPSERT
|
||||||
await txnAsPromise(txn);
|
await txnAsPromise(txn);
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { logger } from '../logger';
|
|
||||||
import { defer } from '../utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An IndexedDB store backend where the actual backend sits in a web
|
|
||||||
* worker.
|
|
||||||
*
|
|
||||||
* Construct a new Indexed Database store backend. This requires a call to
|
|
||||||
* <code>connect()</code> before this store can be used.
|
|
||||||
* @constructor
|
|
||||||
* @param {string} workerScript URL to the worker script
|
|
||||||
* @param {string=} dbName Optional database name. The same name must be used
|
|
||||||
* to open the same database.
|
|
||||||
* @param {Object} workerApi The web worker compatible interface object
|
|
||||||
*/
|
|
||||||
export function RemoteIndexedDBStoreBackend(
|
|
||||||
workerScript, dbName, workerApi,
|
|
||||||
) {
|
|
||||||
this._workerScript = workerScript;
|
|
||||||
this._dbName = dbName;
|
|
||||||
this._workerApi = workerApi;
|
|
||||||
this._worker = null;
|
|
||||||
this._nextSeq = 0;
|
|
||||||
// The currently in-flight requests to the actual backend
|
|
||||||
this._inFlight = {
|
|
||||||
// seq: promise,
|
|
||||||
};
|
|
||||||
// Once we start connecting, we keep the promise and re-use it
|
|
||||||
// if we try to connect again
|
|
||||||
this._startPromise = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoteIndexedDBStoreBackend.prototype = {
|
|
||||||
/**
|
|
||||||
* Attempt to connect to the database. This can fail if the user does not
|
|
||||||
* grant permission.
|
|
||||||
* @return {Promise} Resolves if successfully connected.
|
|
||||||
*/
|
|
||||||
connect: function() {
|
|
||||||
return this._ensureStarted().then(() => this._doCmd('connect'));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the entire database. This should be used when logging out of a client
|
|
||||||
* to prevent mixing data between accounts.
|
|
||||||
* @return {Promise} Resolved when the database is cleared.
|
|
||||||
*/
|
|
||||||
clearDatabase: function() {
|
|
||||||
return this._ensureStarted().then(() => this._doCmd('clearDatabase'));
|
|
||||||
},
|
|
||||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
|
||||||
isNewlyCreated: function() {
|
|
||||||
return this._doCmd('isNewlyCreated');
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* @return {Promise} Resolves with a sync response to restore the
|
|
||||||
* client state to where it was at the last save, or null if there
|
|
||||||
* is no saved sync data.
|
|
||||||
*/
|
|
||||||
getSavedSync: function() {
|
|
||||||
return this._doCmd('getSavedSync');
|
|
||||||
},
|
|
||||||
|
|
||||||
getNextBatchToken: function() {
|
|
||||||
return this._doCmd('getNextBatchToken');
|
|
||||||
},
|
|
||||||
|
|
||||||
setSyncData: function(syncData) {
|
|
||||||
return this._doCmd('setSyncData', [syncData]);
|
|
||||||
},
|
|
||||||
|
|
||||||
syncToDatabase: function(users) {
|
|
||||||
return this._doCmd('syncToDatabase', [users]);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the out-of-band membership events for this room that
|
|
||||||
* were previously loaded.
|
|
||||||
* @param {string} roomId
|
|
||||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
|
||||||
* @returns {null} in case the members for this room haven't been stored yet
|
|
||||||
*/
|
|
||||||
getOutOfBandMembers: function(roomId) {
|
|
||||||
return this._doCmd('getOutOfBandMembers', [roomId]);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores the out-of-band membership events for this room. Note that
|
|
||||||
* it still makes sense to store an empty array as the OOB status for the room is
|
|
||||||
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
|
|
||||||
* @param {string} roomId
|
|
||||||
* @param {event[]} membershipEvents the membership events to store
|
|
||||||
* @returns {Promise} when all members have been stored
|
|
||||||
*/
|
|
||||||
setOutOfBandMembers: function(roomId, membershipEvents) {
|
|
||||||
return this._doCmd('setOutOfBandMembers', [roomId, membershipEvents]);
|
|
||||||
},
|
|
||||||
|
|
||||||
clearOutOfBandMembers: function(roomId) {
|
|
||||||
return this._doCmd('clearOutOfBandMembers', [roomId]);
|
|
||||||
},
|
|
||||||
|
|
||||||
getClientOptions: function() {
|
|
||||||
return this._doCmd('getClientOptions');
|
|
||||||
},
|
|
||||||
|
|
||||||
storeClientOptions: function(options) {
|
|
||||||
return this._doCmd('storeClientOptions', [options]);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load all user presence events from the database. This is not cached.
|
|
||||||
* @return {Promise<Object[]>} A list of presence events in their raw form.
|
|
||||||
*/
|
|
||||||
getUserPresenceEvents: function() {
|
|
||||||
return this._doCmd('getUserPresenceEvents');
|
|
||||||
},
|
|
||||||
|
|
||||||
_ensureStarted: function() {
|
|
||||||
if (this._startPromise === null) {
|
|
||||||
this._worker = new this._workerApi(this._workerScript);
|
|
||||||
this._worker.onmessage = this._onWorkerMessage.bind(this);
|
|
||||||
|
|
||||||
// tell the worker the db name.
|
|
||||||
this._startPromise = this._doCmd('_setupWorker', [this._dbName]).then(() => {
|
|
||||||
logger.log("IndexedDB worker is ready");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this._startPromise;
|
|
||||||
},
|
|
||||||
|
|
||||||
_doCmd: function(cmd, args) {
|
|
||||||
// wrap in a q so if the postMessage throws,
|
|
||||||
// the promise automatically gets rejected
|
|
||||||
return Promise.resolve().then(() => {
|
|
||||||
const seq = this._nextSeq++;
|
|
||||||
const def = defer();
|
|
||||||
|
|
||||||
this._inFlight[seq] = def;
|
|
||||||
|
|
||||||
this._worker.postMessage({
|
|
||||||
command: cmd,
|
|
||||||
seq: seq,
|
|
||||||
args: args,
|
|
||||||
});
|
|
||||||
|
|
||||||
return def.promise;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_onWorkerMessage: function(ev) {
|
|
||||||
const msg = ev.data;
|
|
||||||
|
|
||||||
if (msg.command == 'cmd_success' || msg.command == 'cmd_fail') {
|
|
||||||
if (msg.seq === undefined) {
|
|
||||||
logger.error("Got reply from worker with no seq");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const def = this._inFlight[msg.seq];
|
|
||||||
if (def === undefined) {
|
|
||||||
logger.error("Got reply for unknown seq " + msg.seq);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
delete this._inFlight[msg.seq];
|
|
||||||
|
|
||||||
if (msg.command == 'cmd_success') {
|
|
||||||
def.resolve(msg.result);
|
|
||||||
} else {
|
|
||||||
const error = new Error(msg.error.message);
|
|
||||||
error.name = msg.error.name;
|
|
||||||
def.reject(error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.warn("Unrecognised message from worker: " + msg);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
192
src/store/indexeddb-remote-backend.ts
Normal file
192
src/store/indexeddb-remote-backend.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logger } from "../logger";
|
||||||
|
import { defer, IDeferred } from "../utils";
|
||||||
|
import { ISavedSync } from "./index";
|
||||||
|
import { IStartClientOpts } from "../client";
|
||||||
|
import { IEvent, ISyncResponse } from "..";
|
||||||
|
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
|
||||||
|
|
||||||
|
export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
|
||||||
|
private worker: Worker;
|
||||||
|
private nextSeq = 0;
|
||||||
|
// The currently in-flight requests to the actual backend
|
||||||
|
private inFlight: Record<number, IDeferred<any>> = {}; // seq: promise
|
||||||
|
// Once we start connecting, we keep the promise and re-use it
|
||||||
|
// if we try to connect again
|
||||||
|
private startPromise: Promise<void> = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An IndexedDB store backend where the actual backend sits in a web
|
||||||
|
* worker.
|
||||||
|
*
|
||||||
|
* Construct a new Indexed Database store backend. This requires a call to
|
||||||
|
* <code>connect()</code> before this store can be used.
|
||||||
|
* @constructor
|
||||||
|
* @param {Function} workerFactory Factory which produces a Worker
|
||||||
|
* @param {string=} dbName Optional database name. The same name must be used
|
||||||
|
* to open the same database.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private readonly workerFactory: () => Worker,
|
||||||
|
private readonly dbName: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to connect to the database. This can fail if the user does not
|
||||||
|
* grant permission.
|
||||||
|
* @return {Promise} Resolves if successfully connected.
|
||||||
|
*/
|
||||||
|
public connect(): Promise<void> {
|
||||||
|
return this.ensureStarted().then(() => this.doCmd('connect'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the entire database. This should be used when logging out of a client
|
||||||
|
* to prevent mixing data between accounts.
|
||||||
|
* @return {Promise} Resolved when the database is cleared.
|
||||||
|
*/
|
||||||
|
public clearDatabase(): Promise<void> {
|
||||||
|
return this.ensureStarted().then(() => this.doCmd('clearDatabase'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return {Promise<boolean>} whether or not the database was newly created in this session. */
|
||||||
|
public isNewlyCreated(): Promise<boolean> {
|
||||||
|
return this.doCmd('isNewlyCreated');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise} Resolves with a sync response to restore the
|
||||||
|
* client state to where it was at the last save, or null if there
|
||||||
|
* is no saved sync data.
|
||||||
|
*/
|
||||||
|
public getSavedSync(): Promise<ISavedSync> {
|
||||||
|
return this.doCmd('getSavedSync');
|
||||||
|
}
|
||||||
|
|
||||||
|
public getNextBatchToken(): Promise<string> {
|
||||||
|
return this.doCmd('getNextBatchToken');
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSyncData(syncData: ISyncResponse): Promise<void> {
|
||||||
|
return this.doCmd('setSyncData', [syncData]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public syncToDatabase(userTuples: UserTuple[]): Promise<void> {
|
||||||
|
return this.doCmd('syncToDatabase', [userTuples]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the out-of-band membership events for this room that
|
||||||
|
* were previously loaded.
|
||||||
|
* @param {string} roomId
|
||||||
|
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||||
|
* @returns {null} in case the members for this room haven't been stored yet
|
||||||
|
*/
|
||||||
|
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
|
||||||
|
return this.doCmd('getOutOfBandMembers', [roomId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the out-of-band membership events for this room. Note that
|
||||||
|
* it still makes sense to store an empty array as the OOB status for the room is
|
||||||
|
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
|
||||||
|
* @param {string} roomId
|
||||||
|
* @param {event[]} membershipEvents the membership events to store
|
||||||
|
* @returns {Promise} when all members have been stored
|
||||||
|
*/
|
||||||
|
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
||||||
|
return this.doCmd('setOutOfBandMembers', [roomId, membershipEvents]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearOutOfBandMembers(roomId: string): Promise<void> {
|
||||||
|
return this.doCmd('clearOutOfBandMembers', [roomId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getClientOptions(): Promise<IStartClientOpts> {
|
||||||
|
return this.doCmd('getClientOptions');
|
||||||
|
}
|
||||||
|
|
||||||
|
public storeClientOptions(options: IStartClientOpts): Promise<void> {
|
||||||
|
return this.doCmd('storeClientOptions', [options]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all user presence events from the database. This is not cached.
|
||||||
|
* @return {Promise<Object[]>} A list of presence events in their raw form.
|
||||||
|
*/
|
||||||
|
public getUserPresenceEvents(): Promise<UserTuple[]> {
|
||||||
|
return this.doCmd('getUserPresenceEvents');
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureStarted(): Promise<void> {
|
||||||
|
if (this.startPromise === null) {
|
||||||
|
this.worker = this.workerFactory();
|
||||||
|
this.worker.onmessage = this.onWorkerMessage;
|
||||||
|
|
||||||
|
// tell the worker the db name.
|
||||||
|
this.startPromise = this.doCmd('_setupWorker', [this.dbName]).then(() => {
|
||||||
|
logger.log("IndexedDB worker is ready");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.startPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private doCmd<T>(command: string, args?: any): Promise<T> {
|
||||||
|
// wrap in a q so if the postMessage throws,
|
||||||
|
// the promise automatically gets rejected
|
||||||
|
return Promise.resolve().then(() => {
|
||||||
|
const seq = this.nextSeq++;
|
||||||
|
const def = defer<T>();
|
||||||
|
|
||||||
|
this.inFlight[seq] = def;
|
||||||
|
|
||||||
|
this.worker.postMessage({ command, seq, args });
|
||||||
|
|
||||||
|
return def.promise;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onWorkerMessage = (ev: MessageEvent): void => {
|
||||||
|
const msg = ev.data;
|
||||||
|
|
||||||
|
if (msg.command == 'cmd_success' || msg.command == 'cmd_fail') {
|
||||||
|
if (msg.seq === undefined) {
|
||||||
|
logger.error("Got reply from worker with no seq");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const def = this.inFlight[msg.seq];
|
||||||
|
if (def === undefined) {
|
||||||
|
logger.error("Got reply for unknown seq " + msg.seq);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete this.inFlight[msg.seq];
|
||||||
|
|
||||||
|
if (msg.command == 'cmd_success') {
|
||||||
|
def.resolve(msg.result);
|
||||||
|
} else {
|
||||||
|
const error = new Error(msg.error.message);
|
||||||
|
error.name = msg.error.name;
|
||||||
|
def.reject(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn("Unrecognised message from worker: ", msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -16,9 +14,15 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend.js";
|
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend";
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
|
|
||||||
|
interface ICmd {
|
||||||
|
command: string;
|
||||||
|
seq: number;
|
||||||
|
args?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class lives in the webworker and drives a LocalIndexedDBStoreBackend
|
* This class lives in the webworker and drives a LocalIndexedDBStoreBackend
|
||||||
* controlled by messages from the main process.
|
* controlled by messages from the main process.
|
||||||
@@ -35,16 +39,13 @@ import { logger } from '../logger';
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export class IndexedDBStoreWorker {
|
export class IndexedDBStoreWorker {
|
||||||
|
private backend: LocalIndexedDBStoreBackend = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {function} postMessage The web worker postMessage function that
|
* @param {function} postMessage The web worker postMessage function that
|
||||||
* should be used to communicate back to the main script.
|
* should be used to communicate back to the main script.
|
||||||
*/
|
*/
|
||||||
constructor(postMessage) {
|
constructor(private readonly postMessage: InstanceType<typeof Worker>["postMessage"]) {}
|
||||||
this.backend = null;
|
|
||||||
this.postMessage = postMessage;
|
|
||||||
|
|
||||||
this.onMessage = this.onMessage.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Passes a message event from the main script into the class. This method
|
* Passes a message event from the main script into the class. This method
|
||||||
@@ -52,17 +53,15 @@ export class IndexedDBStoreWorker {
|
|||||||
*
|
*
|
||||||
* @param {Object} ev The message event
|
* @param {Object} ev The message event
|
||||||
*/
|
*/
|
||||||
onMessage(ev) {
|
public onMessage = (ev: MessageEvent): void => {
|
||||||
const msg = ev.data;
|
const msg: ICmd = ev.data;
|
||||||
let prom;
|
let prom;
|
||||||
|
|
||||||
switch (msg.command) {
|
switch (msg.command) {
|
||||||
case '_setupWorker':
|
case '_setupWorker':
|
||||||
this.backend = new LocalIndexedDBStoreBackend(
|
// this is the 'indexedDB' global (where global != window
|
||||||
// this is the 'indexedDB' global (where global != window
|
// because it's a web worker and there is no window).
|
||||||
// because it's a web worker and there is no window).
|
this.backend = new LocalIndexedDBStoreBackend(indexedDB, msg.args[0]);
|
||||||
indexedDB, msg.args[0],
|
|
||||||
);
|
|
||||||
prom = Promise.resolve();
|
prom = Promise.resolve();
|
||||||
break;
|
break;
|
||||||
case 'connect':
|
case 'connect':
|
||||||
@@ -72,23 +71,16 @@ export class IndexedDBStoreWorker {
|
|||||||
prom = this.backend.isNewlyCreated();
|
prom = this.backend.isNewlyCreated();
|
||||||
break;
|
break;
|
||||||
case 'clearDatabase':
|
case 'clearDatabase':
|
||||||
prom = this.backend.clearDatabase().then((result) => {
|
prom = this.backend.clearDatabase();
|
||||||
// This returns special classes which can't be cloned
|
|
||||||
// across to the main script, so don't try.
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case 'getSavedSync':
|
case 'getSavedSync':
|
||||||
prom = this.backend.getSavedSync(false);
|
prom = this.backend.getSavedSync(false);
|
||||||
break;
|
break;
|
||||||
case 'setSyncData':
|
case 'setSyncData':
|
||||||
prom = this.backend.setSyncData(...msg.args);
|
prom = this.backend.setSyncData(msg.args[0]);
|
||||||
break;
|
break;
|
||||||
case 'syncToDatabase':
|
case 'syncToDatabase':
|
||||||
prom = this.backend.syncToDatabase(...msg.args).then(() => {
|
prom = this.backend.syncToDatabase(msg.args[0]);
|
||||||
// This also returns IndexedDB events which are not cloneable
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case 'getUserPresenceEvents':
|
case 'getUserPresenceEvents':
|
||||||
prom = this.backend.getUserPresenceEvents();
|
prom = this.backend.getUserPresenceEvents();
|
||||||
@@ -130,7 +122,7 @@ export class IndexedDBStoreWorker {
|
|||||||
result: ret,
|
result: ret,
|
||||||
});
|
});
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
logger.error("Error running command: "+msg.command);
|
logger.error("Error running command: " + msg.command);
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
this.postMessage.call(null, {
|
this.postMessage.call(null, {
|
||||||
command: 'cmd_fail',
|
command: 'cmd_fail',
|
||||||
@@ -142,5 +134,5 @@ export class IndexedDBStoreWorker {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
@@ -19,12 +19,14 @@ limitations under the License.
|
|||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
import { MemoryStore, IOpts as IBaseOpts } from "./memory";
|
import { MemoryStore, IOpts as IBaseOpts } from "./memory";
|
||||||
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend.js";
|
import { LocalIndexedDBStoreBackend } from "./indexeddb-local-backend";
|
||||||
import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend.js";
|
import { RemoteIndexedDBStoreBackend } from "./indexeddb-remote-backend";
|
||||||
import { User } from "../models/user";
|
import { User } from "../models/user";
|
||||||
import { MatrixEvent } from "../models/event";
|
import { IEvent, MatrixEvent } from "../models/event";
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import { ISavedSync } from "./index";
|
import { ISavedSync } from "./index";
|
||||||
|
import { IIndexedDBBackend } from "./indexeddb-backend";
|
||||||
|
import { ISyncResponse } from "../sync-accumulator";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is an internal module. See {@link IndexedDBStore} for the public class.
|
* This is an internal module. See {@link IndexedDBStore} for the public class.
|
||||||
@@ -41,8 +43,7 @@ const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
|
|||||||
interface IOpts extends IBaseOpts {
|
interface IOpts extends IBaseOpts {
|
||||||
indexedDB: IDBFactory;
|
indexedDB: IDBFactory;
|
||||||
dbName?: string;
|
dbName?: string;
|
||||||
workerScript?: string;
|
workerFactory?: () => Worker;
|
||||||
workerApi?: typeof Worker;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IndexedDBStore extends MemoryStore {
|
export class IndexedDBStore extends MemoryStore {
|
||||||
@@ -50,8 +51,7 @@ export class IndexedDBStore extends MemoryStore {
|
|||||||
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
|
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO these should conform to one interface
|
public readonly backend: IIndexedDBBackend;
|
||||||
public readonly backend: LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend;
|
|
||||||
|
|
||||||
private startedUp = false;
|
private startedUp = false;
|
||||||
private syncTs = 0;
|
private syncTs = 0;
|
||||||
@@ -110,16 +110,8 @@ export class IndexedDBStore extends MemoryStore {
|
|||||||
throw new Error('Missing required option: indexedDB');
|
throw new Error('Missing required option: indexedDB');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.workerScript) {
|
if (opts.workerFactory) {
|
||||||
// try & find a webworker-compatible API
|
this.backend = new RemoteIndexedDBStoreBackend(opts.workerFactory, opts.dbName);
|
||||||
let workerApi = opts.workerApi;
|
|
||||||
if (!workerApi) {
|
|
||||||
// default to the global Worker object (which is where it in a browser)
|
|
||||||
workerApi = global.Worker;
|
|
||||||
}
|
|
||||||
this.backend = new RemoteIndexedDBStoreBackend(
|
|
||||||
opts.workerScript, opts.dbName, workerApi,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
this.backend = new LocalIndexedDBStoreBackend(opts.indexedDB, opts.dbName);
|
this.backend = new LocalIndexedDBStoreBackend(opts.indexedDB, opts.dbName);
|
||||||
}
|
}
|
||||||
@@ -222,7 +214,7 @@ export class IndexedDBStore extends MemoryStore {
|
|||||||
|
|
||||||
// work out changed users (this doesn't handle deletions but you
|
// work out changed users (this doesn't handle deletions but you
|
||||||
// can't 'delete' users as they are just presence events).
|
// can't 'delete' users as they are just presence events).
|
||||||
const userTuples = [];
|
const userTuples: [userId: string, presenceEvent: Partial<IEvent>][] = [];
|
||||||
for (const u of this.getUsers()) {
|
for (const u of this.getUsers()) {
|
||||||
if (this.userModifiedMap[u.userId] === u.getLastModifiedTime()) continue;
|
if (this.userModifiedMap[u.userId] === u.getLastModifiedTime()) continue;
|
||||||
if (!u.events.presence) continue;
|
if (!u.events.presence) continue;
|
||||||
@@ -236,7 +228,7 @@ export class IndexedDBStore extends MemoryStore {
|
|||||||
return this.backend.syncToDatabase(userTuples);
|
return this.backend.syncToDatabase(userTuples);
|
||||||
});
|
});
|
||||||
|
|
||||||
public setSyncData = this.degradable((syncData: object): Promise<void> => {
|
public setSyncData = this.degradable((syncData: ISyncResponse): Promise<void> => {
|
||||||
return this.backend.setSyncData(syncData);
|
return this.backend.setSyncData(syncData);
|
||||||
}, "setSyncData");
|
}, "setSyncData");
|
||||||
|
|
||||||
@@ -247,7 +239,7 @@ export class IndexedDBStore extends MemoryStore {
|
|||||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||||
* @returns {null} in case the members for this room haven't been stored yet
|
* @returns {null} in case the members for this room haven't been stored yet
|
||||||
*/
|
*/
|
||||||
public getOutOfBandMembers = this.degradable((roomId: string): Promise<MatrixEvent[]> => {
|
public getOutOfBandMembers = this.degradable((roomId: string): Promise<IEvent[]> => {
|
||||||
return this.backend.getOutOfBandMembers(roomId);
|
return this.backend.getOutOfBandMembers(roomId);
|
||||||
}, "getOutOfBandMembers");
|
}, "getOutOfBandMembers");
|
||||||
|
|
||||||
@@ -259,7 +251,7 @@ export class IndexedDBStore extends MemoryStore {
|
|||||||
* @param {event[]} membershipEvents the membership events to store
|
* @param {event[]} membershipEvents the membership events to store
|
||||||
* @returns {Promise} when all members have been stored
|
* @returns {Promise} when all members have been stored
|
||||||
*/
|
*/
|
||||||
public setOutOfBandMembers = this.degradable((roomId: string, membershipEvents: MatrixEvent[]): Promise<void> => {
|
public setOutOfBandMembers = this.degradable((roomId: string, membershipEvents: IEvent[]): Promise<void> => {
|
||||||
super.setOutOfBandMembers(roomId, membershipEvents);
|
super.setOutOfBandMembers(roomId, membershipEvents);
|
||||||
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
|
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
|
||||||
}, "setOutOfBandMembers");
|
}, "setOutOfBandMembers");
|
||||||
|
|||||||
@@ -23,12 +23,13 @@ import { EventType } from "../@types/event";
|
|||||||
import { Group } from "../models/group";
|
import { Group } from "../models/group";
|
||||||
import { Room } from "../models/room";
|
import { Room } from "../models/room";
|
||||||
import { User } from "../models/user";
|
import { User } from "../models/user";
|
||||||
import { MatrixEvent } from "../models/event";
|
import { IEvent, MatrixEvent } from "../models/event";
|
||||||
import { RoomState } from "../models/room-state";
|
import { RoomState } from "../models/room-state";
|
||||||
import { RoomMember } from "../models/room-member";
|
import { RoomMember } from "../models/room-member";
|
||||||
import { Filter } from "../filter";
|
import { Filter } from "../filter";
|
||||||
import { ISavedSync, IStore } from "./index";
|
import { ISavedSync, IStore } from "./index";
|
||||||
import { RoomSummary } from "../models/room-summary";
|
import { RoomSummary } from "../models/room-summary";
|
||||||
|
import { ISyncResponse } from "../sync-accumulator";
|
||||||
|
|
||||||
function isValidFilterId(filterId: string): boolean {
|
function isValidFilterId(filterId: string): boolean {
|
||||||
const isValidStr = typeof filterId === "string" &&
|
const isValidStr = typeof filterId === "string" &&
|
||||||
@@ -59,9 +60,9 @@ export class MemoryStore implements IStore {
|
|||||||
// filterId: Filter
|
// filterId: Filter
|
||||||
// }
|
// }
|
||||||
private filters: Record<string, Record<string, Filter>> = {};
|
private filters: Record<string, Record<string, Filter>> = {};
|
||||||
private accountData: Record<string, MatrixEvent> = {}; // type : content
|
public accountData: Record<string, MatrixEvent> = {}; // type : content
|
||||||
private readonly localStorage: Storage;
|
private readonly localStorage: Storage;
|
||||||
private oobMembers: Record<string, MatrixEvent[]> = {}; // roomId: [member events]
|
private oobMembers: Record<string, IEvent[]> = {}; // roomId: [member events]
|
||||||
private clientOptions = {};
|
private clientOptions = {};
|
||||||
|
|
||||||
constructor(opts: IOpts = {}) {
|
constructor(opts: IOpts = {}) {
|
||||||
@@ -340,7 +341,7 @@ export class MemoryStore implements IStore {
|
|||||||
* @param {Object} syncData The sync data
|
* @param {Object} syncData The sync data
|
||||||
* @return {Promise} An immediately resolved promise.
|
* @return {Promise} An immediately resolved promise.
|
||||||
*/
|
*/
|
||||||
public setSyncData(syncData: object): Promise<void> {
|
public setSyncData(syncData: ISyncResponse): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,7 +416,7 @@ export class MemoryStore implements IStore {
|
|||||||
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
|
||||||
* @returns {null} in case the members for this room haven't been stored yet
|
* @returns {null} in case the members for this room haven't been stored yet
|
||||||
*/
|
*/
|
||||||
public getOutOfBandMembers(roomId: string): Promise<MatrixEvent[] | null> {
|
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
|
||||||
return Promise.resolve(this.oobMembers[roomId] || null);
|
return Promise.resolve(this.oobMembers[roomId] || null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +428,7 @@ export class MemoryStore implements IStore {
|
|||||||
* @param {event[]} membershipEvents the membership events to store
|
* @param {event[]} membershipEvents the membership events to store
|
||||||
* @returns {Promise} when all members have been stored
|
* @returns {Promise} when all members have been stored
|
||||||
*/
|
*/
|
||||||
public setOutOfBandMembers(roomId: string, membershipEvents: MatrixEvent[]): Promise<void> {
|
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
||||||
this.oobMembers[roomId] = membershipEvents;
|
this.oobMembers[roomId] = membershipEvents;
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,19 +23,21 @@ import { EventType } from "../@types/event";
|
|||||||
import { Group } from "../models/group";
|
import { Group } from "../models/group";
|
||||||
import { Room } from "../models/room";
|
import { Room } from "../models/room";
|
||||||
import { User } from "../models/user";
|
import { User } from "../models/user";
|
||||||
import { MatrixEvent } from "../models/event";
|
import { IEvent, MatrixEvent } from "../models/event";
|
||||||
import { Filter } from "../filter";
|
import { Filter } from "../filter";
|
||||||
import { ISavedSync, IStore } from "./index";
|
import { ISavedSync, IStore } from "./index";
|
||||||
import { RoomSummary } from "../models/room-summary";
|
import { RoomSummary } from "../models/room-summary";
|
||||||
|
import { ISyncResponse } from "../sync-accumulator";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a stub store. This does no-ops on most store methods.
|
* Construct a stub store. This does no-ops on most store methods.
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export class StubStore implements IStore {
|
export class StubStore implements IStore {
|
||||||
|
public readonly accountData = {}; // stub
|
||||||
private fromToken: string = null;
|
private fromToken: string = null;
|
||||||
|
|
||||||
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
|
/** @return {Promise<boolean>} whether or not the database was newly created in this session. */
|
||||||
public isNewlyCreated(): Promise<boolean> {
|
public isNewlyCreated(): Promise<boolean> {
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
@@ -212,7 +214,7 @@ export class StubStore implements IStore {
|
|||||||
* @param {Object} syncData The sync data
|
* @param {Object} syncData The sync data
|
||||||
* @return {Promise} An immediately resolved promise.
|
* @return {Promise} An immediately resolved promise.
|
||||||
*/
|
*/
|
||||||
public setSyncData(syncData: object): Promise<void> {
|
public setSyncData(syncData: ISyncResponse): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,11 +266,11 @@ export class StubStore implements IStore {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getOutOfBandMembers(): Promise<MatrixEvent[]> {
|
public getOutOfBandMembers(): Promise<IEvent[]> {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setOutOfBandMembers(roomId: string, membershipEvents: MatrixEvent[]): Promise<void> {
|
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ export interface IEphemeral {
|
|||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
interface IUnreadNotificationCounts {
|
interface IUnreadNotificationCounts {
|
||||||
highlight_count: number;
|
highlight_count?: number;
|
||||||
notification_count: number;
|
notification_count?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRoomEvent extends IMinimalEvent {
|
export interface IRoomEvent extends IMinimalEvent {
|
||||||
@@ -64,7 +64,7 @@ interface IState {
|
|||||||
|
|
||||||
export interface ITimeline {
|
export interface ITimeline {
|
||||||
events: Array<IRoomEvent | IStateEvent>;
|
events: Array<IRoomEvent | IStateEvent>;
|
||||||
limited: boolean;
|
limited?: boolean;
|
||||||
prev_batch: string;
|
prev_batch: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +169,13 @@ interface IRoom {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ISyncData {
|
||||||
|
nextBatch: string;
|
||||||
|
accountData: IMinimalEvent[];
|
||||||
|
roomsData: IRooms;
|
||||||
|
groupsData: IGroups;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The purpose of this class is to accumulate /sync responses such that a
|
* The purpose of this class is to accumulate /sync responses such that a
|
||||||
* complete "initial" JSON response can be returned which accurately represents
|
* complete "initial" JSON response can be returned which accurately represents
|
||||||
@@ -544,8 +551,8 @@ export class SyncAccumulator {
|
|||||||
* /sync response from the 'rooms' key onwards. The "accountData" key is
|
* /sync response from the 'rooms' key onwards. The "accountData" key is
|
||||||
* a list of raw events which represent global account data.
|
* a list of raw events which represent global account data.
|
||||||
*/
|
*/
|
||||||
public getJSON(forDatabase = false): object {
|
public getJSON(forDatabase = false): ISyncData {
|
||||||
const data = {
|
const data: IRooms = {
|
||||||
join: {},
|
join: {},
|
||||||
invite: {},
|
invite: {},
|
||||||
// always empty. This is set by /sync when a room was previously
|
// always empty. This is set by /sync when a room was previously
|
||||||
@@ -575,7 +582,7 @@ export class SyncAccumulator {
|
|||||||
prev_batch: null,
|
prev_batch: null,
|
||||||
},
|
},
|
||||||
unread_notifications: roomData._unreadNotifications,
|
unread_notifications: roomData._unreadNotifications,
|
||||||
summary: roomData._summary,
|
summary: roomData._summary as IRoomSummary,
|
||||||
};
|
};
|
||||||
// Add account data
|
// Add account data
|
||||||
Object.keys(roomData._accountData).forEach((evType) => {
|
Object.keys(roomData._accountData).forEach((evType) => {
|
||||||
@@ -678,7 +685,7 @@ export class SyncAccumulator {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add account data
|
// Add account data
|
||||||
const accData = [];
|
const accData: IMinimalEvent[] = [];
|
||||||
Object.keys(this.accountData).forEach((evType) => {
|
Object.keys(this.accountData).forEach((evType) => {
|
||||||
accData.push(this.accountData[evType]);
|
accData.push(this.accountData[evType]);
|
||||||
});
|
});
|
||||||
|
|||||||
17
src/sync.ts
17
src/sync.ts
@@ -135,7 +135,7 @@ export class SyncApi {
|
|||||||
private syncStateData: ISyncStateData = null; // additional data (eg. error object for failed sync)
|
private syncStateData: ISyncStateData = null; // additional data (eg. error object for failed sync)
|
||||||
private catchingUp = false;
|
private catchingUp = false;
|
||||||
private running = false;
|
private running = false;
|
||||||
private keepAliveTimer: NodeJS.Timeout = null;
|
private keepAliveTimer: number = null;
|
||||||
private connectionReturnedDefer: IDeferred<boolean> = null;
|
private connectionReturnedDefer: IDeferred<boolean> = null;
|
||||||
private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response
|
private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response
|
||||||
private failedSyncCount = 0; // Number of consecutive failed /sync requests
|
private failedSyncCount = 0; // Number of consecutive failed /sync requests
|
||||||
@@ -318,7 +318,7 @@ export class SyncApi {
|
|||||||
this._peekRoom = this.createRoom(roomId);
|
this._peekRoom = this.createRoom(roomId);
|
||||||
return this.client.roomInitialSync(roomId, 20).then((response) => {
|
return this.client.roomInitialSync(roomId, 20).then((response) => {
|
||||||
// make sure things are init'd
|
// make sure things are init'd
|
||||||
response.messages = response.messages || {};
|
response.messages = response.messages || { chunk: [] };
|
||||||
response.messages.chunk = response.messages.chunk || [];
|
response.messages.chunk = response.messages.chunk || [];
|
||||||
response.state = response.state || [];
|
response.state = response.state || [];
|
||||||
|
|
||||||
@@ -329,8 +329,7 @@ export class SyncApi {
|
|||||||
const stateEvents = response.state.map(client.getEventMapper());
|
const stateEvents = response.state.map(client.getEventMapper());
|
||||||
const messages = response.messages.chunk.map(client.getEventMapper());
|
const messages = response.messages.chunk.map(client.getEventMapper());
|
||||||
|
|
||||||
// XXX: copypasted from /sync until we kill off this
|
// XXX: copypasted from /sync until we kill off this minging v1 API stuff)
|
||||||
// minging v1 API stuff)
|
|
||||||
// handle presence events (User objects)
|
// handle presence events (User objects)
|
||||||
if (response.presence && Array.isArray(response.presence)) {
|
if (response.presence && Array.isArray(response.presence)) {
|
||||||
response.presence.map(client.getEventMapper()).forEach(
|
response.presence.map(client.getEventMapper()).forEach(
|
||||||
@@ -643,12 +642,12 @@ export class SyncApi {
|
|||||||
// Now wait for the saved sync to finish...
|
// Now wait for the saved sync to finish...
|
||||||
debuglog("Waiting for saved sync before starting sync processing...");
|
debuglog("Waiting for saved sync before starting sync processing...");
|
||||||
await savedSyncPromise;
|
await savedSyncPromise;
|
||||||
this._sync({ filterId });
|
this.doSync({ filterId });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (client.isGuest()) {
|
if (client.isGuest()) {
|
||||||
// no push rules for guests, no access to POST filter for guests.
|
// no push rules for guests, no access to POST filter for guests.
|
||||||
this._sync({});
|
this.doSync({});
|
||||||
} else {
|
} else {
|
||||||
// Pull the saved sync token out first, before the worker starts sending
|
// Pull the saved sync token out first, before the worker starts sending
|
||||||
// all the sync data which could take a while. This will let us send our
|
// all the sync data which could take a while. This will let us send our
|
||||||
@@ -754,7 +753,7 @@ export class SyncApi {
|
|||||||
* @param {string} syncOptions.filterId
|
* @param {string} syncOptions.filterId
|
||||||
* @param {boolean} syncOptions.hasSyncedBefore
|
* @param {boolean} syncOptions.hasSyncedBefore
|
||||||
*/
|
*/
|
||||||
private async _sync(syncOptions: ISyncOptions): Promise<void> {
|
private async doSync(syncOptions: ISyncOptions): Promise<void> {
|
||||||
const client = this.client;
|
const client = this.client;
|
||||||
|
|
||||||
if (!this.running) {
|
if (!this.running) {
|
||||||
@@ -851,7 +850,7 @@ export class SyncApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Begin next sync
|
// Begin next sync
|
||||||
this._sync(syncOptions);
|
this.doSync(syncOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IRequestPromise<ISyncResponse> {
|
private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): IRequestPromise<ISyncResponse> {
|
||||||
@@ -956,7 +955,7 @@ export class SyncApi {
|
|||||||
catchingUp: true,
|
catchingUp: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this._sync(syncOptions);
|
this.doSync(syncOptions);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.currentSyncRequest = null;
|
this.currentSyncRequest = null;
|
||||||
|
|||||||
49
src/utils.ts
49
src/utils.ts
@@ -31,15 +31,28 @@ import type NodeCrypto from "crypto";
|
|||||||
* @return {string} The encoded string e.g. foo=bar&baz=taz
|
* @return {string} The encoded string e.g. foo=bar&baz=taz
|
||||||
*/
|
*/
|
||||||
export function encodeParams(params: Record<string, string>): string {
|
export function encodeParams(params: Record<string, string>): string {
|
||||||
let qs = "";
|
return new URLSearchParams(params).toString();
|
||||||
for (const key in params) {
|
}
|
||||||
if (!params.hasOwnProperty(key)) {
|
|
||||||
continue;
|
export type QueryDict = Record<string, string | string[]>;
|
||||||
}
|
|
||||||
qs += "&" + encodeURIComponent(key) + "=" +
|
/**
|
||||||
encodeURIComponent(params[key]);
|
* Decode a query string in `application/x-www-form-urlencoded` format.
|
||||||
|
* @param {string} query A query string to decode e.g.
|
||||||
|
* foo=bar&via=server1&server2
|
||||||
|
* @return {Object} The decoded object, if any keys occurred multiple times
|
||||||
|
* then the value will be an array of strings, else it will be an array.
|
||||||
|
* This behaviour matches Node's qs.parse but is built on URLSearchParams
|
||||||
|
* for native web compatibility
|
||||||
|
*/
|
||||||
|
export function decodeParams(query: string): QueryDict {
|
||||||
|
const o: QueryDict = {};
|
||||||
|
const params = new URLSearchParams(query);
|
||||||
|
for (const key of params.keys()) {
|
||||||
|
const val = params.getAll(key);
|
||||||
|
o[key] = val.length === 1 ? val[0] : val;
|
||||||
}
|
}
|
||||||
return qs.substring(1);
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -116,10 +129,10 @@ export function isFunction(value: any) {
|
|||||||
* @throws If the object is missing keys.
|
* @throws If the object is missing keys.
|
||||||
*/
|
*/
|
||||||
// note using 'keys' here would shadow the 'keys' function defined above
|
// note using 'keys' here would shadow the 'keys' function defined above
|
||||||
export function checkObjectHasKeys(obj: object, keys_: string[]) {
|
export function checkObjectHasKeys(obj: object, keys: string[]) {
|
||||||
for (let i = 0; i < keys_.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
if (!obj.hasOwnProperty(keys_[i])) {
|
if (!obj.hasOwnProperty(keys[i])) {
|
||||||
throw new Error("Missing required key: " + keys_[i]);
|
throw new Error("Missing required key: " + keys[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -464,7 +477,7 @@ export async function promiseMapSeries<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function promiseTry<T>(fn: () => T): Promise<T> {
|
export function promiseTry<T>(fn: () => T | Promise<T>): Promise<T> {
|
||||||
return new Promise((resolve) => resolve(fn()));
|
return new Promise((resolve) => resolve(fn()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -670,3 +683,13 @@ export function lexicographicCompare(a: string, b: string): number {
|
|||||||
// hidden the operation in this function.
|
// hidden the operation in this function.
|
||||||
return (a < b) ? -1 : ((a === b) ? 0 : 1);
|
return (a < b) ? -1 : ((a === b) ? 0 : 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const collator = new Intl.Collator();
|
||||||
|
/**
|
||||||
|
* Performant language-sensitive string comparison
|
||||||
|
* @param a the first string to compare
|
||||||
|
* @param b the second string to compare
|
||||||
|
*/
|
||||||
|
export function compare(a: string, b: string): number {
|
||||||
|
return collator.compare(a, b);
|
||||||
|
}
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ export class MatrixCall extends EventEmitter {
|
|||||||
// yet, null if we have but they didn't send a party ID.
|
// yet, null if we have but they didn't send a party ID.
|
||||||
private opponentPartyId: string;
|
private opponentPartyId: string;
|
||||||
private opponentCaps: CallCapabilities;
|
private opponentCaps: CallCapabilities;
|
||||||
private inviteTimeout: NodeJS.Timeout; // in the browser it's 'number'
|
private inviteTimeout: number;
|
||||||
|
|
||||||
// The logic of when & if a call is on hold is nontrivial and explained in is*OnHold
|
// The logic of when & if a call is on hold is nontrivial and explained in is*OnHold
|
||||||
// This flag represents whether we want the other party to be on hold
|
// This flag represents whether we want the other party to be on hold
|
||||||
|
|||||||
@@ -47,12 +47,12 @@ export class CallEventHandler {
|
|||||||
|
|
||||||
public start() {
|
public start() {
|
||||||
this.client.on("sync", this.evaluateEventBuffer);
|
this.client.on("sync", this.evaluateEventBuffer);
|
||||||
this.client.on("event", this.onEvent);
|
this.client.on("Room.timeline", this.onRoomTimeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
public stop() {
|
public stop() {
|
||||||
this.client.removeListener("sync", this.evaluateEventBuffer);
|
this.client.removeListener("sync", this.evaluateEventBuffer);
|
||||||
this.client.removeListener("event", this.onEvent);
|
this.client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
private evaluateEventBuffer = async () => {
|
private evaluateEventBuffer = async () => {
|
||||||
@@ -89,7 +89,7 @@ export class CallEventHandler {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onEvent = (event: MatrixEvent) => {
|
private onRoomTimeline = (event: MatrixEvent) => {
|
||||||
this.client.decryptEventIfNeeded(event);
|
this.client.decryptEventIfNeeded(event);
|
||||||
// any call events or ones that might be once they're decrypted
|
// any call events or ones that might be once they're decrypted
|
||||||
if (this.eventIsACall(event) || event.isBeingDecrypted()) {
|
if (this.eventIsACall(event) || event.isBeingDecrypted()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user