1
0
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:
Brad Murray
2021-07-25 11:23:11 -04:00
56 changed files with 3887 additions and 2467 deletions

View File

@@ -1,3 +1,7 @@
<!-- Please read https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst before submitting your pull request --> <!-- Please read https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md before submitting your pull request -->
<!-- Include a Sign-Off as described in https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst#sign-off --> <!-- Include a Sign-Off as described in https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md#sign-off -->
<!-- To specify text for the changelog entry (otherwise the PR title will be used):
Notes:
-->

194
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,194 @@
Contributing code to matrix-js-sdk
==================================
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
willing to license their contributions under the same license as the project
itself. We follow a simple 'inbound=outbound' model for contributions: the act
of submitting an 'inbound' contribution means that the contributor agrees to
license the code under the same terms as the project's overall 'outbound'
license - in this case, Apache Software License v2 (see
[LICENSE](LICENSE)).
How to contribute
-----------------
The preferred and easiest way to contribute changes to the project is to fork
it on github, and then create a pull request to ask us to pull your changes
into our repo (https://help.github.com/articles/using-pull-requests/)
We use GitHub's pull request workflow to review the contribution, and either
ask you to make any refinements needed or merge it and make them ourselves.
Things that should go into your PR description:
* A changelog entry in the `Notes` section (see below)
* References to any bugs fixed by the change (in GitHub's `Fixes` notation)
* Notes for the reviewer that might help them to understand why the change is
necessary or how they might better review it.
Things that should *not* go into your PR description:
* Any information on how the code works or why you chose to do it the way
you did. If this isn't obvious from your code, you haven't written enough
comments.
We rely on information in pull request to populate the information that goes
into the changelogs our users see, both for the JS SDK itself and also for some
projects based on it. This is picked up from both labels on the pull request and
the `Notes:` annotation in the description. By default, the PR title will be
used for the changelog entry, but you can specify more options, as follows.
To add a longer, more detailed description of the change for the changelog:
*Fix llama herding bug*
```
Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase
```
For some PRs, it's not useful to have an entry in the user-facing changelog (this is
the default for PRs labelled with `T-Task`):
*Remove outdated comment from `Ungulates.ts`*
```
Notes: none
```
Sometimes, you're fixing a bug in a downstream project, in which case you want
an entry in that project's changelog. You can do that too:
*Fix another herding bug*
```
Notes: Fix a bug where the `herd()` function would only work on Tuesdays
element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays
```
This example is for Element Web. You can specify:
* matrix-react-sdk
* element-web
* element-desktop
If your PR introduces a breaking change, use the `Notes` section in the same
way, additionally adding the `X-Breaking-Change` label (see below). There's no need
to specify in the notes that it's a breaking change - this will be added
automatically based on the label - but remember to tell the developer how to
migrate:
*Remove legacy class*
```
Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead.
```
Other metadata can be added using labels.
* `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a *major* version bump.
* `T-Enhancement`: A new feature - adding this label will mean the change causes a *minor* version bump.
* `T-Defect`: A bug fix (in either code or docs).
* `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one.
If you don't have permission to add labels, your PR reviewer(s) can work with you
to add them: ask in the PR description or comments.
We use continuous integration, and all pull requests get automatically tested:
if your change breaks the build, then the PR will show that there are failed
checks, so please check back after a few minutes.
Code style
----------
The js-sdk aims to target TypeScript/ES6. All new files should be written in
TypeScript and existing files should use ES6 principles where possible.
Members should not be exported as a default export in general - it causes problems
with the architecture of the SDK (index file becomes less clear) and could
introduce naming problems (as default exports get aliased upon import). In
general, avoid using `export default`.
The remaining code-style for matrix-js-sdk is not formally documented, but
contributors are encouraged to read the
[code style document for matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md)
and follow the principles set out there.
Please ensure your changes match the cosmetic style of the existing project,
and ***never*** mix cosmetic and functional changes in the same commit, as it
makes it horribly hard to review otherwise.
Attribution
-----------
Everyone who contributes anything to Matrix is welcome to be listed in the
AUTHORS.rst file for the project in question. Please feel free to include a
change to AUTHORS.rst in your pull request to list yourself and a short
description of the area(s) you've worked on. Also, we sometimes have swag to
give away to contributors - if you feel that Matrix-branded apparel is missing
from your life, please mail us your shipping address to matrix at matrix.org
and we'll try to fix it :)
Sign off
--------
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've
adopted the same lightweight approach that the Linux Kernel
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO (Developer Certificate of Origin:
http://developercertificate.org/). This is a simple declaration that you wrote
the contribution or otherwise have the right to contribute it to Matrix:
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
If you agree to this for your contribution, then all that's needed is to
include the line in your commit or pull request comment:
```
Signed-off-by: Your Name <your@email.example.org>
```
We accept contributions under a legally identifiable name, such as your name on
government documentation or common-law names (names claimed by legitimate usage
or repute). Unfortunately, we cannot accept anonymous contributions at this
time.
Git allows you to add this signoff automatically when using the `-s` flag to
`git commit`, which uses the name and email set in your `user.name` and
`user.email` git configs.
If you forgot to sign off your commits before making your pull request and are
on Git 2.17+ you can mass signoff using rebase:
```
git rebase --signoff origin/develop
```

View File

@@ -1,131 +0,0 @@
Contributing code to matrix-js-sdk
==================================
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
willing to license their contributions under the same license as the project
itself. We follow a simple 'inbound=outbound' model for contributions: the act
of submitting an 'inbound' contribution means that the contributor agrees to
license the code under the same terms as the project's overall 'outbound'
license - in this case, Apache Software License v2 (see `<LICENSE>`_).
How to contribute
~~~~~~~~~~~~~~~~~
The preferred and easiest way to contribute changes to the project is to fork
it on github, and then create a pull request to ask us to pull your changes
into our repo (https://help.github.com/articles/using-pull-requests/)
**The single biggest thing you need to know is: please base your changes on
the develop branch - /not/ master.**
We use the master branch to track the most recent release, so that folks who
blindly clone the repo and automatically check out master get something that
works. Develop is the unstable branch where all the development actually
happens: the workflow is that contributors should fork the develop branch to
make a 'feature' branch for a particular contribution, and then make a pull
request to merge this back into the matrix.org 'official' develop branch. We
use GitHub's pull request workflow to review the contribution, and either ask
you to make any refinements needed or merge it and make them ourselves. The
changes will then land on master when we next do a release.
We use continuous integration, and all pull requests get automatically tested:
if your change breaks the build, then the PR will show that there are failed
checks, so please check back after a few minutes.
Code style
~~~~~~~~~~
The js-sdk aims to target TypeScript/ES6. All new files should be written in
TypeScript and existing files should use ES6 principles where possible.
Members should not be exported as a default export in general - it causes problems
with the architecture of the SDK (index file becomes less clear) and could
introduce naming problems (as default exports get aliased upon import). In
general, avoid using `export default`.
The remaining code-style for matrix-js-sdk is not formally documented, but
contributors are encouraged to read the code style document for matrix-react-sdk
(`<https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md>`_)
and follow the principles set out there.
Please ensure your changes match the cosmetic style of the existing project,
and **never** mix cosmetic and functional changes in the same commit, as it
makes it horribly hard to review otherwise.
Attribution
~~~~~~~~~~~
Everyone who contributes anything to Matrix is welcome to be listed in the
AUTHORS.rst file for the project in question. Please feel free to include a
change to AUTHORS.rst in your pull request to list yourself and a short
description of the area(s) you've worked on. Also, we sometimes have swag to
give away to contributors - if you feel that Matrix-branded apparel is missing
from your life, please mail us your shipping address to matrix at matrix.org
and we'll try to fix it :)
Sign off
~~~~~~~~
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've
adopted the same lightweight approach that the Linux Kernel
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO (Developer Certificate of Origin:
http://developercertificate.org/). This is a simple declaration that you wrote
the contribution or otherwise have the right to contribute it to Matrix::
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
If you agree to this for your contribution, then all that's needed is to
include the line in your commit or pull request comment::
Signed-off-by: Your Name <your@email.example.org>
We accept contributions under a legally identifiable name, such as your name on
government documentation or common-law names (names claimed by legitimate usage
or repute). Unfortunately, we cannot accept anonymous contributions at this
time.
Git allows you to add this signoff automatically when using the ``-s`` flag to
``git commit``, which uses the name and email set in your ``user.name`` and
``user.email`` git configs.
If you forgot to sign off your commits before making your pull request and are
on Git 2.17+ you can mass signoff using rebase::
git rebase --signoff origin/develop

View File

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

View File

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

View File

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

164
src/@types/PushRules.ts Normal file
View File

@@ -0,0 +1,164 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// allow camelcase as these are things that go onto the wire
/* eslint-disable camelcase */
export enum PushRuleActionName {
DontNotify = "dont_notify",
Notify = "notify",
Coalesce = "coalesce",
}
export enum TweakName {
Highlight = "highlight",
Sound = "sound",
}
export type Tweak<N extends TweakName, V> = {
set_tweak: N;
value: V;
};
export type TweakHighlight = Tweak<TweakName.Highlight, boolean>;
export type TweakSound = Tweak<TweakName.Sound, string>;
export type Tweaks = TweakHighlight | TweakSound;
export enum ConditionOperator {
ExactEquals = "==",
LessThan = "<",
GreaterThan = ">",
GreaterThanOrEqual = ">=",
LessThanOrEqual = "<=",
}
export type PushRuleAction = Tweaks | PushRuleActionName;
export type MemberCountCondition
<N extends number, Op extends ConditionOperator = ConditionOperator.ExactEquals>
= `${Op}${N}` | (Op extends ConditionOperator.ExactEquals ? `${N}` : never);
export type AnyMemberCountCondition = MemberCountCondition<number, ConditionOperator>;
export const DMMemberCountCondition: MemberCountCondition<2> = "2";
export function isDmMemberCountCondition(condition: AnyMemberCountCondition): boolean {
return condition === "==2" || condition === "2";
}
export enum ConditionKind {
EventMatch = "event_match",
ContainsDisplayName = "contains_display_name",
RoomMemberCount = "room_member_count",
SenderNotificationPermission = "sender_notification_permission",
}
export interface IPushRuleCondition<N extends ConditionKind | string> {
[k: string]: any; // for custom conditions, there can be other fields here
kind: N;
}
export interface IEventMatchCondition extends IPushRuleCondition<ConditionKind.EventMatch> {
key: string;
pattern: string;
}
export interface IContainsDisplayNameCondition extends IPushRuleCondition<ConditionKind.ContainsDisplayName> {
// no additional fields
}
export interface IRoomMemberCountCondition extends IPushRuleCondition<ConditionKind.RoomMemberCount> {
is: AnyMemberCountCondition;
}
export interface ISenderNotificationPermissionCondition
extends IPushRuleCondition<ConditionKind.SenderNotificationPermission> {
key: string;
}
export type PushRuleCondition = IPushRuleCondition<string>
| IEventMatchCondition
| IContainsDisplayNameCondition
| IRoomMemberCountCondition
| ISenderNotificationPermissionCondition;
export enum PushRuleKind {
Override = "override",
ContentSpecific = "content",
RoomSpecific = "room",
SenderSpecific = "sender",
Underride = "underride",
}
export enum RuleId {
Master = ".m.rule.master",
ContainsDisplayName = ".m.rule.contains_display_name",
ContainsUserName = ".m.rule.contains_user_name",
AtRoomNotification = ".m.rule.roomnotif",
DM = ".m.rule.room_one_to_one",
EncryptedDM = ".m.rule.encrypted_room_one_to_one",
Message = ".m.rule.message",
EncryptedMessage = ".m.rule.encrypted",
InviteToSelf = ".m.rule.invite_for_me",
MemberEvent = ".m.rule.member_event",
IncomingCall = ".m.rule.call",
SuppressNotices = ".m.rule.suppress_notices",
Tombstone = ".m.rule.tombstone",
}
export type PushRuleSet = {
[k in PushRuleKind]?: IPushRule[];
};
export interface IPushRule {
actions: PushRuleAction[];
conditions?: PushRuleCondition[];
default: boolean;
enabled: boolean;
pattern?: string;
rule_id: RuleId | string;
}
export interface IAnnotatedPushRule extends IPushRule {
kind: PushRuleKind;
}
export interface IPushRules {
global: PushRuleSet;
device?: PushRuleSet;
}
export interface IPusher {
app_display_name: string;
app_id: string;
data: {
format?: string; // TODO: Types
url?: string; // TODO: Required if kind==http
brand?: string; // TODO: For email notifications only?
};
device_display_name: string;
kind: string; // TODO: Types
lang: string;
profile_tag?: string;
pushkey: string;
}
export interface IPusherRequest extends IPusher {
append?: boolean;
}
/* eslint-enable camelcase */

View File

@@ -144,6 +144,28 @@ export const UNSTABLE_MSC3089_LEAF = new UnstableValue("m.leaf", "org.matrix.msc
*/ */
export const UNSTABLE_MSC3089_BRANCH = new UnstableValue("m.branch", "org.matrix.msc3089.branch"); export const UNSTABLE_MSC3089_BRANCH = new UnstableValue("m.branch", "org.matrix.msc3089.branch");
/**
* Functional members type for declaring a purpose of room members (e.g. helpful bots).
* Note that this reference is UNSTABLE and subject to breaking changes, including its
* eventual removal.
*
* Schema (TypeScript):
* {
* service_members?: string[]
* }
*
* Example:
* {
* "service_members": [
* "@helperbot:localhost",
* "@reminderbot:alice.tdl"
* ]
* }
*/
export const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new UnstableValue(
"io.element.functional_members",
"io.element.functional_members");
export interface IEncryptedFile { export interface IEncryptedFile {
url: string; url: string;
mimetype?: string; mimetype?: string;

View File

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

View File

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

View File

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

118
src/@types/search.ts Normal file
View File

@@ -0,0 +1,118 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Types relating to the /search API
import { IRoomEvent, IStateEvent } from "../sync-accumulator";
import { IRoomEventFilter } from "../filter";
import { SearchResult } from "../models/search-result";
/* eslint-disable camelcase */
export interface IEventWithRoomId extends IRoomEvent {
room_id: string;
}
export interface IStateEventWithRoomId extends IStateEvent {
room_id: string;
}
export interface IMatrixProfile {
avatar_url?: string;
displayname?: string;
}
export interface IResultContext {
events_before: IEventWithRoomId[];
events_after: IEventWithRoomId[];
profile_info: Record<string, IMatrixProfile>;
start?: string;
end?: string;
}
export interface ISearchResult {
rank: number;
result: IEventWithRoomId;
context: IResultContext;
}
enum GroupKey {
RoomId = "room_id",
Sender = "sender",
}
export interface IResultRoomEvents {
count: number;
highlights: string[];
results: ISearchResult[];
state?: { [roomId: string]: IStateEventWithRoomId[] };
groups?: {
[groupKey in GroupKey]: {
[value: string]: {
next_batch?: string;
order: number;
results: string[];
};
};
};
next_batch?: string;
}
interface IResultCategories {
room_events: IResultRoomEvents;
}
export type SearchKey = "content.body" | "content.name" | "content.topic";
export enum SearchOrderBy {
Recent = "recent",
Rank = "rank",
}
export interface ISearchRequestBody {
search_categories: {
room_events: {
search_term: string;
keys?: SearchKey[];
filter?: IRoomEventFilter;
order_by?: SearchOrderBy;
event_context?: {
before_limit?: number;
after_limit?: number;
include_profile?: boolean;
};
include_state?: boolean;
groupings?: {
group_by: {
key: GroupKey;
}[];
};
};
};
}
export interface ISearchResponse {
search_categories: IResultCategories;
}
export interface ISearchResults {
_query?: ISearchRequestBody;
results: SearchResult[];
highlights: string[];
count?: number;
next_batch?: string;
pendingRequest?: Promise<ISearchResults>;
}
/* eslint-enable camelcase */

40
src/@types/spaces.ts Normal file
View File

@@ -0,0 +1,40 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IPublicRoomsChunkRoom } from "../client";
// Types relating to Rooms of type `m.space` and related APIs
/* eslint-disable camelcase */
export interface ISpaceSummaryRoom extends IPublicRoomsChunkRoom {
num_refs: number;
room_type: string;
}
export interface ISpaceSummaryEvent {
room_id: string;
event_id: string;
origin_server_ts: number;
type: string;
state_key: string;
content: {
order?: string;
suggested?: boolean;
auto_join?: boolean;
via?: string[];
};
}
/* eslint-enable camelcase */

40
src/@types/synapse.ts Normal file
View File

@@ -0,0 +1,40 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { IdServerUnbindResult } from "./partials";
// Types relating to Synapse Admin APIs
/* eslint-disable camelcase */
export interface ISynapseAdminWhoisResponse {
user_id: string;
devices: {
[deviceId: string]: {
sessions: {
connections: {
ip: string;
last_seen: number; // millis since epoch
user_agent: string;
}[];
}[];
};
};
}
export interface ISynapseAdminDeactivateResponse {
id_server_unbind_result: IdServerUnbindResult;
}
/* eslint-enable camelcase */

28
src/@types/threepids.ts Normal file
View File

@@ -0,0 +1,28 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export enum ThreepidMedium {
Email = "email",
Phone = "msisdn",
}
// TODO: Are these types universal, or specific to just /account/3pid?
export interface IThreepid {
medium: ThreepidMedium;
address: string;
validated_at: number; // eslint-disable-line camelcase
added_at: number; // eslint-disable-line camelcase
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,3 +1,19 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from "../logger"; import { logger } from "../logger";
import { MatrixEvent } from "../models/event"; import { MatrixEvent } from "../models/event";
import { EventEmitter } from "events"; import { EventEmitter } from "events";
@@ -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 {};
}); });
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ export interface IDehydratedDeviceKeyInfo {
passphrase?: string; passphrase?: string;
} }
interface DeviceKeys { export interface IDeviceKeys {
algorithms: Array<string>; algorithms: Array<string>;
device_id: string; // eslint-disable-line camelcase device_id: string; // eslint-disable-line camelcase
user_id: string; // eslint-disable-line camelcase user_id: string; // eslint-disable-line camelcase
@@ -44,7 +44,7 @@ interface DeviceKeys {
signatures?: Signatures; signatures?: Signatures;
} }
export interface OneTimeKey { export interface IOneTimeKey {
key: string; key: string;
fallback?: boolean; fallback?: boolean;
signatures?: Signatures; signatures?: Signatures;
@@ -222,7 +222,7 @@ export class DehydrationManager {
// send the keys to the server // send the keys to the server
const deviceId = dehydrateResult.device_id; const deviceId = dehydrateResult.device_id;
logger.log("Preparing device keys", deviceId); logger.log("Preparing device keys", deviceId);
const deviceKeys: DeviceKeys = { const deviceKeys: IDeviceKeys = {
algorithms: this.crypto.supportedAlgorithms, algorithms: this.crypto.supportedAlgorithms,
device_id: deviceId, device_id: deviceId,
user_id: this.crypto.userId, user_id: this.crypto.userId,
@@ -244,7 +244,7 @@ export class DehydrationManager {
logger.log("Preparing one-time keys"); logger.log("Preparing one-time keys");
const oneTimeKeys = {}; const oneTimeKeys = {};
for (const [keyId, key] of Object.entries(otks.curve25519)) { for (const [keyId, key] of Object.entries(otks.curve25519)) {
const k: OneTimeKey = { key }; const k: IOneTimeKey = { key };
const signature = account.sign(anotherjson.stringify(k)); const signature = account.sign(anotherjson.stringify(k));
k.signatures = { k.signatures = {
[this.crypto.userId]: { [this.crypto.userId]: {
@@ -257,7 +257,7 @@ export class DehydrationManager {
logger.log("Preparing fallback keys"); logger.log("Preparing fallback keys");
const fallbackKeys = {}; const fallbackKeys = {};
for (const [keyId, key] of Object.entries(fallbacks.curve25519)) { for (const [keyId, key] of Object.entries(fallbacks.curve25519)) {
const k: OneTimeKey = { key, fallback: true }; const k: IOneTimeKey = { key, fallback: true };
const signature = account.sign(anotherjson.stringify(k)); const signature = account.sign(anotherjson.stringify(k));
k.signatures = { k.signatures = {
[this.crypto.userId]: { [this.crypto.userId]: {

View File

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

View File

@@ -24,6 +24,7 @@ export interface IKeyBackupSession {
ciphertext: string; ciphertext: string;
ephemeral: string; ephemeral: string;
mac: string; mac: string;
iv: string;
}; };
} }

View File

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

View File

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

View File

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

View File

@@ -121,7 +121,7 @@ MatrixHttpApi.prototype = {
}, },
/** /**
* Upload content to the Home Server * Upload content to the homeserver
* *
* @param {object} file The object to upload. On a browser, something that * @param {object} file The object to upload. On a browser, something that
* can be sent to XMLHttpRequest.send (typically a File). Under node.js, * can be sent to XMLHttpRequest.send (typically a File). Under node.js,
@@ -393,7 +393,7 @@ MatrixHttpApi.prototype = {
accessToken, accessToken,
) { ) {
if (!this.opts.idBaseUrl) { if (!this.opts.idBaseUrl) {
throw new Error("No Identity Server base URL set"); throw new Error("No identity server base URL set");
} }
const fullUri = this.opts.idBaseUrl + prefix + path; const fullUri = this.opts.idBaseUrl + prefix + path;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,36 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { ISavedSync } from "./index";
import { IEvent, IStartClientOpts, ISyncResponse } from "..";
export interface IIndexedDBBackend {
connect(): Promise<void>;
syncToDatabase(userTuples: UserTuple[]): Promise<void>;
isNewlyCreated(): Promise<boolean>;
setSyncData(syncData: ISyncResponse): Promise<void>;
getSavedSync(): Promise<ISavedSync>;
getNextBatchToken(): Promise<string>;
clearDatabase(): Promise<void>;
getOutOfBandMembers(roomId: string): Promise<IEvent[] | null>;
setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void>;
clearOutOfBandMembers(roomId: string): Promise<void>;
getUserPresenceEvents(): Promise<UserTuple[]>;
getClientOptions(): Promise<IStartClientOpts>;
storeClientOptions(options: IStartClientOpts): Promise<void>;
}
export type UserTuple = [userId: string, presenceEvent: Partial<IEvent>];

View File

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

View File

@@ -1,196 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from '../logger';
import { defer } from '../utils';
/**
* An IndexedDB store backend where the actual backend sits in a web
* worker.
*
* Construct a new Indexed Database store backend. This requires a call to
* <code>connect()</code> before this store can be used.
* @constructor
* @param {string} workerScript URL to the worker script
* @param {string=} dbName Optional database name. The same name must be used
* to open the same database.
* @param {Object} workerApi The web worker compatible interface object
*/
export function RemoteIndexedDBStoreBackend(
workerScript, dbName, workerApi,
) {
this._workerScript = workerScript;
this._dbName = dbName;
this._workerApi = workerApi;
this._worker = null;
this._nextSeq = 0;
// The currently in-flight requests to the actual backend
this._inFlight = {
// seq: promise,
};
// Once we start connecting, we keep the promise and re-use it
// if we try to connect again
this._startPromise = null;
}
RemoteIndexedDBStoreBackend.prototype = {
/**
* Attempt to connect to the database. This can fail if the user does not
* grant permission.
* @return {Promise} Resolves if successfully connected.
*/
connect: function() {
return this._ensureStarted().then(() => this._doCmd('connect'));
},
/**
* Clear the entire database. This should be used when logging out of a client
* to prevent mixing data between accounts.
* @return {Promise} Resolved when the database is cleared.
*/
clearDatabase: function() {
return this._ensureStarted().then(() => this._doCmd('clearDatabase'));
},
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
isNewlyCreated: function() {
return this._doCmd('isNewlyCreated');
},
/**
* @return {Promise} Resolves with a sync response to restore the
* client state to where it was at the last save, or null if there
* is no saved sync data.
*/
getSavedSync: function() {
return this._doCmd('getSavedSync');
},
getNextBatchToken: function() {
return this._doCmd('getNextBatchToken');
},
setSyncData: function(syncData) {
return this._doCmd('setSyncData', [syncData]);
},
syncToDatabase: function(users) {
return this._doCmd('syncToDatabase', [users]);
},
/**
* Returns the out-of-band membership events for this room that
* were previously loaded.
* @param {string} roomId
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
getOutOfBandMembers: function(roomId) {
return this._doCmd('getOutOfBandMembers', [roomId]);
},
/**
* Stores the out-of-band membership events for this room. Note that
* it still makes sense to store an empty array as the OOB status for the room is
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
* @param {string} roomId
* @param {event[]} membershipEvents the membership events to store
* @returns {Promise} when all members have been stored
*/
setOutOfBandMembers: function(roomId, membershipEvents) {
return this._doCmd('setOutOfBandMembers', [roomId, membershipEvents]);
},
clearOutOfBandMembers: function(roomId) {
return this._doCmd('clearOutOfBandMembers', [roomId]);
},
getClientOptions: function() {
return this._doCmd('getClientOptions');
},
storeClientOptions: function(options) {
return this._doCmd('storeClientOptions', [options]);
},
/**
* Load all user presence events from the database. This is not cached.
* @return {Promise<Object[]>} A list of presence events in their raw form.
*/
getUserPresenceEvents: function() {
return this._doCmd('getUserPresenceEvents');
},
_ensureStarted: function() {
if (this._startPromise === null) {
this._worker = new this._workerApi(this._workerScript);
this._worker.onmessage = this._onWorkerMessage.bind(this);
// tell the worker the db name.
this._startPromise = this._doCmd('_setupWorker', [this._dbName]).then(() => {
logger.log("IndexedDB worker is ready");
});
}
return this._startPromise;
},
_doCmd: function(cmd, args) {
// wrap in a q so if the postMessage throws,
// the promise automatically gets rejected
return Promise.resolve().then(() => {
const seq = this._nextSeq++;
const def = defer();
this._inFlight[seq] = def;
this._worker.postMessage({
command: cmd,
seq: seq,
args: args,
});
return def.promise;
});
},
_onWorkerMessage: function(ev) {
const msg = ev.data;
if (msg.command == 'cmd_success' || msg.command == 'cmd_fail') {
if (msg.seq === undefined) {
logger.error("Got reply from worker with no seq");
return;
}
const def = this._inFlight[msg.seq];
if (def === undefined) {
logger.error("Got reply for unknown seq " + msg.seq);
return;
}
delete this._inFlight[msg.seq];
if (msg.command == 'cmd_success') {
def.resolve(msg.result);
} else {
const error = new Error(msg.error.message);
error.name = msg.error.name;
def.reject(error);
}
} else {
logger.warn("Unrecognised message from worker: " + msg);
}
},
};

View File

@@ -0,0 +1,192 @@
/*
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from "../logger";
import { defer, IDeferred } from "../utils";
import { ISavedSync } from "./index";
import { IStartClientOpts } from "../client";
import { IEvent, ISyncResponse } from "..";
import { IIndexedDBBackend, UserTuple } from "./indexeddb-backend";
export class RemoteIndexedDBStoreBackend implements IIndexedDBBackend {
private worker: Worker;
private nextSeq = 0;
// The currently in-flight requests to the actual backend
private inFlight: Record<number, IDeferred<any>> = {}; // seq: promise
// Once we start connecting, we keep the promise and re-use it
// if we try to connect again
private startPromise: Promise<void> = null;
/**
* An IndexedDB store backend where the actual backend sits in a web
* worker.
*
* Construct a new Indexed Database store backend. This requires a call to
* <code>connect()</code> before this store can be used.
* @constructor
* @param {Function} workerFactory Factory which produces a Worker
* @param {string=} dbName Optional database name. The same name must be used
* to open the same database.
*/
constructor(
private readonly workerFactory: () => Worker,
private readonly dbName: string,
) {}
/**
* Attempt to connect to the database. This can fail if the user does not
* grant permission.
* @return {Promise} Resolves if successfully connected.
*/
public connect(): Promise<void> {
return this.ensureStarted().then(() => this.doCmd('connect'));
}
/**
* Clear the entire database. This should be used when logging out of a client
* to prevent mixing data between accounts.
* @return {Promise} Resolved when the database is cleared.
*/
public clearDatabase(): Promise<void> {
return this.ensureStarted().then(() => this.doCmd('clearDatabase'));
}
/** @return {Promise<boolean>} whether or not the database was newly created in this session. */
public isNewlyCreated(): Promise<boolean> {
return this.doCmd('isNewlyCreated');
}
/**
* @return {Promise} Resolves with a sync response to restore the
* client state to where it was at the last save, or null if there
* is no saved sync data.
*/
public getSavedSync(): Promise<ISavedSync> {
return this.doCmd('getSavedSync');
}
public getNextBatchToken(): Promise<string> {
return this.doCmd('getNextBatchToken');
}
public setSyncData(syncData: ISyncResponse): Promise<void> {
return this.doCmd('setSyncData', [syncData]);
}
public syncToDatabase(userTuples: UserTuple[]): Promise<void> {
return this.doCmd('syncToDatabase', [userTuples]);
}
/**
* Returns the out-of-band membership events for this room that
* were previously loaded.
* @param {string} roomId
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
public getOutOfBandMembers(roomId: string): Promise<IEvent[] | null> {
return this.doCmd('getOutOfBandMembers', [roomId]);
}
/**
* Stores the out-of-band membership events for this room. Note that
* it still makes sense to store an empty array as the OOB status for the room is
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
* @param {string} roomId
* @param {event[]} membershipEvents the membership events to store
* @returns {Promise} when all members have been stored
*/
public setOutOfBandMembers(roomId: string, membershipEvents: IEvent[]): Promise<void> {
return this.doCmd('setOutOfBandMembers', [roomId, membershipEvents]);
}
public clearOutOfBandMembers(roomId: string): Promise<void> {
return this.doCmd('clearOutOfBandMembers', [roomId]);
}
public getClientOptions(): Promise<IStartClientOpts> {
return this.doCmd('getClientOptions');
}
public storeClientOptions(options: IStartClientOpts): Promise<void> {
return this.doCmd('storeClientOptions', [options]);
}
/**
* Load all user presence events from the database. This is not cached.
* @return {Promise<Object[]>} A list of presence events in their raw form.
*/
public getUserPresenceEvents(): Promise<UserTuple[]> {
return this.doCmd('getUserPresenceEvents');
}
private ensureStarted(): Promise<void> {
if (this.startPromise === null) {
this.worker = this.workerFactory();
this.worker.onmessage = this.onWorkerMessage;
// tell the worker the db name.
this.startPromise = this.doCmd('_setupWorker', [this.dbName]).then(() => {
logger.log("IndexedDB worker is ready");
});
}
return this.startPromise;
}
private doCmd<T>(command: string, args?: any): Promise<T> {
// wrap in a q so if the postMessage throws,
// the promise automatically gets rejected
return Promise.resolve().then(() => {
const seq = this.nextSeq++;
const def = defer<T>();
this.inFlight[seq] = def;
this.worker.postMessage({ command, seq, args });
return def.promise;
});
}
private onWorkerMessage = (ev: MessageEvent): void => {
const msg = ev.data;
if (msg.command == 'cmd_success' || msg.command == 'cmd_fail') {
if (msg.seq === undefined) {
logger.error("Got reply from worker with no seq");
return;
}
const def = this.inFlight[msg.seq];
if (def === undefined) {
logger.error("Got reply for unknown seq " + msg.seq);
return;
}
delete this.inFlight[msg.seq];
if (msg.command == 'cmd_success') {
def.resolve(msg.result);
} else {
const error = new Error(msg.error.message);
error.name = msg.error.name;
def.reject(error);
}
} else {
logger.warn("Unrecognised message from worker: ", msg);
}
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2865
yarn.lock

File diff suppressed because it is too large Load Diff