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",
"gendoc": "jsdoc -c jsdoc.json -P package.json",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 57 src spec",
"lint:js": "eslint --max-warnings 7 src spec",
"lint:js-fix": "eslint --fix src spec",
"lint:types": "tsc --noEmit",
"test": "jest",
"test:watch": "jest --watch",
@@ -28,7 +29,7 @@
"keywords": [
"matrix-org"
],
"main": "./lib/index.js",
"main": "./src/index.ts",
"browser": "./lib/browser-index.js",
"matrix_src_main": "./src/index.ts",
"matrix_src_browser": "./src/browser-index.js",
@@ -110,6 +111,5 @@
"coverageReporters": [
"text"
]
},
"typings": "./lib/index.d.ts"
}
}

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,3 +1,19 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from "../logger";
import { MatrixEvent } from "../models/event";
import { EventEmitter } from "events";
@@ -109,8 +125,8 @@ export class EncryptionSetupBuilder {
* @param {Object} content
* @return {Promise}
*/
public setAccountData(type: string, content: object): Promise<void> {
return this.accountDataClientAdapter.setAccountData(type, content);
public async setAccountData(type: string, content: object): Promise<void> {
await this.accountDataClientAdapter.setAccountData(type, content);
}
/**
@@ -284,7 +300,7 @@ class AccountDataClientAdapter extends EventEmitter {
* @param {Object} content
* @return {Promise}
*/
public setAccountData(type: string, content: any): Promise<void> {
public setAccountData(type: string, content: any): Promise<{}> {
const lastEvent = this.values.get(type);
this.values.set(type, content);
// ensure accountData is emitted on the next tick,
@@ -293,6 +309,7 @@ class AccountDataClientAdapter extends EventEmitter {
return Promise.resolve().then(() => {
const event = new MatrixEvent({ type, content });
this.emit("accountData", event, lastEvent);
return {};
});
}
}

View File

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

View File

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

View File

@@ -37,9 +37,9 @@ export interface ISecretRequest {
export interface IAccountDataClient extends EventEmitter {
// Subset of MatrixClient (which also uses any for the event content)
getAccountDataFromServer: (eventType: string) => Promise<any>;
getAccountDataFromServer: (eventType: string) => Promise<Record<string, any>>;
getAccountData: (eventType: string) => MatrixEvent;
setAccountData: (eventType: string, content: any) => Promise<void>;
setAccountData: (eventType: string, content: any) => Promise<{}>;
}
interface ISecretRequestInternal {
@@ -174,7 +174,7 @@ export class SecretStorage {
* the form [keyId, keyInfo]. Otherwise, null is returned.
* XXX: why is this an array when addKey returns an object?
*/
public async getKey(keyId: string): Promise<SecretStorageKeyTuple> {
public async getKey(keyId: string): Promise<SecretStorageKeyTuple | null> {
if (!keyId) {
keyId = await this.getDefaultKeyId();
}
@@ -184,7 +184,7 @@ export class SecretStorage {
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
) as ISecretStorageKeyInfo;
return keyInfo ? [keyId, keyInfo] : null;
}
@@ -248,7 +248,7 @@ export class SecretStorage {
// get key information from key storage
const keyInfo = await this.accountDataAdapter.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
) as ISecretStorageKeyInfo;
if (!keyInfo) {
throw new Error("Unknown key: " + keyId);
}

View File

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

View File

@@ -31,7 +31,7 @@ import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { encodeRecoveryKey } from './recoverykey';
import { encryptAES, decryptAES, calculateKeyCheck } from './aes';
import { getCrypto } from '../utils';
import { ICurve25519AuthData, IAes256AuthData, IKeyBackupInfo } from "./keybackup";
import { ICurve25519AuthData, IAes256AuthData, IKeyBackupInfo, IKeyBackupSession } from "./keybackup";
import { UnstableValue } from "../NamespacedValue";
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
@@ -85,12 +85,22 @@ interface BackupAlgorithmClass {
interface BackupAlgorithm {
untrusted: boolean;
encryptSession(data: Record<string, any>): Promise<any>;
decryptSessions(ciphertexts: Record<string, any>): Promise<Record<string, any>[]>;
decryptSessions(ciphertexts: Record<string, IKeyBackupSession>): Promise<Record<string, any>[]>;
authData: AuthData;
keyMatches(key: ArrayLike<number>): Promise<boolean>;
free(): void;
}
export interface IKeyBackup {
rooms: {
[roomId: string]: {
sessions: {
[sessionId: string]: IKeyBackupSession;
};
};
};
}
/**
* Manages the key backup.
*/
@@ -464,11 +474,11 @@ export class BackupManager {
let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
const data = {};
const rooms: IKeyBackup["rooms"] = {};
for (const session of sessions) {
const roomId = session.sessionData.room_id;
if (data[roomId] === undefined) {
data[roomId] = { sessions: {} };
if (rooms[roomId] === undefined) {
rooms[roomId] = { sessions: {} };
}
const sessionData = await this.baseApis.crypto.olmDevice.exportInboundGroupSession(
@@ -487,7 +497,7 @@ export class BackupManager {
);
const verified = this.baseApis.crypto.checkDeviceInfoTrust(userId, device).isVerified();
data[roomId]['sessions'][session.sessionId] = {
rooms[roomId]['sessions'][session.sessionId] = {
first_message_index: sessionData.first_known_index,
forwarded_count: forwardedCount,
is_verified: verified,
@@ -495,10 +505,7 @@ export class BackupManager {
};
}
await this.baseApis.sendKeyBackup(
undefined, undefined, this.backupInfo.version,
{ rooms: data },
);
await this.baseApis.sendKeyBackup(undefined, undefined, this.backupInfo.version, { rooms });
await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions);
remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
@@ -636,7 +643,9 @@ export class Curve25519 implements BackupAlgorithm {
return this.publicKey.encrypt(JSON.stringify(plainText));
}
public async decryptSessions(sessions: Record<string, Record<string, any>>): Promise<Record<string, any>[]> {
public async decryptSessions(
sessions: Record<string, IKeyBackupSession>,
): Promise<Record<string, any>[]> {
const privKey = await this.getKey();
const decryption = new global.Olm.PkDecryption();
try {
@@ -766,14 +775,12 @@ export class Aes256 implements BackupAlgorithm {
return await encryptAES(JSON.stringify(plainText), this.key, data.session_id);
}
async decryptSessions(sessions: Record<string, any>): Promise<Record<string, any>[]> {
async decryptSessions(sessions: Record<string, IKeyBackupSession>): Promise<Record<string, any>[]> {
const keys = [];
for (const [sessionId, sessionData] of Object.entries(sessions)) {
try {
const decrypted = JSON.parse(await decryptAES(
sessionData.session_data, this.key, sessionId,
));
const decrypted = JSON.parse(await decryptAES(sessionData.session_data, this.key, sessionId));
decrypted.session_id = sessionId;
keys.push(decrypted);
} catch (e) {

View File

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

View File

@@ -44,7 +44,7 @@ import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from "./api";
import { OutgoingRoomKeyRequestManager } from './OutgoingRoomKeyRequestManager';
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
import { ReciprocateQRCode, SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD } from './verification/QRCode';
import { SAS } from './verification/SAS';
import { SAS as SASVerification } from './verification/SAS';
import { keyFromPassphrase } from './key_passphrase';
import { decodeRecoveryKey, encodeRecoveryKey } from './recoverykey';
import { VerificationRequest } from "./verification/request/VerificationRequest";
@@ -53,7 +53,7 @@ import { ToDeviceChannel, ToDeviceRequests } from "./verification/request/ToDevi
import { IllegalMethod } from "./verification/IllegalMethod";
import { KeySignatureUploadError } from "../errors";
import { decryptAES, encryptAES, calculateKeyCheck } from './aes';
import { DehydrationManager } from './dehydration';
import { DehydrationManager, IDeviceKeys, IOneTimeKey } from './dehydration';
import { BackupManager } from "./backup";
import { IStore } from "../store";
import { Room } from "../models/room";
@@ -61,7 +61,7 @@ import { RoomMember } from "../models/room-member";
import { MatrixEvent } from "../models/event";
import { MatrixClient, IKeysUploadResponse, SessionStore, CryptoStore, ISignedKey } from "../client";
import type { EncryptionAlgorithm, DecryptionAlgorithm } from "./algorithms/base";
import type { RoomList } from "./RoomList";
import type { IRoomEncryption, RoomList } from "./RoomList";
import { IRecoveryKey, IEncryptedEventInfo } from "./api";
import { IKeyBackupInfo } from "./keybackup";
import { ISyncStateData } from "../sync";
@@ -70,7 +70,7 @@ const DeviceVerification = DeviceInfo.DeviceVerification;
const defaultVerificationMethods = {
[ReciprocateQRCode.NAME]: ReciprocateQRCode,
[SAS.NAME]: SAS,
[SASVerification.NAME]: SASVerification,
// These two can't be used for actual verification, but we do
// need to be able to define them here for the verification flows
@@ -82,10 +82,13 @@ const defaultVerificationMethods = {
/**
* verification method names
*/
export const verificationMethods = {
RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME,
SAS: SAS.NAME,
};
// legacy export identifier
export enum verificationMethods {
RECIPROCATE_QR_CODE = ReciprocateQRCode.NAME,
SAS = SASVerification.NAME,
}
export type VerificationMethod = verificationMethods;
export function isCryptoAvailable(): boolean {
return Boolean(global.Olm);
@@ -142,6 +145,10 @@ interface IDeviceVerificationUpgrade {
crossSigningInfo: CrossSigningInfo;
}
export interface ICheckOwnCrossSigningTrustOpts {
allowPrivateKeyRequests?: boolean;
}
/**
* @typedef {Object} module:crypto~OlmSessionResult
* @property {module:crypto/deviceinfo} device device info
@@ -1418,7 +1425,7 @@ export class Crypto extends EventEmitter {
*/
async checkOwnCrossSigningTrust({
allowPrivateKeyRequests = false,
} = {}) {
}: ICheckOwnCrossSigningTrustOpts = {}): Promise<void> {
const userId = this.userId;
// Before proceeding, ensure our cross-signing public keys have been
@@ -1772,7 +1779,7 @@ export class Crypto extends EventEmitter {
return this.signObject(deviceKeys).then(() => {
return this.baseApis.uploadKeysRequest({
device_keys: deviceKeys,
device_keys: deviceKeys as Required<IDeviceKeys>,
});
});
}
@@ -1904,9 +1911,9 @@ export class Crypto extends EventEmitter {
private async uploadOneTimeKeys() {
const promises = [];
const fallbackJson = {};
const fallbackJson: Record<string, IOneTimeKey> = {};
if (this.getNeedsNewFallback()) {
const fallbackKeys = await this.olmDevice.getFallbackKey();
const fallbackKeys = await this.olmDevice.getFallbackKey() as Record<string, Record<string, string>>;
for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) {
const k = { key, fallback: true };
fallbackJson["signed_curve25519:" + keyId] = k;
@@ -2252,7 +2259,7 @@ export class Crypto extends EventEmitter {
public async legacyDeviceVerification(
userId: string,
deviceId: string,
method: string,
method: VerificationMethod,
): VerificationRequest {
const transactionId = ToDeviceChannel.makeTransactionId();
const channel = new ToDeviceChannel(
@@ -2465,7 +2472,7 @@ export class Crypto extends EventEmitter {
*/
public async setRoomEncryption(
roomId: string,
config: any, // TODO types
config: IRoomEncryption,
inhibitDeviceQuery?: boolean,
): Promise<void> {
// ignore crypto events with no algorithm defined
@@ -2522,8 +2529,8 @@ export class Crypto extends EventEmitter {
crypto: this,
olmDevice: this.olmDevice,
baseApis: this.baseApis,
roomId: roomId,
config: config,
roomId,
config,
});
this.roomEncryptors[roomId] = alg;
@@ -2878,7 +2885,7 @@ export class Crypto extends EventEmitter {
*/
public async onCryptoEvent(event: MatrixEvent): Promise<void> {
const roomId = event.getRoomId();
const content = event.getContent();
const content = event.getContent<IRoomEncryption>();
try {
// inhibit the device list refresh for now - it will happen once we've

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,6 @@ limitations under the License.
/** @module interactive-auth */
import url from "url";
import * as utils from "./utils";
import { logger } from './logger';
@@ -187,9 +186,7 @@ InteractiveAuth.prototype = {
client_secret: this._clientSecret,
};
if (await this._matrixClient.doesServerRequireIdServerParam()) {
const idServerParsedUrl = url.parse(
this._matrixClient.getIdentityServerUrl(),
);
const idServerParsedUrl = new URL(this._matrixClient.getIdentityServerUrl());
creds.id_server = idServerParsedUrl.host;
}
authDict = {
@@ -217,7 +214,7 @@ InteractiveAuth.prototype = {
/**
* get the client secret used for validation sessions
* with the ID server.
* with the identity server.
*
* @return {string} client secret
*/

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.
* @fires module:client~MatrixClient#event:"User.unstable_statusMessage"
*/
// eslint-disable-next-line camelcase
// eslint-disable-next-line
public unstable_updateStatusMessage(event: MatrixEvent): void {
if (!event.getContent()) this.unstable_statusMessage = "";
else this.unstable_statusMessage = event.getContent()["status"];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2865
yarn.lock

File diff suppressed because it is too large Load Diff